Skip to content
5 min read

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 has attempts_remaining tries left)
  • expired — Code expired (check expires_at from 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 exist
  • 429 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_after seconds and try again
  • Code expired? — Call /resend to get a new code
  • Too many wrong guesses? — Call /resend to reset the attempt counter
  • Plan limit hit? — Upgrade your plan at /organizations/settings → Billing
Was this page helpful?

See Wallin on your own channels.

Book a walkthrough — we'll connect a test account and show you the inbox, broadcasts, and automation live.