Contact Verification (OTP + PIN)
Contact verification proves that the person you are talking to really is a given contact. You start a verification, SendSeven sends a 6-digit one-time code by email, SMS or WhatsApp (or asks for the contact's PIN), and you check what the person enters.
When a verification is approved, SendSeven:
- marks the proven email address or phone number as verified on the contact,
- attaches a newly proven address or number to the contact when it was not on record yet,
- and, when the verification belongs to a conversation, links that conversation to the verified contact.
The same verifications power the live chat widget's secure code input and the start_verification tool of Conversational Agents.
A customer writes on WhatsApp from a new number and asks about their contract. The agent sends a one-time code to the email address on record, and once the code is confirmed it can answer with the account details your context sources provide. See Customer context for how verification fits with the other context sources.
Scopes
| Scope | Allows | OAuth apps |
|---|---|---|
verifications:create | Start, check, resend and cancel verifications | Yes |
verifications:read | Read a verification's status | Yes |
contact_pins:manage | Set, replace and remove PINs, reset PIN lockouts | No (API keys and the app only) |
verification_settings:update | Change the verification settings | Yes |
Reading the settings needs settings:read. Owners and Admins have all four scopes; the Developer role has verifications:create and verifications:read.
Configure the methods
curl -X PUT https://api.sendseven.com/api/v1/verification-settings \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled_methods": ["email", "sms", "whatsapp", "pin"],
"email_sender": "generic",
"sms_sender": "generic"
}'
| Field | Values |
|---|---|
enabled_methods | Any of email, sms, whatsapp, pin. Default ["email"]. |
email_sender | generic (a neutral SendSeven sender using your company name) or the id of one of your mailboxes. |
sms_sender | generic (sent from your prepaid SMS balance with a neutral sender ID) or the id of one of your SMS channels. |
whatsapp_channel_id, whatsapp_template_id | The WhatsApp channel and the approved authentication template used for codes. |
allowed_countries | ISO 3166 alpha-2 country codes (for example ["DE", "AT", "CH"]) that SMS and WhatsApp codes may be sent to. null resets to the default: your workspace country. |
agent_new_number_daily_cap | How many SMS / WhatsApp codes the AI agent may send per day to phone numbers that are not on file (0–200). 0 turns these sends off; null resets to the default of 10. The limit of one such code per conversation always applies. |
Fields you leave out are kept. GET /verification-settings returns the same object plus whatsapp_template_name, whatsapp_template_status, allowed_countries (the list in effect, including the default), allowed_countries_is_default, agent_new_number_daily_cap (the value in effect), agent_new_number_daily_cap_is_default, sms_country_cap and eu_countries (the 27 EU member states, handy for an "add all EU countries" action).
Destination countries and number types
Codes by SMS and WhatsApp are only sent to phone numbers that pass these rules:
| Rule | SMS | |
|---|---|---|
Country is in allowed_countries | Required | Required |
Country is in the EU or EEA (sms_country_cap: the EU member states plus Iceland, Liechtenstein and Norway) | Required | Not required |
| Number is a valid number | Required | Required |
| Number is a mobile number | Required | Not required |
So a country outside the EU/EEA, such as US, can be on your list for WhatsApp codes, but SMS codes never go there. "Mobile" means numbers that are mobile, or that may be mobile in countries where landline and mobile numbers look the same. Landlines, premium-rate, shared-cost, toll-free, VoIP, pager and personal numbers never receive SMS codes. The check uses the numbering plan of each country (number metadata). There is no live lookup with the carrier, so a ported number is judged by its original number range.
Email codes are not country-restricted.
WhatsApp authentication template
WhatsApp codes need an approved template in Meta's Authentication category. SendSeven can create a compliant one for you (copy-code button, security recommendation, 10-minute expiry):
curl -X POST https://api.sendseven.com/api/v1/verification-settings/whatsapp-template \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"channel_id": "CHANNEL_ID", "language": "en"}'
Response: {"whatsapp_template_id", "template_id", "name", "status"}. Meta reviews the template; codes can be sent once its status is approved.
Start a verification
curl -X POST https://api.sendseven.com/api/v1/verifications \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id": "CONTACT_ID", "method": "email", "locale": "de"}'
Pass exactly one of contact_id or identifier:
{"identifier": {"type": "email", "value": "[email protected]"}, "method": "email"}
| Field | Notes |
|---|---|
method | email, sms, whatsapp or pin |
identifier.type | email, phone or whatsapp (must suit the method; pin accepts any) |
conversation_id | Optional. Links the result to this conversation. |
sender | Optional tenant or generic to override the settings for this send. |
channel_id | Optional sending channel (or mailbox for email). |
locale | Optional language of the code message, e.g. de. |
Where the code goes. The code is always sent to a contact method on record:
- With
contact_id: that contact's matching email or phone number (verified first, then primary, then newest). If there is none, the request fails with422 no_method_on_record. - With an
identifierthat belongs to a contact: that contact's stored method. The verification then proves that contact's identity. - With an
identifierthat is not on record: the code goes to the supplied value (proof of possession). On approval it is attached to the conversation's contact as a verified method.
The response never contains the destination address or a masked hint:
{
"id": "7c1e…",
"status": "pending",
"method": "email",
"expires_at": "2026-10-08T10:10:00Z",
"resend_available_at": "2026-10-08T10:01:00Z",
"contact_id": "CONTACT_ID",
"attempts_remaining": 5
}
For pin, nothing is sent; the verification waits for the PIN.
Check the code
curl -X POST https://api.sendseven.com/api/v1/verifications/VERIFICATION_ID/check \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
For a PIN verification send {"pin": "4711"} instead.
{"id": "7c1e…", "status": "approved", "attempts_remaining": 5, "contact_id": "CONTACT_ID"}
status | Meaning |
|---|---|
approved | Correct code or PIN. |
pending | Wrong code; attempts_remaining went down by one. |
locked | The 5th wrong attempt. Start a new verification. |
expired | The code is older than 10 minutes. Start a new verification. |
Checking is free. Spaces and dashes in the code are ignored.
Resend, cancel, status
POST /verifications/{id}/resendsends a new code for the same verification (the old code stops working, validity restarts at 10 minutes). Allowed 60 seconds after the last send, at most 5 times. Errors:429 resend_too_soon(withretry_afterin seconds),409 resend_limit_reached,409 not_pending.POST /verifications/{id}/cancelcancels a pending verification.GET /verifications/{id}returns the verification (verifications:read).
A conversation has at most one pending verification: starting a new one cancels the previous one.
Customer identification PINs
A PIN is a 4–12 digit secret per contact, set by your team or your backend. It is stored hashed and can never be read back.
| Request | Scope | Response |
|---|---|---|
PUT /contacts/{id}/pin with {"pin": "4711"} | contact_pins:manage | {"success": true, "id"} |
DELETE /contacts/{id}/pin | contact_pins:manage | {"success": true, "id"} |
POST /contacts/{id}/pin/reset-lockout | contact_pins:manage | {"success": true, "id"} |
GET /contacts/{id}/pin | contact_pins:manage or contacts:read | {"is_set", "locked", "locked_until", "escalated", "set_at"} |
After 5 consecutive wrong PINs the PIN is locked for 1 hour: even the correct PIN is refused, and locked_until tells you when the lock ends. The PIN unlocks automatically after that hour.
If the PIN gets locked a 3rd time within 24 hours, the lock is escalated: escalated is true, locked_until is null, and the PIN stays locked until your team or your backend resets it with reset-lockout. SendSeven sends the contact_pin.lockout_escalated webhook and notifies the workspace members who can manage PINs in the app.
Self-unlock with a code
A contact can also unlock an escalated PIN themselves with a one-time code sent to one of their verified email addresses or phone numbers. Start a verification with "purpose": "pin_unlock":
curl -X POST https://api.sendseven.com/api/v1/verifications \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id": "CONTACT_ID", "purpose": "pin_unlock", "method": "pin_unlock_auto"}'
methodisemail,sms,whatsapporpin_unlock_auto. Withpin_unlock_autoSendSeven picks the first verified method in the order email, SMS, WhatsApp, considering only the methods you enabled and your destination rules.- The code only ever goes to a verified method on record. An
identifieronly selects the contact; the code is never sent to the value you pass. - Check the code as usual with
POST /verifications/{id}/check. When it isapproved, the lockout is cleared and SendSeven sends thecontact_pin.self_unlockedwebhook. The response has"purpose": "pin_unlock". - Unlocking does not verify the person's identity, and contacts are never merged or moved. To verify identity afterwards, check the PIN or a code with the normal
identitypurpose. - Errors:
409 pin_not_locked(the PIN is not locked until reset) and409 no_verified_method(no verified email or phone number can receive the code). - The same attempt, resend and rate limits apply as for any code.
In the Live Chat widget and with the AI agent, the visitor always gets the same answer ("If we have verified contact details on file, we sent a code"), whether or not a code was actually sent. A code is only sent when the PIN is locked until reset and a verified method exists.
reset-lockout clears the lock, the wrong-attempt counter and the 24-hour lockout history. Setting a new PIN also clears all of it. PIN endpoints are not available to OAuth apps.
Webhooks
| Event | When |
|---|---|
verification.sent | A code was sent (also on resend). |
verification.approved | Correct code or PIN. |
verification.failed | Wrong code or PIN (reason: wrong_code / wrong_pin), or the code could not be sent (reason is the error code). |
verification.expired | A pending verification expired. |
verification.locked | Locked after 5 wrong attempts. |
contact_pin.lockout_escalated | A contact's PIN was locked for the 3rd time within 24 hours and now stays locked until reset. data: {"contact_id", "lockout_count"}. |
contact_pin.self_unlocked | The contact unlocked their PIN with a code sent to a verified email or phone number. data: {"contact_id", "method"}. |
{
"id": "evt_…",
"type": "verification.approved",
"created_at": "2026-10-08T10:02:11Z",
"tenant_id": "…",
"data": {
"verification_id": "7c1e…",
"method": "email",
"status": "approved",
"contact_id": "…",
"conversation_id": null,
"source": "api",
"attempts": 0
}
}
Payloads never contain the code, the PIN or the email address / phone number.
When a code is not sent because of the destination rules, verification.failed carries reason country_not_allowed, not_mobile or invalid_number.
Billing
Each code sent is billed as one message on its channel, at your plan's message price (SMS additionally uses your prepaid SMS balance; WhatsApp authentication templates are charged by Meta on your WhatsApp Business account). Checking codes and PINs is free.
Limits
| Limit | Value |
|---|---|
| Code validity | 10 minutes |
| Attempts per verification | 5 |
| Resends | 5 for API requests, 2 for verifications started by the AI agent or the Live Chat widget; at least 60 seconds apart |
| Codes per workspace | 30 per minute, 1,000 per day |
| Codes per address / number | 5 per hour, 10 per day |
| Verifications per conversation | 3 code verifications and 5 PIN verifications per hour |
| Failed PIN checks | 10 per conversation per hour, 10 per contact per hour |
| Codes the AI agent or widget sends to addresses not yet on record | 50 per workspace per day; beyond that no code is sent, but the visitor still sees a pending verification and you receive verification.failed |
| SMS / WhatsApp codes the AI agent sends to numbers not yet on record | 10 per workspace per day by default (setting agent_new_number_daily_cap, 0–200) and 1 per conversation (same behavior when exceeded) |
| Wrong PINs from one visitor for one contact | 5 per 24 hours; after that the visitor's PIN attempts for that contact are refused and no longer count toward a lockout, so one visitor can lock a PIN at most once |
| PIN lockout | 5 consecutive wrong PINs lock the PIN for 1 hour; the 3rd lock within 24 hours keeps it locked until reset |
Codes started by the AI agent or the Live Chat widget never reveal whether an address could be reached: if sending fails, or the number is not allowed by your destination rules, the visitor still sees a pending verification, and you receive the verification.failed webhook.
Errors
Error bodies look like {"detail": {"code": "...", "error_code": "...", "error": "...", "message": "..."}}.
| Status | Code | When |
|---|---|---|
| 402 | insufficient_sms_balance | Generic SMS without enough prepaid SMS balance |
| 404 | contact_not_found, conversation_not_found, verification_not_found, pin_not_set | |
| 409 | no_whatsapp_auth_template | No approved WhatsApp authentication template configured |
| 409 | channel_unavailable | The sending channel or mailbox is inactive |
| 409 | not_pending, resend_limit_reached | Resend not possible |
| 409 | pin_not_locked | pin_unlock: the contact's PIN is not locked until reset |
| 409 | no_verified_method | pin_unlock: no verified email or phone number can receive the code |
| 422 | invalid_request | Not exactly one of contact_id / identifier |
| 422 | invalid_identifier, method_identifier_mismatch | The identifier is malformed or cannot receive this method |
| 422 | method_not_enabled | The method is not in enabled_methods |
| 422 | no_method_on_record | The contact has no matching email / phone |
| 422 | destination_not_allowed | The number does not pass the destination rules. reason: country_not_allowed, not_mobile (SMS only) or invalid_number |
| 422 | invalid_agent_new_number_daily_cap | Settings: must be an integer from 0 to 200, or null |
| 422 | invalid_allowed_countries | Settings: allowed_countries must be a non-empty list of ISO 3166 alpha-2 codes, or null |
| 422 | invalid_pin | A PIN must be 4–12 digits |
| 422 | invalid_email_sender, invalid_channel, invalid_template | Settings: email_sender is not generic or an active mailbox; sms_sender / template is not valid |
| 429 | rate_limited, resend_too_soon | See limits; retry_after in seconds |
| 502 | send_failed | The code could not be sent; the verification is failed (API requests only) |