API reference

One POST, a small JSON contract, and no surprises. The endpoint returns an opaque submission id on success and a plain, safe error otherwise: never a spam score, a recipient or a provider detail.

Works on every planUpdated Sep 22, 2026

Endpoint

MethodPathWho calls it
POST/f/{public_form_key}Browsers and any client. The key is a public address, safe in HTML.
POST/v1/forms/{form_id}/submissionsYour server, with a secret API key.

The public endpoint is the everyday one and the default for a new form. Any origin may submit; the honeypot, content scoring and rate limits do the defending. Turn on an allowed-domains list and a request from anywhere else, or with no Origin at all, is refused with origin_not_allowed before it is parsed.

mtform.co and api.maketheform.com are the same service. Endpoints deployed on the longer host keep working; snippets use the short one.

Server-side submissions

Protected mode authenticates with a secret key instead of a public form key, carries a higher rate limit, and is the only way to reach a form set to protected. Create a key under Settings in the dashboard; it is shown once and stored only as a hash.

curl
curl -X POST https://mtform.co/v1/forms/YOUR_FORM_ID/submissions \
  -H "Authorization: Bearer mtf_sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{"email":"someone@example.com","message":"Hello"}'

The origin allow-list and the bot challenge do not apply to an authenticated caller: both are questions about browsers. A key reaches only forms in its own workspace, and a form id belonging to anyone else answers form_not_found, exactly as an invented one does.

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. A browser sending Accept: text/html is redirected with a 303 instead; everything else gets JSON.

Request · curl
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"}'
Response · 201 Created
HTTP/1.1 201 Created

{
  "ok": true,
  "submission_id": "5f0c2b8e-3d1a-4c6e-9b7f-2a8d4e6c1b90",
  "message": "Submission received"
}
Error response · 4xx / 5xx
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": "0b5d7e2c-8f41-4a3e-b9d6-1c2e3f4a5b6c"
}

Errors

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.

CodeHTTPPublic messageWhen
malformed_request400We could not read that request.The body could not be parsed as the declared content type.
unsupported_content_type415That content type is not supported.Content-Type is not one of the three supported types.
payload_too_large413That submission is too large.The body or a field exceeded the size limits below.
validation_failed422Please check the highlighted fields.One or more fields failed validation; see error.fields.
unauthorized401This endpoint requires a valid API key.The form is in protected mode; use /v1 with a Bearer key. Also returned for a missing, malformed or revoked key.
origin_not_allowed403This form does not accept submissions from that address.The request Origin is not in the form’s allowed list.
form_disabled403This form is not currently accepting submissions.The form is paused and not accepting submissions.
form_not_found404This form does not exist.No form matches that key. Also returned for a rotated key, revealing nothing.
challenge_failed403We could not verify that submission. Please try again.The bot challenge (Turnstile) did not verify server-side.
rate_limited429Too many submissions. Please wait a moment and try again.Too many requests from this IP and form. Respect the Retry-After header.
quota_exceeded429This form has reached its submission limit.The workspace reached its monthly ceiling: the plan limit on Free, twice the limit on paid plans with overage.
idempotency_conflict409That idempotency key was already used with a different submission.The Idempotency-Key was reused with a different payload.
service_unavailable503We could not accept that submission. Please try again.A transient failure accepting the submission. Safe to retry.

Limits and idempotency

Rate limits

LimitValue
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

LimitValue
Max body (excl. uploads)256 KB
Max fields100
Max field name128 bytes
Max field value20 KB
Max total text200 KB
Max JSON depth3

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 honoured for 24 hours.

Generate the key per form load, not per session. A key reused across two genuine submissions turns the second one into a replay of the first.

How a submission is processed

Ordered cheap to expensive, so abuse is rejected before it costs anything. Once the transaction commits, the submission exists and nothing after the commit can turn it back into an error.

  1. Resolve the form by key
  2. Check the form is enabled and the workspace is active
  3. Rate limit, before quota, so an attacker cannot burn your allowance
  4. Origin policy, when the mode requires it
  5. Content type and size limits
  6. Parse, and split out the reserved control fields
  7. Honeypot
  8. Turnstile, verified server-side
  9. Field validation
  10. Content scoring
  11. Quota check
  12. Commit the submission and its outbox row in one transaction