Skip to main content
Every OTP.ID API response uses a consistent JSON envelope. When a request fails, the 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.
A wrong OTP code submitted to POST /v3/verify is not an error response. The API returns HTTP 200 with success: true and data.verified: false. Only check for error codes when success is false.

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.
When 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.
data.failure is absent for every transaction that isn’t "failed". Treat any code you don’t recognize as a generic failure and fall back to displaying failure.message — new codes may be added over time without a breaking change.
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 Authorization header 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
Example response:
The 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:
See the API V3 IP allowlist guide for proxy setup and operational details.

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: the otp field is missing, contains non-digit characters, or its length is outside 4–8
  • On POST /v3/verify: otp_id or otp is missing from the request body
Note that out-of-range 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 channel string does not match any supported value (whatsapp, sms, email, misscall, whatsapp_inbound)
  • You passed misscall or whatsapp_inbound to POST /v3/send — these channels are only valid with POST /v3/request
Example response:
How to resolve: Use one of the supported channel values listed in the Channels guide. If you need Missed Call or WhatsApp Inbound, switch to the 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 destination field is missing on a channel that requires it
Example response:
How to resolve: Send phone numbers as digits only, in international format without a leading + (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
Example response:
How to resolve: Top up your credits from the OTP.ID dashboard or create a payment link with 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_id was typed or stored incorrectly
  • The OTP was created with a different API key (cross-merchant access is not permitted)
Example response:
How to resolve: Confirm that you are storing and forwarding the 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_id but changed the channel, destination, or other parameters between retries
  • Two concurrent processes generated the same external_id with different payloads
Example response:
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.
How to resolve: Read 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 same otp_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_at on the send response)
Example response:
How to resolve: Create a new OTP transaction by calling 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:
How to resolve: Create a new OTP transaction. For Missed Call, the second wrong submission locks the transaction immediately; the mismatch response for that submission is still HTTP 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/verify a second time after receiving data.verified: true
  • A race condition caused two concurrent verify calls to be sent for the same otp_id
Example response:
How to resolve: After a successful verification (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:
How to resolve: Implement exponential backoff — wait a short period before retrying, and increase the wait time on subsequent failures. See the Rate Limits reference for full details and backoff recommendations. If your use case requires a higher limit, contact OTP.ID support.

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
Limits for misscall:
  • Maximum 2 OTPs per 10 minutes to the same destination
  • Maximum 4 OTPs per hour to the same destination
Example response:
How to resolve: Wait until the current window expires before sending another OTP to this destination. Track the number of OTPs you send per destination on your side to surface a friendly waiting message to users instead of showing them an error. See the Rate Limits reference for the full per-destination policy.

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
Example response:
How to resolve: Retry the request after a short delay using exponential backoff. If the channel remains unavailable for an extended period, consider falling back to an alternative channel (e.g., SMS instead of WhatsApp) for time-sensitive use cases.

INTERNAL_ERROR — HTTP 500

An unexpected error occurred on the OTP.ID server. Example response:
How to resolve: Retry the request with exponential backoff. If the error persists, contact OTP.ID support with the timestamp and any otp_id or external_id values from the affected requests to help with investigation.
Log all INTERNAL_ERROR responses in your application alongside request identifiers and timestamps. This trace information significantly speeds up diagnosis if you need to open a support ticket.