Verify Users with OTP

One-Time Passwords (OTPs) let you verify user identity before granting access or authorizing sensitive actions. Generate a random token, deliver it to the user's phone or email, and confirm they control that device by asking them to submit the token back. OTPs protect accounts from unauthorized access, validate phone numbers during registration, and secure high-risk transactions like withdrawals or account changes.

Understanding OTPs

An OTP is a randomly generated token sent to a user's phone number or email address that expires after a short period. Only someone with access to that device or inbox can retrieve the token. When they submit it back to your application, you've confirmed they control that contact method—proving their identity or authorizing their action.

Unlike passwords, OTPs can't be reused, don't need to be remembered, and expire automatically. The short validity window and attempt limits make them resistant to both phishing and brute force attacks. For authentication flows where you already know the user's phone or email, OTPs provide strong verification without requiring them to manage credentials. Common applications include user authentication, phone number verification, transaction authorization, two-factor authentication, password resets, and step-up authentication for sensitive operations. Contact support if you're uncertain whether OTPs are appropriate for your use case.


How OTP verification works

The verification flow has five steps:

1. Initiate transaction – Call /otp/initiate with the recipient's phone number or email address. Commerce generates a random token, delivers it via SMS, WhatsApp, or email (depending on recipient type), and returns a transaction ID.

2. User receives token – The recipient gets a message like "Your Acme Bank code is 847293. Valid for 10 minutes." They read the code from their device.

3. User submits token – Display an input field where users enter the code they received. Collect the token and send it to your backend along with the transaction ID.

4. Verify submission – Call /otp/verify with the transaction ID, recipient, and submitted token. Commerce checks if the token matches, hasn't expired, and hasn't exceeded attempt limits.

5. Grant access or authorization – On successful verification, proceed with the protected operation—log in the user, approve the withdrawal, activate the account, or complete the sensitive action.

Transaction status

Each OTP transaction has a status that changes as verification progresses:

  • pending – Transaction created, delivery in progress
  • verified – User submitted correct token, verification succeeded
  • expired – Token validity period elapsed, no longer accepted
  • canceled – Transaction explicitly canceled before verification

Built-in protections

OTP transactions include several security protections:

  • Attempt limits prevent brute force attacks by limiting how many times a user can submit incorrect tokens before requiring a new OTP
  • Expiration windows limit how long tokens remain valid, reducing exposure if intercepted
  • Idempotency keys prevent duplicate deliveries during network retries—if you retry with the same key, you get the existing transaction instead of sending multiple codes to the user

Implementing OTP verification

Basic authentication flow

Authenticate users with their phone number and an OTP:

# Step 1: Initiate OTP transaction
curl -X POST https://api.zebo.dev/otp/initiate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "auth_user_12345_20231215_143022",
    "recipient": "+233241234567",
    "sender": "MyApp",
    "service_name": "MyApp",
    "token_alphabet_type": "numeric",
    "token_size": 6,
    "validity_duration_in_minutes": 10,
    "purpose": "login"
  }'

# Response: { "transaction": { "id": "3f8a9c7e2d1b4a6f", ... } }

# Step 2: User receives SMS: "Your MyApp code is 847293. Valid for 10 minutes."
# User enters: 847293

# Step 3: Verify submitted token
curl -X POST https://api.zebo.dev/otp/verify \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "3f8a9c7e2d1b4a6f",
    "recipient": "+233241234567",
    "token": "847293"
  }'

# Success: { "transaction": { "status": "verified", ... } }
# Then grant access

See the OTP API reference for complete endpoint documentation including all request parameters and response formats.


Implementation best practices

Match validity to user context

Choose token expiration based on how actively engaged users are. Login flows work well with 5-10 minutes—users are focused and ready. Phone verification during registration should allow 15 minutes since users might be filling out other form fields. High-security operations like large withdrawals benefit from shorter 5-minute windows.

Provide clear user feedback

Show users how many verification attempts they have remaining after each failed submission. After several failures, guide them to request a new code instead of continuing to guess. Error messages should be actionable: "Code expired. Tap to send a new one" is more helpful than technical error text.

Enable code resend

Users don't always receive messages immediately due to network delays or delivery issues. Let them request a new OTP if needed. Use the idempotency key to prevent accidental duplicate sends during network retries.

Secure transaction identifiers

Transaction IDs are sensitive—treat them like session tokens. Store them server-side associated with the user's session rather than passing them through client-side code or URLs. This prevents attackers from attempting verification on someone else's transaction.

Customize messages for your brand

The default message template is functional but generic. Customize it to match your brand voice and provide context to users. Include your service name and what action they're confirming: "Your Acme Bank login code is {token}. Valid for 10 minutes." Keep messages concise—under 160 characters fits in a single SMS.

Log for security monitoring

Track verification attempts, successes, and failures for security analysis. Unusual patterns like many failed attempts from one source, successful verifications from unexpected locations, or rapid transaction creation can indicate attacks or abuse.

Set meaningful purpose values

Use the purpose parameter to tag transactions for your own analytics: login, phone_verification, withdrawal_auth, passwor_reset. This makes it easy to track conversion rates, debug specific flows, and understand which verification types succeed or fail most often.


Delivery and timing

Multiple delivery channels

OTPs are delivered via SMS, WhatsApp, or email depending on the recipient you provide. Phone numbers receive SMS or WhatsApp, while email addresses receive email. The system automatically determines the delivery method—you don't specify it explicitly.

Typical delivery timing

Most OTP messages arrive within seconds, but delivery timing can vary. Network congestion, carrier delays, or regional routing can extend delivery to a few minutes. Give users adequate time before suggesting a resend—at least 30-60 seconds after initiating the transaction.

International delivery

OTPs work for international recipients. Keep in mind that delivery timing and success rates vary by country and carrier. Test your critical markets separately to understand typical delivery patterns in those regions.

Was this page helpful?