Verify (one-time codes)
The Verify API makes a one-time code, sends it to a Malawi number and checks what the person typed.
Use Verify for sign-ups, logins and payment confirmations. SMS YANU makes the code, sends it and checks it. Your system keeps only the verification id. The code is stored only as a keyed hash.
Each code sent is one normal SMS, charged at your price. If the gateway refuses it, it is not charged. Checking a code is free.
Start a verification
POST https://api.smsyanu.com/v1/verify/start
Authorization: Bearer yanu_live_YOUR_KEY
Content-Type: application/json| Field | Type | Required | Meaning |
|---|---|---|---|
to | string | Yes | Malawi number, for example +265990000000. |
sender_id | string | No | Sender name, up to 11 characters. |
app_name | string | No | Your name in the default message, up to 40 characters. |
template | string | No | Your own wording, up to 320 characters. It must contain {code}. {minutes} is replaced with the time limit. |
code_length | integer | No | 4 to 8. Default 6. |
ttl_seconds | integer | No | 60 to 1800. Default 300. |
max_attempts | integer | No | Wrong tries allowed, 1 to 10. Default 5. |
test | boolean | No | Simulate. Nothing is sent or charged, and the code comes back in sandbox_code. |
{
"to": "+265990000000",
"app_name": "Shop",
"ttl_seconds": 300
}The default message is: Shop: your verification code is 482913. It expires in 5 minutes. Do not share it.
HTTP 201:
{
"id": "vrf_8f2c0a6e1b7d4c3a9e5f1d20",
"status": "pending",
"to": "+265990****00",
"environment": "live",
"livemode": true,
"attempts": 0,
"attempts_left": 5,
"sends": 1,
"expires_at": "2026-11-07T10:05:00+00:00",
"verified_at": null,
"cost": 20.0,
"currency": "MWK",
"created_at": "2026-11-07T10:00:00+00:00"
}Starting again for the same number replaces the open verification, so only the newest code works. Add an Idempotency-Key header to make a retry safe: the same key returns the first verification and sends nothing.
Check a code
POST https://api.smsyanu.com/v1/verify/check| Field | Type | Required | Meaning |
|---|---|---|---|
id | string | One of the two | The id from start. |
to | string | One of the two | Uses the newest pending verification for this number. |
code | string | Yes | What the person typed. |
test | boolean | No | Set it when the verification was started with test. |
{ "id": "vrf_8f2c0a6e1b7d4c3a9e5f1d20", "code": "482913" }The answer has the same fields as start, plus valid:
{ "id": "vrf_8f2c0a6e1b7d4c3a9e5f1d20", "status": "approved", "valid": true, "attempts": 1, "attempts_left": 4 }Test for valid. A wrong code answers 200 with "valid": false.
| Status | Meaning |
|---|---|
pending | Waiting for the right code. |
approved | The right code was given. |
expired | The time limit passed. |
failed | Too many wrong tries, or the code could not be sent. |
cancelled | Cancelled, or replaced by a newer verification. |
Other calls
| Method | Path | What it does |
|---|---|---|
GET | /v1/verify/{id} | Status of one verification. Does not use a try. |
POST | /v1/verify/{id}/resend | Send a fresh code. |
POST | /v1/verify/{id}/cancel | Cancel a pending verification. |
GET | /v1/verify | List verifications. |
GET | /v1/verify/stats | Totals and conversion for up to 90 days. |
Test without sending
With a yanu_test_ key, or "test": true, nothing reaches a phone and nothing is charged. The code comes back in sandbox_code, so your automated tests can use it. Never show sandbox_code to a user.
Limits
| Limit | Value |
|---|---|
| Codes to one number | 5 in any 10 minutes |
| Codes per account | 1,000 per hour |
| Wait between resends | 30 seconds |
| Sends per verification | 4, counting the first |
Going over a limit answers 429 with verify_rate_limited and a Retry-After header.
Errors
| HTTP | Code | What it means |
|---|---|---|
| 400 | template_missing_code | Your template has no {code}. |
| 400 | id_required | Pass id or to to check. |
| 404 | verification_not_found | No such verification on this account. |
| 409 | verification_closed | Resend on a verification that has finished. |
| 429 | verify_rate_limited | A limit above was reached. |
| 502 | otp_send_failed | The code could not be sent. Nothing was charged. |