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.
- Create a random
sessionIdof at most 256 characters and keep it for the attempt. - Request a code for the email field.
- Verify the six-digit code using the
challengeIdandattemptSecretreturned by the request. - Submit the form with the same
sessionId, theattemptSecret, and the verificationreceipt.
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
| Code | Status | What to do |
|---|---|---|
endpoint | 404 | Check that the endpoint exists and is enabled. |
auth | 403 | Check that the request has an allowed Origin. |
unsupported | 415 | Send JSON with Content-Type: application/json. |
validation | 400 or 413 | Send a JSON object no larger than 8 KB. |
INVALID_REQUEST | 400 | Provide a sessionId no longer than 256 characters. |
CODE_INVALID | 400 | Check the code and required verification values. |
CODE_EXPIRED | 410 | Request a new code. |
CODE_LOCKED | 423 | Request a new code. |
EMAIL_RISK_BLOCKED | 422 | The address was rejected and no code was sent. |
VERIFICATION_VALUE_CHANGED | 422 | Restart verification with the same address, session, and origin. |
VERIFICATION_POLICY_CHANGED | 409 | Drop stored fieldId values and start again using field. |
UNSUPPORTED_VERIFICATION_POLICY | 409 | Check the form’s verification setup. |
DELIVERY_UNAVAILABLE, VERIFICATION_UNAVAILABLE | 503 | Retry 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.