xYago APIv1

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

  1. You need an approved sender (panel → API Developer → Senders; see Messages). Verify does not use templates: xYago provides the text.
  2. Enable the Verify product and create an API key with the verify:write scope (and verify:read to look verifications up).

Flow

  1. Your backend calls POST /verify/start with 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.
  2. The user types the code into your app.
  3. Your backend calls POST /verify/check with the verification id and 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
}

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

StatusMeaning
pendingCode sent, waiting for the check. delivery_status: sent → delivered → read.
verifiedCorrect code (no longer usable).
expiredThe 10 minutes passed.
too_many_attempts5 wrong codes.
canceledSuperseded by a newer verification to the same phone.
failedCould 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

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