Skip to main content

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.

Pro use case

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​

ScopeAllowsOAuth apps
verifications:createStart, check, resend and cancel verificationsYes
verifications:readRead a verification's statusYes
contact_pins:manageSet, replace and remove PINs, reset PIN lockoutsNo (API keys and the app only)
verification_settings:updateChange the verification settingsYes

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"
}'
FieldValues
enabled_methodsAny of email, sms, whatsapp, pin. Default ["email"].
email_sendergeneric (a neutral SendSeven sender using your company name) or the id of one of your mailboxes.
sms_sendergeneric (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_idThe WhatsApp channel and the approved authentication template used for codes.
allowed_countriesISO 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_capHow 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:

RuleSMSWhatsApp
Country is in allowed_countriesRequiredRequired
Country is in the EU or EEA (sms_country_cap: the EU member states plus Iceland, Liechtenstein and Norway)RequiredNot required
Number is a valid numberRequiredRequired
Number is a mobile numberRequiredNot 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"}
FieldNotes
methodemail, sms, whatsapp or pin
identifier.typeemail, phone or whatsapp (must suit the method; pin accepts any)
conversation_idOptional. Links the result to this conversation.
senderOptional tenant or generic to override the settings for this send.
channel_idOptional sending channel (or mailbox for email).
localeOptional 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 with 422 no_method_on_record.
  • With an identifier that belongs to a contact: that contact's stored method. The verification then proves that contact's identity.
  • With an identifier that 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"}
statusMeaning
approvedCorrect code or PIN.
pendingWrong code; attempts_remaining went down by one.
lockedThe 5th wrong attempt. Start a new verification.
expiredThe 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}/resend sends 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 (with retry_after in seconds), 409 resend_limit_reached, 409 not_pending.
  • POST /verifications/{id}/cancel cancels 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.

RequestScopeResponse
PUT /contacts/{id}/pin with {"pin": "4711"}contact_pins:manage{"success": true, "id"}
DELETE /contacts/{id}/pincontact_pins:manage{"success": true, "id"}
POST /contacts/{id}/pin/reset-lockoutcontact_pins:manage{"success": true, "id"}
GET /contacts/{id}/pincontact_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"}'
  • method is email, sms, whatsapp or pin_unlock_auto. With pin_unlock_auto SendSeven 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 identifier only 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 is approved, the lockout is cleared and SendSeven sends the contact_pin.self_unlocked webhook. 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 identity purpose.
  • Errors: 409 pin_not_locked (the PIN is not locked until reset) and 409 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​

EventWhen
verification.sentA code was sent (also on resend).
verification.approvedCorrect code or PIN.
verification.failedWrong code or PIN (reason: wrong_code / wrong_pin), or the code could not be sent (reason is the error code).
verification.expiredA pending verification expired.
verification.lockedLocked after 5 wrong attempts.
contact_pin.lockout_escalatedA 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_unlockedThe 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​

LimitValue
Code validity10 minutes
Attempts per verification5
Resends5 for API requests, 2 for verifications started by the AI agent or the Live Chat widget; at least 60 seconds apart
Codes per workspace30 per minute, 1,000 per day
Codes per address / number5 per hour, 10 per day
Verifications per conversation3 code verifications and 5 PIN verifications per hour
Failed PIN checks10 per conversation per hour, 10 per contact per hour
Codes the AI agent or widget sends to addresses not yet on record50 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 record10 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 contact5 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 lockout5 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": "..."}}.

StatusCodeWhen
402insufficient_sms_balanceGeneric SMS without enough prepaid SMS balance
404contact_not_found, conversation_not_found, verification_not_found, pin_not_set
409no_whatsapp_auth_templateNo approved WhatsApp authentication template configured
409channel_unavailableThe sending channel or mailbox is inactive
409not_pending, resend_limit_reachedResend not possible
409pin_not_lockedpin_unlock: the contact's PIN is not locked until reset
409no_verified_methodpin_unlock: no verified email or phone number can receive the code
422invalid_requestNot exactly one of contact_id / identifier
422invalid_identifier, method_identifier_mismatchThe identifier is malformed or cannot receive this method
422method_not_enabledThe method is not in enabled_methods
422no_method_on_recordThe contact has no matching email / phone
422destination_not_allowedThe number does not pass the destination rules. reason: country_not_allowed, not_mobile (SMS only) or invalid_number
422invalid_agent_new_number_daily_capSettings: must be an integer from 0 to 200, or null
422invalid_allowed_countriesSettings: allowed_countries must be a non-empty list of ISO 3166 alpha-2 codes, or null
422invalid_pinA PIN must be 4–12 digits
422invalid_email_sender, invalid_channel, invalid_templateSettings: email_sender is not generic or an active mailbox; sms_sender / template is not valid
429rate_limited, resend_too_soonSee limits; retry_after in seconds
502send_failedThe code could not be sent; the verification is failed (API requests only)