API
reference
One POST, a small JSON contract, and no surprises. The endpoint returns an opaque submission id on success and a dull, safe error otherwise: never a spam score, a recipient, or a provider detail.
One route, one method
mtform.co and api.maketheform.com are the same service. Endpoints already deployed on the longer host keep working; new snippets use the short one because it is the string you paste most.
/f/{public_form_key}The everyday endpoint, and the default for a new form. The key is a public address, safe in client-side HTML. Any origin may submit; the honeypot, scoring, and rate limits do the defending.
/f/{public_form_key}The same URL with an Origin allow-list turned on for the form. A request from anywhere else, or with no Origin at all, is refused with origin_not_allowed before it is parsed.
Protected server-to-server mode, authenticated with a secret key instead of a public form key, is designed but not yet built. A form set to that mode is refused with unauthorized rather than falling back to public.
Request and response
Send application/x-www-form-urlencoded, multipart/form-data, application/json. A new submission is a 201. An Idempotency-Key that replays an earlier one is a 200.
curl -X POST https://mtform.co/f/your-form-key \
-H "Content-Type: application/json" \
-H "Idempotency-Key: contact-form-load-5f3c" \
-d '{"name":"Ada","email":"ada@example.com","message":"Quote please"}'HTTP/1.1 201 Created
{
"ok": true,
"submission_id": "sub_3Nk8Qz1aB7",
"message": "Submission received"
}HTTP/1.1 422 Unprocessable Entity
{
"ok": false,
"error": {
"code": "validation_failed",
"message": "Please check the highlighted fields.",
"fields": { "email": "Enter a valid email address." }
},
"request_id": "req_9fQ2Lp"
}Every code, and what causes it
One shape for all of them: { ok: false, error: { code, message, fields? }, request_id }. Branch on the code, show the message, log the request id.
| Code | HTTP | Public message | When |
|---|---|---|---|
| malformed_request | 400 | We could not read that request. | The body could not be parsed as the declared content type. |
| unsupported_content_type | 415 | That content type is not supported. | Content-Type is not one of the three supported types. |
| payload_too_large | 413 | That submission is too large. | The body or a field exceeded the size limits below. |
| validation_failed | 422 | Please check the highlighted fields. | One or more fields failed validation; see error.fields. |
| unauthorized | 401 | This endpoint requires a valid API key. | The form is in protected mode, which the public endpoint does not serve. |
| origin_not_allowed | 403 | This form does not accept submissions from that address. | The request Origin is not in the form’s allowed list. |
| form_disabled | 403 | This form is not currently accepting submissions. | The form is paused and not accepting submissions. |
| form_not_found | 404 | This form does not exist. | No form matches that key. Also returned for a rotated key, revealing nothing. |
| challenge_failed | 403 | We could not verify that submission. Please try again. | The bot challenge (Turnstile) did not verify server-side. |
| rate_limited | 429 | Too many submissions. Please wait a moment and try again. | Too many requests from this IP and form. Respect the Retry-After header. |
| quota_exceeded | 429 | This form has reached its submission limit. | The form reached its monthly submission ceiling. |
| idempotency_conflict | 409 | That idempotency key was already used with a different submission. | The Idempotency-Key was reused with a different payload. |
| service_unavailable | 503 | We could not accept that submission. Please try again. | A transient failure accepting the submission. Safe to retry. |
What the endpoint will accept
Rate limits
- Burst (per IP + form)
- 5 / 30s
- Sustained (per IP + form)
- 30 / 10 min
- Account ceiling (per form)
- 600 / 10 min
A 429 carries a Retry-After header. Windows are short and recover on their own, so one burst from a shared office IP does not lock out a building.
Payload limits
- Max body (excl. uploads)
- 256 KB
- Max fields
- 100
- Max field name
- 128 bytes
- Max field value
- 20 KB
- Max total text
- 200 KB
- Max JSON depth
- 3
Idempotency
Send an Idempotency-Key header to make retries safe. Replaying the same key returns the original result with a 200. Reusing it with a different payload returns 409, so a bug that reuses keys surfaces instead of overwriting data. Keys are honored for 24 hours.
How a submission is processed
- 01Resolve the form by key
- 02Check the form is enabled and the workspace is active
- 03Rate limit, before quota, so an attacker cannot burn your allowance
- 04Origin policy, when the mode requires it
- 05Content type and size limits
- 06Parse, and split out the reserved control fields
- 07Honeypot
- 08Turnstile, verified server-side
- 09Field validation
- 10Content scoring
- 11Quota check
- 12Commit the submission and its outbox row in one transaction
Copy a working snippet and go.
The HTML, React, Next.js, and cURL integrations in the quickstart already implement everything on this page.