Port specification for the Make custom app. Companion to README.md. The
source of truth for behavior is the Zapier integration
(DocSpring/zapier_integration); this doc records what carries over verbatim
and what changes because Make apps are declarative JSON + IML, not Node.js.
Zapier apps are Node.js: perform functions run arbitrary JS. Make apps are
declarative — each module is JSON describing the HTTP request (url,
method, body, qs, headers) plus IML expressions ({{...}}) for
mapping. Non-trivial logic (payload flattening, JSON-Schema → parameters) lives
in IML functions (functions/) or RPCs (rpcs/), not inline JS.
So: the design below ports directly; the implementation is re-expressed in IML. Build and test each module against the live API before moving on — IML mistakes are easiest to catch module-by-module.
Mirror the Zapier custom auth (authentication.js + lib/regions.js):
- Parameters:
region(US / EU / AU / Self-hosted),custom_host,token_id,token_secret. - Base URL resolved from region (IML in
general/base):- US
api.docspring.com/ syncsync.api.docspring.com - EU
api-eu.docspring.com/ syncsync.api-eu.docspring.com - AU
api-au.docspring.com/ syncsync.api-au.docspring.com - Self-hosted →
custom_host(single origin, validated likenormalizeHost)
- US
Authorization: Basic base64(token_id:token_secret)— Make computes the header in the connection/base (IMLbase64()).- Connection validation →
GET /api/v1/authentication(200{status:success}).
Make "instant trigger" modules backed by a shared webhook with attach /
detach IML (the Zapier performSubscribe / performUnsubscribe):
- attach →
POST /api/v1/webhookswith{ webhook: { url, event_types:[<event>], include_submission_data:true, version:3, mode, template_uids, folder_uids } }.version:3is pinned so the delivery shape matches the flattener. - detach →
DELETE /api/v1/webhooks/{uid}; tolerate 404. - payload → an IML function
flattenDeliverymirroringlib/payload.js: the top-levelidstays the event id (uuid, stable across retries); the resource's own id is exposed asresource_id. (Dedup-correctness — never let the resource id overwrite the event id.)
Events: submission.processed / .failed / .created / .expired,
submission_data_request.completed / .viewed,
combined_submission.processed / .failed,
submission_batch.processed / .failed,
template.created / .updated / .deleted.
Scope parameters per event mirror lib/scopeFields.js:
- submission / data-request: Templates + Folders + Mode.
- template: Templates + Folders (no Mode — mode-agnostic events).
- combined / batch: Mode only (not template/folder scopable — the API rejects it).
- Generate PDF —
POST {sync}/api/v1/templates/{template_id}/submissions?wait=true. Template dropdown via RPC (list_templates); dynamic per-template fields via RPC overGET /templates/{id}/schema. Template-field inputs namespaceddata__<field>so a field namedtest/metadata/etc. can't collide with a control input; the action strips the prefix to rebuilddata.pdf_passphrasekeyed (not "password") → mapped to the API'spassword. - Combine PDFs —
POST {sync}/api/v1/combined_submissions?wait=truewith a line-itemsource_pdfs(type+id+ optionaltemplate_version). - Create Data Request —
POST {standard}/api/v1/templates/{id}/submissionswith nowait(a data-request submission returns immediately inwaiting_for_data_requests; it doesn't produce a PDF until recipients finish). Recipients are a line-item (email,name,fields,auth_type— defaultemail_link). Template pre-fill fields are all optional. After creating, mint a 30-dayemailtoken per recipient (POST /data_requests/{id}/tokens,type:email) and expose eachsigning_url(+first_signing_url).
- Find Template —
GET /api/v1/templates?query=…(also backs the template dropdown RPC). - Find Submission — by id (
GET /api/v1/submissions/{id}; 404 → empty) or recent list filtered by mode/date.
list_templates—GET /templates?per_page=100→ dropdown options.list_folders—GET /folders→ dropdown options.template_schema_fields—GET /templates/{id}/schema→ Make parameters (thejsonSchemaToZapierFieldslogic re-expressed for Make: scalar/enum/list → parameter types; nested objects → a JSON "collection";data__namespacing). Optional-variant for Create Data Request (all fields non-required).
flattenDelivery(body)— v3 delivery envelope → flat object (see triggers).toDeliveryShape(type, obj, event)— wrap a list item so RPC/search output matches live deliveries (parity withlib/payload.js).jsonSchemaToParams(schema, opts)— JSON Schema → Make parameter definitions.- small helpers:
asArray,parseDict,normalizeHostequivalents.
version:3pinned on webhook subscribe.data__field namespacing on Generate PDF / Create Data Request.pdf_passphrasefield key (not "password").- Sync host +
?wait=truefor Generate PDF / Combine PDFs; standard host, no wait for Create Data Request. - Self-hosted
custom_hostvalidation (only[scheme://]host[:port]).
Build as a private app first (usable by our org), test every module against the DocSpring test account, then submit for Make app verification to list it in the public app directory (review process, like Zapier's).
Review prerequisites met (see app-review/prerequisites):
- ✅ Universal "Make an API Call" module (typeId 12).
- ✅ Sensitive-data sanitization:
log.sanitize: ["request.headers.authorization"]in base and connection; token secret is apassword-type field. - ✅ Every module has a label + description + interface; dates are
type:date. - ✅ Base + connection error handling; connection validates via
GET /authentication. - ✅
limitparameter on both searches (Find Template, Find Submission). - ✅ 8 demo scenarios (one per module) with successful execution logs, clean data
(
Jane Doe/jane.doe@example.com). Watch Events kept active (instant trigger). Note: the Make plan caps active scenarios (~2), so the on-demand demos are toggled off but retain their green run logs — sufficient for review. - ✅ Handled-API-error demo (
DocSpring — Handled API Error, scenario 6220743): Generate PDF with an invalid enum (favorite_color: chartreuse) fails with the clean, service-sourced message[422] The property '#/favorite_color' value "chartreuse" did not match one of the following values: red, green, blue, ...— the baseresponse.errorhandler surfaces DocSpring's own text, no raw JSON. The review form's "Scenario with an API Error" field requires this link.
The review request is UI-only (no API): in the app editor → Publish, make all 8 modules visible in the Modules tab, then the Review tab → paste the API-docs link + demo-scenario links → Request review. A follow-up form arrives by email. Prepared answers:
- Developer relationship: we are the vendor (DocSpring owns the API).
- Support contact: support@docspring.com.
- Category: Files & Documents (where the competing doc-gen apps are listed).
- Service URL: https://docspring.com.
- API docs: https://docspring.com/docs.
- Trademark / third-party T&C compliance: yes (our own API).
Reviewer (AppBot) feedback addressed in commit 622723b and re-tested:
- Fixed: signing-link
type→ query string; removed the shut-down AU region; self-hosted URL now prependshttps://+ host field locked (editable:false); pagination (cursor on Find a submission, page on Find a template + listTemplates RPC, per_page 50);response.limiton both searches (uinteger, default 10);dataadded to the Watch events + Find a submission interfaces; sentence-case labels; third-person descriptions;actionCrud; meaningful groups; samples on searches + trigger; Combine PDFscombined_submission/urlsource types; dropped the unused listFolders RPC. - Answered (reviewer mistakes, with evidence): the v3 delivery does carry a
top-level
id(event UUID) — captured a live delivery to confirm{{body.id}}populates. POST /webhooks returnsuidat the top level (not wrapped), so attach/detach are correct (add-then-remove verified, DELETE → 204).typeon the tokens endpoint is honoured from both body and query (Rails merges params) — moved toqsanyway per guidance.GET /templates/{id}/schemais a valid live endpoint returning a JSON Schema (properties). Both RPCs declare the connection.
- Confirm the Make Apps SDK local file layout + the push mechanism (SDK CLI vs Make API vs web "Custom apps" editor) once the Make account/API token exists.
- Confirm Make's line-item (array) parameter UX for
source_pdfs/ recipients. - Confirm whether Make strips empty values before requests (Zapier's
cleanInputData); if so, handle blanks in IML as the Zapier performs do.
- One "Watch Events" instant trigger, not 13 discrete triggers. It offers an
event_typesmulti-select (all 13 events) + a Mode filter, and subscribes to the chosen events in one DocSpring webhook. This is the idiomatic Make pattern (cf. Stripe's "Watch Events") and far less to maintain than 13 near-identical modules. Output is the flattened envelope (id= event id,resource_id= the resource's id) + the fulldataobject. - Create Signing Link is its own action (not folded into Create Data
Request), because a Make module makes exactly one HTTP request — so minting the
30-day
emailtoken per recipient (POST /data_requests/{id}/tokens) is a separate, chainable module (map over Create Data Request'sdata_requests). - Dynamic template fields via the
templateFieldsRPC:keys(body.properties)fromGET /templates/{id}/schema→ onedata__<field>text input per field, bound to the Generate PDF / Create Data Requestdatacollection. - Sync host +
?wait=truefor Generate PDF / Combine PDFs (absolute URL in the module, base auth headers still applied); standard host, no wait for Create Data Request.
- ✅ Connection (region base URL + Basic auth IML) — live in a Make scenario.
- ✅ Find Template — returned the Demo template; also backs the template RPC.
- ✅ Generate PDF — dynamic per-template fields (nested
templateFieldsRPC),omit()data assembly, sync host +?wait=true+ explicit auth header. Live run produced a processed submission + download URL. - ✅ Find Submission — by id (
add(emptyarray; body)wraps the single object) and list (body.submissions). Live spot-test passed. - ✅ Combine PDFs — line-item
source_pdfs, sync host +?wait=true. API run produced a processedcom_…combined submission on DocSpring. - ✅ Create Data Request — standard host, no wait. API run produced a
waiting_for_data_requestssubmission with the recipient'sdrq_…. Thefields-as-array change meansdata_requestspasses straight through (no lambda). - ✅ Create Signing Link —
POST /data_requests/{id}/tokens; run status 1 and the DocSpring token response{token:{id, data_request_url, expires_at}}matches the output mapping (signing_url = body.token.data_request_url, 30-day email token). - ✅ Watch Events (instant trigger) — full lifecycle verified: creating the hook
fired
attach(registered DocSpring webhookwhk_…, version 3); a livesubmission.processedevent was delivered and the scenario auto-executed (status 1) with the inline flatten; deleting the hook fireddetach(webhook removed from DocSpring). No dangling webhook left.
All modules were validated via the Make API v2 (token in .env), not the MCP
server (which only triggers existing scenarios). The loop, driven from the shell:
- Clone the connection binding from a working scenario: flow module
app#docspring-sspkqt:<module>,parameters: {"__IMTCONN__": <connId>},mapper. POST /scenarios(orPATCH /scenarios/{id}) with the blueprint as a JSON string.POST /scenarios/{id}/startto activate (on-demand scenarios must be active to run).POST /scenarios/{id}/run{responsive:true}→ returnsexecutionId; pollGET /scenarios/{id}/logsforstatus:1.- Verify ground-truth on the DocSpring side (submission/combined/webhook created).
For the instant trigger:
POST /hooks{typeName:"app#docspring-sspkqt", __IMTCONN__, event_types, mode}creates the hook (firesattach); a scenario withmetadata.instant:true+parameters:{"__IMTHOOK__":<hookId>}binds to it;DELETE /hooks/{id}firesdetach. Team2910546, connection10972788.
- No
array()— build a one-element array withadd(emptyarray; x). get(body.properties; item)inside an RPCiterateoutput fails —bodyisn't reliably in scope there. This blocks the enum→dropdown fix;templateFieldscurrently emits plain text fields (DocSpring still validates enums with a clear 422).- Custom IML functions need an "apps edit" permission the API token lacks, so the JS-function route to schema→fields conversion is gated for now.
- Confirmed-valid functions in use:
if,base64,switch,omit,keys,join,add.