API Reference
Verify (OTP)
POST /v1/verify sends a verification code, POST /v1/verify/:id/check validates the candidate code.
Verify is the OTP channel. Two endpoints: one to send a verification code to a recipient, one to check the code they enter back. The code itself is hashed at rest and never returned in any response. You only learn whether the candidate matched.
Channels
V1 supports sms only. Email and WhatsApp verify share the same
table and lifecycle and will land on this endpoint in a later
release.
Start a verification
/v1/verify curl https://api.zevsend.com/v1/verify \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"to": "+2348012345678",
"channel": "sms"
}'
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
to | string | yes | Recipient phone in E.164 format (+2348012345678). |
channel | string | no | sms. Default: sms. |
Response
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"id": "vrf_01HXYZ...",
"channel": "sms",
"to": "+2348012345678",
"status": "pending",
"sandbox": false,
"expires_at": "2025-05-24T12:44:56.789Z",
"error_message": null
}
Save the id. You’ll pass it back to the check endpoint along
with the code the user entered.
The code expires in 10 minutes and is single-use. After the
expiry the verification flips to expired regardless of whether
the user ever tried.
If the carrier refused the message at submit (invalid number,
network outage, etc.), the response returns status: "failed"
and the error_message field carries the carrier text. You will
not be charged for a failed dispatch.
Check a verification
/v1/verify/:id/check curl https://api.zevsend.com/v1/verify/vrf_01HXYZ.../check \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "code": "847291" }'
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | Candidate code the user entered (4–10 chars). |
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "vrf_01HXYZ...",
"verified": true,
"status": "verified",
"remaining_attempts": 4
}
| Field | Type | Description |
|---|---|---|
verified | boolean | true on the call that accepted the right code. |
status | string | Lifecycle state, see below. |
remaining_attempts | number | How many wrong-code attempts the verification has left before exhaustion. |
Lifecycle
| State | Meaning |
|---|---|
pending | Code sent, awaiting a check. |
verified | Correct code presented. Terminal. |
expired | 10-minute window lapsed without a successful check. Terminal. |
exhausted | 5 wrong codes presented. Terminal. |
Wrong codes increment the attempts counter without flipping
status until the limit. The fifth wrong code flips the
verification to exhausted; further checks return the terminal
state without consuming another attempt.
Pricing
Verify is priced per start call. The rate is a flat amount per
currency, set by the platform. Open the
Pricing and billing page for the
current numbers in your team’s currency.
The matching check call is free. Wrong-code submissions don’t
incur extra charges either. You pay once when the code goes
out, regardless of how the user fares at typing it.
Sandbox verifies are always free. Switch your team to live to start draining credit on Verify sends.
Errors
| Status | Code | Cause |
|---|---|---|
400 | invalid_phone | to isn’t a valid E.164 number. |
400 | insufficient_credit | Zev Credit balance can’t cover the verify rate. Top up + retry. |
403 | feature_not_on_plan | Your plan doesn’t include Verify. Upgrade to a paid plan. |
404 | verification_not_found | Wrong id on the check endpoint, or the verification was on a different team. |
403 | team_suspended | The team is suspended. Contact support. |
Updated at, Thursday, October 1, 2026