Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@

Configuration files for the [OpnForm](https://opnform.com) custom app on [Make.com](https://www.make.com) (formerly Integromat).

This app provides a **"Watch New Submissions"** instant trigger so Make users can automate workflows whenever a form receives a submission, plus the required **"Make an API Call"** universal module for authorized requests to the OpnForm API.
This app provides a **"Watch new submissions"** instant trigger so Make users can automate workflows whenever a form receives a submission, plus the required **"Make an API call"** universal module for authorized requests to the OpnForm API.

## How It Works

1. User searches "OpnForm" in Make's module picker
2. Creates a connection using their OpnForm API key (Sanctum token)
3. Selects a workspace and form from dynamic dropdowns
4. Make auto-registers a webhook via OpnForm's standard integration API
5. On each form submission, OpnForm POSTs data to Make's webhook
4. Make auto-registers the dedicated `make` integration via OpnForm's standard integration API
5. On each form submission, OpnForm POSTs the Make-specific payload to Make's webhook
6. When the webhook is deleted, Make auto-unregisters it from OpnForm

## Architecture

This integration follows the same lightweight pattern as OpnForm's Activepieces integration. It uses OpnForm's **existing `/open/*` API** -- no dedicated Make endpoints are needed.
This integration follows the same lightweight pattern as OpnForm's Activepieces integration. It uses OpnForm's **existing `/open/*` API** -- no dedicated Make endpoints are needed. The existing API accepts `integration_id: "make"` through OpnForm's integration registry and dispatches submissions through `MakeIntegration`, which removes the deprecated generic `submission` field and exposes `form_id`, `form_title`, `form_slug`, `submission_id`, optional `edit_link`, and `data`.

## Structure

Expand Down Expand Up @@ -46,7 +46,7 @@ All endpoints are part of OpnForm's existing API, authenticated via Bearer token
| Endpoint | Method | Purpose |
|---|---|---|
| `/open/workspaces` | GET | Validate API key + list workspaces |
| `/open/workspaces/{id}/forms` | GET | List forms (paginated, up to 100/page) |
| `/open/workspaces/{id}/forms` | GET | List forms (paginated, up to 100/page and 300 results per picker load) |
| `/open/forms/{id}/integrations` | POST | Create Make integration (attach webhook) |
| `/open/forms/{id}/integrations/{integrationId}` | DELETE | Remove integration (detach webhook) |
| Any relative path selected by the user | Any supported REST method | Universal authorized API call |
Expand All @@ -62,7 +62,7 @@ Docs: [developers.make.com/custom-apps-documentation](https://developers.make.co
3. Click **+ Create app** and name it `opnform` (label: "OpnForm")
4. Configure the **Connection** using `connection/parameters.json` and `connection/communication.json`
5. Create a **Webhook** (dedicated + attached) using `webhook/` configs
6. Create an **Instant Trigger** module "Watch New Submissions" using `modules/watchNewSubmissions/` configs
6. Create an **Instant Trigger** module "Watch new submissions" using `modules/watchNewSubmissions/` configs
7. Create **RPCs** `listWorkspaces` and `listForms` using `rpcs/` configs
8. Test end-to-end with a real OpnForm API key
9. Submit for marketplace review (review takes ~4-6 weeks)
Expand Down
3 changes: 2 additions & 1 deletion connection/communication.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
},
"response": {
"metadata": {
"type": "text",
"value": "{{body.length}} workspace(s)"
},
"error": {
"message": "{{body.message}}"
"message": "[{{statusCode}}] {{ifempty(body.message, 'Request failed')}}"
}
},
"log": {
Expand Down
11 changes: 6 additions & 5 deletions connection/parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
{
"name": "apiKey",
"type": "password",
"label": "OpnForm API Key",
"help": "Generate an API key from your [OpnForm account settings](https://opnform.com/home?user-settings=access-tokens).",
"label": "OpnForm API key",
"help": "Generate an API key from your [OpnForm account settings](https://opnform.com/home?user-settings=access-tokens) with the `workspaces-read`, `forms-read`, and `manage-integrations` abilities.",
"required": true
},
{
"name": "baseUrl",
"type": "url",
"label": "API Base URL",
"help": "Leave empty for OpnForm Cloud. For self-hosted instances, enter your API URL (e.g. `https://api.your-domain.com`).",
"default": "https://api.opnform.com"
"label": "API base URL",
"help": "Leave the default for OpnForm Cloud. For self-hosted instances, enter your API URL (e.g. `https://api.your-domain.com`). This URL cannot be changed after the connection is created.",
"default": "https://api.opnform.com",
"editable": false
}
]
12 changes: 6 additions & 6 deletions makecomapp.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"components": {
"connection": {
"apiKeyAuth": {
"label": "OpnForm API Key",
"label": "OpnForm API key",
"connectionType": "basic",
"codeFiles": {
"communication": "connection/communication.json",
Expand All @@ -20,7 +20,7 @@
},
"webhook": {
"newSubmission": {
"label": "New Submission",
"label": "New submission",
"webhookType": "web",
"connection": "apiKeyAuth",
"codeFiles": {
Expand All @@ -32,7 +32,7 @@
},
"module": {
"watchNewSubmissions": {
"label": "Watch New Submissions",
"label": "Watch new submissions",
"description": "Triggers when a new form submission is received.",
"moduleType": "instant_trigger",
"connection": "apiKeyAuth",
Expand All @@ -45,7 +45,7 @@
}
},
"makeAnApiCall": {
"label": "Make an API Call",
"label": "Make an API call",
"description": "Performs an arbitrary authorized API call.",
"moduleType": "universal",
"connection": "apiKeyAuth",
Expand All @@ -60,14 +60,14 @@
},
"rpc": {
"listWorkspaces": {
"label": "List Workspaces",
"label": "List workspaces",
"connection": "apiKeyAuth",
"codeFiles": {
"communication": "rpcs/listWorkspaces.json"
}
},
"listForms": {
"label": "List Forms",
"label": "List forms",
"connection": "apiKeyAuth",
"codeFiles": {
"communication": "rpcs/listForms.json"
Expand Down
2 changes: 1 addition & 1 deletion modules/makeAnApiCall/expect.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
}
],
"type": "array",
"label": "Query String"
"label": "Query string"
},
{
"name": "body",
Expand Down
8 changes: 4 additions & 4 deletions modules/watchNewSubmissions/interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
{
"name": "form_title",
"type": "text",
"label": "Form Title"
"label": "Form title"
},
{
"name": "form_slug",
"type": "text",
"label": "Form Slug"
"label": "Form slug"
},
{
"name": "submission_id",
Expand All @@ -22,12 +22,12 @@
{
"name": "edit_link",
"type": "url",
"label": "Edit Submission URL"
"label": "Edit submission URL"
},
{
"name": "data",
"type": "collection",
"label": "Form Fields",
"label": "Form fields",
"spec": []
}
]
1 change: 1 addition & 0 deletions rpcs/listForms.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"per_page": 100
},
"response": {
"limit": 300,
"output": {
"label": "{{item.title}}",
"value": "{{item.id}}"
Expand Down
38 changes: 38 additions & 0 deletions tests/contracts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ test('attached webhook persists and reuses the remote integration id', async ()
const detach = await readJson('webhook/detach.json')

assert.equal(communication.output, '{{body}}')
assert.equal(communication.respond.type, 'json')
assert.equal(communication.respond.status, 200)
assert.equal(attach.body.integration_id, 'make')
assert.equal(attach.response.data.externalHookId, '{{body.form_integration.id}}')
assert.equal(attach.response.data.formId, '{{parameters.formId}}')
assert.match(detach.url, /{{webhook\.formId}}/)
Expand All @@ -53,13 +55,49 @@ test('form RPC follows the Laravel resource pagination envelope', async () => {
}

assert.equal(rpc.response.iterate, '{{body.data}}')
assert.equal(rpc.response.limit, 300)
assert.equal(rpc.pagination.url, '{{body.links.next}}')
assert.equal(rpc.pagination.condition, '{{body.links.next}}')
assert.equal(response.links.next.endsWith('page=2'), true)
assert.equal(response.next_page_url, undefined)
assert.equal(rpc.qs.per_page, 100)
})

test('connection protects its domain and explains the required token abilities', async () => {
const parameters = await readJson('connection/parameters.json')
const communication = await readJson('connection/communication.json')
const apiKey = parameters.find(({ name }) => name === 'apiKey')
const baseUrl = parameters.find(({ name }) => name === 'baseUrl')

assert.match(apiKey.help, /workspaces-read/)
assert.match(apiKey.help, /forms-read/)
assert.match(apiKey.help, /manage-integrations/)
assert.equal(baseUrl.editable, false)
assert.match(baseUrl.help, /cannot be changed/)
assert.equal(communication.response.metadata.type, 'text')
assert.equal(
communication.response.error.message,
"[{{statusCode}}] {{ifempty(body.message, 'Request failed')}}"
)
})

test('user-facing labels follow Make sentence case', async () => {
const manifest = await readJson('makecomapp.json')
const parameters = await readJson('connection/parameters.json')
const triggerInterface = await readJson('modules/watchNewSubmissions/interface.json')
const universalExpect = await readJson('modules/makeAnApiCall/expect.json')

assert.equal(manifest.components.connection.apiKeyAuth.label, 'OpnForm API key')
assert.equal(manifest.components.module.watchNewSubmissions.label, 'Watch new submissions')
assert.equal(manifest.components.module.makeAnApiCall.label, 'Make an API call')
assert.equal(parameters.find(({ name }) => name === 'baseUrl').label, 'API base URL')
assert.equal(triggerInterface.find(({ name }) => name === 'form_title').label, 'Form title')
assert.equal(triggerInterface.find(({ name }) => name === 'form_slug').label, 'Form slug')
assert.equal(triggerInterface.find(({ name }) => name === 'edit_link').label, 'Edit submission URL')
assert.equal(triggerInterface.find(({ name }) => name === 'data').label, 'Form fields')
assert.equal(universalExpect.find(({ name }) => name === 'qs').label, 'Query string')
})

test('trigger interface exposes dynamic fields and optional edit link', async () => {
const output = await readJson('modules/watchNewSubmissions/interface.json')
const data = output.find(({ name }) => name === 'data')
Expand Down
1 change: 1 addition & 0 deletions webhook/communication.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"output": "{{body}}",
"respond": {
"type": "json",
"status": 200,
"body": {
"ok": true
Expand Down
Loading