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:
- Initiate – Call
/otp/initiatewith 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. - Customer receives code – The user gets a message like “Your MyApp code is 483920. Valid for 10 minutes.”
- 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. - 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 sendspurpose– Free-form context (e.g.,login,reset_password)recipient– Phone number in international formatsender– Alphanumeric sender ID (we fall back to your default if omitted)service_name– Name shown in the message bodytoken_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/lookupto render delivery state in your UI, and/otp/cancelto invalidate stale flows - Keep idempotency keys unique per logical attempt to prevent duplicate messages during retries