> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thedecard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Events and data structures

> Event list, the common envelope, the data structure of each event, and the key points on idempotency and decryption. For configuration and signature verification, see Webhook configuration.

## 📄 Guide

Whether you are after card issuance progress, card status changes, real-time authorization results or the flow of KYC and tickets, a single webhook subscription covers all of it: the moment an event happens DCS pushes it to the `webhookUrl` you registered, so your own system stays in step with DCS without polling.

As a licensed issuer with its own BIN, DCS notifies you by webhook at every key point in the card lifecycle, in authorization and in compliance. All events share one envelope structure, so a single receiving endpoint on your side is enough to handle every event type.

> **Prerequisites**: before you can receive events you need to register your `webhookUrl`, get your IPs whitelisted, and be ready to verify `X-Signature` with your `secretKey`. For the configuration steps and the HMAC-SHA256 verification code, see [Webhook configuration](./configuration).

***

## Mental model: one envelope, many events

Every webhook arrives in the same **common envelope**: the outer layer tells you what the event is, which object it concerns and when it happened, while the inner `data` holds the payload specific to that event type. Your receiver only has to look at `webhookType` first and then parse the matching `data` structure.

<Frame>
  <img className="block dark:hidden" src="https://mintcdn.com/dcs-0bf7a937/oUoeUKtwWWK2o-Li/imgs/en/diagrams/pa-webhook-dispatch-light.svg?fit=max&auto=format&n=oUoeUKtwWWK2o-Li&q=85&s=024c93191b09d57067342893a572ecde" alt="Webhook verification and dispatch by event type" width="714" height="396" data-path="imgs/en/diagrams/pa-webhook-dispatch-light.svg" />

  <img className="hidden dark:block" src="https://mintcdn.com/dcs-0bf7a937/oUoeUKtwWWK2o-Li/imgs/en/diagrams/pa-webhook-dispatch-dark.svg?fit=max&auto=format&n=oUoeUKtwWWK2o-Li&q=85&s=7ec899959be768facd53489fd96e1383" alt="Webhook verification and dispatch by event type" width="714" height="396" data-path="imgs/en/diagrams/pa-webhook-dispatch-dark.svg" />
</Frame>

> **Idempotent deduplication**: the same event may be delivered several times because of network retries. Use the `webhookId` in the envelope as your idempotency key and acknowledge and skip any `webhookId` you have already processed, so you never double-post an entry or issue a card twice.

***

## Common envelope structure

Every webhook shares these outer fields:

| Field              | Type   | Always present | Description                                                                                                               |
| :----------------- | :----- | :------------- | :------------------------------------------------------------------------------------------------------------------------ |
| `webhookId`        | string | Yes            | Unique webhook ID, **use it as the idempotency key**                                                                      |
| `webhookType`      | string | Yes            | Event category, see the enum below                                                                                        |
| `webhookSubType`   | string | Yes            | Event subtype: `CREATE` (created) / `UPDATE` (updated)                                                                    |
| `businessId`       | string | Yes            | ID of the object the event concerns; what it means depends on `webhookType` (such as `cardOrderId` / `cardId` / `authId`) |
| `data`             | object | Yes            | Payload specific to the event, see the sections below                                                                     |
| `notificationTime` | string | Yes            | Notification time in the format `yyyy-MM-dd'T'HH:mm:ss+08:00`                                                             |

> **Time zones**: `notificationTime`, along with `createTime`/`modifyTime` inside each `data`, carries a `+08:00` offset (Singapore time), consistent with the UTC+8 convention used across the business time fields. Parse them using the offset the string carries.

### webhookType event enum

| Value                         | What the event means                                                                 | `data` structure                                            |
| :---------------------------- | :----------------------------------------------------------------------------------- | :---------------------------------------------------------- |
| `CARD_ORDER`                  | Progress on a card order (issuance, conversion to physical, replacement)             | [Card Order](#card-order)                                   |
| `CARD`                        | A card status changed                                                                | [Card](#card)                                               |
| `AUTHORISATION_RESULT`        | Authorization result (the final posting direction of a purchase or refund)           | [Authorization Result](#authorization-result)               |
| `AUTHORISATION_3DS_CHALLENGE` | 3DS challenge notification                                                           | [Authorization 3DS Challenge](#authorization-3ds-challenge) |
| `KYC_TICKET`                  | A KYC verification status changed                                                    | [KYC Ticket](#kyc-ticket)                                   |
| `KYC`                         | User-level KYC event (data has expired and needs renewing, or the renewal completed) | [KYC](#kyc)                                                 |

> `AUTHORISATION_RESULT` is the **receipt** for an authorization, which is a different thing from the **authorization forwarding request** that asks you to decide synchronously while the cardholder is paying. The latter is delivered in real time over `authUrl` (RSA two-way signing) and requires you to return approve or decline within a strict deadline. For how to handle those requests, see [Authorization forwarding](../transactions/authorization).

***

## The `data` structure of each event

### Card Order

`webhookType = CARD_ORDER`, the progress push for card orders such as issuance, virtual-to-physical conversion and replacement.

| Field           | Type    | Description                                                                                                               |
| :-------------- | :------ | :------------------------------------------------------------------------------------------------------------------------ |
| `cardOrderId`   | string  | Card order ID                                                                                                             |
| `profileId`     | string  | Card profile ID                                                                                                           |
| `type`          | string  | Card order type: `VIRTUAL` / `VIRTUAL_TO_PHYSICAL` / `REPLACEMENT`                                                        |
| `customerId`    | string  | User ID                                                                                                                   |
| `cardId`        | string  | Card ID, **populated if and only if `status=COMPLETED`**                                                                  |
| `status`        | string  | Card order status; the possible values depend on `type`, see the table below                                              |
| `errorCode`     | string  | Error code (on failure)                                                                                                   |
| `errorReason`   | string  | Error reason (on failure)                                                                                                 |
| `cardOrderRef`  | string  | Your idempotency key for the card order (the reference you passed in when creating it)                                    |
| `needExtraInfo` | boolean | Whether additional questionnaire information is required (`true` when KYC needs more documents; used when `type=VIRTUAL`) |
| `replaceCardId` | string  | The `cardId` being replaced (used when `type=VIRTUAL_TO_PHYSICAL` / `REPLACEMENT`)                                        |
| `createTime`    | string  | Creation time, `yyyy-MM-dd'T'HH:mm:ss+08:00`                                                                              |
| `modifyTime`    | string  | Time of the last update, same format                                                                                      |

**`status` values per `type`**:

| `type`                | Status sequence                                                                                                 |
| :-------------------- | :-------------------------------------------------------------------------------------------------------------- |
| `VIRTUAL`             | `PENDING` → `CUSTOMER_PASS` → `KYC_PASS` → `CHANNEL_CUSTOMER_PASS` → `COMPLETED` (success) / `FAILED` (failure) |
| `VIRTUAL_TO_PHYSICAL` | `PENDING` → `PHYSICAL_SETTING_COMPLETED` → `COMPLETED` / `FAILED`                                               |
| `REPLACEMENT`         | `PENDING` → `COMPLETED` / `FAILED`                                                                              |

> For the full card order state machine, the meaning of each `errorCode` and what to resubmit, see [Card issuing](../cards/card-issuing) and [Card order error codes](../cards/card-order-codes).

### Card

`webhookType = CARD`, pushed when the card's own status changes (frozen, blocked, canceled and so on).

| Field          | Type   | Description                                                                        |
| :------------- | :----- | :--------------------------------------------------------------------------------- |
| `cardId`       | string | Card ID                                                                            |
| `enterpriseId` | string | Enterprise ID                                                                      |
| `profileId`    | string | Card profile ID                                                                    |
| `type`         | string | Card type: `PHYSICAL` / `VIRTUAL`                                                  |
| `customerId`   | string | User ID                                                                            |
| `status`       | string | Card status: `PENDING_ACTIVATION` / `ACTIVATED` / `FROZEN` / `BLOCKED` / `INVALID` |
| `errorCode`    | string | Error code                                                                         |
| `errorReason`  | string | Error reason                                                                       |
| `panFirst6`    | string | First 6 digits of the card number                                                  |
| `panLast4`     | string | Last 4 digits of the card number                                                   |
| `createTime`   | string | Creation time, `yyyy-MM-dd'T'HH:mm:ss+08:00`                                       |
| `modifyTime`   | string | Time of the last update, same format                                               |

> Webhooks **never** carry sensitive data such as the full card number or the CVV. For the card state machine (`FROZEN` can be lifted by you, `BLOCKED` only by DCS) and for `statusReason`, see [Card management](../cards/card-management).

### Authorization Result

`webhookType = AUTHORISATION_RESULT`, the receipt for how an authorization was posted.

| Field                      | Type   | Description                                                                                              |
| :------------------------- | :----- | :------------------------------------------------------------------------------------------------------- |
| `authId`                   | string | Authorization ID                                                                                         |
| `approveFlag`              | string | Authorization outcome: `A` (approve) / `D` (decline)                                                     |
| `rejectReason`             | string | Reason for the decline (when `approveFlag=D`)                                                            |
| `approveDate`              | string | Time the authorization was approved, `yyyy-MM-dd'T'HH:mm:ss+08:00`                                       |
| `cardId`                   | string | Card ID                                                                                                  |
| `direction`                | string | Direction of funds: `OUTGOING` (a purchase, money out) / `INCOMING` (a refund or reversal, money in)     |
| `authType`                 | string | Authorization type: `NORMAL` / `FORCE_AUTH` / `EXPIRED_RELEASE` / `STATUS_DIFF_RELEASE`                  |
| `outsId`                   | string | Outstanding (bill) ID that ties the authorization to the later settlement                                |
| `originalAuthId`           | string | Original authorization ID, linking back to the original record (reversal and partial reversal scenarios) |
| `customerId`               | string | User ID                                                                                                  |
| `currency`                 | string | Transaction currency                                                                                     |
| `amount`                   | number | Transaction amount                                                                                       |
| `acquirerCurrency`         | string | Acquirer currency                                                                                        |
| `acquirerAmount`           | number | Acquirer amount                                                                                          |
| `cardAcceptorNameLocation` | string | Merchant name and address                                                                                |
| `merchantType`             | string | MCC (merchant category code)                                                                             |
| `transactionType`          | string | Transaction type                                                                                         |

> **Pushed for approvals and declines alike**: DCS pushes this event whether the authorization was approved or declined (`approveFlag=A`/`D`). A decline you return on `authUrl` (`01`/`11`/`21`) is pushed as `approveFlag=D`; a failed DCS pre-check (a frozen or canceled card, for example, where the request was never forwarded to `authUrl`) is also pushed as `approveFlag=D`. You can therefore build a complete authorization ledger from this webhook alone, with the next day's authorization report as the reconciliation backstop.
>
> For how `authType`, `direction` and `originalAuthId` combine in returns, incremental authorizations, multi-part settlements and similar scenarios, see [Authorization and settlement](../transactions/auth-and-settlement) and [Capture scenarios](../transactions/capture-scenarios).

### Authorization 3DS Challenge

`webhookType = AUTHORISATION_3DS_CHALLENGE`, the 3DS challenge notification. `flowsType` tells the two authentication modes apart:

* **`OOB` (out of band)**: the cardholder is sent to you to complete the verification. On receiving this webhook you guide the cardholder through authentication in your own app or another channel, then report the result back to DCS via API.
* **`OTP_DELEGATE`**: you send the verification code to the cardholder by SMS or email. DCS pushes the code to you in this webhook, and you **decrypt it** before sending it on to the cardholder.

| Field                 | Type   | Description                                                                                       |
| :-------------------- | :----- | :------------------------------------------------------------------------------------------------ |
| `challengeId`         | string | 3DS challenge ID                                                                                  |
| `status`              | string | Status: `INIT` / `NOTICED` / `RECEIVED` / `APPROVED` / `REJECTED`                                 |
| `cardId`              | string | Card ID                                                                                           |
| `expiryTime`          | string | Time the challenge expires                                                                        |
| `currency`            | string | Currency of the transaction that triggered it                                                     |
| `amount`              | number | Amount of the transaction that triggered it                                                       |
| `merchantId`          | string | Merchant ID                                                                                       |
| `merchantName`        | string | Merchant name                                                                                     |
| `merchantCountry`     | string | Merchant country                                                                                  |
| `mcc`                 | string | Merchant category                                                                                 |
| `flowsType`           | string | Authentication flow type: `OOB` / `OTP_DELEGATE`                                                  |
| `challengeMethodType` | string | Challenge method type: `DELEGATE_SCA_V1` / `SMS_OTP` / `EMAIL_OTP`                                |
| `otpPasscode`         | string | The verification code (**AES-GCM encrypted**), populated only when `flowsType=OTP_DELEGATE`       |
| `phoneNumber`         | string | Cardholder mobile number (**AES-GCM encrypted**), populated only when `flowsType=OTP_DELEGATE`    |
| `email`               | string | Cardholder email (**AES-GCM encrypted**), populated only when `flowsType=OTP_DELEGATE`            |
| `iv`                  | string | IV used for the AES-GCM encryption (Base64 encoded), populated only when `flowsType=OTP_DELEGATE` |

> **Decrypting the sensitive fields (`OTP_DELEGATE` only)**: DCS encrypts `otpPasscode`, `phoneNumber` and `email` with **AES-GCM** using your `secretKey`, and the webhook carries the matching `iv`. Decrypt them with `secretKey` plus `iv` before you send the verification code to the cardholder. For a reference AES/GCM implementation (12-byte IV, 128-bit authentication tag), see [Card management: reset PIN](../cards/card-management#reset-pin). For the full 3DS forwarding sequence, see [3DS forwarding](../transactions/3ds).

### KYC Ticket

`webhookType = KYC_TICKET`, a status change on a KYC verification ticket.

| Field             | Type   | Description                                                                                                                                                                                                     |
| :---------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kycTicketId`     | string | KYC ticket ID                                                                                                                                                                                                   |
| `kycTicketRef`    | string | Idempotency key of the KYC ticket                                                                                                                                                                               |
| `customerId`      | string | User ID                                                                                                                                                                                                         |
| `kycTicketStatus` | string | Ticket status: `INIT` / `NEED_VERIFY` / `PENDING` / `PASSED` / `REJECTED`                                                                                                                                       |
| `kycApplyMode`    | string | Application mode: `API` (submitted via API) / `H5` (KYC applied on the H5 page) / `H5-RENEWAL` (renewing KYC data) / `H5-MIGRATION` (migrating KYC data). Use it to tell which flow the notification belongs to |
| `errorCode`       | string | Error code, **returned only in the `REJECTED` state**                                                                                                                                                           |
| `errorMessage`    | string | Error description, **returned only in the `REJECTED` state**                                                                                                                                                    |

> The statuses a ticket can take differ per `kycApplyMode`: `H5-RENEWAL` and `H5-MIGRATION` only ever use `INIT` / `PASSED` / `REJECTED` and never `NEED_VERIFY` or `PENDING`. For each mode's status line, see [Updating KYC information](../kyc/kyc-renewal) and [KYC information migration](../kyc/kyc-migration).
>
> For the KYC state machine, the rejection codes and what to resubmit, see [Query KYC](../kyc/query-kyc) and [KYC rejection codes](../kyc/kyc-reject-codes).

### KYC

`webhookType = KYC`, a **user-level** KYC event: DCS has detected that the user's documents or KYC data have expired and need renewing, or that the renewal has completed. This is not the same as `KYC_TICKET`, which is ticket-level and tracks the progress of a single verification.

| Field                | Type      | Description                                                                                                              |
| :------------------- | :-------- | :----------------------------------------------------------------------------------------------------------------------- |
| `customerId`         | string    | User ID                                                                                                                  |
| `kycRenewalRequired` | boolean   | `true` = the KYC data needs renewing; `false` = the renewal is complete and the restriction is lifted                    |
| `kycRenewalType`     | string\[] | Which factors need renewing: `POI` (proof of identity) / `SELFIE`. Use it to prompt for only that item in your front end |

> For how to guide the user through a renewal after receiving `kycRenewalRequired=true`, see [Updating KYC information](../kyc/kyc-renewal); you can also call [Query a user's KYC information](../kyc/kyc-info) at any time to confirm.

***

## A complete webhook example

Taking a completed virtual card issuance as the example, the HTTP POST body you receive looks like this:

```json theme={null}
{
  "businessId": "co_1234567890",
  "data": {
    "cardId": "card_xxx",
    "cardOrderId": "co_1234567890",
    "cardOrderRef": "your-ref-001",
    "createTime": "2026-01-01T11:58:00+08:00",
    "customerId": "cus_xxx",
    "modifyTime": "2026-01-01T12:00:00+08:00",
    "needExtraInfo": false,
    "profileId": "prof_xxx",
    "status": "COMPLETED",
    "type": "VIRTUAL"
  },
  "notificationTime": "2026-01-01T12:00:00+08:00",
  "webhookId": "wh_7f3a1c9e",
  "webhookSubType": "UPDATE",
  "webhookType": "CARD_ORDER"
}
```

> The sample follows the actual lexicographic field order and is only pretty-printed for readability; the real payload is compact (see [Notes](#notes) below).

The headers carry `X-Signature` (HMAC-SHA256, computed over the whole body). On your side you should:

1. Verify `X-Signature` first and discard the request if verification fails;
2. Deduplicate on `webhookId`;
3. Dispatch to the right handler by `webhookType`;
4. Return HTTP 200 (any 2xx response counts as received); otherwise DCS redelivers according to its retry policy.

> Apart from the real-time `AUTHORISATION` channel, a general webhook is retried at most 3 times; retries are driven by a scheduled job and the backoff interval follows that job's configuration. The real-time `AUTHORISATION` channel is never retried and a timeout is treated directly as `responseCode=21`. The outbound DCS `Content-Type` is always `application/json`.

***

## Notes

### Payload format and field order

Webhook payloads are sent as compact JSON (no spaces, no line breaks). At every level (the common envelope, `data` and any nested objects) the fields are sorted in ascending **lexicographic order of the full field name** — compared character by character by character code, moving on to the next character when the previous ones are equal, case-sensitively. This ordering is a determinism guarantee of DCS-side serialisation that helps when you need to inspect a raw payload; parse the JSON normally and **do not rely on field order** to read values.

### Field extensions

New fields may be added to webhook payloads (the `data` of each event) in the future. Field extensions follow these compatibility commitments:

1. **Existing fields stay stable**: the fields already defined in this document never change their name, type or meaning, and are never removed;
2. **Extension happens only by adding fields**: partners can read the new fields as needed; if you do not need them yet, simply ignore them — existing parsing is unaffected;
3. **Ignore unknown fields when parsing**: do not parse webhook payloads in a strict mode that rejects unknown fields, so that new fields never break an existing integration.

### New fields and signature verification

`X-Signature` is computed over the **raw payload string** DCS actually sent. Compute the HMAC-SHA256 directly over the raw body as received and compare — do not parse the payload and regenerate JSON before verifying: the regenerated document can differ in missing fields, field order or formatting, and verification will fail. As long as you verify against the raw payload, new fields never affect the result. See [Webhook configuration](./configuration) for the verification procedure.

***

## Next steps

* Signature verification not set up yet? Start with [Webhook configuration](./configuration) and get a receiver that can verify `X-Signature`.
* Need to approve or decline in real time while the cardholder is paying? Read [Authorization forwarding](../transactions/authorization).
