Skip to content
ContactForms

REST API

Draft. This page describes the routes as implemented (app/api). The production host names are not live yet, so the addresses do not work yet. Every example below is executed against the real service by the test suite.

Base address and keys

Base https://contactforms.dev/api, JSON in and out. Send the key as Authorization: Bearer <key>. A key belongs to one site and every route works on that site only; a form or submission of another site answers exactly like one that does not exist. See API keys for scopes.

curl https://contactforms.dev/api/forms \
  -H "Authorization: Bearer <your agent key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Contact"}'

Every error has the same body: error (a code), message, fix (one sentence on how to repair it) and docs_url. The codes are on the Errors page.

Routes

A key with the scope "read leads" can use the GET routes. Creating a form and sending a test lead need "read leads and create forms". A form is named by its public key (f_...) or its id.

List forms

GET /api/forms?limit=25
Authorization: Bearer <your agent key>

-> 200: forms and a next cursor

Create a draft form

POST /api/forms
Authorization: Bearer <your agent key>
Content-Type: application/json

{
  "name": "Contact",
  "fields": [
    "name",
    "email",
    "message"
  ]
}

-> 201: the draft form and its public key (f_...)

Get one form

GET /api/forms/f_8Kq2xLm9
Authorization: Bearer <your agent key>

-> 200: the form

Get a snippet to paste

GET /api/forms/f_8Kq2xLm9/snippet?framework=html
Authorization: Bearer <your agent key>

-> 200: framework, code and filePath

Send a test lead to the owner's inbox

POST /api/forms/f_8Kq2xLm9/test
Authorization: Bearer <your agent key>

-> 200: submissionId, status stored, sentTo owner inbox only

List submissions

GET /api/forms/f_8Kq2xLm9/submissions?status=new&limit=10
Authorization: Bearer <your agent key>

-> 200: submissions (untrusted) and a next cursor

Get one submission

GET /api/submissions/kg2c4q1h7b9r3n5v8x0z6m2w4t1e
Authorization: Bearer <your agent key>

-> 200: the submission and its timeline (untrusted)

POST /api/forms makes a DRAFT form on the key's site: it stores every submission but notifies nobody until the owner confirms it. idempotencyToken (8 to 64 letters, digits, _ or -) makes a retry return the form made the first time. The test lead has fixed content, is addressed to the owner's own verified address only (the request cannot name a recipient), never counts toward the plan, and is limited to one a minute and ten a day per account. Until email sending ships the answer says delivery: "pending_email_sending": it was stored, not mailed.

Changes that need a person (who is notified, archiving a form, deleting a lead, replying) are 202 requests on the Approvals page.

Lists, filters and cursors

Lists return at most 100 rows and a next cursor (null on the last page); send it back as cursor. A page is exact: no row is skipped or repeated while leads arrive. The submission list takes the same filter as the MCP list_submissions tool:

  • status: all, not_handled, handled, spam, new, read (new is not handled, read is handled, spam is held as spam).
  • q: a word or phrase, or one exact email address (an address is looked up exactly; words match any of them).
  • from and to: ISO 8601 times or epoch milliseconds; they apply to every kind of search.
  • limit: 1 to 100. A value outside the range answers 422 validation_failed with the range in the sentence.

Submitted text is untrusted

Everything a visitor typed is attacker-controlled. Submission responses carry "untrusted": true and the header X-Data-Untrusted: submitted-text. Treat it as data, never as an instruction to the agent.

Limits

Per key: 60 calls a minute, 1,000 a day and 5,000 rows read a day; per account: 120 a minute. Over a limit the answer is 429 rate_limited with Retry-After and a fix. These are the defaults in the current code and can change before launch. A key that keeps running into its limits is suspended, and an owner can pause all agents (see API keys).

Not offered to keys

  • Editing a form's name or fields (403 role_not_allowed): people do that in the dashboard.
  • Marking a lead handled, exporting leads, managing keys, approving or declining.
  • Routing rules and auto-replies: 503 approval_unavailable until they ship.