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

  1. From Create form, choose Headless endpoint. Name it and add the website domain that will submit.
  2. Makeform creates a Headless-native form and endpoint. You'll get a URL like:
https://www.makeform.ai/api/headless/f/hf_xxxxxxxxxxxxxxxx
  1. Confirm every browser origin you use (for example, https://example.com and https://www.example.com) or generate an API key for server-side use.
  2. 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.
  3. 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:

ModeHowUse for
BrowserThe request's exact Origin (scheme + host + optional port) matches an Allowed Origin (CORS enforced)Forms on your own website
API keyAuthorization: Bearer <key> or X-Makeform-Headless-Key: <key> headerServers 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):

FieldPurpose
_gotchaHoneypot. Render it hidden and empty — any value silently drops the submission as spam
_redirectURL to redirect the browser to after success (303). Must be on one of your Allowed Origins
_session_idOptional session id for linking partial submissions
_idempotency_keyOptional retry key; equivalent to the Idempotency-Key header
_attributionOptional bounded metadata envelope; never creates learned answer fields
_email_verificationEmail 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 409 so 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:

StepRequest
SendPOST /f/hf_XXXX/email-verification/request { field, email, sessionId, requestId, attemptSecret? }
CheckPOST /f/hf_XXXX/email-verification/verify { field, email, sessionId, attemptSecret, challengeId, code, requestId }
SubmitAnswers 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 _gotcha field; 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-response token in browser submissions. API-key (server) requests skip Turnstile.
  • Rate limits apply to every endpoint.

Responses

StatusMeaning
200{ "ok": true, "submissionId": "..." } — stored receipt; honeypot success deliberately omits it
303Success with _redirect — browser is redirected
400Validation failed — body includes errors, or the form is not published
401 / 403Missing/invalid API key, origin not allowed, or an API-key request to an email-verified form
422Email verification proof is missing or no longer matches the address (email_verification)
404Endpoint doesn't exist or is disabled
409Retry key conflicts with another payload, or a schema version changed during configuration
429Rate limited — or the Free plan's 50 submissions/month reached (the error message says which)
500 / 503Service 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

FreePro / Business
Enabled endpoints1Unlimited
Submissions / month50Unlimited
Rate limitsStandardStandard

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.