OTP API
Deliver One-Time Passwords via WhatsApp for user verification.
Base URL: {WALLIN_URL}/api/v1/otp
All endpoints require the X-Wallin-OTP-Key header with your API key.
POST /send
Send an OTP code to a phone number.
Request:
curl -X POST https://wallin.example/api/v1/otp/send \
-H "X-Wallin-OTP-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+201001234567"
}'
Query Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
phone_number |
string | yes | Phone number in E.164 format (e.g., +201001234567) |
metadata |
object | no | Custom metadata to store with the request (e.g., {"user_id": "123"}) |
Response (200 OK):
{
"data": {
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"expires_at": "2024-07-19T10:35:00Z"
},
"message": "OTP sent successfully."
}
Save the reference_id — you'll need it to verify the code later.
Errors:
401 Unauthorized — Missing or invalid API key:
{
"message": "Invalid or inactive API key",
"error": "invalid_api_key"
}
403 Forbidden — IP address not allowed (if you set an IP allow-list):
{
"message": "IP address not allowed",
"error": "ip_not_allowed"
}
429 Too Many Requests — Rate limit exceeded:
{
"message": "Maximum 5 OTPs per hour per phone number",
"error": "rate_limit_exceeded",
"retry_after": 3600
}
Rate Limits:
- 5 OTP sends per phone number per hour
- 20 OTP sends per IP address per hour
- Plan usage limit (free: 100/month, starter: 5,000/month, professional: 50,000/month, enterprise: unlimited)
Use the retry_after value to know when to retry.
POST /verify
Verify an OTP code.
Request:
curl -X POST https://wallin.example/api/v1/otp/verify \
-H "X-Wallin-OTP-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"code": "123456"
}'
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
reference_id |
string | yes | The reference_id from the send response |
code |
string | yes | The 6-digit (or configured length) code the user entered |
Response (200 OK) — Verified:
{
"data": {
"verified": true
},
"message": "OTP verified successfully."
}
Response (200 OK) — Invalid Code:
{
"data": {
"verified": false,
"error": "invalid_code",
"attempts_remaining": 2
},
"message": "Verification failed."
}
Response (200 OK) — Expired:
{
"data": {
"verified": false,
"error": "expired"
},
"message": "Verification failed."
}
Response (200 OK) — Max Attempts Exceeded:
{
"data": {
"verified": false,
"error": "max_attempts_exceeded",
"attempts_remaining": 0
},
"message": "Verification failed."
}
Possible errors:
invalid_code— Code doesn't match (user hasattempts_remainingtries left)expired— Code expired (checkexpires_atfrom send response)max_attempts_exceeded— User exceeded max wrong guesses (default 3)already_verified— Code was already used (reference_id marked verified)not_found— reference_id doesn't exist or belongs to a different app
POST /resend
Resend an OTP code to the same phone number.
Request:
curl -X POST https://wallin.example/api/v1/otp/resend \
-H "X-Wallin-OTP-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"reference_id": "550e8400-e29b-41d4-a716-446655440000"
}'
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
reference_id |
string | yes | The reference_id from the original send response |
Response (200 OK):
{
"data": {
"reference_id": "550e8400-e29b-41d4-a716-446655440000",
"expires_at": "2024-07-19T10:40:00Z"
},
"message": "OTP resent successfully."
}
Resending resets:
- The OTP code (a new code is generated)
- The attempt counter (user gets 3 fresh attempts)
- The expiry timer
Limits:
- Max 3 resends per reference_id
- Same rate limits apply as
/send(5/hour per phone, 20/hour per IP)
Errors:
404 Not Found— reference_id doesn't exist429 Too Many Requests— Max resend limit (3) or rate limit exceeded
Common Workflows
Workflow 1: Send & Verify
# 1. Send OTP
curl -X POST https://wallin.example/api/v1/otp/send \
-H "X-Wallin-OTP-Key: key" \
-H "Content-Type: application/json" \
-d '{"phone_number": "+201001234567"}' \
| jq '.data.reference_id' > /tmp/ref_id.txt
# 2. User receives SMS on WhatsApp (e.g., code: 123456)
# 3. User enters code in your app, verify it
REFERENCE=$(cat /tmp/ref_id.txt)
curl -X POST https://wallin.example/api/v1/otp/verify \
-H "X-Wallin-OTP-Key: key" \
-H "Content-Type: application/json" \
-d "{\"reference_id\": \"$REFERENCE\", \"code\": \"123456\"}" \
| jq '.data.verified'
# Output: true (success) or false (failure)
Workflow 2: Resend on User Request
# User clicks "Send code again"
REFERENCE=$(cat /tmp/ref_id.txt)
curl -X POST https://wallin.example/api/v1/otp/resend \
-H "X-Wallin-OTP-Key: key" \
-H "Content-Type: application/json" \
-d "{\"reference_id\": \"$REFERENCE\"}" \
| jq '.data.expires_at'
# Output: new expiry timestamp
Configuration
OTP parameters are set when creating the OTP application at /otp-applications:
- Code Length — 4–8 digits (default 6)
- Expiry — seconds until code expires (default 300 = 5 minutes)
- Max Attempts — wrong guesses allowed per code (default 3)
These apply to all codes sent by that application. Change them in the OTP app settings.
Testing
For local testing, set WHATSAPP_SIMULATE_API=true in your .env. The OTP endpoint will:
- Accept any phone number
- Instantly "send" codes (no actual WhatsApp message)
- Allow any 6-digit code to verify (simulated)
Use this to test your integration without consuming real OTP credits.
Support
- Rate limit exceeded? — Wait the
retry_afterseconds and try again - Code expired? — Call
/resendto get a new code - Too many wrong guesses? — Call
/resendto reset the attempt counter - Plan limit hit? — Upgrade your plan at
/organizations/settings→ Billing