Webhooks
Goal: receive near-real-time updates about card activity through encrypted webhook deliveries.
Some events use the { event_type, project_id, data } envelope; transaction, OTP, and 3DS webhooks use typed payloads with a top-level type field.
Quick summary
Subscription endpoints
Subscribe, unsubscribe, and list subscriptions under /api/v2/wh/*.
Encrypted delivery
Webhook deliveries are encrypted and sent with the API-KEY header.
200 + success body
Return HTTP 200 with { "success": true } to stop retries.
Card-scoped
You only receive events for cards created under your API key.
Workflow steps
Create a subscription
POST your callback URL to /api/v2/wh/subscribe.
Receive encrypted deliveries
We POST an encrypted payload to your endpoint with the API-KEY header.
Decrypt and acknowledge
Decrypt the payload and reply with HTTP 200 and { "success": true }. This response is not encrypted.
Success response
Return plain JSON (not encrypted).
Field Type Required Description successboolean Required Must be true to stop retries. JSON{ "success": true }Handle retries and duplicates
Deliveries are at-least-once. Deduplicate by delivery_id.
Subscription endpoints
All subscription endpoints are encrypted like other /api/v2/* requests. See Encrypted Payload.
- POST
/api/v2/wh/subscribeCreate a webhook subscription for your URL. - POST
/api/v2/wh/unsubscribeDeactivate a subscription by id. - POST
/api/v2/wh/subscriptionsList active subscriptions.
- Subscriptions only receive events created after the subscription is created.
- We do not backfill older events.
- You can register multiple subscriptions per API key (the same event is delivered to each).
Delivery request
| Field | Details |
|---|---|
| Method | POST |
| Header | API-KEY: <your project API key> |
| Body | { "encrypted": "<base64 string>" } |
Delivery envelope schema
| Field | Type | Required | Description |
|---|---|---|---|
encrypted | string | Required | Base64-encoded encrypted payload. |
{
"encrypted": "<base64 string>"
}Retry behavior
| Item | Details |
|---|---|
| Retry trigger | Any response that is not HTTP 200 with { "success": true }. |
| Cadence | Retries are periodic (about once per minute). |
| Guarantee | Deliveries are at-least-once; duplicates are possible. |
| Dedup key | Use delivery_id from decrypted payload. |
Event catalog
These are the webhook shapes currently emitted by the onpremise code path.
Envelope events
| Type | When sent | Notes |
|---|---|---|
card.created | When a card issue order is created. | data includes card_id, order_id, product_code, and status. |
card.topup | When a card topup order is created. | data includes card_id, order_id, amount, and status. |
card.freeze | When POST /api/v2/cards/freeze succeeds. | data includes card_id, order_id, and status=frozen. |
card.unfreeze | When POST /api/v2/cards/unfreeze succeeds. | data includes card_id, order_id, and status=active. |
Typed events
| Type | When sent | Notes |
|---|---|---|
card_transaction | Transaction webhooks from auth, clear, refund, reversal, fee, and other adjustment flows. | Canonical typed payload. The same type also covers simulation events in this repo. |
card_otp | OTP-based 3DS and verification flows. | Contains the OTP code and the time it was sent. |
card_3ds | OOB 3DS confirmations. | Contains securement_id, confirm, status, and merchant details. |
Envelope schema
| Field | Type | Required | Description |
|---|---|---|---|
event_type | string | Required | Event name delivered to subscribers. |
project_id | string | Required | Project identifier associated with the event. |
delivery_id | string | Required | Stable per-delivery identifier. Reused on retries. |
data | object | Required | Event-specific payload. See per-event schemas below. |
Envelope data by event
The data object depends on event_type.
Card created
card.created| Field | Type | Required | Description |
|---|---|---|---|
card_id | string | Required | Card identifier. |
order_id | string | Required | Issue order identifier. |
product_code | string | Required | Card product code. |
status | string | Required | Order status at emission time. |
{
"event_type": "card.created",
"project_id": "project_1",
"delivery_id": "67f3f91c0f9b7462a5d7d0a0",
"data": {
"card_id": "697c9b7559fad5ba001068ce",
"order_id": "69827898db842395ebd4821d",
"product_code": "CARD-USD-001",
"status": "paid"
}
}Card topup
card.topup| Field | Type | Required | Description |
|---|---|---|---|
card_id | string | Required | Card identifier. |
order_id | string | Required | Topup order identifier. |
amount | string | Required | Topup amount as a string. |
status | string | Required | Order status at emission time. |
{
"event_type": "card.topup",
"project_id": "project_1",
"delivery_id": "67f3f91c0f9b7462a5d7d0a1",
"data": {
"card_id": "697c9b7559fad5ba001068ce",
"order_id": "69827898db842395ebd4821e",
"amount": "100",
"status": "new"
}
}Card frozen / unfrozen
card.freezecard.unfreeze| Field | Type | Required | Description |
|---|---|---|---|
card_id | string | Required | Card identifier. |
order_id | string | Required | Status-change order identifier. |
status | string | Required | New card status. Enum: frozen, active |
{
"event_type": "card.freeze",
"project_id": "project_1",
"delivery_id": "67f3f91c0f9b7462a5d7d0a2",
"data": {
"card_id": "697c9b7559fad5ba001068ce",
"order_id": "69827898db842395ebd4821d",
"status": "frozen"
}
}Card transaction
card_transactionEmitted for card transaction webhooks from auth, clear, refund, reversal, fee, and other adjustment flows.
| Status | Type | Meaning |
|---|---|---|
pending | auth | Authorization pending. |
approved | auth | Authorization approved. |
declined | auth | Authorization declined. |
approved | reversal | Authorization reversed. |
approved | fee | Fee posted. |
approved | refund | Refund posted. |
approved | other | Other approved adjustment. |
other | refund | Refund with provider-specific status. |
The tx_type value is derived from provider status/type mappings. This payload has the same shape as POST /api/v2/cards/txs items, and transaction_id is the shared correlation key.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Required | Event type. Enum: card_transaction |
delivery_id | string | Required | Stable per-delivery identifier. The same value is reused on retries. |
tx_type | string | Required | Provider-derived transaction type (e.g., transaction_created_auth_pending). |
transaction_id | string | Required | Unique transaction identifier. Use to correlate with /cards/txs responses and deduplicate webhook deliveries. |
card_id | string | Required | Card identifier. |
tx_at | stringdate-time | Required | Transaction time (ISO 8601). |
card_tx_data | object | Required | Normalized transaction fields. |
{
"type": "card_transaction",
"delivery_id": "67f3f91c0f9b7462a5d7d0a1",
"tx_type": "transaction_created_auth_pending",
"transaction_id": "683a1f4e09abe640561c4a12",
"card_id": "697c9b7559fad5ba001068ce",
"tx_at": "2026-02-03T22:37:11.597606+00:00",
"card_tx_data": {
"auth_type": "purchase",
"status": "pending",
"type": "auth",
"card_label": "3691",
"merchant": {
"name": "GOOGLE *TEMPORARY HOLD",
"mcc": "5734"
},
"fee_amount": 0.25,
"fee_currency": "usd",
"card_currency": "usd",
"card_amount": 99.0,
"merchant_currency": "hkd",
"merchant_amount": 771.2,
"failure_reason": "Card Blocked"
}
}Card OTP
card_otpOTP payloads are emitted from verification flows when the incoming method is OTP.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Required | Event type. Enum: card_otp |
delivery_id | string | Required | Stable per-delivery identifier. The same value is reused on retries. |
card_id | string | Required | Card identifier. |
code | string | Required | One-time password. |
sent_at | stringdate-time | Required | OTP send time (ISO 8601). |
{
"type": "card_otp",
"delivery_id": "67f3f91c0f9b7462a5d7d0a4",
"card_id": "697c9b7559fad5ba001068ce",
"code": "687524",
"sent_at": "2026-02-03T22:37:11.597606+00:00"
}Card 3DS
card_3dsOOB 3DS confirmations carry the securement identifier that the client must pass back to /api/v2/cards/3ds/confirm.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Required | Event type. Enum: card_3ds |
delivery_id | string | Required | Stable per-delivery identifier. The same value is reused on retries. |
card_id | string | Required | Card identifier. |
securement_id | string | Required | Provider securement identifier. Use it to confirm the 3DS flow. |
method | string | Required | 3DS method. Enum: OOB |
confirm | string | Required | Confirmation state from the provider payload. |
status | string | Required | 3DS status from the provider payload. |
currency | string | Required | Settlement currency (ISO 4217). |
amount | string | Required | Amount as a string. |
occurred_at | stringdate-time | Required | Event time (ISO 8601). |
reason | string | Optional | Optional provider reason. |
remark | string | Optional | Optional provider remark. |
merchant | object | Required | Merchant information. |
{
"type": "card_3ds",
"delivery_id": "67f3f91c0f9b7462a5d7d0a5",
"card_id": "697c9b7559fad5ba001068ce",
"securement_id": "71859228876451840",
"method": "OOB",
"confirm": "PENDING",
"status": "PENDING",
"currency": "cny",
"amount": "0",
"occurred_at": "2026-02-03T22:37:11.597606+00:00",
"reason": null,
"remark": null,
"merchant": {
"name": "Alipay",
"country": "CHN"
}
}Idempotency and duplicates
- We avoid duplicate inserts using internal idempotency keys, but delivery is at-least-once.
- Use
delivery_idas the primary deduplication key on your receiver side. - Always respond quickly with
{"success": true}once your handler has accepted the event.