Verify respondent email

Use respondent email verification to confirm that a respondent owns an email address before a Headless form accepts the submission. You can enable it for up to five email fields on a Business plan.

Beta

This feature is available to all accounts. Its request and response format can change without a new API version. Do not depend on it for anything you cannot update quickly.

Enable verification

Use the MCP tool update_headless_endpoint with verified_email_fields. Send this setting alone in its call, along with the form_id identifying the form. Verification is available on the Business plan and is not available in the dashboard.

{ "form_id": "FORM_ID", "verified_email_fields": ["email"] }

Each value names an email field to verify. You can include up to five fields.

Verification flow

Verification works in the browser. The endpoint must allow the browser’s origin, and the code request, code verification, and form submission must all come from that same origin. Use the endpoint ID returned by update_headless_endpoint in the Headless URLs; it is different from the form ID.

  1. Create a random sessionId of at most 256 characters and keep it for the attempt.
  2. Request a code for the email field.
  3. Verify the six-digit code using the challengeId and attemptSecret returned by the request.
  4. Submit the form with the same sessionId, the attemptSecret, and the verification receipt.

Both verification endpoints accept JSON objects up to 8 KB. Their responses are not cacheable.

Example

This browser example requests and verifies a code, then submits the form with its receipt. In your page, collect the code from the respondent before calling the verify endpoint.

const endpoint = "https://submit.makeform.ai/f/ENDPOINT_ID"; const sessionId = "mfs_4c1d9a7e2b6f4f0e9a3d5c7b1e8f2a60"; const email = "[email protected]"; async function postJson(url, body) { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const result = await response.json(); if (!response.ok) throw new Error(result.error || result.code); return result; } const challenge = await postJson( `${endpoint}/email-verification/request`, { field: "email", email, sessionId, requestId: "mfr_91b7c3e5d2a84f6fb0c1d2e3f4a5b6c7", }, ); // Collect the six-digit code from the respondent before continuing. const code = document.querySelector("#verification-code").value; const verification = await postJson( `${endpoint}/email-verification/verify`, { fieldId: challenge.fieldId, email, sessionId, attemptSecret: challenge.attemptSecret, challengeId: challenge.challengeId, code, requestId: "mfv_5d3e1f7a9b2c4d6e8f0a1b2c3d4e5f60", }, ); const submission = await postJson(endpoint, { name: "Ada Lovelace", email, _session_id: sessionId, _email_verification: { attemptSecret: challenge.attemptSecret, receipts: [ { fieldId: verification.fieldId, receipt: verification.receipt, }, ], }, });

Keep the attemptSecret, fieldId, and receipt from the responses for the attempt. A receipt is opaque; do not parse it. It is valid only for the verified address until its expiresAt. If the respondent changes the address, request and verify a code for the new address.

The email field in the submission must match the verified address. _session_id and _email_verification are reserved keys and are not stored as answers. For multiple verified fields, include one receipt entry per field, up to 20 entries.

Errors

CodeStatusWhat to do
endpoint404Check that the endpoint exists and is enabled.
auth403Check that the request has an allowed Origin.
unsupported415Send JSON with Content-Type: application/json.
validation400 or 413Send a JSON object no larger than 8 KB.
INVALID_REQUEST400Provide a sessionId no longer than 256 characters.
CODE_INVALID400Check the code and required verification values.
CODE_EXPIRED410Request a new code.
CODE_LOCKED423Request a new code.
EMAIL_RISK_BLOCKED422The address was rejected and no code was sent.
VERIFICATION_VALUE_CHANGED422Restart verification with the same address, session, and origin.
VERIFICATION_POLICY_CHANGED409Drop stored fieldId values and start again using field.
UNSUPPORTED_VERIFICATION_POLICY409Check the form’s verification setup.
DELIVERY_UNAVAILABLE, VERIFICATION_UNAVAILABLE503Retry later.

Requests can be rate limited. The response is 429 with retryAfter; back off by that value. resendAfter in the code request response is the earliest time you can request another code for that address.

While any field requires verification, submitting with a backend key returns 403 with code email_verification and verificationCode EMAIL_VERIFICATION_REQUIRED. Verification requests and the final submission must come from the browser using an allowed origin.