diff --git a/packages/backend/src/apps/jotform/assets/favicon.svg b/packages/backend/src/apps/jotform/assets/favicon.svg
new file mode 100644
index 00000000..99500044
--- /dev/null
+++ b/packages/backend/src/apps/jotform/assets/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/backend/src/apps/jotform/auth/index.js b/packages/backend/src/apps/jotform/auth/index.js
new file mode 100644
index 00000000..9d5cf656
--- /dev/null
+++ b/packages/backend/src/apps/jotform/auth/index.js
@@ -0,0 +1,30 @@
+import verifyCredentials from './verify-credentials.js';
+import isStillVerified from './is-still-verified.js';
+
+export default {
+ fields: [
+ {
+ key: 'apiUrl',
+ label: 'API URL',
+ type: 'string',
+ required: false,
+ readOnly: false,
+ value: 'https://api.jotform.com',
+ placeholder: 'https://${subdomain}.jotform.com/api',
+ clickToCopy: true,
+ },
+ {
+ key: 'apiKey',
+ label: 'API Key',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ clickToCopy: false,
+ },
+ ],
+
+ verifyCredentials,
+ isStillVerified,
+};
diff --git a/packages/backend/src/apps/jotform/auth/is-still-verified.js b/packages/backend/src/apps/jotform/auth/is-still-verified.js
new file mode 100644
index 00000000..39867547
--- /dev/null
+++ b/packages/backend/src/apps/jotform/auth/is-still-verified.js
@@ -0,0 +1,8 @@
+import getCurrentUser from '../common/get-current-user.js';
+
+const isStillVerified = async ($) => {
+ const user = await getCurrentUser($);
+ return !!user.username;
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/jotform/auth/verify-credentials.js b/packages/backend/src/apps/jotform/auth/verify-credentials.js
new file mode 100644
index 00000000..0acb407d
--- /dev/null
+++ b/packages/backend/src/apps/jotform/auth/verify-credentials.js
@@ -0,0 +1,12 @@
+import getCurrentUser from '../common/get-current-user.js';
+
+const verifyCredentials = async ($) => {
+ const user = await getCurrentUser($);
+
+ await $.auth.set({
+ screenName: user.name,
+ apiKey: $.auth.data.apiKey,
+ });
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/jotform/common/add-auth-header.js b/packages/backend/src/apps/jotform/common/add-auth-header.js
new file mode 100644
index 00000000..4d6296fb
--- /dev/null
+++ b/packages/backend/src/apps/jotform/common/add-auth-header.js
@@ -0,0 +1,9 @@
+const addAuthHeader = ($, requestConfig) => {
+ if ($.auth.data?.apiKey) {
+ requestConfig.headers['APIKEY'] = `${$.auth.data.apiKey}`;
+ }
+
+ return requestConfig;
+};
+
+export default addAuthHeader;
diff --git a/packages/backend/src/apps/jotform/common/get-current-user.js b/packages/backend/src/apps/jotform/common/get-current-user.js
new file mode 100644
index 00000000..e0cf0186
--- /dev/null
+++ b/packages/backend/src/apps/jotform/common/get-current-user.js
@@ -0,0 +1,7 @@
+const getCurrentUser = async ($) => {
+ const response = await $.http.get('/user');
+ const currentUser = response.data.content;
+ return currentUser;
+};
+
+export default getCurrentUser;
diff --git a/packages/backend/src/apps/jotform/common/set-base-url.js b/packages/backend/src/apps/jotform/common/set-base-url.js
new file mode 100644
index 00000000..d2f6851d
--- /dev/null
+++ b/packages/backend/src/apps/jotform/common/set-base-url.js
@@ -0,0 +1,11 @@
+const setBaseUrl = ($, requestConfig) => {
+ if ($.auth.data.apiUrl) {
+ requestConfig.baseURL = $.auth.data.apiUrl;
+ } else if ($.app.apiBaseUrl) {
+ requestConfig.baseURL = $.app.apiBaseUrl;
+ }
+
+ return requestConfig;
+};
+
+export default setBaseUrl;
diff --git a/packages/backend/src/apps/jotform/dynamic-data/index.js b/packages/backend/src/apps/jotform/dynamic-data/index.js
new file mode 100644
index 00000000..0a58430e
--- /dev/null
+++ b/packages/backend/src/apps/jotform/dynamic-data/index.js
@@ -0,0 +1,3 @@
+import listForms from './list-forms/index.js';
+
+export default [listForms];
diff --git a/packages/backend/src/apps/jotform/dynamic-data/list-forms/index.js b/packages/backend/src/apps/jotform/dynamic-data/list-forms/index.js
new file mode 100644
index 00000000..c96f34e6
--- /dev/null
+++ b/packages/backend/src/apps/jotform/dynamic-data/list-forms/index.js
@@ -0,0 +1,41 @@
+export default {
+ name: 'List forms',
+ key: 'listForms',
+
+ async run($) {
+ const forms = {
+ data: [],
+ };
+ let hasMore = false;
+
+ const params = {
+ limit: 1000,
+ offset: 0,
+ orderby: 'created_at',
+ };
+
+ do {
+ const { data } = await $.http.get('/user/forms', { params });
+ params.offset = params.offset + params.limit;
+
+ if (data.content?.length) {
+ for (const form of data.content) {
+ if (form.status === 'ENABLED') {
+ forms.data.push({
+ value: form.id,
+ name: form.title,
+ });
+ }
+ }
+ }
+
+ if (data.resultSet.count >= data.resultSet.limit) {
+ hasMore = true;
+ } else {
+ hasMore = false;
+ }
+ } while (hasMore);
+
+ return forms;
+ },
+};
diff --git a/packages/backend/src/apps/jotform/index.js b/packages/backend/src/apps/jotform/index.js
new file mode 100644
index 00000000..b4242bdc
--- /dev/null
+++ b/packages/backend/src/apps/jotform/index.js
@@ -0,0 +1,21 @@
+import defineApp from '../../helpers/define-app.js';
+import addAuthHeader from './common/add-auth-header.js';
+import auth from './auth/index.js';
+import setBaseUrl from './common/set-base-url.js';
+import triggers from './triggers/index.js';
+import dynamicData from './dynamic-data/index.js';
+
+export default defineApp({
+ name: 'Jotform',
+ key: 'jotform',
+ iconUrl: '{BASE_URL}/apps/jotform/assets/favicon.svg',
+ authDocUrl: 'https://automatisch.io/docs/apps/jotform/connection',
+ supportsConnections: true,
+ baseUrl: 'https://www.jotform.com',
+ apiBaseUrl: 'https://api.jotform.com',
+ primaryColor: 'FF6100',
+ beforeRequest: [setBaseUrl, addAuthHeader],
+ auth,
+ triggers,
+ dynamicData,
+});
diff --git a/packages/backend/src/apps/jotform/triggers/index.js b/packages/backend/src/apps/jotform/triggers/index.js
new file mode 100644
index 00000000..b25ac2ce
--- /dev/null
+++ b/packages/backend/src/apps/jotform/triggers/index.js
@@ -0,0 +1,3 @@
+import newSubmissions from './new-submissions/index.js';
+
+export default [newSubmissions];
diff --git a/packages/backend/src/apps/jotform/triggers/new-submissions/index.js b/packages/backend/src/apps/jotform/triggers/new-submissions/index.js
new file mode 100644
index 00000000..e5c99460
--- /dev/null
+++ b/packages/backend/src/apps/jotform/triggers/new-submissions/index.js
@@ -0,0 +1,109 @@
+import Crypto from 'crypto';
+import { URLSearchParams } from 'url';
+import defineTrigger from '../../../../helpers/define-trigger.js';
+
+export default defineTrigger({
+ name: 'New submissions',
+ key: 'newSubmissions',
+ type: 'webhook',
+ description:
+ 'Triggers when a new submission has been added to a specific form.',
+ arguments: [
+ {
+ label: 'Form',
+ key: 'formId',
+ type: 'dropdown',
+ required: true,
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listForms',
+ },
+ ],
+ },
+ },
+ ],
+
+ async run($) {
+ const dataItem = {
+ raw: $.request.body,
+ meta: {
+ internalId: Crypto.randomUUID(),
+ },
+ };
+
+ $.pushTriggerItem(dataItem);
+ },
+
+ async testRun($) {
+ const sampleEventData = {
+ ip: '127.0.0.1',
+ type: 'WEB',
+ appID: '',
+ event: '',
+ action: '',
+ formID: Crypto.randomUUID(),
+ parent: '',
+ pretty: 'Name:test, E-mail:user@automatisch.io',
+ teamID: '',
+ unread: '',
+ product: '',
+ subject: '',
+ isSilent: '',
+ username: 'username',
+ deviceIDs: 'Array',
+ formTitle: 'Opt-In Form-Get Free Email Updates!',
+ fromTable: '',
+ customBody: '',
+ documentID: '',
+ rawRequest: '',
+ webhookURL: '',
+ customTitle: '',
+ trackAction: 'Array',
+ customParams: '',
+ submissionID: Crypto.randomUUID(),
+ customBodyParams: 'Array',
+ customTitleParams: 'Array',
+ };
+
+ const dataItem = {
+ raw: sampleEventData,
+ meta: {
+ internalId: sampleEventData.submissionID,
+ },
+ };
+
+ $.pushTriggerItem(dataItem);
+ },
+
+ async registerHook($) {
+ const formId = $.step.parameters.formId;
+
+ const params = new URLSearchParams({
+ webhookURL: $.webhookUrl,
+ });
+
+ const { data } = await $.http.post(
+ `/form/${formId}/webhooks`,
+ params.toString()
+ );
+
+ await $.flow.setRemoteWebhookId(data.content[0]);
+ },
+
+ async unregisterHook($) {
+ const formId = $.step.parameters.formId;
+
+ const { data } = await $.http.get(`/form/${formId}/webhooks`);
+
+ const webhookURLs = Object.values(data.content);
+ const webhookId = webhookURLs.findIndex((url) => url === $.webhookUrl);
+
+ await $.http.delete(`/form/${formId}/webhooks/${webhookId}`);
+ },
+});
diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js
index 0912d90b..ad681517 100644
--- a/packages/docs/pages/.vitepress/config.js
+++ b/packages/docs/pages/.vitepress/config.js
@@ -252,6 +252,15 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/invoice-ninja/connection' },
],
},
+ {
+ text: 'Jotform',
+ collapsible: true,
+ collapsed: true,
+ items: [
+ { text: 'Triggers', link: '/apps/jotform/triggers' },
+ { text: 'Connection', link: '/apps/jotform/connection' },
+ ],
+ },
{
text: 'Mailchimp',
collapsible: true,
diff --git a/packages/docs/pages/apps/jotform/connection.md b/packages/docs/pages/apps/jotform/connection.md
new file mode 100644
index 00000000..1c1179e7
--- /dev/null
+++ b/packages/docs/pages/apps/jotform/connection.md
@@ -0,0 +1,15 @@
+# Jotform
+
+:::info
+This page explains the steps you need to follow to set up the Jotform
+connection in Automatisch. If any of the steps are outdated, please let us know!
+:::
+
+1. Login to your Jotform account: [https://www.jotform.com/](https://www.jotform.com/).
+2. Click on your account image and go to **Settings**.
+3. Click on the **API** tab on the left.
+4. Click on the **Create New Key** button.
+5. Give "Full Access" permission to the created API key.
+6. Copy the **API key** from the page to the `API Key` field on Automatisch.
+7. Enter your API URL in the respective field if it's different than the default value. For EU, it's https://eu-api.jotform.com. For HIPAA, it's https://hipaa-api.jotform.com. For the Jotform Enterprise customers, it should be the API URL of your Jotform Enterprise instance, e.g. https://subdomain.jotform.com/API or https://your-domain.com/API. More information may be found on the [Jotform API documentation](https://api.jotform.com/docs/).
+8. Now, you can start using the Jotform connection with Automatisch.
diff --git a/packages/docs/pages/apps/jotform/triggers.md b/packages/docs/pages/apps/jotform/triggers.md
new file mode 100644
index 00000000..febf5f55
--- /dev/null
+++ b/packages/docs/pages/apps/jotform/triggers.md
@@ -0,0 +1,12 @@
+---
+favicon: /favicons/jotform.svg
+items:
+ - name: New submissions
+ desc: Triggers when a new submission has been added to a specific form.
+---
+
+
+
+
diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md
index e2ace1b9..faebf873 100644
--- a/packages/docs/pages/guide/available-apps.md
+++ b/packages/docs/pages/guide/available-apps.md
@@ -25,6 +25,7 @@ The following integrations are currently supported by Automatisch.
- [HTTP Request](/apps/http-request/actions)
- [HubSpot](/apps/hubspot/actions)
- [Invoice Ninja](/apps/invoice-ninja/triggers)
+- [Jotform](/apps/jotform/triggers)
- [Mailchimp](/apps/mailchimp/triggers)
- [MailerLite](/apps/mailerlite/triggers)
- [Mattermost](/apps/mattermost/actions)
diff --git a/packages/docs/pages/public/favicons/jotform.svg b/packages/docs/pages/public/favicons/jotform.svg
new file mode 100644
index 00000000..99500044
--- /dev/null
+++ b/packages/docs/pages/public/favicons/jotform.svg
@@ -0,0 +1 @@
+
\ No newline at end of file