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

  1. Create a subscription

    POST your callback URL to /api/v2/wh/subscribe.

  2. Receive encrypted deliveries

    We POST an encrypted payload to your endpoint with the API-KEY header.

  3. 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).

    FieldTypeRequiredDescription
    success
    booleanRequiredMust be true to stop retries.
    JSON
    {
      "success": true
    }
  4. 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

FieldDetails
MethodPOST
HeaderAPI-KEY: <your project API key>
Body{ "encrypted": "<base64 string>" }

Delivery envelope schema

FieldTypeRequiredDescription
encrypted
stringRequiredBase64-encoded encrypted payload.
JSON
{
  "encrypted": "<base64 string>"
}

Retry behavior

ItemDetails
Retry triggerAny response that is not HTTP 200 with { "success": true }.
CadenceRetries are periodic (about once per minute).
GuaranteeDeliveries are at-least-once; duplicates are possible.
Dedup keyUse delivery_id from decrypted payload.

Event catalog

These are the webhook shapes currently emitted by the onpremise code path.

Envelope events

TypeWhen sentNotes
card.createdWhen a card issue order is created.data includes card_id, order_id, product_code, and status.
card.topupWhen a card topup order is created.data includes card_id, order_id, amount, and status.
card.freezeWhen POST /api/v2/cards/freeze succeeds.data includes card_id, order_id, and status=frozen.
card.unfreezeWhen POST /api/v2/cards/unfreeze succeeds.data includes card_id, order_id, and status=active.

Typed events

TypeWhen sentNotes
card_transactionTransaction 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_otpOTP-based 3DS and verification flows.Contains the OTP code and the time it was sent.
card_3dsOOB 3DS confirmations.Contains securement_id, confirm, status, and merchant details.

Envelope schema

FieldTypeRequiredDescription
event_type
stringRequiredEvent name delivered to subscribers.
project_id
stringRequiredProject identifier associated with the event.
delivery_id
stringRequiredStable per-delivery identifier. Reused on retries.
data
objectRequiredEvent-specific payload. See per-event schemas below.

Envelope data by event

The data object depends on event_type.

Card created

card.created
FieldTypeRequiredDescription
card_id
stringRequiredCard identifier.
order_id
stringRequiredIssue order identifier.
product_code
stringRequiredCard product code.
status
stringRequiredOrder status at emission time.
JSON
{
  "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
FieldTypeRequiredDescription
card_id
stringRequiredCard identifier.
order_id
stringRequiredTopup order identifier.
amount
stringRequiredTopup amount as a string.
status
stringRequiredOrder status at emission time.
JSON
{
  "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
FieldTypeRequiredDescription
card_id
stringRequiredCard identifier.
order_id
stringRequiredStatus-change order identifier.
status
stringRequiredNew card status. Enum: frozen, active
JSON
{
  "event_type": "card.freeze",
  "project_id": "project_1",
  "delivery_id": "67f3f91c0f9b7462a5d7d0a2",
  "data": {
    "card_id": "697c9b7559fad5ba001068ce",
    "order_id": "69827898db842395ebd4821d",
    "status": "frozen"
  }
}

Card transaction

card_transaction

Emitted for card transaction webhooks from auth, clear, refund, reversal, fee, and other adjustment flows.

StatusTypeMeaning
pendingauthAuthorization pending.
approvedauthAuthorization approved.
declinedauthAuthorization declined.
approvedreversalAuthorization reversed.
approvedfeeFee posted.
approvedrefundRefund posted.
approvedotherOther approved adjustment.
otherrefundRefund 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.

FieldTypeRequiredDescription
type
stringRequiredEvent type. Enum: card_transaction
delivery_id
stringRequiredStable per-delivery identifier. The same value is reused on retries.
tx_type
stringRequiredProvider-derived transaction type (e.g., transaction_created_auth_pending).
transaction_id
stringRequiredUnique transaction identifier. Use to correlate with /cards/txs responses and deduplicate webhook deliveries.
card_id
stringRequiredCard identifier.
tx_at
stringdate-timeRequiredTransaction time (ISO 8601).
card_tx_data
objectRequiredNormalized transaction fields.
JSON
{
  "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_otp

OTP payloads are emitted from verification flows when the incoming method is OTP.

FieldTypeRequiredDescription
type
stringRequiredEvent type. Enum: card_otp
delivery_id
stringRequiredStable per-delivery identifier. The same value is reused on retries.
card_id
stringRequiredCard identifier.
code
stringRequiredOne-time password.
sent_at
stringdate-timeRequiredOTP send time (ISO 8601).
JSON
{
  "type": "card_otp",
  "delivery_id": "67f3f91c0f9b7462a5d7d0a4",
  "card_id": "697c9b7559fad5ba001068ce",
  "code": "687524",
  "sent_at": "2026-02-03T22:37:11.597606+00:00"
}

Card 3DS

card_3ds

OOB 3DS confirmations carry the securement identifier that the client must pass back to /api/v2/cards/3ds/confirm.

FieldTypeRequiredDescription
type
stringRequiredEvent type. Enum: card_3ds
delivery_id
stringRequiredStable per-delivery identifier. The same value is reused on retries.
card_id
stringRequiredCard identifier.
securement_id
stringRequiredProvider securement identifier. Use it to confirm the 3DS flow.
method
stringRequired3DS method. Enum: OOB
confirm
stringRequiredConfirmation state from the provider payload.
status
stringRequired3DS status from the provider payload.
currency
stringRequiredSettlement currency (ISO 4217).
amount
stringRequiredAmount as a string.
occurred_at
stringdate-timeRequiredEvent time (ISO 8601).
reason
stringOptionalOptional provider reason.
remark
stringOptionalOptional provider remark.
merchant
objectRequiredMerchant information.
JSON
{
  "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_id as the primary deduplication key on your receiver side.
  • Always respond quickly with {"success": true} once your handler has accepted the event.