# 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.

## API keys

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.

## Base URL and authentication

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.

```bash
curl https://api.formwire.com/v1/forms \
  -H "Authorization: Bearer fw_live_…"
```

```json title="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.

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

### A read-only key in the address

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.

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

## List submissions

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

```bash
curl "https://api.formwire.com/v1/forms/2a9d1f3e-5b6c-4d7e-8f90-a1b2c3d4e5f6/submissions?folder=inbox&limit=50" \
  -H "Authorization: Bearer fw_live_…"
```

```json title="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"
}
```

### Filters

| 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. |

```bash title="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.

## Pagination

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`.

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

## Read a submission

```bash
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:

```bash
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" }'
```

## Export

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

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

## Create a form

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.

```bash
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" }'
```

## Change a form

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.

```bash
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.

## Endpoints

The most used ones. The full list, with every parameter and response, is the OpenAPI document at [api.formwire.com/v1/openapi.json](https://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](/docs/mcp) for why.

## Errors

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).

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

## Rate limits and retries

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.