ZevSend Docs
Sign up

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

POST /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

FieldTypeRequiredDescription
tostringyesRecipient phone in E.164 format (+2348012345678).
channelstringnosms. 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

POST /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

FieldTypeRequiredDescription
codestringyesCandidate 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
}
FieldTypeDescription
verifiedbooleantrue on the call that accepted the right code.
statusstringLifecycle state, see below.
remaining_attemptsnumberHow many wrong-code attempts the verification has left before exhaustion.

Lifecycle

StateMeaning
pendingCode sent, awaiting a check.
verifiedCorrect code presented. Terminal.
expired10-minute window lapsed without a successful check. Terminal.
exhausted5 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

StatusCodeCause
400invalid_phoneto isn’t a valid E.164 number.
400insufficient_creditZev Credit balance can’t cover the verify rate. Top up + retry.
403feature_not_on_planYour plan doesn’t include Verify. Upgrade to a paid plan.
404verification_not_foundWrong id on the check endpoint, or the verification was on a different team.
403team_suspendedThe team is suspended. Contact support.

Updated at, Thursday, October 1, 2026