Sendblue Verify
Verify phone numbers by asking users to text a code to a Sendblue number
Sendblue Verify lets you confirm that a user can send messages from a phone
number. Your app shows the user a short code and a Sendblue number. The user
texts the code to that number. When Sendblue receives the reply, the
verification is marked approved.
How it works
Section titled “How it works”- Create a Verify Service.
- Create a Verification for the phone number you want to verify.
- Show the returned code and destination number to your user.
- The user texts the code to the destination number.
- Poll the Verification status or receive a webhook when it is approved.
This is an inverted-OTP flow: Sendblue does not send the first OTP message to the user. The user sends the code back to Sendblue.
Base URL
Section titled “Base URL”https://api.sendblue.comAll Verify endpoints are under:
/api/v2/verifyAuthentication
Section titled “Authentication”Verify uses your Sendblue API credentials. Include your API key and secret in the standard Sendblue request headers:
sb-api-key-id: YOUR_API_KEYsb-api-secret-key: YOUR_API_SECRETExamples in this guide use those headers:
curl https://api.sendblue.com/api/v2/verify/services \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET"Services
Section titled “Services”A Verify Service stores configuration for a group of verifications, such as code length, expiration window, and whether your account-assigned lines should be used for verification.
Service SIDs start with SV.
Create a service
Section titled “Create a service”POST /api/v2/verify/servicesRequest body
Section titled “Request body”| Parameter | Type | Required | Description |
|---|---|---|---|
friendly_name | string | Yes | Human-readable name for the service. |
code_length | integer | No | Verification code length, from 4 to 8 characters. Default: 6. |
ttl_seconds | integer | No | How long a verification remains pending, from 60 to 3600 seconds. Default: 300. |
use_customer_lines | boolean | No | If true, Sendblue first uses phone numbers already assigned to your account for verification, then falls back to the shared Verify pool. Default: false. |
PascalCase keys are also accepted:
{ "FriendlyName": "Production login verify", "CodeLength": 6, "TtlSeconds": 300, "UseCustomerLines": false}Example request
Section titled “Example request”curl -X POST https://api.sendblue.com/api/v2/verify/services \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "friendly_name": "Production login verify", "code_length": 6, "ttl_seconds": 300 }'Success response (201)
Section titled “Success response (201)”{ "sid": "SV7a634393b19c141ac07520602f0e38b6", "account_sid": "c6812cd8-dd8f-4558-87db-cc41...", "friendly_name": "Production login verify", "code_length": 6, "ttl_seconds": 300, "use_customer_lines": false, "date_created": "2026-06-26T18:00:00.000Z", "date_updated": "2026-06-26T18:00:00.000Z", "url": "https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6"}List services
Section titled “List services”GET /api/v2/verify/servicesExample request
Section titled “Example request”curl https://api.sendblue.com/api/v2/verify/services \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET"Success response (200)
Section titled “Success response (200)”{ "services": [ { "sid": "SV7a634393b19c141ac07520602f0e38b6", "account_sid": "c6812cd8-dd8f-4558-87db-cc41...", "friendly_name": "Production login verify", "code_length": 6, "ttl_seconds": 300, "use_customer_lines": false, "date_created": "2026-06-26T18:00:00.000Z", "date_updated": "2026-06-26T18:00:00.000Z", "url": "https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6" } ], "meta": { "page": 0, "page_size": 1, "first_page_url": "https://api.sendblue.com/api/v2/verify/services", "previous_page_url": null, "next_page_url": null }}Retrieve a service
Section titled “Retrieve a service”GET /api/v2/verify/services/:service_sidExample request
Section titled “Example request”curl https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6 \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET"Update a service
Section titled “Update a service”POST /api/v2/verify/services/:service_sidOnly include fields you want to change.
Example request
Section titled “Example request”curl -X POST https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6 \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "ttl_seconds": 600, "use_customer_lines": true }'Updates apply to future verifications. Existing pending verifications keep the settings they were issued with.
Delete a service
Section titled “Delete a service”DELETE /api/v2/verify/services/:service_sidDeleting a service prevents new verifications from being issued with that service. Existing verification history is retained.
Example request
Section titled “Example request”curl -X DELETE https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6 \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET"Success returns 204 No Content.
Verifications
Section titled “Verifications”A Verification represents one attempt to verify one phone number.
Verification SIDs start with VR.
Create a verification
Section titled “Create a verification”POST /api/v2/verify/services/:service_sid/verificationsRequest body
Section titled “Request body”| Parameter | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Phone number being verified, in E.164 format. |
PascalCase is also accepted:
{ "To": "+14155551212"}Example request
Section titled “Example request”curl -X POST https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6/verifications \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "to": "+14155551212" }'Success response (201)
Section titled “Success response (201)”{ "sid": "VR51d9c7dee89c6d7627c318a773bef7e9", "service_sid": "SV7a634393b19c141ac07520602f0e38b6", "account_sid": "c6812cd8-dd8f-4558-87db-cc41...", "to": "+14155551212", "channel": "imessage", "status": "pending", "date_created": "2026-06-26T18:05:00.000Z", "date_updated": "2026-06-26T18:05:00.000Z", "url": "https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6/verifications/VR51d9c7dee89c6d7627c318a773bef7e9", "delivery_target": { "pool_number": "+17865640246", "code": "CB6DJ9", "sms_deep_link": "sms:+17865640246?body=CB6DJ9" }}Show the user:
- the
code - the
pool_number - optionally, a button or QR code using
sms_deep_link
The user should send the code from the phone number in to. A code sent from a
different phone number will not approve the verification.
Check verification status
Section titled “Check verification status”GET /api/v2/verify/services/:service_sid/verifications/:verification_sidExample request
Section titled “Example request”curl https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6/verifications/VR51d9c7dee89c6d7627c318a773bef7e9 \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET"Pending response (200)
Section titled “Pending response (200)”{ "sid": "VR51d9c7dee89c6d7627c318a773bef7e9", "service_sid": "SV7a634393b19c141ac07520602f0e38b6", "account_sid": "c6812cd8-dd8f-4558-87db-cc41...", "to": "+14155551212", "channel": "imessage", "status": "pending", "date_created": "2026-06-26T18:05:00.000Z", "date_updated": "2026-06-26T18:05:00.000Z", "url": "https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6/verifications/VR51d9c7dee89c6d7627c318a773bef7e9", "delivery_target": { "pool_number": "+17865640246", "code": "CB6DJ9", "sms_deep_link": "sms:+17865640246?body=CB6DJ9" }}Approved response (200)
Section titled “Approved response (200)”{ "sid": "VR51d9c7dee89c6d7627c318a773bef7e9", "service_sid": "SV7a634393b19c141ac07520602f0e38b6", "account_sid": "c6812cd8-dd8f-4558-87db-cc41...", "to": "+14155551212", "channel": "imessage", "status": "approved", "date_created": "2026-06-26T18:05:00.000Z", "date_updated": "2026-06-26T18:05:24.000Z", "url": "https://api.sendblue.com/api/v2/verify/services/SV7a634393b19c141ac07520602f0e38b6/verifications/VR51d9c7dee89c6d7627c318a773bef7e9"}For recently approved verifications, to is returned while the live
verification record is still available. Later terminal-state lookups may return
to: null; use the approved webhook if your backend needs the verified phone
number asynchronously.
Possible statuses:
| Status | Meaning |
|---|---|
pending | Waiting for the user to text the code. |
approved | The expected phone number sent the correct code. |
expired | The verification was not approved before its expiration time. |
canceled | The verification was canceled by Sendblue operational action. |
Recommended UI flow
Section titled “Recommended UI flow”After creating a Verification:
- Display the code clearly.
- Display the destination number clearly.
- Offer a “Text code” button using
delivery_target.sms_deep_link. - Poll the status endpoint every few seconds while the verification is pending.
- Stop polling when the status becomes
approved,expired, orcanceled. - Prefer webhooks for backend systems that need reliable asynchronous updates.
Example copy:
Text CB6DJ9 to +1 (786) 564-0246 from the phone number you want to verify.Account-assigned lines
Section titled “Account-assigned lines”By default, Verify uses Sendblue’s shared verification pool.
If your account has assigned verification numbers and your service has
use_customer_lines: true, Sendblue first uses your own active numbers for
verification. If no assigned number is available, it falls back to the shared
pool.
Using account-assigned lines is useful when you want the same lines used for verification to stay available for ongoing conversations with your users.
Webhooks
Section titled “Webhooks”You can configure a Verify webhook to receive terminal verification events.
Webhook configuration uses the existing Sendblue account webhook API:
POST /api/account/webhooksOnly one Verify webhook URL is supported per account.
Add a Verify webhook
Section titled “Add a Verify webhook”curl -X POST https://api.sendblue.com/api/account/webhooks \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "type": "verify", "webhooks": ["https://example.com/sendblue/verify"] }'Success response (200)
Section titled “Success response (200)”{ "status": "OK", "message": "Webhooks added successfully", "webhooks": { "receive": [], "verify": [ { "url": "https://example.com/sendblue/verify", "secret": "whsec_..." } ] }}Store the returned secret. It is used to verify webhook signatures and is also
available in the Sendblue dashboard UI in your webhook settings.
Replace a Verify webhook
Section titled “Replace a Verify webhook”Use PUT /api/account/webhooks with a verify array:
curl -X PUT https://api.sendblue.com/api/account/webhooks \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "webhooks": { "verify": ["https://example.com/sendblue/verify"] } }'If the URL already had a secret, Sendblue preserves it so your signature verification does not break unexpectedly.
Webhook events
Section titled “Webhook events”| Event | Description |
|---|---|
verification.approved | The user texted the correct code from the expected phone number. |
verification.expired | The verification expired before approval. |
verification.canceled | The verification was canceled by Sendblue operational action. |
Approved payload:
{ "event": "verification.approved", "verification_id": "VR51d9c7dee89c6d7627c318a773bef7e9", "phone": "+14155551212", "completed_at": "2026-06-26T18:05:24.000Z"}Expired payload:
{ "event": "verification.expired", "verification_id": "VR51d9c7dee89c6d7627c318a773bef7e9", "completed_at": "2026-06-26T18:10:00.000Z"}phone is included only for verification.approved.
Webhook signatures
Section titled “Webhook signatures”If a secret is configured, Sendblue signs each Verify webhook with:
X-Sendblue-Signature: t=<unix_timestamp>,v1=<signature>The signature is:
HMAC_SHA256(secret, `${timestamp}.${raw_request_body}`)Verify the signature against the raw request body before parsing JSON.
Node.js example:
import crypto from "crypto";
function verifySendblueSignature(rawBody, signatureHeader, secret) { const parts = Object.fromEntries( signatureHeader.split(",").map((part) => { const [key, value] = part.split("="); return [key, value]; }) );
const timestamp = parts.t; const signature = parts.v1; if (!timestamp || !signature) return false;
const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex");
if (signature.length !== expected.length) return false;
return crypto.timingSafeEqual( Buffer.from(signature, "hex"), Buffer.from(expected, "hex") );}Recommended replay protection: reject signatures whose timestamp is more than 5 minutes old.
Rate limits and capacity
Section titled “Rate limits and capacity”Sendblue applies protective limits to keep the Verify system healthy:
- A short per-account create burst limit protects against accidental loops.
- A per-line active pending cap spreads load across available Sendblue numbers.
- Repeated wrong-code replies to the same Sendblue number can temporarily cool that number down.
Valid matching codes are not blocked by the wrong-code cooldown.
If a request is rate-limited, the response includes HTTP 429 and may include a
Retry-After header.
If no verification line is currently available, the API returns 503.
Error format
Section titled “Error format”Verify errors use this error envelope:
{ "code": 60200, "message": "Invalid request.", "more_info": "https://docs.sendblue.com/errors/60200", "status": 400}Common errors:
| HTTP | Code | Message | Meaning |
|---|---|---|---|
| 400 | 60200 | Invalid request. | Request body or parameters are invalid. |
| 401 | 20003 | Authentication failed. | API credentials are missing or invalid. |
| 404 | 60002 | Company not found for credentials. | The API key is not linked to a company. |
| 404 | 60201 | Service not found. | Service SID does not exist, was deleted, or belongs to another account. |
| 404 | 60223 | Verification not found. | Verification SID does not exist or belongs to another service/account. |
| 429 | 60207 | Rate limit exceeded; please retry later. | A protective burst limit was exceeded. |
| 503 | 60226 | Verify service pool unavailable; no active pool worker. | No eligible verification line is currently available. |
| 503 | 60227 | Verify code generation failed; please retry. | Code collision retries were exhausted. |
| 500 | 20500 | Internal server error. | Unexpected server error. |
End-to-end example
Section titled “End-to-end example”Create a service:
SERVICE=$(curl -s -X POST https://api.sendblue.com/api/v2/verify/services \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{"friendly_name":"Login Verify","code_length":6,"ttl_seconds":300}')
SERVICE_SID=$(echo "$SERVICE" | jq -r '.sid')Issue a verification:
VERIFICATION=$(curl -s -X POST "https://api.sendblue.com/api/v2/verify/services/$SERVICE_SID/verifications" \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" \ -H "Content-Type: application/json" \ -d '{"to":"+14155551212"}')
echo "$VERIFICATION" | jq '.delivery_target'Show the user the returned code and destination number.
Poll status:
VERIFICATION_SID=$(echo "$VERIFICATION" | jq -r '.sid')
curl "https://api.sendblue.com/api/v2/verify/services/$SERVICE_SID/verifications/$VERIFICATION_SID" \ -H "sb-api-key-id: YOUR_API_KEY" \ -H "sb-api-secret-key: YOUR_API_SECRET" | jq .When the user texts the correct code from the expected phone number, the status
changes to approved.
- The user must text the code from the phone number being verified.
- The verification code is only valid during the service’s TTL window.
- Sendblue does not expose a “submit code” endpoint for this flow. Approval happens when Sendblue receives the inbound message from the user.
- Use webhooks when your backend needs asynchronous notification.
- Use polling when your frontend needs to update a waiting screen.