Skip to main content
The otp.verified webhook is an outbound HTTP request that OTP.ID sends to your server when an OTP transaction reaches the verified state. Unlike the API endpoints documented elsewhere, you do not call this — you receive it. Configure your webhook URL in the OTP.ID dashboard. Every incoming request carries a cryptographic signature you must validate before trusting the payload.

Direction and Trigger

OTP.ID sends a POST request to your configured webhook URL whenever a transaction is verified — either by a successful call to POST /v3/verify or by an automatic token match in a whatsapp_inbound flow. The delivery is asynchronous: the webhook fires after OTP.ID records the verification and may arrive a few seconds after the verified status is set.
If no webhook URL is configured in your OTP.ID dashboard, this event is silently dropped. The webhook is a no-op until you register a URL.

Request from OTP.ID

OTP.ID sends the following request to your endpoint:
  • Method: POST
  • Content-Type: application/json

Headers

Payload Fields

string
Always "otp.verified". Use this field if your endpoint handles multiple event types.
string
The verified transaction ID (e.g. "OTP20260807ABCD000001").
string
The delivery channel used for the transaction (e.g. "whatsapp", "sms", "email").
string
The destination phone number or email address associated with the transaction.
string
The datetime at which verification occurred, in YYYY-MM-DD HH:MM:SS format (WIB, UTC+7).

Payload Example

The payload never contains the OTP code, any hash or salt of the code, or inbound tokens. It carries only the identity and outcome of the transaction.

Signature Verification

Every request from OTP.ID includes an X-OTPID-Signature header. Always verify this signature before acting on the payload — it protects you against spoofed requests. Algorithm (HMAC-SHA256):
  1. Read the X-OTPID-Timestamp header value.
  2. Read the raw, unparsed request body as a string (before JSON parsing).
  3. Concatenate: "{timestamp}." + raw_body
  4. Compute HMAC-SHA256 of that string using your webhook_secret (from the OTP.ID dashboard).
  5. Compare the result (hex-encoded) with X-OTPID-Signature using a constant-time comparison to prevent timing attacks.
Use the raw request body — not a serialized version of the parsed JSON — to compute the signature. JSON serialization can alter whitespace or key ordering, causing valid signatures to fail.

Retry Policy

If your endpoint returns a non-2xx response or OTP.ID cannot reach it within the timeout, OTP.ID retries the delivery automatically: Each attempt has a 10-second timeout. After 3 failed retries the event is dropped and not retried further.
Respond with an HTTP 2xx status code as quickly as possible — within the 10-second timeout. If your processing logic is slow (database writes, downstream API calls, etc.), acknowledge receipt immediately and process the event asynchronously in a background job or queue.

Checklist for Your Endpoint

  • ✅ Parse the raw body before any middleware that mutates it
  • ✅ Validate X-OTPID-Signature using constant-time comparison
  • ✅ Optionally reject requests where X-OTPID-Timestamp is too far in the past (e.g. > 5 minutes) to guard against replay attacks
  • ✅ Return HTTP 200 immediately; process async if needed
  • ✅ Treat duplicate deliveries of the same otp_id idempotently — retries may cause the same event to arrive more than once