SMS PIN verification
The start to verify handshake that lets a phone-verified visitor submit an SMS PIN form.
SMS PIN verification
When a form's login_modes includes sms_pin, an unauthenticated visitor proves they control a phone number before submitting. The flow is a two-call handshake: start sends a 6-digit code by SMS, verify checks it and returns a smsPinToken you attach to the submit.
Flow
- Start - POST a phone number plus a Turnstile token. The API generates a 6-digit code, stores it for 10 minutes, and texts it to the phone via the owner's SMS key.
- Verify - POST the phone number and the code. On success you get
smsPinToken. - Submit - call
/submitwithsmsPinTokenandsmsPinPhone. The token both satisfies thesms_pinlogin mode and exempts the submit from captcha.
The verified marker is good for 30 minutes after verify, so do the submit promptly.
Start verification
https://forms.hep.gg/api/v1/forms/public/:slug/sms/startPublicOnly works when the form accepts sms_pin. A Cloudflare Turnstile token is mandatory on every start. The endpoint is rate limited per form and per IP: a burst limit of 3 sends per 60 seconds and a daily limit of 12 sends per 24 hours.
phone^\+?[0-9]{7,16}$ (an optional leading +, then 7 to 16 digits). Use full international format, for example +14155550101.turnstileTokencurl -X POST https://forms.hep.gg/api/v1/forms/public/my-form/sms/start \
-H "Content-Type: application/json" \
-d '{ "phone": "+14155550101", "turnstileToken": "<cf-turnstile-response>" }'await fetch("https://forms.hep.gg/api/v1/forms/public/my-form/sms/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
phone: "+14155550101",
turnstileToken: "<cf-turnstile-response>",
}),
});
// { ok: true, data: { sent: true } }{ "ok": true, "data": { "sent": true } }Start errors
| Status | When | error |
|---|---|---|
| 400 | Phone fails the format check | Invalid phone number |
| 400 | Turnstile token missing or rejected | Captcha verification failed |
| 400 | Form does not accept sms_pin | This form doesn't support SMS PIN |
| 404 | Form not found | Form not found |
| 429 | Burst limit (3 / 60s); includes Retry-After | Slow down. Try again in Ns. |
| 429 | Daily limit (12 / 24h) | Too many verification codes from this connection today. |
| 502 | The SMS send failed downstream | Could not send verification code |
| 503 | The form owner has no enabled SMS key | Form owner hasn't set up SMS |
Verify the code
https://forms.hep.gg/api/v1/forms/public/:slug/sms/verifyPublicConfirms the 6-digit code. On success the code is consumed, a 30-minute verified marker is set, and you receive the smsPinToken to carry into the submit.
phonestart.codeverifiedtrue on success.smsPinTokensmsPinToken in the submit body. Also pass the phone as smsPinPhone.# 1. Verify
curl -X POST https://forms.hep.gg/api/v1/forms/public/my-form/sms/verify \
-H "Content-Type: application/json" \
-d '{ "phone": "+14155550101", "code": "123456" }'
# 2. Submit with the token returned above
curl -X POST https://forms.hep.gg/api/v1/forms/public/my-form/submit \
-H "Content-Type: application/json" \
-d '{
"data": { "your_name": "Ada" },
"smsPinToken": "+14155550101",
"smsPinPhone": "+14155550101"
}'const base = "https://forms.hep.gg/api/v1/forms/public/my-form";
const verify = await fetch(`${base}/sms/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone: "+14155550101", code: "123456" }),
}).then((r) => r.json());
if (!verify.ok) throw new Error(verify.error); // e.g. "Code is invalid or expired"
await fetch(`${base}/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: { your_name: "Ada" },
smsPinToken: verify.data.smsPinToken,
smsPinPhone: "+14155550101",
}),
});Verify errors
| Status | When | error |
|---|---|---|
| 400 | Phone or code missing | Phone and code required |
| 401 | Code wrong or expired (10-minute window) | Code is invalid or expired |
| 404 | Form not found | Form not found |