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. To activate it, contact the OTP.ID team to register your webhook URL and receive your webhook_secret (self-service configuration from the dashboard is coming soon). 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 registered for your account, 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 external_id you sent when creating the transaction, echoed back so you can match this event to your own record without an extra lookup. Always present — an empty string ("") when you did not send one. See Idempotency.
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 (provided when your webhook URL is registered).
  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. Delivery is attempted up to 3 times in total: Each attempt has a 10-second timeout, and X-OTPID-Timestamp (and therefore the signature) is regenerated per attempt. After the 3rd failed attempt 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