Headless Forms
Create a backend-only Makeform form, then POST submissions to its HTTPS endpoint from your own website, app, or server. Responses land in your Makeform inbox and can run the form's enabled notifications and integrations.
Private preview — Headless creation is currently available only to selected creators. Free includes 1 enabled endpoint and 50 Headless submissions per month. Pro and Business include unlimited enabled endpoints and Headless submissions. Respondent auto-reply is paid-only.
Headless-native forms do not have a hosted /f/... page. The endpoint is their
public collection surface.
Quick start
- From Create form, choose Headless endpoint. Name it and add the website domain that will submit.
- Makeform creates a Headless-native form and endpoint. You'll get a URL like:
https://www.makeform.ai/api/headless/f/hf_xxxxxxxxxxxxxxxx- Confirm every browser origin you use (for example,
https://example.comandhttps://www.example.com) or generate an API key for server-side use. - In Setup, copy an example generated from your published fields. An enabled endpoint is ready to receive requests; Completed receipts confirms whether requests have actually been stored. Refresh after your first test.
- Use Responses for the inbox and Integrations for owner notifications, paid respondent auto-reply and connected services.
Official script
The script preserves entered answers on failure, focuses invalid fields, and handles retries with a submission key. It also collects page, referrer and campaign attribution separately from answers.
<script src="https://submit.makeform.ai/embed.js" defer></script>
<form data-makeform="hf_XXXX">
<label>Email <input name="email" type="email" required /></label>
<label>Message <textarea name="message"></textarea></label>
<button type="submit">Send</button>
</form>Use the script URL shown in Setup. Listen for makeform:success and
makeform:error on the form to connect your own UI. The success event's
detail.submissionId is the stored receipt when available. A honeypot response
intentionally does not include a receipt.
Plain HTML
<form action="https://www.makeform.ai/api/headless/f/hf_XXXX" method="POST">
<input type="text" name="name" required />
<input type="email" name="email" required />
<input type="text" name="_gotcha" style="display:none" tabindex="-1" />
<input type="hidden" name="_redirect" value="https://example.com/thanks" />
<button type="submit">Send</button>
</form>JavaScript (fetch)
// Keep this key and body for retries of this submission.
const submissionKey = crypto.randomUUID()
const answers = { name: 'Ada', email: '[email protected]' }
async function submit() {
try {
const response = await fetch('https://www.makeform.ai/api/headless/f/hf_XXXX', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': submissionKey,
},
body: JSON.stringify(answers),
})
const result = await response.json()
if (!response.ok) {
if (result.code === 'validation') console.error(result.fieldErrors)
else if (result.code === 'rate' || result.code === 'quota') {
console.error(result.error, 'Retry after', result.retryAfter, 'seconds')
} else console.error(result.code, result.error)
return result
}
console.log('Receipt:', result.submissionId)
return result
} catch {
// The response may have been lost after storage. Reuse the same key.
console.error('Could not confirm submission. Keep your answers and retry.')
}
}
await submit()Server-side (cURL + API key)
curl -X POST https://www.makeform.ai/api/headless/f/hf_XXXX \
-H "Authorization: Bearer mfh_live_..." \
-H "Content-Type: application/json" \
-d '{"name":"Ada","email":"[email protected]"}'Authentication
Every request must satisfy one of two modes:
| Mode | How | Use for |
|---|---|---|
| Browser | The request's exact Origin (scheme + host + optional port) matches an Allowed Origin (CORS enforced) | Forms on your own website |
| API key | Authorization: Bearer <key> or X-Makeform-Headless-Key: <key> header | Servers and trusted scripts |
API keys are shown once at creation and can be rotated or revoked in Setup. Rotating immediately invalidates the previous key. Store keys on your server; never embed them in browser JavaScript or a distributable mobile app. If a key is the endpoint's only submission method, revoking it also pauses the endpoint.
Request format
Send application/json or application/x-www-form-urlencoded (what a plain HTML form sends). multipart/form-data is not supported — don't set enctype on your form. Field names must match your form's field names.
Reserved fields (never stored as answers):
| Field | Purpose |
|---|---|
_gotcha | Honeypot. Render it hidden and empty — any value silently drops the submission as spam |
_redirect | URL to redirect the browser to after success (303). Must be on one of your Allowed Origins |
_session_id | Optional session id for linking partial submissions |
_idempotency_key | Optional retry key; equivalent to the Idempotency-Key header |
_attribution | Optional bounded metadata envelope; never creates learned answer fields |
_email_verification | Email verification receipts (see Email verification) |
cf-turnstile-response (or _turnstile) | Cloudflare Turnstile token, when Turnstile is enabled |
Validation
Submissions are validated against your published form:
- Loose mode (new Headless forms): unknown fields are accepted, stored, and learned into the Headless-native form schema. Primitive arrays are learned as multi-value fields; complex values are safely normalized before storage. Useful when the payload evolves, but review learned fields before switching to strict mode. Newly learned JSON numbers and booleans retain their types on later submissions; older learned text fields keep their existing string behavior.
- Strict mode: unknown fields are rejected; required fields, types and
configured choices are enforced. Inspect Published schema and review the
field and choice list before locking. A stale schema version returns
409so you can review changes first. Empty strict schemas reject all answer keys.
Learning is bounded to 100 fields and 100 observed choices per learned choice field. If an accepted payload exceeds those bounds, Setup warns that the schema is incomplete; switching to strict mode requires acknowledging that only the listed fields and choices will be accepted. Learning and locking serialize on the same database rows. A valid request can teach fields before storage; if storage then fails, those fields remain available for the retry. Requests already validated against loose mode can still finish while the owner switches modes. Locking freezes the reviewed schema; it is not a cancellation mechanism for submissions already in progress.
Validation failures return 400, code: "validation", an errors array of
{ field, message }, and a fieldErrors map from field names to messages.
Use fieldErrors for inline UI. It is derived from the same validation errors;
the errors array remains for compatibility with existing clients.
Attribution
{
"email": "[email protected]",
"_attribution": {
"page": "https://example.com/contact",
"referrer": "https://example.com/",
"utm": { "utm_source": "newsletter", "utm_campaign": "launch" }
}
}Page/referrer contain only the origin and path of an absolute HTTP(S) URL.
Credentials, query strings and fragments are removed before collection and
again on the server; invalid, unsupported or over-8,192-character input URLs are
omitted. Stored page/referrer values are capped at 2,048 characters; campaign
values at 256 characters.
The accepted tracking keys are utm_source, utm_medium, utm_campaign,
utm_term, utm_content, gclid, fbclid, and msclkid. Unknown metadata keys
are discarded. For HTML, use names such as _attribution[utm_source] and
_attribution[page]. Ordinary business fields named page or utm_source
remain ordinary answers and must match the schema in strict mode.
Safe retries
Send a random Idempotency-Key header or _idempotency_key field, 8–128 ASCII
letters, digits, dots, underscores, colons or hyphens. If both are supplied,
they must match. Generate a new key when the answers change; reuse it when the
outcome is uncertain. Keep attribution, session and redirect values unchanged.
Concurrent submissions with the same endpoint, key and payload share one
stored submission and one receipt. Reusing a key with different payload values
returns 409 / idempotency_conflict. Deleted receipts cannot be restored by
replaying a request. Current endpoint permissions are checked on every retry.
Without a key, separate requests can create separate submissions.
An unfinished keyed attempt retains its payload identity for 24 hours so a
retry cannot teach different fields before the first attempt is resolved.
Completed submissions retain their receipt identity beyond that window. If an
attempt never completed, use a new key for a different payload.
This prevents duplicate stored submissions and duplicate integration scheduling
from normal concurrent retries. It does not guarantee external delivery across
server crashes. Webhooks include a stable deliveryId and submissionId for
recipient deduplication; inspect their Events log for observed outcomes.
Email verification
On the Business plan, a Headless form can require respondents to confirm an
email address with a 6-digit code before the submission is accepted. During
the private preview, verified fields are chosen with Makeform's Headless
management tool (update_headless_endpoint with
verified_email_fields: ["email"], in its own call); a Setup control is not
available yet. Each listed field becomes a required Email field. Send [] to
turn verification off.
Verification is browser-only: codes can be requested and checked only from an
Allowed Origin. The proof is bound to that exact origin, the browser session
and the address, so a code checked on https://example.com cannot complete a
submission sent from https://www.example.com. While any field requires
verification, API-key (server) submissions are rejected with 403 and
code: "email_verification".
With the official script, mark your own elements. The script adds no styling:
<script src="https://submit.makeform.ai/embed.js" defer></script>
<form data-makeform="hf_XXXX">
<label>Email <input name="email" type="email" required data-makeform-verify-email /></label>
<button type="button" data-makeform-send-code>Send code</button>
<label>Code <input data-makeform-code-input inputmode="numeric" autocomplete="one-time-code" maxlength="6" /></label>
<button type="button" data-makeform-verify-code>Verify</button>
<label>Message <textarea name="message"></textarea></label>
<button type="submit">Send</button>
<p data-makeform-status role="status" aria-live="polite"></p>
</form>Leave the code input without a name. The verify button is optional:
submitting with a typed code checks it first. When a form verifies several
addresses, give each button and code input the email field name, for example
data-makeform-send-code="work_email". Verified inputs receive
data-makeform-verified="true". Listen for makeform:code-sent,
makeform:email-verified and makeform:verification-error on the form;
override the default copy with data-code-sent, data-email-verified and
data-verification-error on the form.
Custom clients call the same endpoints from the browser:
| Step | Request |
|---|---|
| Send | POST /f/hf_XXXX/email-verification/request { field, email, sessionId, requestId, attemptSecret? } |
| Check | POST /f/hf_XXXX/email-verification/verify { field, email, sessionId, attemptSecret, challengeId, code, requestId } |
| Submit | Answers plus _session_id and _email_verification: { attemptSecret, receipts: [{ fieldId, receipt }] } |
Generate one random sessionId and one random attemptSecret (32–256
characters) per submission and send both with every call; if you omit
attemptSecret, reuse the one returned by the first send. Use a new session
and secret after a successful submission.
Send responses include fieldId, challengeId, expiresAt and resendAfter;
check responses include fieldId and receipt. Failures return
success: false and a code such as CODE_INVALID, CODE_EXPIRED,
CODE_LOCKED, RESEND_TOO_SOON or RATE_LIMITED (with retryAfter).
A submission without valid proof returns code: "email_verification", a
verificationCode, and fieldErrors naming the email field.
Spam protection
- Honeypot (default): include the hidden
_gotchafield; bots that fill it are silently dropped (the request still returns success, so bots learn nothing). - Cloudflare Turnstile (optional): add your Turnstile site key + secret in Headless settings, then include the widget's
cf-turnstile-responsetoken in browser submissions. API-key (server) requests skip Turnstile. - Rate limits apply to every endpoint.
Responses
| Status | Meaning |
|---|---|
200 | { "ok": true, "submissionId": "..." } — stored receipt; honeypot success deliberately omits it |
303 | Success with _redirect — browser is redirected |
400 | Validation failed — body includes errors, or the form is not published |
401 / 403 | Missing/invalid API key, origin not allowed, or an API-key request to an email-verified form |
422 | Email verification proof is missing or no longer matches the address (email_verification) |
404 | Endpoint doesn't exist or is disabled |
409 | Retry key conflicts with another payload, or a schema version changed during configuration |
429 | Rate limited — or the Free plan's 50 submissions/month reached (the error message says which) |
500 / 503 | Service could not finish or confirm the request; preserve answers and reuse the same retry key |
Errors include ok: false, error, and a stable code. Codes include auth,
endpoint, validation, captcha, unsupported, rate, quota, service,
email_verification, idempotency_key_invalid, idempotency_conflict, and
idempotency_unavailable.
Rate/quota responses include Retry-After and retryAfter in seconds. A quota
failure needs the billing owner to upgrade or wait for the UTC monthly reset;
repeated immediate retries will not help.
Limits
| Free | Pro / Business | |
|---|---|---|
| Enabled endpoints | 1 | Unlimited |
| Submissions / month | 50 | Unlimited |
| Rate limits | Standard | Standard |
The monthly quota counts completed Headless submissions across personal forms
and forms owned by your teams during the UTC calendar month. When a Free
account hits it, requests return 429 until the month rolls over or the billing
owner upgrades.
FAQ
Do integrations fire for Headless submissions? Enabled integrations and owner notifications run through the normal submission service. Owner notification is enabled by default at creation and can be opted out. Respondent auto-reply is available on Pro and Business. Headless entries are tagged with their source in your inbox.
Can I use one endpoint for several websites? Yes — add each site's origin to Allowed Origins.
File uploads? Not yet supported over the headless endpoint — use a hosted or embedded form for file fields.
CORS errors in the browser? The exact origin (scheme + host + port) must be listed in Allowed Origins. https://www.example.com and https://example.com are different origins.
The endpoint returns 404 even though the URL looks correct? The endpoint
may be disabled, deleted, owned by an account outside the current preview, or
attached to a suspended/deleted form. A disabled endpoint deliberately looks
missing.
The endpoint returns 401 or 403 from a server? Do not send a browser
Origin header from server code. Send a current mfh_live_... key in either
supported authentication header, and rotate the key if it may have leaked.
Can I submit multipart data? No. Send JSON or URL-encoded fields. File
uploads, multipart/form-data, and file-field payloads are not supported.