success field is false, the data field is null, and the error object contains a machine-readable code and a human-readable message. Always branch your error-handling logic on error.code — the code string is stable across API versions, while the message text may change without notice.
Quick reference
Looking for the reason a specific transaction failed to deliver (
data.status: "failed")? That is a separate mechanism from the error codes on this page — see Delivery failure reasons below.Delivery failure reasons (data.failure)
This is different from the
error codes above. data.failure appears on a successful HTTP 200 response (success: true) when data.status is "failed" — the request itself was valid and accepted, but OTP.ID could not deliver the code to the destination. Credits are not deducted in this case. See Delivery Failures in the API reference.data.status is "failed" on POST /v3/request, POST /v3/send, or GET /v3/otp/{otp_id}, data includes a failure object with a stable, machine-readable code and a human-readable Indonesian message. The raw vendor/provider error is never exposed to keep this contract stable across vendor changes.
CHANNEL_UNAVAILABLE is intentionally reused between the two mechanisms on this page, with different meanings: as data.failure.code it means this one delivery failed because its channel was unavailable (the transaction still exists, status is failed, HTTP 200). As the error.code documented below, it means the entire request was rejected before any transaction was created (HTTP 503) — for example, whatsapp_inbound returns this error.code when OTP.ID’s central WhatsApp number is temporarily not configured.Authentication errors
UNAUTHORIZED — HTTP 401
Your request did not include a valid API key.
Causes:
- The
Authorizationheader is missing entirely - The header is present but empty or malformed (e.g., not using
Bearer <api_key>) - The API key has been revoked or does not exist
message varies by cause: "Sertakan header Authorization: Bearer <api_key>" (header missing/malformed), "API key kosong" (empty token), or "API key tidak dikenal" (unknown key).
How to resolve: Verify that every request includes the header Authorization: Bearer <api_key> using your key from the OTP.ID dashboard (Integration page → API Key). If the key was recently rotated, update it in your application configuration.
IP_NOT_ALLOWED — HTTP 403
The request came from an IP address that is not in the merchant’s API V3 IP whitelist. IP allowlisting is independent of API-key authentication and is evaluated after the API key is validated.
How to resolve: Ask OTP.ID to configure the whitelist for your merchant, then provide the exact public IPv4 or IPv6 addresses of the servers that call the API. Multiple addresses are comma-separated, for example 203.0.113.10,198.51.100.7,2001:db8::7. Whitespace around commas is ignored; CIDR ranges are not accepted. An empty whitelist allows requests from any resolved IP.
OTP.ID resolves the client IP automatically in this order: CF-Connecting-IP, X-Real-IP, the first address in X-Forwarded-For, then the direct connection address. If Cloudflare is bypassed, the resolver automatically falls back to the next available value.
Example response:
Request validation errors
VALIDATION_ERROR — HTTP 400
The request body is missing required fields or contains values that fail validation.
Causes:
- On
POST /v3/send: theotpfield is missing, contains non-digit characters, or its length is outside 4–8 - On
POST /v3/verify:otp_idorotpis missing from the request body
ttl and otp_length values do not trigger this error — they are silently clamped to the valid range.
Example response:
POST /v3/verify uses the message "otp_id dan otp wajib diisi".)
How to resolve: Check the error.message text for the specific field that failed validation, then correct your request body. Consult the API Reference for each endpoint’s required and optional fields.
INVALID_CHANNEL — HTTP 400
The channel value you provided is not recognised, or you used a channel that is not permitted on the specified endpoint.
Causes:
- The
channelstring does not match any supported value (whatsapp,sms,email,misscall,whatsapp_inbound) - You passed
misscallorwhatsapp_inboundtoPOST /v3/send— these channels are only valid withPOST /v3/request
POST /v3/request endpoint. See the Missed Call guide and WhatsApp Inbound guide for the full flows.
INVALID_NUMBER — HTTP 400
The destination phone number or email address could not be parsed or validated.
Causes:
- The phone number contains non-digit characters (e.g. a leading
+, spaces, or dashes) - The phone number is shorter than 10 digits
- The email address is syntactically invalid
- The
destinationfield is missing on a channel that requires it
+ (e.g., 6281234567890 for an Indonesian number). For email, ensure the address conforms to standard format (user@example.com).
Credit errors
INSUFFICIENT_BALANCE — HTTP 402
Your merchant account does not have enough prepaid credits to cover the cost of this transaction.
Causes:
- Your credit balance has fallen below the per-transaction price for the requested channel
- A recent top-up has not yet been applied to your account
POST /v3/topups. Review the Billing guide for per-channel pricing details. Consider setting up a low-credit alert to avoid disruption in production.
Not found errors
OTP_NOT_FOUND — HTTP 404
The otp_id you referenced does not exist or is not owned by the API key making the request.
Causes:
- The
otp_idwas typed or stored incorrectly - The OTP was created with a different API key (cross-merchant access is not permitted)
otp_id value exactly as returned by POST /v3/request or POST /v3/send. Ensure you are using the same API key that created the OTP transaction.
Conflict errors
DUPLICATE_EXTERNAL_ID — HTTP 409
You submitted an external_id that has already been used for a different OTP transaction with different parameters.
Causes:
- Your system retried a request with the same
external_idbut changed thechannel,destination, or other parameters between retries - Two concurrent processes generated the same
external_idwith different payloads
This is the only error code that includes an
error.details object. The existing_otp_id field gives you the otp_id of the original transaction. Use GET /v3/otp/{otp_id} to retrieve its current status and continue the verification flow without creating a new OTP.error.details.existing_otp_id and look up the original transaction with GET /v3/otp/{otp_id}. If the parameters genuinely need to change (different channel, different destination), generate a new unique external_id. See the Idempotency guide for best practices on external_id generation.
Terminal state errors (422)
These three errors indicate that the OTP transaction has reached a point from which it cannot recover. You must create a new OTP transaction — calling verify again on the sameotp_id will continue to fail.
OTP_EXPIRED — HTTP 422
The OTP’s time-to-live (TTL) has elapsed and the code is no longer valid.
Causes:
- The user took too long to enter the code
- Your application delayed the verify call beyond the OTP’s expiry time (visible in
data.expires_aton the send response)
POST /v3/request or POST /v3/send again. Display a “resend OTP” option in your UI before the expiry time so users can request a new code proactively.
TOO_MANY_ATTEMPTS — HTTP 422
The user has submitted the maximum number of incorrect codes for this OTP transaction. The default limit is 5 failed attempts; Missed Call transactions have a stricter limit of 2 because their caller-ID OTP is four digits. Further verification attempts are blocked.
Example response:
200, while subsequent verification calls return this 422 error. Consider adding client-side attempt tracking to disable the submit button after several failures before reaching the server-side limit.
ALREADY_USED — HTTP 422
The OTP has already been successfully verified and cannot be used again.
Causes:
- Your application called
POST /v3/verifya second time after receivingdata.verified: true - A race condition caused two concurrent verify calls to be sent for the same
otp_id
data.verified: true), store the result and do not call verify again for the same otp_id. If your architecture has concurrent processes, add a local lock or idempotency check before calling the verify endpoint.
Rate limit errors
RATE_LIMITED — HTTP 429
Your API key has exceeded the global rate limit of 20 requests per second across all /v3/* endpoints.
Example response:
DESTINATION_RATE_LIMITED — HTTP 429
You have sent too many OTPs to the same destination phone number or email address in a short window.
Limits for standard channels:
- Maximum 5 OTPs per 10 minutes to the same destination
- Maximum 10 OTPs per hour to the same destination
misscall:
- Maximum 2 OTPs per 10 minutes to the same destination
- Maximum 4 OTPs per hour to the same destination
Infrastructure errors
CHANNEL_UNAVAILABLE — HTTP 503
The delivery channel or underlying vendor is temporarily unavailable.
Causes:
- The WhatsApp, SMS, email, or missed call provider is experiencing an outage or maintenance window
- A transient network issue between OTP.ID and the delivery vendor
- For
whatsapp_inbound, OTP.ID’s central WhatsApp number is temporarily not configured
INTERNAL_ERROR — HTTP 500
An unexpected error occurred on the OTP.ID server.
Example response:
otp_id or external_id values from the affected requests to help with investigation.

