OTP verification

Confirm that a user owns a phone number with a one-time code.

How it works

  1. Your server calls POST /v1/otp/send with the user’s number. Nopexo creates a verification and texts a code.
  2. You store the requestId (e.g. in the user’s session) and ask the user for the code.
  3. Your server calls POST /v1/otp/verify with the requestId and what the user typed.
  4. On verified: true, mark the number as verified. Otherwise show the error and let them retry or resend.

Turn OTP on first

OTP must be enabled for the project (Project → Settings → Services). Defaults — 6 digits, 5-minute expiry, 5 attempts, 60 seconds between sends, 3 sends — can be changed in Verify → Settings.

Send a code

curl -X POST "https://api.nopexo.com/v1/otp/send" \
  -H "Authorization: Bearer $NOPEXO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "to": "0200000009",
    "purpose": "login",
    "reference": "user_42"
  }'

purpose labels the verification (login, signup, payment…) for your analytics; reference is your own ID, returned with the result and in webhooks.length (4–10) and expiresIn (60–1800 seconds) override your settings per request.

Check the code

curl -X POST "https://api.nopexo.com/v1/otp/verify" \
  -H "Authorization: Bearer $NOPEXO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "otp_5hj3k8m2n9p4q7r1",
    "code": "123456"
  }'
ResultHTTPWhat to do
verified: true200Success. The verification is closed; the code can’t be reused.
OTP_INCORRECT422Wrong code. Show attemptsRemaining and let them try again.
OTP_MAX_ATTEMPTS429Too many wrong codes. Start a new verification.
OTP_EXPIRED410Too late. Start a new verification.
OTP_ALREADY_VERIFIED409Already used. Don’t verify again.
OTP_PHONE_BLOCKED429Too many wrong codes for this number recently; blocked for an hour.

Only trust the API

Decide on the server, from the verify response. Never compare codes in your app, and never send the code to the browser.

Resend

After resendAvailableAt, and while sendsRemaining is above zero, you can send a new code for the same verification. The previous code stops working. Each send is charged.

curl -X POST "https://api.nopexo.com/v1/otp/resend" \
  -H "Authorization: Bearer $NOPEXO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "otp_5hj3k8m2n9p4q7r1"
  }'

Check status

curl -X GET "https://api.nopexo.com/v1/otp/otp_5hj3k8m2n9p4q7r1" \
  -H "Authorization: Bearer $NOPEXO_API_KEY"

Security built in

  • Codes come from a cryptographically secure generator. Nopexo stores only a keyed hash, never the code, and never returns it outside sandbox.
  • Each check uses one attempt atomically, so parallel guesses can’t exceed the limit, and only one request can ever succeed.
  • Limits per number (per project and across Nopexo), per project and per key stop SMS pumping and brute force.
  • Optional network restrictions and a daily OTP spending cap (Verify → Settings).
  • Suspicious activity — limits hit, blocked numbers, spending caps — is listed in Verify → Overview.

Billing

Each code sent (including resends) is one SMS at the OTP price for the destination. Checking codes is free. Sandbox is free.