Verify (codes)
Send one-time verification codes (sign-in, sign-up, confirming an operation) through the xYago app chat, from your verified sender. If the phone does not use xYago you find out immediately and send the code through your other channel (e.g. SMS).
Before you start
- You need an approved sender (panel → API Developer → Senders; see Messages). Verify does not use templates: xYago provides the text.
- Enable the Verify product and create an API key with the
verify:writescope (andverify:readto look verifications up).
Flow
- Your backend calls
POST /verify/startwith the phone number. xYago generates a 6-digit code, valid for 10 minutes with 5 attempts, and sends it to the user's chat (with a "Copy code" button). The response never contains the code. - The user types the code into your app.
- Your backend calls
POST /verify/checkwith the verificationidand the code.
curl -X POST 'https://api.xyago.com/v1/verify/start' \
-H "Authorization: Bearer $XYAGO_API_KEY" \
-H "Idempotency-Key: login-7f3a9c2e-0001" \
-H "Content-Type: application/json" \
-d '{"to": "+573005550123", "sender_id": "tiendaexpress", "language": "en",
"reference": "login-CLI-000123"}'
201 response:
{
"id": "3e8b1f6a-9c2d-4a7e-b5f0-6d1c2a3b4e59",
"object": "verification",
"sender_id": "tiendaexpress",
"to_masked": "+57 ••• ••• 0123",
"channel": "xyago_chat",
"language": "es",
"reference": "login-CLI-000123",
"status": "pending",
"delivery_status": "sent",
"failure_code": null,
"attempts": 0,
"max_attempts": 5,
"expires_at": "2026-11-04T15:10:00Z",
"verified_at": null,
"created_at": "2026-11-04T15:00:00Z"
}
curl -X POST 'https://api.xyago.com/v1/verify/check' \
-H "Authorization: Bearer $XYAGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"verification_id": "3e8b1f6a-9c2d-4a7e-b5f0-6d1c2a3b4e59", "code": "482913"}'
{
"id": "3e8b1f6a-9c2d-4a7e-b5f0-6d1c2a3b4e59",
"valid": true,
"status": "verified",
"attempts_remaining": 0
}
A wrong code returns 200 with valid: false and the attempts
left:
{
"id": "3e8b1f6a-9c2d-4a7e-b5f0-6d1c2a3b4e59",
"valid": false,
"status": "pending",
"attempts_remaining": 4
}
- Codes are single use: after
valid: trueanother check returns409 verification_not_pending. - After the fifth wrong code the verification becomes
too_many_attempts(then409 max_attempts_reached). Expired:409 verification_expired. In both cases start a new one. - Requesting a new code for the same phone supersedes the previous
one (it becomes
canceledand the chat hides its code). language(es,en,de) is optional: by default it follows your account's country (Spanish in Spanish-speaking countries, German in DE/AT/CH/LI, English elsewhere). The text is fixed, e.g. "482913 is your Tienda Express verification code. It expires in 10 minutes. Don't share it with anyone; Tienda Express will never ask for it."
Phones without xYago: your SMS fallback
If the phone does not use xYago, POST /verify/start immediately returns
404 recipient_not_on_xyago: nothing is sent, nothing waits and nothing is
retried. Send the code through your usual channel (SMS, email...). If the user cannot
receive messages from your sender the response is 409 undeliverable (handle
it the same way: use your other channel).
Statuses
| Status | Meaning |
|---|---|
pending | Code sent, waiting for the check. delivery_status: sent → delivered → read. |
verified | Correct code (no longer usable). |
expired | The 10 minutes passed. |
too_many_attempts | 5 wrong codes. |
canceled | Superseded by a newer verification to the same phone. |
failed | Could not be sent (failure_code: recipient_not_on_xyago, undeliverable...). |
Look a verification up with GET /verify/{id}.
Idempotency-Key (required)
POST /verify/start requires the Idempotency-Key header, like
POST /messages. If the response is lost (timeout, 502,
503), repeat it with the same key and the same body: two codes are never
sent at once. If the first one reached the chat you get the same verification; if it did
not, a new one is sent (the previous one stops working), but only once the wait between
codes has passed since the previous attempt, within the limits and while Verify is not
paused (otherwise: 429 rate_limited with Retry-After, or
403 verify_paused; then repeat with the same key). Same key with a different
body: 409 idempotency_key_reused.
Limits
- Between two codes to the same phone (same sender): 30 seconds.
- Per sender and phone: 5 codes per 15 minutes and 10 per day; per phone across all senders: 20 per day; plus a daily cap per sender.
- Wrong codes per sender and phone: 10 per hour (on top of the 5 attempts of each verification).
When a limit is exceeded: 429 rate_limited with Retry-After.
If the limits cannot be checked, POST /verify/start returns
503 limits_unavailable (retry with the same Idempotency-Key);
POST /verify/check keeps working (the hourly wrong-code cap is not applied at
that moment, but the 5 attempts, single use and expiry of each verification are), so a user
holding a valid code is never locked out. If many of your codes go to phones without xYago, xYago
pauses Verify for your sender (403 verify_paused; contact xYago).
Sandbox
With an xy_test_ key real codes are only sent to your
confirmed test numbers
(422 sandbox_recipient_not_allowed otherwise). While your sender is not
approved yet, the code comes from xYago Sandbox with the
[Sandbox · Your sender] prefix.
Webhooks
Subscribe to verification.delivered, verification.read,
verification.verified, verification.expired and
verification.failed (also sent when the attempts run out). data
is the verification (without the code):
{
"created_at": "2026-03-02T15:00:04Z",
"data": {
"id": "3e8b1f6a-9c2d-4a7e-b5f0-6d1c2a3b4e59",
"object": "verification",
"sender_id": "tiendaexpress",
"to_masked": "+57 ••• ••• 0123",
"channel": "xyago_chat",
"language": "es",
"reference": "login-CLI-000123",
"status": "verified",
"delivery_status": "read",
"failure_code": null,
"attempts": 1,
"max_attempts": 5,
"expires_at": "2026-11-04T15:10:00Z",
"verified_at": "2026-11-04T15:01:12Z",
"created_at": "2026-11-04T15:00:00Z"
},
"environment": "live",
"id": "evt_4f9c2b7a1d3e4c5b8a6f0e1d2c3b4a59",
"type": "verification.verified"
}
Good practices
- Never log the code the user types (neither in logs nor in your
database); send it only to
POST /verify/check. - Always check from your backend, never from the user's app with your API key.
- Do not reveal to the user whether their phone uses xYago: show the same message whether the code arrives through xYago or by SMS.