card.otp — delivered when the card network issues a one-time passcode (OTP) for one of your
cards. You deliver the passcode to the cardholder.When it fires#
scene | When |
|---|
THREE_DS | The cardholder is paying online and the merchant requires 3D Secure verification |
WALLET_BINDING | The cardholder is adding the card to a mobile wallet (Google Pay / Apple Pay) |
The cardholder is waiting on a verification page while this event is in flight. Deliver the
passcode through your own channel — your app, SMS, or email — as quickly as you can.PIK does not email the passcode to the cardholder while your webhook is set up. If your Issuing
credential has no webhook URL, or its webhook is switched off, PIK falls back to emailing the
address you supplied for the cardholder.How it differs from other events#
| Other events | card.otp |
|---|
| Retries | 5 attempts, 5 minutes apart | One immediate retry only, then dropped. The retry carries the same eventId and occurredAt |
| Your time budget | 10 seconds | 5 seconds |
| Recoverable later | Yes — query the card or transaction through the API | No — PIK never stores the passcode |
The passcode expires within minutes, so a late delivery is useless. If a delivery is lost, the
cardholder requests a new code on the verification page and a new card.otp follows.Payload#
Envelope#
| Field | Type | Description |
|---|
eventId | string | Unique per event, and unchanged on the retry. Deduplicate on this value |
eventType | string | Always card.otp |
version | string | Payload schema version. Additive changes do not bump it |
occurredAt | string | When PIK first assembled the event; unchanged on the retry (ISO 8601) |
data | object | See below |
data#
| Field | Type | Description |
|---|
cardNo | string | PIK card number |
scene | string | THREE_DS / WALLET_BINDING. Tolerate values not listed here |
otp | string | The passcode. A live credential — see Handling below |
referenceCode | string | THREE_DS only: reference code also shown on the cardholder's verification page, so they can tell it is the same request. Otherwise null |
transactionAmount | string | THREE_DS only: amount of the purchase being verified. Otherwise null |
transactionCurrency | string | THREE_DS only: currency of the purchase. Otherwise null |
merchantName | string | THREE_DS only: merchant name. Otherwise null |
walletType | string | WALLET_BINDING only: GOOGLE_PAY / APPLE_PAY. Otherwise null |
Show the cardholder the merchant and amount together with the passcode, so they can refuse a
request they did not start.Example — 3D Secure#
{
"eventId": "evt_260923K7Q2M9X4TB",
"eventType": "card.otp",
"version": "1.0",
"occurredAt": "2026-09-23T06:02:11Z",
"data": {
"cardNo": "CD260811X9Y8Z7",
"scene": "THREE_DS",
"otp": "482913",
"referenceCode": "RC7F",
"transactionAmount": "40.00",
"transactionCurrency": "USD",
"merchantName": "STEAM PURCHASE",
"walletType": null
}
}
Example — wallet binding#
{
"eventId": "evt_260923W2R8N6J1PC",
"eventType": "card.otp",
"version": "1.0",
"occurredAt": "2026-09-23T06:15:40Z",
"data": {
"cardNo": "CD260811X9Y8Z7",
"scene": "WALLET_BINDING",
"otp": "719052",
"referenceCode": null,
"transactionAmount": null,
"transactionCurrency": null,
"merchantName": null,
"walletType": "APPLE_PAY"
}
}
Handling#
Verify the signature before you deliver anything. A forged card.otp could be used to
phish your cardholder.
Deliver the passcode to the cardholder only. Do not log it, do not show it to your support
staff, and do not store it longer than the delivery takes.
Acknowledge quickly: return 2xx first, then deliver. A slow response counts as a failure and the
single retry may arrive after the cardholder has already given up.
Verifying the signature#
Three headers accompany every delivery:| Header | Value |
|---|
X-Webhook-Event | Event category, always ISSUING for this event |
X-Webhook-Event-Type | Always card.otp |
X-Webhook-Signature | HMAC-SHA256 signature, lowercase hex |
X-Webhook-Signature = hex_lower( HMAC_SHA256( appSecret, rawBody ) )
The signed content is the raw request body only, with no timestamp and no separator. The signing
key is your appSecret — there is no separate webhook secret. Compare in constant time, and
deduplicate on eventId.Modified at 2026-09-30 09:26:56