One-Time Passwords

One-time passwords (OTPs) let you prove a customer controls a phone number or inbox without asking them to remember a credential. Send a short-lived code to a trusted channel, have the customer echo it back, and you’ve verified possession with minimal friction. OTPs excel at account sign-in, step-up checks before risky actions, and validating contact details during onboarding.

Prerequisites

  • API key with access to the OTP service
  • Customer phone number in international format (current flow delivers via SMS)
  • Ability to store a transaction ID between the initiate and verify steps

What OTPs are and why they matter

An OTP is a random, single-use token that expires quickly. Because the token is generated server-side, delivered out-of-band (SMS or email), and bound to a short validity window with attempt limits, it’s hard to phish and impractical to brute force. OTPs are great when:

  • You need lightweight authentication without passwords
  • You must prove ownership of a phone number during signup
  • You want step-up verification before money movement, profile edits, or data access

How our OTP flow works

The flow is designed to minimize latency for users and boilerplate for developers. For full field-level detail, see the OTP API reference:

  1. Initiate – Call /otp/initiate with the recipient and context. We generate a token, format the message (or use your template), deliver via SMS, and return a transaction ID plus delivery metadata. See the full payload details in Initiate OTP.
  2. Customer receives code – The user gets a message like “Your MyApp code is 483920. Valid for 10 minutes.”
  3. Verify – Send the transaction ID and the user’s submitted token to /otp/verify. We check token match, expiry, attempt limits, and destination consistency. Keep the exact fields handy via Verify OTP.
  4. Proceed – On success, you continue the protected action (create a session, confirm a withdrawal, etc.). You can also lookup or cancel the transaction if you need to audit or stop the flow. Refer to Lookup OTP and Cancel OTP when you need the field-by-field spec.

Built-in protections include idempotency on initiation, configurable token size/alphabet, delivery length checks, maximum verification attempts, and validity windows. The API returns clear statuses so you can drive your UX without polling internal state.

Integrate the OTP flow

The minimal loop is two calls: initiate, then verify. The payload keys below are alphabetical for quick scanning:

  • idempotency_key – Unique per attempt to avoid duplicate sends
  • purpose – Free-form context (e.g., login, reset_password)
  • recipient – Phone number in international format
  • sender – Alphanumeric sender ID (we fall back to your default if omitted)
  • service_name – Name shown in the message body
  • token_size – Optional length of the generated token
# Initiate OTP
curl -X POST https://api.zebo.dev/otp/initiate \
  -H "Authorization: Bearer $COMMERCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "otp_login_{{user_id}}_{{timestamp}}",
    "purpose": "login",
    "recipient": "+233241234567",
    "sender": "MyApp",
    "service_name": "MyApp",
    "token_size": 6
  }'

# Verify OTP
curl -X POST https://api.zebo.dev/otp/verify \
  -H "Authorization: Bearer $COMMERCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient": "+233241234567",
    "token": "483920",
    "transaction_id": "txn_from_initiate"
  }'

Next steps

  • Persist the transaction ID so you can retry verification without re-sending codes
  • Use /otp/lookup to render delivery state in your UI, and /otp/cancel to invalidate stale flows
  • Keep idempotency keys unique per logical attempt to prevent duplicate messages during retries

Was this page helpful?