Skip to content

API

The same API the dashboard runs on. Read submissions into your own systems, create forms from a script, or wire up a CI check.

Create a key at Settings, API keys (organization owners and admins). Choose its permissions (Read only, Full access, or custom per area), which workspaces it reaches (all, or a list), and when it expires. The secret starts with fw_live_ and is shown once; what you see afterwards is its name and prefix. Revoke a key from the same page and it stops working at once.

Every call a key makes is recorded in the workspace’s audit log with the key’s name.

All paths below are relative to https://api.formwire.com/v1. Send the key as a bearer token on every request. Request and response bodies are JSON.

curl https://api.formwire.com/v1/forms \
-H "Authorization: Bearer fw_live_…"
Response
{
"data": [
{
"object": "form",
"id": "2a9d1f3e-5b6c-4d7e-8f90-a1b2c3d4e5f6",
"workspace_id": "5d0c8e1f-…",
"name": "Contact",
"status": "active",
"access_key": "a3f1c2d4-…",
"website_url": "https://example.com",
"version": 3,
"settings": { "…": "…" },
"recipients": [ "…" ],
"unread_count": 2,
"created_at": "2026-10-01T09:12:44.000Z"
}
],
"has_more": false,
"next_cursor": null
}

A key that reaches one workspace needs nothing more. A key that reaches several names the workspace on each request with an Fw-Workspace header; leave it out and the error’s details lists the workspaces the key can reach.

curl https://api.formwire.com/v1/forms \
-H "Authorization: Bearer fw_live_…" \
-H "Fw-Workspace: 5d0c8e1f-…"

For a link that should work anywhere, such as a spreadsheet import or a browser tab, a read-only key can go in the address as ?api_key= on GET requests instead of the header. Add &workspace= when the key reaches more than one workspace. Addresses end up in browser history and logs, so prefer the header from code.

curl "https://api.formwire.com/v1/forms/2a9d1f3e-…/submissions?api_key=fw_live_…"

Submissions are listed per form, one folder at a time, newest first, with every field value as stored.

curl "https://api.formwire.com/v1/forms/2a9d1f3e-5b6c-4d7e-8f90-a1b2c3d4e5f6/submissions?folder=inbox&limit=50" \
-H "Authorization: Bearer fw_live_…"
Response
{
"data": [
{
"object": "submission",
"id": "7c1e9b2a-4f3d-4e8a-9b1c-2d5f6a7b8c9d",
"form_id": "2a9d1f3e-5b6c-4d7e-8f90-a1b2c3d4e5f6",
"source": "form",
"folder": "inbox",
"spam_reason": null,
"fields": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"message": "Hello"
},
"submitter_email": "ada@example.com",
"submitter_name": "Ada Lovelace",
"subject": "New submission to Contact",
"page_url": "https://example.com/contact",
"ip_address": "203.0.113.7",
"user_agent": "Mozilla/5.0 …",
"attachments": [],
"read_at": null,
"starred_at": null,
"created_at": "2026-10-14T13:53:19.000Z"
}
],
"has_more": true,
"next_cursor": "7c1e9b2a-4f3d-4e8a-9b1c-2d5f6a7b8c9d"
}
Parameter Meaning
folder inbox (default), spam or archive.
q Full-text search over field values, up to 200 characters.
submitter_email Only submissions from this address.
since Only submissions after this ISO 8601 time.
unread true for unread only.
starred true for starred only.
deleted true lists the form’s trash instead.
preview_chars Shorten each field value to about this many characters (20 to 2000).
limit 1 to 250 per page; 50 by default.
cursor The next_cursor from the previous page.
Unread submissions mentioning an invoice since 1 October
curl "https://api.formwire.com/v1/forms/2a9d1f3e-…/submissions?q=invoice&since=2026-10-01T00:00:00Z&unread=true" \
-H "Authorization: Bearer fw_live_…"

GET /forms/{form_id}/submissions/count takes the same filters and returns how many match.

Every list returns data, has_more and next_cursor. While has_more is true, send next_cursor back as cursor to get the next page. IDs are time-ordered, so a page stays stable while new submissions arrive. Lists that are always returned whole, such as a form’s webhooks, come back with has_more: false.

curl "https://api.formwire.com/v1/forms/2a9d1f3e-…/submissions?cursor=7c1e9b2a-4f3d-4e8a-9b1c-2d5f6a7b8c9d" \
-H "Authorization: Bearer fw_live_…"
curl https://api.formwire.com/v1/submissions/7c1e9b2a-4f3d-4e8a-9b1c-2d5f6a7b8c9d \
-H "Authorization: Bearer fw_live_…"

The response is one submission object as above. Uploaded files are listed in attachments, each with a signed download_url that works for an hour; request the submission again for a fresh link. Reading a submission doesn’t mark it read. To mark it read, star it or move it to another folder:

curl -X PATCH https://api.formwire.com/v1/submissions/7c1e9b2a-4f3d-4e8a-9b1c-2d5f6a7b8c9d \
-H "Authorization: Bearer fw_live_…" \
-H "Content-Type: application/json" \
-d '{ "read": true, "starred": true, "folder": "archive" }'

The same filters as the list, as a CSV file with a column per field, newest first, up to 50,000 rows.

curl "https://api.formwire.com/v1/forms/2a9d1f3e-…/submissions/export?folder=inbox" \
-H "Authorization: Bearer fw_live_…" -o contact.csv

A form needs a name. Recipients are optional: recipient_ids are the workspace’s linked emails (see GET /linked_emails); pending ones start receiving once confirmed. The response includes the access key, and GET /forms/{form_id}/snippet returns ready-to-paste code with it filled in.

curl -X POST https://api.formwire.com/v1/forms \
-H "Authorization: Bearer fw_live_…" \
-H "Content-Type: application/json" \
-d '{ "name": "Newsletter signup", "website_url": "https://example.com" }'

Send the version you read with If-Match. If someone changed the form since, nothing changes and you get 412 precondition_failed; read it again and retry.

curl -X PATCH https://api.formwire.com/v1/forms/2a9d1f3e-… \
-H "Authorization: Bearer fw_live_…" \
-H "Content-Type: application/json" \
-H 'If-Match: "3"' \
-d '{ "status": "paused" }'

recipient_ids, cc_recipient_ids and bcc_recipient_ids each replace one list of the form’s notification recipients (To, CC and BCC) and leave the others as they are. An address can be in only one list, and CC and BCC are only sent while the form has a confirmed To recipient. settings.retention_days sets how many days submissions are kept, up to 365. An API key can lengthen it; shortening it deletes older submissions, so that’s done in the dashboard.

The most used ones. The full list, with every parameter and response, is the OpenAPI document at api.formwire.com/v1/openapi.json.

Method Path What it does
GET /workspace The current workspace: plan, region and this month’s usage.
GET /workspace/usage This month’s usage against the plan’s limits.
GET /workspaces The workspaces the key reaches.
GET /forms List forms. ?deleted=true lists the trash.
POST /forms Create a form.
GET /forms/{form_id} One form, with settings, recipients, observed fields and version.
PATCH /forms/{form_id} Change a form. Send its version in If-Match.
DELETE /forms/{form_id} Move a form to the trash.
POST /forms/{form_id}/restore Restore it.
POST /forms/{form_id}/access_key Rotate the access key; the old one works for seven more days.
GET /forms/{form_id}/snippet Embed code (?framework=html, javascript, react, astro, vue or svelte).
GET /forms/{form_id}/fields The fields defined on the form’s Fields tab.
POST /forms/{form_id}/test_submissions Send a sample submission that emails the recipients and calls the webhooks.
GET /forms/{form_id}/origins Sites that have posted to the form, with accepted and blocked counts.
GET /forms/{form_id}/submissions List a form’s submissions (filters above).
GET /forms/{form_id}/submissions/count How many submissions match the filters.
GET /forms/{form_id}/submissions/export CSV of the same.
GET /submissions/recent The newest submissions across the workspace.
GET /submissions/{submission_id} One submission with full fields and attachments.
PATCH /submissions/{submission_id} Mark read or unread, star or unstar, move to a folder.
DELETE /submissions/{submission_id} Move to the trash.
POST /submissions/{submission_id}/restore Restore it.
POST /submissions/bulk One action on up to 100 submissions.
GET /submissions/{submission_id}/deliveries Email and webhook attempts for a submission.
POST /deliveries/{delivery_id}/retry Send a delivery again.
GET /forms/{form_id}/webhooks The form’s webhooks.
POST /forms/{form_id}/webhooks Add a webhook (unconfirmed until confirmed in the dashboard).
PATCH /webhooks/{webhook_id} Switch a webhook off or on, or change its URL.
DELETE /webhooks/{webhook_id} Remove a webhook.
POST /webhooks/{webhook_id}/tests Send a test delivery to it.
GET /linked_emails Addresses the workspace can send to and whether each is confirmed.
POST /linked_emails Add one; it receives mail once its owner clicks the confirmation link.
GET /api_keys The organization’s keys (names and prefixes, never the secret).
GET /audit_events The organization’s audit log.

Some things stay in the dashboard on purpose: creating API keys, confirming a webhook added by a key, and inviting people. See the MCP page for why.

Errors use one envelope. Branch on error.code; message is safe to show a person. Codes you’ll meet: unauthenticated (401, bad or missing key), forbidden (403, the key lacks the permission), not_found (404, including IDs from another workspace), precondition_failed (412, the version in If-Match is out of date), invalid_request (400, with field details) and rate_limited (429).

{
"error": {
"code": "not_found",
"message": "Not found.",
"details": null
}
}

1200 requests a minute per key. Over that, requests get 429 rate_limited; wait and retry. Writes that could create something twice accept an Idempotency-Key header: a retry with the same key returns the first result instead of repeating the write.