Send a code. Verify it.

You send a 6-digit code to your user. They type it. You never see the digits — you hold a reference. Authentication and verification only. Not a broadcast channel.

What you need

An account, a Bearer key, a funded USD balance, and an approved Sender ID. Sends return 403 until that name is approved. Impersonating a bank will not work. We checked.

Programming bar: HTTP + JSON. Persist the reference. Never log the token.

Three channels. One loop.

You pick a channel per send. The code, the expiry, and the verify step stay the same. What changes is the inbox the user opens — and the recipient format you send us.

sms

SMS

A text message. Recipient is E.164 (+15551234567). Price can vary by country. The approved Sender ID is the name in the message.

whatsapp

WhatsApp

Same 6 digits, same verify, different inbox. Recipient is still E.164. If they live in WhatsApp, send them there.

email

Email

Same code, to an inbox. Recipient is an email address. One price, everywhere — email is not billed per country.

Who actually delivers the message is an internal routing decision. You send sms, whatsapp, or email. That is the contract.

See what a send costs

What a send costs

You pay when the send is accepted, in USD. Guessing the code is free. If the wallet cannot cover it, the API returns 400.

Rates will appear here once they are published. Authenticated GET /otp/pricing is the same card.

Country-specific rows win when they exist. Email has one rate.

Don't get charged twice

Retries are a feature. We planned for them.

Your HTTP client will retry. Timeout, 502, a cat on the enter key. Send idempotency_key as shs- plus a UUID. Omit it and we mint one and hand it back.

  • Same key, same recipient, same channel. We return the original reference, status 200, and Idempotency-Replayed: true. No second text. No second charge. That is the point of the header.
  • Same key, different recipient or channel. We reject it. A key names one logical send, not a lucky dip.
  • A new key. A new send. The previous code for that recipient becomes superseded once the new one actually goes out.

Reuse the key on retry. A replay returns 200 and Idempotency-Replayed: true.

POST https://seamtel.com/api/v1/otp/send
{
            "channel": "sms",
            "recipient": "+15551234567",
            "idempotency_key": "shs-550e8400-e29b-41d4-a716-446655440000"
            }

Send the code

POST /otp/send

You get a reference and an expires_at. You do not get the digits. Store the reference. The user reads the code on their phone or in their inbox.

Field Notes
channel sms, whatsapp, or email
recipient E.164 for SMS and WhatsApp. An email address for email.
idempotency_key Optional. shs- + UUID. We mint one if you skip it.

You hold a reference. They hold the digits.

POST https://seamtel.com/api/v1/otp/send
{
            "channel": "sms",
            "recipient": "+15551234567"
            }

Now verify it

POST /otp/verify

Pair the reference with the token they typed. Success is { verified: true }. Failure is 400 with a stable errors.code. “It didn’t work” is not an error model.

errors.code Meaning
invalid_token Wrong code
expired The window passed
superseded A newer code was sent
pending Not ready yet — retry shortly
already_verified Used once. Done.
too_many_attempts Request a new code

Reference plus the token they typed.

POST https://seamtel.com/api/v1/otp/verify
{
            "reference": "550e8400-e29b-41d4-a716-446655440000",
            "token": "123456"
            }

Endpoints, grouped

Same envelope as the rest of the API: { msg, data } on success. Every request: Authorization: Bearer SHS_your_api_key.

Group Method Path Purpose
Account GET /me Balance before a batch
Rate card GET /otp/pricing Prices by channel
Send POST /otp/send Save the reference
Verify POST /otp/verify Pair reference + token

Read the field-by-field schema

Or try a request in the API Reference.