Base URL https://YOUR-QUICKFORMS-HOST/api/v1. Everything you can do in the dashboard, the API can do too.
Authentication
Create an API key on the API keys page of your team in the dashboard and send it as Authorization: Bearer <api_key>. API keys belong to a team, and every request only sees that team’s forms, submissions, deliveries, destinations, partial submissions and mail providers. Send Accept: application/json.
401for a missing or invalid API key,404for anything outside the API key’s team,422for validation errors.- Requests are rate limited to 120 a minute per API key.
403when the API key lacks the ability or the user’s role doesn’t allow it. See API key abilities.POST /api/v1/forms/generateis also limited to 30 generated forms an hour per user by default (QUICKFORMS_FORM_BUILDER_PER_HOUR), and returns429past that.- The forms, submissions and partial submissions lists are paginated with
pageandper_page(up to 100, default 25). Personal links are paginated withper_pageup to 200, default 50.
Key abilities
Choose an API key’s abilities when you create it on the API keys page. New API keys have read and write by default.
| Ability | Allows |
|---|---|
read |
GET endpoints, the destination preview dry run and read-only MCP tools. |
write |
Every other endpoint and MCP tool: creating, changing and deleting forms, destinations, links, mail providers and submissions. |
proxy |
Server-side proxy mode. Only API keys with this ability are trusted to forward visitor details, and the API key’s user must still be on the team and able to manage forms. |
An API key can never do more than its user’s team role allows. Members are read-only, so a member’s API key gets a 403 on writes even with the write ability. Admins and owners can write. A 403 comes back when the API key lacks the ability or the role doesn’t allow the action.
Endpoints
Every /api/v1 endpoint. PATCH /api/v1/forms/{form} also accepts PUT. For the public endpoints that forms post to, see public endpoints.
Account
GET /api/v1/me
The user, team and API key behind the current request. Handy for checking an API key works.
curl https://YOUR-QUICKFORMS-HOST/api/v1/me \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": {
"user": { "id": "01jqk7m2x4v8n3p5r6s9t0w1ya", "name": "Ada Lovelace", "email": "ada@example.com" },
"team": { "id": "01jqk7m3b6c2d8e4f5g7h9j0kb", "name": "Acme", "slug": "acme" },
"token": { "id": "01jqk8a1c3d5e7f9g2h4j6k8mc", "name": "CI", "last_used_at": "2026-11-06T10:00:00.000000Z" }
}
}Forms
GET /api/v1/forms
The team’s forms, newest first, with submission counts.
curl https://YOUR-QUICKFORMS-HOST/api/v1/forms?per_page=10 \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": [
{ "id": "01jc8xk2m5t9qw3e7r4y6u8v0p", "name": "Contact", "mode": "live", "status": "published", "submissions_count": 61, "...": "..." }
],
"links": { "first": "...?page=1", "last": "...?page=3", "prev": null, "next": "...?page=2" },
"meta": { "current_page": 1, "from": 1, "last_page": 3, "per_page": 25, "to": 25, "total": 61 }
}POST /api/v1/forms
Create a form. name and schema are required, unless you pass template. Optional: key (a unique key for forms as code, 1 to 64 lowercase letters, numbers, dashes and underscores), template (a key from GET /api/v1/form-templates: the template’s fields and settings are used and anything else you pass overrides them, see creating forms; an unknown key is a 422 on template), description (what the form is for, up to 1000 characters, used as context by the spam filter), mode (live or sandbox), is_active, webhook_url, mail_provider_id, settings (see form settings), turnstile_secret (see Turnstile keys), stripe_secret (see Stripe keys) and draft. With "draft": true the form is created unpublished (status draft): it takes sandbox submissions only until you publish it. Without it the form is published straight away. See drafts and publishing. The sandbox key and webhook secret are generated for you.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name": "Contact", "description": "General enquiries about our products", "schema": {"fields": [{"name": "email", "type": "email", "required": true}]}, "webhook_url": "https://example.com/hooks", "settings": {"notify": {"emails": ["you@yourdomain.com"]}}}'{
"data": {
"id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"key": null,
"name": "Contact",
"description": "General enquiries about our products",
"mode": "live",
"is_active": true,
"status": "published",
"published_at": "2026-11-06T10:00:00.000000Z",
"draft": null,
"endpoint_url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
"hosted_url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
"embed": {
"script": "<div data-quickforms=\"01jc8xk2m5t9qw3e7r4y6u8v0p\"></div>\n<script src=\"https://YOUR-QUICKFORMS-HOST/embed.js\" defer></script>",
"script_sri": "<div data-quickforms=\"01jc8xk2m5t9qw3e7r4y6u8v0p\"></div>\n<script src=\"https://YOUR-QUICKFORMS-HOST/embed/v1/embed.HASH.js\" integrity=\"sha384-...\" crossorigin=\"anonymous\" defer></script>",
"tracker_sri": "<script src=\"https://YOUR-QUICKFORMS-HOST/embed/v1/quickforms-track.HASH.js\" integrity=\"sha384-...\" crossorigin=\"anonymous\" defer></script>",
"iframe": "<iframe src=\"https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p/embed\" ...></iframe>"
},
"sandbox_key": "qf_sandbox_...",
"schema": { "fields": [{ "name": "email", "type": "email", "label": "Email", "required": true }] },
"settings": {
"notify": { "enabled": true, "emails": ["you@yourdomain.com"], "subject": "New submission: {{_form_name}}", "reply_to_field": null },
"routing": { "rules": [] },
"autoresponder": { "enabled": false, "email_field": null, "subject": "Thanks, we got your message", "body": "..." },
"redirect_url": null,
"success_message": "Thanks! Your submission has been received.",
"allowed_origins": ["*"],
"honeypot_field": null,
"honeypot_fields": [],
"turnstile": { "enabled": false, "site_key": null },
"altcha": { "enabled": false, "max_number": 100000 },
"geo": { "mode": "off", "countries": [], "unknown": "allow" },
"block_disposable_emails": false,
"payment": { "enabled": false, "publishable_key": null, "currency": "gbp", "amount": null, "amount_field": null, "prices": [], "description": "{{_form_name}}" },
"hosted": { "enabled": true, "layout": "classic", "...": "..." },
"partials": { "enabled": false, "abandon_after_minutes": 30, "retention_days": 30 },
"require_link": false,
"confirmation": { "enabled": false, "...": "..." },
"spam": { "enabled": true, "threshold": 0.8, "rules": [] }
},
"webhook_url": "https://example.com/hooks",
"webhook_secret": "whsec_...",
"turnstile_secret_set": false,
"stripe_secret_set": false,
"mail_provider_id": null,
"submissions_count": 0,
"created_at": "2026-11-06T10:00:00.000000Z",
"updated_at": "2026-11-06T10:00:00.000000Z"
}
}GET /api/v1/form-templates
The ready-made templates. Each has a key, name, summary, category, fields (the field labels) and the full draft (name, description, schema and settings). Not paginated.
curl https://YOUR-QUICKFORMS-HOST/api/v1/form-templates \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": [
{
"key": "contact",
"name": "Simple contact form",
"summary": "Name, email and a message. The classic.",
"category": "General",
"fields": ["Name", "Email", "Message"],
"draft": {
"name": "Contact",
"description": "General enquiries from visitors to our website.",
"schema": {
"fields": [
{ "name": "name", "type": "text", "label": "Name", "required": true, "max": 100 },
{ "name": "email", "type": "email", "label": "Email", "required": true },
{ "name": "message", "type": "textarea", "label": "Message", "required": true, "min": 10, "max": 5000 }
]
},
"settings": {
"autoresponder": { "enabled": true, "subject": "Thanks {{name}}, we got your message" }
}
}
},
{ "key": "service-business", "name": "Service business enquiry", "...": "..." }
]
}Create a form from one with POST /api/v1/forms and {"template": "contact"}, plus any overrides. The example values below are illustrative.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"template": "contact", "name": "Website contact", "settings": {"notify": {"emails": ["you@yourdomain.com"]}}}'{
"message": "The selected template is invalid.",
"errors": { "template": ["The selected template is invalid."] }
}A valid key responds 201 with the created form, the same as POST /api/v1/forms. The 422 above is what an unknown key returns.
POST /api/v1/forms/generate
Design a form with the AI form builder from a plain-English prompt (5 to 2000 characters). With save false (the default) it returns a draft and saves nothing. With save true it creates the form and responds 201 with it, the same as POST /api/v1/forms.
The draft has a name, a description, a schema and settings with a success message, an autoresponder and up to 3 smart rules. It is in the shape POST /api/v1/forms accepts. Review it before you save it.
The prompt and draft below are illustrative. The model’s answer varies.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/generate \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"prompt": "A quote request form for a garden landscaping business", "save": false}'{
"data": {
"draft": {
"name": "Landscaping quote",
"description": "Homeowners asking for a quote for garden landscaping work.",
"schema": {
"fields": [
{ "name": "name", "type": "text", "label": "Name", "required": true },
{ "name": "email", "type": "email", "label": "Email", "required": true },
{ "name": "work", "type": "checkboxes", "label": "What work do you need?", "required": true, "options": ["Design", "Planting", "Paving", "Fencing"] },
{ "name": "details", "type": "textarea", "label": "Tell us about your garden", "required": false }
]
},
"settings": {
"success_message": "Thanks, we will be in touch about your quote.",
"autoresponder": { "enabled": true, "subject": "Thanks {{name}}, we got your quote request", "body": "..." },
"spam": { "rules": ["Must be a request for garden landscaping work"] }
}
}
}
}{
"message": "The AI form builder is not configured. Set OPENAI_API_KEY to use it.",
"errors": { "prompt": ["The AI form builder is not configured. Set OPENAI_API_KEY to use it."] }
}Errors are a 422 on prompt when the prompt is too short or too long, when the builder is not configured, or when the model could not create a form, and a 429 when the team has used its generated forms for the hour.
GET /api/v1/forms/{form}
One form with its schema, settings, endpoint URL, hosted page URL (hosted_url, the same address), embed snippets, sandbox key and webhook secret. The top-level attributes are the published version. status is draft (never published), changes_pending (live, with a saved draft) or published, published_at is when it was last published (null if never), and draft holds the unpublished changes or null. See drafts and publishing.
curl https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": {
"id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"key": null,
"name": "Contact",
"description": "General enquiries about our products",
"mode": "live",
"is_active": true,
"status": "published",
"published_at": "2026-11-06T10:00:00.000000Z",
"draft": null,
"endpoint_url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
"hosted_url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
"embed": {
"script": "<div data-quickforms=\"01jc8xk2m5t9qw3e7r4y6u8v0p\"></div>\n<script src=\"https://YOUR-QUICKFORMS-HOST/embed.js\" defer></script>",
"script_sri": "<div data-quickforms=\"01jc8xk2m5t9qw3e7r4y6u8v0p\"></div>\n<script src=\"https://YOUR-QUICKFORMS-HOST/embed/v1/embed.HASH.js\" integrity=\"sha384-...\" crossorigin=\"anonymous\" defer></script>",
"tracker_sri": "<script src=\"https://YOUR-QUICKFORMS-HOST/embed/v1/quickforms-track.HASH.js\" integrity=\"sha384-...\" crossorigin=\"anonymous\" defer></script>",
"iframe": "<iframe src=\"https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p/embed\" ...></iframe>"
},
"sandbox_key": "qf_sandbox_...",
"schema": { "fields": [{ "name": "email", "type": "email", "label": "Email", "required": true }] },
"settings": {
"notify": { "enabled": true, "emails": ["you@yourdomain.com"], "subject": "New submission: {{_form_name}}", "reply_to_field": null },
"routing": { "rules": [] },
"autoresponder": { "enabled": false, "email_field": null, "subject": "Thanks, we got your message", "body": "..." },
"redirect_url": null,
"success_message": "Thanks! Your submission has been received.",
"allowed_origins": ["*"],
"honeypot_field": null,
"honeypot_fields": [],
"turnstile": { "enabled": false, "site_key": null },
"altcha": { "enabled": false, "max_number": 100000 },
"geo": { "mode": "off", "countries": [], "unknown": "allow" },
"block_disposable_emails": false,
"payment": { "enabled": false, "publishable_key": null, "currency": "gbp", "amount": null, "amount_field": null, "prices": [], "description": "{{_form_name}}" },
"hosted": { "enabled": true, "layout": "classic", "...": "..." },
"partials": { "enabled": false, "abandon_after_minutes": 30, "retention_days": 30 },
"require_link": false,
"confirmation": { "enabled": false, "...": "..." },
"spam": { "enabled": true, "threshold": 0.8, "rules": [] }
},
"webhook_url": "https://example.com/hooks",
"turnstile_secret_set": false,
"stripe_secret_set": false,
"mail_provider_id": null,
"submissions_count": 0,
"created_at": "2026-11-06T10:00:00.000000Z",
"updated_at": "2026-11-06T10:00:00.000000Z"
}
}PATCH /api/v1/forms/{form}
Update a form (PUT works too). Only the keys you send change. Settings are merged, but lists such as notify.emails, allowed_origins, spam.rules, honeypot_fields, routing.rules, geo.countries and payment.prices are replaced, and sending schema replaces the whole field list.
Pass "draft": true to keep the changes as a draft instead of making them live: the published version keeps taking live submissions until you publish. Drafts build on each other, and a draft identical to the live version is dropped. is_active, key, turnstile_secret and stripe_secret always apply immediately. Without draft, changes go live straight away, together with any pending draft. See drafts and publishing.
curl -X PATCH https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"mode": "sandbox", "settings": {"allowed_origins": ["https://yoursite.com"]}}'{
"data": {
"id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"key": null,
"name": "Contact",
"description": "General enquiries about our products",
"mode": "live",
"is_active": true,
"status": "published",
"published_at": "2026-11-06T10:00:00.000000Z",
"draft": null,
"endpoint_url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
"hosted_url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
"embed": {
"script": "<div data-quickforms=\"01jc8xk2m5t9qw3e7r4y6u8v0p\"></div>\n<script src=\"https://YOUR-QUICKFORMS-HOST/embed.js\" defer></script>",
"script_sri": "<div data-quickforms=\"01jc8xk2m5t9qw3e7r4y6u8v0p\"></div>\n<script src=\"https://YOUR-QUICKFORMS-HOST/embed/v1/embed.HASH.js\" integrity=\"sha384-...\" crossorigin=\"anonymous\" defer></script>",
"tracker_sri": "<script src=\"https://YOUR-QUICKFORMS-HOST/embed/v1/quickforms-track.HASH.js\" integrity=\"sha384-...\" crossorigin=\"anonymous\" defer></script>",
"iframe": "<iframe src=\"https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p/embed\" ...></iframe>"
},
"sandbox_key": "qf_sandbox_...",
"schema": { "fields": [{ "name": "email", "type": "email", "label": "Email", "required": true }] },
"settings": {
"notify": { "enabled": true, "emails": ["you@yourdomain.com"], "subject": "New submission: {{_form_name}}", "reply_to_field": null },
"routing": { "rules": [] },
"autoresponder": { "enabled": false, "email_field": null, "subject": "Thanks, we got your message", "body": "..." },
"redirect_url": null,
"success_message": "Thanks! Your submission has been received.",
"allowed_origins": ["*"],
"honeypot_field": null,
"honeypot_fields": [],
"turnstile": { "enabled": false, "site_key": null },
"altcha": { "enabled": false, "max_number": 100000 },
"geo": { "mode": "off", "countries": [], "unknown": "allow" },
"block_disposable_emails": false,
"payment": { "enabled": false, "publishable_key": null, "currency": "gbp", "amount": null, "amount_field": null, "prices": [], "description": "{{_form_name}}" },
"hosted": { "enabled": true, "layout": "classic", "...": "..." },
"partials": { "enabled": false, "abandon_after_minutes": 30, "retention_days": 30 },
"require_link": false,
"confirmation": { "enabled": false, "...": "..." },
"spam": { "enabled": true, "threshold": 0.8, "rules": [] }
},
"webhook_url": "https://example.com/hooks",
"turnstile_secret_set": false,
"stripe_secret_set": false,
"mail_provider_id": null,
"submissions_count": 0,
"created_at": "2026-11-06T10:00:00.000000Z",
"updated_at": "2026-11-06T10:00:00.000000Z"
}
}POST /api/v1/forms/{form}/publish
Make the saved draft live, or put a form that has never been published live. Clears draft and sets published_at. Returns the form.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/publish \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": {
"id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"name": "Contact",
"status": "published",
"published_at": "2026-11-06T10:30:00.000000Z",
"draft": null,
"...": "..."
}
}DELETE /api/v1/forms/{form}/draft
Throw away the saved draft. The live version is untouched. Returns the form.
curl -X DELETE https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/draft \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": {
"id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"name": "Contact",
"status": "published",
"draft": null,
"...": "..."
}
}The response values above are illustrative.
POST /api/v1/forms/{form}/webhook-secret/rotate
Replace the form’s webhook signing secret. Returns the form with the new webhook_secret. The old secret stops working straight away, so update your receiver.
webhook_secret is returned by this endpoint and when a form is created. Other form responses leave it out.
sandbox_key and webhook_secret are only included for an API key with the write ability whose user can manage forms. Read-only access leaves them out.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/webhook-secret/rotate \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"POST /api/v1/forms/{form}/sandbox-key/rotate
Replace the form’s sandbox key. Returns the form with the new sandbox_key. The old key stops working straight away.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/sandbox-key/rotate \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"DELETE /api/v1/forms/{form}
Delete a form with no live submissions. Responds 204 with no body. Sandbox submissions don’t count, so a form you have only tested can be deleted. A form with live submissions responds 422 on form: archive it instead. Every form response has can_delete.
curl -X DELETE https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"POST /api/v1/forms/{form}/archive
Stop a form taking responses and keep its submissions. Sets archived_at and returns the form. Its hosted page shows the closed message, and submissions respond 410 with the code form_closed. Archived forms are still returned by GET /api/v1/forms.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/archive \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"POST /api/v1/forms/{form}/restore
Bring an archived form back. Clears archived_at and returns the form.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/restore \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"Forms as code
Address a form by its key instead of its ID. This is what the CLI, GitHub Action and Netlify plugin use.
PUT /api/v1/forms/by-key/{key}
Create or update the form with this key from a definition: the attributes of POST /api/v1/forms, plus destinations (matched to the form’s destinations by name) and prune_destinations (true deletes destinations not in the list). Add ?dry_run=1 to validate everything without saving. Responds 201 when it creates the form, otherwise 200.
curl -X PUT https://YOUR-QUICKFORMS-HOST/api/v1/forms/by-key/contact \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name": "Contact", "schema": {"fields": [{"name": "email", "type": "email", "required": true}]}, "destinations": [{"name": "Team Slack", "type": "slack", "config": {"webhook_url": "https://hooks.slack.com/services/..."}}]}'{
"data": {
"id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"key": "contact",
"name": "Contact",
"status": "published",
"...": "...",
"destinations": [{ "id": "01jcd0...", "type": "slack", "name": "Team Slack", "...": "..." }]
},
"meta": {
"dry_run": false,
"created": true,
"status": "published",
"destinations": { "created": ["Team Slack"], "updated": [], "deleted": [] }
}
}Keys are 1 to 64 lowercase letters, numbers, dashes and underscores, starting with a letter or number. Anything else is a 422 on key. Destination errors carry their position, such as destinations.0.config.access_token. See forms as code.
GET /api/v1/forms/by-key/{key}
The form with this key, with a destinations list. 404 if there is none.
curl https://YOUR-QUICKFORMS-HOST/api/v1/forms/by-key/contact \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"Form settings
| Key | Default | Description |
|---|---|---|
notify.enabled |
true |
Send the notification email. |
notify.emails |
[] |
Up to 10 recipients. Empty means the team owner. |
notify.subject |
"New submission: {{_form_name}}" |
Up to 200 characters. Supports {{field}}, {{_form_name}} and {{_submission_id}}. |
notify.reply_to_field |
null |
The email field replies go to, or "none" for no reply-to. null picks the autoresponder’s email field or the first email field. Must be one of the form’s email fields. |
routing.rules |
[] |
Up to 20 routing rules. Each has name, conditions (up to 10 {field, operator, value}, all must match), notify_emails (up to 10), notify_mode (add or replace, default add) and redirect_url. operator is equals, not_equals, contains, is_set or is_empty. |
autoresponder.enabled |
false |
Send the autoresponder. |
autoresponder.email_field |
null |
Field holding the recipient. Defaults to the first email field. |
autoresponder.subject |
"Thanks, we got your message" |
Supports {{field}}. |
autoresponder.body |
A short thank-you | Plain text, up to 5000 characters. Supports {{field}}. |
redirect_url |
null |
Where HTML posts go after success. |
success_message |
"Thanks! Your submission has been received." |
Up to 500 characters. Shown on the thank-you page, in the embed and in JSON responses. Placeholders like {{name}} are filled from the submission. |
allowed_origins |
["*"] |
Up to 20 origins: *, https://example.com or https://*.example.com. Browser-only, see CORS. |
honeypot_field |
null |
Overrides the main honeypot name (_gotcha). |
honeypot_fields |
[] |
Up to 5 extra honeypots, such as website or fax. |
turnstile.enabled |
false |
Require a valid Cloudflare Turnstile token on live submissions. Needs site_key and turnstile_secret. |
turnstile.site_key |
null |
Your Turnstile widget’s site key, up to 100 characters. |
altcha.enabled |
false |
Require an Altcha proof-of-work solution on submissions. |
altcha.max_number |
100000 |
The upper bound of the Altcha challenge, 1000 to 1000000. |
geo.mode |
off |
Geo filters: off, allow (only the listed countries) or block (all but the listed countries). Filtered submissions are held as spam. |
geo.countries |
[] |
Up to 250 two-letter country codes. |
geo.unknown |
allow |
allow or block submissions whose country is unknown. |
block_disposable_emails |
false |
Hold submissions with a disposable email address as spam. |
payment.enabled |
false |
Take a Stripe payment before the submission is accepted. Needs publishable_key, stripe_secret, and amount or amount_field. |
payment.publishable_key |
null |
Your Stripe publishable key, starting pk_. |
payment.currency |
gbp |
A three-letter currency code. |
payment.amount |
null |
A fixed amount in the smallest currency unit (pence, cents), 1 to 99999999. |
payment.amount_field |
null |
A field whose answer sets the price, with prices. |
payment.prices |
[] |
A map of the field’s options to amounts in the smallest unit. Replaced whole. |
payment.description |
"{{_form_name}}" |
Up to 300 characters. |
webhook.events |
null |
The webhook events to send. null is submission.created only. Replaced whole, and an empty list sends nothing. |
partials.enabled |
false |
Save progress from the embed and hosted page while people type. See partial submissions. |
partials.abandon_after_minutes |
30 |
Minutes without activity before a partial submission counts as abandoned, 5 to 1440. |
partials.retention_days |
30 |
Days partial submissions are kept, 1 to 90. |
confirmation.enabled |
false |
Turn on double opt-in: send a confirmation email and hold everything else until it is confirmed. Needs an email field. |
confirmation.email_field |
null |
The email field to confirm. Defaults to the first email field. |
confirmation.subject |
"Please confirm your email address" |
Up to 200 characters. Supports {{field}}. |
confirmation.body |
A short request to confirm | Plain text, up to 5000 characters. Supports {{field}}. |
confirmation.button_label |
"Confirm my email" |
Up to 60 characters. |
confirmation.expires_hours |
48 |
How long the confirmation link works, 1 to 720. |
confirmation.on_expiry |
keep |
keep marks unconfirmed submissions expired, delete deletes them. |
confirmation.redirect_url |
null |
Where people go after confirming. |
confirmation.confirmed_message |
"Thanks, your email address is confirmed." |
Shown on the hosted confirmed page when there is no redirect. Up to 500 characters. Supports {{field}}. |
require_link |
false |
Only accept live submissions that come through a personal link. Sandbox submissions don’t need one. |
hosted.enabled |
true |
Serve the hosted form page at GET /f/{form}. |
hosted.layout |
classic |
classic, steps or compact. |
hosted.appearance |
auto |
auto, light or dark. |
hosted.accent |
#0b0b0f |
A six-digit hex colour. |
hosted.font |
sans |
sans, serif or mono. |
hosted.radius |
rounded |
sharp, rounded or pill. |
hosted.background |
plain |
plain, gradient or grid. |
hosted.title |
null |
Up to 120 characters. Defaults to the form name. |
hosted.intro |
null |
Up to 1000 characters. |
hosted.submit_label |
"Submit" |
Up to 40 characters. |
hosted.logo_url |
null |
An https URL. |
hosted.favicon_url |
null |
An https URL. |
hosted.show_branding |
true |
Show “Powered by quickforms” under the form. |
spam.enabled |
true |
Run the spam filter on this form’s submissions. |
spam.threshold |
0.8 |
Between 0.5 and 0.99. Submissions at or above it are flagged. |
spam.rules |
[] |
Up to 10 smart rules, each up to 300 characters. |
Honeypot names
honeypot_field and each entry in honeypot_fields can be up to 64 letters, numbers, dashes and underscores, optionally starting with one underscore. Entries in honeypot_fields must be distinct. A honeypot can’t share a name with one of the form’s fields: that is a 422 on settings.honeypot_fields.
{
"message": "Honeypots can't share a name with a field: website.",
"errors": { "settings.honeypot_fields": ["Honeypots can't share a name with a field: website."] }
}Turnstile keys
turnstile_secret is a top-level attribute, not part of settings. It is your Turnstile widget’s secret key, up to 200 characters. It is write-only: it is encrypted at rest and never returned. Forms return turnstile_secret_set instead, true once a secret is saved. Sending a blank turnstile_secret keeps the saved one. The secret is not part of a draft, so it is saved straight away even with "draft": true.
Switching settings.turnstile.enabled on without both a site key and a secret (sent now or saved before) is a 422 on settings.turnstile.site_key:
{
"message": "Add your Turnstile site key and secret key to switch Turnstile on.",
"errors": { "settings.turnstile.site_key": ["Add your Turnstile site key and secret key to switch Turnstile on."] }
}See spam protection for setting up the widget.
Stripe keys
stripe_secret is a top-level attribute, not part of settings: your Stripe secret key, sk_test_, sk_live_, rk_test_ or rk_live_, up to 200 characters. Like turnstile_secret it is write-only, encrypted at rest and never returned. Forms return stripe_secret_set instead, and a blank value keeps the saved one.
Switching settings.payment.enabled on without a publishable key and a secret is a 422 on settings.payment.publishable_key, and without amount or amount_field a 422 on settings.payment.amount. See payments.
Routing and reply-to
Each routing rule’s condition field, and notify.reply_to_field, must name one of the form’s fields (for reply-to, an email field, or "none"). Otherwise it is a 422, for example on settings.routing.rules.0.conditions.0.field. See routing rules.
Double opt-in
Switching settings.confirmation.enabled on needs an email field, and confirmation.email_field must name one of the form’s email fields. Otherwise it is a 422 on settings.confirmation.email_field:
{
"message": "Double opt-in needs an email field to send the confirmation to.",
"errors": { "settings.confirmation.email_field": ["Double opt-in needs an email field to send the confirmation to."] }
}See double opt-in.
Personal links
Personal links are unique URLs to a form’s hosted page, one per person.
POST /api/v1/forms/{form}/links
Create one link, or up to 500 with {"links": [...]}. Each link takes label, email, prefill, metadata, max_submissions (default 1, null for unlimited) and expires_at, all optional. The response is always a list. See link fields.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/links \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"label": "Ada Lovelace", "email": "ada@example.com", "metadata": {"customer_id": "cus_123"}}'{
"data": [
{
"id": "01jcb2m4k8q6r7s9t0v1w2x3y4",
"form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"token": "lnk_Xq3...",
"url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p?link=lnk_Xq3...",
"status": "open",
"label": "Ada Lovelace",
"email": "ada@example.com",
"prefill": null,
"metadata": { "customer_id": "cus_123" },
"max_submissions": 1,
"submissions_count": 0,
"last_submitted_at": null,
"expires_at": null,
"revoked_at": null,
"created_at": "2026-11-06T10:00:00.000000Z"
}
]
}prefill keys must be fields of the form, otherwise it is a 422 on prefill (or links.N.prefill in a bulk call).
GET /api/v1/forms/{form}/links
A form’s links, newest first. Filter with status (open, completed, expired or revoked) and email. Paginated with per_page (1 to 200, default 50) and page.
GET /api/v1/links/{link}
One link.
DELETE /api/v1/links/{link}
Revoke a link. It stops working straight away and its submissions are kept. Returns the revoked link.
Submissions
POST /api/v1/forms/{form}/test-submissions
Send a sandbox test submission with {"data": {...}}. It is validated and stored, and every delivery is simulated with its full payload. Like every sandbox submission it runs against the form’s draft, if it has one. Validation errors come back as a 422.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/test-submissions \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"data": {"name": "Ada Lovelace", "email": "ada@example.com"}}'{
"data": {
"id": "01jca9qf3tz5b8m2k7w4r6x0hn",
"sandbox": true,
"source": "api",
"deliveries": [{ "channel": "webhook", "status": "simulated", "payload": { "...": "..." } }],
"...": "..."
}
}GET /api/v1/forms/{form}/submissions
A form’s submissions, newest first. Filters:
| Query | Description |
|---|---|
sandbox |
true or false. Omit for both. |
spam |
true for submissions that failed the base spam check, false for ones that passed. Omit for both. |
failed_rules |
true for submissions that broke the form’s smart rules, false for ones that followed them. Omit for both. |
confirmation |
pending, confirmed or expired, for forms with double opt-in. Omit for all. |
delivery |
Submissions with a delivery that is failed, retrying, or issue for either. Omit for all. |
link_id |
Only submissions made through this personal link. |
search |
Free text matched anywhere in the submission data. |
since |
ISO 8601 date or datetime. Submissions created at or after it. |
until |
ISO 8601 date or datetime. Submissions created at or before it. |
fields[name]=value |
Exact match on a field value. Repeat for more fields. Unknown field names are ignored. |
per_page |
1 to 100, default 25. |
page |
Page number. |
curl -G https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/submissions \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
--data-urlencode "sandbox=false" \
--data-urlencode "since=2026-11-01" \
--data-urlencode "fields[plan]=pro"{
"data": [
{
"id": "01jca9qf3tz5b8m2k7w4r6x0hn",
"form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"link_id": null,
"sandbox": false,
"spam": false,
"failed_rules": false,
"held": false,
"classification": { "spam": false, "rules_broken": false, "checks": { "spam": "passed", "rules": "none" }, "spam_probability": 0.0213, "threshold": 0.8, "rules": [], "reasons": [], "model": "jev-latest", "error": null, "duration_ms": 388 },
"reviewed_at": null,
"confirmation_status": null,
"confirmed_at": null,
"source": "endpoint",
"data": { "name": "Ada Lovelace", "email": "ada@example.com", "plan": "pro" },
"meta": { "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 ...", "referer": "https://yoursite.com/contact", "origin": "https://yoursite.com" },
"deliveries": [{ "id": "01jca9qf4b...", "channel": "webhook", "status": "succeeded" }],
"created_at": "2026-11-06T10:24:00.000000Z"
}
],
"links": { "first": "...?page=1", "last": "...?page=3", "prev": null, "next": "...?page=2" },
"meta": { "current_page": 1, "from": 1, "last_page": 3, "per_page": 25, "to": 25, "total": 61 }
}GET /api/v1/forms/{form}/export
Download a form’s submissions as a file, oldest first. It is streamed, so large exports work. Query parameters:
| Query | Description |
|---|---|
format |
csv (default), json or ndjson. |
sandbox, spam, failed_rules |
true or false. Omit for both. |
confirmation |
pending, confirmed or expired. |
delivery |
failed, retrying, or issue for either. |
link_id |
Only submissions made through this personal link. |
since, until |
Dates or datetimes. |
curl "https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/export?format=csv&sandbox=false" \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-o submissions.csvCSV has the columns id, submitted_at, sandbox, spam, failed_rules, confirmation and link (the link’s label), then one per field, headed by its label. Cells starting with =, +, - or @ get a leading ' so spreadsheets don’t run them as formulas. JSON is one array and NDJSON one object per line, each with id, submitted_at, sandbox, spam, failed_rules, confirmation_status, link_id, data and tracking. File fields become signed download links that work for 30 days. See exports.
GET /api/v1/submissions/{submission}
One submission with its data, metadata (IP, user agent, referer, origin), spam verdict and every delivery including each attempt. spam says whether it failed the base spam check, failed_rules whether it broke the form’s smart rules, and held whether its emails and webhooks are being held because of either flag. classification holds the spam filter’s result (null when the filter did not run) and reviewed_at is when someone last marked it as spam or not spam. link_id is the personal link it came through, with the link’s label, email and metadata in meta.link, otherwise null. confirmation_status is null when the form doesn’t use double opt-in (or there was no address to confirm), otherwise pending, confirmed or expired, and confirmed_at is when it was confirmed.
curl https://YOUR-QUICKFORMS-HOST/api/v1/submissions/SUBMISSION_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": {
"id": "01jca9qf3tz5b8m2k7w4r6x0hn",
"form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"link_id": null,
"sandbox": false,
"spam": false,
"failed_rules": false,
"held": false,
"classification": { "spam": false, "rules_broken": false, "checks": { "spam": "passed", "rules": "none" }, "spam_probability": 0.0213, "threshold": 0.8, "rules": [], "reasons": [], "model": "jev-latest", "error": null, "duration_ms": 388 },
"reviewed_at": null,
"confirmation_status": null,
"confirmed_at": null,
"source": "endpoint",
"data": { "name": "Ada Lovelace", "email": "ada@example.com", "plan": "pro" },
"meta": { "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 ...", "referer": "https://yoursite.com/contact", "origin": "https://yoursite.com" },
"deliveries": [
{ "id": "01jca9qf4bx2c3d4e5f6g7h8j9", "channel": "webhook", "status": "succeeded", "attempts_count": 2, "attempts": ["..."], "...": "..." }
],
"created_at": "2026-11-06T10:24:00.000000Z"
}
}POST /api/v1/submissions/{submission}/review
Mark a submission as spam or not spam with {"spam": true} or {"spam": false}. Returns the submission.
- Marking a flagged submission as not spam clears both
spamandfailed_rulesand releases it: its emails and webhooks are created and delivered. Sandbox submissions are simulated as usual. - Marking a delivered submission as spam only labels it. Anything already delivered stays delivered.
- On a form with double opt-in, marking a flagged submission as not spam sends the confirmation email first. The rest is held until the person confirms.
See flagged submissions.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/submissions/SUBMISSION_ID/review \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"spam": false}'{
"data": {
"id": "01jca9qf3tz5b8m2k7w4r6x0hn",
"form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"link_id": null,
"sandbox": false,
"spam": false,
"failed_rules": false,
"held": false,
"classification": {
"spam": false,
"rules_broken": true,
"checks": { "spam": "passed", "rules": "failed" },
"spam_probability": 0.3127,
"threshold": 0.8,
"rules": [{ "rule": "Must be about bike fitting", "probability": 0.9412, "broken": true }],
"reasons": ["Breaks the rule \"Must be about bike fitting\""],
"model": "jev-latest",
"error": null,
"duration_ms": 412
},
"reviewed_at": "2026-11-06T11:02:00.000000Z",
"confirmation_status": null,
"confirmed_at": null,
"source": "endpoint",
"data": { "name": "Ada Lovelace", "email": "ada@example.com", "message": "..." },
"meta": { "...": "..." },
"deliveries": [
{ "id": "01jcab2r7k...", "channel": "notification", "status": "pending", "...": "..." },
{ "id": "01jcab2r7m...", "channel": "webhook", "status": "pending", "...": "..." }
],
"created_at": "2026-11-06T10:24:00.000000Z"
}
}classification keeps the filter’s original verdict. spam and failed_rules are the current labels.
POST /api/v1/submissions/{submission}/confirm
Double opt-in: confirm a pending submission on the person’s behalf, for example when they confirmed another way. It becomes confirmed, confirmed_at is set and its held emails and webhooks are created and delivered. Returns the submission. Confirming a confirmed submission changes nothing.
It is a 422 on submission with “This submission expired before it was confirmed.” for an expired submission (resend the confirmation email instead), or “This submission is not waiting for confirmation.” for a submission that doesn’t use double opt-in.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/submissions/SUBMISSION_ID/confirm \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": {
"id": "01jca9qf3tz5b8m2k7w4r6x0hn",
"form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"confirmation_status": "confirmed",
"confirmed_at": "2026-11-06T11:15:00.000000Z",
"deliveries": [
{ "id": "01jca9qf4b...", "channel": "confirmation", "status": "succeeded", "...": "..." },
{ "id": "01jcac5t2n...", "channel": "notification", "status": "pending", "...": "..." },
{ "id": "01jcac5t2p...", "channel": "webhook", "status": "pending", "...": "..." }
],
"...": "..."
}
}POST /api/v1/submissions/{submission}/resend-confirmation
Double opt-in: send a new confirmation email with a fresh link. Works on pending and expired submissions, and sets an expired one back to pending, so the expiry clock starts again from the new email. Returns the submission.
It is a 422 on submission with “Only unconfirmed submissions can be sent a confirmation email.” for a confirmed submission or one that doesn’t use double opt-in, or “This submission has no valid email address to confirm.” when the email field has no valid address.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/submissions/SUBMISSION_ID/resend-confirmation \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"Public endpoints
These are not part of /api/v1. They need no API key, because browsers and other people’s websites call them. Every POST route also answers OPTIONS preflight requests.
| Method and path | What it does | Details |
|---|---|---|
POST /f/{form} |
Submit the form. | Submit endpoint |
GET /f/{form} |
The hosted form page. | Hosted forms |
GET /f/{form}/embed |
The form page for an <iframe>. |
Embed |
GET /f/{form}/schema.json |
The published schema the embed widget renders. | Embed |
POST /f/{form}/uploads |
Get a pre-signed URL to upload a file directly. | Direct uploads |
POST /f/{form}/payment |
Check the answers and create a Stripe PaymentIntent, before submitting. | Payments |
POST /f/{form}/progress |
Save progress while someone fills the form in. | Partial submissions |
POST /f/{form}/view |
Count an anonymous view. | Analytics |
GET /f/{form}/altcha |
A fresh Altcha challenge. | Altcha |
POST /f/{form}/uploads
Called from the browser or server that submits the form. Send {"field", "filename", "size", "content_type"} for a file field and get back an upload ID and a pre-signed URL to PUT the file to. Then submit the form as JSON with the upl_... ID as the file field’s value.
curl -X POST https://YOUR-QUICKFORMS-HOST/f/FORM_ID/uploads \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"field": "cv", "filename": "cv.pdf", "size": 204800, "content_type": "application/pdf"}'{
"upload_id": "upl_01jcb4m7x2k9p3q8r5t6v0w1yz",
"url": "https://YOUR-BUCKET-ENDPOINT/submissions/01jc8xk2m5t9qw3e7r4y6u8v0p/01jcb4m7x2k9p3q8r5t6v0w1yz.pdf?X-Amz-Signature=...",
"method": "PUT",
"headers": { "Content-Type": "application/pdf" },
"expires_at": "2026-11-06T10:39:00+00:00"
}The URL works for 15 minutes and the upload ID must be used within 24 hours. Unpublished forms need the X-Quickforms-Sandbox header, and requests are limited to 60 a minute per IP address. See file uploads for the full flow, limits, errors and storage setup.
Deliveries
GET /api/v1/submissions/{submission}/deliveries
Every delivery for a submission (notification, autoresponder, webhook) with its attempts.
curl https://YOUR-QUICKFORMS-HOST/api/v1/submissions/SUBMISSION_ID/deliveries \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": [
{ "id": "01jca9qf4b...", "channel": "notification", "status": "succeeded", "attempts": ["..."], "...": "..." },
{ "id": "01jca9qf4c...", "channel": "webhook", "status": "succeeded", "attempts": ["..."], "...": "..." }
]
}GET /api/v1/deliveries/{delivery}
One delivery with its payload and the full attempt log.
curl https://YOUR-QUICKFORMS-HOST/api/v1/deliveries/DELIVERY_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": {
"id": "01jca9qf4bx2c3d4e5f6g7h8j9",
"submission_id": "01jca9qf3tz5b8m2k7w4r6x0hn",
"channel": "webhook",
"status": "succeeded",
"target": "https://example.com/hooks",
"attempts_count": 2,
"next_retry_at": null,
"delivered_at": "2026-11-06T10:24:12.000000Z",
"last_error": null,
"payload": { "method": "POST", "headers": { "...": "..." }, "body": "..." },
"attempts": [
{ "number": 1, "succeeded": false, "status_code": 503, "response_excerpt": "Service Unavailable", "error": "Endpoint responded with HTTP 503", "duration_ms": 1840, "next_retry_at": "2026-11-06T10:24:12.000000Z", "created_at": "2026-11-06T10:24:02.000000Z" },
{ "number": 2, "succeeded": true, "status_code": 200, "response_excerpt": "{\"received\":true}", "error": null, "duration_ms": 96, "next_retry_at": null, "created_at": "2026-11-06T10:24:12.000000Z" }
],
"created_at": "2026-11-06T10:24:00.000000Z"
}
}POST /api/v1/deliveries/{delivery}/redeliver
Queue a fresh attempt for a live delivery, whatever its state. Responds 202. Sandbox deliveries, and deliveries whose payload has been cleared by retention, return a 422.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/deliveries/DELIVERY_ID/redeliver \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": { "id": "01jca9qf4bx2c3d4e5f6g7h8j9", "status": "pending", "next_retry_at": null, "...": "..." }
}Destinations
Destinations send submissions to Slack, Discord, HubSpot, Attio, Customer.io, Google Sheets or any HTTP endpoint, alongside email and the form webhook. They are delivered, retried and simulated like webhooks, and show up as deliveries with channel destination.
GET /api/v1/destination-types
The destination types: slack, discord, http, hubspot, attio, customerio and google_sheets. Each has a type, label, description, config_fields (each with its key, label, type, required and, where it has them, options, default and help), mapping_targets and custom_mapping. Not paginated.
GET /api/v1/forms/{form}/destinations
A form’s destinations, oldest first. Not paginated.
POST /api/v1/forms/{form}/destinations
Add a destination. type, name (up to 80 characters) and config are required. Optional: enabled (default true), mapping (up to 100 target keys, each a template with {{field}} placeholders; leave it out for the type’s default mapping) and conditions (up to 10 {field, operator, value}, all must match, with operator one of equals, not_equals, contains, is_set or is_empty). A form can have up to 20 destinations. Responds 201.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/destinations \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"type": "slack", "name": "Sales only", "config": {"webhook_url": "https://hooks.slack.com/services/..."}, "conditions": [{"field": "topic", "operator": "equals", "value": "sales"}]}'{
"data": {
"id": "01jcd0m2x4...",
"form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"type": "slack",
"service": "Slack",
"name": "Sales only",
"enabled": true,
"config": { "webhook_url": "••••••••••••••••••••••••••••••••••••••••••..." },
"mapping": { "...": "..." },
"conditions": [{ "field": "topic", "operator": "equals", "value": "sales" }],
"created_at": "2026-11-06T10:00:00.000000Z",
"updated_at": "2026-11-06T10:00:00.000000Z"
}
}Secret config values are write-only and come back masked, showing only their last 4 characters. Validation errors are a 422 on the key, such as config.access_token, mapping.email (a required mapping target left empty), conditions.0.field (not one of the form’s fields) or type.
GET /api/v1/destinations/{destination}
One destination.
PATCH /api/v1/destinations/{destination}
Update a destination. Only the keys you send change, and type can’t be changed. A blank or missing secret keeps the saved one. Returns the destination.
DELETE /api/v1/destinations/{destination}
Delete a destination. Responds 204.
POST /api/v1/destinations/{destination}/preview
A dry run: exactly what the destination would send, with secrets redacted. Nothing is sent. Send data with sample field values, or submission_id for one of the form’s submissions. With neither it uses the latest submission, or made-up values when there are none.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/destinations/DESTINATION_ID/preview \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"data": {"name": "Grace", "email": "grace@example.com", "message": "Hi"}}'{
"data": {
"would_send": true,
"requests": [{ "method": "POST", "url": "https://api.hubapi.com/...", "headers": { "Authorization": "[redacted]", "...": "..." }, "body": { "...": "..." } }],
"mapped": { "email": "grace@example.com", "firstname": "Grace" },
"submission_id": null
}
}would_send is false when the destination’s conditions don’t match. submission_id is the stored submission used, or null for sample data.
Automation hooks
REST hooks for Zapier, Make and n8n. A subscription is an http destination that sends the submission.created event, so it is retried and logged like any other destination.
POST /api/v1/hooks
Subscribe a URL to a form. form_id and target_url (an https URL, up to 2048 characters) are required. name is optional, up to 80 characters, and defaults to “Automation hook”. Responds 201 with the destination it created.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/hooks \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"form_id": "FORM_ID", "target_url": "https://hooks.zapier.com/hooks/standard/...", "name": "Zapier"}'DELETE /api/v1/hooks/{hook}
Unsubscribe: delete the http destination with this ID. Responds 204.
GET /api/v1/forms/{form}/sample
A sample submission.created event for building a Zap or workflow, as a one-item list. It is the latest submission’s event, or one made up from the schema when the form has none.
{
"data": [
{
"id": "evt_sample",
"type": "submission.created",
"sandbox": true,
"form": { "id": "01jc8xk2m5t9qw3e7r4y6u8v0p", "name": "Contact" },
"submission": { "id": "sample", "data": { "name": "Example", "email": "Example" } }
}
]
}Partial submissions and analytics
GET /api/v1/forms/{form}/partials
A form’s partial submissions, most recently active first. Filter with status (in_progress, abandoned or completed) and sandbox (true or false). Paginated with per_page (1 to 100, default 25) and page.
{
"data": [
{
"id": "01jce1...",
"form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"status": "abandoned",
"sandbox": false,
"data": { "name": "Ada" },
"fields_filled": 1,
"last_field": "name",
"link_id": null,
"submission_id": null,
"meta": { "...": "..." },
"started_at": "2026-11-06T10:00:00.000000Z",
"last_activity_at": "2026-11-06T10:01:00.000000Z",
"abandoned_at": "2026-11-06T10:31:00.000000Z",
"completed_at": null
}
],
"links": { "...": "..." },
"meta": { "...": "..." }
}GET /api/v1/partials/{partial}
One partial submission.
DELETE /api/v1/partials/{partial}
Delete a partial submission. Responds 204.
GET /api/v1/forms/{form}/funnel
Started, completed and abandoned counts from partial submissions over the last days (1 to 365, default 30), and drop_off: the last field people touched before abandoning, most common first. Live only, unless you pass sandbox=true. completion_rate is null when nothing was started.
{
"data": {
"started": 4,
"completed": 1,
"abandoned": 3,
"in_progress": 0,
"completion_rate": 0.25,
"drop_off": [{ "field": "name", "label": "Name", "abandoned": 2 }]
}
}GET /api/v1/forms/{form}/analytics
Anonymous analytics over the last days (1 to 365, default 30): views, unique visitors, starts (live partial submissions), live submissions and conversion rate (submissions over visitors, null with no visitors), a daily series, and the top 10 sources (utm_source), campaigns (utm_campaign) and referrers (domain) behind live submissions. click_ids counts live submissions by ad network click ID. See tracking.
{
"data": {
"days": 7,
"views": 3,
"visitors": 2,
"starts": 0,
"submissions": 2,
"conversion_rate": 1,
"series": [{ "day": "2026-11-06", "views": 3, "visitors": 2, "submissions": 2 }, "..."],
"sources": [{ "value": "newsletter", "submissions": 2 }],
"campaigns": [],
"referrers": [{ "value": "news.example.com", "submissions": 1 }],
"click_ids": { "Google Ads": 1 }
}
}click_ids uses the labels Google Ads (gclid), Meta (fbclid), Microsoft Ads (msclkid), TikTok (ttclid), LinkedIn (li_fat_id) and X (twclid), and leaves out networks with no submissions.
Mail providers
GET /api/v1/mail-providers
The team’s mail providers with masked credentials. Not paginated.
curl https://YOUR-QUICKFORMS-HOST/api/v1/mail-providers \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": [
{ "id": "01jc8y0a7n...", "name": "Main", "driver": "postmark", "is_default": true, "...": "..." }
]
}POST /api/v1/mail-providers
Add a provider. name, driver, credentials and from_address are required; from_name and is_default are optional. The credential keys depend on the provider (see the providers table). Making one the default unsets the previous default.
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/mail-providers \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name": "Main", "driver": "postmark", "credentials": {"api_key": "YOUR_POSTMARK_KEY"}, "from_address": "hello@yourdomain.com", "from_name": "Your company", "is_default": true}'{
"data": {
"id": "01jc8y0a7n...",
"name": "Main",
"driver": "postmark",
"driver_label": "Postmark",
"credentials": { "api_key": "••••••••••••••••••••••••••••••••4f2a" },
"from_address": "hello@yourdomain.com",
"from_name": "Your company",
"is_default": true,
"created_at": "2026-11-06T10:00:00.000000Z"
}
}GET /api/v1/mail-providers/{mailProvider}
One provider with masked credentials.
curl https://YOUR-QUICKFORMS-HOST/api/v1/mail-providers/PROVIDER_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"{
"data": {
"id": "01jc8y0a7n...",
"name": "Main",
"driver": "postmark",
"driver_label": "Postmark",
"credentials": { "api_key": "••••••••••••••••••••••••••••••••4f2a" },
"from_address": "hello@yourdomain.com",
"from_name": "Your company",
"is_default": true,
"created_at": "2026-11-06T10:00:00.000000Z"
}
}DELETE /api/v1/mail-providers/{mailProvider}
Delete a provider. Responds 204. There is no update endpoint: delete and add again to rotate keys.
curl -X DELETE https://YOUR-QUICKFORMS-HOST/api/v1/mail-providers/PROVIDER_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"