Send OTP codes

Goal: deliver one-time passwords (OTP) for login/verification flows over SMS, with the code body masked in logs and reports.

Prerequisites:

  • An API key with the omni-api scope.
  • An Active SMS Sender ID (or the generic default) — see Register a Sender ID.

How OTP works over SMS

For SMS, you generate and verify the code yourself; the platform delivers it. The flow is:

  1. Your backend generates a random code and stores it with an expiry (e.g. 5 minutes).
  2. You send it via POST /messages with isOtp: true.
  3. The user types the code into your app; your backend compares it against the stored value and expiry.

isOtp: true masks the message body in the platform’s logs and reports, so the code is not exposed in delivery records.

The platform also offers a managed verification flow — where the platform generates the code and checks it for you via POST /messages/verification and POST /messages/revoke — but that flow is specific to the Telegram Gateway channel (channel: 8), not SMS. It is described at the end of this guide.

Steps

1. Generate the code in your backend

Use a cryptographically secure random generator, e.g. a 6-digit numeric code. Store it server-side together with the recipient’s phone number, a creation timestamp, and a TTL. Never accept a code after its TTL, and limit verification attempts.

2. Send the code

curl -sS -X POST "https://client.routeon.io/api/v1/messages" \
  -H "X-API-Key: <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "contact": "905063565285",
    "channel": 1,
    "senderId": "Routeon",
    "isOtp": true,
    "payload": { "text": "Your verification code is 483920. It expires in 5 minutes." },
    "clientInfo": "otp;userId=42"
  }'

Expected response (202 Accepted):

{
  "error": false,
  "data": {
    "channel": 1,
    "transactionId": "95e3e0da-9684-4ba4-88a3-e9207ddeca05",
    "messageId": "6719b5f4-36af-4fba-b111-49ff07f4f96b"
  }
}

Store messageId so you can check delivery if the user reports the code never arrived.

Keep the OTP text short — an all-GSM-7 body under 160 characters is a single segment and a single charge. See Send an SMS for segmentation rules.

3. Verify the code in your backend

When the user submits the code, compare it to the stored value:

  • match and not expired → success;
  • mismatch → count the attempt, reject after a small number of tries;
  • expired → offer to resend (generate a new code; invalidate the old one in your store).

4. Handle "code didn’t arrive"

Before resending, check what happened to the original message (see Track delivery):

curl -sS -H "X-API-Key: <API_KEY>" \
  "https://client.routeon.io/api/v1/edrs?messageIds=6719b5f4-36af-4fba-b111-49ff07f4f96b"

With isOtp: true, the stored body is masked in the record — but mdnStatus, isDelivered, and errorMessage still tell you whether delivery succeeded.

Verify

  • The recipient receives the SMS with the code within seconds.
  • GET /edrs for the messageId shows isDelivered: true (with the body masked).
  • Your backend accepts the correct code and rejects wrong/expired ones.

Managed verification flow (Telegram Gateway only)

If you also deliver OTPs via the Telegram Gateway channel (channel: 8), the platform can generate and verify the code for you:

  1. POST /messages with channel: 8 and payload: { "code_length": 5, "ttl": 300 }, isOtp: true — the platform generates the code and delivers it via Telegram; you get back a messageId.
  2. When the user types the code, call:
curl -sS -X POST "https://client.routeon.io/api/v1/messages/verification" \
  -H "X-API-Key: <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "messageId": "019372a1-d252-7908-9216-028bd36c7af1", "code": "12345" }'

Response:

{
  "success": true,
  "message_id": "019372a1-d252-7908-9216-028bd36c7af1",
  "delivery_status": "read",
  "verification_status": "code_valid",
  "error": ""
}

verification_status values include code_valid, code_invalid, expired, and revoked.

  1. To invalidate an in-flight code early (user cancelled, resend, suspected fraud):
curl -sS -X POST "https://client.routeon.io/api/v1/messages/revoke" \
  -H "X-API-Key: <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "messageId": "019372a1-d252-7908-9216-028bd36c7af1" }'

These two endpoints work only for Telegram Gateway messages — calling them with an SMS messageId returns 404. For SMS, keep code generation and verification in your own backend as described above.

Common failures

Symptom Cause Fix
Code arrives late or not at all Delivery issue on the operator side Check GET /edrs by messageId: UNDELIV = bad/unreachable number, EXPIRED = delivery window ran out, REJECTD = SMSC rejection (sender or destination route)
401 on OTP sends API key missing or without the omni-api scope A missing scope is rejected with 401 before any REJECTD delivery status. See Send an SMS
REJECTD on OTP sends Sender ID not Active, or the destination route is closed This is an SMSC status, not an authorization error. See the checklist in Send an SMS
Code visible in delivery reports isOtp not set on the send Always set "isOtp": true on OTP messages
404 from POST /messages/verification The messageId is not a Telegram Gateway message (e.g. it was an SMS) Use the managed verification endpoints only for channel: 8; verify SMS codes in your own backend
Users retry and receive multiple valid codes Old codes not invalidated on resend Invalidate the previous code in your store whenever you issue a new one