# Compliance and the KYC flow
Source: https://docs.thedecard.com/en/decard-managed/basic-concepts/compliance-kyc-flow
The material the KYC flow needs, the markets cards can be issued in and the per-region document requirements; statuses and error codes are covered in the related sub-pages.
## π Guide
As a card issuer regulated by the Monetary Authority of Singapore (MAS) with its own BIN, DCS has full KYC/AML and Travel Rule capability built in β an end user must pass KYC before a card can be issued, topped up or spent, and cross-border fund movements must additionally satisfy the Travel Rule. We offer both a direct API integration and a hosted (H5) self-service flow, so you can choose how deep to integrate. In DeCard-managed mode the **review decision happens inside DCS** (automated review plus manual review where needed), so you never handle document retention, liveness detection or anti-money-laundering screening yourself.
***
## Compliance: supported markets and document requirements
Before issuing a card to an end user, settle two compliance facts with DCS first: whether cards can be issued in that user's region, and which documents they need to prepare. Both are decided by your agreement with DCS together with the user's region.
## What KYC consists of
DCS compliance requires four categories of material from each user:
1. **POI (proof of identity)** β establishes *who this person is*
2. **POA (proof of address)** β establishes *where this person lives*
3. **Liveness (face verification)** β establishes *that this is a real person, acting for themselves*
4. **Background information** (employment status, source of funds, account purpose and so on) β feeds the anti-money-laundering risk assessment
## KYC documents (POI and POA)
DCS whitelists the documents of certain countries and regions so that one document serves as both proof of identity (POI) and proof of address (POA) β a user whose document is whitelisted submits that single document plus a face check and background information, with no separate proof of address. If the identity document carries no address, the user must supply a POA as well. For the image requirements, see [KYC document requirements](../customer-success/kyc-documents).
## Cardholder eligibility
* **Age**: between 18 and 70
* **Tax status**: not a US resident and not a US tax resident
* **Residence**: only regions covered by your agreement with DCS can be issued cards
## Background information
AML rules require the user to submit the relevant information. For the complete enumerations, see [card application parameter dictionary](../how-to-use/managing-cards/application-data-dictionary).
| Item | Description / options |
| - | - |
| Employment status | Employed / business owner / self-employed / unemployed / retired / student |
| Industry, occupation, seniority | See the parameter dictionary |
| Account purpose | Everyday spending / investment / bill payment and so on |
| Source of funds, source-of-funds country | Own bank account / third-party account / crypto top-up and so on |
| Source of wealth | Employment income / savings / inheritance / business income and so on |
***
## Mental model: one application, one status line
A user (`externalUserId`) submits KYC once. DCS runs the automated review internally (roughly 3 minutes) plus manual review where needed, and the outcome always resolves to a single top-level `status` (five values in total). Within `PENDING`, the case may move from automated to manual review; that transition is expressed by the sub-status `statusDescription` rather than by two separate top-level states.
| Status | Meaning |
| - | - |
| `UNDO` | No KYC attempted; the user has not submitted any KYC material or application |
| `INIT` | Initial / in progress β the user has entered the KYC flow; the exact stage is broken out by `statusDescription` (material / POA / POI under review or rejected) |
| `PENDING` | Application submitted successfully and under review (awaiting automated review, or awaiting manual review) |
| `PASS` | KYC approved; the user is compliance-cleared |
| `REFUSE` | KYC not approved; the material does not meet requirements or failed verification |
> For how to tell "rejected but resubmission is possible" apart from "not approved", see [KYC rejections and resubmission](../customer-success/kyc-rejections).
The overall lifecycle of a user's KYC and compliance status:
> **Who does what**: DCS receives and securely stores documents, runs automated and manual review, performs AML screening, advances the status, and notifies you. The partner initiates the application, guides the user through the steps that need user participation (face verification, resubmission), and drives card issuing based on the outcome.
> Besides the API, DCS also offers a hosted H5 guidance page, where the user completes KYC (identity verification, face verification, resubmission) directly on a DCS-hosted page, so you do not need to build a KYC front end. See [H5 KYC / card application guidance page](../integration-resources/h5-kyc-guidance).
***
## Travel Rule: a separate gate beyond KYC
The Travel Rule is a compliance gate separate from KYC: before a user can obtain an on-chain deposit address, they must first upload their Travel Rule information (identity, address and, where required, source of funds or wealth). See [Travel Rule](../how-to-use/virtual-accounts/travel-rule).
***
## Sumsub token integration (data provider)
* **You are the data provider**: obtain the UAT and PROD tokens from DCS and add them in the Sumsub console; your registered Sumsub name is visible in the DCS Sumsub console.
For more on Sumsub and KYC vendor integration, see [Sumsub KYC data sharing](../customer-success/kyc-vendor); Sumsub's own documentation is at [Manage sharing partners](https://docs.sumsub.com/docs/manage-sharing-partners).
***
## Next steps
Once KYC is approved (`PASS`), you can apply for a virtual card for the user β continue with [How-to Β· Issuing cards](../how-to-use/managing-cards/issuing-cards). If users are to top up themselves, complete [Travel Rule](../how-to-use/virtual-accounts/travel-rule) first.
> The other two pages in this Basic Concepts group β [Account and asset model: ledgering](./ledgering-system) and [Transaction lifecycle](./transaction-lifecycle) β are background reading and do not block card issuing; you can come back to them before you integrate balances and transactions.
# Accounts and Asset Model
Source: https://docs.thedecard.com/en/decard-managed/basic-concepts/ledgering-system
A concept page on the account and asset model: per-user balances (free / freeze / total), booking per user and per asset, and custody strategy. For the actual API calls and field details, go to the matching How-to page (see "Next steps" at the end). This page does not repeat the field tables.
## π Guide
In DeCard-Managed mode, DCS holds a dedicated funding ledger for every user. Both the reservation of funds (an authorization hold) and the deduction of funds (settlement) act directly on that user's balance, with no authorization request forwarded to the partner. As a licensed issuer running its own BINs, DCS therefore keeps regulated custody and booking of user assets entirely inside its own systems.
DCS holds the user's spendable balance directly and describes the state of those funds at any point in time with three numbers: **available, frozen, and total**.
## Balance model (free / freeze / total)
Each user account is booked separately **per currency (asset)**. A call to `/user-asset/v1/balance` returns one balance object per asset:
| Field | Meaning | Description |
| - | - | - |
| `free` | Available balance | The part currently available to spend or withdraw |
| `freeze` | Frozen balance | The part held by an authorization and not yet released by settlement |
| `total` | Total | Normally equal to `free + freeze` |
> The balance object also carries `asset`, `network`, and `logo`. The assets known today are `USDT`, `USDC`, and `USD`. The list may grow, so always rely on what the balance endpoint actually returns rather than hard-coding today's list as the permanent set.
> **Terminology**: what the business side calls "available balance" and "frozen balance" are the API fields `free` and `freeze`. This documentation set uses the API field names `free` / `freeze` / `total` as canonical and glosses their plain meaning in parentheses where useful.
Funds move between `free` and `freeze` as a transaction progresses:
* **Authorization (hold)**: when a purchase is approved, the amount moves from `free` into `freeze`. The funds are held but do not leave the account, so `total` is unchanged.
* **Settlement (debit)**: once the merchant submits the final amount, the actual deduction takes place, the matching `freeze` is released, and the account balance drops accordingly.
* **Deposits and refunds**: incoming funds land in `free`.
* **Fee debits**: card production fees (`CARD_PRINTING_FEE`), postage fees (`CARD_POSTAL_FEE`), freeze fees (`CARD_VIP_FROZEN_FEE`), and similar charges are debited straight from `free`. `free` goes down and `total` goes down with it, without passing through `freeze`, unlike the two-step authorization-then-settlement path of a purchase.
> For how the two stages affect balances, see [Authorizing Transactions](../how-to-use/managing-transactions/authorizing-transactions) and [Settlement](../how-to-use/managing-transactions/settlement).
## Deposits and debits (credit / debit)
In DeCard-Managed mode, per-user balances are adjusted and queried through a set of user-asset endpoints:
| Endpoint | Purpose |
| - | - |
| `POST /user-asset/v1/credit` | Credit (increase a user balance) |
| `POST /user-asset/v1/debit` | Debit (decrease a user balance) |
| `GET /user-asset/v1/balance` | Query a user balance (free / freeze / total) |
| `GET /user-asset/v1/transactions` | List asset movements |
| `POST /user-asset/v1/transaction-detail` | Retrieve one movement in detail |
| `GET /user-asset/v1/transfer-query` | Look up a transfer result by externalTranId |
`credit` and `debit` take the same request body: `{ externalTranId, asset, amount, externalUserId, remark }`. Here `externalTranId` is the unique idempotency ID, `asset` must be a currency that DCS supports, and `amount` must be positive with at most 18 decimal places.
> For the full field tables and runnable examples, see How to Use βΊ [User Balance](../how-to-use/managing-transactions/user-balance) and [Managing Transactions](../how-to-use/managing-transactions/overview).
## Movement types (transaction type)
`POST /user-asset/v1/transactions` and `POST /user-asset/v1/transaction-detail` use the `type` field to mark the business nature of each movement, effectively the ledger entry type:
| type value | Meaning | Direction |
| - | - | - |
| `DEPOSIT` | Deposit | Balance increases |
| `WITHDRAW` | Withdrawal | Balance decreases |
| `CARD_PRINTING_FEE` | Card production fee | Balance decreases |
| `CARD_POSTAL_FEE` | Postage fee | Balance decreases |
| `CARD_VIP_FROZEN_FEE` | VIP freeze fee | Balance decreases |
| `CONVERSION` | Currency conversion | Balance changes (two currencies) |
| `REWARD_DISTRIBUTION` | Reward distribution | Balance increases |
| `REWARD_PAY` | Reward payment | Balance decreases |
| `REWARD_REFUND` | Reward refund | Balance increases |
| `MIGRATION` | User asset migration | Balance changes |
## Key principles
1. **Available and frozen are kept apart**: an authorization moves funds from `free` into `freeze` immediately, reserving the limit ahead of actual settlement so a user cannot overspend; settlement then releases the hold from `freeze` and completes the debit.
2. **Booked per user and per asset**: every asset of every user has its own `free`, `freeze`, and `total`, with no commingling.
3. **Idempotency**: both credit and debit use `externalTranId` as the idempotency key, so a repeated request is rejected.
## Next steps
* The mental model for the transaction flow (two stages, the Outstanding bridge, the invariants): [Transaction Lifecycle Β· Overview](./transaction-lifecycle)
* How an authorization freezes funds: [Authorizing Transactions](../how-to-use/managing-transactions/authorizing-transactions)
* How funds are actually debited: [Settlement](../how-to-use/managing-transactions/settlement)
* Querying user balances (free / freeze / total): [User Balance](../how-to-use/managing-transactions/user-balance)
* Crypto deposits, chains, and assets: [Crypto Deposit](../how-to-use/virtual-accounts/crypto-deposit)
* Crypto withdrawals: [Crypto Withdrawal](../how-to-use/virtual-accounts/withdraw-offramp)
# Overview (Access Credentials and Developer Configuration)
Source: https://docs.thedecard.com/en/decard-managed/basic-concepts/overview
Covers the three developer configurations you complete before integration: apiKey / secretKey credential provisioning (including secure extraction in production), API IP whitelisting, and Webhook setup.
## π Guide
Before you start integrating, you need a set of access credentials from DCS, plus registered API whitelisting and callback (Webhook) settings. As a licensed issuer with its own BINs, DCS handles credential provisioning, IP whitelist registration, and Webhook setup jointly with your team, so that every production key is delivered securely. The three steps are described below.
## Credential provisioning: apiKey / secretKey
DCS provisions one set of access credentials per Enterprise (the entity you integrate under). These credentials gate every API call:
| Field | Description |
| - | - |
| `apiKey` | The key used to call open-api; identifies you and lets DCS trace the origin of each request |
| `secretKey` | The key used to sign requests (HmacSHA256). Store it securely and never disclose it |
The DCS team hands the credentials over to you:
* **Sandbox**: contact the DCS team directly for your `apiKey` / `secretKey`.
* **Production**: credentials are delivered through a secure email plus a one-time extraction link (see [Secure extraction of production credentials](#secure-extraction-of-production-credentials)), so that they are never exposed to a third party in transit.
> For how the credentials feed into request signing (`X-DAPI-API-KEY` / `X-DAPI-SIGN` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE`, and the HmacSHA256 signing rules), see [Integration Resources βΊ Authentication Guide](../integration-resources/overview).
### Secure extraction of production credentials
To keep the production `apiKey` / `secretKey` from reaching any third party, production credentials are extracted once, through the following flow:
1. Give the DCS team a **secure email address** (which receives the extraction instructions) and the **IP address that will perform the extraction** (added to the extraction whitelist).
2. DCS sends an email to that address containing a **temporary secure link, valid for a single extraction**.
3. Concatenate `extractUrl` with `extractSecretKey` and run it on the designated machine to retrieve the `apiKey` / `secretKey`.
Fields contained in the email:
| Field | Type | Description |
| - | - | - |
| `expireTime` | string | Validity period of the extraction security code |
| `extractSecretKey` | string | Extraction security code |
| `extractUrl` | string | Extraction URL |
| `howToUse` | string | How to use the link |
| `notes` | string | Reminder that the link can only be used once |
Successful response from the extraction flow (keys, URL, and timestamp below are placeholders):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": {
"expireTime": "",
"extractSecretKey": "",
"extractUrl": "https:///internal/open-api/v1/secret-extract/",
"howToUse": "Please concatenate the url with the secret-key and execute it on the specified machine.",
"notes": "This link is only valid for one AKSK extraction. If the content is not properly accessed, the AKSK may have been compromised, please contact us promptly."
}
}
```
> **Response envelope**: every endpoint returns `{code, message, messageDetail, data}`. The success code literal is `SYS_SUCCESS`; there is no `success` boolean field.
> **Security note**: the temporary link **can only be used once**. If the extraction does not succeed, assume the keys may already have been exposed within the validity window; contact your DCS business contact immediately and have a new email issued.
## API whitelisting (IP Whitelisting)
DCS enforces **API whitelisting**: only registered network egress addresses may call open-api. When you request credentials, also send the DCS team your **network egress addresses**:
* Sandbox egress IP
* Production egress IP
> For more on IP whitelisting (multiple addresses, change procedure, and so on), see [Integration Resources βΊ IP Whitelisting](../integration-resources/ip-whitelisting).
## Webhook setup
DCS pushes events such as KYC status changes, asset movements, card transactions, and order status through Webhooks. You implement the receiver, then the DCS team registers it:
1. **Implement the receiver**: expose a POST endpoint that accepts `application/json` and returns `200 OK` within 2 seconds (any other status code triggers a retry).
2. **Send the endpoint to the DCS team**: give DCS your callback URL (HTTPS, publicly reachable) and DCS registers it.
3. **Verify and go live**: go live once verification in UAT passes.
DCS signs each Webhook by computing `X-Signature` over the message body with HmacSHA256 using your `secretKey`; you verify it. For the full event format, signature verification, and receiver examples, see [Integration Resources βΊ Webhook and WebSocket Real-Time Notifications](../integration-resources/webhook-websocket).
## Provisioning and go-live sequence
The diagram below shows the joint path from receiving credentials to putting Webhooks into production:
## Next steps
* Complete credential provisioning and request signing: [Integration Resources βΊ Authentication Guide](../integration-resources/overview)
* Register your API whitelist: [Integration Resources βΊ IP Whitelisting](../integration-resources/ip-whitelisting)
* Configure event callbacks: [Integration Resources βΊ Webhook and WebSocket Real-Time Notifications](../integration-resources/webhook-websocket)
# Transaction Lifecycle Β· Overview
Source: https://docs.thedecard.com/en/decard-managed/basic-concepts/transaction-lifecycle
The mental model for the transaction flow. It covers only how the core path, authorization (freeze) then settlement (posting), works under the DeCard-Managed model; endpoints, field tables and scenario-by-scenario walkthroughs are linked out to the How-to guides.
## π Guide
Under the DeCard-Managed model, every movement of card funds follows the same core path: **authorization (freeze) then settlement (posting)**. Once you understand this path, you can account for any change in a user's balance and build your own reconciliation logic on top of it.
Unlike the Partner-Managed model, **authorization decisions in the DeCard-Managed model are made inside DCS**: when the cardholder pays, the system checks that user's own balance, card status and limits, and approves or declines on the spot. No authorization request is forwarded to the partner, and there is no synchronous callback requiring the partner to respond per transaction. Authorization results and balance changes are **notified** to you by webhook, and both the hold and the deduction land on that user's own balance.
## Two stages, one bridge
Each user account is ledgered per currency and described at any point in time by three figures: **available balance (`free`), frozen balance (`freeze`) and total (`total`)**. A transaction rewrites these three figures in two stages, with the **Outstanding record** acting as the bridge between them.
| Stage | Effect on balances | Notes |
| - | - | - |
| **Authorization (freeze)** | `free` moves into `freeze`, `total` unchanged | The decision is made internally; on approval the corresponding amount is frozen. **Funds are frozen but never leave the account**, no actual deduction takes place |
| **Settlement (posting)** | Releases the `freeze` and deducts the final amount from `total` | Once the merchant submits the final amount, the transaction moves **from outstanding to posted**; the funds were already frozen, so settlement draws down the frozen amount and the total β the available balance is untouched |
The bridge is **Outstanding**: authorization creates an outstanding record that holds the frozen amount, and settlement advances it to posted and releases the freeze. Each transaction record carries `postIndicator` (`1` for posted, `0` for outstanding) to indicate its posting state; if you only have a transaction ID and are unsure which type it is, call `POST /card/v1/transaction/id/resolve` to resolve it (the response includes a `posted` boolean).
> **Invariant**: once a transaction is **fully settled, the amount frozen at authorization converges to 0** β any excess hold (partial settlement) returns to the available balance automatically. This is what makes reconciliation closable: as long as a transaction is not yet posted and its freeze has not been released, that transaction is not settled.
## Authorization types at a glance (authType)
Authorization results are delivered by the `CARD_TRANSACTION` webhook, where `authType` states the nature of the authorization. The DeCard-Managed model uses only the following three.
| Enum value | Name | Meaning for balances |
| - | - | - |
| `EXPEND` | Purchase | Freezes the corresponding amount (`free` β / `freeze` β) |
| `REFUND` | Refund | Credit for a return (the final credit lands in `free` at settlement) |
| `REVERSAL` | Purchase reversal | Releases the original purchase hold (`freeze` β / `free` β) |
> The direction of an authorization is carried by `direction` (`DEBIT` for a debit, `CREDIT` for a credit); reversing transactions (void, reversal, refund) return `originalExternalTranId` to link back to the original transaction. For the full field tables and the step-by-step sequences of the three scenario types, see [Authorizing Transactions](../how-to-use/managing-transactions/authorizing-transactions).
## One core path, several settlement shapes
In the real world, authorizations and settlements do not always match one to one: a merchant may settle for less (tip dropped), for more (tip or surcharge added), ship in several batches, or settle with no prior authorization at all. All of these are just different ways the same authorization-then-settlement path releases holds and posts amounts. What differs is **how the frozen amount is released and what amount is posted**.
| Shape | Trigger | What happens to the hold and the posting |
| - | - | - |
| Standard settlement | Settled in full for the authorized amount | Releases the full hold, posts the authorized amount, closed |
| Partial settlement | Settled amount \< authorized (for example, tip dropped) | Releases the **full** hold but posts only the actual amount; the difference returns to the available balance |
| Overage settlement | Settled amount > authorized (dining or hotel with tip) | After card network and MCC rule checks, releases the original hold and posts the actual amount |
| Multiple settlement | One authorization settled in several batches (staged e-commerce shipments) | Keeps the hold until every batch has settled, then releases and posts in one go |
| Force settlement | Settled with no prior authorization (offline or in-flight scenarios) | No hold to release; posts directly against the available balance |
| Refund | A negative transaction credited back to the account (may not reference an original transaction) | Posts directly and increases the available balance |
> For the sequence diagram, field definitions and hands-on reconciliation of each shape, see [Settlement](../how-to-use/managing-transactions/settlement).
## Where balances are ledgered and where authorization is decided
This page covers only the core mental model. The detail at each end lives on its own page.
* **Balance model** (`free` / `freeze` / `total`, per-user per-asset ledgering, custody strategies): see [Account and Asset Model (Ledgering)](./ledgering-system).
* **Authorization decisions** (the checks made inside the system, webhook notification fields, sandbox simulation): see [Authorizing Transactions](../how-to-use/managing-transactions/authorizing-transactions).
* **Settlement and posting** (the five settlement shapes, statement queries, asset movement records, ID resolution): see [Settlement](../how-to-use/managing-transactions/settlement).
## Next steps
* To see how an authorization freezes funds inside the system and which webhooks you receive, read [Authorizing Transactions](../how-to-use/managing-transactions/authorizing-transactions).
* To see how funds are actually posted, how statements are issued and how to query settlement results, read [Settlement](../how-to-use/managing-transactions/settlement).
* For the full picture of available and frozen balances and of custody strategies, read [Account and Asset Model (Ledgering)](./ledgering-system).
# Escalations and Support Paths
Source: https://docs.thedecard.com/en/decard-managed/customer-success/escalations-and-support-paths
Explains when and how to escalate an issue to DCS: the escalation channel and the reference fields to include, the categories of scenarios worth reporting, emergency response to unauthorized use, how disputes are handled, and a self-service troubleshooting index.
## π Overview
In the DeCard-Managed model, **both KYC review and authorization decisions are made by DCS inside the system**, and user assets are held in each user's own account (available balance and frozen balance). Most operational questions can be answered from this documentation set on your own; when you hit a scenario that needs DCS to investigate, coordinate, or intervene manually, report it through the path on this page.
***
## 1. How to escalate an issue to DCS
Report through the **Customer Success channel** agreed with DCS during onboarding (a dedicated chat group, email, or a ticketing system, whichever was agreed).
To help the DCS team locate and resolve the issue quickly, include as much of the following reference information as applies to the issue type. All of these are identifier fields from the DeCard-Managed API:
| Reference information | Field in the interface definition | Description |
| :- | :- | :- |
| User identifier | `externalUserId` | User ID (the DeCard-Managed model uses `externalUserId`; there is no `customerId`) |
| Card identifier | `cardId` | Card ID (string) |
| Last four digits of the card number | `cardMantissa` | Last 4 digits of the card number (this model uses `cardMantissa`; there is no `panLast4`) |
| Transaction identifier | `transId` / `orderId` | Identifier of a single transaction or order (for example a QR Pay order or entry). If you need to resolve a transaction ID into its posting status and related records, use `POST /card/v1/transaction/id/resolve`, whose input is `ids[]` (values are `outstandingTransactionId` / `postedTransactionId`); that is unrelated to the `transId` / `orderId` in this table |
| Statement identifier | `statementId` | Identifier of a statement or a statement line item |
| Symptoms and evidence | β | Reproduction steps, timestamps, error messages, and the relevant response bodies (masked) |
**Field note**: the DeCard-Managed model **does not have** the fields `transactionId` / `authId` / `outsId` / `customerId` / `panLast4`; those belong to the Partner-Managed model. Use the fields in the table above when reporting, to avoid confusion.
**PII red line**: report only the identifier fields needed to locate the case. If you must attach an example, mask it (for example `externalUserId` = `usr_xxx`, card last four = `1234`, email = `u***@example.com`). **Never paste a user's real phone number, identity document, full card number, address, or other personal data in plain text into a chat group or a ticket.**
***
## 2. When to escalate an issue to DCS
Report to DCS when you hit one of the scenarios below and self-service troubleshooting with this documentation set does not resolve it. The list is organized around the independent-account custody model, and covers the asset, deposit and withdrawal, transfer, and mobile wallet issues specific to the DeCard-Managed model:
### Assets, deposits, and withdrawals (specific to this model)
* **A crypto deposit has not been credited for a long time**: the transfer is confirmed on chain but the user's **available balance has not increased**, well beyond a reasonable window. Report with the `externalUserId` plus the chain, token, and on-chain transaction hash of the deposit. For the troubleshooting path, see [Crypto Deposits](../how-to-use/virtual-accounts/crypto-deposit).
* **A crypto withdrawal has not arrived for a long time**: the on-chain withdrawal has been initiated but has not arrived, well beyond a reasonable window. For the troubleshooting path, see [Crypto Withdrawals](../how-to-use/virtual-accounts/withdraw-offramp).
### Cards and spending
* **A user reports an unauthorized transaction (suspected fraud)**: treat this as high priority. **Freeze the card concerned first**, then report to DCS for follow-up; see "Emergency response to unauthorized use" below.
* **QR Pay or mobile wallet (Apple Pay / Google Pay) provisioning or payment failures**: the user cannot add the card to a mobile wallet, or QR payments keep failing. Report with the `externalUserId` and `cardId` plus a description of the failure.
* **You want a suspicious merchant blocked**: provide the **merchant identifier and the reason for blocking**, and DCS will handle it at the risk-control or card network level (there is no self-service merchant-blocking endpoint for integrators in this model).
### KYC and card review
* **A KYC application has been stuck in review for a long time**: the documents are correct but the application stays in review well beyond a reasonable window. Report with the `externalUserId` and the KYC ticket identifier.
* **Transaction disputes or refund issues**: see "How disputes are handled" below.
***
## 3. Emergency response to unauthorized use (suspected unauthorized transactions)
When a user reports suspected fraud or an unauthorized transaction, **containing the loss comes first**:
1. **Freeze the card concerned first**: call `POST /card/v2/block`, identify the card by `cardId`, and set `block` = `true` in the body (this model uses the `block` field, **not** `freeze`, and has no `/freeze` endpoint). Once frozen, new authorizations on that card are declined. For the full description of freezing and unfreezing, see [Managing Cards Β· Overview](../how-to-use/managing-cards/overview).
2. **Identify the transaction concerned**: get its identifier from the transaction or statement queries (for example `transId` / `orderId`, or a posted / outstanding transaction ID). If you need to resolve a transaction ID into its posting status and related records, use `POST /card/v1/transaction/id/resolve`, whose input is `ids[]` (values are `outstandingTransactionId` / `postedTransactionId`); it returns whether that ID has been posted and the related transaction ID.
3. **Escalate to DCS along the path above**: include the `externalUserId` and `cardId`, the identifier of the transaction concerned (`transId` / `orderId`, or the posted / outstanding transaction ID you resolved), and the reproduction details, and hand the case to DCS to follow up.
Freeze request body (masked placeholders):
```json theme={null}
{
"externalUserId": "usr_xxx",
"block": true,
"cardId": "card_xxxxxxxx"
}
```
Successful response (masked placeholders):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": false
}
```
> In the interface definition, the required fields of `POST /card/v2/block` are `externalUserId`, `block`, and `cardId`; unfreezing (`block=false`) additionally requires either `smsCode` or `emailCode` for verification (freezing to contain a loss needs no verification code). The response envelope is always `{ code, message, messageDetail, data }` with the success code `code` = `SYS_SUCCESS`; `messageDetail` may be `null` or a structured object (containing `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`). For the complete freeze and unfreeze fields and examples, see [Managing Cards Β· Overview](../how-to-use/managing-cards/overview) and [Transaction Issues and Disputes](./transaction-issues-disputes).
***
## 4. How disputes are handled
**Capability boundary (important)**: the DeCard-Managed model currently **does not provide** a dispute or chargeback API for integrators to raise a dispute or track its progress in the system.
Disputes, refunds, and unauthorized-use cases in this model are therefore handled as follows:
* **Today**: DCS **helps you submit and follow up** through the Customer Success channel, and handles the case at the card network and risk-control level according to the established process.
* **A self-service dispute API in the system**: on the roadmap.
For the full description and handling path, see [Transaction Issues and Disputes](./transaction-issues-disputes) in the FAQ group. This page **does not describe any self-service dispute endpoint that does not exist** in this model.
***
## 5. Reference codes and troubleshooting index (where to look)
Before escalating, use the table below to locate the answer yourself; most questions have a matching troubleshooting page in this documentation set:
| What you want to look up | Where to look |
| :- | :- |
| Transaction and authorization fields and scenarios (`DEBIT` / `CREDIT`, refunds, settlement) | [Real-Time Authorization](../how-to-use/managing-transactions/authorizing-transactions), [Managing Transactions Β· Overview](../how-to-use/managing-transactions/overview) |
| User balances (available / frozen) and the asset model | [User Balance](../how-to-use/managing-transactions/user-balance) |
| Crypto deposits, deposit addresses, the chain and token matrix, crediting | [Crypto Deposits](../how-to-use/virtual-accounts/crypto-deposit) |
| Meaning of reconciliation and report fields | [Reporting Field Descriptions](../how-to-use/managing-transactions/reporting-field-descriptions) |
| KYC rejection reasons | [KYC Rejections and Resubmissions](./kyc-rejections) |
| Card status, freezing, cancellation | [Managing Cards Β· Overview](../how-to-use/managing-cards/overview) |
| Physical card shipping progress | [Physical Card Shipping](./physical-card-shipping) |
| Decline code dictionary / error code dictionary | Responses from endpoints such as simulated authorization, card application, deposit, and withdrawal may carry `data.errorCode` / `data.errorMsg`, but there is no central dictionary page for this product explaining what each code means; for now, judge from the response `code` / `message` and escalate along the path above |
**On the error code dictionary**: this product has no central dictionary page explaining each code. Judge from the `code` / `message` the API actually returns, and escalate along the path above when needed.
***
## Next steps
* For common questions and handling paths for transaction problems, refunds, and disputes, see [Transaction Issues and Disputes](./transaction-issues-disputes).
* For consolidated answers to common operational questions after launch, see [Customer Success βΊ Pre-Go-Live FAQ](./pre-go-live) and [Post-Go-Live FAQ](./post-go-live).
* For the complete freeze, unfreeze, and cancellation operations on a card, see [Managing Cards Β· Overview](../how-to-use/managing-cards/overview).
# Overview
Source: https://docs.thedecard.com/en/decard-managed/customer-success/faq-overview
The entry point for this FAQ section, bringing together the questions that come up most often when integrating and running the DeCard-Managed card issuing service, grouped by identity verification and compliance, physical card shipping, transactions and disputes, and escalation paths.
This section collects the questions that come up most often when integrating and running the DeCard-Managed card issuing service, covering identity verification and compliance, physical card shipping, transaction and dispute handling, and escalation and support paths. Use the cards below to jump to the topic you need; if this is your first integration, start with [Getting started βΊ Overview](../getting-started/overview) and [Integration resources βΊ Overview](../integration-resources/overview).
## In this section
* [Identity verification and compliance](./verification-and-compliance) β questions about end-customer KYC, acceptable documents, proof of address (POA) and compliance.
* [Physical card shipping](./physical-card-shipping) β questions about physical card shipping, including where we ship, delivery methods, and activation on arrival.
* [Transaction issues and disputes](./transaction-issues-disputes) β questions and handling paths for transaction anomalies, refunds and disputes.
* [Escalations and support paths](./escalations-and-support-paths) β the escalation channels and support contacts to use when something goes wrong during integration or day-to-day operation.
## Next steps
* For how KYC states progress, which documents are acceptable, and the Travel Rule, see [Basic concepts βΊ Compliance βΊ Overview](../basic-concepts/compliance-kyc-flow).
* For real-time authorization, clearing and the account asset model, see [Basic concepts βΊ Transaction lifecycle βΊ Overview](../basic-concepts/transaction-lifecycle).
* For integration configuration such as authentication, webhooks and IP whitelisting, see [Integration resources βΊ Overview](../integration-resources/overview).
# KYC document requirements
Source: https://docs.thedecard.com/en/decard-managed/customer-success/kyc-documents
The identity documents (POI) accepted in each country or region, together with the validity and file requirements.
## π Guide
## Core rules
* **Whitelisted identity documents**: the user submits a single document, completes face verification and fills in the required background information β that is the whole verification.
* **Non-whitelisted identity documents**: the user must submit a passport (proof of identity) plus one proof-of-address document, and complete face verification and background information.
* **Cardholder age**: between 18 and 70.
* **Document validity**: an identity document must have at least 6 months of validity remaining; a proof of address must have been issued within the last 3 months.
* **No screenshots**: every document must be a photo or scan of the original β screenshots are not accepted.
## Proof of identity (POI)
### POI basic requirements
* **Age**: the cardholder must be between 18 and 70.
* **Validity**: the identity document must have at least 6 months of validity remaining.
The document must carry all of the following, legibly:
* Full name
* Date of birth
* Photo
* Document number
* Issue / expiration date
* A legible MRZ (machine-readable zone)
### Upload requirements
Uploaded files must satisfy all of the following:
* An original photo β screenshots are not accepted
* Accepted formats: JPG, JPEG, PNG, PDF
* At least 100 KB, or a resolution of 300 DPI
* No larger than 20 MB
* Must be in colour
* Sharp, good quality and legible
* All four corners must be visible
* One or both sides of the document
* Do not upload anything retouched or altered in image-editing software
### Accepted documents by country / region
The table below lists the identity documents accepted in each country or region, and whether that document still requires a separate proof of address (POA).
| Country / region | Accepted identity document | Separate POA required |
| - | - | - |
| Philippines PHL | Philippine national ID | No |
| Philippines PHL | Driving license | No |
| Philippines PHL | Passport | Required |
| Taiwan TWN | ROC national ID | No |
| Taiwan TWN | Passport | Required |
| Malaysia MYS | MyKad (Malaysian national ID) | No |
| Malaysia MYS | Driving license | No |
| Malaysia MYS | Passport | Required |
| Vietnam VNM | Vietnamese citizen ID | No |
| Vietnam VNM | Driving license | No |
| Vietnam VNM | Passport | Required |
| Japan JPN | My Number CardΒΉ | No |
| Japan JPN | Driving licenseΒΉ | No |
| Japan JPN | Passport | Required |
| South Korea KOR | Resident registration card | No |
| South Korea KOR | Passport | Required |
| Australia AUS | Passport | Required |
| Thailand THA | Thai national ID card | No |
| Thailand THA | Passport | Required |
| Singapore SGP | Singapore NRIC | No |
| Singapore SGP | Passport | Required |
| Other countries | Passport | Required |
ΒΉ **Japan-specific requirement**: when using a My Number Card or a driving license, the user's IP address must be inside Japan; otherwise the user must also submit a passport for verification.
## Proof of address (POA)
When the user presents a passport, a separate proof of address is required. Accepted documents include bank statements, utility bills and official government documents.
A proof of address must also be an original β screenshots are not accepted.
* **Purpose**: a POA verifies the user's residential address.
* **Validity**: the document must have been issued within the last 3 months.
* **Content**: it must show the full name and the address, and the name must match the identity document.
* **Upload limits**: JPG, JPEG, PNG or PDF only; each file no larger than 10 MB; up to 3 files.
### Validity rules at a glance
| Document class | Rule |
| - | - |
| POI (ID card / passport / driving license) | At least 6 months before expiry |
| POA (utility bill / bank statement and so on) | Issued within the last 3 months |
## How documents are submitted, and the related fields
Documents accompany the card application: with Sumsub the user's documents are collected in Sumsub and you share them with DCS through `kycInfo.sumsubShareToken` β for how the sharing partner is configured, see [Sumsub KYC data sharing](./kyc-vendor). The POA fields (`poaDocType` / `poaDocUrlList[]` / `poaDocDate` / `addressLine1` / `addressLine2` / `state` / `city` / `postalCode` / `country`) are submitted with the virtual card endpoint `POST /card/v1/virtual-card/apply`; for the field specs see the [card application parameter dictionary](../how-to-use/managing-cards/application-data-dictionary).
For the material KYC needs, the markets cards can be issued in and the per-region document requirements, see [Compliance and the KYC flow](../basic-concepts/compliance-kyc-flow).
## Next steps
* The resubmission path when a document fails: [KYC rejections and resubmission](./kyc-rejections)
* Compliance overview and where KYC sits in the flow: [Compliance and the KYC flow](../basic-concepts/compliance-kyc-flow)
* Sharing-partner configuration when using Sumsub: [Sumsub KYC data sharing](./kyc-vendor)
# KYC Rejections and Resubmissions
Source: https://docs.thedecard.com/en/decard-managed/customer-success/kyc-rejections
When a KYC application does not pass, this page helps you tell two kinds of rejection apart from the API response: a fixable rejection (the problem can be corrected, and the application is re-reviewed once the documents are re-uploaded) and a REFUSE decision (the review has concluded and the application was not approved). Each has its own handling path. This page covers only the rejection branch.
## π Guide
A KYC status in the DeCard-Managed model consists of a top-level `status` plus a `statusDescription` text, and resubmission is signalled in a machine-readable way through `needExtraInfo` (boolean): `true` = prompt the user to supply more information, `false` = the user has already submitted, so do not prompt again. The "fixable vs. `REFUSE`" split below is a business-process distinction, derived from `needExtraInfo`, the review outcome, and `statusDescription` together. Once you understand this fork, you can guide users correctly: a case that only needs a re-upload is not treated as unrecoverable, and a case already in `REFUSE` is not pushed through repeated submissions.
### Terminology (REFUSE / REJECTED)
Different layers of the same system use different English variants, so here is the clarification:
| Where it appears | English | Description |
| - | - | - |
| **Top-level `status` enum value in the API response** | `REFUSE` | Code literal, uppercase; the top-level status for a KYC review that was not approved (alongside `UNDO` / `INIT` / `PENDING` / `PASS`) |
| **`statusDescription` sub-status value** | `POA_REJECTED` / `POI_REJECTED` | Code literal, uppercase; appears only under `status=INIT`, meaning proof of address / proof of identity was not accepted (the application is still in progress under `INIT` and is fixable, so this is **not** a top-level `REFUSE`) |
| **Narrative wording on this page** | "rejected", "not accepted", "not approved" | Plain-language wording; uppercase English is used only when quoting code enums |
> Always rely on the literal values actually returned in the API response fields.
***
## 1. Fixable Rejection (Temporary / Fixable)
When the KYC review finds a **correctable** problem (expired document, blurred photo, address mismatch, missing page, and so on), DCS does not immediately reach a `REFUSE` decision. Instead, it reports a specific sub-status through `statusDescription` while `status` stays at `INIT`. The user **does not have to redo the whole KYC flow** and only needs to re-upload the document concerned.
### Signals to check
| Signal | Source | Meaning |
| - | - | - |
| `status = INIT` | KYC top-level status | The user's KYC is still in progress and has not reached `PENDING` (submission complete) or `REFUSE` (review concluded) |
| `statusDescription = POA_REJECTED` | KYC sub-status | Proof of address (POA) was not accepted; address documents must be re-uploaded |
| `statusDescription = POI_REJECTED` | KYC sub-status | Proof of identity (POI) was not accepted; identity documents must be re-uploaded |
| `needExtraInfo = true` | Card application / query responses | **Native machine-readable resubmission signal of the DeCard-Managed model**: the user has not submitted the additional information yet, so prompt them to upload |
| `needExtraInfo = false` | Card application / query responses | The user has already submitted; even if `status` is still `PENDING`, simply wait for the review and **do not prompt again** |
> `needExtraInfo` is the most direct machine-readable field for driving the resubmission flow; it answers "is anything still needed from the user?". The breakdown of what was rejected comes from `status` plus the `statusDescription` text.
### Resubmission steps
1. **You do not need a new Sumsub share token.** When POA/POI is not accepted, the original application context still applies (the original `sumsubShareToken` remains valid), so you can send the user straight to re-upload **without issuing a new token**. This differs from starting a brand-new KYC application.
2. **Guide the user to resubmit**: generate a resubmission link through the H5 guidance-page endpoint and bring the user back to the upload page:
```
POST /redirect/v2/guidance-link
```
Set `action` to:
| `action` | Meaning |
| - | - |
| `KYC_EXTRA_DOC` | KYC additional information (re-upload a missing or unacceptable document). **`applyId` is conditionally required for this action** |
**`applyId` is conditionally required**: when `action = KYC_EXTRA_DOC`, the guidance-link request **must carry `applyId`**, and omitting it makes the call fail. For the remaining guidance-page parameters (language / theme / mode / redirect URLs, and so on), see [H5 KYC and Card Application Guidance Page](../integration-resources/h5-kyc-guidance).
3. **Once the user has re-uploaded**, the application goes back into review (`status` returns to the in-progress description of `PENDING` / `INIT`). You can keep polling or wait for the webhook carrying the final outcome.
4. **For acceptable POA/POI document types** and which countries require additional POA, see [KYC Documents](./kyc-documents).
> **How Sumsub tokens relate to the rejection flow**: configuring the token sharing mechanism (data provider / data recipient) is a one-off integration task. Once it is set up **before go-live**, **nothing needs to be reconfigured** during a rejection and resubmission cycle. For the steps, see [Sumsub Token Sharing](./kyc-vendor).
***
## 2. Not Approved (Final / `REFUSE`)
When `status` is **`REFUSE`**, the KYC review **was not approved**: the information the user submitted does not meet the requirements or failed verification. This decision is normally made by the Operations team during manual review, for business reasons such as an AML hit, a sanctions list match, a restricted regulatory jurisdiction, or identity fraud.
For `REFUSE`:
* Unlike a fixable rejection (`statusDescription = POA_REJECTED / POI_REJECTED`, which is still in progress under `INIT`), `REFUSE` is a not-approved decision at the **KYC top-level status**, so you should generally not keep pushing the user through the resubmission flow to re-upload documents.
* Whether and how to show the rejection reason to the end user must follow the customer-facing wording you agreed with DCS (some reasons cannot be disclosed externally for compliance reasons).
* If you disagree with the decision, use the manual appeal and support escalation path described in [Escalations and Support Paths](./escalations-and-support-paths).
> `REFUSE` is the terminal state of that KYC application, and the same application cannot be continued with additional documents. Business rules allow a brand-new application to be started, but the system caps submissions at 10 per 24 hours (the limit is configurable). Once the cap is reached, wait for the window to reset instead of retrying in a loop.
***
## Next steps
* Country-by-country allow lists for proof of identity (POI) and proof of address (POA): [KYC Documents](./kyc-documents)
* The two integration directions for Sumsub token sharing and how to configure them: [Sumsub Token Sharing](./kyc-vendor)
* Complete parameter reference for the resubmission guidance page (guidance-link): [H5 KYC and Card Application Guidance Page](../integration-resources/h5-kyc-guidance)
* The appeal path when you disagree with a KYC rejection: [Escalations and Support Paths](./escalations-and-support-paths)
# Sumsub Token Sharing
Source: https://docs.thedecard.com/en/decard-managed/customer-success/kyc-vendor
If you already use Sumsub as your compliance vendor, token sharing lets you reuse the verification data you have already collected with DCS, so your users do not have to verify twice. This page covers the two sharing modes (agreed offline, not API fields), the two integration directions for a data provider and a data recipient, and how to pass kycInfo.sumsubShareToken when applying for a card.
## π Guide
If you already use [Sumsub](https://sumsub.com) as your compliance (identity verification) vendor, you can share the verification data you have collected with DCS through Sumsub's **Sharing Partners** mechanism. This simplifies KYC, because end users do not have to complete identity verification a second time on the DCS side.
Once integration is complete, you pass a Sumsub **share token** in the card application request (field `kycInfo.sumsubShareToken`). DCS uses that token to fetch and reuse the user's verification data from Sumsub, and the application then continues through the rest of KYC review.
> Sumsub token sharing must be configured once in **each of the two environments, sandbox (UAT) and production (PROD)**. A sandbox token cannot be used in production and vice versa, so always confirm that the environments match when configuring.
## Two integration directions
Depending on who collects the verification data and who reuses it, there are two directions in which the token is added between you and DCS. Follow the steps that match your actual role.
### configure the sharing direction that matches your role
**If you are the data provider** (you collect the verification data and share it with DCS):
1. Obtain the UAT and PROD partner tokens from your DCS contact.
2. On your Sumsub platform, go to **Sharing Partners** (see the [Sumsub documentation](https://docs.sumsub.com/docs/manage-sharing-partners)) and add the token as prompted.
3. Verify with your DCS contact: the name you registered with Sumsub should now be visible on the DCS Sumsub platform.
**If you are the data recipient** (DCS collects the verification data and shares it with you, or you need to give DCS your token):
1. Obtain your UAT and PROD tokens on your Sumsub platform and share them with your DCS contact.
2. After DCS has added them, confirm on your Sumsub platform that DCS appears in the **Donor list**, and let DCS know the result.
### Step 3: generate the share token
Generate a share token for the target applicant on the Sumsub side. A share token looks like `_act-sbx-***` (sandbox) or `_act-***` (production); these are **illustrative only, so never write a real token anywhere**.
> Sumsub share tokens are normally single-use, so generate a new token for every application you submit. The environment (sandbox or production) must match the DCS environment you are currently integrating with.
### Step 4: use the share token in a card application request
In the virtual card application request POST `/card/v1/virtual-card/apply`, put the generated share token in `kycInfo.sumsubShareToken`.
| Field | Type | Required | Constraint | Description |
| - | - | - | - | - |
| `kycInfo.sumsubShareToken` | string | Yes | Max 500 characters | Sumsub share token; DCS uses it to reuse the user's verification data from Sumsub |
**There is no mode field in `kycInfo`**: within the card application body, `sumsubShareToken` is the only Sumsub-token-related field. There is **no** mode enum (such as `kycSubmitMode` or `sumsubShareTokenMode`) β the sharing relationship is decided by the Sharing Partners configuration in Sumsub and by your contract, and is never carried in the request body.
Besides `sumsubShareToken`, `kycInfo` must also carry data that is not fetched from Sumsub, such as `email`, the POA fields `poaDocType` / `poaDocUrlList` / `poaDocDate` / `addressLine1` / `city` / `country`, and the employment and source-of-funds fields `employmentStatus` / `occupation` / `sourceOfFunds` / `sourceOfWealth`. The card application endpoint specification is authoritative for the complete field list.
## Sample request fragment (masked)
> Every value in the `kycInfo` fragment below is a placeholder. **Never write real user PII (phone number, email, identity document, address) or a real token.**
```json theme={null}
{
"externalUserId": "",
"categoryId": 0,
"applyRef": "",
"kycInfo": {
"sumsubShareToken": "_act-sbx-***",
"email": "user@example.com",
"occupation": "",
"sourceOfFunds": ""
}
}
```
## Sandbox testing: generate a simulated KYC token
During sandbox (UAT) integration testing, if you do not yet have a working real Sumsub share token, you can call the **simulated token endpoint** provided by DCS to generate a test token:
POST `/simulation/v1/generate-kyc-token`
| Parameter | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | Yes | External user ID |
The response envelope is always `{ code, message, messageDetail, data }` (there is no `success` boolean), and the simulated token is returned in `data.token`. Note that `messageDetail` is an object (`message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`, all empty on a normal success) rather than `null`:
```json theme={null}
{
"code": "",
"message": "",
"messageDetail": {
"message": "",
"title": "",
"type": "",
"icon": "",
"action": "",
"linkTitle": "",
"linkUrl": ""
},
"data": {
"token": ""
}
}
```
**This token is for simulation and sandbox use only.** It is a different thing from a real Sumsub share token in production, so do not mix the two and never use it in production.
## Troubleshooting
If Sumsub token sharing is not working as expected, work through these common causes:
| Problem | Symptom | Diagnosis and fix |
| - | - | - |
| Token already used | After the application is submitted, the token is reported as invalid or expired | Sumsub share tokens are **single-use only**. Generate a fresh share token for the same applicant for every new card application, and never reuse an old one. |
| Sandbox and production mismatch | The token works in one environment but is rejected in the other | The environment in which you generated the token in Sumsub (sandbox or production) must match the DCS environment you are integrating with. A sandbox token cannot be used against DCS production, and vice versa. When switching environments, check the environment indicator at the bottom of the Sumsub dashboard (a yellow banner means sandbox mode). |
| Required fields missing on the Sumsub side | The KYC application fails to be created, or the review is sent back | If the applicant record on your Sumsub side is missing fields that DCS requires (name, date of birth, nationality, address, and so on), DCS cannot complete the review. **Ask DCS in advance for the list of fields your Program requires on the Sumsub side**, and make sure every one of them is fully populated in the applicant record before you generate the share token. |
> If none of the above resolves the issue, contact DCS technical support with the exact error message and the time it occurred.
## The Sumsub POI / POA configuration baseline (required reading)
for the Sumsub token you send to DCS to pass verification, your POI (proof of identity) and POA (proof of address) configuration in the Sumsub console **must match DCS exactly**. Follow the screenshots below closely; a mismatched configuration will cause the token to fail verification and will hold up your launch.
This baseline may be adjusted from time to time as compliance or regulatory requirements change, so DCS's latest notice always prevails. For the table of accepted document types by country and when an additional POA is required, see [About KYC Documents](./kyc-documents).
### Part 1: Setting for POI (proof of identity)
#### 1. ID Verification
#### 2. Supported ID Documents
##### 2.1 Blacklisted countries (34)
None of the following documents are accepted: passport, ID card, driving license or residence permit.
Blacklisted countries (34): Afghanistan, Belarus, Bosnia and Herzegovina, Burundi, Central African Republic, Congo (DRC), Cuba, Guatemala, Guinea, Guinea-Bissau, Haiti, Iran, Iraq, Lebanon, Libya, Mali, Moldova, Montenegro, Myanmar, Nicaragua, Niger, North Korea, Russia, Serbia, Somalia, South Sudan, Sudan, Syria, Tunisia, Turkey, Ukraine, Venezuela, Yemen, Zimbabwe.
##### 2.2 POI accepted as POA for nine designated countries
Supported countries: Taiwan, Australia, Japan, South Korea, Vietnam, the Philippines, Malaysia, Thailand and Singapore. Configure the accepted document types and subtypes for each country in Sumsub as shown in the screenshots below.
**Taiwan** β passport and ID card accepted (subtype `twn.id.type1` only)
**Australia** β passport only
**Japan** β passport, My Number Card and driving license accepted (the IP address must be in Japan; if it is not, the user must submit a passport for verification)
**South Korea** β passport and ID card accepted (subtype `kor.id.type1` only)
**Vietnam** β passport, ID card (subtypes `vnm.id.type3` and `vnm.id.type4` only) and driving license (subtype `vnm.drvlic.type1` only) accepted
**Philippines** β passport, ID card (subtype `phl.id.type11` only) and driving license (subtype `phl.drvlic.type1` only) accepted
**Malaysia** β passport, ID card (subtype `mys.id.type1` only) and driving license (subtype `mys.drvlic.type1` only) accepted
**Thailand** β passport and Thai National ID Card accepted
**Singapore** β passport and Singapore identity card (NRIC) accepted
#### 2.3 USA
#### 2.4 Other countries
Passport only, single-sided; ID cards, driving licenses and residence permits are not accepted.
### Part 2: Setting for POA (proof of address)
#### 1. Workflow builder
If the POI document carries no address, the user goes on to POA (proof of address) verification. If the POI document does carry an address but the address country is Japan and the user's IP is outside Japan, the user must additionally submit a passport to confirm nationality.
#### 2. User verification
Note: proof of address (POA) is accepted only from designated countries, as set out in your agreement with DCS.
#### 3. Verification level
**USA residents need to be rejected.**
***
## Next steps
* The resubmission path when shared data is rejected: [KYC Rejections and Resubmissions](./kyc-rejections)
* Acceptable document types and POA requirements: [KYC Documents](./kyc-documents) (same group)
* Compliance capability overview and navigation to each sub-page: [Compliance Overview](../basic-concepts/compliance-kyc-flow)
* Complete field specification for the card application endpoint: `POST /card/v1/virtual-card/apply`
# Physical Card Shipping
Source: https://docs.thedecard.com/en/decard-managed/customer-success/physical-card-shipping
Explains the shipping status state machine for physical cards, the shipping information query endpoint, and the real-time WebSocket push; it also marks the boundaries of the shipping capability in the DeCard-Managed model.
## π Overview
A physical card enters the production and shipping flow as soon as the user's application succeeds. The system keeps one shipping tracking record per physical card and advances it through a fixed state machine. You can query the current status at any time, and once the card reaches the shipping stage you also get the tracking number and the carrier name. Status changes are pushed in real time over WebSocket as well (see below).
**Capability boundary (important)**: for physical card shipping, the DeCard-Managed model exposes **a single read-only query endpoint** (status, tracking number, and carrier). It **does not provide** shipping quotes, per-country rate tables, shipping method selection, delivery address changes, batch shipping, metal card surcharges, or similar capabilities. Confirm delivery lead times, shipping fees, and address-change policy with DCS during integration.
***
## Shipping status state machine
Physical card shipping advances through the following four states in **strict order**:
| Status code | Name | Meaning |
| :- | :- | :- |
| **PENDING\_EMBOSSING** | Pending embossing | The user has applied for a physical card and the system has created a shipping tracking record, waiting for embossing to start |
| **EMBOSSING\_IN\_PROGRESS** | Embossing in progress | The physical card is being produced; the production system has generated and is processing the embossing file |
| **IN\_DELIVERY** | In delivery | The physical card has been produced and handed to the carrier; the tracking number and carrier name are returned from this point on |
| **DELIVERY\_COMPLETE** | Delivery complete | The physical card has been delivered and the shipping flow is closed (whether it has been activated is reflected in `physicalCardStatus`, not here) |
> Transitions follow a strict sequence: PENDING\_EMBOSSING β EMBOSSING\_IN\_PROGRESS β IN\_DELIVERY β DELIVERY\_COMPLETE. `trackingNumber` and `trackingCompanyName` are populated only after the card reaches IN\_DELIVERY.
> This shipping state machine and the card's `physicalCardStatus` (`UN_APPLY` / `INACTIVE` / `ACTIVE` / `REPLACE` / `FROZEN` / `CANCELLED`, see [Managing Cards Β· Overview](../how-to-use/managing-cards/overview)) are **two separate status fields**: the former describes where the card sits in the shipping chain, the latter describes whether the card itself is usable. Do not mix them up.
***
## Query shipping information
### Preconditions
* The card is a **physical card** and has been applied for successfully, so a shipping tracking record exists. A virtual card, or a card for which no physical card has been requested, has no matching record.
* You already hold the user's `externalUserId` and the target card's `cardId`.
### Endpoint
| Endpoint | Query parameters | Description |
| :- | :- | :- |
| `GET /card/v2/physical-shipping-info` | `externalUserId` (required), `cardId` (required, string) | Identifies the card precisely by card ID |
### Request example
```bash theme={null}
curl -X GET "{{dicard-server}}/card/v2/physical-shipping-info?externalUserId=&cardId=" \
-H "Content-Type: application/json"
```
> The request must carry the authentication headers (`X-DAPI-API-KEY` / `X-DAPI-SIGN` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE`); see [Integration Resources βΊ Authentication Guide](../integration-resources/overview). In the example, `` and `` are placeholders that you replace with real values. **Never write real PII into examples.**
### Response fields (`data`)
| Field | Type | Description |
| :- | :- | :- |
| `cardId` | string | Card ID |
| `externalUserId` | string | External user ID |
| `cardMantissa` | string | Last four digits of the card number |
| `status` | string | Shipping status; see the state machine above |
| `trackingNumber` | string | Tracking number (populated only from `IN_DELIVERY` onwards) |
| `trackingCompanyName` | string | Carrier name (populated only from `IN_DELIVERY` onwards) |
`cardId` is a **string**, not a number, so write it as a string in your examples.
### Response example (masked placeholders)
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": {
"cardId": "",
"externalUserId": "",
"cardMantissa": "",
"status": "IN_DELIVERY",
"trackingNumber": "",
"trackingCompanyName": ""
}
}
```
> The response envelope is always `{ code, message, messageDetail, data }` (there is no `success` boolean), and the success literal for `code` is `SYS_SUCCESS` (consistent across this documentation set). On success, `messageDetail` is usually `null`.
### Error handling
| Scenario | What you see and what to do |
| :- | :- |
| The card is not a physical card, or no physical card has been requested | There is no shipping tracking record, so `data` is empty or carries no `status`. First confirm that the physical card application has been completed through the H5 page (see [Issuing Cards](../how-to-use/managing-cards/issuing-cards)) |
| The card has not reached the shipping stage yet | While `status` is `PENDING_EMBOSSING` or `EMBOSSING_IN_PROGRESS`, `trackingNumber` and `trackingCompanyName` are empty. This is expected, so do not retry |
| Missing parameter, or the card does not belong to that user | The request fails; judge from the response `code` and `message`. `externalUserId` and `cardId` (or `cardMantissa`) must resolve to a card held by the same user |
***
## Real-time WebSocket notifications
Besides querying on demand, DCS pushes a **real-time WebSocket notification** whenever the shipping status changes (a signature capability of the DeCard-Managed model), so you learn the latest progress without polling.
### When a push is sent
* When the shipping tracking record is created
* When the shipping status changes
* When the shipping information (tracking number or carrier) is updated
### Message type and data structure
The WebSocket message type is `CARD_PHYSICAL_SHIPPING`, and its payload fields match the response fields above (`cardId` / `externalUserId` / `cardMantissa` / `status` / `trackingNumber` / `trackingCompanyName`).
The WebSocket handshake, authentication, and full data structures are consolidated on the [Integration Resources βΊ Webhook and WebSocket Real-Time Notifications](../integration-resources/webhook-websocket) page; the payload fields for this message type are in the [`CARD_PHYSICAL_SHIPPING` physical card shipping information](../integration-resources/webhook-websocket#3-4-card_physical_shipping-physical-card-shipping-information) section of that page (`messageType` = `CARD_PHYSICAL_SHIPPING`).
***
## Next steps
* To **apply for, activate, or set the PIN of** a physical card (H5 guidance page), see [Issuing Cards](../how-to-use/managing-cards/issuing-cards).
* For overall card status and the `physicalCardStatus` state machine, see [Managing Cards Β· Overview](../how-to-use/managing-cards/overview).
* For WebSocket and webhook connectivity and the full data structures, see [Integration Resources βΊ Webhook and WebSocket Real-Time Notifications](../integration-resources/webhook-websocket).
# Post-Go-Live FAQ
Source: https://docs.thedecard.com/en/decard-managed/customer-success/post-go-live
This page lists the front-line issues that come up most often once you are live, organized by scenario: cards that will not add to a digital wallet, cards that will not save at a merchant, declined transactions, frozen cards, refunds and reversals that have not landed, disputes and fraud claims, KYC stuck in review, changing a shipping address, and blocking a merchant. For each one you get the steps your own support team can work through unaided, plus the threshold at which to escalate to DCS, so that most first-line cases can be resolved on your side.
Going live is only the start. Whether you are an exchange, a wallet or a platform, your end customers will run into real situations: the card will not add to Apple Pay, a refund is taking too long, a purchase gets declined. This page walks through those situations one by one.
As a licensed issuer with its own BINs, DCS provides **per-customer account custody, in-system authorization decisions, clearing and reconciliation** under DeCard-Managed. When a case genuinely needs DCS, raise it through [Escalations and support paths](./escalations-and-support-paths).
> Where responsibility sits under DeCard-Managed: **authorization decisions are made inside DCS**, and every end customer holds their own balance (available `free` / held `freeze`). So for questions like "why was this declined" or "did they have enough money", start with that customer's [per-customer balance](../how-to-use/managing-transactions/user-balance) and card status, then factor in risk and MCC rules. This differs from the Partner-Managed model, where the partner runs its own limits and authorization decisions.
***
## The card will not add to a digital wallet (Apple Pay / Google Pay)
When an end customer cannot add their card to a mobile wallet, walk them through the following in order:
1. **Try a different card**: this tells you straight away whether the problem is the card or the wallet and handset.
2. **Common causes**:
* the country or region the handset is in does not match the card's issuing market;
* Apple or Google has flagged the handset as a high-risk device;
* several cards were added in quick succession, tripping a rate limit on the wallet side.
3. **What to advise**: a single device can normally only add **one card successfully per 24 hours**, so ask the customer to wait 24 hours and try again.
> DeCard-Managed supports in-app provisioning (one-tap card binding inside your app) through `POST /card/v1/apple-bind-wallet` and `/card/v1/google-bind-wallet` (Google also has a V2 endpoint of the same name). For the integration and field details, see [Push provisioning (Apple / Google Pay)](../how-to-use/managing-cards/push-provisioning).
## The card will not save at a merchant (card-on-file)
When an end customer cannot store the card with a merchant or app:
* some merchants validate the billing address strictly, so suggest the customer enter address details matching the card's issuing market at checkout;
* if it still fails, note the **transaction identifier** and escalate to DCS for investigation through the [escalation path](./escalations-and-support-paths). You can use `POST /card/v1/transaction/id/resolve` to pull the transaction detail and pin down the record.
## How to troubleshoot a declined transaction
Under DeCard-Managed, the authorization decision is made **inside DCS**. Work through it in this order:
0. **Confirm from the webhook whether it was declined**: the callback URL you configured receives a `CARD_TRANSACTION` webhook in which `data.response` = `"A"` means approved and `"D"` means declined. That field is binary approve/decline only and carries no detailed decline reason code, so the reason has to be established manually through the steps below.
1. **Check the customer's own balance**: call `GET /user-asset/v1/balance` to see the customer's wallet balance, specifically whether `free` (available) covers the purchase and how much is tied up in `freeze` (held). Insufficient balance is by far the most common decline reason.
```http theme={null}
GET /user-asset/v1/balance?externalUserId=
```
> `data` in the response is an array returning `{asset, free, freeze, total}` per currency. For the fields and how to use them, see [User balance](../how-to-use/managing-transactions/user-balance).
2. **Check the card status**: call `GET /card/v3/detail` and read `cardStatus` (virtual card: `NORMAL`/`FROZEN`/`BLOCKED`/`CANCELLED`), `statusReason`, and `physicalCardStatus` (physical card: `UN_APPLY`/`INACTIVE`/`ACTIVE`/`REPLACE`/`FROZEN`/`CANCELLED`). A card in `FROZEN`, `BLOCKED` or `CANCELLED` will never authorize. Troubleshoot with v3 rather than v2: v2 normalizes `BLOCKED` to `FROZEN`, which reads as "just have the user unfreeze it" for a card that cannot be unfrozen.
```http theme={null}
GET /card/v3/detail?externalUserId=&cardId=
```
> `cardId` is returned in the successful card issuing response, and can also be looked up per customer with `GET /card/v3/detail` (leave `cardId` empty to list all of the user's cards). For frozen or canceled physical cards, read `physicalCardStatus`. See the next section, "The card is frozen".
3. **Check risk rules and MCC**: was a restricted merchant category (MCC) or a risk rule hit? If the balance is sufficient and the card status is fine but the transaction is still declined, contact DCS through the [escalation path](./escalations-and-support-paths) to confirm whether it hit a risk or MCC rule.
4. **Tell the customer**: explain the decline reason based on what you found.
> Authorization fields such as `direction` and `authType` are described in [Real-time authorization](../how-to-use/managing-transactions/authorizing-transactions); the account asset and hold/debit model is in [Basic concepts βΊ Ledgering system](../basic-concepts/ledgering-system).
## The card is frozen: telling the states apart and what to do
Read `cardStatus` from `GET /card/v3/detail` (virtual card):
| Status | Meaning | Who can lift it |
| :- | :- | :- |
| `NORMAL` | The card is active and usable | β |
| `FROZEN` | Frozen by the cardholder, or after three consecutive wrong CVV / PIN entries | The partner can unfreeze it themselves |
| `BLOCKED` | Blocked on the issuer side: compliance, risk, sanctions screening, product retirement, unpaid account management fee, and similar | **DCS only** β an unfreeze call from the partner will fail |
| `CANCELLED` | The card is canceled and permanently out of use | Not reversible |
When the card is `BLOCKED`, `statusReason` gives the specific reason (values and meanings in [Card Management βΊ Card status reason](../how-to-use/managing-cards/overview#card-status-reason-statusreason)). One of them, `ACCOUNT_FEE_INSUFFICIENT`, clears **automatically on our side** once the user tops up in D-Vault β no need to escalate.
The `CARD_STATUS` webhook normalizes `BLOCKED` to `FROZEN`. Troubleshooting from it alone will treat a card that cannot be unfrozen as an ordinary frozen card, and the user taps unfreeze for nothing. Always troubleshoot with `GET /card/v3/detail` and the `CARD` event.
* **Unfreezing**: call `POST /card/v2/block` with `externalUserId`, `cardId` and `block` (boolean, `false` = unfreeze / `true` = freeze) in the body, plus **either** `smsCode` (SMS OTP) **or** `emailCode` (email OTP), since unfreezing requires a one-time code. Freeze and unfreeze are the **same endpoint distinguished by the boolean `block`**, not two separate endpoints.
```http theme={null}
POST /card/v2/block
```
```json theme={null}
{
"externalUserId": "user_xxxxxxxx",
"block": false,
"cardId": "card_xxxxxxxx",
"smsCode": "",
"emailCode": ""
}
```
> Obtain `smsCode` from `POST /captcha/v1/send-mobile-code` (with `behavioral=CARD_UNFROZEN`) and `emailCode` from `POST /captcha/v1/send-email-code`. Either one is enough.
> This endpoint identifies a card precisely by `cardId`. The card state machine and the operations allowed in each state are covered in [Card management βΊ Overview](../how-to-use/managing-cards/overview).
## A refund or reversal has not landed yet
Refunds and reversals appear in the system as credit-direction records (`direction` = `CREDIT`), releasing held funds back into the customer's `free` balance:
* once the merchant has initiated the refund or reversal there is an unavoidable delay before it lands, so ask the customer to be patient first.
* Use `GET /card/v2/statements` (statements) and `POST /card/v1/transaction/id/resolve` (single transaction detail) to check the status and direction of the record.
```http theme={null}
POST /card/v1/transaction/id/resolve
```
```json theme={null}
{
"ids": [""]
}
```
> Pass one or more transaction IDs in `ids` (`outstandingTransactionId` or `postedTransactionId`). The response distinguishes `posted` (whether it has been booked), `outstandingTransactionId` (null once booked) and `postedTransactionId` (populated once booked).
* If it still has not landed well beyond any reasonable window, escalate to DCS for investigation through the [escalation path](./escalations-and-support-paths), quoting the **original transaction identifier**.
> Actual refund and reversal timings depend on how the channel processes them. The fields and scenarios for refunds and reversals (`authType=REFUND` / `REVERSAL`, `direction=CREDIT`) are in [Real-time authorization](../how-to-use/managing-transactions/authorizing-transactions).
## A cardholder disputes a transaction (dispute / fraud)
When an end customer claims a transaction was unauthorized, or is in dispute with the merchant:
* first check the transaction detail with `GET /card/v2/statements` / `POST /card/v1/transaction/id/resolve` (merchant name, amount, timestamp) to rule out a forgotten purchase or a family member's spending;
* if it really is a dispute or fraud claim, contact DCS through the [escalation path](./escalations-and-support-paths) to take it forward.
**How disputes are handled**: DeCard-Managed currently has **no self-service dispute or chargeback API**. Disputes and chargebacks are handled **through coordination with DCS Operations**, so escalate the case through the support path and DCS will follow its established process. This FAQ section also has a dedicated page, [Transaction issues and disputes](./transaction-issues-disputes).
## KYC has been in review for a long time on an issued card
If a customer's KYC status has not moved on well after they applied:
1. **Start with document quality and labeling**: confirm the documents submitted are legible, are not screenshots, and are labeled with the correct type and side (for example the front of a national ID, and a correctly labeled selfie). The list of acceptable documents is in [KYC documents](./kyc-documents).
2. **Check the current state**: establish which stage the application is at, and if a resubmission is needed, guide the customer through it.
3. **Escalate after a reasonable wait**: if the documents are sound and the application is still sitting in review, escalate to DCS through the [escalation path](./escalations-and-support-paths), quoting the `externalUserId` and the KYC ticket identifier.
> Rejection reasons are in [KYC rejections and resubmission](./kyc-rejections).
## Changing the shipping address for a physical card
Once physical card shipping details are submitted they **normally cannot be changed in flight**. To correct them:
1. contact DCS through the [escalation path](./escalations-and-support-paths) to cancel that customer's current card;
2. have the customer apply for a new card with the correct address.
> Canceling the current card and reapplying with the correct address is the standard way to correct a shipping address today. Shipping details and the physical card flow are covered in [Card management βΊ Overview](../how-to-use/managing-cards/overview).
## Blocking a suspicious merchant
To block a specific merchant:
* under DeCard-Managed the authorization decision is made inside DCS, so merchant-level and MCC-level blocking has to be configured on the DCS risk side;
* escalate through the [escalation path](./escalations-and-support-paths) with the **merchant identifier and the reason for blocking**, and DCS will apply it at the risk or scheme layer.
***
## Next steps
* To escalate a case to DCS, or to understand the response path for each type of issue, go to [Escalations and support paths](./escalations-and-support-paths).
* For questions about getting ready to launch, see [Customer success βΊ Overview (Pre-Go-Live FAQ)](./pre-go-live).
# Pre-Go-Live FAQ
Source: https://docs.thedecard.com/en/decard-managed/customer-success/pre-go-live
Whether you are an exchange, a wallet or a platform, only a handful of things need to be in place before you go live: have DCS issue your access credentials and card configuration, agree with DCS on how KYC will be handled, send DCS your callback and whitelist details, and run the full "register β KYC β issue card β deposit β spend" loop in the sandbox. This page answers the questions we hear most often before launch, in Q&A and checklist form, covering the launch path and who owns each step, customer registration and KYC documents, the Sumsub + POA options, the H5 guidance page, the configuration you need up front, and a pre-go-live self-check list.
DCS is a licensed issuer with its own BINs. Under DeCard-Managed, **both KYC review and authorization decisions happen inside DCS**, so you do not have to build your own authorization engine or manage collateral. This is what sets it apart from the Partner-Managed model, where the partner owns the authorization decision. The pre-launch configuration work is split between DCS and the partner, and every step below states who does it.
***
## DeCard-Managed capabilities at a glance
The capabilities below are what DeCard-Managed adds on top of the Partner-Managed model. Each has a dedicated page in this documentation set; the table is a navigation aid rather than a full description:
| Capability | Description | Read more |
| - | - | - |
| **Per-customer balance model** | Every end customer has their own balance, expressed through three fields, `free` (available), `freeze` (held) and `total`; DCS checks the available balance internally before approving an authorization | [User balance](../how-to-use/managing-transactions/user-balance) |
| **Crypto deposits and withdrawals** | Crypto deposits across multiple chains and tokens (credited to the DCS ledger once confirmed on chain), plus withdrawals | [Crypto deposit](../how-to-use/virtual-accounts/crypto-deposit) |
| **QR Pay** | Cardholders can pay by scanning a QR code | [QR Pay](../how-to-use/managing-transactions/qr-pay) |
| **Apple Pay / Google Pay** | One-tap card provisioning from inside your app (push provisioning) | [Push provisioning](../how-to-use/managing-cards/push-provisioning) |
| **WebSocket real-time push** | Real-time messaging to end customers (a DeCard-Managed capability) | [Webhook + WebSocket](../integration-resources/webhook-websocket) |
***
## What do I need to prepare before going live?
Partners on DeCard-Managed follow the path below. Ownership of each step is as follows:
| Step | Owner | Description |
| - | - | - |
| 1. Issue access credentials and card configuration (Program / categoryId) | DCS | You tell us which card types you want to issue (virtual or physical), the card scheme, the card BIN and the issuing markets; DCS generates your access credentials and configures the matching card category (`categoryId`) |
| 2. Agree on the KYC approach (Sumsub Share Token / DCS-hosted H5) | Partner + DCS | Decide whether end customers go through Sumsub Share Token embedded in the card application, or through the DCS-hosted H5 KYC guidance page (see below) |
| 3. Submit your callback URL and network egress IPs | Partner | Give DCS your webhook callback URL, and the egress IPs to be whitelisted (see below) |
| 4. Integrate real-time notifications (webhook / WebSocket) | Partner | Subscribe to card status, transaction and KYC status events; WebSocket is used for real-time push to end customers (a DeCard-Managed capability) |
| 5. Issue your first card end to end in the sandbox | Partner | Complete the "register β KYC β issue card β deposit β spend" loop in the sandbox |
| 6. Cut over to production | Partner + DCS | Collect your production credentials and switch to the production base URL |
> For the end-to-end sequence and prerequisites of the integration path, see [First Steps](../getting-started/first-steps) and [Quickstart](../getting-started/quickstart).
***
## What information is needed to register an end customer?
Under DeCard-Managed, end customers are registered inside the system. There are **two mutually exclusive** registration paths, mobile and email. Pick one:
| Registration path | Required fields | Description |
| - | - | - |
| **Mobile registration** | `mobileCode` (country calling code) + `mobile` (mobile number) + `smsCode` (SMS OTP) | All three must be supplied together. Note that DeCard-Managed uses `mobileCode` / `mobile`, which differ from the `phoneCountryCode` / `phone` fields used in the Partner-Managed model |
| **Email registration** | `email` + `emailCode` (email OTP) | Both must be supplied together |
**The two paths are mutually exclusive**: `smsCode` and `emailCode` must never be sent in the same request; doing so is rejected outright with `SMS_EMAIL_CODE_MUTUALLY_EXCLUSIVE`. If neither path's field set is satisfied, the API returns `INVALID_PARAMETER`.
The registration path (endpoints are always as defined in the API reference):
1. Call `POST /captcha/v1/send-mobile-code` **or** `POST /captcha/v1/send-email-code` to send the one-time code, depending on the path you chose;
2. Call `POST /account/v1/register` with the field set for that path to complete registration.
> Every endpoint returns the same `{code, message, messageDetail, data}` envelope, with `code=SYS_SUCCESS` on success. See the [authentication guide](../integration-resources/overview).
>
> Once registration succeeds, the system generates a customer identifier (`externalUserId`) that ties together all later KYC, card issuing, deposit and query calls. The `register` endpoint returns it directly in the `data` field of the response (`data` is a plain string), so store it as soon as registration succeeds. Full details of the registration endpoints and fields are in [Signing Up a Customer](../how-to-use/signing-up-a-customer/overview).
>
> Examples always use masked placeholders: mobile number `+65xxxxxxxx`, email `user@example.com`. **Never** put real end-customer data in support tickets or logs.
***
## What documents does end-customer KYC require?
DCS runs both the automated and the manual review inside the system. End customers need to provide:
* **Proof of identity (POI)**: which document types are accepted (passport, national ID, driving license and so on) varies by country or region; the DCS document whitelist is authoritative;
* **Proof of address (POA)**: required in some countries or regions, and whenever the Sumsub verification covers POI only;
* **Selfie / liveness**: used to confirm that the document belongs to the person presenting it.
> For the accepted POI/POA whitelist per country or region and whether POA is mandatory, see [Compliance Β· KYC documents](./kyc-documents). The five KYC states (`UNDO`/`INIT`/`PENDING`/`PASS`/`REFUSE`) are in [Compliance Β· Overview](../basic-concepts/compliance-kyc-flow).
DeCard-Managed currently **does not support** corporate onboarding (KYB), ultimate beneficial owner (UBO) collection or company registration documents; the pre-launch document list covers **individual KYC only**.
KYC review turnaround times and standardized rejection reason codes still need to be confirmed per Program and against compliance policy. `REFUSE` is the terminal state of that application; if a new application is needed, the system allows at most 10 submissions in any 24-hour window (configurable).
***
## Sumsub Share Token or DCS-hosted H5? How do I choose?
It comes down to whether you already run KYC yourself:
| Your KYC setup | Approach | Key points |
| - | - | - |
| **Already using Sumsub** | Sumsub Share Token | You complete verification in Sumsub, then pass the Sumsub Share Token (field `sumsubShareToken`) to DCS, and DCS retrieves the verification result. If the Sumsub applicant covers POI only, POA still has to be supplied |
| **No KYC capability of your own** | DCS-hosted H5 guidance page | Send the customer to the DCS-hosted KYC and card application pages through the H5 guidance link (`/redirect/v*/guidance-link`); DCS collects the documents and completes verification |
If you go the Sumsub route, you need to configure the DCS partner token separately for sandbox and production on the Sumsub platform. **Before going live, confirm with DCS which token sharing mode applies, and make sure the corresponding contract with Sumsub is signed.** An unsigned contract will hold up your launch.
> The two Sumsub token sharing options and the integration steps are covered in [Sumsub KYC data sharing](./kyc-vendor); H5 guidance page parameters are in [H5 KYC / card application guidance page](../integration-resources/h5-kyc-guidance).
***
## What has to happen before cross-border fund movements? (Travel Rule)
DeCard-Managed follows Travel Rule requirements: **a customer must upload their Travel Rule information (identity, source of funds, source of wealth and so on) before you can obtain an on-chain deposit address for them.**
* Upload: `POST /account/v1/update-travel-rule`
* Query: `GET /account/v2/query-travel-rule`
> This is a DeCard-Managed compliance capability; see [Compliance Β· Travel Rule](../how-to-use/virtual-accounts/travel-rule).
***
## What configuration is needed up front?
| Item | Purpose | Provided by |
| - | - | - |
| Access credentials (API key / auth headers) | Admission credential for API calls and identification of the caller | **DCS gives these to you** (contact the DCS team for sandbox) |
| API base URL | Target host for every API request | Sandbox: `https://api.thedecard-sandbox.com`; production: `https://api.thedecard.com`. See [First Steps](../getting-started/first-steps) |
| Webhook callback URL | Receives the events DCS pushes to you (card status, transactions, KYC status, deposit credited and so on) | **You give this to DCS** |
| Network egress IPs (whitelist) | Caller egress IPs to be added to the DCS whitelist | **You give these to DCS** |
Provide the egress IPs for sandbox and production separately, matching your actual network topology, so that a missing whitelist entry does not block you at launch.
> Auth headers and signing rules are in [Integration Resources Β· Overview / authentication guide](../integration-resources/overview); whitelisting is covered in [IP whitelisting](../integration-resources/ip-whitelisting); webhook events and WebSocket real-time push (a DeCard-Managed capability) are in [Webhook + WebSocket real-time notifications](../integration-resources/webhook-websocket).
Access credentials are sensitive. **Never** put them in support tickets, logs or the body of outbound email; confirm with your DCS business or technical contact how production credentials will be handed over.
***
## Pre-go-live self-check list
* [ ] Card types, card scheme, card BIN and issuing markets are confirmed, and DCS has configured your access credentials and card category (`categoryId`)
> (Evidence: an authenticated sandbox call such as `/card/v3/detail` succeeds and returns something other than 401/403)
* [ ] The KYC approach (Sumsub Share Token or DCS-hosted H5) is agreed with DCS, and if you are using Sumsub, the contract and token setup are done
> (Evidence: one complete KYC run through the chosen approach in the sandbox, reaching a final state of `PASS`)
* [ ] The individual KYC document list (POI / POA / selfie) is confirmed and checked against the document whitelist for each issuing market
> (Evidence: an item-by-item comparison against [KYC documents](./kyc-documents), with every document required in each issuing market ready)
* [ ] Your webhook callback URL and network egress IPs are submitted to DCS, for both sandbox and production
> (Evidence: the DCS team confirms the whitelist is active, or a test push triggered from the sandbox console proves your callback endpoint is reachable)
* [ ] Real-time notifications are integrated (webhook event subscriptions, plus WebSocket if you need real-time push to end customers)
> (Evidence: a test webhook triggered from the DCS sandbox console arrives at your endpoint and passes signature verification)
* [ ] The "register β KYC β issue card β deposit β spend" loop runs end to end in the sandbox
> (Evidence: at least one sandbox card created, one simulated deposit credited and visible as available balance via [User balance](../how-to-use/managing-transactions/user-balance), and one simulated purchase or authorization completed)
* [ ] The handover process for production credentials is agreed with DCS and you have switched to the production base URL
> (Evidence: the production base URL (`https://api.thedecard.com`) and production credentials are confirmed ready)
***
## In this section
* [Post-Go-Live FAQ](./post-go-live) β day-to-day questions once you are live: card spending and refunds, adding cards to mobile wallets, transaction disputes and escalation paths.
## Next steps
* To prepare the launch path and its prerequisites, see [First Steps](../getting-started/first-steps) and [Quickstart](../getting-started/quickstart).
* For end-customer registration and KYC integration, see [Signing up a customer](../how-to-use/signing-up-a-customer/overview) and [Compliance Β· Overview](../basic-concepts/compliance-kyc-flow).
* For card issuing and management, see [Issuing cards](../how-to-use/managing-cards/issuing-cards).
* For questions that come up once you are live, bookmark the [Post-Go-Live FAQ](./post-go-live) in this section.
# Transaction Issues and Disputes
Source: https://docs.thedecard.com/en/decard-managed/customer-success/transaction-issues-disputes
Explains how to handle a disputed transaction in the DeCard-Managed model: how to tell a refund or reversal (credited automatically by the system) apart from a dispute or chargeback (handled through the operations channel), how to contain losses from unauthorized use by freezing the card, and how dispute proceeds get back to the user's independent balance.
## π Guide
When a cardholder challenges a transaction, whether the goods never arrived, the amount does not match, or the transaction was never initiated by them at all, you need a clear handling path: contain the loss first, then work out whether this is a refund or reversal that will be credited automatically, or a chargeback that has to go through the dispute process. DCS is a card issuer regulated by the Monetary Authority of Singapore (MAS); the final ruling on a dispute is made by the card network (Visa/Mastercard) under its own rules, and DCS sits between the card network and the integrator to take in the case, present the evidence, and pass the recovered funds back.
**Current capability (read this first)**: the DeCard-Managed model currently has **no self-service dispute or chargeback API**, and no refund or reversal endpoint that an integrator can call. Disputes are submitted and followed up through the **operations channel**, and a self-service dispute API is still on the roadmap. This page describes the paths that genuinely exist today: automatic crediting of refunds and reversals, loss containment by freezing the card, and dispute submission through operations.
***
## First, separate three things: refunds, reversals, and disputes
These three are often lumped together, but in the DeCard-Managed funds chain they are entirely different events with different handling paths. Note that **refunds and reversals are read-only transaction classifications** credited automatically by the system; they are **not** refund or reversal endpoints that an integrator can call:
| Event | Who initiates it | How it appears in the DeCard-Managed model | Dispute process needed |
| :- | :- | :- | :- |
| **Refund / return** | The merchant refunds voluntarily | Card transaction `authType=REFUND` (for QR payments, `transType=REFUND`/`REFUND_PART`); appears as a credit on the user's independent balance (`free` increases) and is handled automatically by the system | No |
| **Reversal / cancellation** | The merchant or the card network reverses the original authorization | Card transaction `authType=REVERSAL` (for QR, `transType=REVERSAL`); the hold is released at the authorization stage and the balance is credited, handled automatically by the system | No |
| **Dispute / chargeback** | The cardholder challenges the transaction | No self-service API exists; currently submitted through the **operations channel** | Yes |
> The `EXPEND` (purchase), `REFUND` (return), and `REVERSAL` (purchase reversal) values above are **read-only classification enums** of the `authType` field on the card transaction endpoint (on the QR payment side, the corresponding `transType` values are `PAY` / `REFUND` / `REFUND_PART` / `REVERSAL`). They describe what kind of record the system wrote down; there is **no** endpoint that lets an integrator "initiate a refund" or "initiate a reversal".
**The key test**: the dispute process starts only when the merchant **will not refund**, or when the transaction **was not initiated by the cardholder at all** (unauthorized use). If the merchant has already refunded, the money is credited automatically to that user's independent balance, so **do not raise a dispute on top of it**. A merchant refund plus a dispute recovery for the same transaction is treated by the card network as an anomaly.
***
## Refunds and reversals: credited automatically to the user's independent balance
In the DeCard-Managed model, user assets are recorded on a **user-level independent balance**. When a merchant refund or a transaction reversal occurs:
* The funds are posted as a **credit** to that end user's independent balance: the available balance `free` (conceptual alias availableBalance) increases; if a hold is released, the frozen balance `freeze` (frozenBalance) decreases accordingly.
* This happens **automatically, with no refund or reversal call from you and no second allocation step**. That is exactly the core difference from the Partner-Managed model: the money lands directly on the user, instead of landing first in the integrator's aggregate balance account and then being matched to a user by the integrator.
* In the user's card transaction detail the entry carries the debit/credit indicator `debitCreditIndicator=C` (Credit, a reverse transaction such as a return or a repayment); a normal purchase carries `D` (Debit).
> The interface definition is authoritative for balance field naming: the query endpoint returns `free` / `freeze` / `total`, while `availableBalance` / `frozenBalance` are conceptual aliases only. The balance query returns an **array**, so iterate over it by `asset`. See [User Balance](../how-to-use/managing-transactions/user-balance).
***
## Loss containment for unauthorized use: freeze the card (`/card/v2/block`)
When a user reports an unauthorized transaction, the first step is to **freeze the card concerned immediately** to stop further losses. This is the containment action an integrator can perform on its own today.
> **Who does what**
>
> * **The integrator**: (1) freeze the card concerned immediately; (2) guide the user to have a replacement card issued where needed (card replacement in the DeCard-Managed model goes through the hosted guidance page, see below); (3) gather the evidence and submit a dispute request to DCS through the operations channel.
> * **DCS**: takes in the case, raises the chargeback with the card network, tracks the ruling, and reports the recovered funds and progress back to the integrator.
**Freezing a card** uses the endpoint `POST /card/v2/block` (freeze and unfreeze are distinguished by the `block` boolean; **there is no separate unfreeze/unblock endpoint**, and paths in this model carry **no** `/open-api/` prefix):
Request (masked placeholders; never write real PII):
```json theme={null}
{
"externalUserId": "user_xxxxxxxx",
"block": true,
"cardId": "card_xxxxxxxx",
"smsCode": "",
"emailCode": ""
}
```
| Parameter | Type | Required | Description |
| :- | :- | :- | :- |
| `externalUserId` | String | Yes | Channel user ID |
| `block` | Boolean | Yes | `true` = freeze (contain the loss) / `false` = unfreeze |
| `cardId` | String | Yes | Card ID |
| `smsCode` | String | Conditional | SMS verification code: required to unfreeze, not required to freeze |
| `emailCode` | String | Conditional | Email verification code; can be used instead of the SMS code when unfreezing |
Successful response (standard envelope `{code, message, messageDetail, data}`, success code `code = SYS_SUCCESS`):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": false
}
```
**Error handling**: containment is a race against time, so build a fallback for a failed freeze call.
* Missing `smsCode`/`emailCode` when unfreezing: supply the verification code and retry (freezing itself needs no verification code and can be called immediately).
* If the call returns anything other than `SYS_SUCCESS`: trust `message` and `messageDetail`, and **do not** treat HTTP 200 alone as success. If the retry also fails, escalate to the operations channel for a manual freeze right away rather than leaving exposure on a compromised card.
For the full description of freeze and unfreeze and of card detail and status queries, see [Managing Cards Β· Overview](../how-to-use/managing-cards/overview); if the user needs a replacement card, contact DCS for assistance.
***
## Decision flow for handling unauthorized use
> On-chain and off-chain boundary: in the diagram above, the card network ruling and chargeback are an **off-chain** card network process, and crediting the funds to the user's independent balance is a bookkeeping action on the DeCard-Managed ledger (off-chain). This model involves no on-chain collateral or signatures, so nothing in this flow touches the chain.
***
## How dispute proceeds get back to the user
Once the card network rules in the cardholder's favour, the recovered funds are **credited directly to that end user's independent balance account** (`free` increases), in exactly the same form as a merchant refund, with no second allocation step from the integrator:
* In the user's card transaction detail it appears as a credit entry with the debit/credit indicator `debitCreditIndicator=C` (Credit).
* Because the DeCard-Managed model holds user assets in custody and records them on a user-level independent balance, the recovered funds land in that user's balance account. **This differs from the Partner-Managed model, where the funds land first in the integrator's balance account and the integrator then matches them to the end user.**
> **Reconciliation tip**: record the corresponding transaction ID when you follow up a dispute through the operations channel, so that you can match the incoming credit to the specific dispute later.
***
## FAQ
### A user reports an unauthorized transaction. What should I do?
1. Freeze the card concerned immediately to contain the loss: `POST /card/v2/block` with `block=true`.
2. Arrange a replacement card for the user where needed (contact DCS for assistance).
3. First retrieve the full detail of the transaction (transaction ID, amount, currency, merchant name, timestamp) through `GET /card/v2/statements/detail` or `GET /card/v1/fiat/transactions`, package it together with the card's last four digits as evidence, and submit the dispute to DCS through the operations channel. DCS then raises the dispute with the card network.
There is currently **no self-service dispute API** in this model, so dispute submission goes through the operations channel.
### After the merchant refunds, when and where does the money arrive?
A merchant refund appears in the user's card transactions as a credit with `authType=REFUND` (for QR payments, `transType=REFUND` or `REFUND_PART`), and the system **credits it automatically to that end user's independent balance** (`free` increases). You do not call any endpoint and you do not allocate the money a second time. The arrival time depends on how quickly the merchant and the card network process it; if a refund stays outstanding for an unusually long time, ask the operations channel to investigate.
### A transaction was declined at the terminal but the account was still debited. What should I do?
First confirm with the merchant whether the transaction was declined in their system. If the authorization is never finally posted (settled), the hold is normally released back to the user's available balance automatically, showing up as a `REVERSAL` and a released hold, with no further action needed. See [Settlement](../how-to-use/managing-transactions/settlement) and [User Balance](../how-to-use/managing-transactions/user-balance).
### Are refunds (`REFUND`) and reversals (`REVERSAL`) endpoints I can call?
No. `EXPEND` / `REFUND` / `REVERSAL` (and the QR values `PAY` / `REFUND` / `REFUND_PART` / `REVERSAL`) are all **read-only transaction classification enums** that label what kind of record the system wrote down. There is no endpoint for an integrator to "initiate a refund or reversal"; these actions are initiated by the merchant or the card network and credited automatically by the system.
### Which transactions can be disputed?
As a rule, only **settled** transactions with a **non-zero amount** can be disputed; pending transactions and zero-amount (verification) transactions cannot. Card network disputes have a deadline, usually **120 days** from the transaction date, and the closer you get to it the higher the risk of rejection, so submit through the operations channel as early as possible. Only one dispute may be open on a given transaction at a time, so do not submit the same case twice.
### The merchant has already refunded. Should I still raise a dispute?
No. The merchant refund is credited back to the user's independent balance automatically, and raising a dispute on top of it produces a double recovery that the card network may treat as an anomaly. Confirm first whether the credit has landed, and use the dispute process only when the merchant **refuses to refund or has not refunded for an unusually long time**.
### Who handles disputes, and where do I follow up on the outcome?
DCS takes in the case and raises it with the card network, and the outcome and progress are reported back to you through the operations channel (customer success or an operations ticket). When you submit, provide at minimum: the transaction ID, the card's last four digits (masked, for example `****1234`), the merchant details, the dispute category with a written description, and any supporting evidence files.
***
## Confirm with DCS during integration
Align the following points with DCS when you integrate:
* The roadmap for a self-service dispute and chargeback API, plus the exact format and turnaround for dispute submission through the operations channel.
* Whether webhook events are available to subscribe to at each stage of a dispute.
* Customer-facing commitments such as dispute fees and refund arrival deadlines.
## Next steps
* How the release of holds and the crediting of refunds and reversals play out in settlement: [Basic Concepts βΊ Transaction Lifecycle βΊ Settlement](../how-to-use/managing-transactions/settlement)
* Reconciling recovered funds against the user's independent balance: [How-To Guide βΊ User Balance](../how-to-use/managing-transactions/user-balance)
* The freeze and card replacement operations needed to contain losses: [How-To Guide βΊ Managing Cards Β· Overview](../how-to-use/managing-cards/overview)
* Common questions on identity verification and compliance: [FAQ βΊ Verification and Compliance](./verification-and-compliance)
* Escalation and support paths: [FAQ βΊ Escalations and Support Paths](./escalations-and-support-paths)
# Identity verification and compliance
Source: https://docs.thedecard.com/en/decard-managed/customer-success/verification-and-compliance
A Q&A round-up of the questions we hear most often about end-customer identity verification (KYC) and compliance: reasons for rejection, KYC stuck in pending, AML hits, acceptable documents, Sumsub token modes, the Travel Rule and more.
## π Guide
This page collects the questions that come up most often about end-customer identity verification (KYC) and compliance during integration and day-to-day operation: why a customer was rejected, why KYC sits in pending for a long time, how AML hits are handled, which documents are acceptable, which Sumsub token mode to integrate with, and what the Travel Rule requires. Each answer is kept short and links to the corresponding detail page in the "Basic concepts βΊ Compliance" section.
***
### What KYC states exist, and what does the query return?
DCS models KYC state on two levels: a **top-level `status`** and a **sub-state `statusDescription`**, the latter returned only when `status=INIT` or `PENDING`.
* Top-level `status`: `UNDO` (no KYC yet) / `INIT` (started, in progress) / `PENDING` (submitted, under review) / `PASS` (approved) / `REFUSE` (rejected).
* Sub-states when `status=INIT`: `PENDING_DOC_VERIFICATION` / `DOC_VERIFICATION_PASS` / `PENDING_POA_DOC` / `POA_REJECTED` / `POI_REJECTED` / `OTHERS`.
* Sub-states when `status=PENDING`: `PENDING_MANUAL_REVIEW` / `PENDING_DOC`.
### A customer's proof of identity (POI) was rejected. What now?
When proof of identity (POI) is rejected, the KYC sub-state is **`POI_REJECTED`** (under `status=INIT`). The usual causes are a document type that is not on the whitelist, a document that does not match the customer's details, or a document that is illegible or a screenshot (screenshots are not accepted).
Guide the customer to resubmit a compliant document as described in [KYC documents](./kyc-documents). Whether a rejection is temporary or final, and how resubmission works, is covered in [KYC rejections and resubmission](./kyc-rejections).
### A customer's proof of address (POA) was rejected. What now?
When proof of address (POA) is rejected, the KYC sub-state is **`POA_REJECTED`** (under `status=INIT`).
A failed POA can be corrected by submitting a new proof of address through the resubmission endpoint. **Whether a fresh Sumsub token is needed is determined by DCS operational policy**, so confirm it with DCS during integration. For acceptable POA documents and which countries require a separate POA, see [KYC documents](./kyc-documents).
### KYC has been pending (under review) for a while. Why?
When the top-level `status=PENDING` there are two possibilities, told apart by the sub-state:
* **`PENDING_MANUAL_REVIEW` (awaiting manual review)**: the automated check (typically around 3 minutes) did not clear the case, so it moved to manual review. The operations team may reach out to the customer by email or through a KYC questionnaire for more information.
* **`PENDING_DOC` (awaiting documents)**: waiting for the customer to supply the required documents.
Check that the customer uploaded legible, complete images of both sides of the document, and allow time for processing.
### A customer was flagged by an AML hit. How is that handled?
When a customer's name matches an anti-money laundering (AML) list, the system flags the application automatically and routes it to manual review. The hit alone does not reject the KYC.
To reduce false positives, always use the customer's **full legal name** rather than an abbreviation or nickname. After a hit, DCS operations and compliance review the case, and KYC moves to `PASS` once it clears.
### How the Sumsub token is configured
The sharing relationship is decided by the Sharing Partners configuration in the Sumsub console and by your contract, and is **not carried in any API field**.
Before going live, make sure your Sumsub contract covers the sharing relationship with DCS β without it your launch will be held up. The **only** Sumsub-related field in `kycInfo` on the card application request is `sumsubShareToken`; there is no mode enumeration field.
For both token sharing directions (you as data provider, you as data recipient) and the integration steps, see [Sumsub token sharing](./kyc-vendor).
### Which documents count as both proof of identity (POI) and proof of address (POA)?
As an issuer regulated by the Monetary Authority of Singapore (MAS), DCS maintains a whitelist of specific documents for certain countries and regions. Where a document is on that whitelist, the customer **only needs to submit that one document, complete face verification and fill in the required background information**, with no separate POA.
* Identity documents must be within their validity period; a document used as proof of address must have been issued within the last three months. **Screenshots are never accepted**.
* Whether an additional POA is required varies by country and document type (passports, for instance, generally still need a separate POA).
* If your agreement with DCS does not cover a given country or region, customers there must still provide proof of address separately under the standard requirements.
The full matrix of acceptable document types per country and whether an additional POA is required is in [KYC documents](./kyc-documents).
### Can KYC information be filled in Chinese?
Text fields should follow the language requirements agreed between DCS and Sumsub at integration time; the **documents themselves** may be uploaded in their original language, Chinese included. Confirm the exact language requirements with DCS during integration.
### What is the Travel Rule, and how does DCS support it?
The Travel Rule is an anti-money laundering requirement covering virtual asset transfers. DeCard-Managed provides Travel Rule query capability through `GET /account/v2/query-travel-rule`.
This is a DeCard-Managed compliance capability. For the detailed description and field specifications, see [Travel Rule](../how-to-use/virtual-accounts/travel-rule).
### Can a customer resubmit after `REFUSE`?
Whether a top-level `REFUSE` can be followed by a resubmission, and whether it is truly terminal, depends on the specific rejection type. For the difference between temporary and final rejections and the resubmission path for each, see [KYC rejections and resubmission](./kyc-rejections).
## Next steps
* Rejections and resubmission (temporary versus final rejection, resubmission paths): [Basic concepts βΊ Compliance flow βΊ KYC rejections and resubmission](./kyc-rejections)
* KYC documents (whitelisted documents plus the POI/POA matrix): [Basic concepts βΊ Compliance βΊ KYC documents](./kyc-documents)
* Sumsub token sharing (sharing-partner configuration, and the steps in both directions): [Basic concepts βΊ Compliance βΊ Sumsub token sharing](./kyc-vendor)
* Travel Rule: [Basic concepts βΊ Compliance βΊ Travel Rule](../how-to-use/virtual-accounts/travel-rule)
* Compliance overview and where KYC sits in the overall flow: [Basic concepts βΊ Compliance βΊ Overview](../basic-concepts/compliance-kyc-flow)
# First Steps
Source: https://docs.thedecard.com/en/decard-managed/getting-started/first-steps
Everything you need to settle before integrating with DCS: getting your Enterprise created, collecting your API Key and Secret Key, submitting your IP whitelist, and importing the Postman collection for integration testing.
## π Guide
Before you call any endpoint, DCS must create your Enterprise and issue your API credentials, and you must give DCS the network egress addresses to whitelist. This page walks you through those steps. As a licensed issuer running its own BINs, DCS issues production credentials through a one-time secure retrieval mechanism so that keys cannot be intercepted by a third party. The full rules for the request signature headers and the HMAC-SHA256 algorithm are covered in the [Authentication Guide](../integration-resources/overview).
***
### Environments and conventions (integration reference)
**Environment URLs**
| Environment | URL |
| - | - |
| Sandbox | `https://api.thedecard-sandbox.com` |
| Production | `https://api.thedecard.com` |
**Global conventions**
* Timestamp fields carry a timezone offset (for example `+08:00`)
* Currencies use **ISO 3-letter currency codes**; countries use **ISO 2-letter country codes**
* Unless stated otherwise, **Enterprise** refers to your partner team
***
### Step 1: Confirm your Enterprise and integration model
DCS creates an **Enterprise** for you. It is the identity that carries your access to the DeCard-Managed model, together with all of your card issuing, authorization, and account management capabilities. In this model, authorization decisions are made inside the DCS platform and each cardholder holds an independent account balance. See the [Overview](./overview) for the full model.
### Step 2: Collect your API Key and Secret Key
DCS provisions one pair of API credentials per Enterprise:
| Credential | Purpose |
| - | - |
| **API Key** | A globally unique identifier used to identify you and trace the origin of each request. It is the admission credential for every API call |
| **Secret Key** | Paired with the API Key to sign requests. Store it securely and never share it with any third party |
* **Sandbox**: contact the DCS team directly for your API Key and Secret Key.
* **Production**: credentials are delivered through a one-time secure retrieval link so that keys cannot be intercepted by a third party. The full retrieval procedure (secure mailbox, retrieval IP, assembling and running the temporary link) is described in the [Authentication Guide](../integration-resources/overview).
> For how the API Key and Secret Key are used to sign requests with HMAC-SHA256, see the [Authentication Guide](../integration-resources/overview).
### Step 3: Submit your IP whitelist
Give DCS your **network egress addresses**, listed separately for sandbox and production. You can only call the APIs once DCS has whitelisted them. For what each type of whitelist covers, how sandbox differs from production, and the full submission instructions, see the dedicated [IP Whitelisting](../integration-resources/ip-whitelisting) page.
> The retrieval IP used to collect production credentials (Step 2) and the egress IP used for API calls are two different whitelists. Submit both.
### Step 4: Import the Postman collection for fast integration testing
> Make sure Postman is installed. If you are not familiar with git, open [dcs-api-postman](https://github.com/decard-tech/dcs-api-postman) on GitHub and select **Code > Download ZIP** to get the repository files.
DCS publishes a Postman collection and environments (JSON files) so you can try the RESTful APIs sooner. Import them, fill your own API Key, Secret Key, and host into the environment variables, and you are ready to go:
1. Download the [dcs-api-postman](https://github.com/decard-tech/dcs-api-postman) repository, then click **Import** in Postman (on macOS the button sits in the top-left corner).
2. In the Import dialog, open the "Folders" tab and import the collection and environment files you downloaded.
3. Open the "Environments" tab on the left and fill in `apiKey` (your API Key), `secretKey` (your Secret Key), and `host` (the environment URL) in the "Current value" column of the environment you picked. Then select that environment from the drop-down in the top-right corner and start sending signed requests.
> **Security practices**
>
> * Do not use collections from unknown sources
> * Review the contents of an environment JSON file before you use it
> * Do not run any code you do not understand
Once that is done, head to [Quickstart](./quickstart) and issue your first card in the sandbox.
***
## Next steps
With credentials and whitelists in place, go to [Quickstart](./quickstart) to issue your first card in the sandbox. For the request signature headers and the HMAC-SHA256 signing rules, see the [Authentication Guide](../integration-resources/overview).
# Overview
Source: https://docs.thedecard.com/en/decard-managed/getting-started/overview
The entry point for integrating with DeCard-Managed mode. It covers what the DCS platform is, how the managed model works, and which capabilities it provides.
## π Guide
DCS is a modern card issuing platform built for enterprises. Whether you run an exchange, a wallet, or another platform, you can integrate issuing into your own system through developer-friendly APIs, issue virtual and physical cards to your end users, and let them deposit crypto and spend in fiat.
DCS holds a Singapore MAS license and issues on its own BINs across multiple issuing markets and card networks, providing full-stack regulated infrastructure that spans card issuing, KYC, real-time authorization, and settlement.
This documentation is written for partners integrating with **DeCard-Managed mode**.
## What DeCard-Managed mode is
DCS offers two integration modes: **DeCard-Managed** and **Partner-Managed**. This documentation covers **DeCard-Managed**, where **the authorization decision is made inside the DCS platform**. The cardholder moves funds into DCS custody, and DCS decides in real time how much the card may spend based on the assets available. The partner does not have to operate its own authorization service.
In this mode every cardholder has a dedicated funding account, and balances are booked per currency (asset). In the balance query API, the state of funds is expressed by three fields: `free` (available), `freeze` (frozen), and `total`. When a payment is made, DCS checks the available balance and then handles authorization and settlement.
> For how funding accounts and limits are actually booked, see [Basic Concepts βΊ Ledgering](../basic-concepts/ledgering-system).
## What DCS provides
* **End-to-end issuing**: user registration, KYC, and card issuing, through to real-time authorization, settlement, and daily reconciliation inside DCS
* **Developer first**: RESTful APIs, webhooks, and WebSocket streaming; crypto deposit and withdrawal, QR pay, Apple Pay, and Google Pay
* **Compliance and security**: licensed issuing, Sumsub KYC/AML and Travel Rule coverage, and protection of sensitive card data within PCI scope
## Recommended reading path
This documentation is organized around getting you live as fast as possible: **First Steps β Quickstart β Compliance (KYC) β Card Issuing β Transactions and Balances β Webhooks / Reconciliation**. The Basic Concepts group (Ledgering, transaction lifecycle) is background material. It does not block integration, so read it when you reach the matching step.
## Next steps
Go to [First Steps](./first-steps) to get ready for integration: have your environment enabled, collect your API credentials, and configure your RSA keys and callback endpoints. That page also lists the actual environment endpoints (sandbox and production) and the authentication setup. Once you are set up, issue your first card in the [Quickstart](./quickstart).
# Quickstart
Source: https://docs.thedecard.com/en/decard-managed/getting-started/quickstart
Issue your first working card in the sandbox along the shortest path: send an OTP β register the user β get the onboarding guidance link β receive the card-application Webhook β manage the card β fund it and simulate a purchase.
This page walks you through issuing your first working card in the sandbox using the **recommended best-practice path**. As a licensed card issuer with its own BINs, DCS closes the loop on issuing, KYC, authorization and settlement inside a single API set β you do not need to build any card-core capability yourself.
This page follows one minimal path only (mobile registration, hosted guidance page, a single virtual card, a single deposit currency). The full KYC flow, chain/currency matrix, card management and reconciliation are covered on their own pages.
***
## Before you start
Make sure the following are in place (see [First Steps](./first-steps) for details):
* You understand the **DeCard-Managed model**: authorization decisions are made inside DCS; cardholders move funds into DCS custody and DCS derives the card's spending limit from their available assets in real time (see [Overview](./overview)).
* You have received your **API Key / Secret Key**.
* You have given DCS your **network egress IP** (DCS enforces an API whitelist).
* **Webhook callback URL** (required): the card-issuing result in this flow is delivered by Webhook. Without it you cannot complete Step 4.
* **WebSocket connection** (optional): configure it if you want real-time pushes of balance changes, transaction results, etc. See [Webhook & WebSocket](../integration-resources/webhook-websocket).
> Environments β Sandbox: `https://api.thedecard-sandbox.com`; Production: `https://api.thedecard.com`.
> **Authentication**: every request must carry the four headers `X-DAPI-API-KEY` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE` / `X-DAPI-SIGN` with an HMAC-SHA256 signature. The full rules (signature concatenation formula, replay protection, code samples) are in the [Authentication Guide](../integration-resources/overview). The examples below **omit the auth headers** to keep the focus on business fields β they are mandatory in real calls. `SecretKey` is only used locally to compute the signature and is never sent over the wire.
> **Path prefix**: DeCard-Managed modules use the `/account/`, `/card/`, `/crypto/`, `/user-asset/`, `/redirect/` and `/simulation/` prefixes directly (no `/open-api/` prefix).
All mobile numbers, user IDs and addresses below are **masked placeholders** β replace them with your own values.
***
## π Guide
Six steps in total. The overall shape is: **you call the API to initiate, the cardholder completes sensitive operations on a DCS-hosted page, and the result comes back to you over Webhook.**
> The diagram shows the **production path**. In the sandbox, the deposit and purchase in Step 6 use simulator endpoints instead β see that step for details.
### Step 1: Send the SMS verification code
In the DeCard-Managed model, users register in two stages with "mobile number + SMS OTP". First request a code for the target mobile number:
```http theme={null}
POST /captcha/v1/send-mobile-code
```
```json theme={null}
{
"mobileCode": "SG",
"mobile": "91234567",
"externalUserId": "",
"behavioral": "REGISTER"
}
```
> `mobileCode` is the ISO 2-letter country code (e.g. `SG` / `CN` / `US`); `mobile` is the local number without the dialing prefix. Leave `externalUserId` empty when registering a new user.
> `behavioral` is a **scenario enum** identifying what the code is for: `REGISTER` (registration) / `CARD_UNFROZEN` (unfreeze a card) / `WITHDRAW` (withdrawal). Use `REGISTER` here.
Successful response (the envelope is always `{code, message, messageDetail, data}`, success code `code = SYS_SUCCESS`):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": ""
}
```
> `data` is always an empty string for this endpoint β the code is delivered by SMS, not in the response body. Judge success by `code = SYS_SUCCESS`.
> About the envelope: `messageDetail` is usually `null` on success; on failure, or when the end user needs to be prompted, it is an **object** (with `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`). To keep the focus on business fields, the remaining examples on this page show the envelope in its success form only.
### Step 2: Register the user
Complete registration with the SMS code from the previous step:
```http theme={null}
POST /account/v1/register
```
```json theme={null}
{
"mobileCode": "SG",
"mobile": "91234567",
"smsCode": ""
}
```
> Mobile registration requires `mobileCode` + `mobile` + `smsCode` **all three together**.
> DCS also supports an email registration path (`email` + `emailCode`), which is **strictly mutually exclusive** with the mobile path β sending `smsCode` and `emailCode` in the same request is rejected with `SMS_EMAIL_CODE_MUTUALLY_EXCLUSIVE`. This page uses the mobile path only; for the email path see [Signing Up a Customer](../how-to-use/signing-up-a-customer/overview).
The `data` field of the response **returns `externalUserId` directly** (the user ID, a plain string). Every later endpoint identifies the user by it β store it:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": "4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c"
}
```
### Step 3: Get the onboarding guidance link
**This is the recommended way to issue a card.** Both KYC and the card application happen inside a DCS-hosted H5 page β the cardholder's documents and selfie go straight to DCS without passing through your backend, so you do not take on that compliance and storage burden.
```http theme={null}
POST /redirect/v2/guidance-link
```
```json theme={null}
{
"action": "KYC_GUIDE",
"externalUserId": "4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c",
"successRedirectUrl": "https://your-app.example.com/kyc/success",
"errorRedirectUrl": "https://your-app.example.com/kyc/failed",
"language": "en"
}
```
> `action`, `externalUserId`, `successRedirectUrl` and `errorRedirectUrl` are **all required**.
> `language` uses the **lower-case hyphenated** form and is **case-sensitive**: `zh` / `en` / `ko` / `ja` / `zh-Hant` / `th` / `vi`. A value outside this whitelist is silently downgraded to the default language rather than rejected β stick to this list exactly.
> For the full parameter set (`theme` / `mode` / `primaryColor` / `selectCardPageShow`, etc.) see [H5 KYC / Card Onboarding Guidance Page](../integration-resources/h5-kyc-guidance).
The `data` field of the response is the one-time guidance link (a plain string). Hand it to the cardholder to open in a browser:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": "https://h5.thedecard-sandbox.com/en/kyc?secret=xxxxxxxx"
}
```
> β± **The link is valid for 5 minutes** (the server-side secret has a 300-second TTL); once expired the page stops working and you must request a new one.
> Therefore **do not deliver the link over asynchronous channels such as email or support tickets** β generate it and redirect while the cardholder is in an active session. If the user abandons and comes back, call this endpoint again for a fresh link rather than caching and reusing one.
> **Two pre-checks will reject the request outright β handle them during integration**:
>
> * If the user's KYC is already `PASS` or `PENDING`, the API returns `OPERATION_UNSUPPORTED` β re-initiation is not allowed.
> * If the daily application rate limit is hit, the API returns `KYC_APPLY_LIMIT_EXCEEDED` β throttle your retries and surface a message to the user.
### Step 4: Receive the card-application Webhook
Once the cardholder submits on the hosted page, **the result is pushed to you as a `CARD_APPLY` Webhook**. In this flow the partner never initiates the card application, so this is the only way to obtain `applyId` (once issuing succeeds, `cardId` can also be read from the card query endpoint in Step 5).
> The same event is **delivered over both Webhook and WebSocket** (the server pushes WS first, then the Webhook), with identical payloads. Consuming the Webhook alone is enough for this flow; if you also consume WebSocket, de-duplicate on your side.
The `data` structure of the `CARD_APPLY` event:
| Field | Type | Description |
| :- | :- | :- |
| `applyId` | string | Application ID, needed for the extra-document branch |
| `cardId` | string | Card ID, populated only after the card is issued |
| `categoryId` | Long | Card category ID, decided when the cardholder picks a card on the hosted page β the partner does not supply it |
| `network` | string | Card network (e.g. `VISA`) |
| `currency` | string | Card currency |
| `applyRef` | string | Idempotency reference of the application |
| `status` | string | Application status, see the table below |
| `errorCode` | string | Failure reason, provided only when `FAILED` |
| `needExtraInfo` | boolean | Whether extra documents are required, see the branch below |
`status` has only **three external values** (internal intermediate states such as KYC-passed and card-created are all collapsed into `PENDING`):
| Value | Terminal | Meaning and action |
| :- | :- | :- |
| `PENDING` | No | In progress. If `needExtraInfo = true`, follow the branch below |
| `SUCCEED` | Yes | Card issued, `cardId` is usable, go to Step 5 |
| `FAILED` | Yes | Issuing failed, read `errorCode` for the reason |
Return `2xx` **within 2 seconds** of receiving the Webhook (DCS uses a 2000 ms connect timeout and a 2000 ms socket timeout). Persist first and process asynchronously, otherwise the delivery is treated as a timeout and retried. See [Webhook & WebSocket](../integration-resources/webhook-websocket).
#### Branch: `needExtraInfo = true`
When KYC requires additional documents from the cardholder, the Webhook carries `needExtraInfo = true`. Request a second guidance link with the **same `applyId`**:
```http theme={null}
POST /redirect/v2/guidance-link
```
```json theme={null}
{
"action": "KYC_EXTRA_DOC",
"externalUserId": "4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c",
"applyId": "A100001",
"successRedirectUrl": "https://your-app.example.com/kyc/success",
"errorRedirectUrl": "https://your-app.example.com/kyc/failed"
}
```
> For `KYC_EXTRA_DOC`, `applyId` is **required** β omitting it returns `OPERATION_UNSUPPORTED`; an `applyId` that does not belong to the user returns `PERMISSION_DENIED`; and an application whose `needExtraInfo` is not `true` also returns `OPERATION_UNSUPPORTED`. **Drive this off the Webhook's `needExtraInfo` β do not call it speculatively.**
After the cardholder submits the documents, DCS pushes `CARD_APPLY` again until `status` reaches the terminal `SUCCEED` or `FAILED`.
### Step 5: Card management
Once you have `cardId`, day-to-day card management is available.
**List cards / query a single card**
```http theme={null}
GET /card/v3/detail?externalUserId=4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c
```
> `externalUserId` is required; `cardId` is optional β leave it empty to return all of the user's cards, or pass it to query one.
> The response includes `cardStatus` (`NORMAL` / `FROZEN` / `BLOCKED` / `CANCELLED`), `statusReason`, `cardNo` (masked), `cardHolder`, `physicalCardStatus` and more.
> `BLOCKED` means the card is blocked on the issuer side and cannot be unfrozen by the cardholder; `statusReason` gives the specific reason. See [Card Management](../how-to-use/managing-cards/overview).
**Show the full card number to the cardholder**
For PCI compliance, the full PAN, CVV and expiry are **never** returned to your backend by the API. Use the `CARD_INFO` guidance page so the cardholder views them directly on the DCS-hosted page:
```http theme={null}
POST /redirect/v2/guidance-link
```
```json theme={null}
{
"action": "CARD_INFO",
"externalUserId": "4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c",
"cardId": "1234567890123456789",
"successRedirectUrl": "https://your-app.example.com/card/back",
"errorRedirectUrl": "https://your-app.example.com/card/error"
}
```
> The four actions `CARD_INFO` / `CREATE_PHYSICAL_CARD` / `ACTIVE_PHYSICAL_CARD` / `UPDATE_PIN` **must carry `cardId`**, and the user must already hold a card β otherwise the request is rejected.
**Freeze / unfreeze**
```http theme={null}
POST /card/v2/block
```
```json theme={null}
{
"externalUserId": "4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c",
"cardId": "1234567890123456789",
"block": true
}
```
> `block = true` freezes, `false` unfreezes. In the response, `data` is a boolean indicating whether this freeze or unfreeze operation succeeded (`true` = success / `false` = failure).
**Unfreezing requires second-factor verification**: when `block = false` you must also supply `smsCode` or `emailCode`. Issue it first with the Step 1 endpoint using `behavioral = CARD_UNFROZEN`. Freezing does not require it.
> Physical card application and activation, PIN reset and the remaining capabilities are covered in [Managing Cards](../how-to-use/managing-cards/overview).
### Step 6: Fund the account and simulate a purchase
**Deposit β production**
Fetch the user's crypto deposit address and transfer on-chain to it:
```http theme={null}
GET /crypto/v2/deposit-address?externalUserId=4dc3e854-...&network=&coin=
```
> All three parameters are required. v2 returns `network` / `address` / `coin` / `fxRate` / `status`.
This endpoint does not return the minimum deposit amount or the required on-chain confirmation count. **If you need to tell the cardholder either of those, read them from the `network-coin` configuration (`minConfirm` and related fields) or maintain that configuration yourself.** The supported chain and currency matrix is in [Crypto Deposit](../how-to-use/virtual-accounts/crypto-deposit).
**Deposit β sandbox**
In the sandbox, use the simulator to credit the user's account directly, which keeps integration testing simple:
```http theme={null}
POST /simulation/v2/deposit
```
```json theme={null}
{
"chain": "",
"currency": "USDT",
"amount": "100.0",
"address": ""
}
```
> This simulates funds arriving at the user's crypto deposit address. Take `address` from the previous step.
**Confirm the balance**
```http theme={null}
GET /user-asset/v1/balance?externalUserId=4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c
```
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": [
{ "asset": "USDT", "logo": "", "network": "", "free": 100.0, "freeze": 0.0, "total": 100.0 }
]
}
```
> Balance fields: `free` = available, `freeze` = frozen, `total` = total (these are the actual API field names). The response is an **array** β iterate by `asset`.
**Simulate a purchase**
Use the simulator to raise a card authorization and verify your Webhook handling and limit changes:
```http theme={null}
POST /simulation/v2/fund-auth
```
```json theme={null}
{
"externalUserId": "4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c",
"cardId": "1234567890123456789",
"authType": "EXPEND",
"amount": 10.0,
"currency": "USD"
}
```
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": { "approved": true, "errorCode": "" }
}
```
> Prefer **v2** β it locates the card by `cardId`, which chains directly off the `cardId` from Step 4. v1 locates the card by `cardMantissa` (last four digits) and is kept for compatibility only.
> `authType` enum: `EXPEND` (purchase) | `REFUND` (refund) | `REVERSAL` (purchase reversal).
> Authorization is decided **inside DCS** against the user's available assets; `approved` is the result. Sandbox simulation produces real transaction records, triggers Webhooks and updates balances, but involves no real funds. Full scenarios in [Simulating Transactions](../how-to-use/simulating-transactions/overview).
Your first working card is now issued, funded, and has completed a simulated purchase.
***
## Next steps
* **Full KYC flow and state machine** β [Compliance](../basic-concepts/compliance-kyc-flow) β the complete swimlane for application, review, POA supplementation and AML handling
* **All guidance-page actions and parameters** β [H5 KYC / Card Onboarding Guidance Page](../integration-resources/h5-kyc-guidance) β the seven actions, language and theme customization, link validity
* **Physical cards, replacement, PIN reset** β [Managing Cards](../how-to-use/managing-cards/overview) β full card lifecycle management
* **Crypto deposit chain/currency matrix and on-chain withdrawal** β [Virtual Accounts](../how-to-use/virtual-accounts/crypto-deposit) β multi-chain deposit and withdrawal
* **All sandbox simulation scenarios** β [Simulating Transactions](../how-to-use/simulating-transactions/overview) β purchase, refund, reversal and every other transaction type
* **Auth headers and the HMAC-SHA256 signature rule** β [Authentication Guide](../integration-resources/overview) β required headers, concatenation formula, replay protection and code samples
* **Webhook & WebSocket real-time push** β [Webhook & WebSocket](../integration-resources/webhook-websocket) β payload structures for all 9 event types, retry and idempotency handling
# Card Application Data Dictionary
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-cards/application-data-dictionary
To make the enum fields inside kycInfo on the virtual card application endpoint (POST /card/v1/virtual-card/apply) easier to fill in, this page lists every accepted value and what it means. Always submit the enum code (for example EMPLOYED, UTILITY_BILL), never the display label. For how these fields are assembled into the request body, see Issuing Cards.
## π Guide
> The values below apply to the `kycInfo` sub-fields of `POST /card/v1/virtual-card/apply`. Submit the value from the `code` column.
### Proof of address document type (poaDocType)
`poaDocType` values do not differ between integration models β both share one list. **Which types are actually accepted depends on the user's country or region**: the accepted proof-of-address types vary by country, so follow the coverage list you confirmed with DCS.
> A proof of address must be **no older than three months**, and **screenshots are not accepted**.
| code | Name | Validity / notes |
| :- | :- | :- |
| `UTILITY_BILL` | Utility bill | **No older than 90 days (75 days in France)** |
| `TELECOM` | Telecom bill | No older than 90 days |
| `BANK_STATEMENT` | Bank statement | No older than 90 days |
| `BANK_LETTER` | Bank letter | An official letter from a financial institution carrying account details, stamped or signed |
| `CREDIT_CARD_STATEMENT` | Credit card statement | No older than 90 days |
| `LEASE_AND_PAYMENT` | Lease agreement plus proof of payment | Proof of payment is also required, and must be no older than 90 days |
| `MORTGAGE_AND_PAYMENT` | Mortgage agreement plus proof of payment | Proof of payment is also required, and must be no older than 90 days |
| `SALARY_STATEMENT` | Salary statement | No older than 90 days |
| `ID_CARD` | Identity card | Accepted when it is not also used as the identity document |
| `DRIVING_LICENSE` | Driving license | Accepted when it is not also used as the identity document |
| `GOVERNMENT_STATEMENT` | Government statement | β |
| `GOVERNMENT_TAX_BILL` | Government tax bill | β |
### Employment status (employmentStatus)
| code | Name | Description |
| :- | :- | :- |
| `EMPLOYED` | Employed | In regular paid employment |
| `BUSINESS_OWNER` | Business owner | Owns and runs a business |
| `SELF_EMPLOYED` | Self-employed | Works independently or freelances |
| `UNEMPLOYED` | Unemployed | Currently out of work |
| `PENSIONER` | Pensioner | Retired and drawing a pension |
| `STUDENT` | Student | Currently in education |
> When `employmentStatus = EMPLOYED`, `employerName` becomes required.
### Industry (employmentJobIndustry)
| code | Name | English label |
| :- | :- | :- |
| `INFORMATION` | Information and communications | Information and Communications |
| `RETAIL_TRADE` | Retail trade | Retail Trade |
| `FINANCIAL` | Financial and insurance activities | Financial and Insurance Activities |
| `ELECTRICITY` | Electricity | Electricity |
| `TRANSPORT` | Transport and storage | Transport and Storage |
| `CONSTRUCTION` | Construction | Construction |
| `TRAVEL_RELATED` | Travel related | Travel Related |
| `ACCOMMODATION` | Accommodation and food service | Accommodation and Food Service |
| `ENTERTAINMENT` | Entertainment | Entertainment |
| `PAYMENTS` | Payments | Payments |
| `PROFESSIONAL` | Professional services | Professional Services |
| `LUXURY_GOODS` | Luxury goods | Luxury Goods |
| `MANUFACTURING` | Manufacturing | Manufacturing |
| `MINING_AND_QUARRYING` | Mining and quarrying | Mining and Quarrying |
| `AGRICULTURE_AND_FISHING` | Agriculture and fishing | Agriculture and Fishing |
| `REAL_PROPERTY` | Real property and land development | Real Property and Land Development |
| `WHOLESALE_TRADE` | Wholesale trade | Wholesale Trade |
| `NON_EMPLOYED` | Non-employed | Non-Employed |
### Occupation (occupation)
> Submit the `code` value; the `number` column is the internal code for that occupation and is provided for cross-reference only.
| code | Name | number |
| :- | :- | :- |
| `PHYSIOTHERAPIST` | Physiotherapist | 308 |
| `SPECIALIST` | Specialist | 309 |
| `SURGEON` | Surgeon | 303 |
| `PATHOLOGIST` | Pathologist | 304 |
| `PLASTIC_SURGEON` | Plastic surgeon | 318 |
| `PSYCHOLOGIST` | Psychologist | 314 |
| `SR_EXECUTIVE` | Senior executive | 180 |
| `CHAIRMAN` | Chairman | 106 |
| `MANAGING_DIR` | Managing director | 108 |
| `JR_EXECUTIVE` | Junior executive | 200 |
| `TERTIARY_STUDENT` | Tertiary student | 2222 |
| `UNKNOWN` | Unknown occupation | 1200 |
| `GENERAL_WORKER` | General worker | 1198 |
| `UNEMPLOYED` | Unemployed | 1199 |
| `PILOT` | Pilot | 1169 |
| `AIR_STEWARD_STEWARDESS` | Flight attendant | 1170 |
| `CLERK` | Clerk | 1157 |
| `CUST_SERVICE_CONSULTANT` | Customer service consultant | 1156 |
| `ARTIST_ENTERTAINMENT` | Artist or entertainer | 1183 |
| `BEAUTY_CONSULTNT_STYLIST` | Beauty consultant or stylist | 1184 |
| `SOCIAL_WORKER` | Social worker | 1141 |
| `TECHNICAL_CONSULTANT` | Technical consultant | 1131 |
| `IT_CONSULTANT` | IT consultant | 1123 |
| `COMPUTER_ANALYST` | Computer analyst | 1124 |
| `TECHNICIAN` | Technician | 1151 |
| `WAREHOUSING_PERSONNEL` | Warehousing personnel | 1152 |
| `SYSTEMS_PROGRAMMER` | Systems programmer | 1121 |
| `TEACHER` | Teacher | 1113 |
| `INSTRUCTOR` | Instructor | 1114 |
| `ASST_PROFESSOR` | Assistant professor | 1107 |
| `VICE_DEAN` | Vice dean | 1104 |
| `PROFESSOR` | Professor | 1106 |
| `APPLICATION_PROGRAMMER` | Application programmer | 1122 |
| `OTHERS` | Other occupation | 1100 |
| `REMISIER` | Remisier | 1040 |
| `PROPERTY_AGENT` | Property agent | 1050 |
| `SALES_EXEC` | Sales executive | 1020 |
| `MARKETING_EXEC` | Marketing executive | 1010 |
| `HAWKER` | Hawker | 990 |
| `CONTRACTOR` | Contractor | 985 |
| `PARTNER` | Partner | 970 |
| `ARMY_SPECIALIST` | Army specialist | 860 |
| `CIVIL_SERVANT` | Civil servant | 840 |
| `DEPUTY_PM` | Deputy prime minister | 807 |
| `CABINET_MINISTER` | Cabinet minister | 809 |
| `BANKING_PROFESSIONAL` | Banking professional | 660 |
| `BANKER_FUND_MANAGER` | Banker or fund manager | 650 |
| `POLITICIAN_CIVIL_SERVANT` | Politician or civil servant | 800 |
| `DRAFTSMAN` | Draftsman | 740 |
| `QUANTITY_SURVEYOR` | Quantity surveyor | 750 |
| `CHEM_ENGINEER` | Chemical engineer | 540 |
| `FIN_CONTROLLER` | Financial controller | 610 |
| `PROF_ACCOUNTANCY` | Accountancy professional | 600 |
| `HIGH_COURT_JUDGE` | High Court judge | 406 |
| `CHIEF_JUSTICE` | Chief justice | 401 |
| `AUDITOR_GENERAL` | Auditor general | 403 |
| `CIVIL_ENGINEER` | Civil engineer | 510 |
| `ELEC_ENGINEER` | Electrical engineer | 520 |
| `LEGAL_ADVISER` | Legal adviser | 490 |
| `PROF_ENGINEER` | Professional engineer | 500 |
| `JUDGE` | Judge | 408 |
| `LAWYER` | Lawyer | 420 |
| `DOCTOR` | Doctor | 360 |
| `GEN_PRACTITIONER` | General practitioner | 365 |
| `RESEARCHER` | Researcher | 350 |
| `BIOCHEMIST` | Biochemist | 351 |
| `GYNAECOLOGIST` | Gynaecologist | 322 |
| `MATRON` | Matron | 380 |
| `ORTHOPAEDICIAN` | Orthopaedic surgeon | 310 |
| `OFFICER` | Officer | 250 |
| `PROF_MEDICAL` | Medical professional | 300 |
| `OBSTETRICIAN` | Obstetrician | 320 |
| `PSYCHIATRIST` | Psychiatrist | 316 |
| `MGT_CONSULTANT` | Management consultant | 160 |
| `REG_MANAGER` | Regional manager | 140 |
| `DIV_MANAGER` | Divisional manager | 150 |
| `TOP_EXECUTIVE` | Top executive | 110 |
| `GENERAL_MGR` | General manager | 130 |
| `DEPT_MANAGER` | Department manager | 210 |
| `EXEC_OFFICER` | Executive officer | 230 |
| `PRESIDENT` | President | 102 |
| `V_PRESIDENT_DIR` | Vice president or director | 104 |
| `SECURITY_SAFETY_PERSON` | Security and safety personnel | 1196 |
| `HOSPITALITY_PERSONNEL` | Hospitality personnel | 1197 |
| `HOUSEWIFE` | Homemaker | 1190 |
| `STUDENT` | Student | 1191 |
| `COOK_CHEF` | Cook or chef | 1180 |
| `DRIVER` | Driver | 1181 |
| `DIVER` | Diver | 1160 |
| `SECRETARY` | Secretary | 1155 |
| `DESIGNER` | Designer | 1182 |
| `SUPERVISOR` | Supervisor | 1150 |
| `EDITOR` | Editor | 1145 |
| `JOURNALIST` | Journalist | 1146 |
| `PASTOR` | Pastor | 1140 |
| `PROJECT_CO_ORDINATOR` | Project co-ordinator | 1130 |
| `SYSTEMS_ANALYST` | Systems analyst | 1120 |
| `PRINCIPAL` | Principal | 1110 |
| `V_PRINCIPAL` | Vice principal | 1111 |
| `LECTURER` | Lecturer | 1108 |
| `DEAN` | Dean | 1103 |
| `CHANCELLOR` | Chancellor | 1101 |
| `INSURANCE_AGENT` | Insurance agent | 1030 |
| `SALESMAN` | Salesperson | 1000 |
| `SOLE_PROPRIETOR` | Sole proprietor | 980 |
| `SELF_EMPLOYED` | Self-employed | 910 |
| `BUSINESS` | Business owner | 900 |
| `POLICE` | Police officer | 850 |
| `AMBASSADOR` | Ambassador | 820 |
| `PARLIAMENTARY_SECRETARY` | Parliamentary secretary | 830 |
| `AUDITOR` | Auditor | 640 |
| `V_PRESIDENT` | Vice president | 803 |
| `PRIME_MINISTER` | Prime minister | 805 |
| `PROF_ARCHITECT` | Professional architect | 700 |
| `ARCHITECT` | Architect | 701 |
| `MECH_ENGINEER` | Mechanical engineer | 550 |
| `SYSTEMS_ENGINEER` | Systems engineer | 560 |
| `PROCESS_ENGINEER` | Process engineer | 530 |
| `ASST_ACCOUNTANT` | Assistant accountant | 622 |
| `CREDIT_ANALYST` | Credit analyst | 630 |
| `TREASURER` | Treasurer | 612 |
| `ACCOUNTANT` | Accountant | 620 |
| `FIN_CONSULTANT` | Financial consultant | 601 |
| `HEALTHCARE_PROFESSIONAL` | Healthcare professional | 390 |
| `PROF_LAW` | Legal professional | 400 |
| `DENTIST` | Dentist | 340 |
| `CHIEF_NURSE` | Chief nurse | 382 |
| `NURSE` | Nurse | 383 |
| `RADIOGRAPHER` | Radiographer | 370 |
| `PHARMACIST` | Pharmacist | 368 |
### Job seniority (jobSeniority)
| code | Name | English label |
| :- | :- | :- |
| `SENIOR_MANAGEMENT` | Senior management | Senior Management |
| `DIRECTOR` | Director | Director |
| `PARTNER` | Partner | Partner |
| `EXECUTIVE` | Executive | Executive |
| `TEACHER` | Teacher | Teacher |
| `MANAGERIAL` | Managerial | Managerial |
| `NON_EMPLOYED` | Non-employed | Non Employed |
| `SALES` | Sales | Sales |
| `ASSOCIATE` | Associate | Associate |
| `DIPLOMAT` | Diplomat | Diplomat |
### Purpose of account (purposeOfAccount)
| code | Name | English label |
| :- | :- | :- |
| `CASH_ADVANCE_PROGRAM` | Cash advance programme | Cash Advance Program |
| `INVESTMENT` | Investment | Investment |
| `DAILY_SPENDING` | Daily spending | Daily Spending |
| `BILL_PAYMENT` | Bill payment | Bill Payment |
| `OTHERS` | Others | others |
### Source of funds (sourceOfFunds)
| code | Name | English label |
| :- | :- | :- |
| `OWN_BANK_ACCOUNT` | Own bank account | Own Bank Account |
| `THIRD_PARTY_BANK_ACCOUNT` | Third-party bank account | Third Party Bank Account |
| `CRYPTO_TOP_UP` | Crypto deposit | Crypto Top Up |
| `DUSD` | DUSD | DUSD |
### Source of wealth (sourceOfWealth)
| code | Name | English label |
| :- | :- | :- |
| `EMPLOYMENT` | Employment income | Employment |
| `SAVINGS` | Savings | Savings |
| `INHERITANCE` | Inheritance | Inheritance |
| `BUSINESS` | Business income | Business |
| `INVESTMENT` | Investment income | Investment |
| `TRUST_FUND` | Trust fund | Trust Fund |
| `CRYPTOCURRENCIES` | Cryptocurrencies | Cryptocurrencies |
## Next steps
* [Issuing Cards](./issuing-cards) β how these enum fields fit into `kycInfo` and the card issuing request body
* [Authentication guide](../../integration-resources/overview) β the common response envelope and how to judge errors
# Issuing Cards (Virtual and Physical)
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-cards/issuing-cards
How to issue a virtual card (two options: the H5 hosted guidance page or the OPEN-API) and a physical card (hosted guidance page only) for a user who has passed KYC, including POA file exchange, application status lookups, and what to do once you have the card.
## π Overview
Once a user has passed KYC, a single API call issues them a virtual card they can spend online immediately, and they can order a physical card afterwards if they want one. As a licensed issuer with its own BINs, DCS handles card issuing, KYC review, authorization decisions and settlement; all you deal with is how to submit the application, how to check its status, and what to do once the card exists.
DCS supports two card forms:
* **Virtual card**: no physical medium. It is activated automatically once the application is approved and can be spent online right away.
* **Physical card**: shipped to the user, and **must be activated and given a PIN** on arrival before it can be used.
> Virtual cards fall into two usage patterns: **single-use virtual cards** (void after one transaction, the most secure option, suited to one-off payments) and **reusable virtual cards** (usable repeatedly until the card expires, normally with per-transaction and cumulative limits). Neither can be swiped in a physical store or used at an ATM. Which pattern applies is determined by the card product (`categoryId`), so confirm it with DCS before you start issuing.
### Prerequisites
* The user has registered and has an account (see [Signing Up a Customer](../signing-up-a-customer/overview)).
* The KYC material is complete: the Sumsub `shareToken`, any required POA documents, and employment and source-of-funds information.
### Two ways to apply
The two options coexist; pick one:
| Option | Best for | Entry point |
| - | - | - |
| **Embedded H5 guidance page** | Reusing the ready-made DCS KYC and card issuing UI so you write less frontend code | `POST /redirect/v2/guidance-link` (see [H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance)) |
| **OPEN-API** | Running your own frontend and collecting KYC material yourself | `POST /card/v1/virtual-card/apply` (virtual cards only) |
**The DeCard-Managed model has no direct "order physical card" API**. Ordering, activating and setting the PIN on a physical card **all happen through the H5 hosted guidance page** (see the physical card section below). There is no `physical-card/apply` endpoint and no virtual-to-physical conversion endpoint.
### The application flow at a glance
The OPEN-API path has only two steps: submit the application, then poll until it reaches a terminal state (you can also receive the outcome by webhook):
> **If you need to submit a POA document with the application**, complete these three steps first:
>
> 1. Call `POST /account/v1/generate-file-upload-prepare` to obtain a pre-signed upload URL and an `objectKey`;
> 2. `PUT` the POA file straight to the URL you were given;
> 3. Put the `objectKey` into the POA fields of the application request (see the field table below).
***
## Applying for a virtual card
### Option 1: embedded H5 guidance page
Call `POST /redirect/v2/guidance-link` with `action` set to `KYC_GUIDE` to obtain a guidance page URL. Open it in your frontend and DCS walks the user through KYC and card issuing. For the parameters and rendering options, see [H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance).
#### Screenshots of the H5 application flow
The two sets of screenshots below show the full experience of applying for a virtual card through the H5 guidance page, so you can preview what the pages look like.
**KYC vendor verification** (the user completes each identity verification step inside the guidance page):
**POA collection during KYC** (the user fills in and submits proof of address and other supporting information inside the guidance page):
### Option 2: OPEN-API
Call `POST /card/v1/virtual-card/apply` to submit the application directly, KYC material included.
**Request fields**:
| Field | Type | Required | Description | Constraint |
| - | - | - | - | - |
| `externalUserId` | string | REQUIRED | User ID | External user ID |
| `categoryId` | integer (Long) | REQUIRED | Card category ID | |
| `applyRef` | string | REQUIRED | Idempotency key for the application | |
| `kycInfo.sumsubShareToken` | string | REQUIRED | Sumsub token | Max 500 |
| `kycInfo.email` | string | REQUIRED | Email address | Max 100 |
| `kycInfo.poaDocType` | string | POA cases | POA document type, as an enum (for example `BANK_STATEMENT`; a `UTILITY_BILL` must be **no older than 90 days, or 75 days in France**). For the full list, see [Card Application Data Dictionary](./application-data-dictionary#proof-of-address-document-type-poadoctype) | Max 50 |
| `kycInfo.poaDocUrlList` | string\[] | POA cases | List of POA file references (see [POA file exchange](#poa-file-exchange)) | |
| `kycInfo.poaDocDate` | string | POA cases | Date on the POA document, which must be no older than three months | Max 10 |
| `kycInfo.addressLine1` | string | Conditional | Address line 1 | Max 1024 |
| `kycInfo.addressLine2` | string | Optional | Address line 2 | Max 1024 |
| `kycInfo.state` | string | Optional | State or province | Max 128 |
| `kycInfo.city` | string | Conditional | City | Max 128 |
| `kycInfo.postalCode` | string | Conditional | Postal code | Max 50 |
| `kycInfo.country` | string | Conditional | Two-letter ISO country code (for example CN/US/SG) | Exactly 2 |
| `kycInfo.employmentStatus` | string | Conditional | Employment status | Max 50 |
| `kycInfo.employerName` | string | Conditional | Employer name; **required** when `employmentStatus = EMPLOYED` | Max 100 |
| `kycInfo.employmentJobIndustry` | string | Conditional | Industry | Max 50 |
| `kycInfo.occupation` | string | Conditional | Occupation | Max 50 |
| `kycInfo.jobSeniority` | string | Conditional | Job seniority | Max 50 |
| `kycInfo.purposeOfAccount` | string | Conditional | Purpose of the account | Max 100 |
| `kycInfo.sourceOfFunds` | string | Conditional | Source of funds | Max 100 |
| `kycInfo.sourceOfFundsCountry` | string | Conditional | Two-letter ISO country code for the source of funds | Exactly 2 |
| `kycInfo.sourceOfWealth` | string | Conditional | Source of wealth | Max 100 |
| `kycInfo.fullName` | string | Optional | Full name, used by enhanced KYC in the H5 flow | |
| `kycInfo.birthday` | string | Optional | Date of birth, used by enhanced KYC in the H5 flow | |
| `kycInfo.gender` | string | Optional | Gender, used by enhanced KYC in the H5 flow | |
| `kycInfo.residenceCountry` | string | Optional | Country of residence, used by enhanced KYC in the H5 flow | |
Treat the field lengths above as indicative; the validation errors returned by a live call are the safer reference. Do not carry over the length rules from any other solution.
**Request example** (placeholders, no real data):
```json theme={null}
{
"externalUserId": "",
"categoryId": 1001,
"applyRef": "",
"kycInfo": {
"sumsubShareToken": "",
"email": "user@example.com",
"poaDocType": "BANK_STATEMENT",
"poaDocUrlList": [""],
"poaDocDate": "",
"addressLine1": "",
"city": "",
"postalCode": "",
"country": "SG",
"employmentStatus": "EMPLOYED",
"employerName": "",
"employmentJobIndustry": "",
"occupation": "",
"jobSeniority": "",
"purposeOfAccount": "",
"sourceOfFunds": "",
"sourceOfFundsCountry": "SG",
"sourceOfWealth": ""
}
}
```
**Response fields** (`data`):
| Field | Type | Description |
| - | - | - |
| `applyId` | string | Application ID |
| `cardId` | string | Card ID, returned once the application succeeds |
| `categoryId` | integer | Card category ID |
| `applyRef` | string | Idempotency key, echoed back |
| `status` | string | Application status, one of `PENDING / SUCCEED / FAILED`; see [Application status](#application-status) |
| `errorCode` | string | Error code on failure |
| `errorReason` | string | Description of the failure |
| `needEddFile` | boolean | Whether EDD (enhanced due diligence) documents are still needed |
| `needExtraInfo` | boolean | Whether the user must supply more information; see [needExtraInfo](#needextrainfo) |
| `remark` | string | Remarks |
**Successful response example** (masked):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "SUCCESS",
"messageDetail": null,
"data": {
"applyId": "",
"cardId": "",
"categoryId": 1001,
"applyRef": "",
"status": "SUCCEED",
"needEddFile": false,
"needExtraInfo": false,
"remark": null
}
}
```
**Failure response example** (masked):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "SUCCESS",
"messageDetail": null,
"data": {
"applyId": "",
"status": "FAILED",
"errorCode": "",
"errorReason": "",
"needEddFile": false,
"needExtraInfo": false
}
}
```
Every response uses the envelope `{ code, message, messageDetail, data }` and there is no `success` boolean. `code = SYS_SUCCESS` only means the request was accepted; **the business outcome is in `data.status`**.
***
## POA file exchange
POA (proof of address) documents are exchanged through a **pre-signed upload**; you never post the file to the card issuing endpoint. Three steps:
1. Call `POST /account/v1/generate-file-upload-prepare` with the body `{ "fileNames": [""] }` to obtain an upload address (a pre-signed URL) and an `objectKey` for each file.
2. From your server, **`PUT`** the POA file to the pre-signed `url` you were given.
3. Pass the returned `objectKey` back as an element of `poaDocUrlList` on the card issuing endpoint (or on a later KYC top-up endpoint).
Each item in the returned `data` array carries `fileName` (the original file name), `url` (the pre-signed upload address) and `objectKey` (the server-side object key you send back).
**Upload example** (Java, placeholder pre-signed URL, no PII):
```java theme={null}
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.File;
import java.io.IOException;
class UploadPoa {
public static void main(String[] args) throws IOException {
String preUploadURL = "";
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPut httpPut = new HttpPut(preUploadURL);
File file = new File("");
httpPut.setEntity(new FileEntity(file));
try (CloseableHttpResponse response = httpClient.execute(httpPut)) {
if (response.getStatusLine().getStatusCode() == 200) {
System.out.println(EntityUtils.toString(response.getEntity()));
}
}
}
}
```
> The pre-signed URL accepts a `PUT` upload, so use `HttpPut`.
***
## Ordering a physical card (H5 guidance page)
Ordering, activating and setting the PIN on a physical card all run through `POST /redirect/v2/guidance-link`, which returns the URL of the matching H5 page for your frontend to open so the user can finish the job. **There is no direct API.** For the guidance page parameters, see [H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance).
| Operation | `action` | Description |
| - | - | - |
| Order a physical card | `CREATE_PHYSICAL_CARD` | The user fills in the shipping details and submits the order; once accepted, the physical card sits in the "inactive" state |
| Activate a physical card | `ACTIVE_PHYSICAL_CARD` | The user activates the card once it arrives |
| Set or update the PIN | `UPDATE_PIN` | A PIN must be set after activation before the card can be used |
**Ordering a physical card** (`action=CREATE_PHYSICAL_CARD`; the user fills in the shipping details inside the guidance page and submits):
**Activating a physical card** (`action=ACTIVE_PHYSICAL_CARD`; the user activates the card inside the guidance page once it arrives):
> The full instructions for setting and updating a PIN live in [Managing a Card's PIN](./managing-a-cards-pin) and are not repeated here.
> For shipping lookups and card status, see [Managing Cards Β· Overview](./overview) and [Physical Card Shipping](../../customer-success/physical-card-shipping).
***
## Application status
Virtual card application statuses (the `status` field on `/card/v1/apply-list` and on the application response):
| Status | Meaning | Terminal |
| - | - | - |
| `PENDING` | Application in progress | No |
| `SUCCEED` | The virtual card was created and activated | Yes |
| `FAILED` | Terminal state caused by a failure at any step | Yes |
In the DeCard-Managed model, a card application ends in either `SUCCEED` or `FAILED`.
### needExtraInfo
`needExtraInfo`, on the application response and on the application record, tells you whether the user still has to supply more information:
| Value | Meaning and what to do |
| - | - |
| `true` | The user has not submitted the extra material yet. Send them through `POST /redirect/v2/guidance-link` with `action = KYC_EXTRA_DOC` to upload it |
| `false` | The user has submitted everything. Even if `status` is still `PENDING`, simply wait for the review; **do not prompt them again** |
***
## Checking application status
After submitting an application, use these two endpoints to close the loop. Keep **`applyId` (the application)** and **`cardId` (the card)** apart.
| Endpoint | Purpose | Key parameters / return |
| - | - | - |
| `GET /card/v1/virtual-card/detail` | Read one application | Query: `externalUserId` / `applyId` / `applyRef`; returns the same structure as the application response, including `status`, `cardId` and `needExtraInfo` |
| `GET /card/v1/apply-list` | List all of a user's applications | Query: `externalUserId` (**required**); each item carries `applyId` / `cardId` / `categoryId` / `network` / `currency` / `status` / `errorCode` / `needExtraInfo` |
***
## Card statuses (for reference)
Cards have two sets of status values, defined in one place in [Managing Cards Β· Overview](./overview). They are listed here for convenience only; **the Overview page is authoritative across the documentation**:
* **`cardStatus`** (overall card status): `NORMAL` / `FROZEN` (unfreezable by the cardholder) / `BLOCKED` (not unfreezable, returned only by `/card/v3/detail`) / `CANCELLED`
* **`physicalCardStatus`** (physical card status): `UN_APPLY` / `INACTIVE` / `ACTIVE` / `REPLACE` / `FROZEN` / `CANCELLED`
***
## Once you have the card
1. When the application returns `status=SUCCEED`, `data.cardId` is ready to use; **a virtual card needs no activation and is usable the moment it is issued**.
2. Use `cardId` with `GET /card/v3/detail` to read the card status and the balances tied to it (see [Managing Cards Β· Overview](./overview)).
3. To show the full PAN or CVV, send the end user through the hosted guidance page (`action=CARD_INFO`); see [Viewing Encrypted Card Details](./viewing-encrypted-card-details).
4. If a physical card is wanted, run the H5 guidance pages in order: `CREATE_PHYSICAL_CARD β ACTIVE_PHYSICAL_CARD β UPDATE_PIN` (see the physical card section above).
## Next steps
* Values for the `kycInfo` enum fields (poaDocType, employmentStatus, occupation, sourceOfFunds and so on): see [Card Application Data Dictionary](./application-data-dictionary).
* Freezing and unfreezing, cancellation, card replacement and the card state machine: see [Managing Cards Β· Overview](./overview).
* Setting or updating the PIN: see [Managing a Card's PIN](./managing-a-cards-pin).
* Adding the card to Apple Wallet or Google Wallet: see [Push Provisioning](./push-provisioning).
* Deposits and spending authorization: see [User Balance](../managing-transactions/user-balance).
# Managing a Card's PIN
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-cards/managing-a-cards-pin
Why setting and updating a PIN in the DeCard-Managed model runs through a hosted guidance page rather than a direct API, plus how to obtain the PIN guidance link (action=UPDATE_PIN), the full sequence, and how to handle errors.
## π Guide
Cardholders need a PIN to withdraw cash at an ATM and to pay at some offline POS terminals. As a licensed issuer with its own BINs, DCS treats setting a PIN as a sensitive operation that has to happen inside a trusted page: in the DeCard-Managed model, **PINs are not set or updated through a direct API**. Instead you send the user to a **DCS-hosted H5 guidance page** where they set the PIN themselves, so PIN entry and encryption both happen inside the DCS-hosted page and never touch your backend.
> **Prerequisite**: you must already have issued the card and hold its `cardId`. If you are not yet sure how to apply for a card, read [Issuing Cards](./issuing-cards) first.
### Mental model: why the PIN goes through a guidance page instead of an API
The DeCard-Managed model groups PIN setting with the operations an end user must complete in person inside a trusted page, alongside KYC, viewing sensitive card details and activating a physical card. All of them run through the same **redirect guidance page (guidanceLink)**:
1. Your backend calls the guidance page endpoint with the operation type (`action` / `type`) and the target card identifier, and receives a single-use guidance link.
2. You redirect the user to that link.
3. The user enters and submits the new PIN inside the DCS-hosted H5 page, which validates and encrypts it.
4. When they are done, the page returns the user to your app at the redirect URL you supplied.
That way **neither your client nor your backend ever handles the PIN in plaintext**, which keeps your PCI and compliance burden low.
## Obtaining the PIN guidance link
The PIN page reuses the common guidance page endpoint, which identifies the card precisely with `cardId`.
| Endpoint | Card identifier field | Value that triggers the PIN flow |
| :- | :- | :- |
| `POST /redirect/v2/guidance-link` | `cardId` | `action = UPDATE_PIN` |
> The **full parameter table and redirect mechanics** for the guidance page endpoint are documented in one place, [H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance). This page lists only the fields that matter for the PIN flow, to avoid duplication.
### Request fields (PIN flow)
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `action` | string | Yes | Always `UPDATE_PIN` for the PIN flow |
| `externalUserId` | string | Yes | User ID, max length 50 |
| `cardId` | string | Yes | Target card ID, required for any action that operates on a specific card, `UPDATE_PIN` included |
| `successRedirectUrl` | string | Yes | Redirect URL used after the PIN is set, max length 300 |
| `errorRedirectUrl` | string | Yes | Redirect URL used if setting the PIN fails, max length 300 |
| `referer` | string | No | Referrer, used for security checks, max length 300 |
| `userAgent` | string | No | User agent, used for security checks, max length 300 |
| `language` | string | No | Language of the customer-facing `guidance-link`, lowercase hyphenated and case-sensitive: `zh` / `en` / `ko` / `ja` / `zh-Hant` / `th` / `vi`, max length 10 |
| `selectCardPageShow` | string | No | Whether to skip the card selection page when the user holds several cards: `0` = do not skip, `1` = skip, max length 1 |
| `theme` | string | No | Theme, for example `blue`, max length 10 |
| `mode` | string | No | Light or dark mode, for example `dark` / `light`, max length 10 |
> The required fields are `action` / `externalUserId` / `successRedirectUrl` / `errorRedirectUrl` / `cardId`. `theme` and `mode` are optional, but sending them every time is recommended, in line with the sibling page [Viewing Encrypted Card Details](./viewing-encrypted-card-details).
>
> Four further fields belong to the full guidance page contract but are not used by the PIN flow: `nationality` (nationality, honoured only by `KYC_GUIDE`), `selectNationalityShow` (nationality selection page, honoured only by `KYC_GUIDE`), `primaryColor` (primary colour, a hex value starting with `#`) and `applyId` (required when uploading extra documents). For the complete field table, see [H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance).
### Request example (V2, masked)
```bash theme={null}
curl -X POST "{{dicard-server}}/redirect/v2/guidance-link" \
-H "Content-Type: application/json" \
-d '{
"action": "UPDATE_PIN",
"externalUserId": "",
"cardId": "",
"successRedirectUrl": "https://your-app.example.com/pin/done",
"errorRedirectUrl": "https://your-app.example.com/pin/error",
"language": "en",
"theme": "blue",
"mode": "light"
}'
```
> **Authentication**: the example omits the authentication headers to keep the focus on the business fields. A real call must carry `X-DAPI-API-KEY` / `X-DAPI-SIGN` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE` (HMAC-SHA256 signature); for the rules, see the [Authentication Guide](../../integration-resources/overview).
Every example value is a placeholder. **Never** put a real `externalUserId`, `cardId`, API key or secret, or any cardholder personal data into a request or a log.
### Response
The guidance page endpoint returns the common envelope `{ code, message, messageDetail, data }`. On success `code = SYS_SUCCESS` and `data` is the guidance page link, as a string. `messageDetail` is a **message object** (its schema has seven sub-fields: `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`) used to carry structured messaging on failure; on success it is normally `null`.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "...",
"messageDetail": null,
"data": "https:///...?token="
}
```
> On failure, `messageDetail` may come back in this shape (every sub-field is a string):
> `{ "message": "...", "title": "...", "type": "...", "icon": "...", "action": "...", "linkTitle": "...", "linkUrl": "..." }`
Redirect the user to the link in `data` and they land on the DCS-hosted PIN page.
## The full PIN-setting sequence
### Screenshots of the H5 PIN page
Once the user follows the guidance link, they set the PIN inside the DCS-hosted page. The flow looks like this (`action=UPDATE_PIN`):
## Error handling
| Situation | What to do |
| :- | :- |
| `code` in the response is not `SYS_SUCCESS` | Follow the hint in `message` and check your parameters (`action`, `cardId`, and whether both redirect URLs are present and valid) |
| The user fails to set a PIN inside the page | The DCS guidance page redirects them to `errorRedirectUrl`; offer a retry entry point on that page, which can request a fresh guidance link |
| The guidance link has expired or has already been used | Guidance links are single-use; call the endpoint again to obtain a new one |
> Whether the PIN was set successfully is determined by which URL the guidance page redirects to, `successRedirectUrl` or `errorRedirectUrl`. If you need server-side asynchronous confirmation, see the card events in [Webhook + WebSocket notifications](../../integration-resources/webhook-websocket).
## About PIN rules
PIN complexity and length rules (permitted number of digits, whether simple sequences or repeated digits are rejected, and so on) are enforced inside the DCS-hosted guidance page and are transparent to the partner, so you do not need to reimplement the validation in your own system. If the user enters a PIN that breaks the rules, the page tells them straight away and asks for another.
## Next steps
* [Managing Cards Β· Overview](./overview) β freezing and unfreezing, card replacement, the state machine
* [H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance) β the complete guidance page parameters and redirect mechanics
* [Viewing Encrypted Card Details](./viewing-encrypted-card-details) β also delivered through a guidance page (`action = CARD_INFO`)
# Managing Cards Β· Overview
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-cards/overview
The landing page for the card management group: the two card forms (virtual and physical), the card state machine, and how capabilities such as freezing, unfreezing and reading card details are delivered along two different paths (REST endpoints and hosted guidance pages).
## π Guide
Once a user has passed KYC, you can issue them a card and manage it for the rest of its life: freeze it to stop fraudulent use, cancel it when it is lost, activate it once the physical card arrives, reset the PIN when the cardholder forgets it. As a licensed issuer with its own BINs, DCS keeps card state in sync with the card scheme and the issuer processor underneath, so you only deal with the business semantics: what state is this card in, and which operations does that state allow.
**Every card belongs to a user**, so make sure the user exists and has passed KYC before you issue a card (see [Managing Users](../managing-users/overview)).
This group covers card issuing, viewing sensitive card details, freezing and unfreezing, managing the PIN, ordering and activating a physical card, and adding a card to Apple Wallet or Google Wallet (Push Provisioning).
### Virtual cards vs physical cards
Cards come in two forms, and they differ in how they are delivered and managed:
* **Virtual card**: usable the moment it is issued, with no shipping and no activation step.
* **Physical card**: shipped to the cardholder and **must be activated on arrival** before it can be used; activation and shipping lookups are covered below.
### Two capability delivery paths (important)
In the DeCard-Managed model, card management is **not** exposed as a symmetrical set of standalone REST endpoints. It is delivered along two paths, so do not assume the "one endpoint per action" model you may know from other platforms:
| Capability | Delivered via | Entry point |
| :- | :- | :- |
| Freeze / unfreeze | **REST endpoint** | `POST /card/v2/block` |
| Read card details and status | **REST endpoint** | `GET /card/v3/detail` (v2 still works β see below) |
| View sensitive card details (PAN / CVV / expiry) | **Hosted guidance page** | `/redirect/v2/guidance-link` βΆ `action=CARD_INFO` |
| Order a physical card | **Hosted guidance page** | `/redirect/v2/guidance-link` βΆ `action=CREATE_PHYSICAL_CARD` |
| Activate a physical card | **Hosted guidance page** | `/redirect/v2/guidance-link` βΆ `action=ACTIVE_PHYSICAL_CARD` |
| Update the PIN | **Hosted guidance page** | `/redirect/v2/guidance-link` βΆ `action=UPDATE_PIN` |
> Sensitive operations β reading sensitive card details, ordering and activating a physical card, updating the PIN β all run through the hosted guidance page `/redirect/v2/guidance-link`, selected by the `action` enum. The user completes the operation inside a DCS-hosted page with SMS verification, and sensitive data never touches the partner's backend. The published `action` values for guidance-link are `KYC_GUIDE` / `CARD_INFO` / `CREATE_PHYSICAL_CARD` / `ACTIVE_PHYSICAL_CARD` / `UPDATE_PIN` / `TRAVEL_RULE` / `KYC_EXTRA_DOC`. The DeCard-Managed model has no standalone reset-pin, activate, convert-to-phy or invalidate REST endpoints.
## Mental model: the card state machine
DCS expresses a card's lifecycle stage through its status fields, and every management operation is ultimately a way of driving that state machine. Virtual and physical cards each have their own set of status values.
**Virtual card status (`cardStatus`)**
| Status | Meaning | Can the cardholder unfreeze it? |
| :- | :- | :- |
| **NORMAL** | The card is active and can be used normally | β |
| **FROZEN** | Frozen by the cardholder, or after three consecutive wrong CVV / PIN entries | **Yes** β call [Freezing and unfreezing](#freezing-and-unfreezing) (unfreezing needs an SMS or email code) |
| **BLOCKED** | Blocked on the issuer side: compliance, risk, sanctions screening, product retirement, unpaid account management fee, and similar | **No** β contact DCS |
| **CANCELLED** | The card has been canceled and is permanently unusable | β |
The **`CARD_STATUS` webhook event** **normalizes `BLOCKED` to `FROZEN`** for backward compatibility, so it does not let you tell "the cardholder can unfreeze this" apart from "this cannot be unfrozen". Use `GET /card/v3/detail` and the [`CARD` webhook event](../../integration-resources/webhook-websocket#3-9-card-card-status-and-block-reason) when you need that distinction.
### Card status reason (`statusReason`)
When a card is `FROZEN` or `BLOCKED`, `statusReason` tells you why, so you can explain it to the cardholder or decide what to do next. It is returned by `GET /card/v3/detail` and the `CARD` webhook event; `CARD_STATUS` does not return it.
| statusReason | Card status while not canceled | Meaning |
| :- | :- | :- |
| `NORMAL` | Any | No specific reason |
| `USER_FREEZE` | FROZEN | The cardholder froze the card |
| `AUTHENTICATION_FAILED` | FROZEN | Frozen after three consecutive wrong CVV / PIN entries |
| `USER_REQUESTED_CLOSURE` | BLOCKED | The cardholder has asked to close the card and the closure is still in progress, so the card is blocked pending closure. This is **not** `CANCELLED` β the card is still valid and still appears in the card list |
| `COMPLIANCE_REVIEW` | BLOCKED | Blocked for compliance review |
| `SECURITY_RESTRICTION` | BLOCKED | Blocked by risk and security controls |
| `VERIFICATION_OVERDUE` | BLOCKED | Verification overdue β the cardholder did not respond or supply the required documents in time |
| `ACCOUNT_TERMINATED` | BLOCKED | The account has been terminated |
| `ACCOUNT_FEE_INSUFFICIENT` | BLOCKED | Unpaid account management fee. Ask the user to top up in D-Vault; once settled **DCS lifts the block** and notifies you through the `CARD` event β the partner calls nothing |
| `OTHER` | BLOCKED | Some other reason |
The second column holds **while the card has not been canceled**. Once a card is closed in our core, `cardStatus` is always `CANCELLED` regardless of that column β `statusReason` then keeps the last reason recorded before closure. So **never infer `cardStatus` from `statusReason`**: seeing `USER_REQUESTED_CLOSURE` does not mean the card is `BLOCKED`; it may already be `CANCELLED`. **Always decide whether a card is usable, or can be unfrozen, from `cardStatus`.**
**Physical card status (`physicalCardStatus`)**
| Status | Meaning |
| :- | :- |
| **UN\_APPLY** | No physical card has been ordered yet |
| **INACTIVE** | A physical card has been ordered but not activated, so it cannot be used |
| **ACTIVE** | The card is active and can be used normally |
| **REPLACE** | The card is going through card replacement |
| **FROZEN** | The card is frozen and temporarily unusable |
| **CANCELLED** | The card has been canceled and can no longer be used |
> **How cardholders reach card replacement**: the published API surface has no card replacement `action` or REST endpoint; `REPLACE` appears only as one of the `physicalCardStatus` values. Contact the DCS team to confirm the entry point for card replacement.
## Operations at a glance
Each operation is summarised in one line below, with its entry point. The REST operations (freeze/unfreeze, read card details) are documented further down this page; the guidance-page operations (sensitive card details, physical card order and activation, PIN) each have their own page.
| Operation | Who performs it | Summary | Details |
| :- | :- | :- | :- |
| Read card details and status | Partner | Use `/card/v3/detail` to read card status and balances; leave `cardId` empty to list every card the user holds | [This page βΊ Retrieving card details](#retrieving-card-details) |
| Freeze / unfreeze | Partner | Use `/card/v2/block` and flip the `block` boolean to freeze against fraud or restore the card | [This page βΊ Freezing and unfreezing](#freezing-and-unfreezing) |
| View sensitive card details | End user | Guidance page with `action=CARD_INFO` shows the full PAN, CVV and expiry inside a DCS-hosted page | [Viewing Encrypted Card Details](./viewing-encrypted-card-details) |
| Order a physical card | End user | Guidance page with `action=CREATE_PHYSICAL_CARD`; the user fills in the shipping details and submits the order | [Issuing Cards](./issuing-cards) |
| Activate a physical card | End user | Guidance page with `action=ACTIVE_PHYSICAL_CARD`; the user activates the card once it arrives | [Issuing Cards](./issuing-cards) |
| Reset or update the PIN | End user | Guidance page with `action=UPDATE_PIN`; the user sets the PIN inside the hosted page and the plaintext never reaches the partner | [Managing a Card's PIN](./managing-a-cards-pin) |
| Add to a digital wallet | End user | Call the wallet binding API to push the card into Apple Wallet or Google Wallet | [Push Provisioning](./push-provisioning) |
### Retrieving card details
Read card details with `GET /card/v3/detail`. On this endpoint `cardId` is optional: **pass a value** to query one specific card, **leave it empty** to list the cards the user holds. The `data` field is always an array. The response carries the card status (`cardStatus` for the virtual card and `physicalCardStatus` for the physical card, with the values shown in the state machine above), `statusReason`, and the balances tied to the card (`walletBalance` / `caBalance` / `cardBalance` / `balanceCurrency` / `billingCurrency`).
Each card is tied to a user-level balance and authorization decisions are made inside DCS: that is the defining trait of the DeCard-Managed (dedicated-account) model. For balance management, see [User Balance](../managing-transactions/user-balance).
> This endpoint identifies a card precisely with `cardId`.
### Freezing and unfreezing
Freeze or unfreeze a card with `POST /card/v2/block`. It is a single endpoint, and the `block` boolean sets the direction: `block=true` freezes, `block=false` unfreezes. Freezing takes effect immediately, which is what you want when a card looks compromised or the user asks to pause it.
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `externalUserId` | string | Yes | User ID |
| `cardId` | string | Yes | Card ID |
| `block` | boolean | Yes | `true` = freeze; `false` = unfreeze |
| `smsCode` / `emailCode` | string | Required when unfreezing | Verification code for unfreezing; supply one of the two |
* **Freezing** (`block=true`) normally needs no verification code.
* **Unfreezing** (`block=false`) requires a verification code: supply **either** `smsCode` (SMS) **or** `emailCode` (email).
**Freeze a card (no verification code)**
```json theme={null}
{
"externalUserId": "user_xxxxxxxx",
"block": true,
"cardId": "card_xxxxxxxx"
}
```
**Unfreeze a card (verification code required)**
```json theme={null}
{
"externalUserId": "user_xxxxxxxx",
"block": false,
"cardId": "card_xxxxxxxx",
"smsCode": ""
}
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": true
}
```
`data` is a boolean indicating **whether this freeze or unfreeze operation succeeded**: `true` = success, `false` = failure. The envelope `{code, message, messageDetail, data}` is the same across the API, as described in [Quickstart](../../getting-started/quickstart).
> The `block` endpoint only moves a card between `NORMAL` β `FROZEN` (or `ACTIVE` β `FROZEN`).
A `BLOCKED` card is blocked on the issuer side and the partner **cannot** lift it with this endpoint β contact DCS. Because v1 and v2 normalize `BLOCKED` to `FROZEN`, their responses give you no way to spot these cards in advance; use `GET /card/v3/detail`.
**Three ways an unfreeze can fail**
A status that allows self-service unfreezing does not guarantee the next call succeeds. Handle each code separately:
| Error code | Meaning | What to do |
| :- | :- | :- |
| `UNFREEZE_DAILY_LIMIT_EXCEEDED` | The card has hit its daily limit of successful unfreezes | Tell the user to try again tomorrow. **Note this code has no `CARD_` prefix** β match the exact string |
| `CARD_UNFREEZE_UNSUPPORTED_L4` | Unpaid account management fee | Ask the user to top up in D-Vault; **do not retry the unfreeze** |
| `CARD_UNFREEZE_UNSUPPORTED` | The card is out of scope for self-service unfreezing (`cardStatus` is `BLOCKED`) | Direct the user to support |
## Next steps
* [Issuing Cards](./issuing-cards) β apply for a virtual or physical card for a user who has passed KYC. A virtual card is usable as soon as it is issued; a physical card is shipped and then activated.
* [Viewing Encrypted Card Details](./viewing-encrypted-card-details) β show the full PAN, CVV and expiry securely through a hosted guidance page (action=CARD\_INFO), without the data passing through the partner's backend.
* [Managing a Card's PIN](./managing-a-cards-pin) β let the user set or update the PIN inside a DCS-hosted page through a hosted guidance page (action=UPDATE\_PIN); the partner never handles the PIN, in plaintext or encrypted form.
* [Push Provisioning (Apple / Google Pay)](./push-provisioning) β add a DCS-issued Visa card to Apple Wallet or Google Wallet from inside your app, with no manual card entry in the wallet app.
# Push Provisioning (Adding Cards to Apple Pay / Google Pay)
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-cards/push-provisioning
How to let a cardholder add a DCS-issued Visa card to Apple Wallet or Google Wallet from inside the partner's app: the tokenization mental model, the commercial onboarding process, the runtime binding sequence, and the dedicated Apple and Google wallet binding REST endpoints.
## π Guide
Push provisioning lets a cardholder add a DCS-issued Visa card to **Apple Wallet (iOS)** or **Google Wallet (Android)** with **a single tap inside your app**, instead of typing the card number into the wallet app. As a licensed issuer with its own BINs, DCS is the party that integrates with Apple's and Google's tokenization and provisioning authorization.
Unlike the capabilities that can only redirect the user, the DeCard-Managed model **does expose its own wallet binding REST endpoints** (`/card/v1/apple-bind-wallet` and `/card/v*/google-bind-wallet`): your app forwards the cryptographic material the wallet gives it to DCS, DCS calls Visa tokenization, and DCS returns the provisioning payload the wallet needs, which your app passes back to the system wallet to finish the job.
> **Recommendation for new Apple Pay integrations**: use [DCSProvisioningSDK](../../sdk/apple-pay/installation). The SDK uses a temporary ticket from `POST /card/v1/bind-wallet-ticket` and encapsulates the later PassKit and DCS exchange. This page keeps the underlying wallet binding REST endpoints for field reference, existing direct integrations and Google Pay integrations.
> **Prerequisite**: the user must have passed KYC and been issued a card, and you must hold that card's identifier (the last four digits of the PAN `cardMantissa` for Apple and Google V1, or `cardId` for Google V2). For the issuing flow, see [Issuing Cards](./issuing-cards).
### Mental model: tokenization
Digital wallets protect a card through **tokenization**: the real card number (PAN) is replaced by a unique digital token.
* **One token per card per device**: every "card Γ device" pair gets its own token, and the tokens are independent of each other.
* **End-to-end protection**: the token stands in for the real PAN at transaction time, so the plaintext PAN never reaches the merchant or the wallet.
* **Token lifecycle** (five stages): created when the card is added β activated after verification β used in place of the real PAN at transaction time β rotated periodically for security β destroyed when the card is removed.
### Two provisioning paths
| Path | What it is | Needs the API on this page |
| :- | :- | :- |
| **Manual provisioning** | The cardholder types the card details into the Apple or Google Wallet app to add it | No. DCS supports this out of the box and no development is required (steps below) |
| **In-app provisioning** | The cardholder taps "add to wallet" **inside your app** and the app pushes the card into the wallet securely | **Yes.** You call the wallet binding endpoints on this page |
This page is about **in-app provisioning**, which requires integration work on your side and calls to the DCS wallet binding API. Manual provisioning needs no development at all (DCS supports it by default) and is done by the cardholder inside the wallet app; the steps are reproduced below so you can reuse them in your own help content.
#### Apple Pay manual provisioning (iPhone, 3 steps)
1. **Open the Wallet app**: open Wallet on the iPhone and tap "+" in the top right corner.
2. **Enter the card details**: hold the card near the iPhone to add it, or tap "Enter Card Details Manually" and follow the on-screen instructions.
3. **Verify identity**: choose a verification method when prompted (SMS or email) and enter the code to finish.
#### Google Pay manual provisioning (Android, 7 steps)
1. **Open the Google Wallet app**: the cardholder opens Google Wallet and **must be signed in to a Google account** for mobile payments to work.
2. **Start adding a card**: the cardholder taps "Add to Wallet" to begin manual provisioning.
3. **Choose the card type**: if prompted, the cardholder selects "Payment card".
4. **Enter the card details manually**: the cardholder chooses "Or enter details manually" and **types in the card number, expiry date and CVV**.
5. **Accept the terms**: the cardholder **reviews and accepts the terms and conditions**.
6. **Verify identity**: a **verification code** is sent by SMS or email for identity verification.
7. **Finish provisioning**: the cardholder enters the code and, on success, sees a confirmation in the Google Wallet app.
> The exact steps vary a little with the handset, the OS version and wallet app updates. Contact the DCS team if anything is unclear.
### The four roles
| Role | Who | Responsibility |
| :- | :- | :- |
| Partner app | You | Show the "add to Apple Pay / Google Pay" button, integrate the wallet SDK, forward the cryptographic material |
| Apple Pay / Google Pay API | Apple / Google | Perform tokenization and wallet provisioning |
| DCS wallet binding service | DCS | Call Visa tokenization and return the provisioning payload the wallet needs (the API on this page) |
| BIN Sponsor (Apple) / Issuer (Google) | DCS | Request provisioning authorization from Apple and Google as the issuer |
> On the Apple side DCS acts as the **BIN Sponsor** and the partner as the **Program Manager**; on the Google side DCS acts as the **Issuer** and the partner as the **Program Manager**.
***
## Commercial onboarding (one-off, before go-live)
Before cards can be provisioned into wallets in production, you, DCS and Apple or Google have to complete a one-off round of authorization and certification. Apple and Google run separate tracks:
### Apple Pay onboarding
| Step | What happens |
| :- | :- |
| β Confirm roles | Partner = Program Manager; DCS = BIN Sponsor |
| β‘ Partner Hub onboarding | DCS registers the partner as Program Manager in Partner Hub; the partner signs the NDA and the Program Manager agreement; DCS signs the addendum with Apple |
| β’ Technical integration | Manual provisioning is already supported by DCS. For a new Apple Pay integration, the partner integrates **DCSProvisioningSDK**, the backend obtains a temporary ticket, and the SDK calls DCS / Visa to provision the card. Existing direct integrations can keep using the endpoints on this page |
| β£ Lab certification | Every Program Manager must pass Apple's **Limited Labs Certification** in its own right (book ahead; allow roughly **2β3 weeks**) |
| β€ App release | Once lab certification has passed and every Partner Hub review step is complete, the partner submits the app to the store for review and release |
### Google Pay onboarding
| Step | What happens |
| :- | :- |
| β Confirm roles | DCS = Issuer; Partner = Program Manager |
| β‘ Google Wallet Console | Sign the NDA; the partner registers through the issuer documentation access form; complete and submit the **Push Provisioning API Access** request (company and app details, SDK access); make sure the app is registered in the Google Wallet Console |
| β’ Technical integration | Manual provisioning is already supported by DCS; for in-app provisioning the partner integrates the **Google Pay SDK** and calls the DCS wallet binding endpoints on this page to enable "Add to Google Pay" in the app |
| β£ UI/UX review | Submit the UX and branding review Google requires |
| β€ Field testing | Complete Google's field testing and meet its exit criteria before go-live |
| β₯ App release | The partner submits the Launch Approval Form with "Program Manager" selected, then submits the app to Google Play for review |
> The screen-by-screen detail on the wallet side is governed by Apple's and Google's own issuer documentation, which this page does not reproduce. Contact the DCS team for further integration detail.
***
## Runtime provisioning flow (in-app provisioning)
Once you are integrated, every tap on "add to wallet" in your app triggers the exchange shown below. Apple is used as the example; Google works the same way and differs only in the request and response fields:
> The diagram shows the underlying direct flow. With DCSProvisioningSDK, your app hands the ticket and card parameters to the SDK, which encapsulates the exchange of PassKit device artifacts with DCS; see the [Implementation Guide](../../sdk/apple-pay/implementation).
The Apple in-app provisioning cryptographic flow in nine steps: β the app starts provisioning β β‘ the wallet asks Apple for certificates and a nonce β β’ Apple returns them β β£ the wallet passes the certificates, nonce and nonce signature back to the app β β€ the app forwards them to the DCS wallet binding service β β₯ DCS generates an ephemeral key pair, encrypts the payment data with a shared key derived from Apple's public key, generates the encrypted OTP, and returns the encrypted data together with the ephemeral public key β β¦ the app sends the encrypted data, the ephemeral public key and the encrypted OTP to the wallet β β§ the wallet verifies the provisioning with Apple β β¨ Apple and the payment network operator (PNO) complete standard provisioning.
Apple Pay in-app provisioning cryptographic flow:
***
## Wallet binding API
The Apple binding endpoint is `POST /card/v1/apple-bind-wallet` (it identifies the card with the last four digits of the PAN, `cardMantissa`). For Google, DCS offers **V1 and V2**; **use V2 for every new Google integration** (it identifies the card precisely with `cardId`).
| Platform | Version | Endpoint | Card identifier field |
| :- | :- | :- | :- |
| Apple | V1 | `POST /card/v1/apple-bind-wallet` | `cardMantissa` |
| Google | **V2 (recommended)** | `POST /card/v2/google-bind-wallet` | `cardId` |
| Google | V1 | `POST /card/v1/google-bind-wallet` | `cardMantissa` |
> **The only difference between Google V2 and V1 is how the card is identified**: V2 uses `cardId` where V1 uses `cardMantissa`. `externalUserId` is required in both versions, and every other field is identical.
### Apple Pay wallet binding
#### Request fields
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `externalUserId` | string | Yes | User ID |
| `cardMantissa` | string | Optional | Last four digits of the PAN |
| `applePublicCertificates` | string\[] | Yes | List of Apple public key certificates, supplied by the wallet |
| `appleNonce` | string | Yes | Apple nonce |
| `appleNonceSignature` | string | Yes | Apple nonce signature |
> The required fields are `externalUserId` / `applePublicCertificates` / `appleNonce` / `appleNonceSignature`; `cardMantissa` is optional.
#### Request example (masked)
```bash theme={null}
curl -X POST "{{dicard-server}}/card/v1/apple-bind-wallet" \
-H "Content-Type: application/json" \
-d '{
"externalUserId": "",
"cardMantissa": "",
"applePublicCertificates": [""],
"appleNonce": "",
"appleNonceSignature": ""
}'
```
#### Response
A successful call returns the common envelope `{ code, message, messageDetail, data }` with `code = SYS_SUCCESS`. `data` holds the payload Apple Wallet needs to complete tokenization:
| `data` field | Type / spec | Description |
| :- | :- | :- |
| `encryptedPassData` | string (0β8192 characters, base64) | Encrypted authentication data, containing the encrypted PAN, expiry and timestamp |
| `activationData` | string (0β8192 characters, base64) | Activation data, containing the encrypted nonce, nonceSignature and authCode |
| `ephemeralPublicKey` | string (0β8192 characters, base64) | The `VISA.ECC.ePK` generated when building the MBPAD encrypted payload: an EC point on the P-256 curve, uncompressed |
| `vCardID` | string (42 characters) | Visa's unique card identifier, used to generate the provisioning payload |
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": {
"encryptedPassData": "",
"activationData": "",
"ephemeralPublicKey": "",
"vCardID": "<42-char-id>"
}
}
```
### Google Pay wallet binding
#### Request fields
| Field | Type | Required (V2 / V1) | Description |
| :- | :- | :- | :- |
| `externalUserId` | string | Yes / Yes | User ID |
| `cardId` | string | Yes / not present in V1 | Card ID (**V2 only**) |
| `cardMantissa` | string | not present in V2 / Optional | Last four digits of the PAN (**V1 only**) |
| `clientCustomerId` | string | Optional / Optional | The user's Google Wallet account ID (2β36 characters) |
| `deviceId` | string | Optional / Optional | The user's Android device ID, unique per device (2β24 characters) |
> Google V2 requires only `externalUserId` and `cardId`; `clientCustomerId` and `deviceId` are both optional. V1 requires only `externalUserId`.
#### Request example (V2, masked)
```bash theme={null}
curl -X POST "{{dicard-server}}/card/v2/google-bind-wallet" \
-H "Content-Type: application/json" \
-d '{
"externalUserId": "",
"cardId": "",
"clientCustomerId": "",
"deviceId": ""
}'
```
#### Response
On success `code = SYS_SUCCESS` and `data` holds what Google Pay needs for tokenization:
| `data` field | Type / spec | Description |
| :- | :- | :- |
| `last4` | string (4 characters) | Last four digits of the PAN |
| `opaquePaymentCard` | string | The opaque card data Google Pay tokenization requires |
| `vCardID` | string (42 characters) | Visa's unique card identifier |
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": {
"last4": "",
"opaquePaymentCard": "",
"vCardID": "<42-char-id>"
}
}
```
Every example value is a placeholder. **Never** put a real `externalUserId`, `cardId`, `cardMantissa`, cryptographic material, API key or secret, or any cardholder personal data into a request or a log.
**Authentication**: the examples omit the authentication headers to keep the focus on the business fields. A real call must carry `X-DAPI-API-KEY` / `X-DAPI-SIGN` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE` (HMAC-SHA256 signature); for the rules, see the [Authentication Guide](../../integration-resources/overview).
***
## Error handling
Before it can be provisioned, the card must be in a **normal, usable state**: activated, not frozen and not canceled. If the status is anything else, activate the card or lift the restriction first, then start provisioning.
| Situation | What to do |
| :- | :- |
| `code` in the response is not `SYS_SUCCESS` | Follow the hint in `message` and check your parameters: is `cardId` / `externalUserId` valid, is the card activated, is the cryptographic material complete |
| Tokenization fails on the wallet side | The risk engines built into Apple Wallet and Google Wallet can block provisioning (several attempts in quick succession, a flagged device, a mismatch between device region and card region, and so on). Ask the user to try again after **24β48 hours**; DCS cannot override a wallet's risk decision |
| Apple Pay is unavailable in the user's region | The wallet raises an iOS error, and DCS cannot work around Apple's regional restrictions |
## Testing notes
* Push provisioning **has to be tested with production cards**: Visa provides no sandbox tokenization cards, so the full provisioning flow cannot be verified in the sandbox.
* iOS builds must be installed through **TestFlight** for testing; running straight from Xcode makes provisioning fail.
> Today, provisioning success or failure does **not** generate an extra webhook event. Treat the response from this API and from Apple or Google Wallet as authoritative. Asynchronous provisioning events would require new platform capability.
## Next steps
* [Apple Pay SDK Installation](../../sdk/apple-pay/installation) β add DCSProvisioningSDK and configure the entitlement, App Group and card profile
* [Implementation Guide](../../sdk/apple-pay/implementation) β exchange the ticket and start in-app provisioning
* [Wallet Extensions Guide](../../sdk/apple-pay/wallet-extensions) β expose the provisioning entry point inside Apple Wallet
* [Issuing Cards](./issuing-cards) β issue the card first and obtain its `cardId` / `cardMantissa`
* [Managing Cards Β· Overview](./overview) β freezing and unfreezing, card replacement, the state machine
* [Managing a Card's PIN](./managing-a-cards-pin) β also card management, with the PIN handled through a guidance page
* [Webhook + WebSocket notifications](../../integration-resources/webhook-websocket) β other card and transaction events; provisioning events are not among them today
# Decline Reason Code Dictionary
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-cards/reject-reason-dictionary
To make it easier to determine why a card transaction was declined, this page lists every value the rejectReason field can take and what it means. Approved transactions return an empty string; declines return one of the codes below, and reasons DeCard does not recognize are mapped to OTHER_FAIL. The field appears in the CARD_TRANSACTION event on both Webhook and WebSocket.
## π Guide
> The values below apply to the `rejectReason` field of the `CARD_TRANSACTION` event and are populated only when `response=D`. The transaction query API does not return this field yet; contact DCS if you need it there.
### Decline reason codes (rejectReason)
For approved transactions (`response=A`) the field is an empty string `""`. Declined transactions return one of the reason codes below; reasons that DeCard does not recognize are mapped to `OTHER_FAIL`.
| Value | Meaning |
| :- | :- |
| `CARD_NOT_FOUND` | The card could not be found. |
| `CARD_NOT_ACTIVE` | The card is not active. |
| `CARD_EXPIRED` | The card has expired. |
| `CARD_RESTRICTED` | The card is restricted and cannot be used for this transaction. |
| `ACCOUNT_RESTRICTED` | The account is restricted and cannot be used for this transaction. |
| `AUTHENTICATION_FAILED` | The transaction authentication failed. |
| `FRAUD_OR_RISK_REJECTED` | The transaction was rejected by fraud or risk controls. |
| `LIMIT_EXCEEDED` | The transaction exceeds the applicable limit. |
| `MERCHANT_RESTRICTED` | The transaction is not allowed at this merchant. |
| `CURRENCY_NOT_SUPPORTED` | The transaction currency is not supported. |
| `INVALID_TRANSACTION` | The transaction information is invalid. |
| `DUPLICATE_TRANSACTION` | The transaction has already been processed or submitted. |
| `ORIGINAL_TRANSACTION_ERROR` | The original transaction could not be matched or is in an invalid state. |
| `TRANSACTION_NOT_ALLOWED` | The transaction is not allowed. |
| `ISSUER_OR_PARTNER_DECLINED` | The transaction was declined by the issuer or processing partner. |
| `PROCESSING_ERROR` | The transaction could not be processed due to a processing or system error. |
| `OTHER_FAIL` | The transaction was declined for a reason not recognized by DeCard. |
| `CONVERSION_INSUFFICIENT_BALANCE` | Insufficient balance |
| `CONVERSION_INVALID_REQUEST_AMOUNT` | Invalid conversion request amount |
| `USERASSET_INSUFFICIENT_BALANCE` | Insufficient balance |
| `PAY_INSUFFICIENT_BALANCE` | Insufficient balance |
| `PAY_EXCEED_PAY_AMOUNT` | Refunded or reversed amount exceeds the original transaction |
| `PAY_FULL_REFUNDED` | The original transaction has already been fully refunded |
| `PAY_NO_SUCH_ORDER` | The corresponding order could not be found |
| `PAY_UNKNOWN` | Unspecified payment processing error |
| `CARD_TRANSACTION_TRANSACTION_PERMISSION_DISABLED` | Card transactions are disabled |
| `CARD_TRANSACTION_UNSUPPORTED_CURRENCY` | The transaction currency is not supported |
| `CARD_TRANSACTION_UNREASONABLE_AMOUNT` | The transaction amount is not valid |
| `CARD_TRANSACTION_NO_SUCH_TRANSACTION` | The original transaction could not be found |
| `CARD_CARD_NOT_EXISTED` | The card could not be found |
| `SYS_ILLEGAL_PARAM` | Invalid request parameter |
| `HANDLE_USER_REWARD_HAS_ERROR` | Reward processing failed |
| `KYC_SYSTEM_ERROR` | KYC verification system error |
## Next steps
* [Webhook and WebSocket](../../integration-resources/webhook-websocket) β full field list and samples for the `CARD_TRANSACTION` event
* [Card Application Data Dictionary](./application-data-dictionary) β enum values for `kycInfo` on the card application endpoint
# Viewing Encrypted Card Details
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-cards/viewing-encrypted-card-details
How the one mechanism available in the DeCard-Managed model, the H5 hosted guidance page (action=CARD_INFO), securely shows the full PAN, CVV and expiry without the plaintext ever passing through the partner's backend: the three-step flow, the endpoint, and the messageDetail fields.
## π Guide
For security and compliance reasons (PCI DSS), a card's **full number (PAN), CVV and expiry date** count as highly sensitive data and are never returned in plaintext by the ordinary card lookup endpoints. Those endpoints return masked data only, such as `cardMantissa`, the last four digits of the PAN; the full number is shown exclusively through the H5 hosted guidance page described on this page. As a licensed issuer with its own BINs, DCS presents the sensitive data securely inside its own hosted page, which is what allows partners without PCI certification to show card details to their end users compliantly. To let an end user see the full card details, you must use the secure flow on this page.
### The one mechanism in the DeCard-Managed model: the H5 hosted guidance page
In the DeCard-Managed model, DCS **does not offer** an endpoint that returns encrypted card data to the partner's backend for the partner to decrypt. The **only supported mechanism** for showing full card details is the DCS-hosted H5 guidance page:
The partner calls the guidance page endpoint with `action=CARD_INFO` and DCS returns a single-use guidance link; the partner hands that link to the end user to open in their own browser, and **the full PAN, CVV and expiry are rendered to the end user by the DCS-hosted page, so the plaintext never passes through the partner's backend**.
Because the plaintext is only ever rendered by the DCS-hosted page and never lands in the partner's backend, partners without PCI certification can still let their end users see their card details safely.
**Security notes**
* Never store a card's full number, CVV or expiry date in the partner's backend or on any persistent medium.
* Only request the link when there is a real need, for example when the end user actively taps "show card number".
* The guidance link is a single-use, short-lived credential. Hand it to the end user promptly once you have it, and do not cache or forward it.
***
### The three-step flow
The secure display flow in the DeCard-Managed model comes down to three steps:
**Step 1: call the guidance page endpoint for a link** β the partner's backend calls `POST /redirect/v2/guidance-link` with `action=CARD_INFO` and the card identifier, and DCS returns a single-use hosted guidance page link in `data`.
**Step 2: hand the link to the end user** β the partner passes the link to the end user's frontend. **Do not open or cache the link in the partner's backend.**
**Step 3: the DCS-hosted page renders the card details** β the end user opens the link in a browser and the DCS-hosted H5 page renders the full PAN, CVV and expiry; the plaintext never passes through the partner's backend.
***
### Endpoint (V2 recommended, card located by `cardId`)
```http theme={null}
POST /redirect/v2/guidance-link
Content-Type: application/json
```
For the authentication headers and signature, see [Authentication Guide](../../integration-resources/overview).
#### Request parameters (card details flow, `action=CARD_INFO`)
| Field | Type | Required | Description |
| - | - | - | - |
| `action` | String | Yes | Always `CARD_INFO` for card details |
| `externalUserId` | String | Yes | User ID, max length 50 |
| `successRedirectUrl` | String | Yes | Redirect URL used on success, max length 300 |
| `errorRedirectUrl` | String | Yes | Redirect URL used on failure, max length 300 |
| `cardId` | String | Yes | Card ID; V2 locates the specific card by `cardId`, and it is required in the `CARD_INFO` flow |
| `referer` | String | No | Referrer, used for security checks, max length 300 |
| `userAgent` | String | No | User agent, used for security checks, max length 300 |
| `language` | String | No | Language of the customer-facing `guidance-link`, max length 10, lowercase hyphenated and case-sensitive: `zh` / `en` / `ko` / `ja` / `zh-Hant` / `th` / `vi` |
| `theme` | String | No | Theme, max length 10, for example `blue` (recommended) |
| `mode` | String | No | Display mode, max length 10, for example `dark` / `light` (recommended) |
| `selectCardPageShow` | String | No | When the user holds only one card: `0` = still show the card selection page, `1` = skip it and go straight to the card details page, max length 1 |
| `primaryColor` | String | No | Primary colour, max length 7, for example `#FFFFFF` (recommended) |
> The required fields are `action` / `externalUserId` / `successRedirectUrl` / `errorRedirectUrl` / `cardId`. `theme` and `mode` are optional, but sending them every time is recommended.
```json theme={null}
{
"action": "CARD_INFO",
"externalUserId": "",
"cardId": "",
"successRedirectUrl": "https://your-app.example.com/card/ok",
"errorRedirectUrl": "https://your-app.example.com/card/err",
"language": "zh",
"theme": "blue",
"mode": "light"
}
```
#### Response
Every response uses the common envelope `{ code, message, messageDetail, data }` and there is no `success` boolean; the envelope is explained once in [Authentication Guide](../../integration-resources/overview). On success `code = SYS_SUCCESS` and `data` is a string: the guidance page link for the end user to open. `messageDetail` is a **structured message object** (its schema has seven sub-fields: `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`) used to carry structured messaging; on an ordinary success every sub-field is an empty string, and the system fills them in as needed:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": {
"message": "",
"title": "",
"type": "",
"icon": "",
"action": "",
"linkTitle": "",
"linkUrl": ""
},
"data": "https://"
}
```
Hand that link (the `data` field, a plain URL string) to the end user to open in their frontend, and the DCS-hosted page renders the full PAN, CVV and expiry.
#### messageDetail sub-fields
`messageDetail` is a structured message object with seven sub-fields, used to carry business messaging or error information. On an ordinary success every sub-field is an empty string, and the system fills them in as needed:
| Sub-field | Type | Description |
| - | - | - |
| `type` | String | Message type: `INFO` / `WARN` / `ERROR` |
| `icon` | String | Icon identifier, which the frontend can map to an icon |
| `action` | String | Suggested action code, such as `RETRY` or `CONTACT_SUPPORT` |
| `message` | String | Body text of the message, written for the end user |
| `title` | String | Message title |
| `linkTitle` | String | Label of the actionable link |
| `linkUrl` | String | Target of the actionable link |
#### Common error response
On failure, `code` carries the **specific business error code** for that failure (there is no generic failure code). Below is a typical error response for a frozen card (only `NORMAL` cards support the lookup). Note that even when the request fails (`messageDetail.type=ERROR`) the HTTP status is still `200`, so decide success or failure from `code` rather than from the HTTP status:
```json theme={null}
{
"code": "CARD_CARD_FROZEN_STATUS_UNSUPPORTED",
"message": "card is frozen",
"messageDetail": {
"message": "The card is frozen, so its details cannot be viewed at the moment.",
"title": "Lookup failed",
"type": "ERROR",
"icon": "error",
"action": "RETRY",
"linkTitle": "",
"linkUrl": ""
},
"data": ""
}
```
> Which `messageDetail` sub-fields are populated in which situation, and whether they appear together, is ultimately determined by what the API actually returns.
> For the other `action` values on the guidance page (KYC guidance, ordering and activating a physical card, updating the PIN, Travel Rule, extra KYC documents and so on) and the fields each of them requires, see [H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance).
***
### Prerequisites
* The card has been issued and you hold its `cardId`; see [Issuing Cards](./issuing-cards).
* You have confirmed which `externalUserId` the card belongs to.
* You have confirmed the card's status: **only a card in `NORMAL` status can have its sensitive details queried and displayed**. Querying a `FROZEN` card returns the error code `CARD_CARD_FROZEN_STATUS_UNSUPPORTED`, and any other non-`NORMAL` status likewise cannot be queried. In addition, a LUMINARY card whose annual fee has not been paid returns the error code `CARD_ANNUAL_FEE_NOT_CHARGED`. The full state machines (virtual cards: `NORMAL` / `FROZEN` / `BLOCKED` / `CANCELLED`; physical cards have their own: `UN_APPLY` / `INACTIVE` / `ACTIVE` / `REPLACE` / `FROZEN` / `CANCELLED`) are described in [Managing Cards Β· Overview](./overview).
* When the details are shown through the hosted H5 page, DCS's page performs these status checks itself β the partner does not need to run them.
### Next steps
* For freezing and unfreezing a card, card replacement and resetting the PIN, see [Managing Cards Β· Overview](./overview).
* For the common guidance page fields and the other `action` flows, see [H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance).
> **On returning encrypted card data**: the DeCard-Managed model offers no endpoint that returns encrypted card data to the partner's backend for the partner to decrypt, and there is no client-side encryption or decryption flow. Sensitive card details are always shown through the H5 hosted guidance page described above.
# 3DS Strong Authentication
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-transactions/3ds-forwarding
How 3DS strong authentication works in the DeCard-Managed model: DCS closes the loop directly with the card network, 3DS challenges are not forwarded to the partner, and the partner has no 3DS integration work to do.
## DCS handles 3DS directly on the issuer side
In the DeCard-Managed model, 3DS strong authentication is handled directly between DCS and the card network: generating, delivering and verifying the code all happen on the issuer side, so you have no 3DS integration work at all. As a licensed issuer running its own BINs, DCS takes on the issuer's authentication duties and absorbs this extra strong-authentication step in the online payment path on your behalf.
This page explains how 3DS is handled in the DeCard-Managed model and what the partner does, or does not, need to do.
The DeCard-Managed model currently **does not forward 3DS challenges to the partner**; there is no capability for handing an OTP or challenge to the partner over a webhook for delivery. If your business genuinely requires the partner to deliver the 3DS code or to guide the user through authentication inside its own app, raise it with the DCS team: that capability already exists in the other integration model, Partner-Managed.
## What Three-Domain Secure (3DS) is
3DS (Three-Domain Secure, 3D Secure) is the security protocol used in online payments to verify the cardholder's identity, adding an extra verification step to the payment flow based on the risk of the transaction.
* Lower-risk transactions can be let through **frictionlessly**.
* Higher-risk transactions, such as large payments, or transactions where the merchant asks for it, trigger a **challenge**: the cardholder has to confirm their identity, typically with a one-time password (OTP), before the transaction can continue.
This extra step is how 3DS confirms cardholder identity and reduces fraud.
## How 3DS works in the DeCard-Managed model
In the **DeCard-Managed** model, authorization decisions are made **inside DCS** and act on the user's own balance (see [Authorizing Transactions](./authorizing-transactions)). Consistently with that, **3DS strong authentication is currently handled directly between DCS and the card network** and is not forwarded to the partner:
1. The cardholder pays with the card at a merchant.
2. The merchant sends a 3DS authentication request to the card network.
3. The card network (Visa, for example) routes the authentication request to DCS as the issuer.
4. DCS triggers the authentication step and delivers a one-time password (OTP) to the cardholder.
5. The cardholder enters the code in the authentication iFrame displayed by the merchant.
6. DCS returns the authentication result to the card network and the transaction continues.
> In this setup, **the partner has no 3DS integration work to do**: DCS generates, delivers and verifies the code on the issuer side, and **no 3DS challenge webhook is sent to the partner**.
## About 3DS forwarding (passing the code to the partner)
"3DS forwarding" means passing the OTP and challenge details **to the partner** over a webhook, so that the partner controls the last mile of delivery through its own channels, such as SMS, email or WhatsApp.
**The DeCard-Managed model does not currently offer this capability**, which means:
* This page **does not cover** the fields of a 3DS challenge webhook, the `flowsType` modes (`OTP_DELEGATE` / `OOB`) or a result callback endpoint. In the DeCard-Managed model, 3DS authentication is closed out on the issuer side and nothing is handed to the partner.
* If your business genuinely needs the partner to deliver the 3DS code or to guide the user through authentication inside its own app, raise it with the DCS team. That capability already exists in the other integration model, Partner-Managed.
## DeCard-Managed webhook events (and where 3DS fits)
For reference, these are the webhook event types the DeCard-Managed model currently sends. **None of them is a 3DS challenge event**:
| Event type `type` | Meaning |
| - | - |
| `BALANCE_CHANGE` | Digital asset or balance movement |
| `CARD_TRANSACTION` | Card transaction (authorization) record |
| `CARD_TRANSACTION_SETTLEMENT` | Card settlement record |
| `CARD_PHYSICAL_SHIPPING` | Physical card shipping information |
| `ORDER_STATUS` | Top-up (FOMO) order status |
| `QR_ORDER_STATUS` | QR Pay order status |
| `CARD_APPLY` | Card application status |
| `CARD_STATUS` | Card status change (three-value `cardStatus`) |
| `CARD` | Card status and block reason change (four-value `cardStatus` plus `statusReason`; Webhook only) |
> For how these events are delivered, how they are wrapped and how to verify their signatures, see [Webhook & WebSocket](../../integration-resources/webhook-websocket).
## Do you need 3DS forwarding?
**Whether 3DS forwarding is offered in the DeCard-Managed model**
If you need 3DS challenges (OTP or OOB) forwarded to you over a webhook so that you can deliver them through your own channels, **raise it with the DCS team**. The capability already exists in the other integration model, Partner-Managed, including both the `OTP_DELEGATE` and `OOB` modes and the result callback endpoint.
## Next steps
* How authorization is completed inside DCS: [Authorizing Transactions](./authorizing-transactions)
* The full authorization mechanism (sequences, authType, fields): [Authorization (handled inside DCS)](./authorizing-transactions)
* Receiving transaction and balance notifications: [Webhook & WebSocket](../../integration-resources/webhook-websocket)
# Authorization (handled inside DCS, no partner decision required)
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-transactions/authorizing-transactions
The how-to page for the authorization stage: how an authorization is completed inside DCS in the DeCard-Managed model, what you have to do, and how to verify it in the sandbox. For the mental model of the transaction flow (authorization then settlement, Outstanding, invariants), see the concept page Transaction Lifecycle Β· Overview.
## Authorization: DCS decides for you, in real time, inside the system
In the DeCard-Managed model, DCS makes every authorization decision for you in real time: it checks the card status, the available balance and the limits, then approves or declines on the spot, with no per-transaction response from you. As a licensed issuer running its own BINs, DCS connects directly to the card networks and holds the cardholder's own balance in custody, which takes the most latency-sensitive and reconciliation-sensitive question in the whole flow, whether to let a payment through at the moment the card is presented, off your integration list entirely.
Precisely because the decision happens inside DCS, the authorization decision needs no integration work on your side: there is no `auth_url`, no per-transaction response and no authorization decision webhook to register. This page explains how the mechanism works and the few things you actually need to do; for the mental model of the transaction flow (the two stages of authorization and settlement, the Outstanding bridge, the invariants), see the concept page [Transaction Lifecycle Β· Overview](../../basic-concepts/transaction-lifecycle).
## How DeCard-Managed differs from Partner-Managed
| Dimension | Partner-Managed | **DeCard-Managed (this documentation set)** |
| - | - | - |
| Who makes the authorization decision | The partner (you) approves or declines each transaction | **DCS, inside its own system** |
| Is an authorization decision webhook required | Yes (if you do not respond, the default settings apply) | **No, this integration item does not exist** |
| Where the balance or limit lives | Your reserve, in your own ledger | **Per-user balances held in custody by DCS** (`free` / `freeze`) |
| What you receive | A synchronous authorization request for you to decide on | A **notification** of the authorization result (`CARD_TRANSACTION` / `BALANCE_CHANGE` webhook) |
In the DeCard-Managed model, therefore, **do not** try to configure default authorization settings or register an authorization webhook to approve and decline: there is **no** authorization forwarding endpoint, decision callback or `auth_url` configuration here. If you genuinely need to own the authorization decision, that is the **Partner-Managed** model and is out of scope for this documentation set.
## How an authorization is handled in the DeCard-Managed model
The moment the cardholder swipes, taps or inserts the card, the merchant requests authorization from the card network and DCS decides internally:
1. **System checks** (any failure results in a decline):
* the card status must allow transactions (not `FROZEN`, not `BLOCKED`, not `CANCELLED`);
* the user's transaction status, since card transactions are blocked for that user when `forbidCardTransaction = true` in `GET /account/v1/user-status`;
* the user's **available balance** (`free`) must fully cover the authorization amount;
* card-level and user-level limits and risk rules.
2. **Funds are frozen** (no actual deduction takes place): on approval the corresponding amount moves from available to frozen, so `free` decreases, `freeze` increases and `total` is unchanged.
3. **Result notification**: DCS notifies the authorization result through the `CARD_TRANSACTION` webhook (`response` = `A` for approved, `D` for declined) and the balance change through the `BALANCE_CHANGE` webhook (`freeDelta` \< 0, `freezeDelta` > 0).
4. **Settlement follows**: once the merchant submits the final amount, settlement releases the hold and performs the actual deduction.
> Both steps that really change a user's balance, the freeze at authorization and the deduction at settlement, land on **that user's own account held in custody by DCS**. That is what fundamentally separates this model from Partner-Managed, where the reserve sits with the partner. For details, see [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system).
### The journey of one authorization
### The two notifications you receive (key fields)
Authorization results reach you as two kinds of webhook or WebSocket event. The fields below are the ones most used for reconciliation; the full detail lives in [Webhook & WebSocket](../../integration-resources/webhook-websocket) and on the concept page:
| Event `type` | Field | Meaning |
| - | - | - |
| `CARD_TRANSACTION` | `response` | Authorization result: `A` = approved / `D` = declined |
| `CARD_TRANSACTION` | `authType` | `EXPEND` (purchase) / `REFUND` (return) / `REVERSAL` (purchase reversal) |
| `CARD_TRANSACTION` | `amount` / `currency` | Authorization amount / currency |
| `CARD_TRANSACTION` | `cardId` / `externalUserId` | The card and user involved |
| `BALANCE_CHANGE` | `freeDelta` | Change in available balance (negative when an authorization is approved) |
| `BALANCE_CHANGE` | `freezeDelta` | Change in frozen balance (positive when an authorization is approved) |
| `BALANCE_CHANGE` | `free` / `freeze` | Absolute available and frozen balances after the change |
The full field tables for both webhooks are in the "Authorization webhook field definitions" section below; the step-by-step sequences for the three `authType` values are in "The three scenarios by authType".
## What you need to do
| Scenario | What you do |
| - | - |
| Authorization decision | **Nothing**, DCS decides internally and you never respond per transaction |
| Receiving authorization results | Configure a webhook receiver for `CARD_TRANSACTION` and use it to post to your books or notify the user. See [Webhook & WebSocket](../../integration-resources/webhook-websocket) |
| Keeping the card authorizable | Make sure the user account holds enough `free` balance (top it up) and that `forbidCardTransaction` is not set |
| Balance operations | Top up with `user-asset/v1/credit`, deduct with `user-asset/v1/debit` and read balances with `user-asset/v1/balance`. See [User Balance](./user-balance) |
> In other words, whether an authorization succeeds depends mainly on **whether the user's own balance is sufficient** and **whether card transactions are blocked for that user**, not on any response from you.
## Verifying an authorization in the sandbox (no card network needed)
Without connecting to a card network, you can trigger an authorization through the **sandbox simulation** endpoints and verify your webhook handling and balance logic:
**`POST /simulation/v2/fund-auth`**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | Yes | External user ID (redacted placeholder ``) |
| `cardId` | string | Yes | Card ID |
| `authType` | string | Yes | `EXPEND` (purchase) / `REFUND` (return) / `REVERSAL` (purchase reversal) |
| `amount` | number | Yes | Amount, as a decimal |
| `currency` | string | Yes | Transaction currency (USD, RMB, SGD, EUR, JPY and so on) |
These are **sandbox simulation** endpoints, **not production authorization decision endpoints**. The DeCard-Managed model has no production authorization API for a partner to decide on.
The response envelope is the site-wide `{code, message, messageDetail, data}`, with the success code literal `SYS_SUCCESS`:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "...",
"messageDetail": {
"message": "", "title": "", "type": "", "icon": "",
"action": "", "linkTitle": "", "linkUrl": ""
},
"data": {
"approved": true,
"errorCode": ""
}
}
```
* `data.approved` (boolean): whether the authorization was approved.
* `data.errorCode` (string): the error code when it was not.
* `messageDetail`: an optional display object whose fields are usually empty strings on success.
> For more `fund-auth` sandbox scenarios, see [Simulating Transactions](../simulating-transactions/overview).
## Authorization webhook field definitions
Authorization results and balance changes reach the partner through two webhooks. Authorization transaction fields always come from `CARD_TRANSACTION`; balance changes always come from `BALANCE_CHANGE`.
### CARD\_TRANSACTION (authorization transaction, all fields)
The fields are as follows:
| Field | Type | Description |
| - | - | - |
| `cardNumber` | string | Last 4 digits of the card number |
| `transactionCurrencyCode` | string | ISO code of the transaction currency (for example `"840"` or `"702"`) |
| `transactionAmount` | BigDecimal | Transaction amount |
| `localTransactionDate` | string | Transaction date (for example `"1211"`) |
| `localTransactionTime` | string | Transaction time (for example `"161420"`) |
| `response` | string | Transaction result: `A` (accept/success) / `D` (deny/fail) |
| `externalTranId` | Long | External transaction ID |
| `systemTraceAuditNumber` | string | System trace audit number |
| `requestAmountInUsd` | string | Transaction amount in USD |
| `mcc` | string | Merchant Category Code |
| `cardAcceptorNameLocation` | string | Merchant name and location |
| `direction` | string | Direction of funds: `DEBIT` (deduction, a purchase) / `CREDIT` (credit, a refund) |
| `settlementCurrencyCode` | string | Posting currency of the card account (ISO 4217, for example `"840"`) |
| `settlementAmount` | BigDecimal | Posting amount on the card account |
| `transactionType` | string | Transaction type: `R` card purchase / `C` ATM withdrawal / `Q` enquiry / `P` transfer or refund |
| `merchantCountryCode` | string | Merchant country code (3 digits) |
| `originalExternalTranId` | string | External transaction ID of the original transaction, **returned only for reversing transactions** (void, reversal, refund and so on); ordinary authorizations do not carry this field |
> Direction is expressed by `direction` (DEBIT/CREDIT); the nature of the transaction is expressed by `transactionType` (R/C/Q/P).
### BALANCE\_CHANGE (balance change, all fields)
An approved authorization freezes funds: the money never leaves the account, it only moves between the **available** and **frozen** columns, and the real deduction happens later at settlement (see [Settlement](./settlement)). That movement is notified through `BALANCE_CHANGE`:
| Field | Type | Description |
| - | - | - |
| `tranId` | Long | Ledger entry number |
| `externalTranId` | string | External transaction ID |
| `asset` | string | Asset currency |
| `network` | string | Network (empty for fiat and internal movements) |
| `freeDelta` | BigDecimal | Change in the available position (**negative** when an authorization freezes funds) |
| `freezeDelta` | BigDecimal | Change in the frozen position (**positive** when an authorization freezes funds) |
| `free` | BigDecimal | Available position after the change |
| `freeze` | BigDecimal | Frozen position after the change |
| `type` | string | Movement type (such as `CONVERSION`) |
An example (redacted; the semantics are a hold of 2.92, available -2.92 and frozen +2.92):
```json theme={null}
{
"webhookId": "",
"type": "BALANCE_CHANGE",
"externalUserId": "",
"notificationTimestamp": 1767777763336,
"eventTimestamp": 1767777641000,
"data": {
"asset": "USD",
"network": "",
"freeDelta": -2.92,
"freezeDelta": 2.92,
"tranId": 4862356405699379969,
"externalTranId": "4862356404793410306",
"free": 0,
"freeze": 18.1,
"type": "CONVERSION"
}
}
```
> For the overall model behind available and frozen balances (`free` / `freeze` / `total`) and the custody strategies, see [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system).
## Authorization types (authType)
`authType` states the nature of an authorization. The DeCard-Managed model uses only the following three:
| Enum value | Name | Description |
| - | - | - |
| `EXPEND` | Purchase | The cardholder pays, and the corresponding amount is frozen |
| `REFUND` | Return | A credit for a return (the final credit is applied at settlement) |
| `REVERSAL` | Purchase reversal | A reversal (void) of the original purchase authorization |
## The three scenarios by authType
The three `authType` values differ in how they affect the account and which webhooks you receive.
### EXPEND, a purchase
* Approved: the available balance decreases and the frozen balance increases, with `response=A` and `direction=DEBIT`.
* Declined: insufficient balance, a card that cannot transact, `forbidCardTransaction=true` and similar cases give `response=D` and create no hold.
### REFUND, a return
* A return credits the card, so `direction=CREDIT` and `transactionType=P`, and `originalExternalTranId` is returned to link back to the original transaction.
* The credited amount only reaches the user's available balance once **settlement** completes (see [Settlement](./settlement)).
### REVERSAL, a purchase reversal
* A reversal of the original purchase authorization returns `direction=CREDIT` and `originalExternalTranId`, and releases the amount frozen by the earlier `EXPEND` (frozen back to available).
* The exact values of `direction` and `transactionType` are whatever the `CARD_TRANSACTION` webhook actually returns.
## About 3DS
3DS strong authentication is part of the authorization path and is completed by DCS on the issuing side; see [3DS strong authentication](./3ds-forwarding).
## Next steps
* Mental model of the transaction flow (the two stages, the Outstanding bridge, the invariants): [Transaction Lifecycle Β· Overview](../../basic-concepts/transaction-lifecycle)
* Account and asset model (`free` / `freeze` / `total`): [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system)
* Reading and adjusting user balances: [User Balance](./user-balance)
* How funds are actually posted after authorization: [Settlement](./settlement)
* Receiving authorization result notifications: [Webhook & WebSocket](../../integration-resources/webhook-websocket)
* Simulating an authorization in the sandbox: [Simulating Transactions](../simulating-transactions/overview)
# Managing Transactions Β· Overview
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-transactions/overview
The starting point for retrieving end-user transaction data in the DeCard-Managed model: which endpoint serves each of the four views (card spending, statements, fiat transaction records, transaction ID resolution), and how to query and paginate them.
## One place to query, three views
Every card purchase, every statement and every fiat movement belonging to an end user can be retrieved from DCS in one place and shown to the user as it is. As a licensed issuer running its own BINs, DCS keeps the whole "crypto β fiat β card spending" chain inside its own systems in the managed model, so you consume the three views below instead of stitching raw card network and ledger records together yourself.
The DeCard-Managed model organizes this data into three views β statement summaries, fiat transaction records and transaction-ID resolution β each served by its own endpoint. Start with the overview:
| View | Question it answers | Endpoint | Lookup key |
| - | - | - | - |
| **Statement** | How much was spent and repaid in a given billing period? | `GET /card/v2/statements` | `cardId` |
| **Statement detail** | Which line items make up this statement? | `GET /card/v2/statements/detail` | `cardId` |
| **Fiat transaction records** | What are all of this user's credits, debits, repayments and top-ups, including the on-chain deposit hash? | `GET /card/v1/fiat/transactions` | `externalUserId` |
| **Transaction ID resolution** | Is this ID outstanding or posted, and which record does it link to? | `POST /card/v1/transaction/id/resolve` | `ids[]` |
> **Scope**: this page covers only the three views of **statements, fiat records and transaction-ID resolution**. An end user's **crypto funding history** (`user-asset/v1/transactions`, `transaction-detail`) belongs to the User Balance page and is not repeated here; see [User Balance](./user-balance) when you need it.
## Prerequisites
* Your Enterprise account is live and you hold an `ApiKey` and `SecretKey`. If you do not have them yet, see [First Steps](../../getting-started/first-steps) and the [Authentication Guide](../../integration-resources/overview).
* The end user is registered (you hold an `externalUserId`, see [Signing Up a Customer](../signing-up-a-customer/overview)) and holds a card (you hold a `cardId`, see [Issuing Cards](../managing-cards/issuing-cards)).
* Every request must carry the signature headers described in the Authentication Guide. The examples below omit those headers and show only the business fields.
* **The `externalUserId`, `cardId`, last 4 digits, merchant names, addresses and `txHash` values in all examples are placeholders or redacted. Never put real end-user PII in requests or logs.**
## 1. Querying statements
A statement rolls up the spending, repayments and not-yet-posted amounts of one billing period into a single record. The statement endpoints identify the card precisely with `cardId`, and the response carries both the SGD and USD amount fields.
### 1.1 Statement list
```
GET /card/v2/statements?externalUserId=usr_xxxxxxxx&cardId=card_xxxxxxxx&page=1&rows=20
```
| Parameter | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | Yes | User ID |
| `cardId` | string | Yes | Card ID |
| `page` / `rows` | string | No | Pagination: page number / rows per page |
| `startTime` / `endTime` | string | No | Time range |
A successful response returns an array of statements in `data`:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": [
{
"type": "POSTED",
"statementId": "stmt_xxxxxxxx",
"statementDateStart": "2026-06-01",
"statementDateEnd": "2026-06-30",
"cardNumberLast4": "0000",
"cardOrganization": "VISA",
"cardScheme": "Visa Platinum",
"cardOrganizationLogo": "https://example.com/logo.png",
"debitAmountExcludePayment": 100.00,
"paymentAmountInSgd": 0.00,
"paymentAmountInUsd": 100.00,
"nonPostedAmountInSgd": 0.00,
"nonPostedAmountInUsd": 0.00
}
]
}
```
| Field | Description |
| - | - |
| `type` | Statement type: `POSTED` (posted) / `NOT_POSTED` (not yet posted) |
| `statementId` | Statement ID, used to drill down into the detail |
| `statementDateStart` / `statementDateEnd` | Start and end date of the billing period |
| `cardNumberLast4` | Last 4 digits of the card number |
| `cardOrganization` / `cardScheme` / `cardOrganizationLogo` | Card network / card scheme / card network logo |
| `debitAmountExcludePayment` | Actual spending on the statement, with repayments and fees already deducted |
| `paymentAmountInSgd` / `paymentAmountInUsd` | Repayment amount in SGD / USD |
| `nonPostedAmountInSgd` / `nonPostedAmountInUsd` | Not-yet-posted amount in SGD / USD |
### 1.2 Statement detail
Once you have a `statementId`, drill down into the individual line items of that statement.
```
GET /card/v2/statements/detail?externalUserId=usr_xxxxxxxx&cardId=card_xxxxxxxx&statementId=stmt_xxxxxxxx&page=1&rows=20
```
| Parameter | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | Yes | User ID |
| `cardId` | string | Yes | Card ID |
| `statementId` | string | Yes | Statement ID |
| `page` / `rows` / `order` / `sort` | string | No | Pagination and sorting |
**Statement detail response** (redacted excerpt):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": [
{
"merchantName": "EXAMPLE COFFEE",
"postIndicator": 1,
"transactionDateTime": "2026-06-15 10:00:00",
"transactionAmount": 5.50,
"transactionCurrency": "SGD",
"postingAmountInSgd": 5.50,
"postingAmountInUsd": 4.05,
"debitCreditIndcator": "D",
"transactionType": "SPEND",
"mcc": "5814",
"merchantCountryCode": "702",
"transactionDescription": "EXAMPLE COFFEE SG",
"postedTransactionId": "posted_xxxxxxxx",
"assetMovements": [
{
"id": "asset_mvmnt_xxxxxxxx",
"asset": "USDC",
"amount": -4.05,
"movementTime": "2026-06-15 10:00:00",
"movementType": "DEBIT"
}
],
"assetMovementStatus": 1
}
]
}
```
**Key fields** (the API Reference remains authoritative for the complete field set):
| Field | Type | Description |
| - | - | - |
| `merchantName` | string | Merchant name |
| `postIndicator` | integer | Posting state: `1` = posted, `0` = not yet posted |
| `transactionDateTime` / `transactionDateTimeStr` | string | Transaction time / raw string (`yyyy-MM-dd HH:mm:ss`) |
| `transactionAmount` / `transactionCurrency` | number / string | Transaction amount / transaction currency |
| `postingAmountInSgd` / `postingAmountInUsd` | number | Posted amount in SGD / USD (dual currency) |
| `debitCreditIndcator` | string | Debit or credit direction (the field name keeps a historical typo; the intended name is debitCreditIndicator): `C` = credit (money in), `D` = debit (money out) |
| `transactionType` | string | `SPEND` (purchase) / `REFUND` / `PARTIAL_REFUND`. Note: the same-named webhook field uses a separate single-letter coding (R/C/Q/P) β do not mix the two |
| `mcc` | string | Merchant Category Code |
| `merchantCountryCode` | string | Merchant country code (3 digits, for example `702` for Singapore) |
| `postedTransactionId` | string | Posted transaction ID, usable for cross-checking with `transaction/id/resolve` |
| `assetMovements[]` | array | **DeCard-Managed only**: the user's asset movements. Each entry carries `id` (unique identifier), `asset` (asset type, such as USDC), `amount` (negative for a debit, positive for a refund), `movementTime` (time of the movement) and `movementType` (such as DEBIT or CREDIT) |
| `assetMovementStatus` | integer | Fund processing state: `1` = processing complete, `0` = incomplete |
> **`assetMovements` (DeCard-Managed only)**: in the DeCard-Managed model, every card purchase draws down the user's own wallet balance. `assetMovements` records **which asset funded it, how much was deducted and when**, which is a capability specific to the DeCard-Managed model.
## 2. Querying fiat transaction records (DeCard-Managed only)
Fiat transaction records are a view specific to the DeCard-Managed model: they aggregate every transaction, **credits, debits, repayments, disputes and stablecoin top-ups**, into one list retrieved by user, and they expose forensic fields for **on-chain deposits** (hash, network, asset, sending and receiving addresses). This is what end-to-end visibility of the "crypto β fiat β card spending" chain looks like in the DeCard-Managed model.
```
GET /card/v1/fiat/transactions?externalUserId=usr_xxxxxxxx&page=1&rows=20&order=time&sort=desc
```
| Parameter | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | Yes | User ID |
| `page` / `rows` | string | No | Pagination |
| `order` | string | No | Sort field |
| `sort` | string | No | Sort direction: `asc` / `desc` |
A successful response (redacted excerpt):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": [
{
"debitCreditIndcator": "C",
"postingTransType": "CTU01",
"transactionAmount": "100.00",
"transactionCurrency": "USD",
"merchantName": "",
"transactionDateTime": "2026-06-15 10:00:00",
"postedTransactionId": "posted_xxxxxxxx",
"transferDetails": {
"channelCode": "fomo",
"txnAmt": 100.00,
"txnCcy": "USDC",
"sender": "0xSENDER_ADDRESS_REDACTED",
"receiving": "0xRECEIVING_ADDRESS_REDACTED",
"timeStamp": "1718438400",
"txHash": "0xTXHASH_REDACTED",
"network": "Polygon",
"asset": "USDC"
}
}
]
}
```
**Debit or credit direction, `debitCreditIndcator`** (the field name keeps a historical typo; the intended name is `debitCreditIndicator`): `C` = credit (money in) / `D` = debit (money out).
**Transaction type, `postingTransType`** (complete enumeration):
| Value | Meaning | Value | Meaning |
| - | - | - | - |
| `1` | Purchase | `9` | Dispute release |
| `2` | Cash | `M` | Memo transaction |
| `5` | Purchase fee | `A` | Purchase interest |
| `6` | Cash fee | `B` | Cash interest |
| `7` | Repayment | `N` | Annual fee |
| `8` | Dispute registration | `P` | Physical card fee |
| `R` | Card replacement fee | `CTU01` | Stablecoin top-up |
**Top-up information, `transferDetails`** (returned only for top-up transactions):
| Field | Description |
| - | - |
| `channelCode` | Top-up channel: `fomo` (FOMO channel) / `icn` (ICN channel) |
| `txnAmt` / `txnCcy` | Top-up amount / top-up currency |
| `sender` / `receiving` | Sender / recipient (an on-chain address for `fomo`, a name for `icn`) |
| `timeStamp` | Transaction timestamp |
| `txHash` | Blockchain transaction hash (**`fomo` channel only**) |
| `network` | Blockchain network (**`fomo` channel only**, such as Polygon, Base or Solana) |
| `asset` | Asset currency (**`fomo` channel only**) |
> Deposit addresses and the currency and network configuration behind on-chain top-ups belong to the crypto deposit and withdrawal group and are not covered here. `txHash` lets you confirm receipt on a block explorer, which makes it an important anchor for reconciliation in the DeCard-Managed model.
## 3. Resolving transaction IDs
When you hold a transaction ID but are unsure whether it is **outstanding** or **posted**, resolve its posting state and linked IDs in bulk.
```
POST /card/v1/transaction/id/resolve
```
```json theme={null}
{
"ids": ["txn_xxxxxxxx", "txn_yyyyyyyy"]
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `ids` | string\[] | Yes | List of transaction IDs (each may be an `outstandingTransactionId` or a `postedTransactionId`) |
The key fields of a successful response, where `data` is the list of resolution results:
| Field | Description |
| - | - |
| `id` | The original ID you passed in |
| `outstandingTransactionId` | Outstanding transaction ID; `null` when the transaction posted directly with dual messaging and has no outstanding record |
| `postedTransactionId` | Posted transaction ID; `null` while the transaction is still outstanding |
| `posted` | Boolean, whether the transaction has been posted |
> Why you need this: the same purchase can carry different IDs at the hold stage (outstanding) and at the posting stage (posted), and this endpoint ties the two ends together. For the concepts behind authorization and posting, see [Transaction Lifecycle Β· Overview](../../basic-concepts/transaction-lifecycle).
## Pagination, sorting and error handling
* **Pagination**: list endpoints all take `page` (page number) plus `rows` (items per page) in the query string, both as strings. Follow the field table of each endpoint on this page.
* **Sorting**: the endpoints that support `order` (sort field) plus `sort` (`asc` or `desc`) are statements, statements/detail and fiat/transactions.
* **Success check**: treat `code == SYS_SUCCESS` in the response envelope as the request being accepted; for any other value, read `message` and `messageDetail` for the reason. The envelope is documented in full in the [Authentication Guide](../../integration-resources/overview).
* **Time range**: when `startTime` and `endTime` are omitted, each endpoint applies its own default range. Pass them explicitly to keep response sizes under control.
## Capability notes
The DeCard-Managed model does not currently offer transaction memos or receipts. If you need to show memos or receipts in your own interface, store them on your side; adding the capability on the DCS side is a product decision, so raise it with the DCS team if you need it.
DCS does provide two views, **statement summaries** and **fiat transaction records including on-chain deposit forensics**, both covered in sections 1 and 2 above.
## Next steps
* To read a user's asset balances and crypto funding history, see [User Balance](./user-balance).
* To understand how a transaction travels from authorization to posting, see [Transaction Lifecycle Β· Overview](../../basic-concepts/transaction-lifecycle).
* Authorization decisions are made inside DCS and are never forwarded to you (forwarding belongs to the Partner-Managed model); see [Authorizing Transactions](./authorizing-transactions).
# QR Pay
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-transactions/qr-pay
QR Pay is a spending capability specific to the DeCard-Managed model: the three-step flow of decode, create and confirm, plus the order-list and order-detail queries, with the debit landing on the user's DeCard-Managed account balance.
> **Scope**: QR Pay is a spending capability **available only in the DeCard-Managed model**. This page belongs to the Managing Transactions group.
## Scan, pay, and the same balance is debited
QR Pay lets a cardholder pay by **scanning a code** instead of swiping or tapping a physical card, at any merchant that accepts QR payments. Just as with a card authorization, **the debit ultimately lands on the user's DeCard-Managed account balance**: the available balance falls and the total falls with it (referred to below by the internal DCS shorthand `free` and `total`; at API level `free` maps to `availableBalance`, while the held portion is `frozenBalance`, see [Account and Asset Model](../../basic-concepts/ledgering-system)). It is the same balance a card payment draws on. The only difference is how the payment starts: QR Pay does not travel through a card authorization message on the card network. Instead the DCS acquiring side decodes the code and creates the order, and you then guide the cardholder to confirm the payment.
QR Pay consists of 6 endpoints, all under the `/qrpay/v1/` prefix: the main payment flow of **decode, create and confirm**, **order-list and order-detail** for queries, and **submit-extra-info** for the supplementary identity documents some payments require.
## Prerequisites
* The user is registered in the DeCard-Managed model, has passed KYC and holds an `externalUserId` (see [Signing Up a Customer](../signing-up-a-customer/overview)).
* The user holds a card and their **DeCard-Managed account holds enough available balance (`free`)** (for top-ups, see [User Balance](./user-balance)).
* Callers must send the site-wide authentication headers (see the [Authentication Guide](../../integration-resources/overview)); the examples below omit those headers and show only the business payload.
The site-wide response envelope is `CommonRet`: `{ code, message, messageDetail{message,title,type,icon,action,linkTitle,linkUrl}, data }`, with **no `success` boolean**; on success, `code = SYS_SUCCESS`. Every sample response below uses this envelope.
***
## The payment flow
`decode` only decodes and previews, and **debits nothing**; the user's balance is only held or debited at the `confirm` step. Between `create` and `confirm` the order is valid until its `expiryTime` (a millisecond timestamp), after which you have to start again.
***
## 1. Decode the QR code
**`POST /qrpay/v1/decode`** decodes the QR code and returns an order preview (amount, merchant, exchange rate, fee, limits) for your app to show the cardholder for confirmation.
### Request
| Field | Type | Required | Description | Constraint |
| - | - | - | - | - |
| `externalUserId` | string | Yes | External user ID | Max 64 characters |
| `qrCodeValue` | string | Yes | Scanned value (the raw QR code or payment code string) | Max 512 characters |
| `clientIP` | string | Yes | Client request IP | Max 50 characters |
```json theme={null}
{
"externalUserId": "",
"qrCodeValue": "",
"clientIP": ""
}
```
### Response (`data`)
| Field | Type | Description | Constraint |
| - | - | - | - |
| `needExtraInfo` | boolean | Whether the user must supply further documents before the payment can go ahead. When `true`, **every other field is empty** and no payable order is created; see "2. Submit supplementary information" below | |
| `needCashier` | boolean | Whether the cashier page is required (`true` = required, `false` = not required) | |
| `cashierExtensionInfo` | string | Cashier extension information | Max 1000 characters |
| `orderId` | string | System order number | Max 20 characters |
| `acqCurrency` | string | Acquiring currency (SGD, for example) | Max 3 characters |
| `acqAmount` | number | Acquiring amount | |
| `payCurrency` | string | Actual payment currency | Max 3 characters |
| `payAmount` | number | Actual payment amount | |
| `merchantName` | string | Acquiring merchant name | Max 128 characters |
| `expiryTime` | integer | Order expiry timestamp (milliseconds) | |
| `qrCodeType` | string | QR code type | Max 20 characters |
| `promoInfo` | object\[] | List of promotions | See below |
| `promoInfo[].promoName` | string | Promotion name | Max 128 characters |
| `promoInfo[].promoCurrency` | string | Promotion currency | Max 3 characters |
| `promoInfo[].promoAmount` | number | Promotion amount | |
| `sdkActionType` | string | SDK action type (A+ only): `HANDLE_BY_PSP` / `HANDLE_BY_SDK` | Max 20 characters |
| `sdkActionPayload` | string | SDK action payload (A+ with `HANDLE_BY_SDK` only) | Max 512 characters |
| `rateInfo` | object | Exchange rate information (`baseCurrency` / `quoteCurrency` / `rate`) | |
| `usdAmount` | number | Amount in USD | |
| `feeAmount` | number | Fee amount | |
| `feeCurrency` | string | Fee currency | Max 3 characters |
| `minAmount` | number | Minimum amount (in the merchant's collection currency) | |
| `maxAmount` | number | Maximum amount (in the merchant's collection currency) | |
| `maxSingleAmount` | number | Maximum single-payment amount (in the merchant's collection currency) | |
| `remainingAmount` | number | The user's remaining allowance for the day (in the merchant's acquiring currency) | |
| `cashierSession` | string | Cashier session (returned when `needCashier=true`) | Max 32 characters |
| `acqOrderNo` | string | Merchant acquiring order number | Max 32 characters |
| `orderType` | string | Order type: the customer scans the merchant code, or the merchant scans the customer code | Max 15 characters |
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": {
"needCashier": false,
"orderId": "",
"acqCurrency": "SGD",
"acqAmount": 12.50,
"payCurrency": "USD",
"payAmount": 9.30,
"merchantName": "",
"expiryTime": 1717855575000,
"qrCodeType": "",
"promoInfo": [],
"rateInfo": { "baseCurrency": "SGD", "quoteCurrency": "USD", "rate": "0.744" },
"usdAmount": 9.30,
"feeAmount": 0.10,
"feeCurrency": "USD",
"minAmount": 0.01,
"maxAmount": 5000.00,
"maxSingleAmount": 2000.00,
"remainingAmount": 1990.70,
"acqOrderNo": "",
"orderType": "POSITIVE_SCAN"
}
}
```
***
## 2. Submit supplementary information (when required)
**`POST /qrpay/v1/submit-extra-info`** submits the cardholder's proof-of-identity details. Call it when `decode` returns `needExtraInfo = true`, then have the cardholder **scan again** and run `decode` a second time.
When `needExtraInfo` is `true`, **every other field of that `decode` response is empty** and the call produces no payable order β do not pass it to `create` or `confirm`.
Today this is triggered by acquirers in certain corridors that require a passport.
### Request
| Field | Type | Required | Description | Constraint |
| - | - | - | - | - |
| `externalUserId` | string | Yes | External user ID | Max 64 characters |
| `requestNumber` | string | Yes | Your own request number, for tracing only β **not an idempotency key** | Max 64 characters |
| `poiInfo` | object | Yes | Proof of identity (POI); every field below is required except `poiIssueDate` | |
| `poiInfo.poiHolderName` | string | Yes | Name of the document holder | 2-50 characters |
| `poiInfo.nationality` | string | Yes | Nationality | ISO-3166-1 alpha-2, uppercase |
| `poiInfo.dateOfBirth` | string | Yes | Date of birth; must be earlier than today | `yyyy-MM-dd` |
| `poiInfo.poiType` | string | Yes | Document type; currently always `PASSPORT` | Max 20 characters |
| `poiInfo.poiNumber` | string | Yes | Document number | 6-20 alphanumeric characters |
| `poiInfo.poiCountryCode` | string | Yes | Issuing country of the document | ISO-3166-1 alpha-2, uppercase |
| `poiInfo.poiIssueDate` | string | No | Issue date. Optional; when supplied it must be not later than today and earlier than the expiry date. To omit it, leave the field out entirely β **do not send an empty string** | `yyyy-MM-dd` |
| `poiInfo.poiExpiryDate` | string | Yes | Expiry date; an already expired document is not rejected by this endpoint | `yyyy-MM-dd` |
```json theme={null}
{
"externalUserId": "",
"requestNumber": "",
"poiInfo": {
"poiHolderName": "",
"nationality": "MY",
"dateOfBirth": "1990-01-01",
"poiType": "PASSPORT",
"poiNumber": "",
"poiCountryCode": "MY",
"poiIssueDate": "2020-01-01",
"poiExpiryDate": "2030-01-01"
}
}
```
### Response (`data`)
Always `null`; treat `code = SYS_SUCCESS` as a successful submission.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": null
}
```
***
## 3. Create the order
**`POST /qrpay/v1/create`** creates and locks the order once the amount has been decoded and confirmed. Compared with `decode` it also accepts `currency` and `amount`, for the case where the user enters the amount because the merchant code carries none.
### Request
| Field | Type | Required | Description | Constraint |
| - | - | - | - | - |
| `externalUserId` | string | Yes | External user ID | Max 64 characters |
| `qrCodeValue` | string | Yes | Scanned value | Max 512 characters |
| `clientIP` | string | Yes | Client request IP | Max 50 characters |
| `currency` | string | No | Currency (SGD, for example), required when the merchant code carries no amount | Max 10 characters |
| `amount` | string | No | Amount, required when the merchant code carries no amount | Max 20 characters |
The `currency` value "SGD" in the example is illustrative only. Send the currency that matches your acquiring or payment scenario, and do not hard-code SGD.
```json theme={null}
{
"externalUserId": "",
"qrCodeValue": "",
"clientIP": "",
"currency": "SGD",
"amount": "12.50"
}
```
### Response (`data`)
The fields are largely the same as the `data` returned by `decode` (`orderId` / `acqCurrency` / `acqAmount` / `payCurrency` / `payAmount` / `merchantName` / `expiryTime` / `qrCodeType` / `promoInfo` / `sdkActionType` / `sdkActionPayload` / `rateInfo` / `usdAmount` / `feeAmount` / `feeCurrency` / `acqOrderNo` / `orderType`), minus `needCashier`, `cashierSession` and the limit fields. Carry the returned `orderId` into the confirmation step.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": {
"orderId": "",
"acqCurrency": "SGD",
"acqAmount": 12.50,
"payCurrency": "USD",
"payAmount": 9.30,
"merchantName": "",
"expiryTime": 1717855575000,
"qrCodeType": "",
"promoInfo": [],
"sdkActionType": "",
"sdkActionPayload": "",
"rateInfo": { "baseCurrency": "SGD", "quoteCurrency": "USD", "rate": "0.744" },
"usdAmount": 9.30,
"feeAmount": 0.10,
"feeCurrency": "USD",
"acqOrderNo": "",
"orderType": "POSITIVE_SCAN"
}
}
```
***
## 4. Confirm the payment
**`POST /qrpay/v1/confirm`** confirms payment for an `orderId` that has already been created. This step **actually debits the user's DeCard-Managed account** and fails if the available balance (`free`) is insufficient.
### Request
| Field | Type | Required | Description | Constraint |
| - | - | - | - | - |
| `externalUserId` | string | Yes | External user ID | Max 64 characters |
| `orderId` | string | Yes | Order ID (returned by `create`) | Max 50 characters |
`externalUserId` is the **external user ID**, the same as on every other endpoint on this page.
```json theme={null}
{
"externalUserId": "",
"orderId": ""
}
```
### Response (`data`)
| Field | Type | Description | Constraint |
| - | - | - | - |
| `payStatus` | string | Payment status | Max 10 characters |
| `transId` | string | Transaction ID (optional) | Max 20 characters |
| `bizTransId` | string | Business transaction ID (optional) | Max 32 characters |
| `redirectUrl` | string | Redirect URL after payment (optional, for the cashier scenario) | Max 256 characters |
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": {
"payStatus": "SUCCESS",
"transId": "",
"bizTransId": "",
"redirectUrl": ""
}
}
```
A successful `confirm` means the payment has been accepted, not completed. If a `redirectUrl` comes back (the cashier or step-up verification scenario), send the cardholder through it. The final state comes from `transList` in `order-detail` or from the webhook. The balance movement caused by the debit is notified through the `BALANCE_CHANGE` webhook, see [Webhook & WebSocket](../../integration-resources/webhook-websocket).
***
## Querying orders
### 5. Order list
**`GET /qrpay/v1/order-list`** returns a user's QR Pay orders with cursor pagination.
| Query parameter | Type | Required | Description | Constraint |
| - | - | - | - | - |
| `externalUserId` | string | Yes | External user ID | Max 64 characters |
| `cursorOrderId` | string | No | Cursor order ID (for paging, pass the `orderId` of the last item on the previous page) | |
| `limit` | string | No | Items per page, `"30"` by default (passed as a string in the query) | |
```
GET /qrpay/v1/order-list?externalUserId=&cursorOrderId=&limit=30
```
`data` comes back as an **array**, where each item carries:
| Field | Type | Description | Constraint |
| - | - | - | - |
| `orderId` | integer | System order number | |
| `orderStatus` | string | Order status | Max 10 characters |
| `acqCurrency` | string | Acquiring currency | Max 3 characters |
| `acqAmount` | number | Acquiring amount | |
| `payCurrency` | string | Payment currency | Max 3 characters |
| `payAmount` | number | Payment amount | |
| `createTime` | string | Creation time | |
| `merchantName` | string | Acquiring merchant name | Max 128 characters |
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": [
{
"orderId": 100000000000000001,
"orderStatus": "SUCCESS",
"acqCurrency": "SGD",
"acqAmount": 12.50,
"payCurrency": "USD",
"payAmount": 9.30,
"createTime": "2026-06-08 14:16:15",
"merchantName": ""
}
]
}
```
> Cursor pagination: take the `orderId` of the last item on this page as the `cursorOrderId` of the next request; an empty array means you have reached the last page.
### 6. Order detail
**`GET /qrpay/v1/order-detail`** returns one order in detail, including its status and the underlying transaction list `transList`.
| Query parameter | Type | Required | Description | Constraint |
| - | - | - | - | - |
| `externalUserId` | string | Yes | External user ID | Max 64 characters |
| `orderId` | string | Yes | Order ID | Max 50 characters |
```
GET /qrpay/v1/order-detail?externalUserId=&orderId=
```
Beyond the amount, merchant, exchange rate and fee fields shared with `decode` and `create`, `data` also carries:
| Field | Type | Description | Constraint |
| - | - | - | - |
| `orderStatus` | string | Order status | Max 10 characters |
| `createTime` | string | Transaction time | |
| `transList` | object\[] | Transaction list (see below) | |
| `transList[].transId` | integer | Transaction number | Up to 20 digits |
| `transList[].transType` | string | Transaction type: `PAY` / `REFUND` / `REFUND_PART` / `REVERSAL` | Max 20 characters |
| `transList[].transStatus` | string | Transaction status: `INIT` / `SUCCESS` / `FAILED` | Max 10 characters |
| `transList[].transTime` | string | Transaction time | Max 19 characters |
One order can have more than one underlying transaction, for instance a `PAY` followed by a `REFUND_PART`. The `transType` and `transStatus` enumerations:
| `transType` | Meaning |
| - | - |
| `PAY` | Payment |
| `REFUND` | Full refund |
| `REFUND_PART` | Partial refund |
| `REVERSAL` | Reversal |
| `transStatus` | Meaning |
| - | - |
| `INIT` | Processing (initial) |
| `SUCCESS` | Succeeded |
| `FAILED` | Failed |
> `transList[].transId` is defined as an integer in the API; the "20" is the maximum number of digits, not a string length.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": {
"orderId": "",
"acqCurrency": "SGD",
"acqAmount": 12.50,
"payCurrency": "USD",
"payAmount": 9.30,
"merchantName": "",
"expiryTime": 1717855575000,
"qrCodeType": "",
"promoInfo": [],
"sdkActionType": "",
"sdkActionPayload": "",
"rateInfo": { "baseCurrency": "SGD", "quoteCurrency": "USD", "rate": "0.744" },
"usdAmount": 9.30,
"orderStatus": "SUCCESS",
"createTime": "2026-06-08 14:16:15",
"transList": [
{
"transId": 200000000000000001,
"transType": "PAY",
"transStatus": "SUCCESS",
"transTime": "2026-06-08 14:16:15"
}
],
"feeAmount": 0.10,
"feeCurrency": "USD",
"acqOrderNo": "",
"orderType": "POSITIVE_SCAN"
}
}
```
***
## Error handling
* When a call does not succeed, `code β SYS_SUCCESS` and `message` and `messageDetail` carry readable information. Judge the outcome from `code` and the business result in `data`, never from an HTTP 200 alone.
* Common failure causes: insufficient `free` balance on the user's DeCard-Managed account (at `confirm`), an expired order (past `expiryTime`), an invalid or unsupported QR code (at `decode`), and an amount outside `minAmount`, `maxAmount`, `maxSingleAmount` or the remaining daily allowance `remainingAmount`.
* A `confirm` that returns as accepted can still fail asynchronously; the final state comes from `transList[].transStatus` in `order-detail` or from the webhook.
* For the complete error code dictionary, ask the DCS team.
## How this relates to the DeCard-Managed balance model
QR Pay and card spending share the same per-user account: the debit happens at `confirm` and lands on the user's **DeCard-Managed account** (the `free` available balance). (`free` and `total` are not returned by these endpoints; those balance fields come from the account model APIs such as `/user-asset/v1/balance`, where `free` maps to `availableBalance` and the held portion to `frozenBalance`. See [User Balance](./user-balance) and [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system).) Make sure the user account has been topped up before starting a QR Pay.
## Next steps
* Account and asset model (`free` / `freeze` / `total`): [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system)
* Reading and adjusting user balances: [User Balance](./user-balance)
* Receiving payment result and balance change notifications: [Webhook & WebSocket](../../integration-resources/webhook-websocket)
* Authentication headers and IP allowlisting: [Authentication Guide](../../integration-resources/overview)
# Reporting Field Descriptions
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-transactions/reporting-field-descriptions
The field dictionary for the Managing Transactions group. This page gathers the meaning, type, enumerated values and unit of every field returned when you query transactions and statements, so it can serve as the reference manual you use when storing data, reconciling, and displaying data to end users.
The richest source of fields is the **statement detail** endpoint `GET /card/v2/statements/detail`, so it forms the main table here, supplemented by the fields of **fiat transaction records** `GET /card/v1/fiat/transactions`. How to query each endpoint, its prerequisites and its pagination rules are covered in [Managing Transactions Β· Overview](./overview) and are not repeated here; this page is about fields only.
> **Why this is not a CSV column dictionary**: in the DeCard-Managed model, data comes back as JSON from **real-time query APIs**, and there is no offline artefact such as a daily full reconciliation file. This page therefore documents **API response fields**, not file columns. Whether daily reconciliation files are offered to partners is a product decision, so raise it with the DCS team if you need one.
## Prerequisites
* Your Enterprise account is live and you hold an `ApiKey` and `SecretKey`; see [First Steps](../../getting-started/first-steps) and the [Authentication Guide](../../integration-resources/overview).
* The end user holds an `externalUserId` (see [Signing Up a Customer](../signing-up-a-customer/overview)) and a card (a `cardId`, see [Issuing Cards](../managing-cards/issuing-cards)).
* **The `externalUserId`, `cardId`, card numbers, merchant names and `postedTransactionId` values in every example on this page are placeholders or redacted. Never put real end-user PII in a request or a log.**
## Field conventions (site-wide)
Before you store anything, note the following conventions of the DeCard-Managed model:
| Dimension | DCS convention |
| - | - |
| Amount unit | **High-precision decimals with a currency** (such as `transactionAmount: 12.34` or `postingAmountInUsd: 12.34`), **not** minor-unit integers, and with no `Γ10^6` scaling |
| Debit or credit direction | `debitCreditIndcator`: `C` = credit (money in) / `D` = debit (money out) |
| Posting state | `postIndicator`: `1` = posted / `0` = not yet posted (see "Posted versus outstanding" below for the semantics) |
| Card number | The field name is `cardNumber` (a masked card number); transaction and statement lists use `cardNumberLast4` (the last 4 digits) |
| MCC | The field name is `mcc`, the Merchant Category Code |
| Country code | `merchantCountryCode`, the merchant country code |
| Response envelope | Site-wide `{code, message, messageDetail, data}`, with **no `success` boolean**; the success code is `code = SYS_SUCCESS`. `messageDetail` is a **structured object** `{message, title, type, icon, action, linkTitle, linkUrl}` (it carries front-end prompts, action buttons and links; on a successful request every sub-field is an **empty string `""`**, not `null` and not omitted, while on a business exception or when user action is needed the system fills in the relevant fields), so it is **not always `null`** |
The field set of the DeCard-Managed model is exactly what the tables on this page describe. Columns such as `authorizationAmount`, `settlementAmount`, `interchangeAmount(Γ10^6)`, `cardLast4`, `isThreeDSecureTransaction`, `walletName` and `foreignExchangeFees` **do not exist** in this model, so do not build your schema around them.
## 1. Statement detail field dictionary (main table)
A redacted sample response, where `data` is an array of the line items in the statement:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": {
"message": "",
"title": "",
"type": "",
"icon": "",
"action": "",
"linkTitle": "",
"linkUrl": ""
},
"data": [
{
"merchantName": "EXAMPLE MERCHANT",
"mcc": "5411",
"merchantCountryCode": "702",
"transactionType": "SPEND",
"cardOrganizationLogo": "https://example.com/logo.png",
"postIndicator": 1,
"transactionDateTime": "2026-06-15 10:00:00",
"postingAmountInSgd": 0.00,
"postingAmountInUsd": 12.34,
"debitCreditIndcator": "D",
"transactionDescription": "POS PURCHASE",
"cardNumber": "************0000",
"postingDate": "2026-06-16",
"transactionAmount": 12.34,
"transactionCurrency": "USD",
"externalTranId": 0,
"cardOrganization": "VISA",
"postedTransactionId": "posted_xxxxxxxx",
"cardScheme": "Visa Platinum",
"systemTraceAuditNumber": "000000",
"transactionDateTimeStr": "2026-06-15 10:00:00",
"assetMovements": [
{
"id": "00_xxxxxxxx",
"asset": "USDC",
"amount": -12.34,
"movementTime": "2026-06-15 10:00:01",
"movementType": "DEBIT"
}
],
"assetMovementStatus": 1
}
]
}
```
At runtime the success code literal is `SYS_SUCCESS` (identical in both integration models). Judge success and failure against the runtime values shown in the examples on this page.
| Field | Type | Meaning and values |
| - | - | - |
| `merchantName` | string | Merchant name |
| `mcc` | string | Merchant Category Code (MCC) |
| `merchantCountryCode` | string | Merchant country code |
| `transactionType` | string | Card transaction type (`CardTransactionTypeEnum`). For the known values, see "Transaction type" below |
| `cardOrganizationLogo` | string | Card network logo image URL |
| `postIndicator` | integer | Posting state: `1` = posted / `0` = not yet posted |
| `transactionDateTime` | string | Transaction time |
| `postingAmountInSgd` | number | Posted amount in SGD (high-precision decimal) |
| `postingAmountInUsd` | number | Posted amount in USD (high-precision decimal) |
| `debitCreditIndcator` | string | Debit or credit direction: `C` = credit (money in) / `D` = debit (money out) |
| `transactionDescription` | string | Transaction description |
| `cardNumber` | string | Card number (masked) |
| `postingDate` | string | Posting date |
| `transactionAmount` | number | Transaction amount (high-precision decimal, paired with `transactionCurrency`) |
| `transactionCurrency` | string | Transaction currency |
| `externalTranId` | integer | External transaction ID |
| `cardOrganization` | string | Card network name (such as `VISA`) |
| `postedTransactionId` | string | Posted transaction ID (present once the transaction has posted) |
| `cardScheme` | string | Card scheme type (such as `Visa Platinum`) |
| `systemTraceAuditNumber` | string | System trace audit number (STAN) |
| `transactionDateTimeStr` | string | Raw transaction time string, formatted `yyyy-MM-dd HH:mm:ss` |
| `assetMovements` | array | The user's asset movements, see "Asset movements" below |
| `assetMovementStatus` | integer | Asset processing state: `1` = funds processed / `0` = funds not yet processed |
### Transaction type (`transactionType`)
`transactionType` comes from the card transaction type enumeration `CardTransactionTypeEnum`. Its known values are:
| Enum value | Meaning |
| - | - |
| `SPEND` | Purchase |
| `REFUND` | Refund |
| `PARTIAL_REFUND` | Partial refund |
> The table above lists the values of `CardTransactionTypeEnum` confirmed at the transaction endpoint. If statement detail ever returns a value outside it, trust what DCS actually returns, and ask the DCS team for the complete enumeration dictionary.
### Asset movements (`assetMovements[]`)
Specific to the DeCard-Managed model: every card transaction carries the movements of the underlying user assets (stablecoins) behind it, which is part of end-to-end visibility across the "crypto β fiat β card spending" chain.
| Field | Type | Meaning |
| - | - | - |
| `id` | string | Unique identifier of the asset movement (the last two digits of `userId` plus an auto-increment ID within the shard) |
| `asset` | string | Asset type (such as `USDT` or `USDC`) |
| `amount` | number | Amount moved: **negative = a deduction, positive = a refund** |
| `movementTime` | string | Time of the movement |
| `movementType` | string | Movement type (such as `DEBIT` or `CREDIT`) |
## 2. Card spending fields (supporting)
| Field | Type | Meaning and values |
| - | - | - |
| `id` | string | Transaction ID |
| `status` | string | Transaction status: `COMPLETE` (completed) |
| `type` | string | Transaction type: `SPEND` / `REFUND` / `PARTIAL_REFUND` |
| `asset` | string | Pricing asset or currency |
| `amount` | number | Transaction amount (high-precision decimal) |
| `refundAmount` | number | Refunded amount (high-precision decimal) |
| `merchantName` | string | Merchant name |
| `merchantNo` | string | Merchant number |
| `time` | string | Transaction time |
## 3. Fiat transaction record fields (supporting)
The key fields follow the same conventions as the rest of this page: `debitCreditIndcator` (`C`/`D`), `transactionAmount` (a high-precision decimal paired with `transactionCurrency`), `postedTransactionId` and `merchantName`. Its transaction type field is `postingTransType` (for example `CTU01` for a stablecoin top-up), whose enumeration is in [Overview (section 2)](./overview).
**The same field name has different types across endpoints**: `transactionAmount` on this endpoint, `GET /card/v1/fiat/transactions`, is a **string amount**, whereas `transactionAmount` in statement detail (`GET /card/v2/statements/detail`) is a **number**. When storing fiat transaction records, parse `transactionAmount` as a string first and then convert it to a high-precision decimal; do not assume it is numeric.
## Posted versus outstanding
The DeCard-Managed model has no daily report file, so "has this transaction finally settled" is expressed through the **posting state fields**:
| Dimension | Field and values | Meaning |
| - | - | - |
| Line item posting state | `postIndicator = 1` / `0` | `1` = posted (settlement complete, the amount is final) / `0` = not yet posted (still an authorization hold, the amount may change) |
| Statement type | `type = POSTED` / `NOT_POSTED` | The statement list (`/card/v2/statements`) groups and totals by posted and not yet posted, see [Overview Β· Querying statements](./overview#1-querying-statements) |
| Linking the two | `postedTransactionId` | The posted transaction ID of a posted record; `POST /card/v1/transaction/id/resolve` ties the outstanding and posted ends of the same purchase together |
> Reconciliation rule: treat **posted records (`postIndicator = 1` / `POSTED`)** as the authoritative basis for final settlement of funds. Outstanding records are authorization holds whose amounts may still change at settlement, so they cannot be treated as deductions. For the concepts behind authorization and posting, see [Transaction Lifecycle Β· Overview](../../basic-concepts/transaction-lifecycle).
## Notes on the reported data
* **Delivery**: JSON returned by real-time query APIs; no daily report files or reconciliation CSV downloads are offered.
* **Amount unit**: high-precision decimals with a currency, unscaled.
* **Posting**: expressed through `postIndicator` and `POSTED`.
* **Distinctive fields**: `assetMovements` (movements of the underlying stablecoin assets), `postingAmountInSgd` and `postingAmountInUsd` (dual-currency posted amounts), and the on-chain deposit forensic fields in the fiat transaction records.
## Next steps
* For which endpoints return these fields and how to query and paginate them, see [Managing Transactions Β· Overview](./overview).
* To read a user's asset balances and crypto funding history, see [User Balance](./user-balance).
* To understand how a transaction travels from authorization to posting, see [Transaction Lifecycle Β· Overview](../../basic-concepts/transaction-lifecycle).
# Settlement (handled inside DCS, plus statement queries)
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-transactions/settlement
The how-to reference for the settlement stage: how the five settlement shapes and refunds are handled inside DCS, how to reconcile against the webhook that follows settlement, and how to query statements, line items, asset movements and the posting state of a given ID. For the semantics and mental model of settlement (the two stages of authorization and settlement, the Outstanding bridge, the invariants), see the concept page Transaction Lifecycle Β· Overview.
## π Guide
Settlement is the **second stage** of the card funding lifecycle: after [authorization](./authorizing-transactions) has frozen the funds, the merchant submits the final amount to the card network, the network and the issuer post the actual amount, and the corresponding hold is released. **Settlement is the mechanism; statements are the queryable view it produces.**
In the DeCard-Managed model, the whole settlement process happens **inside DCS**: on receiving the settlement message from the card network, DCS moves the corresponding statement from **not posted (`NOT_POSTED`)** to **posted (`POSTED`)**, draws down the user's frozen balance, performs the actual deduction and applies the matching asset movement on the user's own account. You never have to track holds yourself or post entries manually after a settlement webhook, which is exactly the convenience the DeCard-Managed model buys you.
The core semantics of settlement is the transition from **outstanding to posted**, expressed by two sets of fields:
* At statement level: the statement `type` moves between `NOT_POSTED` (statement not yet issued) and `POSTED` (statement issued).
* At line item level: `postIndicator` (`1` posted, `0` outstanding) tells you whether each line item has landed in an issued statement.
> The complete definition of the balance model (`free` / `freeze` / `total` and the custody strategies) is in [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system).
## Five settlement shapes and refunds
Settlement can take several shapes. Each one below explains how it arises and how DCS internally releases the hold and posts the amount, so you never have to write your own logic per shape.
### Standard settlement
The most common shape: the merchant settles the full authorized amount. On receiving the settlement message, DCS releases the entire authorization hold and posts the authorized amount.
### Partial settlement
The merchant settles less than the authorized amount (a restaurant bill settled without the tip, for example). DCS releases the **full** authorization hold, posts only the actual amount, and the excess hold returns to the available balance automatically.
### Overage settlement
Certain merchant categories, such as dining and hotels where tips or surcharges apply, may settle slightly above the authorized amount. Once the checks pass, DCS releases the original hold and posts the actual amount.
> Overage settlement only takes effect within the limits allowed by card network rules and the relevant Merchant Category Codes (MCC); requests above the permitted ceiling are rejected.
### Multiple settlement
One authorization settled in several batches, as with staged e-commerce shipments. DCS **keeps** the authorization hold through the early batches and only releases it and posts once every batch has settled.
### Force settlement
Settlement with no prior authorization, as in offline or disconnected scenarios such as in-flight shopping. DCS has no hold to release and posts directly against the available balance.
> Force settlement only applies to certain merchant categories; offline scenarios normally allow a tolerance of up to 15% to absorb adjustments such as shipping and tax.
### Refund
A refund is a negative transaction that credits the amount back to the cardholder's account and may not reference an original transaction. DCS validates the refund request and posts it directly, increasing the available balance.
> A refund can be initiated at any time after the original transaction has completed. In the statement detail, `debitCreditIndcator=C` marks a refund credit.
## The reconciliation webhook after settlement
Once settlement completes, DCS pushes the `CARD_TRANSACTION_SETTLEMENT` webhook event carrying the settled amount (`settledAmount`), the settlement currency (`settledCurrencyCode`), the original transaction amount and currency (`transactionAmount` / `transactionCurrencyCode`), the direction (`direction`), the external transaction ID (`externalTranId`), the last 4 digits of the card number (`cardNumber`), the transaction type (`transactionType`, the single-letter code R/C/Q/P), the local transaction date and time (`localTransactionDate` / `localTransactionTime`), the merchant information (`cardAcceptorNameLocation`, `mcc`, `merchantCountryCode`) and the remaining fields. For every webhook field, the signature verification and a sample JSON payload, see [Webhook & WebSocket](../../integration-resources/webhook-websocket).
### Authorization webhook versus settlement webhook
| Dimension | Authorization webhook `CARD_TRANSACTION` | Settlement webhook `CARD_TRANSACTION_SETTLEMENT` | What the partner does |
| - | - | - | - |
| Stage | When the authorization happens (freeze) | When settlement completes (posting) | β |
| Key fields | `response` (A approved / D declined), `authType` | `settledAmount`, `settledCurrencyCode`, `transactionType` (single letter) | β |
| Meaning of the amount | Pre-authorized amount, which may differ from the final posting | Final posted amount | Reconcile against this |
| Effect on balances | No deduction from the available balance, only a hold | Actual deduction or credit | Already done by DCS, **no manual balance operation needed** |
### What you need to do
* **Query statements**: use `statements` and `statements/detail` (see the next section) to confirm the settled amount and its line items.
* **Reconcile against the webhook**: use the `CARD_TRANSACTION_SETTLEMENT` event to compare DCS settlement records with your own books asynchronously. **Do not use it to post entries manually.**
* **Resolve state by ID** (optional): when you hold a set of transaction IDs and are unsure whether they have posted, call `POST /card/v1/transaction/id/resolve` (see below).
> In the DeCard-Managed model, posting and the balance deduction are already completed inside DCS, so the partner never needs to act on this webhook to change balances on the DCS side.
## How to query settlement results
Once settlement completes, the endpoints below return statements and their line items. Both **identify the card with `cardId`**.
| Endpoint | Method | Path | Purpose |
| - | - | - | - |
| Statement list | GET | `/card/v2/statements` | List statements (filtered by `externalUserId` plus `cardId`) |
| Statement detail | GET | `/card/v2/statements/detail` | Line items for a given `statementId` plus `cardId` (carries the `assetMovements` asset movement detail) |
| Resolve transaction ID | POST | `/card/v1/transaction/id/resolve` | Resolve the posting state of a transaction ID |
> See also `/card/v1/fiat/transactions` (GET, the fiat transaction records). Its `postingTransType` field classifies the transaction after settlement; it is semantically related to `transactionType` in the statement detail but is a different field name, so do not confuse the two. For details, see [Managing Transactions](./overview).
### Statement list
Returns a user's card statements, including posted (`POSTED`) and not yet posted (`NOT_POSTED`) ones, with pagination and filtering.
**Prerequisite**: the user is registered, holds a card and has transacted.
**Request** (v2, GET `/card/v2/statements`):
| Parameter | Required | Description |
| - | - | - |
| `externalUserId` | Required | Partner-side user ID |
| `cardId` | Required | Card ID |
| `page` | Optional | Page number |
| `rows` | Optional | Items per page |
| `order` | Optional | Sort direction |
| `sort` | Optional | Sort field |
| `startTime` | Optional | Start time |
| `endTime` | Optional | End time |
```
GET /card/v2/statements?externalUserId=usr_demo_001&cardId=card_demo_001&page=1&rows=20
```
**Response** (amounts in both SGD and USD; the example uses redacted placeholder data):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": [
{
"type": "POSTED",
"statementDateStart": "2025-01-01",
"statementDateEnd": "2025-01-31",
"paymentAmountInSgd": 0.0,
"paymentAmountInUsd": 0.0,
"nonPostedAmountInSgd": 0.0,
"nonPostedAmountInUsd": 0.0,
"statementId": "stmt_demo_001",
"cardNumberLast4": "1234",
"cardOrganizationLogo": "https://example.com/logo.png",
"cardOrganization": "VISA",
"cardScheme": "VISA",
"debitAmountExcludePayment": 0.0
}
]
}
```
**Fields**:
| Field | Description |
| - | - |
| `type` | Statement type: `POSTED` (statement issued) / `NOT_POSTED` (statement not yet issued) |
| `statementDateStart` / `statementDateEnd` | Statement period |
| `paymentAmountInSgd` / `paymentAmountInUsd` | Repayment amount (SGD and USD) |
| `nonPostedAmountInSgd` / `nonPostedAmountInUsd` | Not-yet-posted amount (SGD and USD) |
| `statementId` | Statement ID |
| `cardNumberLast4` | Last 4 digits of the card number |
| `cardOrganizationLogo` | Card network logo image URL |
| `cardOrganization` / `cardScheme` | Card network / card scheme type (such as `VISA` or `MASTERCARD`) |
| `debitAmountExcludePayment` | Actual spending on the statement, with all repayments and fees deducted |
### Statement detail
Returns the line items of a given `statementId`, including merchant, amount, currency, posting state and asset movement records.
**Request** (v2, GET `/card/v2/statements/detail`):
| Parameter | Required | Description |
| - | - | - |
| `externalUserId` | Required | Partner-side user ID |
| `cardId` | Required | Card ID |
| `statementId` | Required | Statement ID |
| `page` / `rows` / `order` / `sort` | Optional | Pagination and sorting |
```
GET /card/v2/statements/detail?externalUserId=usr_demo_001&cardId=card_demo_001&statementId=stmt_demo_001&page=1&rows=20
```
**Response** (redacted placeholder data with a masked card number and a placeholder merchant name):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": [
{
"merchantName": "DEMO MERCHANT",
"mcc": "5411",
"merchantCountryCode": "702",
"transactionType": "SPEND",
"cardOrganizationLogo": "https://example.com/logo.png",
"postIndicator": 1,
"transactionDateTime": "2025-01-15T10:00:00+08:00",
"postingAmountInSgd": 0.0,
"postingAmountInUsd": 0.0,
"debitCreditIndcator": "D",
"transactionDescription": "DEMO MERCHANT PURCHASE",
"cardNumber": "************1234",
"postingDate": "2025-01-16",
"transactionAmount": 0.0,
"transactionCurrency": "USD",
"externalTranId": 0,
"cardOrganization": "VISA",
"postedTransactionId": "ptx_demo_001",
"cardScheme": "VISA",
"systemTraceAuditNumber": "000000",
"transactionDateTimeStr": "2025-01-15 10:00:00",
"assetMovements": [
{
"id": "am_demo_001",
"asset": "USDT",
"amount": 0.0,
"movementTime": "2025-01-15T10:00:01+08:00",
"movementType": "DEBIT"
}
],
"assetMovementStatus": 1
}
]
}
```
**Fields**:
| Field | Description |
| - | - |
| `merchantName` | Merchant name |
| `mcc` | Merchant Category Code (MCC) |
| `merchantCountryCode` | Merchant country code |
| `transactionType` | Transaction type (values include `SPEND`, `REFUND` and `PARTIAL_REFUND`) |
| `cardOrganizationLogo` | Card network logo image URL |
| `postIndicator` | Posting state: `1` posted, `0` not yet posted |
| `transactionDateTime` / `transactionDateTimeStr` | Transaction time / raw time string (`yyyy-MM-dd HH:mm:ss`) |
| `postingAmountInSgd` / `postingAmountInUsd` | Posted amount (SGD and USD) |
| `debitCreditIndcator` | Debit or credit direction: `C` credit (money in, such as a refund) / `D` debit (money out, such as a purchase) |
| `transactionDescription` | Transaction description |
| `cardNumber` | Card number (masked) |
| `postingDate` | Posting date |
| `transactionAmount` / `transactionCurrency` | Transaction amount / transaction currency |
| `externalTranId` | External transaction ID |
| `cardOrganization` / `cardScheme` | Card network / card scheme type |
| `postedTransactionId` | Posted transaction ID |
| `systemTraceAuditNumber` | System trace audit number |
| `assetMovements` | List of the user's asset movements (see the next section) |
| `assetMovementStatus` | Asset processing state: `1` funds processed, `0` funds not yet processed |
> For the field definitions, see [Managing Transactions βΊ Reporting Field Descriptions](./reporting-field-descriptions).
**Three different transaction type encodings coexist, so keep them apart**: this section describes the **word encoding used by the query APIs** (`SPEND`/`REFUND`/`PARTIAL_REFUND`). **The same field name in webhooks uses a single-letter encoding instead**: `R` card purchase / `C` ATM withdrawal / `Q` enquiry / `P` transfer or refund (see the `CARD_TRANSACTION_SETTLEMENT` field table in [Webhook & WebSocket](../../integration-resources/webhook-websocket)). On top of that, the fiat transaction endpoint `/card/v1/fiat/transactions` uses the field name **`postingTransType`** rather than `transactionType`, with yet another set of values.
`assetMovements`, the field that ties card settlement to on-chain asset movements, is covered in the next section.
## Asset movement detail: assetMovements
`assetMovements` is the field that makes the DeCard-Managed model distinctive, because it **connects card settlement to the on-chain asset movements on the user's own account and ledger**. While querying a card purchase, you can see directly which of the user's crypto assets (USDT, USDC and so on) were deducted or returned, which keeps the flow of funds transparent end to end.
| Field | Description |
| - | - |
| `id` | Unique identifier of the asset movement (the last two digits of the userId plus an auto-increment ID within the shard) |
| `asset` | Asset type (such as `USDT` or `USDC`) |
| `amount` | Amount moved (**negative for a deduction, positive for a refund**) |
| `movementTime` | Time of the movement |
| `movementType` | Movement type (such as `DEBIT` for a deduction or `CREDIT` for a credit) |
The outer `assetMovementStatus` tells you whether the funds behind this settlement have finished processing (`1` complete, `0` incomplete).
> This field arrived with the response format update in August 2025 and is returned by the statement detail endpoint. It reflects the transparency of DCS's own ledger (Crypto-Ledger) mode, where card spending draws down stablecoins directly.
## Resolving posting state by ID: transaction/id/resolve
When you only hold a set of transaction IDs and need to know quickly whether each is **outstanding** or **posted**, use the resolution endpoint POST `/card/v1/transaction/id/resolve`. It is the by-ID entry point into the outstanding-to-posted semantics described above.
**Request** (POST `/card/v1/transaction/id/resolve`):
```json theme={null}
{
"ids": ["tx_demo_001", "tx_demo_002"]
}
```
**Response** (redacted placeholder data):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": [
{
"id": "tx_demo_001",
"outstandingTransactionId": "ost_demo_001",
"postedTransactionId": null,
"posted": false
},
{
"id": "tx_demo_002",
"outstandingTransactionId": null,
"postedTransactionId": "ptx_demo_002",
"posted": true
}
]
}
```
| Field | Description |
| - | - |
| `id` | The original ID you passed in |
| `outstandingTransactionId` | Outstanding transaction ID; **`null` when the transaction posted directly with dual messaging and has no outstanding record** |
| `postedTransactionId` | Posted transaction ID; **`null` while the transaction is still outstanding** |
| `posted` | Whether the transaction has posted (`true` / `false`) |
> How to read it: `posted=false` with an `outstandingTransactionId` means the transaction is still before posting; `posted=true` with a `postedTransactionId` means settlement and posting are complete.
## Response envelope and error handling
Every endpoint on this page uses the site-wide response envelope `{code, message, messageDetail, data}` (**there is no `success` boolean**). The success code literal is `SYS_SUCCESS`. `messageDetail` is usually `null`; when it is not, the structured object contains the following sub-fields:
| Field | Type | Description |
| - | - | - |
| `message` | string | Message body |
| `title` | string | Title |
| `type` | string | Type |
| `icon` | string | Icon identifier |
| `action` | string | Action |
| `linkTitle` | string | Link title |
| `linkUrl` | string | Link URL |
On failure, `code` and `message` explain the cause, such as a card or user that does not exist. The example below is redacted and its error code is a placeholder; in simple failure cases `messageDetail` is usually `null`:
```json theme={null}
{
"code": "CARD_NOT_FOUND",
"message": "Card not found",
"messageDetail": null,
"data": null
}
```
For the complete error code dictionary, see [Integration Resources βΊ Error codes](../../integration-resources/overview).
## Next steps
* For the hold decision that precedes settlement (the authorization stage), read [Authorizing Transactions](./authorizing-transactions).
* For the mental model of the transaction flow (the two stages, the Outstanding bridge, the invariants), read [Transaction Lifecycle Β· Overview](../../basic-concepts/transaction-lifecycle).
* For the `free` / `freeze` / `total` balance model and the custody strategies, read [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system).
* To integrate webhook notifications and verify their signatures, read [Webhook & WebSocket](../../integration-resources/webhook-websocket).
# User Balance
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-transactions/user-balance
The endpoint page for reading one user's per-currency wallet balances (free / freeze / total) in the DeCard-Managed model: how to call GET /user-asset/v1/balance, what the balance model means, and the custody arrangement that makes these balances queryable in the first place.
## One call returns every asset a user holds
In the DeCard-Managed model, every end user holds their own wallet balance, kept in custody by DCS and ledgered per currency, and a single call returns the available, frozen and total amount of each of their assets. As a licensed issuer running its own BINs, DCS keeps this user-level ledger in custody for you, and both authorization and settlement act directly on that user's own balance. This endpoint is how that model is exposed to you.
A user can hold several assets at once, and the balance endpoint returns the available, frozen and total amount **per currency**.
This page covers **balance queries only**. Money moving in, out and around is documented on the sibling pages, with no overlap:
| What you want to do | Endpoint or page |
| - | - |
| **Read** a user's balances per currency (this page) | `GET /user-asset/v1/balance` |
| **Top up or debit** a user | `POST /user-asset/v1/credit` Β· `POST /user-asset/v1/debit`, see [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system) |
| Read **asset transactions and movement history** | `POST /user-asset/v1/transactions` Β· `POST /user-asset/v1/transaction-detail`, see [Overview (transaction and statement queries)](./overview) |
| Read **internal transfer records** | `GET /user-asset/v1/transfer-query`, see the same page as above |
## The balance model: free / freeze / total
Under custody, DCS maintains three figures for each user and each asset:
| Field | Meaning | Description |
| - | - | - |
| `free` | **Available balance** | The portion immediately usable for spending, transfers and withdrawals. An authorization (a card payment) is deducted from here and moved into `freeze`. |
| `freeze` | **Frozen balance** | The portion already committed and temporarily unusable, typically in-flight transactions that are **authorized but not yet settled** (authorization moves the amount from `free` into `freeze`). |
| `total` | **Total balance** | Always equal to `free + freeze`. |
> When an authorization happens, `free` decreases, `freeze` increases and `total` is unchanged; at settlement (the actual deduction) the amount comes out of `freeze` and `total` falls with it. For how authorization and settlement rewrite these three figures, see [Authorizing Transactions](./authorizing-transactions) and [Settlement](./settlement); for the boundary of DCS custody, see [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system).
**Term alignment**: the "availableBalance / frozenBalance" you may see in conceptual material are the `free` and `freeze` of this endpoint. **Always integrate against the API field names `free` / `freeze` / `total`**; `availableBalance` and `frozenBalance` are conceptual aliases only.
## Prerequisites
* The user is registered through [Managing Users](../managing-users/overview) and you hold their `externalUserId`.
* Balances are normally non-zero only after the user has passed KYC and assets have arrived (a top-up or a transfer); a new user may return an empty array or all zeros.
* The caller is a partner onboarded to the DeCard-Managed model and sends the site-wide authentication headers (see [Quickstart](../../getting-started/quickstart)).
## Endpoint contract
The `user-asset` module also offers `credit`, `debit`, `transactions`, `transaction-detail` and `transfer-query`, documented in [Overview (transaction and statement queries)](./overview) and [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system) within this group. This page covers only the balance query.
**`GET /user-asset/v1/balance`**
| Parameter | In | Type | Required | Description |
| - | - | - | - | - |
| `externalUserId` | query | string | β | User ID (the unique identifier of the user on your side). Redacted placeholder: ``. |
> That is the only query parameter; there are **no path parameters and no request body**. This endpoint takes no path parameters and has no notion of a `tenant`, since balances are always read for one user keyed by `externalUserId`.
### Request example
```
GET /user-asset/v1/balance?externalUserId=
```
```bash theme={null}
curl -X GET \
"https:///user-asset/v1/balance?externalUserId=usr_xxxxxxxx" \
-H "X-DAPI-API-KEY: " \
-H "X-DAPI-SIGN: " \
-H "X-DAPI-TIMESTAMP: " \
-H "X-DAPI-NONCE: "
```
> The `usr_xxxxxxxx`, key and signature in the example are all placeholders. **Never paste a real user ID, API key or secret into any document, log or support ticket.**
### Response
The response uses the site-wide envelope `{ code, message, messageDetail, data }`, where `data` is an **array** with one entry per asset the user holds, each describing the balance of one currency.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": {
"message": "",
"title": "",
"type": "",
"icon": "",
"action": "",
"linkTitle": "",
"linkUrl": ""
},
"data": [
{
"asset": "USDT",
"logo": "https:///usdt.png",
"network": "Ethereum",
"free": 100.000000,
"freeze": 20.000000,
"total": 120.000000
},
{
"asset": "USDC",
"logo": "https:///usdc.png",
"network": "Polygon",
"free": 0.0,
"freeze": 0.0,
"total": 0.0
}
]
}
```
Fields of each `data[]` element:
| Field | Type | Description |
| - | - | - |
| `asset` | string | Asset currency code; currently `USDT`, `USDC` and `USD` are known, the set may grow, so trust what is actually returned |
| `logo` | string | Currency logo image URL |
| `network` | string | Network name, such as `Bitcoin`, `Ethereum` or `Polygon` |
| `free` | number | **Available balance** |
| `freeze` | number | **Frozen balance** |
| `total` | number | **Total balance** (= `free` + `freeze`) |
`data` is an **array**, not a single object. Iterate over it by `asset` (and by `network` where that matters), and **do not** assume an object carrying `availableBalance` and `frozenBalance` fields.
The site-wide response envelope is `{ code, message, messageDetail, data }`, and on success the `code` literal is `SYS_SUCCESS` (identical in both integration models).
## Error handling
* **The user does not exist, or `externalUserId` is invalid**: `code` is not the success value, `message` and `messageDetail` explain why, and `data` carries no valid balance. Confirm first that the `externalUserId` was registered through the create-user endpoint.
* **A required parameter is missing**: a request without `externalUserId` is rejected.
* **Authentication failure**: a missing authentication header (`X-DAPI-API-KEY` / `X-DAPI-SIGN` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE`), a bad signature or a replayed `X-DAPI-NONCE` all return the standard authentication error (see Quickstart).
* **Empty account**: when the user exists but holds no assets yet, `data` may be an empty array `[]` or contain all zeros. **That is not an error.**
## About the per-user account and DeCard-Managed model (why balances are queryable)
In the DeCard-Managed model, DCS holds each user's funds in custody internally, keyed by `externalUserId`, with sub-accounts isolated per currency. That is precisely what allows this endpoint to return a balance **per user and per currency**. By contrast:
* **Partner-Managed**: limits and authorization decisions belong to the partner, and DCS exposes no per-user balance endpoint.
* **DeCard-Managed (this documentation set)**: DCS keeps the user's own balance in custody and decides authorizations internally, acting directly on that user's `free` and `freeze`. This endpoint is how that model is exposed to you.
It also explains why whether an authorization succeeds depends mainly on **whether the user's `free` balance is sufficient** and **whether card transactions are blocked for that user**; see [Authorizing Transactions](./authorizing-transactions).
### Real-time push of balance changes (WebSocket BALANCE\_CHANGE)
Whenever an authorization, a settlement, a top-up (`credit`), a debit (`debit`) or an internal transfer changes a user's `free` or `freeze`, DCS pushes a `BALANCE_CHANGE` event in real time over **WebSocket**, carrying `freeDelta` (the change in available balance), `freezeDelta` (the change in frozen balance) and the absolute `free` and `freeze` after the change. You **do not need to poll** `/user-asset/v1/balance` to track balances: subscribe to the WebSocket channel and receive the pushes. See [Webhook & WebSocket](../../integration-resources/webhook-websocket).
## Next steps
* Top-ups and debits (`credit` / `debit`) and the accounts DCS holds in custody: [Account and Asset Model (Ledgering)](../../basic-concepts/ledgering-system)
* How authorization rewrites `free` and `freeze`: [Authorizing Transactions](./authorizing-transactions)
* How settlement deducts from `freeze`: [Settlement](./settlement)
* Asset transactions and movement history (`transactions` / `transaction-detail` / `transfer-query`): [Overview (transaction and statement queries)](./overview)
* User status and transaction restrictions (`forbidCardTransaction`): [Managing Users](../managing-users/overview)
# Managing Users
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/managing-users/overview
Covers the day-to-day management that follows user creation: querying a user's current transaction restrictions (whether withdrawals or card transactions are blocked, GET /account/v1/user-status), and acting as the navigation hub for every capability across the user lifecycle.
## π Guide
In the DeCard-Managed model, the user (Account) is the identity baseline of the whole issuing stack: it carries the end user's `externalUserId`, compliance status and transaction restrictions, and every capability β card issuance, deposits and withdrawals, spending β is anchored to it. As a licensed issuer with its own BINs, DCS manages user transaction restrictions centrally on the risk and compliance side and exposes them to you through a standard endpoint you can query at any time.
This page is about **the day-to-day management that follows user creation**: reading a user's current transaction restrictions (withdrawals blocked / card transactions blocked), and serving as the navigation hub for the rest of the user lifecycle. To register a user, go to [Signing Up a Customer](../signing-up-a-customer/overview).
## User lifecycle at a glance
Capabilities for an end user, from registration through ongoing management, live on different pages. This page focuses on user status and transaction restrictions; for everything else, go to the page listed below.
| Stage | What you do | Where to go |
| - | - | - |
| Register a user | Register with phone number + OTP, which generates `externalUserId` | [Signing Up a Customer βΊ Overview](../signing-up-a-customer/overview) |
| KYC and status | KYC guidance page, status queries, state machine | [Compliance βΊ Overview](../../basic-concepts/compliance-kyc-flow) |
| Travel Rule | Submit or update compliance information such as source of funds and source of wealth | [Compliance βΊ Travel Rule](../virtual-accounts/travel-rule) |
| Extended information / EDD | Update extended information, pre-upload files, upload EDD documents | [Integration Resources βΊ H5 KYC / Card Application Guidance Page](../../integration-resources/h5-kyc-guidance) |
| Balances and assets | Query available and frozen balances, credit and debit funds, review asset records | [Managing Transactions βΊ User Balance](../managing-transactions/user-balance) |
| Account and asset model | Dedicated accounts, the `free` / `freeze` / `total` concepts | [Basic Concepts βΊ Ledgering](../../basic-concepts/ledgering-system) |
## What we do
To keep financial operations on the platform compliant and secure, DCS applies transaction restrictions to users (for example, blocking withdrawals or card transactions) and provides a standard endpoint so you can read a user's current restriction status at any time. **The rules and trigger conditions behind these restrictions are managed by DCS risk and compliance**; the partner reads the outcome through this endpoint and guides the user accordingly in its own system.
## Query user status
Use the query endpoint to read a user's current transaction restrictions: whether withdrawals are blocked, and whether card transactions are blocked.
### Prerequisites
* You hold your Enterprise `ApiKey` / `SecretKey`. If you have not received them yet, see [First Steps](../../getting-started/first-steps).
* You have the target user's `externalUserId` (generated by DCS at registration).
### Endpoint
**`GET /account/v1/user-status`**
#### Query parameters
| Parameter | In | Type | Required | Description |
| - | - | - | - | - |
| `externalUserId` | query | string | β | User ID (generated at registration) |
#### Request example
```
GET /account/v1/user-status?externalUserId=u_***** HTTP/1.1
Host:
```
> For how to build the authentication headers, see [First Steps](../../getting-started/first-steps).
The example uses the masked placeholder `u_*****` for `externalUserId`; replace it with a real user ID (never record real PII in documentation or logs).
#### Response example (200)
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"forbidWithdraw": false,
"forbidCardTransaction": false
}
}
```
#### `data` fields (user status / transaction restrictions)
| Field | Type | Description |
| - | - | - |
| `forbidWithdraw` | boolean | Whether withdrawals are blocked for the user: `true` = blocked, `false` = allowed |
| `forbidCardTransaction` | boolean | Whether card transactions are blocked for the user: `true` = blocked, `false` = allowed |
> **How to use this**: when `forbidWithdraw=true`, block or hide the withdrawal entry point in your own system; when `forbidCardTransaction=true`, tell the user that card transactions are currently restricted. The two flags are independent of each other.
### Common response envelope
Every `/account/` endpoint returns the same envelope; the business payload of this endpoint sits in `data`.
| Field | Type | Description |
| - | - | - |
| `code` | string | System-level return code; `SYS_SUCCESS` on success |
| `message` | string | Return message (for example, `success`) |
| `messageDetail` | object \| null | Content intended for display to the end user (`title`/`message`/`type`/`icon`/`action`/`linkTitle`/`linkUrl`); usually `null` on success |
| `data` | object | Business payload; see the field table above |
> When integrating, use `code` (`SYS_SUCCESS`) to decide whether the call itself succeeded, and the fields inside `data` to decide the user's business status.
### Error handling
Every `/account/` endpoint uses the common response envelope. **Use the `code` field to decide whether the call succeeded**:
* `code` = `SYS_SUCCESS` β the call succeeded and the business payload is in `data`
* `code` β `SYS_SUCCESS` β the call failed; read `message` and `messageDetail` (`title` / `message` / `type`, and so on) to understand why
#### Typical error scenarios
| HTTP status | Typical cause | Where to look |
| - | - | - |
| `400` | A request parameter is missing or malformed (for example, `externalUserId` is absent) | Check that all query parameters are present and that values match the expected format |
| `401` / `403` | Authentication failed (invalid ApiKey, bad signature, expired secret) | Review how you build the authentication headers; see the [authentication guide](../../integration-resources/overview) |
| `404` | The user does not exist (`externalUserId` was never registered, or has been closed) | Verify that `externalUserId` is correct and that the user has completed [registration](../signing-up-a-customer/overview) |
| `500` | Server-side error | Retry; if it persists, contact DCS technical support |
**Note**: the HTTP statuses above group typical scenarios only. The exact error code and the `messageDetail` content returned in production take precedence; do not hard-code checks against codes that are not listed here.
## Assets and balances
This page covers **user status and transaction restrictions** only. A user's funding account (available balance `free` / frozen balance `freeze` / total `total`, credits, debits, asset records) belongs to the balance domain and is not repeated here. Instead, go to:
* Full endpoint and field tables plus runnable examples: [Managing Transactions βΊ User Balance](../managing-transactions/user-balance)
* The account and asset model (dedicated accounts, the `free` / `freeze` / `total` concepts): [Basic Concepts βΊ Ledgering](../../basic-concepts/ledgering-system)
## Next steps
* Register a user: [Signing Up a Customer βΊ Overview](../signing-up-a-customer/overview)
* Travel Rule: [Compliance βΊ Travel Rule](../virtual-accounts/travel-rule)
* User balance: [Managing Transactions βΊ User Balance](../managing-transactions/user-balance)
* Account and asset model: [Basic Concepts βΊ Ledgering](../../basic-concepts/ledgering-system)
# Signing Up a Customer Β· Overview
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/signing-up-a-customer/overview
Covers the two-step registration flow for end users under the DeCard-Managed model: request a verification code (SMS or email) first, then register with the OTP to obtain an externalUserId. Includes the code-sending and registration endpoints, their fields, encrypted transmission, and error handling and resends.
## π Guide
Under the DeCard-Managed model, **the user comes before the card**: an end user must register first to obtain an `externalUserId`, which then identifies that user across every subsequent endpoint β KYC, card issuance, top-ups, spending, balance queries, and so on. As a licensed issuer with its own BINs, DCS creates a dedicated account for each cardholder, and registration is completed with a phone number or email address plus a one-time passcode (OTP).
Registration is a **two-step flow**: request the verification code, then register with it.
> `behavioral=REGISTER` marks this code request as a registration scenario (see step 1 for the field description).
>
> DCS creates a **dedicated account** for each cardholder, with its own available and frozen balances. For account status and asset management, see [Managing Users](../managing-users/overview); this is the entry point to the DeCard-Managed, dedicated-account model.
## Prerequisites
* Your Enterprise account is active and you hold your `ApiKey` / `SecretKey`. If you do not have them yet, see [First Steps](../../getting-started/first-steps) and the [authentication guide](../../integration-resources/overview).
* Every request must carry the signature headers described in the authentication guide. The examples on this page omit those headers and show business fields only.
## Step 1 Β· Send the verification code
Call one of the two code-sending endpoints, depending on the registration method. For both endpoints the **only required field is `behavioral`**, and for registration it is always `REGISTER`.
| Registration method | Endpoint |
| - | - |
| Phone number | `POST /captcha/v1/send-mobile-code` |
| Email address | `POST /captcha/v1/send-email-code` |
### Sending an SMS code
```
POST /captcha/v1/send-mobile-code
```
```json theme={null}
{
"mobileCode": "SG",
"mobile": "91234567",
"behavioral": "REGISTER"
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `behavioral` | string | **Yes** | Scenario enum: `REGISTER` (registration) / `CARD_UNFROZEN` (unfreeze a card) / `WITHDRAW` (withdrawal). Pass `REGISTER` for registration |
| `mobileCode` | string | Either/or | Phone country code as an ISO letter code, such as `CN`, `US`, `SG` |
| `mobile` | string | Either/or | Phone number without the country code; **can be sent AES-encrypted or in plaintext** (see the encryption section below) |
| `externalUserId` | string | Either/or | User ID. A user being registered has no `externalUserId` yet, so leave it out and send `mobileCode` + `mobile` instead; for other scenarios on an existing user (unfreezing a card, withdrawing funds), you may send `externalUserId` instead |
### Sending an email code
```
POST /captcha/v1/send-email-code
```
```json theme={null}
{
"email": "user@example.com",
"behavioral": "REGISTER"
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `behavioral` | string | **Yes** | Scenario enum: `REGISTER` (registration) / `CARD_UNFROZEN` (unfreeze a card) / `WITHDRAW` (withdrawal). Pass `REGISTER` for registration |
| `email` | string | Either/or | Email address; **can be sent AES-encrypted or in plaintext** |
| `externalUserId` | string | Either/or | User ID. Leave it out when registering and send `email` instead; for an existing user you may send `externalUserId` instead |
A successful response from a code-sending endpoint (the envelope is the site-wide `{code, message, messageDetail, data}`, with no `success` boolean; the success code is `code = SYS_SUCCESS`):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": ""
}
```
> `messageDetail` is a display-oriented structure (`message`/`title`/`type`/`icon`/`action`/`linkTitle`/`linkUrl`) and is usually `null`. The envelope is documented in full in the [authentication guide](../../integration-resources/overview).
## Step 2 Β· Register the user
Call the registration endpoint with the verification code received in step 1. Note that the `register` endpoint **does not take `behavioral`** β that field belongs to the code-sending endpoints only, so do not mix them.
```
POST /account/v1/register
```
Registering with a phone number:
```json theme={null}
{
"mobileCode": "SG",
"mobile": "91234567",
"smsCode": ""
}
```
Registering with an email address:
```json theme={null}
{
"email": "user@example.com",
"emailCode": ""
}
```
| Field | Type | Required when | Description |
| - | - | - | - |
| `mobileCode` | string | Registering by phone | Phone country code, such as `CN`, `US`, `SG` |
| `mobile` | string | Registering by phone | Phone number without the country code; AES-encrypted or plaintext |
| `smsCode` | string | Registering by phone | SMS verification code |
| `email` | string | Registering by email | Email address; AES-encrypted or plaintext |
| `emailCode` | string | Registering by email | Email verification code |
> **Either/or rule**: phone registration uses `mobileCode + mobile + smsCode`; email registration uses `email + emailCode`. Send either `smsCode` or `emailCode`, matching the registration method you chose.
The `data` field of a successful response is the **`externalUserId` generated by the system**, which identifies the user in every subsequent endpoint:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": "usr_4dc3e854-xxxx-xxxx-xxxx-91a98b3d832c"
}
```
## Encrypting the phone number or email address (optional)
The `mobile` and `email` fields **can be sent AES-encrypted or in plaintext**; choose whichever suits your integration. To send them encrypted:
1. Encrypt the **plaintext** phone number or email address with AES (symmetric encryption) to get the encrypted byte array.
2. **Base64-encode** the result into a string.
3. Put that Base64 string in the `mobile` or `email` field of the request.
Here is a Java example (the key is a placeholder; replace it with the key you agreed with DCS):
```java theme={null}
import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
public class AES {
private static final String SECRET = "YOUR_AES_SECRET_KEY"; // placeholder, never commit a real key
public static void main(String[] args) {
String mobile = "91234567";
String encryptedMobile = SecureUtil.aes(Base64.decode(SECRET)).encryptBase64(mobile);
String email = "user@example.com";
String encryptedEmail = SecureUtil.aes(Base64.decode(SECRET)).encryptBase64(email);
}
}
```
> The phone number, email address and key in the example are placeholders or masked values. **Never put real end-user PII or real keys into requests or logs.**
## Error handling and resends
* **Judging success**: use `code == SYS_SUCCESS` in the response envelope to decide whether the request was accepted; for any other value, read `message` and `messageDetail` to find out why.
* **Typical error response**: on failure, `code` carries the **specific business error code** for that failure (there is no generic failure code), and the specific reason appears in `message`. When the verification code has expired or was entered incorrectly, the response looks roughly like this (the envelope is unchanged):
```json theme={null}
{
"code": "",
"message": "verification code expired or invalid",
"messageDetail": null,
"data": ""
}
```
> The `code` and `message` above are illustrative placeholders; the actual values returned by the endpoint take precedence. The envelope is always `{code, message, messageDetail, data}`, with the error text in `message`.
* **Expired or mistyped code**: verification codes are time-limited, and an expired or wrong code makes `register` fail. Call the matching code-sending endpoint again (`behavioral=REGISTER`) for a fresh code, then retry.
* **Consistent method**: the registration method must match the way the code was sent. A code sent by SMS can only be used for phone registration (`smsCode`), and the same applies to email (`emailCode`).
* **Idempotency**: registering the same phone number or email address twice fails. If you are unsure whether a user already exists, query their status from [Managing Users](../managing-users/overview).
## Next steps
* Once registration returns an `externalUserId`, the usual next step is completing KYC for that user; see [Compliance Β· Overview](../../basic-concepts/compliance-kyc-flow).
* To manage account status and review a user's assets, see [Managing Users](../managing-users/overview).
* To walk the shortest path from registration to card issuance, funding and spending in one pass, see [Quickstart](../../getting-started/quickstart).
# Simulating Transactions Β· Overview
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/simulating-transactions/overview
The overview of the sandbox simulation endpoints for the DeCard-Managed model: four simulation/ endpoints (KYC simulation token, deposit simulation, authorization simulation v1 and v2) that together cover the minimal KYC β deposit β authorized spend loop. Sandbox only.
## Exercise the whole flow in the sandbox before you go live
Without moving real money or touching real cardholders, you can assemble a complete KYC β deposit β authorized spend flow in the sandbox and validate your integration logic ahead of time (the KYC flow, balance movements, Webhook and WebSocket delivery handling). As a licensed issuer with its own BINs, DCS provides a set of sandbox simulation endpoints so that your integration is solid well before it reaches the card networks.
**The simulation endpoints are for sandbox and non-production environments only.** All four `simulation/*` endpoints belong to the "open API simulator" group; never call them in production.
## What you can simulate
The DeCard-Managed model offers **four simulation endpoints** across three capabilities. Requests and responses for each are covered below in "Endpoint reference".
| Capability | Endpoint | What it simulates | Real capability it maps to |
| - | - | - | - |
| **KYC simulation token** | `POST /simulation/v1/generate-kyc-token` | Issues a KYC simulation token for a given user so you can walk the KYC flow in the sandbox | KYC / H5 guidance page |
| **Deposit simulation (FOMO)** | `POST /simulation/v2/deposit` | Simulates an on-chain crypto deposit arriving (FOMO channel), triggering the user balance and deposit flow | Crypto deposits |
| **Authorization simulation (spend / refund / reversal)** | `POST /simulation/v2/fund-auth` (uses `cardId`) | Simulates a card authorization request, as a spend, a refund or a spend reversal | [Authorizing Transactions](../managing-transactions/authorizing-transactions) |
> The DeCard-Managed model has no concept of on-chain collateral or payment routing (transfers), so there are **no** matching `collateral-funding` / `transfer-transactions` simulations. Authorization is decided in a single pass inside the system, so there are also **no** standalone `settlement` / `authorization-updates` / `authorization-reversals` / `3ds-challenges` simulation endpoints. For how to test those scenarios, see "What is out of scope" below.
## Prerequisites
| Item | Description | Who provides it |
| - | - | - |
| Sandbox API credentials | `ApiKey` + `SecretKey`, used for authentication (`X-DAPI-API-KEY` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE` / `X-DAPI-SIGN`) | DCS |
| A registered user | Complete user registration in the sandbox first and keep the `externalUserId` | Partner |
| A usable card | Walk KYC with the simulation token, then apply for a card and keep the `cardId` | Partner |
| Webhook / WebSocket subscription | Configure callbacks and real-time delivery so you can observe the events the simulations produce | Partner |
* Your Enterprise account is active. If you do not have credentials yet, see [First Steps](../../getting-started/first-steps) and the [authentication guide](../../integration-resources/overview).
* Every request must carry the signature headers described in the authentication guide. The curl examples on this page omit some header details and focus on the business fields and the sandbox host.
* **Every `externalUserId`, `cardId`, last-4 value, deposit address, token and `YOUR_API_KEY` in the examples is a placeholder or masked value. Never put real end-user PII or real keys into requests or logs.**
## Sandbox host
| Environment | Base URL |
| - | - |
| Sandbox | `https://api.thedecard-sandbox.com` |
| Production | `https://api.thedecard.com` |
> The simulation endpoints exist in the **sandbox** only.
## Getting started
1. **Get sandbox access.** Request sandbox `ApiKey` / `SecretKey` from DCS. The simulation endpoints are available in the sandbox only.
2. **Create a test user and a test card.** Use `POST /simulation/v1/generate-kyc-token` to mint a KYC simulation token for the user, walk KYC, then apply for a card and keep the `cardId`. See [Signing Up a Customer](../signing-up-a-customer/overview) and [Managing Cards Β· Issuing Cards](../managing-cards/issuing-cards).
3. **Set up Webhook and WebSocket.** Simulations fire the same events as real transactions, so having your receivers ready makes them easy to observe. See [Webhook + WebSocket](../../integration-resources/webhook-websocket).
4. **Run the simulations.** Start with `POST /simulation/v2/deposit` to simulate an incoming deposit, then `POST /simulation/v2/fund-auth` to simulate a spend, and watch the balance and transaction records change.
## Endpoint reference
> All endpoints share the site-wide response envelope `{ code, message, messageDetail, data }` β with **no `success` boolean**; the success code literal is `code = SYS_SUCCESS`. Use `code == SYS_SUCCESS` to decide whether the request was accepted, and the fields inside `data` to read the business outcome. `messageDetail` is an object intended for display to the end user, with seven optional sub-fields: `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`; usually each sub-field is an empty string.
### 1 Β· Generate a KYC simulation token
Mints a KYC simulation token for a given user so you can walk the KYC flow in the sandbox.
```
POST /simulation/v1/generate-kyc-token
```
```json theme={null}
{
"externalUserId": "usr_xxxxxxxx"
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | Yes | External user ID |
Successful response:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": {},
"data": {
"token": ""
}
}
```
| Field | Description |
| - | - |
| `data.token` | KYC simulation token, used to walk the KYC flow in the sandbox |
### 2 Β· Simulate a deposit (FOMO channel)
Simulates an on-chain crypto deposit arriving (FOMO channel), the sandbox equivalent of faking a payment into a deposit address, which triggers the user balance and deposit flow. **This simulates an on-chain arrival, not a fiat top-up.**
```
POST /simulation/v2/deposit
```
```json theme={null}
{
"chain": "Polygon",
"currency": "USDC",
"amount": "100.0",
"address": ""
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `chain` | string | Yes | Chain name, one of `Ethereum` / `SOL` / `BASE` / `Polygon` / `TRON` |
| `currency` | string | Yes | Currency, either `USDC` or `USDT` |
| `amount` | string | Yes | Deposit amount (decimal string) |
| `address` | string | Yes | Deposit address. The format depends on `chain`: EVM chains (Ethereum/Polygon/BASE) start with `0x`; Solana (`SOL`) is base58; TRON starts with `T`. Use the user's actual deposit address on that chain |
The chain and token matrix supported by this endpoint is exactly the one in the table above: **5 chains (Ethereum / SOL / BASE / Polygon / TRON) Γ 2 stablecoins (USDC / USDT)**. This matrix describes sandbox simulation and may differ from production coverage. Real deposit addresses and the chain and currency configuration belong to the deposits and withdrawals area (crypto deposits) and are not covered here.
Successful response:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": {},
"data": {
"txHash": ""
}
}
```
| Field | Description |
| - | - |
| `data.txHash` | Transaction hash of the simulated deposit |
### 3 Β· Simulate an authorization (spend / refund / reversal)
Simulates a card authorization request. The DeCard-Managed model covers spend, refund and spend reversal with **a single `fund-auth` endpoint plus the `authType` enum**.
The card is identified precisely by `cardId` (not by the last 4 digits of the card number, which can be ambiguous when one user holds several cards).
**Request:**
```
POST /simulation/v2/fund-auth
```
```json theme={null}
{
"externalUserId": "usr_xxxxxxxx",
"cardId": "card_xxxxxxxx",
"authType": "EXPEND",
"amount": 12.50,
"currency": "USD"
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | Yes | External user ID |
| `cardId` | string | Yes | Card ID |
| `authType` | string | Yes | Authorization type (see the enum table below) |
| `amount` | number | Yes | Amount (decimal), in major currency units |
| `currency` | string | Yes | Transaction currency, a standard code such as `USD` / `RMB` / `SGD` / `EUR` / `JPY` |
**`authType` enum:**
| Value | Meaning |
| - | - |
| `EXPEND` | Spend |
| `REFUND` | Refund |
| `REVERSAL` | Spend reversal |
> The DeCard-Managed model covers spend, refund and reversal through this one `fund-auth` endpoint plus `authType`. There are **no** separate settlement, authorization reversal or authorization update endpoints; concepts such as `settle` / `reverse-hold` / `partial-reversal` / `tip-update` do not exist in the DeCard-Managed model.
Successful response:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": {},
"data": {
"approved": false,
"errorCode": ""
}
}
```
| Field | Type | Description |
| - | - | - |
| `data.approved` | boolean | Whether the authorization was approved |
| `data.errorCode` | string | Error code (returned when the authorization is declined) |
`code == SYS_SUCCESS` only means the simulation request was accepted correctly; it **does not mean the authorization was approved**. Read `data.approved` for the authorization outcome.
## Common patterns
### Run the minimal deposit β spend β check balance loop
```bash theme={null}
# 1. Simulate an on-chain deposit arriving (USDC on Polygon)
curl -X POST https://api.thedecard-sandbox.com/simulation/v2/deposit \
-H "Content-Type: application/json" \
-H "X-DAPI-API-KEY: YOUR_API_KEY" \
-H "X-DAPI-TIMESTAMP: MILLIS_TIMESTAMP" \
-H "X-DAPI-SIGN: YOUR_SIGNATURE" \
-H "X-DAPI-NONCE: 12345" \
-d '{
"chain": "Polygon",
"currency": "USDC",
"amount": "100.0",
"address": ""
}'
# 2. Simulate a spend authorization
curl -X POST https://api.thedecard-sandbox.com/simulation/v2/fund-auth \
-H "Content-Type: application/json" \
-H "X-DAPI-API-KEY: YOUR_API_KEY" \
-H "X-DAPI-TIMESTAMP: 1760943263227" \
-H "X-DAPI-NONCE: 12346" \
-H "X-DAPI-SIGN: YOUR_SIGNATURE" \
-d '{
"externalUserId": "usr_xxxxxxxx",
"cardId": "card_xxxxxxxx",
"authType": "EXPEND",
"amount": 12.50,
"currency": "USD"
}'
# 3. Query the user balance and confirm available/frozen balances moved as expected
# See the "User Balance" page: the user-asset endpoints
```
### Test refunds and reversals
Change `authType` in step 2 to `REFUND` (refund) or `REVERSAL` (spend reversal), repeat the call, and watch balances and transaction records move in the opposite direction. That confirms your system transitions state correctly in these scenarios.
## What is out of scope
* **Authorization is decided in a single pass inside the system**, so there are no separate settlement, authorization update or authorization reversal simulation endpoints; spend, refund and reversal are all expressed through `authType` on `fund-auth`.
* **On-chain collateral and payment routing are not involved**, so no simulation endpoints exist for them.
* **3DS authentication is completed on the issuing side**, so there is no standalone 3DS simulation endpoint. To test 3DS handling, see [3DS Forwarding](../managing-transactions/3ds-forwarding).
## Next steps
* To understand how authorization is decided and how it holds balance inside the system, see [Authorizing Transactions](../managing-transactions/authorizing-transactions).
* To see how user balances (`availableBalance` / `frozenBalance`) move after a simulated deposit or spend, see [User Balance](../managing-transactions/user-balance).
* For real on-chain deposit addresses and the chain and currency configuration, see [Crypto Deposit](../virtual-accounts/crypto-deposit).
* For how the KYC token is used in the real flow, see [Authentication and Integration Resources](../../integration-resources/overview).
# Crypto Deposits (Deposit Addresses / Chain and Token Matrix)
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/virtual-accounts/crypto-deposit
The endpoint reference for on-chain crypto deposits under the DeCard-Managed model: confirm the chain and token (network-coin) β obtain a deposit address (deposit-address) β the user transfers funds and the balance is credited. Covers the chain and token matrix, confirmation counts, and simulated deposits in the sandbox.
## Overview
On-chain deposits let your users send crypto over a blockchain network to a deposit address held in custody by DCS. Once credited, the funds appear in the user's dedicated account balance at DCS (`availableBalance`) and can be spent with a linked card. The whole flow runs on the blockchain, so no partner float or intermediary funding is required. As a licensed issuer with its own BINs, DCS handles on-chain monitoring, confirmation, and crediting on your behalf, turning "crypto β available balance β card payment" into a single path.
The flow is: **the user sends crypto in β the platform monitors and confirms it β the funds are credited as available balance**.
Three core steps:
1. Call `GET /crypto/v1/network-coin` to confirm which chains and tokens are currently supported and whether deposits are enabled.
2. Call `GET /crypto/v2/deposit-address` (recommended) to obtain the user's deposit address on the chosen chain and token.
3. The user transfers to that address; the platform monitors the on-chain transaction, credits the funds once the confirmation count is reached, and pushes the status over **WebSocket**.
***
## Prerequisites
Before making an on-chain deposit, the user must complete the following compliance steps. Otherwise no deposit address can be issued and no funds can be credited:
| Prerequisite | Description | Where to go |
| - | - | - |
| **KYC passed** | The user's identity verification must be in a passed state | See [Compliance Β· Overview](../../basic-concepts/compliance-kyc-flow) |
| **Travel Rule completed** | Travel Rule compliance information must be submitted first | Call `POST /account/v1/update-travel-rule`; for the concept, see [Travel Rule](./travel-rule) |
**Method note**: use **`POST /account/v1/update-travel-rule`** to submit Travel Rule information and **`GET /account/v2/query-travel-rule`** to read it back.
***
## Key concepts
### Chain model
The chain model means the user moves crypto directly over a blockchain network by transferring it to an on-chain address issued by the platform (an inbound deposit). The platform keeps the whole flow traceable through the blockchain transaction hash (`txHash`).
### Deposit address
The receiving address the platform allocates to a user for a specific chain and token. Any transfer of the matching asset to that address counts as a deposit into the user's DCS account.
### Confirmation
An on-chain transaction must reach a certain number of block confirmations before it is treated as final. The minimum confirmation count for each chain comes from the `network-coin` configuration (`minConfirm`) and varies by chain.
***
## API flow
***
## Endpoints in detail
### Preliminary step: complete the Travel Rule
> This step is already listed under [Prerequisites](#prerequisites); it is a compliance action that must be completed before the deposit flow begins. A full request example is given here for reference.
```
POST /account/v1/update-travel-rule
```
Request body (8 top-level fields plus 6 address sub-fields; only `externalUserId` is required, and all PII is a placeholder):
```json theme={null}
{
"externalUserId": "",
"channelName": "",
"institutionName": "",
"name": "",
"id": {
"type": "",
"value": "",
"countryOfIssue": "SG"
},
"address": {
"city": "",
"country": "SG",
"dependentLocality": "",
"postalCode": "",
"region": "",
"addressLines": ""
},
"dateOfBirth": "",
"placeOfBirth": ""
}
```
> `dateOfBirth` (date of birth, format YYYY-MM-DD), `placeOfBirth` (place of birth), and `address.dependentLocality` (county or district name) are optional compliance fields; only `externalUserId` is mandatory. Values for the other optional fields such as `channelName` and `institutionName` are supplied by the partner as needed. For the full field list and the query endpoint (`GET /account/v2/query-travel-rule`), see [Travel Rule](./travel-rule).
### Step 1: confirm the supported chains and tokens
```
GET /crypto/v1/network-coin?type=DEPOSIT
```
| Parameter | In | Type | Required | Description |
| - | - | - | - | - |
| `type` | query | string | β | `DEPOSIT` (deposit) / `WITHDRAW` (withdrawal) |
Returns an array of the user's current wallet configuration. **For deposits, use this response to decide dynamically which chains and tokens are available and how many confirmations are needed**, rather than hard-coding a static list. Commonly used fields (`data[]`):
| Field | Type | Description |
| - | - | - |
| `network` | string | Blockchain network identifier |
| `networkNativeAsset` | string | Native asset of the network |
| `asset` | string | Token code |
| `depositEnable` | boolean | Whether deposits are enabled |
| `withdrawEnable` | boolean | Whether withdrawals are enabled |
| `minConfirm` | integer | Minimum confirmations for a deposit |
| `lockConfirm` | integer | Lock confirmation count |
| `estimatedArrivalTime` | integer | Estimated time to credit |
| `addressRegex` | string | Regex for validating the address |
| `memoRegex` | string | Regex for validating the memo/tag |
| `contractAddress` | string | Contract address (token contract) |
| `depositDesc` | string | Deposit instruction copy |
| `specialTips` | string | Special notice copy |
| `withdrawIsTag` | boolean | Whether a tag is required for withdrawals |
| `withdrawFee` / `withdrawMin` / `withdrawMax` | number | Withdrawal fee / minimum / maximum (used in withdrawal scenarios) |
> **Call `network-coin` first to confirm the configuration**, then call `deposit-address`.
### Step 2: obtain the deposit address
**Query parameters**:
| Parameter | In | Type | Required | Description |
| - | - | - | - | - |
| `externalUserId` | query | string | β | User ID |
| `network` | query | string | β | Blockchain network name |
| `coin` | query | string | β | Token code |
**`GET /crypto/v2/deposit-address`** β `data` fields:
| Field | Type | Description |
| - | - | - |
| `network` | string | Blockchain network identifier |
| `address` | string | Deposit address |
| `fxRate` | number | Exchange rate, used for currency conversion |
| `coin` | string | Token code |
| `status` | string (enum) | Address allocation status: `PENDING` / `SUCCESS` / `FAILED` |
> `fxRate` (conversion rate) lets you show a converted amount, and `status` tells you whether the address is ready. The minimum deposit and confirmation count are not returned here; both are available from `network-coin` (see `minConfirm`).
**Request example** (placeholders/redacted; for authentication headers see [Integration Resources Β· Authentication guide](../../integration-resources/overview)):
```bash theme={null}
curl -X GET "https://api.thedecard-sandbox.com/crypto/v2/deposit-address?externalUserId=usr_xxx&network=TRON&coin=USDT" \
-H "X-DAPI-API-KEY: " \
-H "X-DAPI-TIMESTAMP: 1760943263227" \
-H "X-DAPI-NONCE: 12345" \
-H "X-DAPI-SIGN: "
```
**Response example** (v2, populated, address is a placeholder):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": null,
"data": {
"network": "TRON",
"address": "T...placeholder address, do not copy verbatim...",
"fxRate": 1.0,
"coin": "USDT",
"status": "SUCCESS"
}
}
```
> The response envelope is the same across the documentation: `{ code, message, messageDetail, data }` (**no `success` boolean**). `messageDetail` is normally `null`; when populated it is an object `{message, title, type, icon, action, linkTitle, linkUrl}`. The success value of `code` is **`SYS_SUCCESS`** (identical in both integration models).
***
## What happens once the funds arrive
1. **User transfer**: the user sends crypto of the matching chain and token to the deposit address, producing a transaction hash `txHash` on-chain.
2. **Platform monitoring**: DCS watches the blockchain network and waits for the transaction to reach that chain's `minConfirm` / `minConfirmationNo`.
3. **Crediting**: once the confirmation count is reached, the funds are credited to the user's account balance at DCS (the dedicated account `availableBalance`).
4. **WebSocket push**: DCS pushes transaction status changes in **real time over WebSocket** (see [Integration Resources Β· WebSocket real-time push](../../integration-resources/webhook-websocket)).
5. **Partner query**: the partner can call `GET /card/v1/fiat/transactions?externalUserId=...` at any time to retrieve deposit and fiat transaction records and reconcile what was credited.
For a deposit record in `fiat/transactions`, the on-chain details sit in the `transferDetails` sub-object, which contains `channelCode`, `txnAmt`, `txnCcy`, `sender`, `receiving`, `timeStamp`, `txHash`, `network`, and `asset`.
> Real-time push is the proactive "funds credited" notification; `fiat/transactions` is the on-demand reconciliation query. Use both: let WebSocket drive UI updates and use `fiat/transactions` for final reconciliation.
**Time to credit**: deposits in DPT mode are typically credited in about **2 minutes** (after the on-chain confirmation count is reached). Actual timing varies with network congestion and the required confirmation count; treat the WebSocket push and the status in `fiat/transactions` as authoritative.
***
## Supported chain and token matrix
The supported chains and tokens are **whatever `GET /crypto/v1/network-coin?type=DEPOSIT` returns at the time of the call**; do not hard-code a static list, as the configuration changes with the business. The endpoint returns `depositEnable`, `minConfirm`, `addressRegex`, `contractAddress`, and more for each chain and token.
**On specific values**: in production, the available chains and tokens follow the dynamic configuration from `GET /crypto/v1/network-coin`, so do not hard-code a static list. If you need to confirm the exact values accepted by the sandbox simulation endpoint, contact the DCS team.
### Simulating a deposit in the sandbox
To test the on-chain deposit and crediting flow, use the simulation endpoint to create a credit directly:
```
POST /simulation/v2/deposit
```
Request body (placeholder template; do not copy a specific chain or token from it):
```json theme={null}
{
"chain": "",
"currency": "",
"amount": "",
"address": ""
}
```
> When filling this in, set `address` to the deposit address obtained in step 3. If you need to confirm the accepted values for `chain` and `currency`, contact the DCS team.
> Simulated deposits work in the sandbox only, and exist to trigger crediting and the WebSocket push so you can verify your integration. For an overview of the simulation endpoints and other sandbox capabilities, see [Simulating Transactions](../simulating-transactions/overview).
***
## Error handling
| Scenario | Description | Recommended handling |
| - | - | - |
| Travel Rule not completed | The address was requested before calling `update-travel-rule` | Complete `POST /account/v1/update-travel-rule` first |
| KYC not passed | The user's identity verification has not reached a passed state | Guide the user through KYC; see [Compliance Β· Overview](../../basic-concepts/compliance-kyc-flow) |
| Chain or token unsupported or disabled | The `network`/`coin` pair is absent from the `network-coin` response, or `depositEnable=false` | Validate against `network-coin` before requesting an address |
| v2 address `status=PENDING` | The deposit address is still being allocated | Poll until `SUCCESS` before showing it to the user; on `FAILED`, retry or contact support |
| Address or memo format mismatch | The address or memo used for the transfer does not match `addressRegex`/`memoRegex` | Validate against the regex in your front end, and prompt the user to include a memo/tag on chains that require one |
> Request the full error code dictionary from the DCS team; for any specific failure, the `code` and `message` returned by the API are authoritative.
***
## Next steps
* For crypto withdrawals, see [Crypto Withdrawal](./withdraw-offramp)
* For real-time credit notifications, see [Integration Resources Β· WebSocket real-time push](../../integration-resources/webhook-websocket)
# Travel Rule
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/virtual-accounts/travel-rule
The Travel Rule is a compliance gate that stands apart from KYC: a user must upload Travel Rule information before any on-chain deposit address can be issued. This page covers three things: what the Travel Rule is, which capabilities the DeCard-Managed model provides, and one flow diagram. Details such as each endpoint's request and response fields and the EDD sub-object breakdown are linked out to the how-to pages.
## π Guide
The **Travel Rule** is a requirement under the global anti-money laundering (AML) framework. It standardises the information exchanged during cross-border and virtual asset transfers so that financial transactions stay transparent and traceable. In the DeCard-Managed model, an end user must upload Travel Rule information before obtaining an on-chain deposit address: **a user who has not completed the upload cannot obtain a deposit address**, and therefore cannot make a cross-border deposit.
**Prerequisite**: before calling these endpoints, the user must already have been created through the registration endpoint and you must hold their `externalUserId`. The Travel Rule upload should be completed before the user requests an on-chain deposit address.
***
## Capabilities in the DeCard-Managed model
DCS exposes standardised endpoints for managing Travel Rule information. Depending on how deeply you integrate, you can submit it server-side or let the user upload it themselves on a DCS-hosted page:
| Capability | Endpoint | Method | Description |
| - | - | - | - |
| Upload Travel Rule information | `/account/v1/update-travel-rule` | POST | Submit the user's identity, address, and birth details (only `externalUserId` is required) |
| Query Travel Rule information | `/account/v2/query-travel-rule` | GET | Read back what was submitted, by `externalUserId` |
| Self-service upload via hosted page | `/redirect/v2/guidance-link` | POST | With `action=TRAVEL_RULE`, the user fills in and uploads everything on a DCS-hosted page, so you never collect the sensitive data |
| Generate a pre-upload URL for EDD files | `/account/v1/generate-file-upload-prepare` | POST | High-risk cases: obtain a presigned URL plus an `objectKey` |
| Submit EDD files | `/account/v1/upload-edd-file` | POST | Submit references to source of funds (SoF) and source of wealth (SoW) evidence |
DCS focuses on four things: **data standardisation** (a single structure for document types, addresses, dates of birth, and so on), **endpoint support** (upload and query), **file management** (EDD, covering both source of funds and source of wealth), and **process linkage** (tied to obtaining an on-chain deposit address, since requesting an address before the upload is complete is rejected).
> **EDD (Enhanced Due Diligence)**: for high-risk cases such as large cross-border transfers or suspicious activity, DCS lets users submit 12 categories of supporting evidence in total, covering source of funds (SoF, 6 categories) and source of wealth (SoW, 6 categories). The flow is two steps: generate a pre-upload URL and upload the file, then submit the file reference. The full sub-object breakdown and fields are in the how-to links below.
***
## EDD file upload (Enhanced Due Diligence)
For high-risk compliance cases such as large cross-border transfers or suspicious activity, DCS offers EDD (Enhanced Due Diligence) file upload, letting users submit two kinds of supporting evidence: Source of Funds (SoF) and Source of Wealth (SoW). The flow has two steps:
1. **Generate the pre-upload URL**: call POST `/account/v1/generate-file-upload-prepare` with the list of file names to be uploaded, and receive a presigned URL and an `objectKey`.
2. **Submit the EDD files**: upload the files to the URL you received, then call POST `/account/v1/upload-edd-file` to submit the EDD file references for each dimension (each containing `fileName` plus `filePath`/`objectKey`).
**Request fields (EDD submission body)**
| Field | Type | Required | Description |
| - | - | - | - |
| `externalUserId` | string | **Yes** | External user ID |
| `requestRef` | string | **Yes** | Unique idempotency key, used to prevent duplicate submissions |
| `applyId` | string | No | Virtual card application ID, to tie the evidence to a specific card application |
| `eddSowSalary` | object | No | Source of wealth (SoW) β salary income |
| `eddSowBizRevenue` | object | No | Source of wealth β business revenue |
| `eddSowGift` | object | No | Source of wealth β gift |
| `eddSowInheritance` | object | No | Source of wealth β inheritance |
| `eddSowInvest` | object | No | Source of wealth β investment gains |
| `eddSowOther` | object | No | Source of wealth β other |
| `eddSofSaving` | object | No | Source of funds (SoF) β salary savings |
| `eddSofSale` | object | No | Source of funds β asset sale |
| `eddSofBizRevenue` | object | No | Source of funds β business revenue |
| `eddSofCrypto` | object | No | Source of funds β cryptocurrency |
| `eddSofInvest` | object | No | Source of funds β liquidated investments |
| `eddSofOther` | object | No | Source of funds β other |
**EDD sub-object categories** (12 in total: 6 SoW plus 6 SoF, covering both the source of wealth and the source of funds)
| Sub-object | Dimension | Description |
| - | - | - |
| `eddSowSalary` | Source of wealth | Salary income (employer name, job title, annual income, plus evidence) |
| `eddSowBizRevenue` | Source of wealth | Business revenue (company name, nature of business, annual revenue, plus evidence) |
| `eddSowGift` | Source of wealth | Gift (donor, relationship, donor's source of wealth, asset valuation, plus evidence) |
| `eddSowInheritance` | Source of wealth | Inheritance (name of the deceased, relationship, year of inheritance, valuation, plus evidence) |
| `eddSowInvest` | Source of wealth | Investment gains (investment type, valuation, plus evidence) |
| `eddSowOther` | Source of wealth | Other sources of wealth (description plus evidence) |
| `eddSofSaving` | Source of funds | Salary savings (employer name, job title, annual income, plus evidence) |
| `eddSofSale` | Source of funds | Asset sale (evidence) |
| `eddSofBizRevenue` | Source of funds | Business revenue (company name, nature of business, annual revenue, plus evidence) |
| `eddSofCrypto` | Source of funds | Cryptocurrency (platform name, type of holding, plus evidence) |
| `eddSofInvest` | Source of funds | Liquidated investments (investment type, valuation, plus evidence) |
| `eddSofOther` | Source of funds | Other sources of funds (description plus evidence) |
> Every sub-object carries a `docUrls` array (`{ fileName, filePath }`) pointing at files already uploaded through the pre-upload URL. For the full field specification, such as the `recurringGifts` substructure of `eddSowGift` or which fields are mandatory in each sub-object, see the endpoint API reference page.
**Request example** (EDD file submission, showing only some sub-objects; placeholder data)
```http theme={null}
POST /account/v1/upload-edd-file HTTP/1.1
Host: {{dicard-server}}
Content-Type: application/json
```
```json theme={null}
{
"externalUserId": "",
"requestRef": "",
"applyId": "",
"eddSowSalary": {
"employerName": "",
"jobTitle": "",
"annualIncome": "100000",
"docUrls": [
{ "fileName": "payslip.pdf", "filePath": "" }
]
},
"eddSofSaving": {
"employerName": "",
"jobTitle": "",
"annualIncome": "100000",
"docUrls": [
{ "fileName": "bank_statement.pdf", "filePath": "" }
]
}
}
```
**Response** (the envelope is always `{ code, message, messageDetail, data }`; on a successful submission **`data` is `null`**)
```json theme={null}
{
"code": "",
"message": "",
"messageDetail": {},
"data": null
}
```
***
## Flow diagram
> Requesting an on-chain deposit address before the Travel Rule upload is complete will be rejected. The `address` field set in the query response of `/account/v2/query-travel-rule` is not symmetrical with the upload request body (the query returns only `city`, `postalCode`, and `addressLines`), so code against the endpoint field specification.
***
## Next steps
* Full request and response fields and examples for upload and query: [Crypto Deposits](./crypto-deposit) (the Travel Rule is a prerequisite step for deposits; includes the `update-travel-rule` field table)
* Full parameters for the hosted page with `action=TRAVEL_RULE`: [H5 KYC / card application guidance page](../../integration-resources/h5-kyc-guidance) (for the 12 SoF/SoW sub-objects, see the "EDD file upload" section above)
* The overall compliance flow and where KYC sits in it: [Compliance Β· Overview](../../basic-concepts/compliance-kyc-flow)
* Obtaining an on-chain deposit address (the step downstream of the Travel Rule): [Crypto Deposits](./crypto-deposit)
* Full endpoint field specifications: `POST /account/v1/update-travel-rule`, `GET /account/v2/query-travel-rule`, `POST /redirect/v2/guidance-link`, `POST /account/v1/generate-file-upload-prepare`, `POST /account/v1/upload-edd-file`
# Crypto Withdrawals
Source: https://docs.thedecard.com/en/decard-managed/how-to-use/virtual-accounts/withdraw-offramp
The endpoint reference for on-chain crypto withdrawals under the DeCard-Managed model: confirm the chain, token, and withdrawal switch (network-coin) β submit the on-chain withdrawal (/crypto/v1/withdraw-apply, with SMS verification) β track crediting through the transaction records. Covers SMS verification, address tags, and fees.
## Overview
Crypto withdrawals let your users move the available balance (`availableBalance`) in their account held in custody by DCS **out over a blockchain network to an external on-chain address of their choice**. As a licensed issuer with its own BINs, DCS handles the balance deduction, SMS security check, on-chain transfer, and status tracking for you, turning "available balance β funds credited on-chain" into a single path.
This is the reverse of a deposit: **the user submits a withdrawal β the platform validates it (including the SMS code) and deducts the available balance β the funds are sent on-chain to the destination address β crediting is tracked through the transaction records**.
Three core steps:
1. Call `GET /crypto/v1/network-coin` to confirm that the target chain and token have **withdrawals enabled** (`withdrawEnable`), and read the withdrawal fee and limits (`withdrawFee` / `withdrawMin` / `withdrawMax`) plus whether an address tag is required (`withdrawIsTag`).
2. Have the user obtain an **SMS verification code** (`smsCode`, see [Prerequisites](#prerequisites)), then call `POST /crypto/v1/withdraw-apply` to submit the withdrawal.
3. Call `POST /user-asset/v1/transactions` to query crypto balance movements and follow the on-chain status and `txHash` of this withdrawal.
***
## Prerequisites
Confirm the following before calling this endpoint:
1. **The user is onboarded and has passed KYC**: the user has been assigned an `externalUserId`. See [Managing users](../managing-users/overview).
2. **Withdrawals are enabled for the target chain and token**: query `GET /crypto/v1/network-coin` first, confirm that `withdrawEnable = true` for that `network` plus `coin`, and use `withdrawIsTag` to decide whether `addressTag` is mandatory.
3. **The available balance is sufficient**: the user's `availableBalance` must be greater than or equal to the withdrawal amount plus the fee. For the balance model, see [Account and asset model Β· Ledgering](../../basic-concepts/ledgering-system).
4. **SMS verification is complete**: a withdrawal moves money out, so it is treated as high risk and requires an SMS verification code `smsCode` (issued via `POST /captcha/v1/send-mobile-code`).
5. **Compliance groundwork**: where the Travel Rule applies, its requirements must be satisfied first. See [Travel Rule](./travel-rule).
## API flow
## Submitting a withdrawal
**`POST /crypto/v1/withdraw-apply`**
### Request fields
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `externalUserId` | string | Yes | External user ID |
| `smsCode` | string | Yes | SMS verification code, issued via `POST /captcha/v1/send-mobile-code` |
| `network` | string | Yes | Withdrawal chain, for example `TRON` or `ETH` (accepted values follow the `network-coin` response) |
| `coin` | string | Yes | Withdrawal token, for example `USDT` or `USDC` (accepted values follow the `network-coin` response) |
| `name` | string | No | Label for the withdrawal address or beneficiary name (used for address notes and whitelists) |
| `address` | string | Yes | Destination on-chain withdrawal address |
| `addressTag` | string | Conditional | Address tag or memo; required when `withdrawIsTag = true` in `network-coin` |
| `amount` | number | Yes | Withdrawal amount (must be greater than 0; limits follow `withdrawMin` / `withdrawMax` in `network-coin`) |
| `fee` | number | Yes | Withdrawal fee (follows `withdrawFee` in `network-coin`) |
> **Amount precision**: `amount` and `fee` in the request body are numbers. When storing and displaying them, mind the token's on-chain precision so that no precision is lost.
### Request example (redacted)
```bash theme={null}
curl -X POST "{{dicard-server}}/crypto/v1/withdraw-apply" \
-H "Content-Type: application/json" \
-d '{
"externalUserId": "user_demo_001",
"smsCode": "000000",
"network": "TRON",
"coin": "USDT",
"name": "my-wallet",
"address": "T-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"addressTag": "",
"amount": 100.0,
"fee": 1.0
}'
```
> **Authentication**: the example above omits the authentication headers to keep the focus on business fields. Real calls must carry `X-DAPI-API-KEY`, `X-DAPI-SIGN`, `X-DAPI-TIMESTAMP`, and `X-DAPI-NONCE` (HMAC-SHA256 signature). The rules are documented once in the [Authentication guide](../../integration-resources/overview) and are not repeated here.
All example values are placeholders. **Never** put a real on-chain address, `externalUserId`, `smsCode`, amount, cardholder personal data, or API key/secret into a request or a log. On-chain addresses and user identifiers are sensitive and must be redacted.
### Response
The response uses the standard envelope `{ code, message, messageDetail, data }` (**no `success` boolean**). On success, `code = SYS_SUCCESS`, meaning the withdrawal request has been accepted.
> Base your business logic on the actual status in the transaction records. Do not treat `code = SYS_SUCCESS` in the envelope as proof that the withdrawal has been credited; `code` only confirms that the request was accepted, and the on-chain transfer happens asynchronously.
## Tracking withdrawal progress
Once an on-chain withdrawal is accepted, track it with **`POST /user-asset/v1/transactions`** (query crypto balance movements) to read the on-chain status and the transaction hash `txHash`; for a single record, use `POST /user-asset/v1/transaction-detail`. For field definitions, see [User Balance](../managing-transactions/user-balance) and [Reporting field descriptions](../managing-transactions/reporting-field-descriptions).
## Status and time to credit
* **Acceptance is not the same as crediting**: `code = SYS_SUCCESS` in the envelope only means DCS accepted the request and deducted the balance; the funds are only really at the destination once confirmed on-chain.
* **Time to credit depends on on-chain confirmation speed**: block times and confirmation counts differ by chain, so rely on the confirmation configuration in `network-coin` and the actual state of the target network.
## Error handling
| Situation | Recommended handling |
| :- | :- |
| Envelope `code` is not `SYS_SUCCESS` | The request was not accepted: use `message` to check the inputs (whether `smsCode` is valid or expired, whether withdrawals are enabled for `network`/`coin`, whether `address`/`addressTag` are valid, whether `amount` is within the limits, and whether the balance is sufficient) |
| SMS code invalid or expired | Have the user request a new `smsCode` (`send-mobile-code`) and retry |
| Withdrawals disabled for the chain or token | `network-coin` returns `withdrawEnable = false`: tell the user withdrawals are unavailable and offer a chain or token that is enabled |
| Tag required but missing | Chains with `withdrawIsTag = true` must carry `addressTag`; otherwise the transfer may fail to credit or the funds may be lost |
| Insufficient balance | Available balance is less than the withdrawal amount plus the fee: prompt the user to deposit more or lower the amount |
## Best practices
1. **Query `network-coin` before every withdrawal**: drive your front-end validation from `withdrawEnable`, `withdrawFee`, `withdrawMin`, `withdrawMax`, and `withdrawIsTag` to cut down invalid requests and failed credits.
2. **Enforce address and tag validation**: validate the address format and tag against the chain's rules while the user is still filling the form, and make the tag mandatory when `withdrawIsTag = true`. A wrong address or a missing tag can make the funds unrecoverable.
3. **Never skip SMS verification**: withdrawals move money out, so always require the second factor via `smsCode`.
4. **Judge crediting by the on-chain status**: acceptance is not crediting. Before telling the user the funds have arrived, check the on-chain confirmation status in the transaction records.
5. **Minimise PII**: on-chain addresses and user identifiers are sensitive. Redact them in logs and retain no more than compliance requires.
## Next steps
* [Crypto Deposits](./crypto-deposit) β the opposite direction (on-chain deposit into the available balance), sharing the `network-coin` chain and token matrix with this page
* [Travel Rule](./travel-rule) β the compliance groundwork that withdrawals depend on
* [User Balance](../managing-transactions/user-balance) β semantics of the `free` / `freeze` / `total` balances and how to query balance movements
* [Account and asset model Β· Ledgering](../../basic-concepts/ledgering-system) β the `availableBalance` / `frozenBalance` model
* [Authentication guide](../../integration-resources/overview) β the shared reference for HMAC-SHA256 signature headers
# H5 KYC / Card Application Guidance Page
Source: https://docs.thedecard.com/en/decard-managed/integration-resources/h5-kyc-guidance
DCS delivers a number of end-user interactions (KYC, card details, applying for and activating a physical card, updating a PIN, Travel Rule and more) as hosted H5 pages. This page explains how to issue a guidance link that sends an end user from your app or website on a temporary detour to a DCS-hosted page to complete the task.
Many sensitive actions (KYC selfie capture, viewing the full card number, setting or resetting a PIN) **should not** pass through your backend at all; they are completed on the front end by a DCS-hosted page. That is exactly what the hosted guidance page mechanism is for:
1. Your backend calls the **issuing API** (`/redirect/v{1,2}/guidance-link`) with the page type and the end user identifier;
2. DCS returns a **guidance link carrying a one-time secret** (in the response `data` field);
3. You hand that link to your front end to open, either as a direct redirect, a new window, or embedded in a WebView or IFrame (for integration conventions see [Web SDK / Front-end Integration](../sdk/web-sdk));
4. When the DCS page opens, it uses the secret in the link to look up the context ([Validating the guidance link](#3-validating-the-guidance-link)) and renders the page once the link is confirmed legitimate;
5. Once the user finishes or fails, the DCS page redirects back to your site using the `successRedirectUrl` / `errorRedirectUrl` you supplied.
This page covers only the **client-facing** endpoints: `guidance-link` and `public/secret-validate`. Paths under `internal` or `private`, such as `/internal/redirect/v1/h5` and `/redirect/private/v1/h5`, are internal DCS service-to-service APIs, **not client-facing APIs you can call**, and are not described here.
***
## 1. Issuing a guidance link
The guidance-link endpoint is:
| Endpoint | Card identifier field |
| :- | :- |
| `POST /redirect/v2/guidance-link` | `cardId` (unique card ID) |
> The card is identified exactly by `cardId` rather than by the last four digits of the card number, which are not guaranteed to be unique across the cards of a single user.
### Request headers
| Header | Required | Description |
| :- | :- | :- |
| `Content-Type` | REQUIRED | Fixed to `application/json` |
> This API is also subject to DCS authentication and IP whitelisting; for the header signing rules see the [Authentication Guide](./overview).
### Request fields
| Field | Type | Required | Constraints / values | Description |
| :- | :- | :- | :- | :- |
| `action` | string | REQUIRED | Enum, see [Guidance page types (`action` enum)](#guidance-page-types-action-enum) | Type of guidance page |
| `externalUserId` | string | REQUIRED | β€50 | User ID |
| `cardId` | string | Conditional | β | Unique card ID, required for card-related actions |
| `language` | string | β | β€10, see [Languages (`language` enum)](#languages-language-enum) | Language of the guidance page for the client-facing `guidance-link` |
| `theme` | string | β | β€10; the official description says it "cannot be empty", so always send a value, e.g. `blue` | Theme |
| `mode` | string | β | β€10; the official description says it "cannot be empty", so always send a value, `dark` / `light` | Light or dark mode |
| `primaryColor` | string | β | β€7, hex starting with `#`, e.g. `#FFFFFF` | Primary colour |
| `successRedirectUrl` | string | REQUIRED | β€300 | Redirect URL on success. Consider appending your own correlation parameters (an order number, for example) so you can resume the flow when the user comes back |
| `errorRedirectUrl` | string | REQUIRED | β€300 | Redirect URL on failure. Consider appending your own correlation parameters so you can resume the flow when the user comes back |
| `referer` | string | β | β€300 | Referring origin, used for security checks |
| `userAgent` | string | β | β€300 | User agent string, used for security checks |
| `selectCardPageShow` | string | β | β€1, `0`=do not skip / `1`=skip | Whether to show the card selection page when the user has only one card |
| `nationality` | string | β | Two-letter uppercase country code (may be empty or `null`), e.g. `SG` | Nationality. **Only takes effect when `action=KYC_GUIDE`** |
| `selectNationalityShow` | string | β | β€1, `0`=do not skip / `1`=skip | Whether to show the nationality selection page. **Only takes effect when `action=KYC_GUIDE`** |
| `applyId` | string | Conditional (KYC\_EXTRA\_DOC) | β€300; required when `action=KYC_EXTRA_DOC` | The related card application ID |
The type of guidance page is controlled by `action`, and the accepted language values are listed below. Treat the fields on this page as authoritative: `otpStatus`, `kycTicketId`, `CN/EN` and similar parameters do not belong to this API.
#### Guidance page types (`action` enum)
Seven values are accepted:
| `action` | Meaning |
| :- | :- |
| `KYC_GUIDE` | KYC guidance page |
| `CARD_INFO` | Card information (view card details) |
| `CREATE_PHYSICAL_CARD` | Apply for a physical card |
| `ACTIVE_PHYSICAL_CARD` | Activate a physical card |
| `UPDATE_PIN` | Update the PIN |
| `TRAVEL_RULE` | Update the user's Travel Rule information |
| `KYC_EXTRA_DOC` | Supply additional KYC information |
#### Languages (`language` enum)
`language` takes **lowercase, hyphenated** values and is **case-sensitive**, with seven accepted values (the same convention as the [Quickstart](../getting-started/quickstart)). Any value outside this whitelist is silently downgraded to the default language, with no error returned:
| `language` | Language |
| :- | :- |
| `zh` | Chinese (Simplified) |
| `en` | English |
| `ko` | Korean |
| `ja` | Japanese |
| `zh-Hant` | Chinese (Traditional) |
| `th` | Thai |
| `vi` | Vietnamese |
### Request example (v2, masked)
```bash theme={null}
curl --location 'https://{dicard-server}/redirect/v2/guidance-link' \
--header 'Content-Type: application/json' \
--data '{
"action": "KYC_GUIDE",
"externalUserId": "ext_user_********",
"cardId": "card_********",
"language": "en",
"theme": "blue",
"mode": "dark",
"primaryColor": "#FFFFFF",
"successRedirectUrl": "https://your-app.example.com/kyc/success",
"errorRedirectUrl": "https://your-app.example.com/kyc/error",
"referer": "https://your-app.example.com",
"selectCardPageShow": "0",
"nationality": "SG",
"selectNationalityShow": "1",
"applyId": "apply_********"
}'
```
### Response
Every guidance API returns the common response envelope. **The issued guidance link comes back in `data` (a string), not in a top-level `linkUrl`.**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": {
"message": null,
"title": null,
"type": null,
"icon": null,
"action": null,
"linkTitle": null,
"linkUrl": null
},
"data": "https://{h5-host}/guidance?secret="
}
```
| Field | Type | Description |
| :- | :- | :- |
| `code` | string | Business status code; `SYS_SUCCESS` on success, and this is what you check to decide success |
| `message` | string | Short message, usually `null` on success |
| `messageDetail` | object | Detailed prompt object (`message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`), whose fields are usually all `null` on success |
| `data` | string | **The generated guidance page link**, which you hand to your front end to open |
**When `messageDetail` is populated**: on a normal success its fields are usually all `null`. It is filled in when the flow needs to show an extra prompt to you or to the end user, such as guidance copy for a redirect or a retry link. `type` then identifies the message category, and `linkTitle` / `linkUrl` provide a clickable link.
The link in `data` carries a one-time `secret`. Read [Validating the guidance link](#3-validating-the-guidance-link) to understand its lifecycle, and **never expose the full link in logs or screenshots**.
***
## 2. Redirecting back to your site
Once the user finishes, abandons or fails on the DCS-hosted page, DCS redirects to the address you supplied when the link was issued:
* Success β `successRedirectUrl`
* Failure or cancellation β `errorRedirectUrl`
> Both URLs are limited to 300 characters. Consider carrying your own correlation parameters (an order number, for example) on the URL so you can resume the business flow when the user returns, but never put sensitive parameters there in clear text.
***
## 3. Validating the guidance link
The one-time `secret` inside a guidance link is looked up **by the DCS-hosted page when the page opens**, both to confirm that the link is legitimate and to retrieve the user context bound to it. This is a **client-facing public endpoint**:
```
GET /redirect/public/v1/secret-validate?secret=
```
| Parameter | Required | Description |
| :- | :- | :- |
| `secret` | REQUIRED | The one-time security code carried in the guidance link |
**Response (masked example)**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": { },
"data": {
"userId": 0,
"externalUserId": "ext_user_********",
"mobileCode": "+65",
"mobile": "+65*****678",
"email": "u***@example.com",
"channel": "********",
"allowAccessDecard": false,
"successRedirectUrl": "https://your-app.example.com/kyc/success",
"errorRedirectUrl": "https://your-app.example.com/kyc/error",
"referer": "https://your-app.example.com",
"userAgent": "********",
"cardId": 0,
"applyId": "apply_********",
"params": {
"key": ""
}
}
}
```
| `data` field | Description |
| :- | :- |
| `userId` | Internal DCS user ID |
| `externalUserId` | User ID on the partner side |
| `mobileCode` / `mobile` | End user's dialling code and mobile number |
| `email` | End user's email address |
| `channel` | Channel identifier |
| `allowAccessDecard` | Whether access to DeCard is allowed |
| `successRedirectUrl` / `errorRedirectUrl` | The redirect addresses supplied when the link was issued |
| `referer` / `userAgent` | The security-check values supplied when the link was issued |
| `cardId` / `applyId` | The related card and card application |
| `params` | Additional parameters |
In the example `userId` and `cardId` are `0`, the default value for a number. The real values are internal DCS numeric IDs and are never 0; the example only illustrates the structure after masking.
**PII red line**: this response contains genuine personal data about the end user, including `mobile` / `mobileCode` / `email` / `externalUserId`. Every value shown above is a masked placeholder. During integration and operations it is **strictly forbidden** to keep real values in logs, tickets or documents shared outside your organization. Validation is normally initiated by the DCS-hosted page itself, so you rarely need to call this endpoint directly.
***
## 4. Error handling
When `guidance-link` fails, the response envelope returns a `code` other than `SYS_SUCCESS` together with a `message` describing the error. Common failure scenarios:
| Error scenario | Common cause |
| :- | :- |
| Parameter validation failed | `action` is outside the enum, a required field is missing (any of `action` / `externalUserId` / `successRedirectUrl` / `errorRedirectUrl` is empty), or `cardId` was omitted on a card-related action |
| User or card not found | The `externalUserId` or `cardId` does not exist in the DCS system |
| Secret expired or invalid | The `secret` in the guidance link has timed out or has already been used (`secret-validate` returns a non-success code) |
| Authentication failed | `Content-Type` is missing, or the signature is wrong (see the [Authentication Guide](./overview)) |
**Error response example (masked)**
```json theme={null}
{
"code": "PARAM_INVALID",
"message": "Parameter validation failed: successRedirectUrl must not be empty",
"messageDetail": {
"message": null,
"title": null,
"type": null,
"icon": null,
"action": null,
"linkTitle": null,
"linkUrl": null
},
"data": null
}
```
The `code` value `PARAM_INVALID` above is illustrative. In a live integration, always work from the `code` and `message` actually returned.
***
## Language codes are defined by this page
Use only the language codes listed on this page for the client-facing `guidance-link` (`zh` / `en` / `ko` / `ja` / `zh-Hant` / `th` / `vi` β lowercase, hyphenated, case-sensitive, maximum length 10). Any value outside this whitelist is silently downgraded to the default language.
***
## Next steps / Related
* How to embed the guidance link in your front end and communicate with it once you have the URL: [Web SDK / Front-end Integration](../sdk/web-sdk).
* Authentication has to work before you can call this API: [Authentication Guide](./overview).
* Sumsub / POA document requirements: [Compliance Β· KYC Documents](../customer-success/kyc-documents).
# IP Whitelisting
Source: https://docs.thedecard.com/en/decard-managed/integration-resources/ip-whitelisting
This is the network configuration page of Integration Resources. It picks up the "provide your IP whitelist" step from First Steps and sets out which egress IPs a partner has to register with DCS, what each one is used for, and how sandbox is kept apart from production. Request headers and the signing algorithm are covered in the Authentication Guide, and the secure retrieval flow for credentials is covered in First Steps.
**Direction of the whitelist**: in the DeCard-Managed model the whitelist is supplied by the **partner** and contains the **partner's own** network egress addresses, which DCS then allows to call its APIs. This page lists **no** DCS-owned IPs.
***
### 1. Why IP whitelisting is required
DCS enforces IP whitelisting on API calls: only requests coming from a network egress address that the partner has **registered in advance** are let through. This is a network-level admission check on top of API Key / Secret Key signature authentication, and it confines the set of possible callers to trusted network ranges.
Register your **network egress IP** with DCS before you call any API. If it is missing or incorrect, the request is rejected before it reaches any business logic (see [Troubleshooting](#4-troubleshooting)).
### 2. The two kinds of IP to register
In the DeCard-Managed model (dedicated-account model), **authorization decisions are made inside DCS**. There is no leg on which DCS forwards an authorization back to the partner, so there is **no whitelist for an authorization-forwarding direction**. What you do need to register with DCS are the following two egress IPs, which serve different purposes:
| Purpose | Supplied by | Direction | Description |
| :- | :- | :- | :- |
| **API call whitelist** | Partner | Partner β DCS | The network egress address from which you call the DeCard-Managed APIs. Only source IPs on this list can call the APIs successfully. |
| **Production credential retrieval IP** | Partner | Partner β DCS | The IP of the request that performs the **one-time retrieval of the API Key / Secret Key** in production. This IP is added to the "credential retrieval" whitelist, and the temporary link sent by email can only be used from that machine. |
**The two addresses serve different purposes, so register them separately.** The egress IP used for **day-to-day API calls** and the request IP used for the **one-time production credential retrieval** **do not have to be the same**; state what each address is for when you submit them to DCS. The full retrieval flow is described in the "Secure Retrieval of Production Credentials" section of [First Steps](../getting-started/first-steps).
#### About the Webhook direction
DCS pushes events to you over Webhook: KYC status, asset movements, card transactions, order status and so on. The integration requirement for Webhook is that you expose an **HTTPS callback URL reachable from the public internet** and verify the authenticity of every call with a digital signature (see [Webhook + WebSocket Notifications](./webhook-websocket)).
If your Webhook receiver needs IP-level inbound restrictions, contact the DCS team to confirm its outbound IP ranges. This page **lists no DCS IPs**.
### 3. Environment separation (sandbox and production, never mixed)
DCS keeps the **sandbox** and **production** environments apart, and their whitelists are **independent of each other**. Provide the network egress addresses for the two environments **separately** and never mix them:
| Environment | API call whitelist | Production credential retrieval IP | How to submit |
| :- | :- | :- | :- |
| **Sandbox** | Provide your sandbox egress IP | Not applicable (sandbox credentials come directly from the DCS team) | Contact the DCS team |
| **Production** | Provide your production egress IP | Provide the request IP for the one-time production credential retrieval | Submit through your commercial contact or the DCS team |
**What to submit (placeholder example, replace with your own details)**:
```text theme={null}
# API call egress IP
Sandbox egress IP:
Production egress IP:
# One-time production credential retrieval (production only)
Secure email address: # receives the credential retrieval email
Credential retrieval request IP: # IP of the machine on the "credential retrieval" whitelist
```
> How to submit: for the sandbox environment, contact the DCS team directly; for production, submit through your commercial contact or the DCS team and state the purpose of each IP.
### 4. Troubleshooting
Common symptoms when a whitelist entry is missing or wrong, and how to work through them:
| Symptom | Likely cause | What to do |
| :- | :- | :- |
| API request rejected (a network-layer rejection, separate from authentication) | The egress IP actually used is not on the API call whitelist (machines scaled out, egress gateway changed, NAT egress IP drift) | Confirm your current real egress IP and submit it to DCS so it can be added to the whitelist |
| Connection blocked by a firewall or timing out | Your own firewall restricts outbound connections to DCS | Allow outbound access to DCS on your side |
| The production credential retrieval link fails to run | The IP of the machine running it does not match the registered "credential retrieval request IP", or the link has already been used (it is valid once only) | Run it on the machine you registered; if the link is spent, arrange a new email through your commercial contact |
| Works in sandbox but rejected in production | Sandbox and production whitelists were not configured separately (the same set was reused) | Register a production egress IP of its own |
> When tracking down your egress IP, use the **public egress IP of the machine or gateway that actually sends the request**, not a private network address.
***
## Next steps / Related
* Request headers and the signing algorithm: [Authentication Guide](./overview)
* Receiving and securely retrieving credentials: [First Steps](../getting-started/first-steps)
* Integration requirements for event push: [Webhook + WebSocket Notifications](./webhook-websocket)
# Authentication Guide
Source: https://docs.thedecard.com/en/decard-managed/integration-resources/overview
This is the entry point for Integration Resources and the authoritative page on authentication: it covers API Key / Secret Key management and HMAC-SHA256 request signing end to end. Standalone capabilities such as IP whitelisting and the SessionId public key are covered in the sub-pages of this group.
Whether you are an exchange, a wallet or a platform, once DCS has issued you a key pair you can call every **DeCard-Managed** API with a single, uniform signing rule. This page gets authentication working in one pass. As a licensed issuer with its own BINs, DCS requires a verifiable request origin and replay protection on every API call.
Naming note: this documentation set covers the **DeCard-Managed** integration model, previously called the standard-authorization model. In this model DCS maintains a dedicated fiat / digital-asset account and balance for each user, and **no on-chain collateral is involved**.
***
## What is in this group
Integration Resources brings together every technical asset and security credential you need, from integration development through to production go-live:
| Sub-page | Purpose |
| :- | :- |
| **Authentication Guide (this page)** | API Key / Secret Key management and HMAC-SHA256 request signing |
| **[IP Whitelisting](./ip-whitelisting)** | Register your network egress addresses with DCS, both for API access and for secure retrieval of production credentials |
| **[SessionId Public Key / Encryption](./sessionid-keys)** | The encryption and signing capabilities actually available in the DeCard-Managed model |
| **[H5 KYC / Card Application Journey](./h5-kyc-guidance)** | The embedded H5 flow for KYC and card application |
| **[Web SDK Integration](../sdk/web-sdk)** | Front-end integration SDK |
| **[Webhook + WebSocket Notifications](./webhook-websocket)** | Event callbacks (Webhook) and **real-time WebSocket push** (a DeCard-Managed differentiator) |
## 1. Credentials and Request Headers
### What DCS gives you
| Field | Description | Provided by |
| :- | :- | :- |
| `apiKey` | Globally unique identifier for API calls, used for identification and request-origin tracing | DCS |
| `secretKey` | The key used to compute the request signature. **Keep it safe and never disclose it to any third party** | DCS |
The `apiKey` is a globally unique identifier that supports identification and analytics. To stop anyone else from issuing requests under your `apiKey`, it must be paired with the `secretKey`: you generate a signature according to the agreed rules and submit it alongside the request for DCS to verify. DCS delivers this key pair to you privately, and every API call a partner makes must follow the agreed signing protocol.
> For sandbox `apiKey`/`secretKey`, contact the DCS team. Production credentials must be obtained through [Secure Retrieval of Production Credentials](#3-secure-retrieval-of-production-credentials).
### Headers required on every request
Every API call from a partner must carry the following HTTP headers:
| Header | Required | Description |
| :- | :- | :- |
| `Content-Type` | REQUIRED | Fixed to `application/json` when the request body is JSON. If absent, DCS returns HTTP 400 with business code `SYS_ILLEGAL_PARAM` |
| `X-DAPI-API-KEY` | REQUIRED | The `apiKey` issued by DCS |
| `X-DAPI-TIMESTAMP` | REQUIRED | Request timestamp in milliseconds, used for replay protection |
| `X-DAPI-NONCE` | REQUIRED | Random number in the range `[10000, 99999]`, making each request single-use |
| `X-DAPI-SIGN` | REQUIRED | The HMAC-SHA256 signature computed as described below (lowercase hexadecimal) |
the `secretKey` is used only to compute the signature locally. **Never** transmit it in a request header.
Typical response when `Content-Type: application/json` is missing:
```json theme={null}
{"code":"SYS_ILLEGAL_PARAM","message":"illegal param","messageDetail":null,"data":null,"success":false}
```
> This `success=false` comes from the framework-level error response triggered by the missing Content-Type; it does not mean that every business response reliably includes `success`. In normal integration, always decide success or failure from `code` and do not depend on the `success` field.
***
## 2. Request Signing (HMAC-SHA256)
### Signing rule
Sign the concatenated string using the **HmacSHA256** algorithm with the `secretKey` as the key:
```
X-DAPI-SIGN = HmacSHA256( apiKey + timestamp + nonce + payload , secretKey )
```
The output is a lowercase hexadecimal string.
**The segments of the concatenated string:**
| Segment | Value |
| :- | :- |
| `apiKey` | Same as `X-DAPI-API-KEY` |
| `timestamp` | Same as `X-DAPI-TIMESTAMP` (millisecond timestamp) |
| `nonce` | Same as `X-DAPI-NONCE` |
| `payload` | **GET** requests: the URL-encoded query string; **all other methods**: the raw request body |
```text theme={null}
if method is GET:
payload = url.encodedQuery() // e.g. externalUserId=&cardMantissa=1670
else:
payload = body // the raw JSON request body string
```
> The signing algorithm is fixed to HmacSHA256. Always use this algorithm.
### Replay protection (DCS verifies, the partner generates)
* **TIMESTAMP**: must be a **13-digit millisecond timestamp** (for example `Date.now()`, not a 10-digit second timestamp). A malformed value returns `DAPI_TIMESTAMP_FORMAT_ERROR`. DCS only accepts requests within a **5-second validity window**; beyond that it returns `DAPI_TIMESTAMP_EXPIRED`, so regenerate the timestamp from the current time, recompute the signature, and keep your local clock in sync.
* **NONCE**: generate a fresh random number in `[10000, 99999]` for every request so that each request is valid only once. Do not reuse nonces.
> If authentication fails, check the timestamp, the nonce and the string you signed first. For exact error codes, rely on what the API actually returns and on the dictionary DCS provides for this product.
### Signing examples
**JavaScript (Postman pre-request script)**
```javascript theme={null}
const CryptoJS = require('crypto-js');
const apiSecret = '';
const apiKey = '';
// GET uses the query string; other methods use the request body. Concatenate whichever applies
const queryParams = ''; // e.g. externalUserId=&cardMantissa=1670
const requestBody = ''; // the raw body for POST/PUT
const nonce = 10010;
const timestamp = Date.now().toString();
const dataToSign = apiKey + timestamp + nonce + queryParams + requestBody;
const signature = CryptoJS.HmacSHA256(dataToSign, apiSecret).toString(CryptoJS.enc.Hex);
```
**Java**
```java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
public final class HmacSignature {
private static final String HMAC_SHA256 = "HmacSHA256";
public static String getSignature(String apiSecret, String data) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(apiSecret.getBytes(), HMAC_SHA256);
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(keySpec);
byte[] hmac = mac.doFinal(data.getBytes());
return Hex.encodeHexString(hmac);
}
}
```
### Full request example (GET)
```bash theme={null}
curl --location 'https://{domain}/card/v3/detail?externalUserId=&cardId=' \
--header 'Content-Type: application/json' \
--header 'X-DAPI-API-KEY: ' \
--header 'X-DAPI-SIGN: ' \
--header 'X-DAPI-TIMESTAMP: ' \
--header 'X-DAPI-NONCE: '
```
> In this example `payload = externalUserId=&cardId=`, the GET query string. To validate your implementation before go-live, take the same `apiKey/timestamp/nonce/payload` set that DCS provides and confirm you reproduce the identical `X-DAPI-SIGN`.
Every `externalUserId`, `apiKey`, `X-DAPI-SIGN`, `TIMESTAMP` and `NONCE` in the examples is a placeholder and must not be treated as a real value. The DeCard-Managed model handles a large volume of end-user personal data, so debug logs must not record real credentials or PII either.
### Common response envelope
Every API returns the same envelope:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "",
"messageDetail": {
"message": "",
"title": "",
"type": "",
"icon": "",
"action": "",
"linkTitle": "",
"linkUrl": ""
},
"data": ""
}
```
| Field | Type | Description |
| :- | :- | :- |
| `code` | string | Business status code. `SYS_SUCCESS` on success, a specific error code on failure |
| `message` | string | Short message, normally empty on success |
| `messageDetail` | object | Structured display object containing `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`, usable for front-end prompts. Its fields are normally empty on success. **Do not use it to decide success or failure**; always judge the outcome from `code` |
| `data` | object | The business payload |
> Treat `code == "SYS_SUCCESS"` as the sole success condition. The response envelope does **not** include a `success` boolean, so do not depend on that field.
***
## 3. Secure Retrieval of Production Credentials
To keep production `apiKey`/`secretKey` from leaking in transit, **production credentials are never sent to you directly**. They are collected through a one-time secure retrieval flow.
> The sandbox environment does not need this flow; simply contact the DCS team.
### The flow (and who does what)
1. **The partner provides**: one secure email address plus one source IP for retrieval.
* The email address receives the retrieval instructions; the IP is added to the credential-retrieval whitelist.
2. **DCS sends**: an email to that secure address containing a temporary secure link that is valid for **one use only**.
3. **The partner retrieves**: concatenate `extractUrl` with `extractSecretKey` and **run it from the machine at the registered IP** to receive the `apiKey`/`secretKey`.
### Fields in the email
| Field | Type | Description |
| :- | :- | :- |
| `expireTime` | string | Validity period of the retrieval security code |
| `extractSecretKey` | string | Credential-retrieval security code |
| `extractUrl` | string | Credential-retrieval URL |
| `howToUse` | string | Usage instructions |
| `notes` | string | Reminder that the link can be used for one retrieval only |
**Success response**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": {
"expireTime": "2024-10-21T16:52+08:00[Asia/Shanghai]",
"extractSecretKey": "",
"extractUrl": "https://{domain}/internal/open-api/v1/secret-extract/",
"howToUse": "Please concatenate the url with the secret-key and execute it on the specified machine.",
"notes": "This link is only valid for one AKSK extraction, if the content is not properly accessed, the AKSK may have been compromised, please contact us promptly."
}
}
```
**Failure response**
```json theme={null}
{
"code": "ERROR-CODE",
"message": "simple describe, see error-code list",
"messageDetail": null,
"data": null
}
```
> Only the failure envelope is shown here. For exact error codes, rely on what the API actually returns and on the dictionary DCS provides for this product; this version does not reuse error-code tables from other products.
**If retrieval fails**: the link may already have been used inside its validity window, which means the credentials could be compromised. Contact your DCS business contact immediately and repeat the email flow to have them reissued.
The retrieval IP does not have to be the same as your day-to-day API calling IP; declare each purpose to DCS separately. For how to submit them, see [IP Whitelisting](./ip-whitelisting).
***
## Next steps / Related
With authentication working, go to [Quickstart](../getting-started/quickstart) to issue your first card, or start from [First Steps](../getting-started/first-steps) to confirm that your account and callbacks are ready.
The remaining technical assets in Integration Resources:
* **[IP Whitelisting](./ip-whitelisting)**: register your network egress addresses with DCS, both for API access and for secure retrieval of production credentials.
* **[SessionId Public Key / Encryption](./sessionid-keys)**: the encryption and signing capabilities actually available in the DeCard-Managed model.
* **[H5 KYC / Card Application Journey](./h5-kyc-guidance)**: the embedded flow for KYC and card application.
* **[Web SDK Integration](../sdk/web-sdk)**: front-end integration SDK.
* **[Webhook + WebSocket Notifications](./webhook-websocket)**: event callbacks (Webhook) and **real-time WebSocket push** (a DeCard-Managed differentiator).
# SessionId Public Key / Encryption
Source: https://docs.thedecard.com/en/decard-managed/integration-resources/sessionid-keys
This is the key and encryption summary page of Integration Resources. It builds on the signing scheme in the Authentication Guide and the event signature verification in Webhook + WebSocket Notifications, and states factually which encryption and signing capabilities the DeCard-Managed model actually provides.
**The DeCard-Managed model publishes no RSA public key and has no `SessionId` request header**: it has **no** `SessionId` header, **no** published RSA asymmetric public key and **no** JWT-style session token. This page lists **no** public keys, because none are published. Sensitive data such as encrypted card details and PINs is not obtained by "the partner encrypting a session token with a public key and exchanging it for ciphertext"; it is confined to a DCS-hosted H5 page instead (see below).
***
### 1. How the DeCard-Managed model protects sensitive data
For sensitive operations such as viewing encrypted card details and handling PINs, the DeCard-Managed model applies the following security policy:
| Aspect | DeCard-Managed |
| :- | :- |
| **Key scheme** | Symmetric: HMAC-SHA256 (API Key + Secret Key) |
| **Session token** | No `SessionId`. The hosted guidance page uses a one-time secret token generated by DCS (see below) |
| **Display of sensitive card data** | **Clear text never passes through the partner's backend**. Card number, CVV and expiry date are rendered to the end user directly by a DCS-hosted H5 page (see [Viewing Encrypted Card Details](../how-to-use/managing-cards/viewing-encrypted-card-details)) |
| **PIN set / reset** | Completed on a DCS-hosted H5 page (see [Managing a Card's PIN](../how-to-use/managing-cards/managing-a-cards-pin)) |
| **Published public key** | **None published**. There is no public key infrastructure in this model |
**Conclusion**: the DeCard-Managed model confines sensitive data **to DCS-hosted H5 pages, which architecturally removes any path where the partner handles encrypted card data**. The partner does not have to hold a decryption key or mint a session token, yet can still show the card number, CVV and PIN safely to end users on clients with no PCI scope.
### 2. Encryption and signing capabilities available
The DeCard-Managed model offers the capabilities below. Each is documented in full on its own page; this page is the index that helps you find the right one quickly.
#### 2.1 API request signing: HMAC-SHA256
Every call to a DCS API must carry an `X-DAPI-SIGN` signature in the request header. The signature is computed with HMAC-SHA256 over the concatenated string (`apiKey + timestamp + nonce + payload`), keyed with the `SecretKey` DCS issued to you.
> See [Authentication Guide Β· Request Signing](./overview#2-request-signing-hmac-sha256) for details.
#### 2.2 Webhook signature verification: HMAC-SHA256 (X-Signature)
When DCS pushes a Webhook to you, it carries `X-Signature` in the HTTP headers. That signature is also HMAC-SHA256, keyed with your `SecretKey` and computed over the raw message body. Recompute the signature on receipt and compare it, and you have verified both the authenticity of the sender and the integrity of the message.
> See [Webhook + WebSocket Notifications Β· X-Signature verification](./webhook-websocket#1-4-security-x-signature-verification) for details.
#### 2.3 Field-level AES encryption: mobile number
The `mobile` field of the customer registration API `POST /account/v1/register` **supports AES-encrypted transmission** (clear text is also accepted). The API definition annotates it as:
```json theme={null}
{
"mobile": "" // Mobile number (AES-encrypted or clear text), example: 18232339999 (no country code)
}
```
This lets a partner encrypt the mobile number with AES before transmission and so reduce the risk of exposure in transit. The official DCS example uses `SecureUtil.aes(Base64.decode(SECRET)).encryptBase64(mobile)`: build the AES key from the Base64-decoded Secret Key and emit the ciphertext as Base64. The same rule applies to email addresses. The example passes no separate IV parameter, so stay consistent with the official implementation and do not substitute your own AES-GCM or AES-CBC message format. For the exact request and response format of `/account/v1/register`, see [Signing Up a Customer](../how-to-use/signing-up-a-customer/overview).
#### 2.4 One-time secret token for the hosted guidance page
DCS-hosted H5 pages (KYC, viewing card details, activating a physical card, setting a PIN and so on) are authenticated with a **one-time secret token**: you call the issuing API to obtain a guidance link that carries a one-time secret, the end user opens that link, and the DCS page validates it with `GET /redirect/public/v1/secret-validate?secret=β¦`. The secret is generated by DCS and is valid for a single use, expiring once consumed or timed out.
This is a **server-issued, single-use** token model: DCS mints and delivers the token, and DCS validates and consumes it when the end user opens the link. The partner never has to generate or encrypt a session token on the client.
> See [H5 KYC / Card Application Guidance Page](./h5-kyc-guidance) for details.
#### 2.5 Apple Pay / Google Pay provisioning encryption
In the push provisioning flow, the DeCard-Managed model involves encrypted Apple Pay fields such as `encryptedPassData` (encrypted PAN, expiry, date and time, Base64), `activationData` (encrypted nonce + nonceSignature + authCode, Base64) and `ephemeralPublicKey` (ECC P-256 curve, uncompressed format, Base64). These fields are encrypted by the Apple and Google payment tokenization infrastructure, and **the keys are managed by Apple, Google and the card networks. They are not part of any public key scheme published by DCS to partners**.
> See [Push Provisioning (Apple / Google Pay)](../how-to-use/managing-cards/push-provisioning) for details.
### 3. Capability checklist
| Requirement | Available? | Mechanism / where to look |
| :- | :- | :- |
| Verify the origin of an API request | β
Yes | HMAC-SHA256 request signature ([Authentication Guide](./overview)) |
| Verify the authenticity of a Webhook push | β
Yes | `X-Signature` HMAC-SHA256 verification ([Webhook + WebSocket](./webhook-websocket)) |
| Transmit a customer mobile number encrypted | β
Yes | AES encryption of the `mobile` field, following the official `SecureUtil.aes(Base64.decode(SECRET)).encryptBase64(...)` example (see Β§2.3) |
| Show the full card number and CVV safely | β
Yes (DCS-hosted) | DCS-hosted guidance page ([Viewing Encrypted Card Details](../how-to-use/managing-cards/viewing-encrypted-card-details)) |
| Set or reset a PIN | β
Yes (DCS-hosted) | DCS-hosted guidance page ([Managing a Card's PIN](../how-to-use/managing-cards/managing-a-cards-pin)) |
| A server-published RSA/ECC public key for the partner to encrypt a session token | β No | The DeCard-Managed model does not use this security model (see Β§1) |
| Client generates a `SessionId`, sends it in a header, server encrypts and decrypts the response | β No | The DeCard-Managed model does not use this security model (see Β§1) |
| A public key for validating session data | β No | The DeCard-Managed architecture has no session-data public key (see Β§1) |
***
## Next steps / Related
* API authentication and signing: [Authentication Guide](./overview)
* Webhook signature verification: [Webhook + WebSocket Notifications](./webhook-websocket)
* Showing card data safely: [Viewing Encrypted Card Details](../how-to-use/managing-cards/viewing-encrypted-card-details)
* Setting and resetting a PIN safely: [Managing a Card's PIN](../how-to-use/managing-cards/managing-a-cards-pin)
* DCS-hosted pages and the one-time secret: [H5 KYC / Card Application Guidance Page](./h5-kyc-guidance)
* Apple Pay and Google Pay provisioning encryption fields: [Push Provisioning](../how-to-use/managing-cards/push-provisioning)
* Customer registration (including the AES-encrypted `mobile` field): [Signing Up a Customer](../how-to-use/signing-up-a-customer/overview)
# Webhook + WebSocket Real-time Notifications
Source: https://docs.thedecard.com/en/decard-managed/integration-resources/webhook-websocket
DCS keeps you in sync in real time with key events such as KYC status, asset movements, card transactions and order status, over two complementary channels: Webhook, where DCS pushes to your callback URL, and WebSocket, where you subscribe to a private channel and can replay by version. Both channels share the same nine business event types and the same data structures; only the message envelope differs in field names and types. WebSocket is a DeCard-Managed differentiator.
Whether you are an exchange, a wallet or a platform, DCS tells you over a real-time channel whenever a user's KYC, balance, card or order changes, so you never have to poll. This page gets both channels working in one pass:
* **Webhook**: push based. You expose an HTTPS callback endpoint and DCS pushes signed JSON to it as events occur. Best suited to stable server-to-server delivery.
* **WebSocket**: subscription based, and a DeCard-Managed differentiator. You first obtain a dedicated private channel, then hold a `wss` connection open to receive messages live; if you miss anything, you can replay history by `version`. Best suited to cases that need front-end or gateway awareness in real time, or that require a replay path for reconciliation.
Privacy red line: the DeCard-Managed model handles a great deal of end-user personal data. Every `externalUserId`, card number, merchant name, address, `txHash` and `secretKey` in the examples on this page is a **placeholder or masked value** and must not be treated as real data. Mask the same fields in your own receiver logs, and never record real PII or keys in clear text.
***
## 1. Webhook (push based)
### 1.1 Core capabilities
* **Automatic retries**: when delivery fails, DCS retries automatically, **up to 3 times**.
* **Signature verification**: every notification carries an `X-Signature` digital signature in the request header so you can trust its origin.
* **Idempotency**: every event carries a globally unique `webhookId` that you can deduplicate on.
### 1.2 Prerequisites and integration steps
#### Step 1: Implement the Webhook receiver
Your service must expose a `POST` endpoint that meets the following requirements:
* Accepts an `application/json` request body;
* Returns `200 OK` to acknowledge receipt. **Any other status code** makes DCS retry;
* **Must respond within 2 seconds** (both the connect timeout and the socket timeout on the DCS side are 2000 ms), otherwise the delivery counts as a timeout failure and is likewise retried.
#### Step 2: Give DCS the callback URL to configure
Send the callback URL you implemented in step 1 to the DCS team. It **must be HTTPS and reachable from the public internet**.
#### Step 3: Verify, then go live
Verify in the UAT (sandbox) environment first, and go live in production only once verification passes.
### 1.3 Delivery sequence and retries
### 1.4 Security: `X-Signature` verification
To keep the channel secure, DCS carries `X-Signature` in the headers of every Webhook request. The signature is produced with **HmacSHA256**: DCS computes it over the **raw body** using your `SecretKey`. On receipt, recompute the signature over the raw body with the same `SecretKey` and compare it with `X-Signature` to confirm that the request is authentic and intact.
The signature header is always `X-Signature`, and the HMAC key is your `SecretKey`, not the `apiKey`. Implement verification accordingly.
#### Headers
| Header | Description |
| :- | :- |
| `X-Signature` | The signature string (HmacSHA256 over the raw body, keyed with `SecretKey`) |
| `Content-Type` | Always `application/json` |
#### Signature calculation reference (Java)
```java theme={null}
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
class HmacSignature {
public static void main(String[] args) throws Exception {
String secretKey = ""; // Placeholder; read it from your key management service
String webhookPayloadStr = ""; // The raw JSON string; never deserialize and re-serialize it
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(keySpec);
byte[] hmac = mac.doFinal(webhookPayloadStr.getBytes());
System.out.println(Hex.encodeHexString(hmac)); // Compare this with X-Signature
}
}
```
#### Receiver reference (Java, with the 7-step processing convention)
```java theme={null}
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@RestController
public class WebhookController {
private static final String HMAC_SHA256 = "HmacSHA256";
// Illustrative only; read this from your key management service
private static final String apiSecret = "";
@PostMapping("/your-path/webhook")
public ResponseEntity processWebhook(
@RequestHeader("X-Signature") String signature,
@RequestBody String rawBody // Must be received as a String; never bind straight to an object!
) {
// 1. Verify the signature (recompute over the raw body and compare with X-Signature)
if (!verifySignature(rawBody, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature");
}
// 2. Parse rawBody to read webhookId, type and the other envelope fields
// 3. If this type is not one you care about, ignore it and return 200 straight away
// 4. Idempotency: check whether this webhookId has already been processed
// 5. Parse the business data (optionally into a typed object)
// 6. Run the business logic (asynchronously, so you do not exceed the 2-second timeout)
// 7. Mark the webhookId as processed (idempotency)
return ResponseEntity.ok("OK");
}
private boolean verifySignature(String payloadStr, String signature) {
return StringUtils.equals(signature, getSignature(payloadStr));
}
private String getSignature(String data) {
try {
SecretKeySpec keySpec = new SecretKeySpec(apiSecret.getBytes(), HMAC_SHA256);
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(keySpec);
return Hex.encodeHexString(mac.doFinal(data.getBytes()));
} catch (Exception e) {
throw new RuntimeException("Failed to calculate hmac-sha256", e);
}
}
}
```
### 1.5 Webhook common envelope
Watch the differences in field names and types: Webhook uses `notificationTimestamp` / `eventTimestamp` (**`Long`, in milliseconds**), amount and quantity fields such as `freeDelta` and `transactionAmount` are `BigDecimal`, and `tranId` is `Long`. **The WebSocket equivalents are all `string`, and its time field is named `timestamp`** (see [section 2.4](#2-4-websocket-common-envelope)), so do not mix the types up when reading the two side by side.
| Name | Type | Description |
| :- | :- | :- |
| `webhookId` | string | Globally unique webhook ID, usable for idempotent deduplication |
| `type` | string | Notification type (see [the event catalogue in section 3](#3-business-event-catalogue-shared-by-both-channels)) |
| `externalUserId` | string | External user ID |
| `notificationTimestamp` | Long | Time the notification was sent, Unix timestamp in milliseconds |
| `eventTimestamp` | Long | Time the event was recorded, Unix timestamp in milliseconds |
| `data` | object | The business payload of the event; its structure depends on `type` |
**Webhook notification example (asset movement, masked):**
```json theme={null}
{
"webhookId": "4862356405917483776",
"type": "BALANCE_CHANGE",
"externalUserId": "d8ef852e-****-****-****-************",
"notificationTimestamp": 1767777763336,
"eventTimestamp": 1767777641000,
"data": {
"asset": "USD",
"network": "",
"freeDelta": -2.92,
"freezeDelta": 2.92,
"tranId": 4862356405699379969,
"externalTranId": "4862356404793410306",
"free": 0,
"freeze": 18.1,
"type": "CONVERSION"
}
}
```
***
## 2. WebSocket (subscription based, β
a DeCard-Managed differentiator)
> The WebSocket channel is unique to the DeCard-Managed model: you can subscribe to a dedicated private channel to receive messages live, and replay anything you missed by `version`.
Real-time push over WebSocket keeps user-related changes in sync, covering KYC status, asset movements, card transactions, order status and other key information, so you can react to user activity faster.
### 2.1 Prerequisites and three-step integration
1. **Create a listening channel**: call `GET /websocket/v1/get-channel` to generate a dedicated private channel for your organization. The channel string is the unique identifier used to receive messages, for example `HBHmMK99SJEVMVUqCb4`.
2. **Open the `wss` connection**: substitute the channel string for `{channel}` in the `wss` address, pick mainnet or testnet according to your environment (see [section 2.3](#2-3-websocket-environment-endpoints)), open the connection and listen for messages.
3. **Rotate channels and replay gaps**: each channel **lives for 24 hours** and then expires. To avoid losing messages, call `get-channel` again before it expires; calling it an hour ahead of expiry gives the old and new channels an overlap window wide enough for a clean switchover. If you miss messages or need to replay, use `GET /websocket/v1/search` to look up history by `version`.
### 2.2 Channel lifecycle
Channel security: **once a new channel is generated, the old channel stops pushing data**. Time the switchover correctly so you are not left waiting on a dead channel.
### 2.3 WebSocket environment endpoints
| Environment | URL |
| :- | :- |
| Mainnet (production) | `wss://stream.thedecard.com/ws/{channel}` |
| Test (sandbox) | `wss://stream.uatdcd.com/ws/{channel}` |
> Replace `{channel}` with the channel string returned by `get-channel`.
### 2.4 WebSocket common envelope
How it differs from the Webhook envelope: WebSocket uses **`timestamp` (a `string`)** and carries **an extra `version` field**, and every business field inside `data` (amounts, quantities, `tranId` and so on) is a **`string`**. That differs from the `Long` and `BigDecimal` types used by Webhook, so treat this table as authoritative.
| Name | Type | Description |
| :- | :- | :- |
| `type` | string | Message type (see [the event catalogue in section 3](#3-business-event-catalogue-shared-by-both-channels)) |
| `timestamp` | string | Message timestamp |
| `version` | string | Message version number, usable with `GET /websocket/v1/search` to replay history |
| `externalUserId` | string | User ID |
| `data` | object | The business payload of the event; its structure depends on `type` |
**WebSocket message example (asset movement, masked):**
```json theme={null}
{
"type": "BALANCE_CHANGE",
"timestamp": "1733980483134",
"version": "2",
"externalUserId": "9f8b5f82-****-****-****-************",
"data": {
"asset": "USD",
"network": "",
"freeDelta": "12.64",
"freezeDelta": "0",
"tranId": "4285261023005170688",
"externalTranId": "4285261022921284608",
"free": "12.64",
"freeze": "0",
"type": ""
}
}
```
### 2.5 WebSocket endpoint reference
Both endpoints return the site-wide common response envelope:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": {
"message": "",
"title": "",
"type": "",
"icon": "",
"action": "",
"linkTitle": "",
"linkUrl": ""
},
"data": ""
}
```
| Field | Description |
| :- | :- |
| `code` | Business status code; `SYS_SUCCESS` on success (identical in both integration models), or a specific error code on failure |
| `message` | Short message, usually `null` on success |
| `messageDetail` | Detail object, whose sub-fields are usually empty |
| `data` | string. `get-channel` returns the channel identifier string; `search` returns the historical messages found |
> Decide success from `code == "SYS_SUCCESS"`; the response envelope **contains no** `success` boolean field.
#### Obtain a listening channel
```
GET /websocket/v1/get-channel
```
No request parameters. `data` returns the private channel identifier string that belongs to your organization.
#### Look up historical messages by version
```
GET /websocket/v1/search?version=
```
| Parameter | In | Required | Description |
| :- | :- | :- | :- |
| `version` | query | REQUIRED | Message version number; returns the historical message for that version, so you can replay what you missed. The value is the `version` field from the real-time WebSocket push (see the example in section 2.4) |
> These two REST endpoints behave like every other REST endpoint on the site, so carry the signature headers (`X-DAPI-API-KEY` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE` / `X-DAPI-SIGN`) as described in the [Authentication Guide](./overview).
***
## 3. Business event catalogue (shared by both channels)
Both channels **share the same business event types and the same `data` structures**, nine in total (`CARD` being Webhook-only β it is never delivered over WebSocket). Only the envelope differs: Webhook uses `notificationTimestamp` / `eventTimestamp` (`Long`) while WebSocket uses `timestamp` plus `version` (`string`), and the field types inside `data` differ as described above (Webhook `BigDecimal`/`Long` β WebSocket `string`).
| `type` | Meaning | Key `data` fields / state machine |
| :- | :- | :- |
| `BALANCE_CHANGE` | Digital asset movement | `asset` / `freeDelta` / `freezeDelta` / `free` / `freeze` / `tranId` / `externalTranId` / `type` |
| `CARD_TRANSACTION` | Card transaction record | See [3.2](#3-2-card_transaction-card-transactions); includes `direction`, `transactionType`, `rejectReason` (declines only) and `originalExternalTranId` (reversal scenarios only) |
| `CARD_TRANSACTION_SETTLEMENT` | Card settlement record | `settledAmount` / `settledCurrencyCode` / `transactionType` and others |
| `CARD_PHYSICAL_SHIPPING` | Physical card shipping information | `status` (pending embossing / embossing / in delivery / delivered) / `trackingNumber` / `trackingCompanyName` |
| `ORDER_STATUS` | FOMO top-up (order) status | `txHash` / `transStatus` (5 values) / `asset` / `creditCurrency` / `convertCurrency` and others |
| `QR_ORDER_STATUS` | QR Pay order status | `orderId` / `orderStatus` (7 values) |
| `CARD_APPLY` | Card application status | `applyId` / `status` (PENDING/SUCCEED/FAILED) / `needExtraInfo` |
| `CARD_STATUS` | Card status change | `cardId` / `cardMantissa` / `cardStatus` (NORMAL/FROZEN/CANCELLED) |
| `CARD` | Card status and block reason change (**Webhook only**) | `cardId` / `cardMantissa` / `cardStatus` (four values, including BLOCKED) / `statusReason` |
> Terminology: the terminal `SUCCEED` of `CARD_APPLY` is a **card application** status, spelled `SUCCEED` rather than `SUCCESS`, and it follows different conventions from the card order and card state machines, so do not confuse them. `CARD_STATUS` emits only the three values `NORMAL/FROZEN/CANCELLED`; the state machine in [Card Management](../how-to-use/managing-cards/overview) has four, and the fourth β `BLOCKED` β is surfaced only by the `CARD` event and `/card/v3/detail`.
### 3.1 `BALANCE_CHANGE` digital asset movement
| Name | Type (WS / Webhook) | Description |
| :- | :- | :- |
| `tranId` | string / Long | Transaction reference |
| `externalTranId` | string | External transaction ID |
| `asset` | string | Asset currency |
| `network` | string | Network |
| `freeDelta` | string / BigDecimal | Change in asset balance |
| `freezeDelta` | string / BigDecimal | Change in frozen asset balance |
| `free` | string / BigDecimal | Available holding |
| `freeze` | string / BigDecimal | Frozen assets |
| `type` | string | Type of movement |
### 3.2 `CARD_TRANSACTION` card transactions
| Name | Type (WS / Webhook) | Description |
| :- | :- | :- |
| `cardNumber` | string | Last 4 digits of the card number |
| `transactionCurrencyCode` | string | Transaction currency, ISO code (such as `840` or `702`) |
| `transactionAmount` | string / BigDecimal | Transaction amount |
| `localTransactionDate` | string | Transaction date (such as `1211`) |
| `localTransactionTime` | string | Transaction time (such as `161420`) |
| `response` | string | Transaction outcome (A=accept/success, D=deny/fail) |
| `rejectReason` | string | Decline reason code, populated only when `response=D`; an empty string for approvals. See [Transaction decline codes](/en/decard-managed/how-to-use/managing-cards/reject-reason-dictionary) |
| `externalTranId` | string / Long | External transaction ID |
| `systemTraceAuditNumber` | string | System trace audit number |
| `requestAmountInUsd` | string | Transaction amount in USD |
| `mcc` | string | Merchant category code |
| `cardAcceptorNameLocation` | string | Merchant name and location |
| `direction` | string | Direction of funds: `DEBIT` (spend), `CREDIT` (credit or refund) |
| `settlementCurrencyCode` | string | Posting currency of the card account (ISO 4217) |
| `settlementAmount` | string / BigDecimal | Posting amount on the card account |
| `transactionType` | string | Transaction type: R=card purchase, C=ATM withdrawal, Q=enquiry, P=transfer or refund |
| `merchantCountryCode` | string | Merchant country code (3 digits) |
| `originalExternalTranId` | string | External transaction ID of the original transaction, **returned only in reversal scenarios** such as a void, reversal or refund; ordinary authorizations do not carry this field |
> Declined transactions (`response=D`) carry the reason in `rejectReason`; see [Transaction decline codes (rejectReason)](/en/decard-managed/how-to-use/managing-cards/reject-reason-dictionary) for every value and its meaning. The transaction query API does not return this field yet; contact DCS if you need it there.
**Example (refund, with `originalExternalTranId`, masked):**
```json theme={null}
{
"type": "CARD_TRANSACTION",
"version": "562",
"timestamp": "1766134300000",
"externalUserId": "d8ef852e-****-****-****-************",
"data": {
"response": "A",
"rejectReason": "",
"direction": "CREDIT",
"cardNumber": "**** **** **** 0000",
"externalTranId": "4834788305299456000",
"originalExternalTranId": "4834788305187471104",
"settlementAmount": "51",
"transactionAmount": "51",
"requestAmountInUsd": "0",
"localTransactionDate": "1219",
"localTransactionTime": "170000",
"settlementCurrencyCode": "702",
"systemTraceAuditNumber": "185974",
"transactionCurrencyCode": "702",
"mcc": "4511",
"cardAcceptorNameLocation": "DEMO MERCHANT DEMO CITY XX",
"transactionType": "P",
"merchantCountryCode": "458"
}
}
```
### 3.3 `CARD_TRANSACTION_SETTLEMENT` card settlement records
The fields are broadly the same as `CARD_TRANSACTION`, with two differences: the `settlement*` fields are replaced by `settledAmount` (settled amount) and `settledCurrencyCode` (settlement currency, ISO 4217); and only `direction` is present, without `response` or the other authorization-specific fields such as `systemTraceAuditNumber`.
```json theme={null}
{
"type": "CARD_TRANSACTION_SETTLEMENT",
"version": "561",
"timestamp": "1766134239507",
"externalUserId": "d8ef852e-****-****-****-************",
"data": {
"direction": "DEBIT",
"cardNumber": "**** **** **** 0000",
"settledAmount": "51",
"externalTranId": "4834788305187471104",
"transactionAmount": "51",
"settledCurrencyCode": "702",
"localTransactionDate": "1219",
"localTransactionTime": "165028",
"transactionCurrencyCode": "702",
"mcc": "4511",
"cardAcceptorNameLocation": "DEMO MERCHANT DEMO CITY XX",
"transactionType": "R",
"merchantCountryCode": "458"
}
}
```
### 3.4 `CARD_PHYSICAL_SHIPPING` physical card shipping information
| Name | Type | Description |
| :- | :- | :- |
| `cardMantissa` | string | Last 4 digits of the card number |
| `status` | string | Shipping status: `PENDING_EMBOSSING` (awaiting embossing), `EMBOSSING_IN_PROGRESS` (being embossed), `IN_DELIVERY` (in delivery), `DELIVERY_COMPLETE` (delivered) |
| `trackingNumber` | string | Tracking number (provided only in `IN_DELIVERY` / `DELIVERY_COMPLETE` status) |
| `trackingCompanyName` | string | Courier name (provided only in `IN_DELIVERY` / `DELIVERY_COMPLETE` status) |
### 3.5 `ORDER_STATUS` FOMO top-up status
| Name | Type (WS / Webhook) | Description |
| :- | :- | :- |
| `txHash` | string | On-chain transaction hash (may be empty for orders that are not on-chain) |
| `transStatus` | string | Order status, see the enum below |
| `asset` | string | Original asset currency (USDC / USDT) |
| `amount` | string / BigDecimal | Original transaction amount |
| `network` | string | Blockchain network (empty for fiat or internal orders) |
| `fromAddress` | string | Sending address |
| `toAddress` | string | Receiving address |
| `creditCurrency` | string | Currency actually credited (SGD / USD / USDC / USDT) |
| `creditAmount` | string / BigDecimal | Amount actually credited |
| `convertCurrency` | string | Currency after conversion (SGD / USD) |
| `convertAmount` | string / BigDecimal | Amount after conversion |
| `convertFeeCurrency` | string | Conversion fee currency (SGD / USD) |
| `convertFeeAmount` | string / BigDecimal | Conversion fee amount |
**`transStatus` enum:**
| Value | Terminal | Description |
| :- | :- | :- |
| `PENDING_NORMAL` | No | Order created, waiting to be processed |
| `PENDING_CREDIT` | No | External transaction complete, waiting to be credited |
| `PENDING_AUDIT` | No | Order under review |
| `SUCCESS` | Yes | Order processed successfully |
| `FAILED` | Yes | Order processing failed |
### 3.6 `QR_ORDER_STATUS` QR Pay order status
| Name | Type (WS / Webhook) | Description |
| :- | :- | :- |
| `orderId` | string / Long | Order ID |
| `orderStatus` | string | Order status, see the enum below |
**`orderStatus` enum:**
| Value | Terminal | Description |
| :- | :- | :- |
| `PENDING` | No | Awaiting payment |
| `SUCCESS` | Yes | Order processed successfully |
| `FAILED` | Yes | Order processing failed |
| `FULL_REFUNDED` | Yes | Fully refunded |
| `PARTIAL_REFUNDED` | Yes | Partially refunded |
| `PENDING_CONFIRMATION` | No | Payment result awaiting confirmation |
| `PROCESSING` | No | Payment in progress |
### 3.7 `CARD_APPLY` card application status
| Name | Type (WS / Webhook) | Description |
| :- | :- | :- |
| `applyId` | string | Application ID |
| `categoryId` | string / Long | Card category ID |
| `network` | string | Card network (such as VISA or MASTERCARD) |
| `currency` | string | Currency |
| `applyRef` | string | Idempotency key of the application |
| `status` | string | Application status, see the enum below |
| `errorCode` | string | Error code for a failed application (provided only in `FAILED` status) |
| `needExtraInfo` | boolean | Whether the user has to supply additional information, see the notes below |
**`status` enum:**
| Value | Terminal | Description |
| :- | :- | :- |
| `PENDING` | No | Application in progress |
| `SUCCEED` | Yes | Succeeded (the terminal state of a card application, spelled `SUCCEED` rather than `SUCCESS`) |
| `FAILED` | Yes | Failed |
**Notes on `needExtraInfo`:**
| Value | Meaning |
| :- | :- |
| `true` | The user has not yet submitted the additional information; guide them through the upload (use the hosted guidance page with `action=KYC_EXTRA_DOC`) |
| `false` | The user has completed the submission. Even if `status` is still `PENDING`, wait for our review rather than prompting the user again |
### 3.8 `CARD_STATUS` card status
Notifies you of the latest card status whenever it changes. Virtual card status (`cardStatus`) is supported today; physical card status (`physicalCardStatus`) will follow.
| Name | Type | Description |
| :- | :- | :- |
| `cardId` | string | Card ID |
| `cardMantissa` | string | Last 4 digits of the card number |
| `cardStatus` | string | Virtual card status, see the enum below |
**`cardStatus` enum (aligned with the [card management state machine](../how-to-use/managing-cards/overview)):**
| Value | Description |
| :- | :- |
| `NORMAL` | Normal |
| `FROZEN` | Frozen |
| `CANCELLED` | Closed |
This event carries only three `cardStatus` values: a card blocked on the issuer side is pushed **normalized to `FROZEN`**, so you cannot tell "the cardholder can unfreeze this" apart from "this cannot be unfrozen, contact DCS". Use the `CARD` event below when you need that distinction.
### 3.9 `CARD` card status and block reason
Notifies you whenever a card's status or its block reason changes. **Prefer this event over `CARD_STATUS` above**: it adds the four-value card status (with `BLOCKED`) and `statusReason`, so you know a card cannot be unfrozen before you walk the user into an unfreeze attempt.
This event is **pushed to every partner, with nothing to apply for**; if you have not adapted to it yet, return `200` and ignore it as described in section 4.
`CARD` is delivered over **Webhook only** β it never reaches WebSocket, so it has no `version` field and cannot be replayed with `GET /websocket/v1/search`. Configure a Webhook callback URL if you need to react to block reasons in real time.
| Name | Type | Description |
| :- | :- | :- |
| `cardId` | string | Card ID |
| `cardMantissa` | string | Last 4 digits of the card number |
| `cardStatus` | string | Virtual card status, four values β see [Card Management βΊ Virtual card status](../how-to-use/managing-cards/overview#mental-model-the-card-state-machine) |
| `statusReason` | string | Card status reason β see [Card Management βΊ Card status reason](../how-to-use/managing-cards/overview#card-status-reason-statusreason) |
**Example (compliance block, redacted):**
```json theme={null}
{
"webhookId": "4862356405917483777",
"type": "CARD",
"externalUserId": "d8ef852e-****-****-****-************",
"notificationTimestamp": 1766134300000,
"eventTimestamp": 1766134299880,
"data": {
"cardId": "4862356405917483777",
"cardMantissa": "0000",
"cardStatus": "BLOCKED",
"statusReason": "COMPLIANCE_REVIEW"
}
}
```
**`CARD_STATUS` is not being retired**: a single status change delivers **both `CARD` and `CARD_STATUS`**. They carry different `webhookId` values, so you cannot deduplicate one against the other, and their `cardStatus` values disagree β `CARD` says `BLOCKED` while `CARD_STATUS` normalizes it to `FROZEN`.
**Once you switch to `CARD`, drop your `CARD_STATUS` branch entirely** (return `200` and discard). Do not persist and notify on both, or the user gets duplicate notifications and the later `CARD_STATUS` overwrites your stored `BLOCKED` back to `FROZEN`, putting the unfreeze button back on screen.
`statusReason` reflects the value we store, as-is. It describes the current block only while `cardStatus` is `FROZEN` or `BLOCKED`; under `NORMAL` / `CANCELLED` it may still hold the previous reason. **Always decide whether a card is usable, or can be unfrozen, from `cardStatus`.**
***
## 4. Best practices and common questions
* **Verify first, process second**: before you touch any payload, recompute HmacSHA256 over the raw body with your `SecretKey` and compare it with `X-Signature`. If verification fails, reject the request with a 401, raise an alert, and process nothing.
* **Return 200 fast, do the work asynchronously**: DCS expects a response **within 2 seconds**. Persist the notification with its `webhookId`, return `200` immediately, and then process the business logic asynchronously so you never exceed the timeout and trigger needless retries.
* **Deduplicate on `webhookId`**: retries mean the same notification can arrive more than once. Store the `webhookId` values you have processed and check for duplicates before processing.
* **Mind the ordering**: network jitter and retries can make Webhooks arrive out of order. If you need to process events in the order they actually happened, sort by `eventTimestamp` (the time the event was recorded, see section 1.5) rather than by arrival time, and add a short buffering window such as 30 seconds if necessary before processing in batches.
* **Handling event types you do not care about**: for event types you have no use for, simply return `200` and ignore them. Do not return an error code, because error codes trigger retries.
* **Rotate WebSocket channels early**: a channel lives only 24 hours, and the old channel stops pushing as soon as a new one is generated, so call `get-channel` before expiry and switch over smoothly.
* **Use `version` as your safety net**: a WebSocket connection can drop messages when the network is unstable, so reconcile regularly and backfill with `GET /websocket/v1/search?version=`.
* **Choose the channels you need**: if delivery stability matters most, start with Webhook; if you need real-time responsiveness, front-end awareness or replay, add WebSocket. The event semantics are identical on both.
* **No sensitive card data over the real-time channels**: notifications carry only the masked last 4 digits of the card number, never the full PAN or the CVV. To obtain sensitive card data, use the dedicated encrypted API (see [Viewing Encrypted Card Details](../how-to-use/managing-cards/viewing-encrypted-card-details)).
***
## Next steps / Related
* [Authentication Guide](./overview): the two WebSocket REST endpoints behave like every other REST endpoint on the site, so carry the signature headers as described there.
* [Card Management](../how-to-use/managing-cards/overview): the card state machine behind `CARD_STATUS` and `CARD`, and the `statusReason` enum.
* [Transaction Lifecycle](../basic-concepts/transaction-lifecycle): understanding the two stages, authorization and settlement, behind `CARD_TRANSACTION` and `CARD_TRANSACTION_SETTLEMENT`.
# Authorization Forwarding Model
Source: https://docs.thedecard.com/en/partner-managed/basic-concepts/auth-forwarding-model
The core idea behind Partner-Managed: the partner owns the spending limits, and the partner makes a real-time decision on every authorization.
## π Guide
Whether you are an exchange, a wallet or a platform, Partner-Managed lets you **own the available limit of every cardholder and approve or decline every purchase in real time**. DCS issues the cards, forwards each network authorization request to you securely, and completes settlement and reconciliation once you approve, while **the limit rules and the risk logic stay with the partner at all times**.
As a licensed issuer with its own BIN, DCS ships this "the authorization decision goes back to you" capability as ready-to-use infrastructure, so you need neither an issuing license of your own nor a direct card network connection.
## The model in one sentence
> **The partner owns the limits; the partner makes the authorization decision.**
* **Limit rules and the authorization decision belong to the partner.** How much each cardholder may spend, who approves it and whether it goes through are defined and decided by the partner under its own limit rules. DCS acts on that decision and records the fund hold and the settlement entry as it forwards and settles, so both sides reconcile to the same numbers.
* **DCS holds the enterprise security deposit you have paid in.** It backs the T+1 settlement to the card network and is independent of the limit decision on any single authorization.
* **On every purchase, DCS hands the decision to you.** When a cardholder pays, DCS sends one notification for that authorization to the partner `authUrl`, and the partner replies with approve or decline.
This is the fundamental difference from DeCard-Managed. Under DeCard-Managed, DCS applies the limit rules and makes the authorization decision on your behalf. Under Partner-Managed, **both the limit rules and the decision on every single authorization return to the partner**, and DCS takes care of secure forwarding, settlement and backing the settlement payout.
## What happens when a purchase is authorized
The diagram below traces an ordinary purchase (`direction=OUTGOING`) end to end under Partner-Managed:
Key points:
* **The decision sits with the partner**: steps 3 to 5 of the diagram happen entirely inside the partner. Whether to let a transaction through, and under which rules (limits, per-transaction caps, merchant and MCC blocklists and so on), is yours to define; DCS neither participates in those rules nor stores them.
* **DCS is the secure transport**: DCS encrypts the authorization request and forwards it to you, encrypts your decision back towards the card network, and guarantees that both messages are authentic (see the encryption section below).
* **The security deposit is a company-level advance of funds**: what DCS holds is the partner enterprise security deposit, which lets DCS settle to the card network on time on T+1. It is a company-level settlement guarantee, independent of the limit decision on any single authorization. For how the money moves, see [Fund model](./fund-model).
## How the authorization decision gets back to DCS
Once the notification reaches your `authUrl`, the partner applies its business rules and returns a `responseCode`. The permitted values come from the [authorization forwarding notification payload](../how-to-use/transactions/authorization):
| responseCode | Meaning |
| - | - |
| `"00"` | Approve |
| `"01"` | Decline, insufficient funds |
| `"11"` | Decline, transaction not allowed |
| `"21"` | Decline, no response |
return a specific `responseCode` when you decline, so that both sides can tell decline reasons apart during reconciliation.
The mapping from the real-time reply to the stored record is fixed: `responseCode=00` is persisted as `approveFlag=A`, while `01`, `11`, `21` and any other non-`00` result are persisted as `approveFlag=D`. `responseCode` is used in the partner's synchronous response; `approveFlag` appears in the authorization result webhook and the daily authorization report.
## Not every authorization comes from a live card payment
Besides live purchases, authorization records also cover system back-fills and releases. The `authType` field appears both in the forwarding notification and in the daily authorization report (see [authorization enumerations](../how-to-use/transactions/authorization) for details):
| authType | Name | Description |
| - | - | - |
| `NORMAL` | Normal authorization | Created from a real-time authorization callback on the channel; covers purchases, incremental authorizations, reversals, refunds, withdrawals, balance enquiries and other live scenarios |
| `FORCE_AUTH` | Forced authorization | Backfilled by the system: settlement arrives with no matching authorization (an offline transaction, for example), or the settled amount differs from the held amount and the difference has to be booked |
| `EXPIRED_RELEASE` | Expiry release | The authorization was never settled in time, and the channel uses the settlement file to instruct release of the funds it had held |
| `STATUS_DIFF_RELEASE` | Status mismatch release | Reconciliation correction for the case where the channel declined the authorization on timeout while DCS had already approved it |
When you maintain cardholder limits in your own ledger, account for all four types. For example, an `EXPIRED_RELEASE` should release the limit you held earlier.
## Security prerequisites for authorization forwarding
Handing the authorization decision to the partner only works if the forwarding channel is trusted in both directions. DCS uses **RSA-2048 with SHA256withRSA for two-way signing and encryption**, with no additional AES/IV layer:
* **DCS to partner**: the core authorization fields are encrypted with **the partner RSA public key** and signed with **the DCS RSA private key**. The ciphertext goes into `data.encryptedData` in the request body, and the signature into the `X-Auth-Signature` request header.
* **Partner to DCS**: the response is encrypted with **the DCS RSA public key** and signed with **the partner RSA private key**, carried in the response body as `encryptData` and `signature`.
On the request side, `data.encryptedData` is the single ciphertext of all 15 serialized authorization business fields (authorization, user, card, amount, merchant and MCC, transaction type and so on). It is not limited to `authId`, `direction`, `currency` and `amount`.
Prerequisites before you integrate:
* An Enterprise is live, with `authUrl` (the authorization notification endpoint) and `externalPublicKey` (the partner RSA public key) configured. **Who does what: the partner** configures, **DCS** registers
* The IP whitelist for authorization forwarding notifications is in place. **Who does what: the partner** supplies the addresses, **DCS** allows them
* At least one card has been issued successfully
> For encryption details, key generation with openssl, obtaining the production public key and configuring the IP whitelist, see [Authentication and security](../integration-resources/authentication) and [Authorization and authorization forwarding notifications](../how-to-use/transactions/authorization).
## Who this model is for
* Exchanges, wallets and platforms that already run their own user system and limit or risk logic and want to **keep the authorization decision**
* Partners that prefer one enterprise security deposit to back settlement while they themselves control each cardholder limit and each authorization decision
* Teams that want licensed issuing at minimal cost and are happy to leave card network connectivity and settlement to DCS
## Next steps
With "you own the limits, you own the authorizations" clear, start with [Fund model](./fund-model) to see how the enterprise security deposit relates to the T+1 settlement payout, then work through [Authorization and authorization forwarding notifications](../how-to-use/transactions/authorization) to implement receiving, decrypting, deciding and responding on `authUrl`.
# Compliance and the KYC flow
Source: https://docs.thedecard.com/en/partner-managed/basic-concepts/compliance-kyc-flow
The material the KYC flow needs, the markets cards can be issued in and the per-region document requirements; statuses and error codes are covered in the related guides.
## π Guide
Whether you are an exchange, a wallet or a platform, you can hand cardholder identity verification to DCS β we offer both a direct API integration and a hosted (H5) self-service flow, so you can choose how deep to integrate without handling document retention, liveness detection or anti-money-laundering screening yourself. As a card issuer regulated by the Monetary Authority of Singapore (MAS), DCS verifies every user's proof of identity (POI) and proof of address (POA) and runs a risk assessment against anti-money-laundering (AML) rules. Only users who have passed KYC can apply for and use a virtual card.
***
## Compliance: supported markets and document requirements
Before issuing a card to an end user, settle two compliance facts with DCS first: whether cards can be issued in that user's region, and which documents they need to prepare. Both are decided by your agreement with DCS together with the user's region.
## What KYC consists of
DCS compliance requires four categories of material from each user:
1. **POI (proof of identity)** β establishes *who this person is*
2. **POA (proof of address)** β establishes *where this person lives*
3. **Liveness (face verification)** β establishes *that this is a real person, acting for themselves*
4. **Background information** (employment status, source of funds, account purpose and so on) β feeds the anti-money-laundering risk assessment
## KYC documents (POI and POA)
DCS whitelists the documents of certain countries and regions so that one document serves as both proof of identity (POI) and proof of address (POA) β a user whose document is whitelisted submits that single document plus a face check and background information, with no separate proof of address. If the identity document carries no address, the user must supply a POA as well. For the image requirements, see [KYC document requirements](../customer-success/faq-kyc-documents).
## Cardholder eligibility
* **Age**: between 18 and 70
* **Tax status**: not a US resident and not a US tax resident
* **Residence**: only regions covered by your agreement with DCS can be issued cards
## Background information
AML rules require the user to submit the relevant information. For the complete enumerations, see [KYC application parameter dictionary](../how-to-use/kyc/kyc-application-fields).
| Item | Description / options |
| - | - |
| Employment status | Employed / business owner / self-employed / unemployed / retired / student |
| Industry, occupation, seniority | See the parameter dictionary |
| Account purpose | Everyday spending / investment / bill payment and so on |
| Source of funds, source-of-funds country | Own bank account / third-party account / crypto top-up and so on |
| Source of wealth | Employment income / savings / inheritance / business income and so on |
***
## Mental model: one application, one ticket, one status line
In the Partner-Managed model, KYC is carried by a **KYC ticket**: a user (`customerId`) starts KYC once, which creates a single ticket (`kycTicketId`), and from then on every status change and every result lookup happens against that ticket.
Ticket status changes are pushed to the partner through the `KYC_TICKET` webhook, and you can also pull the current status through the query API at any time.
> **Who does what**: DCS receives and securely stores the documents, verifies their authenticity, runs AML screening, performs manual review where needed, and advances and notifies the status. The partner starts the application, guides the user through the steps that need the user's own participation (such as face verification), and moves on to card issuing based on the result.
***
## The KYC flow: two ways to integrate
Pick one based on how deeply you want to integrate. Both converge on the same end state: ticket `PASSED`, then apply for a virtual card.
* **API mode** β Create the user β call `apply-kyc` to get a `kycTicketId` β receive the result through the `KYC_TICKET` webhook or the query API. The user may be asked to complete face verification along the way. For execution details such as status values, face verification guidance, resubmission and reject codes, see [How-to Β· Apply for KYC](../how-to-use/kyc/apply-kyc) and [KYC reject codes](../how-to-use/kyc/kyc-reject-codes).
* **H5 hosted mode** β Create the user β call `apply-kyc-h5` β exchange the guidance link for an H5 URL and send it to the user. Document capture, liveness check and background information are all completed on the DCS-hosted page. For the step-by-step flow and idempotency behavior, see [How-to Β· Apply for KYC (H5 mode)](../how-to-use/kyc/h5-kyc-guidance).
> Tickets in both modes are keyed by `kycTicketId` and share the same query API and the same webhook. The only difference is who captures the documents. For the exact status values and reject codes, follow the links above to the matching How-to page.
***
## Sumsub token integration (data provider)
* **You are the data provider**: obtain the UAT and PROD tokens from DCS and add them in the Sumsub console; your registered Sumsub name is visible in the DCS Sumsub console.
For more on Sumsub and KYC vendor integration, see [KYC vendor](../customer-success/faq-kyc-vendor); Sumsub's own documentation is at [Manage sharing partners](https://docs.sumsub.com/docs/manage-sharing-partners).
***
## Next steps
Once KYC is `PASSED`, you can apply for a virtual card for that user. Continue with [How-to Β· Apply for a virtual card](../how-to-use/cards/virtual-card).
# Fund Model: User Wallets vs the Enterprise Security Deposit
Source: https://docs.thedecard.com/en/partner-managed/basic-concepts/fund-model
The funding mental model behind Partner-Managed mode: the two-leg liability (an authorization hold followed by a settlement debit) and the payout rhythm (DCS pays the card network on T+1, you replenish the security deposit on T+X).
## π Guide
Whether you are an exchange, a wallet or a platform, Partner-Managed mode keeps every cardholder's spending limit **in your hands** and leaves every authorization **decision** to you, while DCS turns those decisions into real money at the card network. This page explains the two ideas that make the fund model click: the **two-leg liability** (an authorization hold and a settlement debit are two separate events) and the **payout rhythm** (DCS pays the card network on T+1, you replenish the security deposit on T+X).
Once these two are clear, you can model your own ledger correctly: no double-holding of user funds, and no security deposit drained before settlement lands.
***
## Two pools of money: user wallets vs the enterprise security deposit
Partner-Managed mode involves two distinct pools of money. Always keep them apart:
| Money | Held where | Who owns the limit rules | Purpose |
| - | - | - | - |
| **User wallet** (user limit/balance) | Limit rules and decisions sit on the partner side | **Partner** | The cardholder's spendable funds. The DCS partner-facing API neither exposes them to you nor requires you to custody user-level balances (DCS internal bookkeeping is transparent to the partner) |
| **Enterprise security deposit** (Enterprise Balance) | DCS side, per enterprise and per currency | **DCS** | The pooled funds you prefund, used as the working capital DCS draws on to settle with the card network on T+1 |
> **Key point**: user limit rules are maintained by the partner, and whether any single authorization passes is **decided by the partner against its own limit rules** (this is exactly what authorization forwarding means, see [Authorization Forwarding Model](./auth-forwarding-model)). After the partner approves, DCS checks that your **enterprise security deposit** is sufficient: if `availableAmount` falls short, the authorization is declined.
Real-time figures for the security deposit come from the enterprise balance endpoint, which returns three amount fields:
| Field | Meaning |
| - | - |
| `totalAmount` | Total security deposit |
| `availableAmount` | Currently available limit (holds already deducted) |
| `usedAmount` | Amount held by authorizations or already used |
> Endpoint: `GET /open-api/enterprise/v2/enterprise-balance` (queried by `cardProfileId`). Line-level records are available from `GET /open-api/enterprise/v1/enterprise-balance-record`.
***
## Fund model 1: the two-leg liability, an authorization hold then a settlement debit
In funding terms a card purchase is not a single deduction but **two events that happen in sequence**. This is the fundamental difference from "a wallet debiting the balance directly".
* **Leg one, authorization**: when the cardholder pays, the card network sends an authorization request in real time. DCS forwards it to the partner, who **decides** whether to approve it against its own user limit rules. On approval the partner places a **hold** on the matching limit, and DCS reserves the same amount against `availableAmount` in the security deposit. At this point **no money has actually moved**.
* **Leg two, settlement**: the merchant later presents the transaction for settlement (usually from T+1 onwards, sometimes later); only once DCS receives it does the **actual debit** happen, and the hold from leg one is released.
The authorized amount and the settled amount **need not match**: a merchant may settle for less (a restaurant dropping the pre-authorized tip), for more (fuel stations, hotels), in several instalments, or settle with no prior authorization at all (offline transactions). DCS uses **Outstanding** (the unsettled balance) as the intermediate ledger that ties the two legs together, so holds and debits always reconcile back to zero.
Common combinations (all 13 scenarios are covered in [Authorization and Settlement Scenarios](../how-to-use/transactions/auth-and-settlement)):
| Scenario | Authorization (hold/release) | Settlement (debit/refund) | Final Outstanding |
| - | - | - | -: |
| Normal authorization to normal settlement | Hold 100 | Debit 100 | 0 |
| Normal authorization, partial reversal, settlement | Hold 100 β release 30 | Debit 70 | 0 |
| Normal authorization, full reversal | Hold 100 β release 100 | None | 0 |
| Normal authorization, over-settlement | Hold 100 (the 50 difference held via ForceAuth) | Debit 150 | 0 |
| Force settlement (no authorization) | No hold | Direct debit 100 | 0 |
> **Practical reminder for partners**: place the hold on the user wallet when you respond to the **authorization forwarding notification (authUrl)**, and only debit and release the hold once the settlement appears in the **daily transaction reconciliation file**. Never debit the balance at authorization time, or reversals, under-settlements and expiry releases will leave your books out of step with reality. The data structures for authorizations and transactions are documented in the [Authorization Data Dictionary](../how-to-use/transactions/authorization) and the [Transaction Data Dictionary](../how-to-use/transactions/transaction).
***
## Fund model 2: the payout rhythm, DCS pays the card network on T+1, the partner replenishes on T+X
The second fund model is about **timing**. Authorization is real time, actual funds settle later, and DCS's payout obligation to the card network is non-negotiable:
* **DCS to the card network: T+1 payout**. Whatever time the settled funds actually arrive from the cardholder side, DCS must front the payout on the card network's schedule (usually T+1). The money for that advance is your prefunded **enterprise security deposit**.
* **Partner to DCS: T+X replenishment**. Once the deposit has been consumed you need to prefund promptly, bringing `availableAmount` back to a safe level. The exact replenishment cycle and method follow your contract.
> **Hard risk limit**: once `availableAmount` in the security deposit is no longer sufficient, DCS declines subsequent authorizations, even if the user wallet still has a balance. Set an alert threshold on `availableAmount`, poll it regularly, and schedule replenishment ahead of time. Push-based alerting and webhooks are on the roadmap; for now, monitor the level by polling the enterprise balance endpoint.
***
## Next steps
With the fund model settled, head to the [Transaction Lifecycle](./transaction-lifecycle) to follow one transaction end to end from authorization to settlement. For reconciliation files, see [Daily Reconciliation Reports](../how-to-use/reports/overview).
# Transaction lifecycle
Source: https://docs.thedecard.com/en/partner-managed/basic-concepts/transaction-lifecycle
The end-to-end path of money on a card, from the authorization hold to the settlement debit. This is the foundation on which you build your own reconciliation and limit logic.
## π Guide
Whether you issue spend cards, withdrawal cards or multi-currency cards, every movement of money on a card follows the same path: **authorization (hold) then settlement (the actual debit or refund)**. Once you understand this path, you can explain any balance change on an account and design your own reconciliation and limit logic around it.
As the issuing platform, DCS forwards the decision to the partner during authorization and freezes or releases funds according to the answer; during settlement it records how the money actually moved and generates reconciliation files daily.
## Two stages, three entities
Inside DCS, a transaction is expressed by three kinds of records working together. Keep their relationship in mind and every scenario that follows becomes just a different combination of the same model.
| Entity | Stage | Role |
| - | - | - |
| **Authorization** | Authorization | The real-time decision recorded when the cardholder pays. An approval holds or releases funds, but **never debits** anything by itself |
| **Outstanding** | Both stages | The bridge between authorization and settlement. Held amounts accumulate on it during authorization and are drawn down during settlement until it reaches zero |
| **Transaction** | Settlement | The authoritative record of funds **actually** debited or credited, and the data behind the daily reconciliation files |
Two IDs tie the three together: an authorization and its settlement share the same `outsId` (Outstanding ID), and a transaction record points back to one or more authorizations through `authIds`.
### Stage 1: authorization (funds are held, never debited)
When a cardholder swipes, dips or taps at a merchant, the merchant estimates an amount and asks the card network for an authorization. **No money moves at this point.** DCS receives the request from the card network, forwards it to the partner for a real-time decision, and then holds or releases the corresponding amount according to that decision.
The key attributes of an authorization record (see the [authorization how-to](../how-to-use/transactions/authorization) for the full field list):
* **direction**: `OUTGOING` holds funds (the cardholder pays) / `INCOMING` releases funds (reversal, refund).
* **authType**: `NORMAL` ordinary authorization / `FORCE_AUTH` created by the system to backfill / `EXPIRED_RELEASE` release on expiry / `STATUS_DIFF_RELEASE` release on status mismatch.
* **approveFlag**: `A` approved (funds are held) / `D` declined (no funds are touched).
* **outsId**: generated with the first authorization. Later increments and reversals belonging to the same purchase reuse that `outsId` through `originalAuthId`.
> The authorization decision belongs to the partner and draws on the corporate margin it has deposited with DCS. For why the money moves this way, see the [funding model](./fund-model).
### Stage 2: settlement (funds actually move)
Hours or days later the merchant submits the final amount to the card network and settlement begins. DCS creates a **Transaction** that records the funds actually debited (`OUTGOING`) or credited (`INCOMING`), and draws the Outstanding down accordingly:
* The settled amount matches the held amount, so the Outstanding reaches zero and the transaction is closed.
* The two differ (more, less, or split into several captures), so the system creates a `FORCE_AUTH` authorization to absorb the gap. See the scenarios below.
Each transaction record carries a `category` for business classification (`RETAIL` ordinary purchase / `CASH` cash withdrawal / `CASH_FEES` cash withdrawal fee / `PAYMENT` return or refund), and `multiClearInd` to show the progress of multiple captures. This record is the authoritative source for the daily reconciliation files.
## One path, many combinations
In the real world an authorization and a settlement do not always match one to one: a merchant may add to the amount, reverse part of it, capture it in several pieces, or settle with no prior authorization at all. All of these are simply different ways of adding to and drawing down the Outstanding along the same authorization-then-settlement path. DCS defines **13 standard scenarios**, which fall into the 9 mechanisms below. What distinguishes them is **how the Outstanding moves**:
| Type | Scenario | How the Outstanding moves |
| - | - | - |
| Standard | Ordinary authorization then ordinary settlement (amounts match) | One increase, one draw-down, back to zero |
| Increment | Incremental authorization (a hotel adding charges, for example); several authorizations share one `outsId` | Several authorizations accumulate on the same Outstanding, which is drawn down in one settlement |
| Reversal | Partial reversal / full reversal | An `INCOMING` authorization releases the held amount (a full reversal produces no settlement entry) |
| Difference | Over-capture / under-capture | The settled amount differs from the held amount, so the system creates a `FORCE_AUTH` to absorb the difference |
| Split | Partial capture then final capture | The same Outstanding is drawn down by successive captures and closes when `multiClearInd=F` |
| No authorization | Forced capture / forced refund (offline transactions) | Settlement arrives first; the Auth and Outstanding are created afterwards |
| Inquiry / notification | Inquiry authorization, card verification (amount 0 or 1) | No funds are held, so the Outstanding does not change |
| Release | Release on authorization expiry / release on status mismatch | `EXPIRED_RELEASE` / `STATUS_DIFF_RELEASE` releases a held amount that was never settled |
| Withdrawal | Ordinary settlement plus a separate withdrawal fee flow | The principal and the fee are drawn down by two separate settlements |
> **The invariant**: whichever combination applies, **the Outstanding converges to 0 once settlement is complete**. That is what makes it usable as your reconciliation check: an `outsId` that has not returned to zero means the transaction still has an authorization or a settlement outstanding.
For the step-by-step sequence of each combination, including how authId, outsId and amounts change, see [authorization and settlement scenarios](../how-to-use/transactions/auth-and-settlement), the authoritative walkthrough of all 13 cases. For the day-to-day reconciliation of the most common settlement deviations, such as partial, over and multiple captures, see [settlement scenarios](../how-to-use/transactions/capture-scenarios).
## Next steps
* To learn why funds are held and how the corporate margin backs them, read the [funding model](./fund-model).
* To handle a real authorization callback yourself, go to the [authorization how-to](../how-to-use/transactions/authorization) and [webhook events](../how-to-use/webhooks/events-and-schema).
# KYC document requirements
Source: https://docs.thedecard.com/en/partner-managed/customer-success/faq-kyc-documents
The identity documents (POI) accepted in each country or region, together with the validity and file requirements.
## π Guide
## Core rules
* **Whitelisted identity documents**: the user submits a single document, completes face verification and fills in the required background information β that is the whole verification.
* **Non-whitelisted identity documents**: the user must submit a passport (proof of identity) plus one proof-of-address document, and complete face verification and background information.
* **Cardholder age**: between 18 and 70.
* **Document validity**: an identity document must have at least 6 months of validity remaining; a proof of address must have been issued within the last 3 months.
* **No screenshots**: every document must be a photo or scan of the original β screenshots are not accepted.
## Proof of identity (POI)
### POI basic requirements
* **Age**: the cardholder must be between 18 and 70.
* **Validity**: the identity document must have at least 6 months of validity remaining.
The document must carry all of the following, legibly:
* Full name
* Date of birth
* Photo
* Document number
* Issue / expiration date
* A legible MRZ (machine-readable zone)
### Upload requirements
Uploaded files must satisfy all of the following:
* An original photo β screenshots are not accepted
* Accepted formats: JPG, JPEG, PNG, PDF
* At least 100 KB, or a resolution of 300 DPI
* No larger than 20 MB
* Must be in colour
* Sharp, good quality and legible
* All four corners must be visible
* One or both sides of the document
* Do not upload anything retouched or altered in image-editing software
### Accepted documents by country / region
The table below lists the identity documents accepted in each country or region, and whether that document still requires a separate proof of address (POA).
| Country / region | Accepted identity document | Separate POA required |
| - | - | - |
| Philippines PHL | Philippine national ID | No |
| Philippines PHL | Driving license | No |
| Philippines PHL | Passport | Required |
| Taiwan TWN | ROC national ID | No |
| Taiwan TWN | Passport | Required |
| Malaysia MYS | MyKad (Malaysian national ID) | No |
| Malaysia MYS | Driving license | No |
| Malaysia MYS | Passport | Required |
| Vietnam VNM | Vietnamese citizen ID | No |
| Vietnam VNM | Driving license | No |
| Vietnam VNM | Passport | Required |
| Japan JPN | My Number CardΒΉ | No |
| Japan JPN | Driving licenseΒΉ | No |
| Japan JPN | Passport | Required |
| South Korea KOR | Resident registration card | No |
| South Korea KOR | Passport | Required |
| Australia AUS | Passport | Required |
| Thailand THA | Thai national ID card | No |
| Thailand THA | Passport | Required |
| Singapore SGP | Singapore NRIC | No |
| Singapore SGP | Passport | Required |
| Other countries | Passport | Required |
ΒΉ **Japan-specific requirement**: when using a My Number Card or a driving license, the user's IP address must be inside Japan; otherwise the user must also submit a passport for verification.
## Proof of address (POA)
When the user presents a passport, a separate proof of address is required. Accepted documents include bank statements, utility bills and official government documents.
A proof of address must also be an original β screenshots are not accepted.
* **Purpose**: a POA verifies the user's residential address.
* **Validity**: the document must have been issued within the last 3 months.
* **Content**: it must show the full name and the address, and the name must match the identity document.
* **Upload limits**: JPG, JPEG, PNG or PDF only; each file no larger than 10 MB; up to 3 files.
### Validity rules at a glance
| Document class | Rule |
| - | - |
| POI (ID card / passport / driving license) | At least 6 months before expiry |
| POA (utility bill / bank statement and so on) | Issued within the last 3 months |
## How documents are submitted, and the related fields
How documents accompany the card / KYC request depends on the KYC integration you use:
* **Sumsub**: the user's documents are collected in Sumsub and you share them with DCS through `sumsubShareToken`. For how the sharing-partner relationship is configured, see [KYC vendor](./faq-kyc-vendor).
* **Non-Sumsub (NORMAL)**: you submit the document details and file URLs directly through the KYC endpoint β see [Apply for KYC](../how-to-use/kyc/apply-kyc). The values you pass for `identityProofType` (`ID_CARD` / `DRIVERS` / `PASSPORT`), `addressProofList` and `poaDocType` must match the rules on this page.
For the material KYC needs, the markets cards can be issued in and the per-region document requirements, see [Compliance and the KYC flow](../basic-concepts/compliance-kyc-flow).
## Next steps
* Once the documents are ready, submit the verification: [Apply for KYC](../how-to-use/kyc/apply-kyc)
* Using Sumsub? configure the sharing partner first: [KYC vendor](./faq-kyc-vendor)
* Rejection reasons and the resubmission path: [KYC reject codes](../how-to-use/kyc/kyc-reject-codes)
# About the KYC Vendor
Source: https://docs.thedecard.com/en/partner-managed/customer-success/faq-kyc-vendor
Explains which KYC vendor DCS uses, how you establish the sharing relationship with DCS in the Sumsub console, and the token requirements for the UAT and production environments.
## KYC Vendor: reuse your existing Sumsub so users do not verify twice
Whether you already hold a large base of verified users in Sumsub or would rather have DCS carry out the review, Sumsub's data sharing mechanism lets you connect the KYC material once so that cardholders never have to upload their documents again at card application. DCS is a licensed issuer with its own BINs, and performs KYC/AML review as required by law. This page explains how you and DCS authorize each other for Share Tokens on the Sumsub platform before integration begins.
DCS uses **Sumsub** as its KYC vendor. If you run your own Sumsub integration, you first need to establish a "sharing partner" relationship with DCS in the Sumsub console so that DCS can retrieve the user's verification data from Sumsub. This page covers how that relationship is configured; for the actual KYC submission and card issuing integration, follow [Apply for KYC](../how-to-use/kyc/apply-kyc).
**Before go-live (do this first)**
**Make sure your Sumsub contract covers the sharing-partner relationship with DCS.** If it does not, your launch will be held up, and a contract amendment usually cannot be arranged at short notice.
***
## First, work out whether you are the provider or the recipient of the data
Sumsub data sharing runs in one direction: one side acts as the **Donor** and shares the user's data, the other acts as the **Recipient** and receives it. Depending on how you work with DCS, one of the two applies:
| Role | What it means | Who works in the Sumsub console |
| - | - | - |
| **You are the Donor** | The user is verified in **your** Sumsub account, and the data is shared with DCS | You add the token DCS gives you, in your own console |
| **You are the Recipient** | The user's data originates in the **DCS** verification stack, and you take a token from your own console and hand it to DCS | You generate the token and hand it to DCS to add |
> If you are unsure which side you are on, check with your DCS contact: it depends on which party has been agreed to carry out the first verification.
***
## Case 1: you are the Donor
> **Ownership**: steps 1 and 2 are done by you in your own Sumsub console; step 3 is confirmed jointly by you and DCS.
1. **Request the tokens from DCS**: ask for both the **UAT (sandbox)** and **PROD (production)** tokens. Tokens from the two environments must never be interchanged.
2. **Add the token in Sumsub**: log in to your Sumsub console, open the sharing partner settings page as shown below, and add the token DCS gave you.
3. **Verify both ways**: confirm with DCS that your Sumsub-registered institution name now appears in the sharing partner list on their side of Sumsub. If it does, the relationship is established.
**Reference**: Sumsub's own documentation, [Manage sharing partners](https://docs.sumsub.com/docs/manage-sharing-partners)
***
## Case 2: you are the Recipient
> **Ownership**: step 1 is done by you in your own Sumsub console; in step 2, DCS adds the token and you verify the result.
1. **Generate and hand over the tokens**: generate the **UAT** and **PROD** tokens in your Sumsub console and pass them to your DCS contact.
2. **Verify the result**: once DCS has added them, confirm that DCS appears in the **Donor list** in your own Sumsub console, and let DCS know. If it appears, the relationship is established.
***
## Frequently asked questions
**Q: Can the UAT and PROD tokens be used interchangeably?**
A: No. The two environments are isolated, so the tokens must be requested or generated separately and added separately.
**Q: Once the token is added, what else is needed before we can issue cards?**
A: The sharing relationship is only a prerequisite. For the actual KYC submission and card issuing integration, follow [Apply for KYC](../how-to-use/kyc/apply-kyc) and [Card Issuing](../how-to-use/cards/card-issuing).
**Q: What must the user's documents satisfy?**
A: See [About KYC Documents](./faq-kyc-documents) for the list of acceptable identity and address documents, validity requirements, and when an additional POA is required.
***
## The Sumsub POI / POA configuration baseline (required reading)
for the Sumsub token you send to DCS to pass verification, your POI (proof of identity) and POA (proof of address) configuration in the Sumsub console **must match DCS exactly**. Follow the screenshots below closely; a mismatched configuration will cause the token to fail verification and will hold up your launch.
This baseline may be adjusted from time to time as compliance or regulatory requirements change, so DCS's latest notice always prevails. For the table of accepted document types by country and when an additional POA is required, see [About KYC Documents](./faq-kyc-documents).
### Part 1: Setting for POI (proof of identity)
#### 1. ID Verification
#### 2. Supported ID Documents
##### 2.1 Blacklisted countries (34)
None of the following documents are accepted: passport, ID card, driving license or residence permit.
Blacklisted countries (34): Afghanistan, Belarus, Bosnia and Herzegovina, Burundi, Central African Republic, Congo (DRC), Cuba, Guatemala, Guinea, Guinea-Bissau, Haiti, Iran, Iraq, Lebanon, Libya, Mali, Moldova, Montenegro, Myanmar, Nicaragua, Niger, North Korea, Russia, Serbia, Somalia, South Sudan, Sudan, Syria, Tunisia, Turkey, Ukraine, Venezuela, Yemen, Zimbabwe.
##### 2.2 POI accepted as POA for nine designated countries
Supported countries: Taiwan, Australia, Japan, South Korea, Vietnam, the Philippines, Malaysia, Thailand and Singapore. Configure the accepted document types and subtypes for each country in Sumsub as shown in the screenshots below.
**Taiwan** β passport and ID card accepted (subtype `twn.id.type1` only)
**Australia** β passport only
**Japan** β passport, My Number Card and driving license accepted (the IP address must be in Japan; if it is not, the user must submit a passport for verification)
**South Korea** β passport and ID card accepted (subtype `kor.id.type1` only)
**Vietnam** β passport, ID card (subtypes `vnm.id.type3` and `vnm.id.type4` only) and driving license (subtype `vnm.drvlic.type1` only) accepted
**Philippines** β passport, ID card (subtype `phl.id.type11` only) and driving license (subtype `phl.drvlic.type1` only) accepted
**Malaysia** β passport, ID card (subtype `mys.id.type1` only) and driving license (subtype `mys.drvlic.type1` only) accepted
**Thailand** β passport and Thai National ID Card accepted
**Singapore** β passport and Singapore identity card (NRIC) accepted
#### 2.3 USA
#### 2.4 Other countries
Passport only, single-sided; ID cards, driving licenses and residence permits are not accepted.
### Part 2: Setting for POA (proof of address)
#### 1. Workflow builder
If the POI document carries no address, the user goes on to POA (proof of address) verification. If the POI document does carry an address but the address country is Japan and the user's IP is outside Japan, the user must additionally submit a passport to confirm nationality.
#### 2. User verification
Note: proof of address (POA) is accepted only from designated countries, as set out in your agreement with DCS.
#### 3. Verification level
**USA residents need to be rejected.**
***
## Next steps
* Accepted document types and the POA rules: [About KYC Documents](./faq-kyc-documents)
* Submitting KYC and issuing cards: [Apply for KYC](../how-to-use/kyc/apply-kyc) and [Card Issuing](../how-to-use/cards/card-issuing)
* The end-to-end verification flow: [Compliance and the KYC Flow](../basic-concepts/compliance-kyc-flow)
# Post-Go-Live FAQ
Source: https://docs.thedecard.com/en/partner-managed/customer-success/post-go-live
Lists the most common front-line issues you will meet in day-to-day operations after go-live, organized by scenario (digital wallet provisioning failures, card-on-file failures, declines, frozen or blocked cards, refunds that have not arrived, disputes and fraud, KYC stuck in review, changing a shipping address, blocking a merchant). Each scenario gives the steps you can work through yourself and the threshold at which the case should be raised with DCS.
## π Guide
Go-live is only the beginning. Whether you are an exchange, a wallet or a platform, your cardholders will run into real situations: a card that will not go into a mobile wallet, a refund that never seems to land, a transaction that gets declined. This page lists the most common post-go-live issues by scenario and gives you both **the steps you can work through yourself** and **the threshold at which to raise the case with DCS**, so that your support team can resolve most front-line issues without help.
As a licensed issuer with its own BINs, DCS keeps providing card issuing, authorization forwarding, clearing and reconciliation in production. When a case genuinely needs DCS, raise it through the [Escalation and Support Path](./support-escalation).
> A note on ownership: under the Partner-Managed model, **you control spending limits and you make the authorization decision**. Questions like "why was this declined" and "was the balance sufficient" should therefore be investigated first in your own authorization logic and enterprise deposit; DCS is responsible for issuing, forwarding and settlement.
***
## Adding the card to a digital wallet (Apple Pay / Google Pay) fails
When a cardholder cannot add a card to their mobile wallet, walk them through the following in order:
1. **Try a different card**: this tells you whether the problem is with *this card* or with *this wallet / this handset*.
2. **Common causes**:
* the country or region of the handset does not match the region the card was issued in;
* the handset has been flagged as a high-risk device by Apple or Google;
* several cards were added in quick succession, tripping the wallet's own rate limit.
3. **What to advise**: a single device can normally add only **one card successfully per 24 hours**, so ask the cardholder to wait 24 hours and try again.
> For one-tap provisioning from inside your app, see [Push Provisioning / Digital Wallets](../how-to-use/cards/push-provisioning).
## Adding the card at a merchant (card on file) fails
When a cardholder cannot save the card with a merchant or app:
* some merchants verify the billing address strictly, so suggest the cardholder enter address details matching the card's issuing region at checkout;
* if it still fails, note the **transaction identifier** (`transactionId` / `authId`) and raise it with DCS through the [escalation path](./support-escalation) for investigation.
## How to troubleshoot a decline
Every authorization request is forwarded by DCS to your `authUrl` in real time for a decision, and the decline is visible in the authorization data as well. Troubleshoot in this order:
1. **Establish who made the decision**: under the Partner-Managed model the vast majority of declines come from your own decision (`approveFlag=D`), so check your authorization logic first: has the user exhausted their limit, is the available balance of the enterprise deposit sufficient, did the request hit one of your own risk or MCC rules.
2. **Then check the DCS-side pre-checks**: DCS validates card status (activated, frozen or blocked) before forwarding. If the card is `FROZEN`, `BLOCKED` or `INVALID` the authorization cannot succeed; query the card detail for `status` and `statusReason` (see "Frozen or blocked cards" below).
3. **Common decline reasons**: insufficient balance or limit, an abnormal card status, a CVV or PIN mismatch, or a restricted MCC or merchant.
4. **Tell the cardholder**: explain the reason to the cardholder based on what you found above.
> For the standardised decline values and their meanings, see [Decline Codes](../how-to-use/transactions/decline-codes). For the authorization fields (`approveFlag`, `direction`, `authType` and others), see [Authorization](../how-to-use/transactions/authorization).
## Frozen or blocked cards: telling them apart and resolving them
Use the `status` and `statusReason` returned by the card detail endpoint (`GET /open-api/card/v1/detail`):
| Status | Meaning | Who can lift it |
| :- | :- | :- |
| `FROZEN` | Freeze initiated by the cardholder or by you, or triggered automatically by failed verification (for example three consecutive wrong CVV/PIN entries) | You can unfreeze it yourself |
| `BLOCKED` | Freeze initiated by the issuer or by risk control (for example a compliance review or a security restriction) | You cannot lift this directly; DCS must handle it |
| `INVALID` | The card's life cycle has ended; permanently void | Not recoverable |
* **Unfreezing a card you froze**: call `POST /open-api/card/v1/freeze` with `freeze=false` to unfreeze (`freeze=true` freezes). Freeze and unfreeze are **the same endpoint distinguished by the boolean `freeze` field**, not two separate endpoints.
* **`BLOCKED` cards**: read `statusReason` first (values such as `COMPLIANCE_REVIEW` or `SECURITY_RESTRICTION`) to understand why, then contact DCS through the [escalation path](./support-escalation) to have it lifted.
> For the card state machine, the full set of `statusReason` values and which operations are permitted in each state, see [Card Management](../how-to-use/cards/card-management).
## A refund or reversal has not arrived
Refunds and reversals appear in the system as authorizations with direction `INCOMING`, releasing funds that were held:
* once the merchant has initiated the refund or reversal there is an unavoidable delay before the money lands, so ask the cardholder to be patient first.
* if it still has not arrived well beyond a reasonable window, raise it with DCS through the [escalation path](./support-escalation) with the **original transaction identifier** (`transactionId`, plus the related `authId` / `originalAuthId`).
> The actual timing of a refund or reversal depends on how the channel processes it; if it clearly exceeds a reasonable window and still has not arrived, raise it with DCS through the escalation path. For the fields and scenarios of `INCOMING` authorizations, see [Authorization](../how-to-use/transactions/authorization).
## A cardholder disputes a transaction (dispute / fraud)
When a cardholder claims a transaction was unauthorised, or is in dispute with the merchant:
* check the transaction details first (merchant name `cardAcceptorNameLocation`, amount, timestamp) to rule out a purchase the cardholder forgot or a family member made;
* if it really is a dispute or a fraudulent transaction, file it following [Disputes](../how-to-use/disputes) and contact DCS through the [escalation path](./support-escalation) to follow up.
> Dispute and chargeback handling is currently coordinated with DCS operations, so contact DCS through the escalation path to follow up.
## KYC for an existing cardholder stays under review for a long time
When a cardholder's KYC status has not moved for a long time after they applied:
1. **Check document quality and labeling first**: make sure the documents submitted are legible, are not screenshots, and are labeled with the correct type and side (front of an ID card, selfie labeled correctly, and so on). For the list of acceptable documents, see [About KYC Documents](./faq-kyc-documents).
2. **Check the current status**: call the KYC query endpoint to see which stage the case is at; if more material is needed, ask the cardholder to supply it.
3. **Escalate after a reasonable wait**: if the documents are fine and the case is still sitting under review, raise it with DCS through the [escalation path](./support-escalation) with the `customerId` / `kycTicketId`.
> For the state machine and how to query it, see [Query KYC](../how-to-use/kyc/query-kyc); for rejection reasons, see [KYC Reject Codes](../how-to-use/kyc/kyc-reject-codes).
## Changing the shipping address of a physical card
Once physical card shipping details have been submitted they **normally cannot be changed in flight**. To correct them:
1. contact DCS through the [escalation path](./support-escalation) to cancel the user's current card;
2. ask the cardholder to reapply with the correct address.
> The standard way to correct a shipping address today is to cancel the current card and reapply with the correct one. To look up shipping details, see [Card Management](../how-to-use/cards/card-management) (`GET /open-api/card/v1/shipping-info`).
## Blocking a suspicious merchant
To block a merchant:
* under the Partner-Managed model you can decline a specific merchant or MCC directly in your own authorization decision (`approveFlag=D`), which is the fastest self-service route;
* if you need a harder block at the DCS or scheme level, raise it through the [escalation path](./support-escalation) with the **merchant identifier and the reason for blocking**.
***
## Next steps
To raise an issue with DCS, or to understand who owns which class of issue and how quickly it is handled, go to [Escalation and Support Path](./support-escalation). For questions about getting ready to launch, see the [Pre-Go-Live FAQ](./pre-go-live).
# Pre-Go-Live FAQ
Source: https://docs.thedecard.com/en/partner-managed/customer-success/pre-go-live
Answers the questions we are asked most often before go-live on the Partner-Managed model: how the launch track is divided between the two sides, the DCS configuration items (apiKey/secretKey/webhookUrl/authUrl/externalPublicKey), the three IP allowlists, RSA public key generation, how to integrate KYC, and the one-time secure retrieval of production keys. A pre-go-live checklist closes the page.
## π Guide
Whether you are an exchange, a wallet or a platform, only a handful of things need to be in place before you go live: have DCS set up your enterprise entity and card configuration, submit your callback and allowlist details to DCS, agree with DCS on how KYC will be integrated, and retrieve your production keys securely. This page answers the most common pre-go-live questions in Q\&A form.
DCS is a licensed issuer with its own BINs, and provides the full compliance stack from onboarding and KYC through to real-time authorization and reconciliation. The pre-go-live setup is shared between DCS and your team, and every item below is labeled with who owns it.
***
## What needs to be in place before go-live?
Partners on the Partner-Managed model complete the following track before go-live, with ownership as shown:
| Step | Owner | Description |
| - | - | - |
| 1. Set up the enterprise entity (Enterprise) and card configuration (Card Profile) | DCS | You tell us the card types you want to issue (virtual / physical), the card scheme and the card BIN; DCS creates your Enterprise together with its `apiKey` / `secretKey`, and creates the Card Profile (which yields `profileId`) |
| 2. Submit callback URLs and the encryption public key | Partner | Provide DCS with `webhookUrl`, `authUrl` and `externalPublicKey` (see below) |
| 3. Submit the three egress IPs for allowlisting | Partner | DCS maintains separate allowlists for API calls, webhooks and authorization forwarding notifications |
| 4. Agree on the KYC integration approach and Sumsub setup | Partner + DCS | KYC is always submitted through the `apply-kyc` endpoint; if you run your own Sumsub integration, you must configure the DCS partner token in your Sumsub console (see below) |
| 5. Issue the first card end to end in sandbox | Partner | Complete the card issuing, authorization and reconciliation loop in sandbox |
| 6. Retrieve production keys securely and switch to production endpoints | Partner + DCS | Retrieve the production `apiKey` / `secretKey` through a one-time secure link |
> For the full sequence diagram of the integration track (card issuing β card operations β physical card β card replacement β 3DS β authorization β reconciliation), see [Quickstart](../getting-started/quickstart) and [Preparation](../getting-started/first-steps).
***
## What will DCS configure for us?
DCS configures the following for every Enterprise. The Enterprise is the core identity object of the Partner-Managed model: it represents the institution integrating card issuing, and it is the hub between DCS services and your end users.
| Configuration item | Purpose | Provided by |
| - | - | - |
| `apiKey` / `secretKey` | Identifies you and traces the origin of each request; the admission credential for all API calls | **DCS provides these to you** |
| `webhookUrl` | Receives event notifications pushed by DCS (card order status changes, transaction authorization results, and so on) | **You provide this to DCS** |
| `authUrl` | Receives authorization forwarding requests; the core interaction endpoint for controlling funds | **You provide this to DCS** |
| `externalPublicKey` | The RSA public key used to encrypt sensitive data (amount, currency, and so on) in authorization notifications | **You provide this to DCS** |
> In sandbox, contact the DCS team for your `apiKey` / `secretKey`. For production, follow the secure retrieval process described below.
***
## How many egress IPs do we need to submit?
DCS maintains a separate allowlist for each of three entry points. When you submit your callback URLs, include the egress addresses for **both** sandbox and production:
* **API allowlist**: the egress IP that calls the `/open-api/` endpoints
* **Webhook allowlist**: the egress IP that receives DCS event notifications
* **Authorization forwarding allowlist**: the egress IP that receives authorization forwarding requests
> These three IPs may or may not be the same. Provide each one according to your actual network topology, so that nothing is rejected at go-live because it was never allowlisted.
***
## How do we generate the RSA public key (externalPublicKey)?
`externalPublicKey` encrypts the sensitive data carried in authorization notifications. You generate the key pair locally, hand the **public key** to DCS and keep the **private key** safe on your side. Generate it as follows:
```shell theme={null}
# Generate private key
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# Derive the public key from the private key
openssl rsa -pubout -in private_key.pem -out public_key.pem
# Strip the header and footer lines (-----BEGIN/END PUBLIC KEY-----) and remove line breaks to get a single-line string
cat public_key.pem | sed '/BEGIN/d; /END/d' | tr -d '\n'
```
Submit that single-line string to DCS as your `externalPublicKey`.
> Authorization notifications use RSA mutual signing plus encryption. For the full explanation of keys, signing and verification, see [Authentication and Signing](../integration-resources/authentication).
***
## How is KYC integrated?
Whether or not you run your own Sumsub integration, DCS always collects KYC material through the `apply-kyc` endpoint, and review takes roughly 2β3 days. When the same user applies for multiple cards, the KYC material only has to be uploaded once and the `kycTicketId` can be reused.
**If you run your own Sumsub integration**, you need to configure the DCS partner token for both your UAT and PROD environments in Sumsub. **Before go-live, make sure your Sumsub contract covers the sharing-partner relationship with DCS** β without it your launch will be held up.
> For the details, see [About the KYC Vendor](./faq-kyc-vendor) and [About KYC Documents](./faq-kyc-documents); for the application flow, see [Apply for KYC](../how-to-use/kyc/apply-kyc).
***
## How do we retrieve production keys securely?
> For sandbox `apiKey` / `secretKey`, simply contact the DCS team.
To keep production keys from being exposed to third parties, the production `apiKey` / `secretKey` are retrieved through a **one-time secure link**:
1. **You provide**: a secure email address, plus the IP that will request the keys. The email address receives the retrieval instructions, and the IP is added to the retrieval allowlist.
2. **DCS sends**: an email arrives at that secure address containing a temporary secure link (**valid for a single retrieval only**).
3. **You retrieve**: concatenate `extractUrl` with `extractSecretKey` and run it on the designated machine to retrieve the keys.
Fields returned in the email:
| Name | Type | Description |
| - | - | - |
| `expireTime` | string | Validity period of the extraction security code |
| `extractSecretKey` | string | Key extraction security code |
| `extractUrl` | string | Key extraction URL |
| `howToUse` | string | How to use it |
| `notes` | string | The keys can only be extracted once |
Example of a successful response:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": {
"expireTime": "2024-10-21T16:52+08:00[Asia/Shanghai]",
"extractSecretKey": "4f72b1e9c49e4ac1bbc2cd12b5e44993",
"extractUrl": "https://{domain}/internal/open-api/v1/secret-extract/",
"howToUse": "Please concatenate the url with the secret-key and execute it on the specified machine.",
"notes": "This link is only valid for one AKSK extraction, if the content is not properly accessed, the AKSK may have been compromised, please contact us promptly."
}
}
```
If the retrieval fails, assume the keys may have been exposed within the validity window: talk to your business contact and have the email reissued.
> Every response on this page uses the standard envelope `{code, message, messageDetail, data}`, where success is indicated by `code` = `SYS_SUCCESS`. See [Authentication and Signing](../integration-resources/authentication) for details.
***
## Pre-go-live checklist
* [ ] Card types, card scheme and card BIN confirmed, and DCS has created the Enterprise (`apiKey`/`secretKey`) and Card Profile (`profileId`)
* [ ] `webhookUrl`, `authUrl` and `externalPublicKey` submitted to DCS
* [ ] Sandbox and production egress IPs submitted for all three entry points: API, webhook and authorization forwarding
* [ ] KYC integration approach agreed with DCS (Sumsub or non-Sumsub), with the Sumsub contract and tokens in place
* [ ] Card issuing β authorization β reconciliation loop completed end to end in sandbox
* [ ] Production keys retrieved through the one-time secure link and endpoints switched to production
***
## Next steps
Once you are live, keep the [Post-Go-Live FAQ](./post-go-live) at hand for day-to-day operational questions; when an issue needs to be escalated, see [Escalation and Support Path](./support-escalation).
# Escalation and Support Path
Source: https://docs.thedecard.com/en/partner-managed/customer-success/support-escalation
A single page on the operational escalation path: when to escalate, who to escalate to (your DCS Customer Success contact channel), what reference details to bring with you (transactionId/authId/outsId/panLast4/cardId/customerId/kycTicketId and so on), how disputes and fraud are handled today, and where to find the reference codes for self-diagnosis.
## π Guide
Whether you are dealing with a transaction that has not landed, a KYC case stuck in review, or a cardholder reporting a suspicious charge, DCS gives you one clear escalation path to get the case to the right team quickly. This page explains **when to escalate, who to escalate to, and what to bring**, so the investigation can be done in a single round.
As a licensed issuer, DCS provides post-launch support to partners through a dedicated Customer Success team. Anything involving the issuing channel (the card scheme), clearing or disputes is taken up with the channel by DCS on your behalf, so you never need to contact the scheme yourself.
## How to escalate
Submit your case through the **Customer Success contact channel** agreed with DCS (your dedicated Customer Success contact, the shared mailbox, or the project chat group). To help the DCS team pinpoint the case faster, **bring as much of the following as you can**:
| Detail | Where the field comes from | Provided by |
| - | - | - |
| Transaction ID | `transactionId` from the transaction report or webhook | Partner |
| Authorization ID | `authId` from the authorization report or authorization webhook | Partner |
| Statement ID | `outsId`, which links authorization to clearing | Partner |
| Last 4 digits of the card number | `panLast4` from the card detail (a card detail endpoint field; see [Card Management](../how-to-use/cards/card-management)) | Partner |
| Card ID | `cardId` | Partner |
| User ID | `customerId` (where the case relates to a user) | Partner |
| Decline code | The decline code from the authorization webhook or the reports (see [Decline Codes](../how-to-use/transactions/decline-codes)) | Partner |
| Error logs / payload screenshots | The raw request and response of the failing call | Partner |
for transaction cases, `transactionId` plus the last 4 digits of the card number is usually enough to locate the record; for KYC cases, attach `customerId` and `kycTicketId`; for API errors, attach the full request and response payload, including `code` and `messageDetail` from the standard response envelope.
## When to escalate
Escalate to the DCS Customer Success team promptly in the following situations:
* **A refund has not arrived for a long time**: the merchant has refunded, but the cardholder's account still shows nothing after a long period (as a guide, more than 30 business days).
* **A KYC application is stuck**: the KYC ticket has been sitting under review well beyond the committed review window (about 2β3 business days). Before escalating, confirm that the documents submitted are legible and that the front, back and selfie are labeled correctly (see [About KYC Documents](./faq-kyc-documents)).
* **A cardholder reports a suspicious or unauthorised transaction**: the cardholder says a charge was not made by them, and suspects fraud or an incorrect debit.
* **You need a suspicious merchant blocked**: you have identified a risky merchant and want it blocked.
* **An API keeps returning errors and you cannot pinpoint the cause**: you have already checked your [authentication configuration](../integration-resources/authentication), the error code and the request payload without resolving it.
## Handling disputes and fraud
When a cardholder needs to raise a transaction dispute or chargeback over an **unauthorised transaction, an incorrect debit, or a merchant that failed to deliver**:
* **Today**: submit the case through the Customer Success contact channel above, and DCS will raise the dispute with the scheme and follow the outcome through on your behalf. Provide the `transactionId`, the last 4 digits of the card number, the dispute reason and whatever evidence you have (order screenshots, correspondence with the merchant, and so on).
* **Fraud containment**: where fraud is confirmed, freeze the affected card at the same time through the [freeze endpoint](../how-to-use/cards/card-management) (`POST /open-api/card/v1/freeze` with `freeze=true`), or cancel it outright, to prevent further loss.
disputes and fraud are currently submitted and followed up with the help of the DCS Customer Success contact channel. Self-service endpoints for raising a dispute, uploading evidence and checking dispute progress are on the roadmap (see [Disputes How-to](../how-to-use/disputes)); this page will be updated to describe the self-service flow once they are live.
## Where to find the reference codes for troubleshooting
Before escalating, check the following data dictionaries: many cases can be diagnosed from them without help.
| Scenario | Reference code / dictionary |
| - | - |
| A card purchase was declined | Decline code β [Decline Codes](../how-to-use/transactions/decline-codes) |
| Card issuing failed | Card order error code (`errorCode` on a `FAILED` status) β [card application error codes](../how-to-use/cards/card-order-codes) |
| KYC failed | KYC reject code β [KYC Reject Codes](../how-to-use/kyc/kyc-reject-codes) |
| An API call returned an error | Standard API response error code (the `code` field) β [decline and error codes](../how-to-use/transactions/decline-codes) |
| Understanding authorization and transaction types | Authorization dictionary (`authType` / `transactionType`) β [Authorization and Settlement](../how-to-use/transactions/auth-and-settlement) |
## Next steps
Before escalating, it is worth going through the [Post-Go-Live FAQ](./post-go-live): many common issues (mobile wallet provisioning failures, delayed refunds, pending applications and so on) already come with self-service guidance.
# First Steps
Source: https://docs.thedecard.com/en/partner-managed/getting-started/first-steps
The second page of Getting Started, following Overview and leading into Quickstart.
***
## π Guide
Before you call any API, you need to complete onboarding with DCS, receive your credentials, and configure your callbacks. This page gets those prerequisites in place; once they are all done, head to [Quickstart](./quickstart) to issue your first card end to end.
### Preparation steps
1. **Contact DCS to get onboarded**
Tell us the card types you plan to issue (virtual card / physical card), the card network, and the BIN. DCS creates an **Enterprise** for you and assigns your **card configuration (`profileId`)** offline.
2. **Receive your credentials**
DCS generates an `api_key` and a `secret_key` and hands them over to you. The `secret_key` is used to sign requests, so keep it safe. For the signing rules, see the [Authentication guide](../integration-resources/authentication).
3. **Configure your callbacks**
Provide DCS with three items: `webhook_url` (the endpoint for business notifications), `auth_url` (the endpoint for authorization notifications), and `external_public_key` (the RSA public key used to verify signatures on authorization notifications).
4. **Get your IP whitelisted**
Send DCS your static egress IP. You can call the APIs only after DCS has added it to the whitelist.
5. **You are ready**
Once the steps above are done, go to [Quickstart](./quickstart) and issue your first card in the sandbox environment.
We provide a Java SDK to accelerate integration (optional): [dcs-connector-java](https://github.com/decard-tech/dcs-connector-java). Import it, plug in your own apiKey / secretKey and you are ready to go β the SDK handles the signing for you.
```xml theme={null}
com.dcscc.connector
dcs-card-bridge-connector-java-clients
{latest-version}
```
***
### Environments and conventions (integration reference)
**Base URLs**
| Environment | URL |
| - | - |
| Sandbox | `https://api.thedecard-sandbox.com` |
| Production | `https://api.thedecard.com` |
**Global conventions**
* Business time fields are in the **UTC+8** time zone (`X-DAPI-TIMESTAMP` is a Unix epoch timestamp in milliseconds, timezone-independent)
* Currencies use **ISO 3-letter currency codes**; countries use **ISO 2-letter country codes**
* Unless stated otherwise, **Enterprise** refers to your partner team
> π **PCI**: Choose the appropriate APIs based on whether your company holds a PCI license. If it does, tell DCS in advance and provide your certification, since PCI status determines whether you can call the sensitive card data APIs.
***
## Next steps
With the prerequisites in place, go to [Quickstart](./quickstart) to issue your first card in the sandbox environment. If you want to understand request signing and field encryption, see the [Authentication guide](../integration-resources/authentication).
# Overview
Source: https://docs.thedecard.com/en/partner-managed/getting-started/overview
An overview of the DCS card issuing platform and the Partner-Managed model. This is the entry point for the whole integration guide.
## π Guide
DCS is a modern card issuing platform built for businesses. Whether you are an exchange, a wallet, or a platform, you can integrate card issuing into your own system through developer-friendly APIs, issue virtual and physical cards to your end users, and let them top up with cryptocurrency and spend in fiat.
DCS is licensed by the Monetary Authority of Singapore (MAS) and issues on its own BINs across multiple issuing markets and card networks, providing a full-stack compliant infrastructure that covers everything from card issuing and KYC to real-time authorization and settlement.
This guide is written for partners integrating under the **Partner-Managed model**.
## What is the Partner-Managed model
DCS offers two integration models: **DeCard-Managed** and **Partner-Managed**. Under the **Partner-Managed model**, **you control the spending limits on the card**: you decide how much a cardholder may spend, and that spending draws down the Enterprise security deposit you hold with DCS. You make the real-time authorization decision, while DCS handles card issuing, settlement, and reconciliation.
> To understand how money moves first (how user wallets relate to the Enterprise security deposit, and why DCS must pay the card networks on a T+1 basis), see the [Fund Model](../basic-concepts/fund-model).
## What DCS provides
* **End-to-end issuing**: from Enterprise and card configuration, card issuing, and KYC through to real-time authorization forwarding, settlement, and daily reconciliation
* **Developer first**: RESTful APIs and webhooks; HMAC authentication, with mutual RSA signing on authorization notifications
* **Compliance and security**: licensed issuing, Sumsub KYC/AML, and protection of sensitive card data within PCI scope
## Suggested reading order
This guide is organized to get you integrated as quickly as possible: **First Steps β Quickstart β Users and KYC β Card Issuing β Authorization and Transactions β Webhooks / Reconciliation**. The Basic Concepts group (fund model, transaction lifecycle, authorization forwarding) is background material. It does not block your integration, so read each topic as you reach the matching step.
## Next steps
Go to [First Steps](./first-steps) to get ready for integration: onboarding, collecting your keys, and configuring callbacks and the IP allowlist. Once that is done, issue your first card in the [Quickstart](./quickstart).
# Quickstart
Source: https://docs.thedecard.com/en/partner-managed/getting-started/quickstart
The third page of Getting Started. It walks you through issuing your first virtual card in the sandbox using the step-by-step path we recommend: create the customer, apply for a KYC ticket, then apply for the virtual card.
***
## π Guide
This guide gets the core endpoints working in the sandbox and issues your first virtual card, in five steps.
Sandbox base URL: `https://api.thedecard-sandbox.com`
> The examples below leave out authentication headers and field encryption. Every request must carry the four authentication headers (`X-DAPI-API-KEY` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE` / `X-DAPI-SIGN`) and set `Content-Type: application/json`, and sensitive fields such as phone number, email and KYC data must be AES encrypted. For the rules, see [Authentication](../integration-resources/authentication).
> **Why the step-by-step path** (create customer, apply for KYC, apply for card):
>
> * **The KYC ticket is reusable**: when the same customer applies for a second card there is no need to resubmit documents, simply reuse the same `kycTicketId`.
> * **Clearer states, easier troubleshooting**: KYC and card issuing are decoupled, each with its own ticket and its own state line.
### 1. Create the customer
```http theme={null}
POST /open-api/customer/v1/create-customer
```
```json theme={null}
{ "phoneCountryCode": "SG", "phone": "+6591234567", "email": "user@example.com", "customerRef": "your-uniq-ref-001" }
```
The response returns `customerId`, which every later step needs:
```json theme={null}
{ "code": "SYS_SUCCESS", "message": "success", "messageDetail": null, "data": { "customerId": "C100001", "status": "SUCCEED" } }
```
### 2. Apply for a KYC ticket
> Upload the identity documents first: call `POST /open-api/intent-ticket/v1/generate-pre-upload-url` (`businessType=CREATE_CARD_KYC`) to get a temporary upload URL, PUT the file, then take the returned `objectKey` and pass it as `identityProofUrl` below. For the full sequence, see [Apply for KYC](../how-to-use/kyc/apply-kyc), "Option 2, step 1".
```http theme={null}
POST /open-api/kyc-ticket/v1/apply-kyc
```
```json theme={null}
{
"kycTicketRef": "your-kyc-ref-001",
"customerId": "C100001",
"identifyProofList": [ { "identityProofType": "PASSPORT", "identityProofUrl": "" } ],
"addressProofList": [],
"kycCareerInfo": { "employmentStatus": "EMPLOYED", "occupation": "..." }
}
```
The response returns `kycTicketId`:
```json theme={null}
{ "data": { "kycTicketId": "KYC_1a2b", "kycTicketRef": "your-kyc-ref-001", "status": "PENDING" } }
```
> **Wait for the ticket to reach `PASSED`**: take the result from the `KYC_TICKET` webhook or from the query endpoint. If the status is `NEED_VERIFY`, guide the customer through liveness verification. For document fields, the state machine and rejection codes, see [How-to Β· Apply for KYC](../how-to-use/kyc/apply-kyc); for eligible regions and the document requirements of each region, see [Compliance and KYC](../basic-concepts/compliance-kyc-flow).
### 3. Apply for the virtual card (NORMAL mode)
Once the KYC ticket is `PASSED`, apply for the card with `kycTicketId` plus `customerId`:
```http theme={null}
POST /open-api/card-order/v1/apply-virtual
```
```json theme={null}
{
"profileId": "your-card-profile-id",
"cardOrderRef": "your-order-ref-001",
"cardApplyMode": "NORMAL",
"customerId": "C100001",
"kycTicketId": "KYC_1a2b"
}
```
> The response returns `cardOrderId`. For any further card the same customer applies for, **just reuse the same `kycTicketId`, there is no need to resubmit KYC**.
### 4. Query the card order and wait for issuing to succeed
```http theme={null}
GET /open-api/card-order/v1/detail?cardOrderId=O100001
```
Poll until `status = COMPLETED`; the response then carries `cardId`:
```json theme={null}
{ "data": { "cardOrderId": "O100001", "status": "COMPLETED", "cardId": "CARD100001" } }
```
> Card order state line: `PENDING β CUSTOMER_PASS β KYC_PASS β CHANNEL_CUSTOMER_PASS β COMPLETED`. On `FAILED`, check `errorCode` / `errorReason` (see [Card application error codes](../how-to-use/cards/card-order-codes)).
### 5. Query the card details, the card is ready to spend
```http theme={null}
GET /open-api/card/v1/detail?cardId=CARD100001
```
```json theme={null}
{ "data": { "cardId": "CARD100001", "status": "ACTIVATED", "panFirst6": "441364", "panLast4": "1234" } }
```
> Virtual cards are **activated automatically** (physical cards must be activated). The full card number and CVV are only available through a dedicated endpoint, see [Retrieving card sensitive data](../how-to-use/cards/secure-card).
>
> Your first virtual card is now active. When the cardholder spends, DCS forwards the authorization request **to your `auth_url`** and you return Approve or Decline in real time. This is the heart of the Partner-Managed model, see [Authorization (forwarded decisions)](../how-to-use/transactions/authorization).
## Next steps
* The full card issuing flow and KYC details β [Card issuing](../how-to-use/cards/card-issuing)
* Freeze, unfreeze, replace a card, reset the PIN β [Card management](../how-to-use/cards/card-management)
* Understand how money moves β [Fund model](../basic-concepts/fund-model)
# Card issuing flow
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/card-issuing
The end-to-end integration path for issuing a card to your customers: create the customer record, submit a KYC ticket, await KYC approval, then apply for a virtual card. Each stage below sets out what you call, what DCS handles, and the status transitions to expect.
## π Guide
Whether your customers originate from an exchange, a wallet or a platform application, you can issue them a virtual card through the DCS card issuing endpoints and upgrade it to a physical card at a later stage. The issuing path is consistent in all cases: establish the customer record and complete KYC with DCS first, then submit a virtual card application once KYC has been approved.
As a licensed issuer operating its own BIN, DCS is responsible for card issuance, KYC review, authorization forwarding, clearing and reconciliation. As the partner, you own the cardholder-facing experience and your own limit and risk decisions.
## Card issuing sequence
The sequence is the same regardless of how the KYC documents are collected: **create the customer, apply for KYC (uploading documents first only if required), wait for KYC to pass, then apply for a virtual card**. When the same customer applies for another card later, the existing `kycTicketId` can be reused and the KYC documents do not have to be submitted again.
You choose one of two document-collection routes: collect the KYC documents in **your own interface** and submit them to DCS, or hand the customer off to a **DCS-hosted H5 page** that captures them for you. In both cases the review is performed by our KYC vendor β see [KYC vendor](../../customer-success/faq-kyc-vendor) for details. Everything after KYC approval is identical.
For the execution details of the two routes, see [Apply for KYC](../kyc/apply-kyc) and [H5 KYC guidance page](../kyc/h5-kyc-guidance).
> The two URLs do different jobs: `generate-pre-upload-url` only issues an S3 upload address, whereas `card-redirect/v1/guidance-link` issues a DCS-hosted H5 page. Use the former when you collect the documents in your own interface and upload the files yourself; use the latter when the end user has to complete liveness, information verification, supplementary card application details, KYC or KYC renewal on a DCS page.
## Core endpoint: apply for a virtual card
**`POST /open-api/card-order/v1/apply-virtual`**
> For the request and response fields, the corresponding endpoint page in the API reference is authoritative.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `profileId` | string | Required | Card profile ID. See [Card profile](./card-profiles); contact your DCS account team to obtain it. Max length 50 |
| `cardOrderRef` | string | Required | Card order idempotency key, generated by you. Max length 50 |
| `cardApplyMode` | string | Required | Fixed value `NORMAL`. The legacy `COMPLETE` mode still visible at the endpoint level is kept only for existing integrations and is scheduled for retirement β do not use it in new integrations |
| `kycTicketId` | string | Required | KYC ticket ID returned by "apply for KYC" |
| `customerId` | string | Required | Customer ID returned by "create customer" |
### Minimum request example
```json theme={null}
{
"profileId": "PROFILE_xxx",
"cardOrderRef": "ord-20260616-0001",
"cardApplyMode": "NORMAL",
"customerId": "C100001",
"kycTicketId": "KYC_1a2b"
}
```
### Response
The unified response structure is `{ code, message, messageDetail, data }`:
* `code` / `message`: the system-level return code and its text.
* `messageDetail`: a prompt you can show to the end user (`title` / `message` / `type` / `action` / `linkUrl` and so on), used in your front end to guide a document resubmission or a retry.
* `data`: the business payload, described in the table below.
**Response structure consistency**: this API uses `code` to indicate system-level success, while the business result is provided separately in `data.status`. When a card order is rejected, the response `code` may still indicate success, while `data.status=FAILED`. Partners should use `data.status` to determine the card application result and should not rely solely on `code` to determine whether a card application was successful. For the unified response structure and successful `code` values, please refer to the [Authentication guide](../../integration-resources/authentication); for the classification of card order failure `errorCode` values, please refer to [Card application error codes](./card-order-codes).
`data` fields:
| Field | Description |
| - | - |
| `cardOrderId` | Card order ID, used to query the order detail |
| `profileId` | Card profile ID |
| `type` | Card type: `VIRTUAL` / `VIRTUAL_TO_PHYSICAL` / `REPLACEMENT` |
| `customerId` | Customer ID |
| `cardId` | Card ID, returned once the order completes; every later card operation uses it |
| `status` | Card order status, see the state machine below |
| `errorCode` / `errorReason` | Failure code and reason when the order fails |
| `cardOrderRef` | The idempotency key echoed back |
| `needExtraInfo` | Whether a supplementary questionnaire is required (used when `type=VIRTUAL`) |
| `createTime` / `modifyTime` | Creation and last update time, formatted `yyyy-MM-dd'T'HH:mm:ss+08:00` |
### Virtual card order state machine
| status | Meaning |
| - | - |
| `PENDING` | Application received and queued for processing |
| `CUSTOMER_PASS` | Customer record checks have passed |
| `KYC_PASS` | KYC review has passed |
| `CHANNEL_CUSTOMER_PASS` | The issuing channel has accepted the cardholder; the card is being created |
| `COMPLETED` | Card issued. `cardId` is returned and the card is ready to spend |
| `FAILED` | The application failed. This can occur at any stage; read `errorCode` / `errorReason` to decide whether to retry or resubmit documents |
Once the order reaches `COMPLETED`, `data.cardId` is usable. **A virtual card needs no activation: it is ready to spend as soon as it is issued.**
> Note: the success value for this endpoint is `COMPLETED`.
## When extra information is requested
DCS may require supplementary information from the cardholder before an application can be approved. When we do, the card application returns `needExtraInfo = true`; the application stays in progress and will not reach `COMPLETED` until the request is resolved.
There is **no dedicated open-api endpoint** for this: the cardholder has to complete the supplementary application details on the H5 guidance page (`guidance-link` `type=6`). Where our review determines that the cardholder must supply additional KYC information, DCS automatically creates a **KYC extra information ticket** that tracks the request from creation through to approval or rejection. You do not create the ticket yourself.
1. **Detect the request.** The card order's `needExtraInfo` becomes `true` and you receive a `KYC_EXTRA_INFO_TICKET` webhook with the ticket in status `INIT`.
2. **Direct the user to submit.** `POST /open-api/card-redirect/v1/guidance-link` with `type=6` and the `cardOrderId` to obtain the H5 link, then open it for the user.
3. **Track the outcome.** The ticket moves to `PENDING` on submission, then to `PASSED` (the card order continues) or `REJECTED`, where the webhook carries `rejectReason` and `rejectRemark`.
4. **Query at any time.** `GET /open-api/kyc-extra-info-ticket/v1/list` returns the ticket list, statuses and rejection reasons for the card.
| Status | Description | Terminal | Your action |
| - | - | - | - |
| `INIT` | Ticket created, awaiting the user | No | Direct the user to the H5 page to submit the information |
| `PENDING` | Submitted, under review | No | Wait for the review result |
| `PASSED` | Approved | Yes | The card order continues; no action needed |
| `REJECTED` | Rejected or closed | Yes | Read `rejectReason` / `rejectRemark`. If the reviewer requests information again, DCS creates a new ticket |
Query by `cardOrderId` to return every ticket for that order, newest first, including the rejection reasons of earlier tickets. Pass `kycExtraInfoTicketId` (from the webhook) to narrow the result to one ticket.
A card order can have several tickets β a rejected request that the reviewer raises again creates a new one β but only one is in progress at any time. `rejectReason` and `rejectRemark` are populated only in `REJECTED`; in all other statuses they are empty strings.
## After the card is issued
1. Poll `GET /open-api/card-order/v1/detail` with `cardOrderId` (the response fields are the same as in the table above), or wait for the card order status webhook. Take `cardId` once the status is `COMPLETED`.
2. Call [card detail](./virtual-card) with `cardId` to read non-sensitive data such as `panFirst6` and `panLast4`.
3. To display the full card number or CVV: PCI-certified partners use [get sensitive card data](./secure-card); partners without PCI certification use the DCS-hosted page (guidance link), where the sensitive data is rendered directly in the end user's front end and never passes through your back end.
4. When the cardholder needs a physical card, call the virtual-to-physical endpoint (see [Physical card](./physical-card)).
5. If the order returns `needExtraInfo`, resolve the request first β see the section "When extra information is requested" above.
## Next steps
* Applying for, shipping and activating a physical card: see [Physical card](./physical-card).
* Day-to-day operations such as freeze, unfreeze, termination and PIN reset: see [Card management](./card-management).
* Card order statuses and how failure `errorCode` values are grouped, including whether a retry or a document resubmission helps: see [Card application error codes](./card-order-codes).
# Card management (freeze / unfreeze / replacement / PIN)
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/card-management
Clears up the ambiguity of freeze and unfreeze sharing one endpoint by spelling out the boolean switch.
## π Guide
Once a card is issued it has to be maintained for the rest of its life: the cardholder may want to freeze it for a while, terminate it after losing it, activate the physical card when it arrives, or reset a forgotten PIN. Whether the request comes from an operator in your back office or from the cardholder tapping a button in your app, the same `/open-api/card/v1/` endpoints let you address one specific card by `cardId` and act on it.
As a licensed issuer with its own BIN, DCS keeps the underlying state in sync with the card network and the issuing processor. You only have to reason about the business semantics: what state this card is in, and what may be done to it.
> **Prerequisite**: you need a successfully issued card and its `cardId`. If you are not sure how to apply for one, read [Card issuing](./card-issuing) first.
## Mental model: the card state machine
DCS expresses the life cycle of a card in the `status` field, and every management operation is really just a transition in this state machine.
| Status | Meaning | Who can change it | Operations allowed |
| :- | :- | :- | :- |
| `PENDING_ACTIVATION` (awaiting activation) | The initial state of an issued physical card that has not been activated yet (physical cards only) | Partner | Activate |
| `ACTIVATED` (active) | The card can transact normally | Partner | Freeze, terminate |
| `FROZEN` (frozen) | A suspension raised by the cardholder or the partner, which the partner can lift | Partner | Unfreeze, terminate |
| `BLOCKED` (blocked) | A block applied by the issuer or by risk control, which the partner **cannot** lift | DCS only | Wait for DCS to lift it, terminate |
| `INVALID` (invalid) | Terminated, permanently dead, **irreversible** | β | None |
> **How FROZEN differs from BLOCKED**: `FROZEN` is a reversible switch in the partner's hands, while `BLOCKED` is a restriction imposed by the issuer or risk control for compliance or security reasons and only DCS can lift it. Neither is final; `INVALID` is the only final state.
### Card status reason (statusReason)
While a card is `FROZEN`, `BLOCKED` or `INVALID`, the `statusReason` returned by the card detail endpoint tells you why it got there, which you can use to explain the situation to the cardholder or to decide what to do next.
| statusReason | Applies to | Description |
| :- | :- | :- |
| `NORMAL` | All | No specific reason |
| `USER_FREEZE` | FROZEN | The cardholder froze the card |
| `USER_REQUESTED_CLOSURE` | BLOCKED | The cardholder asked to close the card |
| `COMPLIANCE_REVIEW` | BLOCKED | Blocked for compliance review |
| `AUTHENTICATION_FAILED` | FROZEN | Frozen after failed verification (three consecutive wrong CVV or PIN entries; the card is suspended) |
| `SECURITY_RESTRICTION` | BLOCKED | Blocked by risk control for security reasons |
| `VERIFICATION_OVERDUE` | BLOCKED | Verification overdue (the customer did not respond or supply the required evidence within the deadline) |
| `ACCOUNT_TERMINATED` | BLOCKED | The account has been terminated |
| `OTHER` | BLOCKED | Other reasons |
***
## Operations at a glance
Apart from the query endpoints, all of the endpoints below are `POST` and all sit under `/open-api/card/v1/`. For the authentication headers, the signature and the response envelope, see [Authentication and security](../../integration-resources/authentication).
| Operation | Method and path | Who does it | Applies to |
| :- | :- | :- | :- |
| Query card detail | `GET /open-api/card/v1/detail` | Partner | All cards |
| Freeze / unfreeze | `POST /open-api/card/v1/freeze` | Partner | All cards |
| Terminate | `POST /open-api/card/v1/terminate` | Partner | All cards |
| Activate a physical card | `POST /open-api/card/v1/physical-active` | Partner | Physical cards only |
| Reset PIN | `POST /open-api/card/v1/reset-pin` | Partner (PCI) | All cards |
| Get card shipping information | `GET /open-api/card/v1/shipping-info` | Partner | Physical cards only |
| Get sensitive card data | `POST /open-api/card/v1/retrieve-secure-card` | Partner (PCI) | All cards |
| Card replacement (reissue) | `POST /open-api/card-order/v1/replace` | Partner | All cards |
most of the operations above live under `/open-api/card/v1/`. **Card replacement** belongs to the card order domain, `/open-api/card-order/v1/replace`, and is covered in [Card replacement (reissue)](#card-replacement-reissue) below.
### Common call sequence for card operations
Freeze, unfreeze, terminate and the other action endpoints all follow the same sequence: the partner receives the cardholder's request, calls the matching endpoint, and returns the result.
> **About the response envelope**: every endpoint returns the same envelope, `{ code, message, messageDetail, data }`. `code` carries the result, `message` and `messageDetail` carry the display text (`messageDetail` can include a title, an icon and a link you can show to the end user), and `data` carries the business payload. For the success value of `code` and the full error code dictionary, see [Authentication and security](../../integration-resources/authentication).
***
## Query card detail
```
GET /open-api/card/v1/detail
```
| Parameter | In | Type | Required | Description |
| :- | :- | :- | :- | :- |
| `cardId` | query | string | Yes | Card ID |
Returns the card status, `statusReason`, card type, `panFirst6` (the first six digits of the card number), `panLast4` (the last four digits) and similar data. It does **not** return the full card number, the CVV or any other sensitive data (for that, see "Get sensitive card data" below).
```json theme={null}
{
"code": "...",
"message": "...",
"data": {
"cardId": "card_xxx",
"enterpriseId": "ent_xxx",
"profileId": "prof_xxx",
"type": "VIRTUAL",
"customerId": "cus_xxx",
"status": "ACTIVATED",
"statusReason": "NORMAL",
"panFirst6": "441364",
"panLast4": "0123",
"createTime": "2026-01-01T10:00:00+08:00",
"modifyTime": "2026-01-01T10:00:00+08:00"
}
}
```
***
## Freeze and unfreeze
**Freeze and unfreeze are the same endpoint**, and the `freeze` boolean decides the direction. This is the easiest trap on this page, so do not treat them as two endpoints.
```
POST /open-api/card/v1/freeze
```
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `cardId` | string | Yes | Card ID, max length 50 |
| `freeze` | boolean | Yes | **The freeze switch**: `true` freezes the card, `false` unfreezes it |
| `freezeReason` | string | See below | Reason for freezing, max length 20, for example `USER_FREEZE` or `NORMAL` |
> **When `freezeReason` is required**: only when freezing (`freeze=true`). When unfreezing (`freeze=false`) the server does not read the field, so it is ignored whether you send it or leave it out.
>
> Unfreezing the same card is rate-limited per day; exceeding it returns `DAPI_CARD_UNFREEZE_DAILY_LIMIT_EXCEEDED`, so retry the next day.
Request example (freeze):
```json theme={null}
{
"cardId": "card_xxx",
"freeze": true,
"freezeReason": "USER_FREEZE"
}
```
Request example (unfreeze):
```json theme={null}
{
"cardId": "card_xxx",
"freeze": false,
"freezeReason": "NORMAL"
}
```
The example above is shown for completeness; an unfreeze request can carry nothing but `cardId` and `freeze=false`.
The response `data` returns the whole card object as it stands after the operation (the same structure as [Query card detail](#query-card-detail)), with `status` now `FROZEN` or `ACTIVATED`.
the `freeze` endpoint only moves a card between `ACTIVATED` and `FROZEN`. A `BLOCKED` card was blocked by the issuer or by risk control, the partner cannot release it through this endpoint, and DCS has to be contacted.
***
## Termination
Termination is **irreversible**. Once a card is terminated its status becomes `INVALID` and it can never be brought back into use.
```
POST /open-api/card/v1/terminate
```
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `cardId` | string | Yes | Card ID, max length 50 |
| `invalidateReason` | string | Yes | Reason for termination, max length 20. `CARD_LOST` for a lost card, `CARD_STOLEN` for a stolen one |
```json theme={null}
{
"cardId": "card_xxx",
"invalidateReason": "CARD_LOST"
}
```
The response `data` returns the terminated card object, with `status` = `INVALID`.
***
## Card replacement (reissue)
When a cardholder's card is lost, damaged or otherwise has to be swapped, you can raise a **card replacement (reissue)** order against an existing card: the old card is voided and a new one is issued. A replacement creates a card order with `type=REPLACEMENT`.
> **Hard rules for card replacement**:
>
> * **The replacement is always a virtual card.** Both virtual and physical cards can be replaced, but the new card is always virtual. If the cardholder still needs a physical card, run [virtual-to-physical](./physical-card) again after the replacement completes.
> * **Card replacement is rate-limited within a 24-hour window**; exceeding it returns `DAPI_REPLACE_CARD_APPLY_LIMIT_EXCEEDED`, so back off before retrying.
> * **The card number (PAN) changes.** This differs from virtual-to-physical, where the PAN is preserved: a replacement issues a brand-new PAN. Remind the cardholder to update any stored auto-debits, subscriptions and card-on-file details with merchants.
```
POST /open-api/card-order/v1/replace
```
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `cardOrderRef` | string | Yes | Card order idempotency key, max length 50 |
| `replaceCardId` | string | Yes | The `cardId` being replaced, max length 50 |
```json theme={null}
{
"cardOrderRef": "your-idempotent-ref",
"replaceCardId": "card_xxx"
}
```
A successful response returns a card order. The key fields are:
| Field | Description |
| :- | :- |
| `cardOrderId` | Card order ID |
| `type` | Order type, `REPLACEMENT` here |
| `cardId` | The `cardId` of the new card |
| `replaceCardId` | The `cardId` of the card that was replaced |
| `status` | Order status, see the note below |
| `errorCode` / `errorReason` | Failure code and reason when the order fails |
> **Which order statuses to trust**: in the current code, a card order succeeds with the final status `COMPLETED` and fails with `FAILED`, and whether `cardId` has been generated is a useful cross-check. Integrate against `COMPLETED`; some older apiReference pages still describe `PENDING/SUCCEED/FAILED`.
### Card replacement flow
***
## Activate a physical card
**Only physical cards need activation; a virtual card is `ACTIVATED` as soon as it is issued.** A physical card sits in `PENDING_ACTIVATION` from the moment it is shipped, and the partner calls this endpoint once the cardholder has it in hand.
```
POST /open-api/card/v1/physical-active
```
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `cardId` | string | Yes | Card ID, max length 50 |
The response `data` returns the card object with `status` moved from `PENDING_ACTIVATION` to `ACTIVATED`. For how a physical card is applied for and shipped, see [Physical card](./physical-card).
***
## Get card shipping information
Once a physical card has shipped you can look up the tracking number and show the cardholder how the delivery is progressing.
```
GET /open-api/card/v1/shipping-info
```
| Parameter | In | Type | Required | Description |
| :- | :- | :- | :- | :- |
| `cardId` | query | string | Yes | Card ID |
```json theme={null}
{
"code": "...",
"data": {
"cardId": "card_xxx",
"trackingNumber": "SF1234567890",
"trackingCompanyName": "SF Express"
}
}
```
***
## Reset PIN
`reset-pin` sets the PIN of a physical card. To meet PCI requirements, every sensitive field (the card's expiry date, the CVV2, the last four digits of the card number and the new PIN) must be **encrypted with AES/GCM** before it is sent, and the request must carry the `iv` used for that encryption.
> **This endpoint is only open to partners that hold PCI certification.** If you do not, use the guidance page instead (see "Resetting the PIN and revealing card credentials without PCI certification" below).
```
POST /open-api/card/v1/reset-pin
```
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `cardId` | string | Yes | Card ID, max length 50 |
| `encryptedExpireDate` | string | Yes | The encrypted expiry date of the card, max length 200 |
| `encryptedCvv2` | string | Yes | The encrypted CVV2 of the card, max length 200 |
| `encryptedPanLast4` | string | Yes | The encrypted last four digits of the card number, max length 200 |
| `encryptedNewPin` | string | Yes | The encrypted new PIN, max length 200 |
| `iv` | string | Yes | The initialization vector used to encrypt and decrypt, max length 20 |
The response `data` is a boolean; `true` means the reset succeeded.
**Encryption algorithm (AES/GCM/NoPadding)**: use the enterprise secret (`enterpriseSecret`) as the AES key, generate a random 12-byte IV (Base64 encoded) for every call, and use a 128-bit authentication tag. Reference implementation:
```java theme={null}
// Generate a random IV (12 bytes is the recommended size for GCM)
public static String generateIV() {
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
return Base64.getEncoder().encodeToString(iv);
}
// GCM encryption
public static String encryptGCM(String plaintext, String enterpriseSecret, String base64IV) {
SecretKey key = new SecretKeySpec(Base64.getDecoder().decode(enterpriseSecret), "AES");
byte[] iv = Base64.getDecoder().decode(base64IV);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
byte[] ct = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(ct);
}
```
> For the concepts behind the PIN, what a PIN and a CVV are each for and when they are needed, see [Virtual card](./virtual-card).
***
## Get sensitive card data
`retrieve-secure-card` returns the full card number (`pan`), the `cvv2` and the expiry date (`expireDate`). The values come back encrypted and are decrypted with the `iv` using the same AES/GCM algorithm as above.
> **This endpoint is only open to partners that hold PCI certification.** The decrypted plaintext should be shown to the cardholder directly in the front end and **must not be persisted anywhere in the partner back end**.
```
POST /open-api/card/v1/retrieve-secure-card
```
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `cardId` | string | Yes | Card ID, max length 50 |
```json theme={null}
{
"code": "...",
"data": {
"cardId": "card_xxx",
"pan": "",
"cvv2": "",
"expireDate": "",
"iv": ""
}
}
```
The alternative for partners without PCI certification, and the details of the two-step SecureToken retrieval, are covered in [Get sensitive card data](./secure-card).
***
## Resetting the PIN and revealing card credentials without PCI certification
A partner without PCI certification **cannot** call `reset-pin` or `retrieve-secure-card` directly and should use the guidance page instead: a DCS-hosted page takes the cardholder through setting the PIN or revealing the card credentials, so the sensitive data never touches the partner's systems.
```
POST /open-api/card-redirect/v1/guidance-link
```
The endpoint returns a DCS-hosted guidance link that the partner passes to the cardholder to open in the front end. The full parameters and usage of the guidance page are described in [Get sensitive card data](./secure-card).
***
## Next steps
Issuing and maintaining cards is now covered end to end. Next you can:
* Configure spending limits, see [Card limits (velocity limits)](./velocity-limits);
* Handle the real-time authorization raised when a cardholder pays, see [Authorization forwarding](../transactions/authorization).
# Card application error codes
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/card-order-codes
The card application error codes from the original data dictionary, broken out into the card domain.
## Guide
Whether a card order fails on an upfront environment check, on a conflict in the customer data, or deep inside KYC review or card network issuing, a single `errorCode` field tells you which of three things to do: change a parameter and resubmit, ask the user to supply more documents, or accept a final decision and hand it to support. Every failed application turns into a clear next action instead of a vague "card application failed". As a licensed issuer, DCS normalizes the failure reasons of all three card order types (virtual card application, virtual-to-physical conversion and card replacement) into one stable set of `errorCode` values that partners can consume directly in their own products.
### Where the error code appears
When a card order moves to `status=FAILED`, the order response carries a pair of fields describing why:
| Field | Type | Description |
| - | - | - |
| `errorCode` | String | The normalized failure reason code; the possible values are listed in the tables below |
| `errorReason` | String | The human-readable text that goes with `errorCode`, which may be phrased for the end user |
When a card order is `status=FAILED`, `cardId` in `data` is empty and only then do `errorCode` and `errorReason` carry values. For example, `errorCode=INVALID_POA` is a case the user can fix by resubmitting documents, and it comes with `needExtraInfo=true` (see "Document resubmission" below). The `data` fields are defined in [Card issuing and card orders](./card-issuing).
the full response envelope (`code`, `message`, `messageDetail`, `data`) is described in [Card issuing and card orders](./card-issuing); this page is only about `errorCode` and `errorReason`.
The pair appears in the two places below with **identical values**, so you can read whichever channel suits you:
* **Query card order detail** (you pull): see [Card issuing and card orders](./card-issuing); when `status=FAILED` the response carries `errorCode` and `errorReason`.
* **CARD\_ORDER webhook** (we push): DCS notifies you whenever a card order changes status, and `data` carries the same `errorCode` and `errorReason`. For the structure, see [Webhook events and data structures](../webhooks/events-and-schema).
> **Who does what**: the normalization is done by **DCS**. You never have to deal with the raw failure codes of the underlying KYC provider (Sumsub, for instance) or of the card network; you only integrate against the single set of `errorCode` values in the tables below. Mapping the raw codes onto this set is DCS's job, and DCS notifies partners in advance whenever a new value is added.
### How card order status relates to the failure reason
The possible `status` values **depend on the card order type (`type`)**. A failure reason only appears once the order reaches `FAILED`; non-final states such as `PENDING` must never be read as a failure:
| `type` | Status sequence, intermediate and final | When `FAILED` appears |
| - | - | - |
| `VIRTUAL` (virtual card application) | `PENDING β CUSTOMER_PASS β KYC_PASS β CHANNEL_CUSTOMER_PASS β COMPLETED` / `FAILED` | As soon as any stage does not pass, most often the KYC stage |
| `VIRTUAL_TO_PHYSICAL` (virtual card to physical card) | `PENDING β PHYSICAL_SETTING_COMPLETED β COMPLETED` / `FAILED` | When card production or the card network step fails |
| `REPLACEMENT` (card replacement) | `PENDING β COMPLETED` / `FAILED` | When the replacement step fails |
> The `status` sequences and what `COMPLETED` means are described in [Webhook events and data structures](../webhooks/events-and-schema). `COMPLETED` means the card was issued, and only then does `cardId` carry a value.
***
## Virtual card application error codes (`type=VIRTUAL`)
Failures on a virtual card application fall into two groups: **partner-side and environment errors**, which you can fix by changing a parameter or checking your data before resubmitting, and **KYC rejections**, which make up the vast majority and come out of the underlying KYC review.
### Partner-side and environment errors
These codes have nothing to do with KYC. Most of them are request parameter problems or idempotency conflicts, and **the partner can fix them alone**:
| `errorCode` | Description | Retryable | What the partner should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `OPERATION_NOT_SUPPORT` | Operation not supported, for example a duplicate application | β | Check whether you are raising the same request twice for the same customer or order; deduplicate in your own logic instead of retrying in a loop | No |
| `EMAIL_ALREADY_EXISTS` | Email address already exists | β οΈ | A customer is already registered with this email; use the existing `customerId` or a different address | No |
| `EMAIL_INVALID` | Email address is invalid | β οΈ | Ask the user for a different email address and resubmit | No |
| `PHONE_ALREADY_EXISTS` | Phone number already exists | β οΈ | A customer is already registered with this phone number; reuse that customer or use a different number | No |
| `CUSTOMER_REF_NOT_UNIQUE` | Customer reference is not unique | β
| Use a globally unique `customerRef` and resubmit | No |
| `CUSTOMER_INFO_INVALID` | Customer information invalid | β
| Check the format of the phone number, email and other fields, then resubmit | No |
| `CARD_CREATION_FAILED` | Card creation failed | β οΈ | Issuing failed on the card network side; retry later and contact DCS if it keeps failing | No |
### KYC rejections
Almost every `FAILED` on a virtual card application comes from a KYC rejection. **These `errorCode` values share exactly one source with the [KYC rejection codes](../kyc/kyc-reject-codes) page and have identical values.** That page already groups them into business buckets with the three columns "retryable", "what the user should do" and "resubmission possible" plus integration guidance, so treat it as the authoritative reference rather than duplicating the handling here.
The list below only enumerates, bucket by bucket, the KYC rejection codes that can appear in a card order `errorCode` (the descriptions follow the data dictionary verbatim). For the action to take, follow the bucket through to the KYC page:
* **Document image quality (fixable: have the user reshoot or re-upload)**: `POOR_IMAGE_CAPTURE_QUALITY`, `DOCUMENT_DAMAGED_OR_UNCLEAR`, `POOR_PHOTO_QUALITY`, `LOW_DOCUMENT_QUALITY`, `DOCUMENT_PAGE_MISSING`, `INCOMPLETE_DOCUMENT_SUBMISSION`, `BACK_SIDE_MISSING`, `FRONT_SIDE_MISSING`, `UNSUPPORTED_DOCUMENT_FORMAT`, `INVALID_UPLOAD_TYPE`, `SCREENSHOT_DETECTED`, `COLORED_COPY_REQUIRED`, `ORIGINAL_DOCUMENT_REQUIRED`
* **Document validity and type (some are fixable)**: `EXPIRED_DOCUMENT`, `UNSUPPORTED_DOCUMENT_TYPE`, `UNSUITABLE_DOCUMENT_SUBMITTED`, `UNSUPPORTED_OR_INVALID_DOCUMENT_TEMPLATE`, `INVALID_IDENTIFICATION_DOCUMENT`, `DOCUMENT_VALIDATION_FAILED`, `UNSUPPORTED_LANGUAGE`, `UNSUPPORTED_DOCUMENT_LANGUAGE`
* **Missing supporting material or incomplete data (fixable through a resubmission)**: `ADDITIONAL_SUPPORTING_DOCUMENT_REQUIRED`, `MORE_SUPPORTING_DOCUMENTS_REQUIRED`, `INCOMPLETE_APPLICANT_DATA`
* **Information mismatch (fixable: check the data)**: `INFORMATION_MISMATCH`, `PROFILE_INFORMATION_MISMATCH`, `SUBMITTED_DATA_MISMATCH`, `DATABASE_INFORMATION_MISMATCH`, `ADDRESS_INFORMATION_MISMATCH`, `AGE_MISMATCH`, `INVALID_POA`, `INVALID_PROOF_OF_ADDRESS`, `INVALID_POI`, `INVALID_PROOF_OF_IDENTITY`, `INVALID_PROOF_OF_PAYMENT`
* **Face and liveness verification (some are fixable)**: `FACE_VERIFICATION_FAILED`, `LIVENESS_CHECK_FAILED`, `THIRD_PARTY_ASSISTANCE_DETECTED`, `IDENTITY_OWNERSHIP_VALIDATION_FAILED`, `MULTIPLE_PERSONS_DETECTED`
* **Compliance and risk decisions, final (do not retry; show a neutral message and never echo the compliance reason)**: `NAME_SCREENING_HIT`, `EDD_REJECTED`, `SUSPICIOUS_APPLICATION_BEHAVIOR`, `SECURITY_VALIDATION_FAILED`, `SECURITY_SCREENING_FAILED`, `SCREENING_UNSUCCESSFUL`, `COMPLIANCE_RESTRICTION`, `SUMSUB_BLOCKED`, `BLOCKED`
* **Eligibility and regional restrictions (final, and dependent on your programme configuration)**: `OUT_OF_ELIGIBLE_COUNTRIES`, `REGION_OR_RESIDENCY_RESTRICTION`, `ELIGIBILITY_OR_REGION_RESTRICTION`, `FAIL_TO_MEET_DCS_REQUIREMENT`, `FAIL_TO_MEET_PARTNER_REQUIREMENT`, `ELIGIBILITY_REQUIREMENT_NOT_MET`, `AGE_REQUIREMENT_NOT_MET`, `USA_TAX_RESIDENT`, `INCOME_REQUIREMENT_NOT_MET`, `CREDIT_ASSESSMENT_FAILED`, `UNSUPPORTED_PRODUCT`
* **Duplicates and suspected fraud (final)**: `DUPLICATE_APPLICATION_DETECTED`, `INVALID_OR_DUPLICATE_SUBMISSION`, `REAPPLICATION_PERIOD_NOT_MET` (the cooling-off period after closing a card has not elapsed, so do not prompt a reapplication)
* **Validation could not complete or the data source failed (depends on the case, usually a system-side issue to retry later)**: `APPLICANT_DATA_VALIDATION_FAILED`, `APPLICANT_DATA_NOT_FOUND`, `VERIFICATION_INCOMPLETE`, `VERIFICATION_CHECK_UNAVAILABLE`, `CONNECTIVITY_OR_SERVICE_ERROR`, `DATA_SOURCE_UNAVAILABLE`, `TIMEOUT`, `OTHER_VALIDATION_ISSUE`
* **Share token and reusable KYC (mostly retryable)**: `INVALID_SHARE_TOKEN`, `SHARE_TOKEN_FAILED`, `INVALID_VERIFICATION_SESSION`, `VERIFICATION_PROCESSING_TIMEOUT`, `REUSABLE_INCOMPATIBLE_DOCUMENT`, `REUSABLE_KYC_NOT_ENABLED`, `REUSABLE_VERIFICATION_NOT_ELIGIBLE`
* **Submission not completed inside the time window (`TIMEOUT_*`, retryable)**: `TIMEOUT_INVALID_PROOF_OF_IDENTITY`, `TIMEOUT_INVALID_PROOF_OF_ADDRESS`, `TIMEOUT_DOCUMENT_VALIDATION_FAILED`, `TIMEOUT_DOCUMENT_PAGE_MISSING`, `TIMEOUT_EXPIRED_DOCUMENT`, `TIMEOUT_POOR_PHOTO_QUALITY`, `TIMEOUT_SCREENSHOT_DETECTED`, `TIMEOUT_FACE_VERIFICATION_FAILED`, `TIMEOUT_AGE_REQUIREMENT_NOT_MET`, `TIMEOUT_ELIGIBILITY_OR_REGION_RESTRICTION`, `TIMEOUT_SUMSUB_BLOCKED`
* **Other**: `UNSATISFACTORY_DOCUMENT`, `OTHERS`
**Do not read `TIMEOUT_*` by its suffix**: this group only means the user did not complete the submission to Sumsub within the time window. The suffix merely says which step it timed out on and does **not** mean that check actually failed. Treat it as a timeout: have the user start again and finish promptly, rather than asking them to re-supply whatever the suffix names.
> **Document resubmission**: when KYC is rejected over POA or missing supporting material (`INVALID_POA` or `ADDITIONAL_SUPPORTING_DOCUMENT_REQUIRED`, for example), `needExtraInfo=true` in the card order response or webhook means more material is needed. Submit it through the resubmission path in [Card issuing and card orders](./card-issuing); **a new Sumsub share token is usually not required**.
***
## Virtual-to-physical error codes (`type=VIRTUAL_TO_PHYSICAL`)
| `errorCode` | Description | Retryable | What the partner should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `VIRTUAL_TO_PHYSICAL_FAILED` | Virtual-to-physical conversion failed | β οΈ | Card production or the card network step failed; check the shipping details and retry, and contact DCS if it keeps failing | No |
> This type has a single failure code with no further breakdown. Read `errorReason` for the specific failure. See [Physical card](./physical-card) for details.
***
## Card replacement error codes (`type=REPLACEMENT`)
| `errorCode` | Description | Retryable | What the partner should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `CARD_REPLACEMENT_FAILED` | Card replacement failed | β οΈ | The replacement step failed; check the status of the original card and the replacement reason, then retry, and contact DCS if it keeps failing | No |
> This type has a single failure code with no further breakdown. The replacement operation is covered in [Card management](./card-management).
***
### Integration guidance
* **Branch on `errorCode`, never on the `errorReason` text**: `errorReason` may be reworded between releases or localized. Keep your program logic on `errorCode` and use `errorReason`, or your own copy, for display.
* **Always have a fallback for unknown codes**: values are added to and removed from these tables as compliance policy and card network capabilities change. Route any `errorCode` you do not recognize into a default branch (handle it as `OTHERS` and raise an alert) so a new code never breaks your front end.
* **Tell non-final states apart from failures**: only read a failure reason when `status=FAILED`. Intermediate states such as `PENDING` must not be treated as failures, or you will wrongly block orders that are still under review.
* **Never retry a final code in a loop**: the compliance, eligibility, duplicate and fraud codes (marked β) are final. Resubmitting achieves nothing and may trip risk controls; show a neutral message and point the user to support.
"retryable" and "resubmission possible" in the tables are integration guidance. Your program logic should key off `errorCode` together with `status=FAILED`, with a fallback for any `errorCode` outside the known list (handle it as `OTHERS` and raise an alert).
***
## Next steps
* Issuing a card, checking card order status and reading `errorCode`: see [Card issuing and card orders](./card-issuing).
* The full handling of every KYC rejection code, covering retryability, what the user should do and whether a resubmission helps: see [KYC rejection codes](../kyc/kyc-reject-codes).
* Receiving `FAILED` notifications and parsing the webhook `data` field: see [Webhook events and data structures](../webhooks/events-and-schema).
# Card profile (profileId)
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/card-profiles
How profileId and cardProfileId are used, and how they are assigned offline.
## π Guide
Whichever card you plan to issue, virtual or physical, single-currency or spread across markets, it is defined by one **card profile**: the profile decides which BIN the card is drawn from, which card network it runs on, the currency, the PAN and PIN rules and the card limits. As a licensed issuer with its own BIN, DCS maintains the card profiles for each partner Enterprise so that the issuing rules line up with the boundaries of your business.
A card profile is the underlying template for the whole life of a card. Once it is set, its rules carry through issuing, authorization and clearing alike; when the way you issue cards changes, DCS updates the profile for you.
### Mental model
* One **Enterprise can hold several card profiles**, which is how one legal entity issues several kinds of card (different currencies, different markets, virtual or physical).
* Each profile is identified uniquely by its `profileId` and belongs to one `enterpriseId`.
* When you create a card order, the `profileId` you pass in says which profile to use.
### What a card profile covers
| Rule category | Typical content |
| - | - |
| Card network and BIN | Issuer identification number (IIN/BIN), card network (Visa and so on) |
| Card attributes | Primary account number (PAN) length, validity period, PIN length, currency |
| Limits | Card spending limits and similar (see [Limit lookup](./velocity-limits)) |
| Physical card manufacturing | The issuer data a card manufacturer needs to produce the plastic |
### Card profile fields
| Field | Type | Description |
| - | - | - |
| `profileId` | String | The unique identifier of a card profile. Endpoints such as create card order (`profileId`) and query enterprise balance (`cardProfileId`) refer to this value |
| `enterpriseId` | String | The ID of the Enterprise the profile belongs to |
> **A note on the field name**: most endpoints identify a card profile as `profileId`, while the enterprise account balance endpoint calls the same thing `cardProfileId`. Both point at the same profile. Always follow the field name printed in the reference for the endpoint you are calling, whether that is create card order (`profileId`) or the guidance page (where `profileId` is required when `type=7`).
### Who configures what
| Task | Who does it |
| - | - |
| Designing and maintaining card profiles (BIN, card network, currency, limits, manufacturing data) | **DCS** |
| Stating the issuing requirements (target markets, currency, virtual or physical, card art) and running the commercial discussion | **Partner** |
| Taking the assigned `profileId` and quoting it when applying for a card | **Partner** |
The DCS team configures the profile for you and hands over the `profileId`; there is no self-service endpoint to create or list profiles yet. To confirm which `profileId` values are available to you, ask your DCS contact.
### Prerequisites
* An Enterprise has been created. A card profile must hang off an Enterprise that already exists; if yours is not live yet, see the [onboarding guide](../../getting-started/first-steps).
* **Optional, for physical cards**: if you plan to issue physical cards, talk to the DCS commercial team early so that the best-suited card manufacturer can be chosen for you.
### How to use profileId
Once you have a `profileId`, pass it in when you create a card order to select the profile:
```http theme={null}
POST /open-api/card-order/v1/apply-virtual
```
```json theme={null}
{
"profileId": "",
"cardOrderRef": "",
"cardApplyMode": "NORMAL",
"kycTicketId": "",
"customerId": ""
}
```
> For the full request fields, the response envelope `{ code, message, messageDetail, data }` and the error codes, see [Card issuing flow](./card-issuing).
## Next steps
With a `profileId` in hand, head to the [card issuing flow](./card-issuing) and create your first card; to understand the limit rules, see [Limit lookup](./velocity-limits).
# Physical card
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/physical-card
Redundant screenshots removed.
## π Guide
Whether you serve high-net-worth cardholders, frequent travellers or in-store spending, the Partner-Managed model lets you issue your end users a DCS physical card: a Visa card that works at merchants, ATMs and digital wallets worldwide. Physical and virtual cards share the same issuing, KYC, authorization and clearing flows; all that differs is the plastic itself, the shipping step and the activation step.
A physical card is a tangible payment instrument with an EMV chip and contactless (NFC) support, printed with the card number, the expiry date and the card identification data, and accepted at more than 99% of in-store acceptance points. The card art can be branded (embossed name, an enterprise card layout code), which helps you carry your brand into your cardholder's wallet.
## Physical card vs virtual card
| Dimension | Physical card | Virtual card |
| :- | :- | :- |
| Form | Tangible card (plastic or metal) | Digital credentials only |
| Main use cases | In-store POS, ATM withdrawals, travel | Online shopping, subscriptions, digital advertising |
| How it is obtained | Shipped after application, needs activation | Issued instantly online, active immediately |
| Digital wallets | Apple Pay and Google Pay supported | Supported (see [Push provisioning](./push-provisioning)) |
| Our recommendation | Complementary to the virtual card, combine them by use case | β |
> Physical and virtual cards complement each other rather than compete. Let your end users cover online payments and subscriptions with a virtual card, and in-store spending and travel with a physical one.
## How to issue a physical card to a user
A DCS physical card comes out of a **virtual-to-physical** order: the cardholder has a virtual card first, and you then raise a conversion order against that card carrying the shipping address and the card art details.
**Endpoint**: `POST /open-api/card-order/v1/virtual-to-physical` (who does it: the partner raises the order)
Minimum request body:
```json theme={null}
{
"cardOrderRef": "your-idempotent-ref",
"replaceCardId": "the cardId of the virtual card being replaced",
"virtualToPhysicalInfo": {
"countryCode": "SG",
"state": "Singapore",
"city": "Singapore",
"postalCode": "049315",
"address": "1 Raffles Place",
"embossingName": "TOM LEE",
"cardLayoutCode": "ABC123",
"needShippingInfo": true
}
}
```
Request fields (an extract; the API reference carries the full constraints):
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `cardOrderRef` | string | Yes | Card order idempotency key, max length 50 |
| `replaceCardId` | string | Yes | The `cardId` of the virtual card being replaced, max length 50 |
| `virtualToPhysicalInfo.countryCode` | string | Yes | Destination country code, 2-letter ISO (SG, US, CN and so on) |
| `virtualToPhysicalInfo.state` | string | Yes | State or province, max length 20 |
| `virtualToPhysicalInfo.city` | string | Yes | City, max length 20 |
| `virtualToPhysicalInfo.postalCode` | string | Yes | Postal code, max length 10 |
| `virtualToPhysicalInfo.address` | string | Yes | Address, max length 40 |
| `virtualToPhysicalInfo.address2` / `address3` | string | No | Additional address lines, max length 40 each |
| `virtualToPhysicalInfo.embossingName` | string | Yes | The name embossed on the card, max length 26 |
| `virtualToPhysicalInfo.cardLayoutCode` | string | Yes | Card layout code, max length 6 (supplied by DCS when the card profile is set up) |
| `virtualToPhysicalInfo.needShippingInfo` | boolean | Yes | Whether DCS should ship the card on your behalf |
| `virtualToPhysicalInfo.mailMobileCode` | string | No | Country code of the delivery contact number (2-letter ISO, SG for example) |
| `virtualToPhysicalInfo.mailMobile` | string | No | Delivery contact number (91159519, for example) |
> Virtual-to-physical conversion is rate-limited within a 24-hour window; exceeding it returns `DAPI_VIRTUAL_TO_PHYSICAL_APPLY_LIMIT_EXCEEDED`, so back off before retrying.
> Keep the shipping address to Latin characters, digits and basic punctuation. Non-Latin characters (Chinese, Arabic and so on) can fail the card network's shipping validation, so transliterate them into Latin script before you submit.
A successful response returns a card order. The key fields are:
| Field | Description |
| :- | :- |
| `cardOrderId` | Card order ID |
| `type` | Order type, `VIRTUAL_TO_PHYSICAL` here |
| `cardId` | Card ID |
| `replaceCardId` | The `cardId` of the virtual card that was replaced |
| `status` | Order status: `PENDING / PHYSICAL_SETTING_COMPLETED / COMPLETED / FAILED` |
| `errorCode` / `errorReason` | Failure code and reason when the order fails |
> **The final status on success**: a card order succeeds with `COMPLETED`, and whether `cardId` carries a value is a useful cross-check. `PHYSICAL_SETTING_COMPLETED` is an intermediate step in the conversion, not a final status (the final statuses are `COMPLETED` and `FAILED`).
>
> Every endpoint returns the same envelope, `{ code, message, messageDetail, data }`; for what its fields mean, the success value of `code` and the error code dictionary, see [Authentication and security](../../integration-resources/authentication). Physical card delivery times and fees follow the card profile agreed with DCS and are currently not returned in any endpoint response or webhook.
### β οΈ How the old and new cards relate (must read)
Once the virtual-to-physical order completes, the same cardholder has **two cardIds**. This section decides which `cardId` you use before and after activation, and which card an authorization is booked against:
| | Description |
| :- | :- |
| **cardId** | The physical card gets a **new `cardId`**, different from the original virtual card. Store it separately |
| **Card number (PAN)** | The physical card carries the **same PAN** as the original virtual card. This is not a re-numbering, just an additional physical carrier |
| **Before activation** | The physical card is `PENDING_ACTIVATION` and **unusable**; the original virtual card still works and **all authorizations still belong to it** |
| **After activation** | The original virtual card is **terminated automatically** (`INVALID`), and from then on all authorizations belong to the physical card |
Activation is therefore a **switch-over point**. Once activation succeeds, move the `cardId` you use for queries, freezes, limit lookups and everything else from the virtual card to the physical card, and note that the original virtual card turning `INVALID` is expected behavior, not a fault.
## How to activate a physical card
A physical card has to be **activated** once it arrives before it can be used; a virtual card is active from the start and skips this step.
**Endpoint**: `POST /open-api/card/v1/physical-active` (who does it: the partner calls it once the cardholder has the card)
Request body:
```json theme={null}
{
"cardId": "the cardId of the physical card"
}
```
| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `cardId` | string | Yes | Card ID, max length 50 |
On success the card `status` moves from `PENDING_ACTIVATION` to `ACTIVATED`, and the response `data` also returns `type` (`PHYSICAL`), `panFirst6` and `panLast4`. Card status transitions and freeze, unfreeze and replacement are covered in [Card management](./card-management).
## Next steps
With the physical card issued, go to [Card management](./card-management) for freeze and unfreeze, PIN and replacement operations; to push the physical card into Apple Pay or Google Pay, see [Push provisioning](./push-provisioning).
# Push Provisioning / Digital Wallets
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/push-provisioning
How to let cardholders add a DCS-issued card to Apple Wallet or Google Wallet with a single tap inside your app.
## π Guide
Whether you are an exchange, a wallet or a platform, you can let cardholders add their DCS-issued card to Apple Wallet and Google Wallet with a single tap inside your app (known as **push provisioning**). There is no need to key in the card number manually, and the cardholder can start paying contactlessly straight from the phone.
As a licensed issuer with its own BINs, DCS issues both virtual and physical cards that are compatible with the major digital wallets, so your cardholders can pay through Apple Pay, Google Pay and similar services.
> **How integration works today (read this first)**: Apple Pay self-service integration is now available through **DCSProvisioningSDK** and `POST /open-api/card/v1/digital-wallet-ticket`. The SDK handles in-app provisioning and Apple Wallet extensions, while the server endpoint issues the temporary ticket. Apple entitlement, card-program and token-service configuration still require coordination with the DCS integration team. See [Apple Pay SDK Installation](../../sdk/apple-pay/installation) and the [Implementation Guide](../../sdk/apple-pay/implementation). Confirm the Google Pay integration path with DCS when the project starts.
***
## Mental model
Provisioning a card into a digital wallet means mapping a physical or virtual card to a **token held inside the wallet**: the real card number (PAN) is never placed on the device. The wallet pays with the token instead of the PAN, and the card scheme translates between the token and the underlying card. Four parties are involved:
| Role | Responsibility | Owner |
| - | - | - |
| Partner app | Show the "Add to Wallet" button and start provisioning | Partner |
| Digital wallet (Apple Pay / Google Pay) | Risk checks and tokenization | Wallet provider |
| DCS / card scheme side (card processors such as XPay) | Supply the card data, configuration and credentials needed to provision | DCS |
| Visa | Token lifecycle management and token-to-PAN mapping | Card scheme |
> Relationship with [Secure card details](./secure-card): the card data used for wallet provisioning comes from the same source as secure card details (both involve the real PAN), so the same PCI scope, key handling and whitelisting requirements apply. Read the two pages together.
***
## Business flow and responsibilities
The table below lists the key steps of today's manually coordinated flow, in the order they happen:
| Step | What happens | Owner |
| - | - | - |
| 1. Submit integration details | Provide the app platform (iOS / Android), bundle ID / package name, Apple Team ID, SHA-256 certificate fingerprint and any other data the wallet whitelist requires | Partner |
| 2. Wallet whitelist request | Request push provisioning entitlements and configuration from Apple / Google | DCS (submits on the partner's behalf) |
| 3. Card scheme side configuration | Complete the wallet provisioning configuration for the card program at the XPay / card processor level | DCS |
| 4. Integration and testing | For Apple Pay, implement the in-app entry point and Wallet extensions from the SDK guides; test Google Pay through the route agreed for the project | Partner + DCS |
| 5. Production validation | Run the full provisioning flow with a **production card** and submit it for validation | Partner |
> **Why DCS submits on your behalf**: push provisioning entitlements are granted by Visa and by Apple / Google at the **issuer (BIN sponsor) level**. DCS handles that relationship centrally as the issuer, so partners neither need to, nor should, contact Apple / Google or the card scheme channels directly.
### Runtime provisioning flow (cardholder's view)
> At Apple Pay runtime, the SDK first generates `originRef`; your backend exchanges it for a ticket through `POST /open-api/card/v1/digital-wallet-ticket`; the SDK then completes the encrypted PassKit and DCS / Visa exchange. See the [Implementation Guide](../../sdk/apple-pay/implementation).
***
## Current constraints and reconciliation
* **No sandbox**: Visa does not provide sandbox cards for provisioning, so you **must test with production cards**. See [Sandbox environment](../sandbox/overview).
* **Not a launch blocker**: provisioning can be enabled in parallel after your main flow is live; it is not a prerequisite for going live with card issuance.
* **Token lifecycle**: when a card is replaced, reissued or its expiry date changes, the card scheme maintains the wallet token automatically through the Visa lifecycle mechanism, so cardholders normally do not need to provision the card again. Event notifications for these changes are on the roadmap.
* **Provisioning failures**: wallet-side risk checks (device risk, repeated attempts in a short window, region mismatch and similar) can decline a provisioning request, and neither DCS nor the card scheme can override the wallet provider's decision. We recommend telling the cardholder to retry after 24-48 hours.
* **Digital wallet reconciliation**: purchases made with a wallet token appear in the daily reconciliation files alongside ordinary card transactions. Whether those files will carry a flag that identifies digital wallet transactions will be confirmed as the reconciliation capability matures. See [Reconciliation files](../reports/overview).
***
## Next steps
* [Apple Pay SDK Installation](../../sdk/apple-pay/installation) β add DCSProvisioningSDK and configure the entitlement, App Group and card profile
* [Implementation Guide](../../sdk/apple-pay/implementation) β exchange the ticket and start in-app provisioning
* [Wallet Extensions Guide](../../sdk/apple-pay/wallet-extensions) β expose the provisioning entry point inside Apple Wallet
* [Physical card](./physical-card) / [Virtual card](./virtual-card) β confirm the card is ready
* [Secure card details](./secure-card) β display the card number, expiry date and other sensitive details when required
* [Support and escalation](../../customer-success/support-escalation) β confirm the schedule, Apple entitlement and card-program configuration
# Retrieving secure card details
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/secure-card
The two paths: PCI and SecureToken
## π Guide
Whether or not you hold PCI certification, you can let end users securely view the full card number, CVV and expiry date of their own card. DCS offers a separate path for each of these two partner types: an encrypted payload returned to your backend, or a DCS-hosted guidance page. As a licensed issuer with its own BINs, DCS encrypts secure card details end to end in transit, and sensitive data is never persisted in the clear.
Secure card details (full PAN, CVV2, expiry date) are highly sensitive data governed by PCI DSS. The card management endpoints only return the first 6 and last 4 digits of the card number (`panFirst6` / `panLast4`); to obtain the full value in the clear, you must use one of the secure retrieval paths described on this page.
**Security notes**
* Never store decrypted secure card details in your backend or on any persistent medium.
* Only issue a retrieval request when it is genuinely needed, for example when the user actively taps "View card number".
* Always use an up-to-date crypto library, and keep the key (`enterpriseSecret`) safe. See [Authentication and security](../../integration-resources/authentication).
### Two paths: start by checking whether you hold PCI certification
DCS splits partners into two groups based on PCI certification, and the two paths are entirely different:
| Path | Who it is for | Who decrypts / displays the plaintext | Does the partner backend touch the plaintext? |
| - | - | - | - |
| Path 1 `retrieve-secure-card` | Partners **holding PCI certification** | Partner backend decrypts, partner frontend displays | Yes (the encrypted payload is returned and decrypted by the partner) |
| Path 2 `guidance-link` | Partners **without PCI certification** | The DCS-hosted page shows the details directly to the end user | No (plaintext never passes through the partner backend) |
> Which path you use is not a design choice; it is determined by your **PCI certification** status. Do not attempt to call Path 1 if you are not certified.
***
### Path 1: retrieve secure card details (PCI certified)
> Who does it: the **partner** (backend call, decryption and frontend display). Available only to PCI-certified partners.
#### Endpoint
```http theme={null}
POST /open-api/card/v1/retrieve-secure-card
Content-Type: application/json
```
For authentication headers and signing, see [Authentication and security](../../integration-resources/authentication).
#### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `cardId` | String | Yes | Card ID, maximum length 50 |
```json theme={null}
{
"cardId": "card_xxxxxxxxxxxx"
}
```
#### Response
Every response uses the standard envelope `{ code, message, messageDetail, data }` (the envelope fields are explained once in [Authentication and security](../../integration-resources/authentication)). All sensitive fields inside `data` are **encrypted**:
```json theme={null}
{
"code": "...",
"message": "...",
"messageDetail": { },
"data": {
"cardId": "card_xxxxxxxxxxxx",
"pan": "",
"cvv2": "",
"expireDate": "",
"iv": ""
}
}
```
| Field | Type | Description |
| - | - | - |
| `cardId` | String | Card ID |
| `pan` | String | Full card number (**encrypted**) |
| `cvv2` | String | CVV2 (**encrypted**) |
| `expireDate` | String | Expiry date (**encrypted**); once decrypted, the format is `mm/yy` |
| `iv` | String | Initialization vector (IV) used for this encryption; use it to decrypt the three fields above |
#### Decryption
The `pan`, `cvv2` and `expireDate` fields are encrypted with **AES-GCM**. The key is the `enterpriseSecret` assigned to you during onboarding (Base64-decoded into key bytes, with the key length determined by the number of decoded bytes), and the initialization vector is the `iv` returned alongside the payload. The official Java implementation from the source documentation (`AES/GCM/NoPadding`, 12-byte IV, 128-bit authentication tag) is:
```java theme={null}
// Excerpt from the AESUtil sample in the official card management documentation
public static String decryptGCM(String base64Ciphertext, String enterpriseSecret, String base64IV) {
SecretKey key = new SecretKeySpec(Base64.getDecoder().decode(enterpriseSecret), "AES");
byte[] iv = Base64.getDecoder().decode(base64IV);
byte[] ciphertext = Base64.getDecoder().decode(base64Ciphertext);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
GCMParameterSpec spec = new GCMParameterSpec(128, iv); // 128-bit authentication tag
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
}
```
All three fields share the same `iv`. Once decrypted, render them in your frontend and **do not write them back or persist them**.
> Encryption specification: the key is the Base64-decoded byte string of `enterpriseSecret` (its bit length follows the secret length; production currently uses 16 bytes, i.e. AES-128). A random 12-byte IV is generated per request and returned in the `iv` field. The 128-bit GCM tag is appended to the ciphertext, and `ciphertext + authTag` is Base64-encoded as a whole. The caller must also be on the PCI whitelist, the `cardId` must belong to the calling Enterprise, and the card status must be `ACTIVATED`.
***
### Path 2: view the details on the hosted guidance page (no PCI certification)
> Who does it: the **partner** requests the link, and **DCS** hosts the page and shows the plaintext to the end user.
Without PCI certification, DCS provides a hosted H5 page that displays the secure card details, so plaintext **never passes through the partner backend**. All you need to do is obtain a one-time guidance page link and have the end user open it in your frontend.
#### Endpoint
```http theme={null}
POST /open-api/card-redirect/v1/guidance-link
Content-Type: application/json
```
#### Request parameters (card details scenario, `type=1`)
| Field | Type | Required | Description |
| - | - | - | - |
| `type` | String | Yes | Always pass `1` (card details lookup) |
| `cardId` | String | Required when `type=1` | Card ID, maximum length 50 |
| `customerId` | String | Yes | Customer ID, maximum length 50 |
| `language` | String | Yes | Language, `zh` / `en` (non-Chinese-speaking countries map to en) |
| `theme` | String | Yes | Theme, maximum length 10, for example `default` / `blue` (`blue` is supported for card details lookup only) |
| `mode` | String | Yes | `light` / `dark` |
| `userAgent` | String | Yes | Maximum length 300 |
| `successfulRedirectUrl` | String | No | Redirect URL on success, maximum length 200 |
| `errorRedirectUrl` | String | No | Redirect URL on failure, maximum length 200 |
| `submitAutoClosed` | String | No | Whether to close the page automatically after a successful submission: `Y` / `N` |
| `primaryColor` | String | No | Primary color, maximum length 7, for example `#FFFFFF` |
> `otpStatus` is required only for `type=2` (set PIN) and `type=3` (reset PIN); it is not needed for a card details lookup (`type=1`). The other `type` values (face verification, KYC and so on) and their required fields are documented in [Hosted guidance page](../kyc/h5-kyc-guidance).
```json theme={null}
{
"type": "1",
"cardId": "card_xxxxxxxxxxxx",
"customerId": "cust_xxxxxxxxxxxx",
"language": "zh",
"theme": "default",
"mode": "light",
"userAgent": "Mozilla/5.0 ...",
"submitAutoClosed": "Y"
}
```
#### Response
`data` is a single string: the guidance page link for the end user to open.
```json theme={null}
{
"code": "...",
"message": "...",
"messageDetail": { },
"data": "https://"
}
```
Hand that link to the end user to open in your frontend. The DCS-hosted page then shows the full card number, CVV and expiry date directly, without any of it passing through the partner backend.
> The guidance page link is backed by a temporary token held in Redis: the token is deleted as soon as it has been validated, so it is a single-use credential and is also subject to a TTL. Pass it to the end user immediately after you obtain it, and do not cache or reuse it. The server verifies that the card belongs to the given user and Enterprise. The public, unauthenticated validation endpoints are `/card-bridge/redirect/public/v1/secret-validate` and `/card-bridge/redirect/public/v1/check-token-valid`.
***
### Prerequisites
* A card has been issued successfully and you have its `cardId` (see [Card issuance](./card-issuing)).
* Path 1: DCS has confirmed that you hold PCI certification, and you are storing `enterpriseSecret` securely.
* Path 2: you have confirmed that the `cardId` belongs to the `customerId` you are passing.
## Next steps
Once you can retrieve secure card details, move on to the freeze / unfreeze, reset PIN and cancellation operations in [Card management](./card-management). For encryption, decryption and key handling details, see [Authentication and security](../../integration-resources/authentication).
# Velocity limit query
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/velocity-limits
Explains how velocity limits work (transaction type Γ time period Γ amount or count cap), how to query them, and what each response field means.
## π Guide
Whether your users are making everyday purchases, paying across borders or withdrawing large amounts, you can look up the velocity limit rules and current usage of any card at any time, and use that to show the available headroom to the cardholder and monitor risk. As a licensed issuer with its own BINs, DCS writes the velocity limit rules onto the card at issuance and accumulates usage in real time for you to query.
Velocity limits are a risk control mechanism that caps either the **number of transactions** or the **transaction amount** for a single card within a given period. Well-chosen limits help prevent fraud, contain spending risk, and give partners a lever for managing cash flow.
## Mental model: one velocity limit rule = three dimensions
A card can carry several velocity limit rules at once, and each rule combines three dimensions:
```
Transaction type (e.g. R, retail purchase) Γ Time period (e.g. 4, calendar month) Γ Cap type (amount maxAmount / count maxCount)
```
For example, "retail purchase Γ calendar month Γ maximum amount of 5,000 USD" is one rule. DCS accumulates `usedAmount` in real time and derives `remainingAmount`, then approves or declines authorizations accordingly.
> **Who does it**: velocity limit rules are defined in the **card profile** and applied automatically by DCS when the card is issued, so partners do not configure them card by card. This endpoint is **read-only** and cannot change a limit. To adjust the rules, contact the DCS team to update the card profile (see [Card profile](./card-profiles)).
### Transaction type (transactionType)
| Code | Transaction type |
| :- | :- |
| **R** | Retail purchase |
| **C** | Cash withdrawal |
| **I** | Standard installment |
| **L** | Special-purpose installment |
| **S** | Cash installment |
| **Q** | Balance inquiry |
| **P** | Repayment |
| **A** | Proxy authorization |
### Time period (periodUnit)
| Code | Period unit |
| :- | :- |
| **1** | Per-transaction limit |
| **2** | Calendar day limit |
| **3** | Calendar week limit |
| **4** | Calendar month limit |
| **5** | Calendar year limit |
| **6** | Billing cycle limit |
| **7** | Rolling period limit (monthly) |
period unit `7` (rolling period limit) appears only in the API field definition, not in the body of the velocity limit guide. Both are documented as they stand; the API response is authoritative.
## Query the velocity limits of a card
**`GET /open-api/card/v1/card-velocity-limits`**
> All date fields on this endpoint are in **UTC**.
### Request parameters
| Field | In | Type | Required | Description |
| - | - | - | - | - |
| `cardId` | query | string | Required | Card ID, returned once the card has been issued. Maximum length 50 |
### Request example
```http theme={null}
GET /open-api/card/v1/card-velocity-limits?cardId=CARD_xxx
```
### Response
Every response uses the standard envelope `{ code, message, messageDetail, data }`:
* `code` / `message`: system-level return code and text.
* `messageDetail`: a message you can show to the end user (`title` / `message` / `type` / `action` / `linkUrl` and similar), meant for frontend guidance.
* `data`: an **array** of velocity limit rules, one element per rule.
Fields inside `data[]`:
| Field | Type | Description |
| - | - | - |
| `transactionType` | string | Transaction type code (R/C/I/L/S/Q/P/A, see the table above) |
| `velocityCode` | string | Velocity check code identifying the specific rule |
| `periodUnit` | string | Period unit code (1-7, see the table above) |
| `startDate` | string | Start of the accumulation window, `yyyy-MM-dd`, UTC |
| `endDate` | string | End of the accumulation window, `yyyy-MM-dd`, UTC |
| `currencyCode` | string | Currency code |
| `maxAmount` | number | Maximum amount (used by amount-based rules) |
| `maxCount` | integer | Maximum number of transactions (used by count-based rules) |
| `usedAmount` | number | Amount already used |
| `usedCount` | integer | Number of transactions already used |
| `remainingAmount` | number | Remaining available amount |
| `remainingCount` | integer | Remaining available transactions |
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": [
{
"transactionType": "R",
"velocityCode": "R-MONTH-AMT",
"periodUnit": "4",
"startDate": "2026-06-01",
"endDate": "2026-06-30",
"currencyCode": "USD",
"maxAmount": 5000,
"maxCount": null,
"usedAmount": 1280.50,
"usedCount": null,
"remainingAmount": 3719.50,
"remainingCount": null
},
{
"transactionType": "C",
"velocityCode": "C-DAY-CNT",
"periodUnit": "2",
"startDate": "2026-06-16",
"endDate": "2026-06-16",
"currencyCode": "USD",
"maxAmount": null,
"maxCount": 3,
"usedAmount": null,
"usedCount": 1,
"remainingAmount": null,
"remainingCount": 2
}
]
}
```
> The standard envelope is `{ code, message, messageDetail, data }`, and on success `code` is `SYS_SUCCESS`.
### How to use these fields
* **Showing available headroom to the cardholder**: read `remainingAmount` for amount-based rules and `remainingCount` for count-based rules.
* **Telling which kind of rule you are looking at**: a populated `maxAmount` means an amount cap, a populated `maxCount` means a count cap. Within one rule only one of the two is normally set, and the other is `null`.
* **Reading the accumulation window**: `startDate` and `endDate` delimit the current window (UTC); usage resets according to the period rule once the window closes.
## Next steps
* Where the rules are defined and how to change them: see [Card profile](./card-profiles).
* Basic card information and status: see [Virtual card](./virtual-card) and [Card management](./card-management).
* The standard response envelope and return codes: see [Authentication and security](../../integration-resources/authentication).
# Virtual card
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/cards/virtual-card
Redundant screenshots removed
## Virtual card
Whichever market you serve and whatever kind of end user you serve, you can issue a virtual card that is ready to spend within seconds, with no card production or shipping to wait for. As a licensed issuer and its own BIN sponsor, DCS gives partners a compliant, controllable way to issue cards under the Partner-Managed model: the partner owns the spending limits, the partner makes the authorization decisions, and DCS handles card issuance, the card lifecycle and settlement with the card scheme.
A virtual card is a digital payment card with no physical medium. It carries the same spending authorization and status management capabilities as a physical card, and suits online payments, subscriptions, cross-border settlement and similar scenarios, which makes it the primary way funds flow between a partner and its end users.
***
## Why choose a virtual card
| Capability | Description |
| - | - |
| **Instantly usable** | No production or shipping step: the card is created as soon as the application succeeds and is **active by default**, so it can be authorized for spending right away. |
| **Secure details delivered encrypted** | The card number, CVV and expiry date can only be obtained through the encrypted endpoint and are rendered directly to the end user in your frontend, never landing in the partner backend, which lowers the risk of exposure. |
| **Full lifecycle control** | Freeze, unfreeze, cancel and replace are all supported. Using `cardId`, a partner can adjust a card's permissions in real time to handle loss reports, account anomalies and similar cases. |
| **Upgradable to a physical card** | A virtual card can later be converted to a physical card, keeping the same card number (see below). |
***
## Mental model: how a virtual card comes into being
Virtual card issuance is driven by a **card order**, and the whole process runs **asynchronously**: you submit one application request, DCS returns a `cardOrderId`, and you then track the status through the query endpoint or a webhook until it reaches a final state.
| Role | Responsibility |
| - | - |
| **Partner** | Collects user data, submits the application, tracks status, retrieves secure card details, and presents and manages the card for the end user |
| **DCS** | Creates the customer profile, runs KYC, issues the card with the card scheme channel, sends status callbacks, and manages settlement with the card scheme |
> The prerequisites for applying (the Enterprise record and the card profile), the full set of request fields and the card order state machine are all consolidated on the [Card issuance](./card-issuing) page and are not repeated here.
***
## Applying for a virtual card (at a glance)
**`POST /open-api/card-order/v1/apply-virtual`**
Minimum required fields:
| Field | Type | Required | Description |
| - | - | - | - |
| `profileId` | string | Yes | Card profile ID, provided by the DCS team (maximum length 50) |
| `cardOrderRef` | string | Yes | Idempotency key for the card order (maximum length 50) |
| `cardApplyMode` | string | Yes | Always `NORMAL` |
| `kycTicketId` | string | Yes | KYC ticket ID (returned by "apply for KYC") |
| `customerId` | string | Yes | User ID (returned by "create user") |
Before calling this endpoint, complete [Apply for KYC](../kyc/apply-kyc) to obtain a `kycTicketId`, then issue the card with `kycTicketId` + `customerId`.
A successful response returns the key details of the card order:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"data": {
"cardOrderId": "CO_xxxxxxxx",
"profileId": "PF_xxxx",
"type": "VIRTUAL",
"customerId": "CU_xxxx",
"cardId": "CARD_xxxx",
"status": "PENDING",
"needExtraInfo": false,
"createTime": "2025-01-01T12:00:00+08:00",
"modifyTime": "2025-01-01T12:00:00+08:00"
}
}
```
**About the standard response envelope**: every endpoint wraps its result in `{ code, message, messageDetail, data }`. `code` is the system-level return code (such as `SYS_SUCCESS`), `message` is a short description, `messageDetail` is structured copy you can show directly to the end user (with `title`, `type`, `action`, `linkUrl` and similar), and the business payload sits inside `data`.
> The authentication headers and `Content-Type` required on the application request, as well as the dictionary of `code` values, are documented once in [Authentication and security](../../integration-resources/authentication).
For the full field reference, the encryption details, and how to handle `needExtraInfo` (supplementary KYC information), see [Card issuing flow](./card-issuing).
***
## Card order statuses (virtual card)
| Status | Meaning | Final state |
| - | - | - |
| `PENDING` | Initial status | No |
| `CUSTOMER_PASS` | Customer profile created successfully inside the system | No |
| `KYC_PASS` | KYC identity verification passed | No |
| `CHANNEL_CUSTOMER_PASS` | Customer created successfully on the channel side | No |
| `COMPLETED` | Virtual card created and activated successfully | Yes |
| `FAILED` | Final state after any step fails | Yes |
When the status is `FAILED`, both the response and the `CARD_ORDER` webhook carry `errorCode` and `errorReason`. For how the error codes are grouped, and whether a case can be retried, resolved by the user or fixed by resubmitting documents, see [Card application error codes](./card-order-codes).
> To track status, either call **`GET /open-api/card-order/v1/detail`** or subscribe to the `CARD_ORDER` webhook and receive updates passively.
***
## Converting a virtual card to a physical card
A virtual card can later be converted to a physical card. **What to expect**:
* The conversion produces a new `cardId`, different from the original virtual card's, but **both cards share the same card number**.
* Until the physical card is activated, all authorizations remain against the original virtual card.
* Once the physical card is activated, the original virtual card is canceled and authorizations move to the physical card.
For the exact steps, see [Physical card](./physical-card).
***
## Retrieving secure card details
The card number, CVV and expiry date of a virtual card are sensitive data, so they must be **retrieved** through the encrypted endpoint and rendered directly to the end user in your frontend:
* **Partners holding PCI certification** call `retrieve-secure-card` for the encrypted card details and decrypt them before showing the user.
* **Partners without PCI certification** present the details through the DCS hosted guidance page, so sensitive data never passes through the partner's systems.
See [Secure card details](./secure-card) for the full flow.
***
## Next steps
* With the prerequisites in place, go to [Card issuance](./card-issuing) for the complete field reference and the application modes.
* Once a card has been issued, see [Card management](./card-management) for freeze, unfreeze, cancellation and replacement.
* To cap per-transaction or periodic spending, see [Velocity limits](./velocity-limits).
# Dispute Handling
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/disputes
The paths available today, and the information they require, when a cardholder disputes a settled transaction (goods not received, wrong amount, fraudulent use).
## Dispute
When a cardholder disputes a settled transaction, because the goods never arrived, the amount does not match what they paid, or the purchase was never theirs to begin with, you need a clear path to take the dispute to the card network, recover the money and keep the user informed. DCS is a licensed issuer; the final ruling on a dispute rests with the card network (Visa/Mastercard) under its own rules, and DCS handles intake, evidence and recovery between the network and the partner.
> **Scope of this page**: disputes are handled entirely through operational and manual channels for now (there is no self-service dispute API yet). This page covers the paths available today, namely raising an operations ticket, containing fraud losses by freezing and replacing the card, and the automatic recovery of a merchant refund, and it spells out what information you need to provide through which channel.
***
## First, tell three things apart: refund, reversal and dispute
The three are easily lumped together, but they are entirely different events in the money flow and follow different paths:
| Event | Who initiates it | How it shows up in DCS | Dispute process needed |
| - | - | - | - |
| **Refund / return** | The merchant refunds voluntarily | A transaction record with `direction=INCOMING` credits the money automatically, with no action from the partner | No |
| **Reversal** | The merchant or the card network reverses the original authorization | The hold is released at the authorization stage plus an INCOMING record; handled automatically | No |
| **Dispute / chargeback** | The cardholder disputes a settled transaction | Handled through operational and manual channels (no self-service API yet) | Yes |
**The deciding question**: you only enter the dispute process when the merchant **will not refund**, or when the cardholder **never made the transaction (fraud)**. If the merchant has already refunded, the money returns to your balance account automatically as an INCOMING record, so **do not raise a dispute on top of it**: recovering twice, once from the merchant and once through a dispute, is flagged as abnormal by the card network.
> For how refunds and reversals land in the transaction records and in authorization and settlement, see [Authorization and settlement scenarios](./transactions/auth-and-settlement) (scenario 9, forced refund, partial reversals and so on) and [Transaction records](./transactions/transaction) (`direction=INCOMING`).
***
## Available today: the operational path for fraud and disputes
When a cardholder raises a dispute with you, work through the operational channels in the order below.
### The user reports fraud (an unauthorized transaction)
> **Who does what**
>
> * **You**: (1) freeze the affected card immediately to contain the loss; (2) reissue a new card if needed; (3) gather the evidence (transaction ID, last four digits of the card number, merchant details, a description of the dispute) and submit a dispute intake request to DCS through the operational channel.
> * **DCS**: takes the case, raises the dispute (chargeback) with the card network, follows the ruling and reports the recovery and progress back to you.
**Step 1: freeze the card immediately** (the only self-service action available to you today for containing the loss)
```
POST /open-api/card/v1/freeze
```
| Parameter | Type | Required | Description |
| - | - | - | - |
| `cardId` | String | Yes | Card ID, maximum length 50 |
| `freeze` | Boolean | Yes | `true` freezes the card (contains the loss) / `false` unfreezes it |
| `freezeReason` | String | Yes | Reason for the freeze, maximum length 20, such as `USER_FREEZE` / `NORMAL` |
> Freezing and unfreezing are the same `freeze` endpoint, told apart by the `freeze` boolean; **there is no separate unfreeze or unblock endpoint**. For card operations in general, see [Card management](./cards/card-management).
**Step 2: reissue a new card** (in a fraud case the original card should be invalidated and replaced, so the cardholder is not left exposed on a compromised card). For card replacement, see [Card management: replacing a card](./cards/card-management).
**Step 3: submit the dispute intake request**, through the operational channel agreed with DCS (customer success or an operations ticket). The minimum information required: the transaction ID (`transactionId`), the last four digits of the card number, the merchant details, the dispute category with a written description, and any supporting evidence files. Once DCS takes the case it raises the dispute with the card network and follows the outcome.
### The merchant owes a refund but has not paid it, or the refund is overdue
First confirm whether the merchant has actually issued a refund (has the INCOMING record been credited?). If the merchant refuses to refund, or the refund never materialises, then submit a dispute along the operational path above.
***
## How the money from a dispute reaches you
Once the card network rules in favour of the partner (the cardholder), the recovery is credited to your balance account:
* It appears in the reconciliation file as a `direction=INCOMING` entry carrying `category: CHARGEBACK`, which marks it as a dispute transaction and lets you identify it programmatically; it is delivered through the daily reconciliation files.
* The recovery is credited as a `direction=INCOMING` entry to the partner's **balance account**; using the dispute transaction Id you can trace it back to the original transaction and attribute the credit to the specific end user and dispute.
> **Reconciliation tip**: `category: CHARGEBACK` is what tells a dispute recovery apart from an ordinary merchant refund. Still record the corresponding transaction ID while you follow the dispute through the operational channel, as an auxiliary cross-check.
***
## A walkthrough of one fraud case
Here is the handling path above on a real timeline. The case: late at night, a cardholder reports an 89.99 USD purchase in your app that they never made; the transaction has already settled.
| Time | Event | What you do | With what |
| - | - | - | - |
| D0, late night | The user reports the fraud | Verify the record: find the settled `direction=OUTGOING` transaction, note its `transactionId` and the merchant details | Transaction records / daily transaction report |
| D0, late night | Stop the bleeding | Freeze the affected card | `POST /open-api/card/v1/freeze`, `freeze=true`, `freezeReason=USER_FREEZE` |
| D1 | Restore card access | Invalidate the old card and issue a replacement (a compromised card should never be unfrozen for reuse) | [Card management Β· replacement](./cards/card-management) |
| D1 | Submit the dispute | Assemble the minimum evidence pack β `transactionId`, last four digits, merchant name, dispute category and description, supporting files β and submit through the agreed operational channel | Operations ticket / customer success channel |
| After D1 | DCS takes over | DCS raises the chargeback with the card network and relays progress through the operational channel | β |
| Weeks later (ruling) | You win the dispute | A `direction=INCOMING` credit carrying `category=CHARGEBACK` appears in one day's reconciliation file; that marks it as a dispute recovery, and the dispute transaction Id traces it back to the original transaction, this user and this dispute | Daily transaction report |
Two lessons:
* **The D0 freeze defines the loss boundary**: the ruling takes weeks, but further spending on the compromised card is blocked from the moment you freeze it β this is the only minute-critical step in the whole flow, and the only one you can complete through a self-service API today.
* **Claim the recovery by `category: CHARGEBACK`**: the dispute credit carries the `category: CHARGEBACK` marker in the reconciliation file, so you can identify it programmatically and trace it back to the original transaction by the dispute transaction Id. The `transactionId` and expected amount you record on D1 serve as an auxiliary cross-check.
***
## Practical reminders when submitting a dispute
To improve your odds of a case being accepted and won, keep the following in mind:
* A dispute can normally only be raised on a transaction that is **settled** and has a **non-zero amount**; pending transactions and zero-amount (verification) transactions cannot be disputed.
* Card network disputes have a deadline (usually **120 days** from the transaction date), and the closer you get to it the more likely the case is thrown out, so submit early.
* Only **one dispute at a time** should be open on a given transaction, so do not submit twice.
* If the merchant refunds directly while the dispute is running, the money returns to your balance account automatically as an INCOMING record, and there is no need to pursue the dispute any further.
***
## Next steps
* Understand how refunds and reversals land in the money flow automatically: [Authorization and settlement scenarios](./transactions/auth-and-settlement)
* Reconcile INCOMING recoveries entry by entry: [Transaction records](./transactions/transaction)
* The freeze and replacement operations that contain a fraud loss: [Card management](./cards/card-management)
* The webhook event types available today: [Webhook events and data structures](./webhooks/events-and-schema)
# Apply for KYC
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/kyc/apply-kyc
Submit a user's KYC information to DCS in a single API call and receive a kycTicketId, the ticket you will quote when issuing the card; it accepts a Sumsub Share Token or direct document upload.
* **Option 1: Sumsub Share Token** β the user has already been verified in your own Sumsub account, so you hand the Share Token to DCS and upload no document files;
* **Option 2: document file upload** β you collect the documents yourself and upload them to DCS.
Both options create the same kind of KYC ticket, with an identical status machine, query API and webhook.
## π Guide
Whether you are an exchange, a wallet or a platform, you can submit a user's KYC information to DCS in a single API call. This is a required step before you can issue a card to that user. As a card issuer licensed by the Monetary Authority of Singapore (MAS), DCS receives the data securely, assesses risk against anti-money laundering (AML) rules, and returns the verification result through a webhook.
## Who does what
| Step | Who | Notes |
| - | - | - |
| Collect the user's identity and career information | Partner | Sumsub route: complete verification in your own Sumsub account and take the Share Token. Upload route: prepare the document files and the career information |
| Upload the files and submit the KYC application | Partner | On the upload route, first request an S3 pre-upload URL, PUT the file to it, then call `apply-kyc` |
| Document verification, AML screening, manual review | DCS | Handled asynchronously after receipt |
| Report the status and the result | DCS | Through the query API or the `KYC_TICKET` webhook |
## Prerequisites
* You hold the Enterprise ApiKey / SecretKey β see the [Authentication guide](../../integration-resources/authentication).
* You have created the user and hold their `customerId` β see [Create a user](../users/create-customer).
* **On the Sumsub route**: you have set up a sharing partner relationship between your own Sumsub account and DCS (see the [KYC vendor notes](../../customer-success/faq-kyc-vendor)) and hold the user's Share Token.
* **On the upload route**: you have the user's identity document to hand (an ID card, passport or driving license, for example), plus a proof of address if one is required.
## Choosing between the two options
| Situation | Recommended option | Key fields |
| - | - | - |
| You already run your own Sumsub integration | Option 1: Sumsub Share Token | `sumsubShareToken` |
| No Sumsub integration; you collect the documents yourself | Option 2: document file upload | `identifyProofList` / `addressProofList` |
**How DCS tells them apart**: when the document-file fields (`identifyProofList` / `addressProofList`) are left empty, DCS skips validating them; when they are supplied, they are checked against the enumerations and the length limits. `sumsubShareToken` is passed downstream with the request and used to pull the data the user has already had verified in Sumsub. Both may appear in the same request, in which case the downstream risk service decides which one prevails. Either way you must submit **career information** (`kycCareerInfo`) β that part is required on both routes.
***
## Option 1: Sumsub Share Token
### Minimum request
`POST /open-api/kyc-ticket/v1/apply-kyc`
```json theme={null}
{
"kycTicketRef": "kyc-20260617-0001",
"customerId": "CUST_8f3a...",
"sumsubShareToken": "_act-xxxxxxxxxxxxxxxxxxxxxx",
"kycCareerInfo": {
"employmentStatus": "EMPLOYED",
"employerName": "Acme Pte Ltd",
"employmentJobIndustry": "INFORMATION",
"occupation": "IT_CONSULTANT",
"jobSeniority": "EXECUTIVE",
"purposeOfAccount": "DAILY_SPENDING",
"sourceOfFunds": "EMPLOYMENT",
"sourceOfFundsCountry": "SG",
"sourceOfWealth": "EMPLOYMENT"
}
}
```
> On the Sumsub route you can leave `identifyProofList` / `addressProofList` empty. `sumsubShareToken` is passed downstream, where it is used to fetch the documents the user has already had verified in Sumsub.
***
## Option 2: document file upload
### Step 1: upload the document files
Identity and address documents are not embedded in the application request itself. Instead you request an S3 pre-upload link, PUT the file to it, and quote the returned `objectKey` in the application.
Call `POST /open-api/intent-ticket/v1/generate-pre-upload-url` with `businessType` set to `CREATE_CARD_KYC`:
```json theme={null}
{
"fileNames": ["id_front.jpg", "id_back.jpg"],
"businessType": "CREATE_CARD_KYC"
}
```
The response carries a temporary upload URL and an `objectKey` for each file. After you PUT the file to the URL, keep the `objectKey`: it is what goes into the `identityProofUrl` / `addressProofUrl` field in the next step, not the full URL.
### Step 2: submit the KYC application
`POST /open-api/kyc-ticket/v1/apply-kyc`
```json theme={null}
{
"kycTicketRef": "kyc-20260617-0001",
"customerId": "CUST_8f3a...",
"identifyProofList": [
{
"identityProofUrl": "",
"identityProofType": "PASSPORT",
"identityProofSubType": "EMPTY",
"identityProofIssuedCountry": "SG"
}
],
"kycCareerInfo": {
"employmentStatus": "EMPLOYED",
"employerName": "Acme Pte Ltd",
"employmentJobIndustry": "INFORMATION",
"occupation": "IT_CONSULTANT",
"jobSeniority": "EXECUTIVE",
"purposeOfAccount": "DAILY_SPENDING",
"sourceOfFunds": "EMPLOYMENT",
"sourceOfFundsCountry": "SG",
"sourceOfWealth": "EMPLOYMENT"
}
}
```
***
## Request fields at a glance
### Common fields (both options)
| Field | Type | Required | Description |
| - | - | :-: | - |
| `kycTicketRef` | string | Yes | KYC idempotency reference; max 50. Resubmitting the same reference does not create a second ticket |
| `customerId` | string | Yes | User ID; max 50 |
| `kycCareerInfo` | object | See notes | Career information (below). **At least one of** this field and `kycCareerInfoEncryption` must be present |
| `kycCareerInfoEncryption` | string | See notes | The `kycCareerInfo` JSON as AES-GCM ciphertext. At least one of the two must be present; when both are, the **ciphertext wins** |
| `encryptionIV` | string | Conditional | The AES-GCM IV used to decrypt `kycCareerInfoEncryption`; required whenever you send the ciphertext |
### Field specific to option 1
| Field | Type | Required | Description |
| - | - | :-: | - |
| `sumsubShareToken` | string | Required for option 1 | Sumsub Share Token; max 500. Passed downstream to fetch the already-verified data, which lets you leave the document-file fields empty |
### Fields used by option 2
| Field | Type | Required | Description |
| - | - | :-: | - |
| `identifyProofList` | object\[] | Recommended for option 2 | List of identity documents (below) |
| `addressProofList` | object\[] | Optional for option 2 | List of address documents (below); up to 5 entries |
Spelling note: the outer list field is spelled `identifyProofList` (identify), while its nested fields start with `identityProof*` (identity). The prefixes differ, so pass each field exactly as spelled in this table.
`identifyProofList` and its nested `identityProofType` / `identityProofIssuedCountry` are all **optional at the endpoint level**, which is what allows the Sumsub route to send nothing but `sumsubShareToken`. On the upload route, however, all three are **required in practice**: verification cannot complete without them.
**`identifyProofList[]` identity document**
| Field | Type | Required | Description |
| - | - | :-: | - |
| `identityProofUrl` | string | No | The file's `objectKey` from step 1; max 512. Required when you upload the document yourself |
| `identityProofType` | string | No | One of `ID_CARD` / `DRIVERS` / `PASSPORT`; max 32. Required when you upload the document yourself |
| `identityProofSubType` | string | No | One of `FRONT_SIDE` / `BACK_SIDE` / `EMPTY`; use `EMPTY` for single-sided documents or documents with no front and back; max 32 |
| `identityProofIssuedCountry` | string | No | Issuing country as a 2-letter ISO country code. Required when you upload the document yourself |
**`addressProofList[]` address document**
| Field | Type | Required | Description |
| - | - | :-: | - |
| `addressProofUrl` | string | No | The file's `objectKey`; max 512. Required when you upload the address document yourself |
| `addressProofType` | string | Yes, when the list is not empty | Value: `UTILITY_BILL`; max 32. Leaving it empty while the list has entries returns `DAPI_SYS_ILLEGAL_PARAM` |
| `addressProofIssuedCountry` | string | No | Issuing country as a 2-letter ISO country code |
### `kycCareerInfo` career information (required on both options)
| Field | Type | Required | Description |
| - | - | :-: | - |
| `employmentStatus` | string | Yes | Employment status; max 32 |
| `employerName` | string | Conditional | Employer name; required when `employmentStatus=EMPLOYED`; max 50 |
| `employmentJobIndustry` | string | Yes | Industry of employment; max 32 |
| `occupation` | string | Yes | Occupation; max 32 |
| `jobSeniority` | string | Yes | Job seniority; max 32 |
| `purposeOfAccount` | string | Yes | Purpose of the account; max 32 |
| `sourceOfFunds` | string | Yes | Source of funds; max 32 |
| `sourceOfFundsCountry` | string | Yes | Source-of-funds country as a 2-letter ISO country code |
| `sourceOfWealth` | string | Yes | Source of wealth; max 32 |
> Career information is sensitive data. If you want it encrypted end to end, serialise `kycCareerInfo` to JSON, encrypt it with AES-GCM, put the ciphertext in `kycCareerInfoEncryption` and the IV in `encryptionIV`; the plaintext `kycCareerInfo` can then be omitted.
> The fields above are validated against server-side enumerations rather than accepting arbitrary strings. For the full set of values, see the [KYC application parameter dictionary](./kyc-application-fields).
## Response
Every `/open-api/` endpoint shares the same response envelope, `{ code, message, messageDetail, data }`. On success, `data` carries the newly created KYC ticket:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"kycTicketId": "KYC_1a2b...",
"kycTicketRef": "kyc-20260617-0001",
"customerId": "CUST_8f3a...",
"status": "INIT",
"createTime": "2026-06-17T08:00:00+08:00"
}
}
```
> About the envelope: judge success or failure by `code` (`SYS_SUCCESS`, for example); `message` is a short caption. `messageDetail` is an optional display object (with `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`) meant for front-end prompts, and should never be used to decide whether a call succeeded.
Note that HTTP 200 with `code=SYS_SUCCESS` only means **the application has been accepted**, not that KYC has passed. After a successful submission `status` is normally `INIT`, and you get the final outcome from the query API or the webhook.
| Response field | Description |
| - | - |
| `kycTicketId` | KYC application ID; quote this in later queries and references |
| `kycTicketRef` | The idempotency reference, echoed back |
| `customerId` | User ID |
| `status` | KYC ticket status: `INIT` / `NEED_VERIFY` / `PENDING` / `PASSED` / `REJECTED` |
| `createTime` | Creation time, formatted `yyyy-MM-dd'T'HH:mm:ss+08:00`, as elsewhere on the platform |
## Following up on the result
Once submitted, the KYC ticket moves through the statuses below. Both options share the same status machine.
| Status | Meaning | Terminal | What you do next |
| - | - | :-: | - |
| `INIT` | Information submitted | No | Wait for the system to process it |
| `NEED_VERIFY` | Waiting for the face scan | No | [Get the face verification link](./h5-kyc-guidance) and walk the user through the face scan |
| `PENDING` | Under review | No | Wait for the review to finish |
| `PASSED` | Approved | Yes | Carry on with the next step, such as applying for a virtual card |
| `REJECTED` | Rejected | Yes | Resubmit under a new `kycTicketRef` |
When `status=REJECTED`, both the [Query KYC](./query-kyc) endpoint and the `KYC_TICKET` webhook return an `errorCode` and an `errorMessage` giving the reason. For how to handle each one, see [KYC reject codes](./kyc-reject-codes).
Prefer webhook subscriptions over polling for status updates; see [Webhook events and schema](../webhooks/events-and-schema).
## Next steps
Once KYC is `PASSED`, go to the [card issuing flow](../cards/card-issuing) to issue the user a card. If the status is stuck at `NEED_VERIFY`, start with the [face verification page](./h5-kyc-guidance) and walk the user through the face scan.
# H5 KYC guidance page
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/kyc/h5-kyc-guidance
Whether you want to hand the whole KYC experience over to us or simply want no documents and no face data anywhere near your own backend, the H5 KYC guidance page is the answer: send the user to a DCS-hosted H5 page where they select their nationality, upload their document, complete the face scan and fill in their address themselves. DCS is a licensed card issuer, so the entire KYC flow is verified and stored securely on our side. Your only jobs are to hand the link to the user and to receive the result.
## π Guide
Whether you want to hand the whole KYC experience over to us or simply want no documents and no face data anywhere near your own backend, the H5 KYC guidance page is the answer: send the user to a DCS-hosted H5 page where they select their nationality, upload their document, complete the face scan and fill in their address themselves. DCS is a licensed card issuer, so the entire KYC flow is verified and stored securely on our side. Your only jobs are to hand the link to the user and to receive the result.
H5 mode and [API mode](../kyc/apply-kyc) are alternatives; pick one:
| What you want | Use |
| - | - |
| To collect the documents and address details yourself and submit them through the API in one call | [Apply for KYC (API mode)](../kyc/apply-kyc) |
| To stay out of capture entirely and let the user fill everything in on a DCS-hosted H5 page | This page (H5 mode) |
***
### Mental model: who does what
In H5 mode you do exactly three things; DCS carries everything else.
| Task | Who | Notes |
| - | - | - |
| Serve the H5 page (document type selection, Sumsub SDK upload and face scan, career and income details, address) | DCS | You build no KYC UI at all |
| Receive and securely store the KYC information the user submits | DCS | |
| Run DCS risk screening and Sumsub identity verification, and advance the ticket status asynchronously | DCS | |
| Send a webhook whenever the status changes | DCS | Event type `KYC_TICKET` |
| Send the H5 URL to the user, receive the result and move on to card issuing | Partner | |
***
### Step 1: create the user (you)
Call `POST /open-api/customer/v1/create-customer` to create the user and take the `customerId`. See [Create a user](../users/create-customer).
***
### Step 2: apply for the H5 KYC ticket (you)
Call `POST /open-api/kyc-ticket/v1/apply-kyc-h5` to create the ticket.
**Request body (`APIApplyKycH5Request`)**
| Field | Type | Required | Constraint | Description |
| - | - | - | - | - |
| `kycTicketRef` | string | Yes | β€50 | Your own idempotency reference, chosen by you (a business order number works well); you can query by it later |
| `customerId` | string | Yes | β€50 | The user ID returned by step 1 |
| `kycApplyMode` | string | No | β€20 | How to apply: `H5` (first-time KYC, the default) / `H5-RENEWAL` (update KYC information) / `H5-MIGRATION` (migrate KYC information). Omitting it is treated as `H5` |
| `sourceCustomerId` | string | Required with `H5-MIGRATION` | β€50 | The source user identifier: pass that user's `externalUserId` from the DeCard-Managed model. Despite the `...CustomerId` name, this is not a `customerId` of this model |
> Note that `kycApplyMode` follows the endpoint definition. It is not part of the standard flow guide. Send `H5-RENEWAL` when an existing user has to resubmit or refresh their information.
>
> This page covers first-time KYC (`H5`) only. For re-verification after the information expires, see [Updating KYC information](./kyc-renewal); for reusing KYC the user already holds under the DeCard-Managed model, see [KYC information migration](./kyc-migration).
**Minimum request**
```json theme={null}
{
"kycTicketRef": "kyc-20260617-0001",
"customerId": "1000000123"
}
```
**Response (`APIApplyKycH5Response`, with the common envelope stripped)**
| Field | Type | Description |
| - | - | - |
| `kycTicketId` | string | KYC ticket ID, used in step 3 and in queries |
| `kycTicketRef` | string | The idempotency reference, echoed back |
| `customerId` | string | Customer ID |
| `status` | string | Ticket status; `INIT` on a new ticket |
| `createTime` | string | Creation time |
| `errorCode` / `errorMessage` | string | Returned on failure |
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"kycTicketId": "9000000888",
"kycTicketRef": "kyc-20260617-0001",
"customerId": "1000000123",
"status": "INIT",
"createTime": "2026-06-17T08:00:00+08:00"
}
}
```
> The common envelope is `{code, message, messageDetail, data}`: `code` is the business status code (`SYS_SUCCESS` on success) and `messageDetail` carries extra error detail (`null` on success). For what the fields mean, see [Authorization declines and error codes](../transactions/decline-codes).
**Idempotency behavior**
* The `kycTicketRef` has not been seen before: a new `INIT` ticket is created.
* The `kycTicketRef` already exists, belongs to the same enterprise and customer, and the ticket is still `INIT`: the existing ticket is returned unchanged, which is what makes retries safe.
* Anything else, such as a mismatched owner or a ticket already at `PENDING` or `PASSED`: the call fails with one of the error codes listed below.
***
### Step 3: exchange the ticket for an H5 link (you)
Call `POST /open-api/card-redirect/v1/guidance-link` with `type=7` to turn the ticket into an H5 URL you can send to the user.
**Request body (the H5 KYC fields of `APIGuidanceRequest`)**
| Field | Type | Required | Constraint | Description |
| - | - | - | - | - |
| `type` | string | Yes | β€1 | Fixed at `7` (apply for KYC-H5). See the full enumeration below |
| `customerId` | string | Yes | β€50 | Same as in steps 1 and 2 |
| `kycTicketId` | string | Required when type=7 | β€50 | The `kycTicketId` returned by step 2 |
| `profileId` | string | **Required when type=7** | β€50 | Card profile ID; ask the DCS team for it. **It must be sent even though it is easy to miss** |
| `language` | string | Yes | Length 2 | `zh` / `en`; non-Chinese-speaking countries all map to `en` |
| `theme` | string | Yes | β€10 | `default`, for example (`blue` supports card information lookup only) |
| `mode` | string | Yes | β€10 | `light` / `dark` |
| `userAgent` | string | Yes | β€300 | The user's browser user agent |
| `successfulRedirectUrl` | string | No | β€200 | Where to send the user after a successful completion |
| `errorRedirectUrl` | string | No | β€200 | Where to send the user after a failure |
| `submitAutoClosed` | string | No | β€1 | `Y` closes the page automatically after a successful submission, `N` leaves it open |
| `primaryColor` | string | No | β€7 | Primary colour, such as `#FFFFFF` |
The full `type` enumeration, as defined by the code:
| type | Meaning | Also required |
| - | - | - |
| 1 | Card information lookup | `cardId` |
| 2 | Set PIN | `cardId`, `otpStatus` |
| 3 | Reset PIN | `cardId`, `otpStatus` |
| 4 | Apply for KYC, liveness (face scan) | `kycTicketId` |
| 5 | Information verification | `cardOrderId` |
| 6 | Supplement card application information | `cardOrderId` |
| 7 | Apply for KYC-H5 | `kycTicketId`, `profileId` |
| 8 | Update KYC information | `kycTicketId`, `errorRedirectUrl` |
| 9 | Migrate KYC information | `kycTicketId`, `errorRedirectUrl` |
> A naming discrepancy worth knowing: the code labels `type=4` as apply for KYC (liveness) and defines a `type=8` for updating KYC information, whereas the API reference calls `type=4` face recognition and lists no `type=8`. This page follows the code.
**Minimum request**
```json theme={null}
{
"type": "7",
"customerId": "1000000123",
"kycTicketId": "9000000888",
"profileId": "",
"language": "en",
"theme": "default",
"mode": "light",
"userAgent": "Mozilla/5.0 ..."
}
```
**Response**
The H5 URL comes back in the `data` field of the common envelope, as a plain string. Send that URL to the user.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": "https://h5.thedecard-sandbox.com/kyc?token=..."
}
```
> H5 links expire, so get the user through the flow while the link is still valid. If it does expire, call this endpoint again for a fresh one, as long as the ticket is still `INIT`.
***
### Step 4: receive the result (webhook, with a query as backup)
**Webhook** (recommended; DCS pushes to you). Once your webhook is configured, every ticket status change arrives as a `KYC_TICKET` event.
| When | Event | status |
| - | - | - |
| The user makes their final submission on the H5 page | KYC\_TICKET | `PENDING` |
| The asynchronous review approves the ticket | KYC\_TICKET | `PASSED` |
For the webhook payload, see [Webhook events and schema](../webhooks/events-and-schema).
**Query as backup** (you): call `GET /open-api/kyc-ticket/v1/detail` at any time with either `kycTicketId` or `kycTicketRef` to read the current status.
***
### KYC ticket statuses (H5 mode)
| Status | Description | Terminal | What you do next |
| - | - | - | - |
| `INIT` | The ticket exists and is waiting for the user to submit on the H5 page | No | Send the H5 link to the user; call `guidance-link` again for a fresh link if it expires |
| `PENDING` | The user has submitted and DCS is reviewing asynchronously | No | Wait for the webhook, or poll |
| `PASSED` | Approved | Yes | Move on to the [virtual card application](../cards/virtual-card) |
> Standard `H5` mode normally shows only `INIT`, `PENDING` and `PASSED`; a problem in review usually leaves the ticket at `PENDING` pending manual intervention. The newer `H5-RENEWAL` route has its own statuses, `RENEWAL_INIT / PENDING / RETRY / PASS / REJECT`, which map back onto the ticket as `INIT / PASSED / REJECTED`. Regenerating a link for the same `kycTicketRef` still requires the ticket to be `INIT`: `NEED_VERIFY` and `PENDING` return in progress, `PASSED` returns already approved, and `REJECTED` returns verification failed. For the statuses used by `H5-MIGRATION`, see [KYC information migration](./kyc-migration).
***
### apply-kyc-h5 error codes
| Error code | Description |
| - | - |
| `DAPI_KYC_TICKET_CUSTOMER_NOT_FOUND` | The user the KYC ticket refers to does not exist |
| `DAPI_KYC_TICKET_CUSTOMER_NOT_FOUND_IN_ENTERPRISE` | That user does not belong to this enterprise |
| `DAPI_KYC_TICKET_REF_NOT_UNIQUE` | The `kycTicketRef` is not unique; it clashes with an existing ticket under a different owner |
| `DAPI_KYC_TICKET_IS_IN_PROCESSING` | The ticket is being processed (already `PENDING`) |
| `DAPI_KYC_TICKET_ALREADY_PASSED` | The ticket has already been approved; apply for the virtual card directly |
For how the apply-kyc-h5 error codes are grouped, see [KYC reject codes](./kyc-reject-codes); for the general and authorization error codes, see [Authorization declines and error codes](../transactions/decline-codes).
***
### Prerequisites
* You hold the Enterprise ApiKey / SecretKey β see [First steps](../../getting-started/first-steps).
* You have created the user (step 1) β see [Create a user](../users/create-customer).
* You have the `profileId` (card profile ID) from the DCS team.
***
## Next steps
* Once KYC is `PASSED`, go to [Apply for a virtual card](../cards/virtual-card) and issue the card by submitting `kycTicketId` and `customerId` with `cardApplyMode=NORMAL`. The same `kycTicketId` can be reused for any further cards you issue to that user.
* Prefer to submit documents through the API rather than a hosted page? See [Apply for KYC](./apply-kyc).
* To read a ticket's current status at any time, see [Query KYC](./query-kyc).
# KYC application parameter dictionary
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/kyc/kyc-application-fields
The complete dictionary of enumerated values accepted by the KYC application endpoint, so that you can fill each field in by code.
## π Guide
When you submit a KYC application (`/open-api/kyc-ticket/v1/apply-kyc`), fields such as the address document type, occupation, employment status and source of funds are all **enumerations**, and you must send the `code` shown in the tables below. This page lists those enumerations in full for you to work against during integration.
***
## Address proof document type (poaDocType)
`poaDocType` values do not differ between integration models β both share one list. **Which types are actually accepted depends on the user's country or region**: the accepted proof-of-address types vary by country, so follow the coverage list you confirmed with DCS.
| code | Name | Notes |
| :- | :- | :- |
| `UTILITY_BILL` | Utility bill | Water, electricity or gas; no older than 90 days (75 days in France) |
| `TELECOM` | Telecom bill | No older than 90 days |
| `BANK_STATEMENT` | Bank statement | No older than 90 days |
| `BANK_LETTER` | Bank letter | An official letter from a financial institution carrying account details, stamped or signed |
| `CREDIT_CARD_STATEMENT` | Credit card statement | No older than 90 days |
| `LEASE_AND_PAYMENT` | Lease agreement plus proof of payment | Proof of payment is also required, and must be no older than 90 days |
| `MORTGAGE_AND_PAYMENT` | Mortgage agreement plus proof of payment | Proof of payment is also required, and must be no older than 90 days |
| `SALARY_STATEMENT` | Salary statement | No older than 90 days |
| `ID_CARD` | Identity card | Accepted when it is not also used as the identity document |
| `DRIVING_LICENSE` | Driving license | Accepted when it is not also used as the identity document |
| `GOVERNMENT_STATEMENT` | Government statement | β |
| `GOVERNMENT_TAX_BILL` | Government tax bill | β |
Format, size and file-count limits are covered in [KYC document requirements](../../customer-success/faq-kyc-documents).
***
## Employment status (employmentStatus)
| code | name | desc |
| :- | :- | :- |
| `EMPLOYED` | Employed | In regular paid employment |
| `BUSINESS_OWNER` | Business owner | Owns and runs a business |
| `SELF_EMPLOYED` | Self-employed | Works for themselves, including freelancers |
| `UNEMPLOYED` | Unemployed | Not currently in work |
| `PENSIONER` | Pensioner | Retired and drawing a pension |
| `STUDENT` | Student | In full-time education |
***
## Industry (employmentJobIndustry)
| code | name | desc |
| :- | :- | :- |
| `INFORMATION` | Information and Communications | Information and Communications |
| `RETAIL_TRADE` | Retail Trade | Retail Trade |
| `FINANCIAL` | Financial and Insurance Activities | Financial and Insurance Activities |
| `ELECTRICITY` | Electricity | Electricity |
| `TRANSPORT` | Transport and Storage | Transport and Storage |
| `CONSTRUCTION` | Construction | Construction |
| `TRAVEL_RELATED` | Travel Related | Travel Related |
| `ACCOMMODATION` | Accommodation and Food Service | Accommodation and Food Service |
| `ENTERTAINMENT` | Entertainment | Entertainment |
| `PAYMENTS` | Payments | Payments |
| `PROFESSIONAL` | Professional Services | Professional Services |
| `LUXURY_GOODS` | Luxury Goods | Luxury Goods |
| `MANUFACTURING` | Manufacturing | Manufacturing |
| `MINING_AND_QUARRYING` | Mining and Quarrying | Mining and Quarrying |
| `AGRICULTURE_AND_FISHING` | Agriculture and Fishing | Agriculture and Fishing |
| `REAL_PROPERTY` | Real Property and Land Development | Real Property and Land Development |
| `WHOLESALE_TRADE` | Wholesale Trade | Wholesale Trade |
| `NON_EMPLOYED` | Non-Employed | Non-Employed |
***
## Occupation (occupation)
| code | name | number |
| :- | :- | :- |
| `PHYSIOTHERAPIST` | Physiotherapist | 308 |
| `SPECIALIST` | Specialist | 309 |
| `SURGEON` | Surgeon | 303 |
| `PATHOLOGIST` | Pathologist | 304 |
| `PLASTIC_SURGEON` | Plastic surgeon | 318 |
| `PSYCHOLOGIST` | Psychologist | 314 |
| `SR_EXECUTIVE` | Senior executive | 180 |
| `CHAIRMAN` | Chairman | 106 |
| `MANAGING_DIR` | Managing director | 108 |
| `JR_EXECUTIVE` | Junior executive | 200 |
| `TERTIARY_STUDENT` | Tertiary student | 2222 |
| `UNKNOWN` | Unknown occupation | 1200 |
| `GENERAL_WORKER` | General worker | 1198 |
| `UNEMPLOYED` | Unemployed | 1199 |
| `PILOT` | Pilot | 1169 |
| `AIR_STEWARD_STEWARDESS` | Air steward / stewardess | 1170 |
| `CLERK` | Clerk | 1157 |
| `CUST_SERVICE_CONSULTANT` | Customer service consultant | 1156 |
| `ARTIST_ENTERTAINMENT` | Artist / entertainer | 1183 |
| `BEAUTY_CONSULTNT_STYLIST` | Beauty consultant / stylist | 1184 |
| `SOCIAL_WORKER` | Social worker | 1141 |
| `TECHNICAL_CONSULTANT` | Technical consultant | 1131 |
| `IT_CONSULTANT` | IT consultant | 1123 |
| `COMPUTER_ANALYST` | Computer analyst | 1124 |
| `TECHNICIAN` | Technician | 1151 |
| `WAREHOUSING_PERSONNEL` | Warehousing personnel | 1152 |
| `SYSTEMS_PROGRAMMER` | Systems programmer | 1121 |
| `TEACHER` | Teacher | 1113 |
| `INSTRUCTOR` | Instructor | 1114 |
| `ASST_PROFESSOR` | Associate professor | 1107 |
| `VICE_DEAN` | Vice dean | 1104 |
| `PROFESSOR` | Professor | 1106 |
| `APPLICATION_PROGRAMMER` | Application programmer | 1122 |
| `OTHERS` | Other occupations | 1100 |
| `REMISIER` | Remisier | 1040 |
| `PROPERTY_AGENT` | Property agent | 1050 |
| `SALES_EXEC` | Sales executive | 1020 |
| `MARKETING_EXEC` | Marketing executive | 1010 |
| `HAWKER` | Hawker | 990 |
| `CONTRACTOR` | Contractor | 985 |
| `PARTNER` | Partner | 970 |
| `ARMY_SPECIALIST` | Army specialist | 860 |
| `CIVIL_SERVANT` | Civil servant | 840 |
| `DEPUTY_PM` | Deputy prime minister | 807 |
| `CABINET_MINISTER` | Cabinet minister | 809 |
| `BANKING_PROFESSIONAL` | Banking professional | 660 |
| `BANKER_FUND_MANAGER` | Banker / fund manager | 650 |
| `POLITICIAN_CIVIL_SERVANT` | Politician / civil servant | 800 |
| `DRAFTSMAN` | Draughtsman | 740 |
| `QUANTITY_SURVEYOR` | Quantity surveyor | 750 |
| `CHEM_ENGINEER` | Chemical engineer | 540 |
| `FIN_CONTROLLER` | Financial controller | 610 |
| `PROF_ACCOUNTANCY` | Accountancy professional | 600 |
| `HIGH_COURT_JUDGE` | High Court judge | 406 |
| `CHIEF_JUSTICE` | Chief Justice | 401 |
| `AUDITOR_GENERAL` | Auditor-General | 403 |
| `CIVIL_ENGINEER` | Civil engineer | 510 |
| `ELEC_ENGINEER` | Electrical engineer | 520 |
| `LEGAL_ADVISER` | Legal adviser | 490 |
| `PROF_ENGINEER` | Professional engineer | 500 |
| `JUDGE` | Judge | 408 |
| `LAWYER` | Lawyer | 420 |
| `DOCTOR` | Doctor | 360 |
| `GEN_PRACTITIONER` | General practitioner | 365 |
| `RESEARCHER` | Researcher | 350 |
| `BIOCHEMIST` | Biochemist | 351 |
| `GYNAECOLOGIST` | Gynaecologist | 322 |
| `MATRON` | Matron | 380 |
| `ORTHOPAEDICIAN` | Orthopaedic doctor | 310 |
| `OFFICER` | Military officer | 250 |
| `PROF_MEDICAL` | Medical professional | 300 |
| `OBSTETRICIAN` | Obstetrician | 320 |
| `PSYCHIATRIST` | Psychiatrist | 316 |
| `MGT_CONSULTANT` | Management consultant | 160 |
| `REG_MANAGER` | Regional manager | 140 |
| `DIV_MANAGER` | Divisional manager | 150 |
| `TOP_EXECUTIVE` | Top executive | 110 |
| `GENERAL_MGR` | General manager | 130 |
| `DEPT_MANAGER` | Department manager | 210 |
| `EXEC_OFFICER` | Executive officer | 230 |
| `PRESIDENT` | President | 102 |
| `V_PRESIDENT_DIR` | Vice-president / director | 104 |
| `SECURITY_SAFETY_PERSON` | Security and safety personnel | 1196 |
| `HOSPITALITY_PERSONNEL` | Hospitality personnel | 1197 |
| `HOUSEWIFE` | Housewife | 1190 |
| `STUDENT` | Student | 1191 |
| `COOK_CHEF` | Cook / chef | 1180 |
| `DRIVER` | Driver | 1181 |
| `DIVER` | Diver | 1160 |
| `SECRETARY` | Secretary | 1155 |
| `DESIGNER` | Designer | 1182 |
| `SUPERVISOR` | Supervisor | 1150 |
| `EDITOR` | Editor | 1145 |
| `JOURNALIST` | Journalist | 1146 |
| `PASTOR` | Pastor | 1140 |
| `PROJECT_CO_ORDINATOR` | Project co-ordinator | 1130 |
| `SYSTEMS_ANALYST` | Systems analyst | 1120 |
| `PRINCIPAL` | Principal | 1110 |
| `V_PRINCIPAL` | Vice-principal | 1111 |
| `LECTURER` | Lecturer | 1108 |
| `DEAN` | Dean | 1103 |
| `CHANCELLOR` | Chancellor | 1101 |
| `INSURANCE_AGENT` | Insurance agent | 1030 |
| `SALESMAN` | Salesperson | 1000 |
| `SOLE_PROPRIETOR` | Sole proprietor | 980 |
| `SELF_EMPLOYED` | Self-employed | 910 |
| `BUSINESS` | Business owner | 900 |
| `POLICE` | Police officer | 850 |
| `AMBASSADOR` | Ambassador | 820 |
| `PARLIAMENTARY_SECRETARY` | Parliamentary secretary | 830 |
| `AUDITOR` | Auditor | 640 |
| `V_PRESIDENT` | Vice-president | 803 |
| `PRIME_MINISTER` | Prime minister | 805 |
| `PROF_ARCHITECT` | Professional architect | 700 |
| `ARCHITECT` | Architect | 701 |
| `MECH_ENGINEER` | Mechanical engineer | 550 |
| `SYSTEMS_ENGINEER` | Systems engineer | 560 |
| `PROCESS_ENGINEER` | Process engineer | 530 |
| `ASST_ACCOUNTANT` | Assistant accountant | 622 |
| `CREDIT_ANALYST` | Credit analyst | 630 |
| `TREASURER` | Treasurer | 612 |
| `ACCOUNTANT` | Accountant | 620 |
| `FIN_CONSULTANT` | Financial consultant | 601 |
| `HEALTHCARE_PROFESSIONAL` | Healthcare professional | 390 |
| `PROF_LAW` | Legal professional | 400 |
| `DENTIST` | Dentist | 340 |
| `CHIEF_NURSE` | Chief nurse | 382 |
| `NURSE` | Nurse | 383 |
| `RADIOGRAPHER` | Radiographer | 370 |
| `PHARMACIST` | Pharmacist | 368 |
***
## Job seniority (jobSeniority)
| code | name | desc |
| :- | :- | :- |
| `SENIOR_MANAGEMENT` | Senior management | Senior Management |
| `DIRECTOR` | Director | Director |
| `PARTNER` | Partner | Partner |
| `EXECUTIVE` | Executive | Executive |
| `TEACHER` | Teacher | Teacher |
| `MANAGERIAL` | Managerial | Managerial |
| `NON_EMPLOYED` | Non-employed | Non Employed |
| `SALES` | Sales | Sales |
| `ASSOCIATE` | Associate | Associate |
| `DIPLOMAT` | Diplomat | Diplomat |
| `SUPERVISOR` | Supervisor | Supervisor |
***
## Purpose of account (purposeOfAccount)
| code | name | desc |
| :- | :- | :- |
| `CASH_ADVANCE_PROGRAM` | Cash advance programme | Cash Advance Program |
| `INVESTMENT` | Investment | Investment |
| `DAILY_SPENDING` | Daily spending | Daily Spending |
| `BILL_PAYMENT` | Bill payment | Bill Payment |
| `OTHERS` | Other | others |
***
## Source of funds (sourceOfFunds)
| code | name | desc |
| :- | :- | :- |
| `OWN_BANK_ACCOUNT` | Own bank account | Own Bank Account |
| `THIRD_PARTY_BANK_ACCOUNT` | Third-party bank account | Third Party Bank Account |
| `CRYPTO_TOP_UP` | Crypto top-up | Crypto Top Up |
| `DUSD` | DUSD | DUSD |
***
## Source of wealth (sourceOfWealth)
| code | name | desc |
| :- | :- | :- |
| `EMPLOYMENT` | Employment income | Employment |
| `SAVINGS` | Savings | Savings |
| `INHERITANCE` | Inheritance | Inheritance |
| `BUSINESS` | Business income | Business |
| `INVESTMENT` | Investment income | Investment |
| `TRUST_FUND` | Trust fund | Trust Fund |
| `CRYPTOCURRENCIES` | Cryptocurrencies | Cryptocurrencies |
## Next steps
* For where these enumerations are used, see the request parameter tables in [Apply for KYC](./apply-kyc).
* For how proof of address (POA) is submitted during the KYC flow, see [Apply for KYC](./apply-kyc).
# Query a user's KYC information
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/kyc/kyc-info
How-to task page: query the KYC summary per user (GET /open-api/kyc/v1/detail). Focuses on how it differs from querying a KYC ticket (both are called "querying KYC" and are easily confused), and routes kycRenewalRequired=true to the renewal page.
## π Guide
Besides checking the progress of one particular verification by ticket, you can also query the current KYC summary for a **user**: which country their proof of identity was issued in, and whether their KYC data has expired and needs renewing. This endpoint is how you make an eligibility decision before issuing a card, and how you discover in day-to-day operations that a user needs to renew.
## How this differs from "Query KYC ticket"
Both endpoints are about "querying KYC" but they answer different questions, and they are easy to confuse:
| | Query KYC ticket | Query a user's KYC information (this page) |
| - | - | - |
| Endpoint | `GET /open-api/kyc-ticket/v1/detail` | `GET /open-api/kyc/v1/detail` |
| Input | `kycTicketId` or `kycTicketRef` | `customerId` |
| Question answered | How far has **this verification** got, and why was it rejected | The current KYC summary of **this user**, and whether the data has expired |
| Typical use | Polling for the result after submitting KYC | Eligibility checks before card issuance; spotting that a renewal is due |
For ticket-level queries see [Query KYC](./query-kyc).
## Endpoint
**`GET /open-api/kyc/v1/detail`**
### Query parameters
| Parameter | In | Type | Required | Description |
| - | - | - | :-: | - |
| `customerId` | query | string | Yes | User ID (returned when the user was created) |
### Request example
```
GET /open-api/kyc/v1/detail?customerId=1000000123
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": {
"poiCountryCode": "SG",
"kycRenewalRequired": false
}
}
```
### `data` fields
| Field | Type | Description |
| - | - | - |
| `poiCountryCode` | string | Issuing country of the proof of identity (POI), 2-letter ISO, such as `CN` / `US` / `SG` |
| `kycRenewalRequired` | boolean | Whether this user's KYC data needs renewing. `true` means the documents / KYC data have expired and the user must be guided through verification again |
> The common envelope is `{ code, message, messageDetail, data }`; judge success by `code == "SYS_SUCCESS"`. See the [Authentication guide](../../integration-resources/authentication) for the field semantics.
## What to do when `kycRenewalRequired=true`
`kycRenewalRequired` set to `true` means the user's documents / KYC data have expired and verification must be completed again. **Until the renewal completes, some of the user's operations (such as spending) may be restricted.**
There are two ways to learn about this, and they always agree:
* **Pull**: call this endpoint and read `kycRenewalRequired`.
* **Push**: DCS sends the user-level [`KYC` webhook](../webhooks/events-and-schema#kyc) with `kycRenewalRequired=true`, and `kycRenewalType` tells you which factors need renewing (`POI` / `SELFIE`) so your front end can prompt for just that item.
For the full two-step flow that guides the user through the renewal, see [Updating KYC information](./kyc-renewal). Once it passes, DCS pushes `kycRenewalRequired=false` and the restriction is lifted.
## Prerequisites
* You hold the Enterprise ApiKey / SecretKey β see [First steps](../../getting-started/first-steps).
* You have created the user and hold their `customerId` β see [Create a user](../users/create-customer).
## Next steps
* The user needs to renew β [Updating KYC information](./kyc-renewal)
* Check the progress of one verification β [Query KYC](./query-kyc)
* Reuse KYC the user already completed under the DeCard-Managed model β [KYC information migration](./kyc-migration)
# KYC information migration
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/kyc/kyc-migration
How-to task page: let a user who already completed KYC under the DeCard-Managed model reuse that data when applying for a card under Partner-Managed (kycApplyMode=H5-MIGRATION / guide page type=9). Organized as a two-step flow with request fields and a minimal request; the three ticket statuses and all error codes are listed inline rather than linked out.
## π Guide
If your user already completed KYC under the **DeCard-Managed model** and you now want to issue them a card under the **Partner-Managed model**, they do not have to submit their documents, selfie and address all over again β you can start a **KYC information migration** and let the user simply confirm the reuse of their existing data on a DCS-hosted H5 page. As a licensed card issuer, DCS verifies whether the source user's KYC is eligible for reuse and, once it passes, attaches usable KYC information to the target user.
Migration reuses the same two-step H5 KYC flow; the only differences are that the application mode is `H5-MIGRATION`, the guide page type is `type=9`, and the source user must be identified.
## When to use it
| Your situation | Use |
| - | - |
| Brand-new user, you collect the documents and submit them via API | [Apply for KYC (API mode)](./apply-kyc) |
| Brand-new user, let them fill everything in on the DCS-hosted page | [H5 KYC guide page](./h5-kyc-guidance) |
| The user's documents / KYC data have expired and need re-verification | [Updating KYC information](./kyc-renewal) |
| **The user already completed KYC under the DeCard-Managed model and you want to reuse it** | **This page** |
## Who does what
| Step | Who | Notes |
| - | - | - |
| Determine that the user already has KYC under the DeCard-Managed model and obtain their `externalUserId` | You | The input to a migration |
| Apply for the migration ticket, exchange it for an H5 link, send the link to the user | You | Two API calls |
| Verify whether the source user's KYC is eligible for reuse | DCS | Not eligible means the ticket is rejected up front |
| Provide the H5 page on which the user confirms the reuse | DCS | No UI to build on your side |
| Advance the ticket status asynchronously and send webhooks | DCS | Event type `KYC_TICKET` |
## Interaction flow
A migration can be rejected. Once rejected the ticket is in a terminal state, so you must **apply for a brand-new migration ticket** to let the user retry β a terminal ticket cannot be reused.
## Step 1: Apply for the migration ticket (you)
**`POST /open-api/kyc-ticket/v1/apply-kyc-h5`**
| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `kycTicketRef` | string | Yes | β€50 | Your own business idempotency key; you can query by it later |
| `customerId` | string | Yes | β€50 | The **target user** β the `customerId` in this model you want to issue a card to |
| `kycApplyMode` | string | Yes | β€20 | Fixed `H5-MIGRATION` |
| `sourceCustomerId` | string | Yes | β€50 | The **source user identifier** β pass that user's `externalUserId` from the DeCard-Managed model. Note that despite the `...CustomerId` name this is **not** a `customerId` of this model |
```json theme={null}
{
"kycTicketRef": "kyc-mig-20260728-0001",
"customerId": "1000000123",
"kycApplyMode": "H5-MIGRATION",
"sourceCustomerId": "usr_8f3a..."
}
```
The response returns the new ticket inside the common envelope's `data`, with `status` set to `INIT`:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"kycTicketId": "9000000888",
"kycTicketRef": "kyc-mig-20260728-0001",
"customerId": "1000000123",
"status": "INIT",
"createTime": "2026-07-28T10:00:00+08:00"
}
}
```
This step **synchronously checks whether the source user is eligible for reuse**. If not, no ticket is created and `DAPI_KYC_MIGRATION_NOT_ELIGIBLE` is returned (see the error codes below). In other words, once you have a `kycTicketId` the source user has already passed the pre-check and all that remains is the user's confirmation on H5.
## Step 2: Exchange for the migration guide link (you)
**`POST /open-api/card-redirect/v1/guidance-link`**
| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `type` | string | Yes | β€1 | Fixed `9` (KYC information migration) |
| `customerId` | string | Yes | β€50 | The same target user as in step 1 |
| `kycTicketId` | string | Yes | β€50 | The `kycTicketId` returned in step 1 |
| `errorRedirectUrl` | string | **Yes** | β€200 | Error page to redirect to when the migration fails. **Required** for `type=9`; omitting it returns `DAPI_SYS_ILLEGAL_PARAM` |
| `language` | string | Yes | Length 2 | `zh` / `en`; non-zh countries are uniformly mapped to `en` |
| `theme` | string | Yes | β€10 | Such as `default` |
| `mode` | string | Yes | β€10 | `light` / `dark` |
| `userAgent` | string | Yes | β€300 | User browser UA |
| `successfulRedirectUrl` | string | No | β€200 | Redirect address on success |
| `submitAutoClosed` | string | No | β€1 | `Y` closes the page automatically after a successful submission / `N` does not |
| `primaryColor` | string | No | β€7 | Main colour, such as `#FFFFFF` |
> `type=9` does **not** need `profileId` (that is required only for `type=7`, apply for KYC-H5).
```json theme={null}
{
"type": "9",
"customerId": "1000000123",
"kycTicketId": "9000000888",
"errorRedirectUrl": "https://your.app/kyc/failed",
"language": "en",
"theme": "default",
"mode": "light",
"userAgent": "Mozilla/5.0 ..."
}
```
The response `data` is a string β the H5 URL you hand to the user:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": "https://h5.thedecard-sandbox.com/kyc?token=..."
}
```
> The H5 link has a limited lifetime. If it expires while the ticket is still `INIT`, call this endpoint again for a fresh link β you do **not** need to apply for a new ticket.
## Step 3: Receive the result and retry if needed
Learn the outcome from the [`KYC_TICKET` webhook](../webhooks/events-and-schema) or by calling `GET /open-api/kyc-ticket/v1/detail` yourself (with `kycTicketId` or `kycTicketRef`):
* **`PASSED`**: the migration is complete, the target user now has usable KYC, and you can go on to [issue a card](../cards/card-issuing) (`cardApplyMode=NORMAL`, submitting that `kycTicketId` and `customerId`).
* **`REJECTED`**: this migration was rejected; `errorCode` / `errorMessage` explain why. Go back to step 1 and apply for a new ticket with a **new `kycTicketRef`**.
### Ticket status (migration mode)
In migration mode a ticket only ever takes these three statuses β `NEED_VERIFY` and `PENDING` do **not** occur:
| Status | Meaning | Terminal | Your next step |
| - | - | :-: | - |
| `INIT` | Ticket created, waiting for the user to confirm on H5; processing also stays at `INIT` | No | Send the H5 link to the user; call `guidance-link` again if the link expires |
| `PASSED` | Migration passed, the target user now has usable KYC | Yes | Nothing to do; proceed to card issuance |
| `REJECTED` | Migration rejected | Yes | Read `errorCode` / `errorMessage` and re-apply with a new `kycTicketRef` |
## Error codes
### apply-kyc-h5 (`kycApplyMode=H5-MIGRATION`)
In addition to the common error codes listed on the [H5 KYC guide page](./h5-kyc-guidance), migration mode can also return:
| Error code | Meaning | Trigger | Retryable |
| - | - | - | :-: |
| `DAPI_KYC_SOURCE_USER_NOT_FOUND` | source user not found | `sourceCustomerId` not supplied, or the source user does not exist | β
Verify the source identifier and retry |
| `DAPI_KYC_TICKET_CUSTOMER_NOT_FOUND` | kyc ticket customer not found | The target `customerId` does not exist or registration is incomplete | β
Create the user first |
| `DAPI_KYC_MIGRATION_ALREADY_EXISTS` | migration KYC ticket already exists for this source or customer | The source user already has an in-progress or passed migration, or the target user already has usable KYC | β No migration needed |
| `DAPI_KYC_MIGRATION_NOT_ELIGIBLE` | not eligible for KYC migration | The source user's KYC is not eligible for reuse (not passed, inconsistent with the target user's details, or migration not enabled for this channel) | β Use the regular KYC flow |
| `DAPI_POA_COUNTRY_INVALID` | proof of address country not supported | The source user's proof-of-address country is out of scope | β Use the regular KYC flow |
| `DAPI_TICKET_APPLY_LIMIT_EXCEEDED` | Ticket application exceeds the limit | Too many applications for the same user; the intake rate limit was hit | β
Back off and retry |
### guidance-link (`type=9`)
| Error code | Meaning | Trigger |
| - | - | - |
| `DAPI_SYS_ILLEGAL_PARAM` | System illegal param | `kycTicketId` or `errorRedirectUrl` is empty |
| `DAPI_KYC_TICKET_NOT_FOUND` | kyc ticket not found | Ticket does not exist / belongs to someone else / is not a migration ticket |
| `DAPI_KYC_TICKET_IS_IN_PROCESSING` | KYC ticket is in processing | The ticket has already been submitted and is still being processed; the operation cannot be repeated |
| `DAPI_KYC_TICKET_ALREADY_PASSED` | KYC ticket already passed | The ticket already passed β go straight to card issuance |
## Prerequisites
* You hold the Enterprise ApiKey / SecretKey β see [First steps](../../getting-started/first-steps).
* You have [created the target user](../users/create-customer) in this model and hold their `customerId`.
* The source user has completed KYC under the DeCard-Managed model and you can obtain their `externalUserId`.
* DCS has enabled migration for your channel (otherwise `DAPI_KYC_MIGRATION_NOT_ELIGIBLE` is returned β contact the DCS team).
## Next steps
* Issue a card for the user after a successful migration β [Card issuance](../cards/card-issuing)
* The user's data has expired and needs re-verification β [Updating KYC information](./kyc-renewal)
* Check the current ticket status β [Query KYC](./query-kyc)
# KYC reject codes
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/kyc/kyc-reject-codes
The KYC error codes from the original data dictionary, broken out into the KYC domain; 60+ reject codes grouped into buckets, plus actionable columns.
## Guide
Whether your user simply needs to retake a blurry photo or has hit a compliance red line and cannot go any further, a single `errorCode` field tells you three things β whether to let the user try again, exactly what the user should do, and whether a resubmission is still possible β so that every KYC rejection becomes clear guidance rather than a vague "verification failed". As a licensed card issuer, DCS normalises the dozens of rejection reasons coming from the underlying KYC vendors into one stable set of error codes that you can consume directly in your own product.
### Where the reject code appears
When a KYC ticket's `status` moves to `REJECTED`, the response carries a pair of fields describing the reason:
| Field | Type | Description |
| - | - | - |
| `errorCode` | String | The normalised reject code; for the values see the [full reject code list](#full-reject-code-list) below |
| `errorMessage` | String | The human-readable text for that `errorCode`, which may be phrased for the end user |
The pair appears in the two places below with **identical values**, so you can read whichever channel suits you:
* **Querying the KYC status** (you pull): see [Query KYC status](./query-kyc); the response carries `errorCode` / `errorMessage` when `status=REJECTED`.
* **The KYC\_TICKET webhook** (you receive): DCS pushes a notification whenever the ticket status changes, and the body carries the identically named `errorCode` / `errorMessage` fields (returned only for `REJECTED`). For the structure see [Webhook data structures](../webhooks/events-and-schema).
> **Who does what**: the normalisation is done by **DCS**. You never have to deal with the raw reject codes of the underlying vendor (such as Sumsub) β you only integrate against the one set of `errorCode` values in the tables below. Mapping the raw codes onto this list is DCS's job, and DCS notifies you in advance whenever a new value is added.
### How to use these codes
Once you have an `errorCode`, work through the three steps below, which map onto the "Retryable / What the user should do / Resubmission possible" columns of the tables:
1. **Decide whether it is retryable.** First establish whether the code is fixable (retry) or terminal (no retry). Never push a user to resubmit against a terminal code; they will just be rejected again.
2. **Tell the user what to do.** For fixable codes, turn `errorMessage` or the "What the user should do" column into an in-product prompt (for example, "Please retake your document in better light"). For terminal codes, show one neutral message (for example, "Sorry, this application cannot proceed β please contact support") and **do not echo the specific compliance reason** (a sanctions or PEP hit is sensitive information).
3. **Decide whether a resubmission is possible.** Codes marked "Yes" go through the supplementary-document path (no need to fetch a new Sumsub token, call the resubmission endpoint directly); codes marked "No" mean starting over or stopping.
### Full reject code list
> The tables below regroup every reject code from the data dictionary into **business buckets** and add actionable columns. The `errorCode` values and their descriptions come faithfully from the data dictionary; the bucket, retryability, user guidance and resubmission columns are DCS documentation guidance, and the compliance position prevails in the end.
>
> **Retryable**: β
fixable, then retry / β terminal, do not retry / β οΈ depends (usually a system-side problem β retry later or escalate to a human).
#### Bucket 1 Β· Document image quality (fixable)
Something is wrong with the image itself (blurred, damaged, missing pages, wrong format); just have the user retake or re-upload it.
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `POOR_IMAGE_CAPTURE_QUALITY` | Please retake the photo in better light | β
| Retake the document photo somewhere well lit | Yes |
| `DOCUMENT_DAMAGED_OR_UNCLEAR` | Please upload a clear, undamaged document | β
| Upload a clear, undamaged document | Yes |
| `POOR_PHOTO_QUALITY` | Please upload a clear photo | β
| Retake and upload a clear photo | Yes |
| `LOW_DOCUMENT_QUALITY` | Please upload a sharper image | β
| Upload a higher-resolution image | Yes |
| `DOCUMENT_PAGE_MISSING` | A required document page is missing | β
| Supply the missing document page | Yes |
| `INCOMPLETE_DOCUMENT_SUBMISSION` | Please upload the complete document | β
| Upload the complete document | Yes |
| `BACK_SIDE_MISSING` | Please upload the back of the document | β
| Upload the back of the document as well | Yes |
| `FRONT_SIDE_MISSING` | Please upload the front of the document | β
| Upload the front of the document as well | Yes |
| `UNSUPPORTED_DOCUMENT_FORMAT` | Please upload the original document / Please upload a colour copy of the document / Please upload the original physical document | β
| Upload the original physical document in colour, not a re-photograph or photocopy | Yes |
| `INVALID_UPLOAD_TYPE` | Please upload a valid document | β
| Upload a valid document that meets the requirements | Yes |
| `SCREENSHOT_DETECTED` | Please upload the original document | β
| Do not submit a screenshot; photograph the original with the camera | Yes |
| `COLORED_COPY_REQUIRED` | Please upload a colour document | β
| Upload it in colour, not a black-and-white photocopy | Yes |
| `ORIGINAL_DOCUMENT_REQUIRED` | Please upload the original document | β
| Upload the original physical document, not a re-photograph or photocopy | Yes |
#### Bucket 2 Β· Document validity / type (partly fixable)
The document itself has expired, its type or template is not supported, or its language is not supported.
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `EXPIRED_DOCUMENT` | The document has expired | β
| Use a document that is still valid | Yes |
| `UNSUPPORTED_DOCUMENT_TYPE` | The document type submitted is not supported | β
| Use a supported document type instead | Yes |
| `UNSUITABLE_DOCUMENT_SUBMITTED` | The document type is not supported | β
| Use a supported document type instead | Yes |
| `UNSUPPORTED_OR_INVALID_DOCUMENT_TEMPLATE` | The document submitted is invalid | β
| Use a supported, valid document instead | Yes |
| `INVALID_IDENTIFICATION_DOCUMENT` | The identity document is invalid | β
| Use a valid identity document instead | Yes |
| `DOCUMENT_VALIDATION_FAILED` | The document could not be validated | β
| Re-upload a clear, valid document | Yes |
| `UNSUPPORTED_LANGUAGE` | The language of the document submitted is not yet supported | β
| Use a document in a supported language | Yes |
| `UNSUPPORTED_DOCUMENT_LANGUAGE` | The language of the document submitted is not yet supported | β
| Use a document in a supported language | Yes |
#### Bucket 3 Β· Supplementary material / incomplete information (fixable via resubmission)
Material is missing or details are incomplete; just prompt the user to supply the rest. **A new Sumsub token is usually not needed.**
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `ADDITIONAL_SUPPORTING_DOCUMENT_REQUIRED` | Supporting documents are required | β
| Upload the supporting documents requested | Yes |
| `MORE_SUPPORTING_DOCUMENTS_REQUIRED` | Supporting documents are required | β
| Upload the supporting documents requested | Yes |
| `INCOMPLETE_APPLICANT_DATA` | The current information needs to be completed | β
| Complete the application details and resubmit | Yes |
#### Bucket 4 Β· Information mismatch (fixable by checking the details)
What the user entered does not match their document or the reference database; prompt them to check and correct it.
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `INFORMATION_MISMATCH` | The information does not match | β
| Check and correct the details entered | Yes |
| `PROFILE_INFORMATION_MISMATCH` | The user's details do not match the document | β
| Correct the details so they match the document | Yes |
| `SUBMITTED_DATA_MISMATCH` | The application could not be processed | β
| Check the details and submit again | Yes |
| `DATABASE_INFORMATION_MISMATCH` | The information could not be verified | β
| Check the personal details and submit again | Yes |
| `ADDRESS_INFORMATION_MISMATCH` | Address verification failed | β
| Check the address or supply proof of address | Yes |
| `AGE_MISMATCH` | The age does not match | β
| Check the date of birth and related details | Yes |
| `INVALID_POA` | The proof of address is invalid | β
| Re-upload a valid proof of address | Yes |
| `INVALID_PROOF_OF_ADDRESS` | Address verification failed | β
| Re-upload a valid proof of address | Yes |
| `INVALID_POI` | The proof of identity is invalid | β
| Re-upload a valid proof of identity | Yes |
| `INVALID_PROOF_OF_IDENTITY` | Identity verification failed | β
| Re-upload a valid proof of identity | Yes |
| `INVALID_PROOF_OF_PAYMENT` | The proof of payment was not accepted | β
| Re-upload a valid proof of payment | Yes |
#### Bucket 5 Β· Face / liveness verification (partly fixable)
The face match or liveness check did not pass. Quality problems can be retaken; suspected third-party assistance, multiple people or spoofing is closer to terminal.
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `FACE_VERIFICATION_FAILED` | Face verification could not be completed / Face verification failed / Identity verification failed | β
| Have the user redo face verification in person, in good light | Yes |
| `LIVENESS_CHECK_FAILED` | The liveness check did not pass | β
| Have the user redo the liveness check in person, following the prompts | Yes |
| `THIRD_PARTY_ASSISTANCE_DETECTED` | Please complete the verification on your own | β | Tell the user they must complete it unassisted (terminal β do not retry in a loop) | No |
| `IDENTITY_OWNERSHIP_VALIDATION_FAILED` | Please complete the verification in person | β | Tell the user they must complete it in person (terminal) | No |
| `MULTIPLE_PERSONS_DETECTED` | Please complete the verification on your own | β | Tell the user they must complete it alone (terminal) | No |
#### Bucket 6 Β· Compliance / risk terminal states (not retryable, show a neutral message)
The application hit a compliance red line such as sanctions, screening, EDD or suspicious behavior, and is **terminal**. Show one neutral message, do not echo the specific reason, and do not prompt a resubmission.
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `NAME_SCREENING_HIT` | Name screening hit | β | Show a neutral message and point the user to support | No |
| `EDD_REJECTED` | Enhanced due diligence rejected | β | Show a neutral message and point the user to support | No |
| `SUSPICIOUS_APPLICATION_BEHAVIOR` | Cannot proceed at this time | β | Show a neutral message | No |
| `SECURITY_VALIDATION_FAILED` | Cannot proceed at this time | β | Show a neutral message | No |
| `SECURITY_SCREENING_FAILED` | Cannot proceed at this time | β | Show a neutral message | No |
| `SCREENING_UNSUCCESSFUL` | Cannot proceed at this time | β | Show a neutral message | No |
| `COMPLIANCE_RESTRICTION` | Cannot proceed at this time | β | Show a neutral message | No |
| `SUMSUB_BLOCKED` | Cannot proceed at this time; please do not resubmit | β | Show a neutral message and point the user to support | No |
| `BLOCKED` | Cannot proceed at this time; please do not resubmit | β | Show a neutral message and point the user to support | No |
#### Bucket 7 Β· Eligibility / region restrictions (terminal, varies by partner configuration)
The user is outside the supported countries or regions, or fails a partner, age or similar eligibility requirement. Mostly terminal: for that user, retrying will not help.
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `OUT_OF_ELIGIBLE_COUNTRIES` | Outside the supported countries | β | Tell the user their country is not supported yet | No |
| `REGION_OR_RESIDENCY_RESTRICTION` | The current eligibility conditions do not allow this service | β | Tell the user their current circumstances are not supported yet | No |
| `ELIGIBILITY_OR_REGION_RESTRICTION` | This service is not yet available in your region / Cannot proceed at this time | β | Tell the user their region is not supported yet | No |
| `FAIL_TO_MEET_DCS_REQUIREMENT` | Does not meet DCS requirements | β | Show a neutral message and point the user to support | No |
| `FAIL_TO_MEET_PARTNER_REQUIREMENT` | Does not meet the partner's requirements | β | Word the message as the partner prefers | No |
| `ELIGIBILITY_REQUIREMENT_NOT_MET` | Does not meet the requirements | β | Show a neutral message | No |
| `AGE_REQUIREMENT_NOT_MET` | Does not meet the eligibility requirements | β | Tell the user the age requirement is not met | No |
| `USA_TAX_RESIDENT` | This product is not currently offered to US tax residents | β | State clearly that the product is not available to US tax residents | No |
| `INCOME_REQUIREMENT_NOT_MET` | Does not meet the eligibility requirements; please do not resubmit | β | Show a neutral message; do not prompt a resubmission | No |
| `CREDIT_ASSESSMENT_FAILED` | Does not meet the eligibility requirements; please do not resubmit | β | Show a neutral message; do not prompt a resubmission | No |
| `UNSUPPORTED_PRODUCT` | Cannot proceed at this time; please do not resubmit | β | Tell the user this product cannot be applied for right now | No |
#### Bucket 8 Β· Duplicates / suspected fraud (terminal)
A duplicate application, or suspected forgery or tampering.
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `DUPLICATE_APPLICATION_DETECTED` | An application record already exists | β | Tell the user an application already exists and not to submit again | No |
| `INVALID_OR_DUPLICATE_SUBMISSION` | The information submitted could not be verified | β | Show a neutral message | No |
| `REAPPLICATION_PERIOD_NOT_MET` | After a card is closed you must wait out the cooling-off period before reapplying | β | Tell the user explicitly to wait until the cooling-off period ends, so they stop retrying | No |
#### Bucket 9 Β· Verification could not complete / data source problem (depends)
The application data could not be verified, or an upstream data source or database was unavailable. Separate "the user needs to check something" from "retry later, it is on the system side".
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `APPLICANT_DATA_VALIDATION_FAILED` | The information submitted could not be verified / Cannot proceed at this time | β οΈ | Check the details and retry; escalate to support if it keeps failing | Depends |
| `APPLICANT_DATA_NOT_FOUND` | The information could not be verified | β οΈ | Check the personal details and retry | Depends |
| `VERIFICATION_INCOMPLETE` | Verification could not be completed | β οΈ | Go through the verification flow again | Depends |
| `VERIFICATION_CHECK_UNAVAILABLE` | System error | β οΈ | Retry later (system-side problem) | Depends |
| `CONNECTIVITY_OR_SERVICE_ERROR` | System error | β οΈ | Retry later (system-side problem) | Depends |
| `DATA_SOURCE_UNAVAILABLE` | System error | β οΈ | Retry later (system-side problem) | Depends |
| `TIMEOUT` | The request timed out because no response arrived within the configured time limit | β οΈ | Retry later; escalate to support if it keeps failing | Depends |
| `OTHER_VALIDATION_ISSUE` | The application cannot be processed for now | β οΈ | Retry later or contact support | Depends |
#### Bucket 10 Β· Share token / reusable KYC (mostly retryable)
Reject reasons specific to the Sumsub share token or reusable KYC path. Most are token or session problems that a fresh attempt fixes; the channel-capability ones are terminal.
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `INVALID_SHARE_TOKEN` | The share token is invalid or has expired | β
| Obtain a fresh share token and submit again | Yes |
| `SHARE_TOKEN_FAILED` | The share token is invalid or has expired | β
| Obtain a fresh share token and submit again | Yes |
| `INVALID_VERIFICATION_SESSION` | Please restart the verification flow | β
| Guide the user through the verification from the beginning | Yes |
| `VERIFICATION_PROCESSING_TIMEOUT` | Please restart the verification flow | β
| Guide the user through the verification from the beginning | Yes |
| `REUSABLE_INCOMPATIBLE_DOCUMENT` | The document type is not compatible | β
| Re-verify with a supported document type | Yes |
| `REUSABLE_KYC_NOT_ENABLED` | Reusable KYC is not supported at the moment | β | Use the regular KYC flow; contact DCS to have it enabled | No |
| `REUSABLE_VERIFICATION_NOT_ELIGIBLE` | The applicant's data is not eligible for reusable KYC | β | Use the regular KYC flow | No |
#### Bucket 11 Β· Submission not completed within the time window (TIMEOUT\_\*)
The `TIMEOUT_` prefix always means **the same thing**: the user failed to complete the submission to Sumsub within the allotted time window, so the request was terminated on timeout. The suffix only tells you which step it was on when it timed out β it does **not** mean that check actually failed.
Troubleshooting tip: these codes account for a non-trivial share of rejections and are easily misread as "there is something wrong with the user's documents". When you see `TIMEOUT_*`, treat it as a **timeout** first β have the user start again and finish promptly, rather than asking them to re-supply whatever the suffix names.
| errorCode | Timed out during | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `TIMEOUT_INVALID_PROOF_OF_IDENTITY` | Proof-of-identity check | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_INVALID_PROOF_OF_ADDRESS` | Proof-of-address check | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_DOCUMENT_VALIDATION_FAILED` | Document validation | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_DOCUMENT_PAGE_MISSING` | Document completeness check | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_EXPIRED_DOCUMENT` | Document expiry check | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_POOR_PHOTO_QUALITY` | Photo quality check | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_SCREENSHOT_DETECTED` | Screenshot detection | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_FACE_VERIFICATION_FAILED` | Face verification | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_AGE_REQUIREMENT_NOT_MET` | Age eligibility check | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_ELIGIBILITY_OR_REGION_RESTRICTION` | Eligibility / region check | β
| Start again and complete the submission promptly | Yes |
| `TIMEOUT_SUMSUB_BLOCKED` | Vendor risk check | β οΈ | Start again; escalate to support if it keeps failing | Depends |
#### Bucket 12 Β· Other
| errorCode | Description | Retryable | What the user should do | Resubmission possible |
| - | - | :-: | - | :-: |
| `UNSATISFACTORY_DOCUMENT` | The document is not acceptable | β
| Re-upload a document that meets the requirements | Yes |
| `OTHERS` | Other reason | β οΈ | Judge from `errorMessage`; escalate to support if unclear | Depends |
### Integration advice
* **Branch on `errorCode`, never hard-code `errorMessage` text.** `errorMessage` may change between versions or be localised; base your program logic on `errorCode` and use either `errorMessage` or your own copy from these tables for display.
* **Treat `errorMessage` as multilingual.** Do not assume which language it comes back in. Program branches always follow `errorCode`; for display, use your own copy from these tables or localise `errorMessage` as a fallback.
* **Handle unknown codes.** Values are added to and removed from these tables as compliance policy evolves. Keep a fallback branch for any `errorCode` outside the known list (handle it as `OTHERS` and raise an alert) so that a new code never breaks your front end.
* **Do not retry terminal codes in a loop.** Buckets 6, 7 and 8 are terminal; resubmitting is pointless and may itself trigger risk controls. Show one neutral message and point the user to support.
* **Resubmissions do not need a new token.** Where a resubmission is possible, such as proof of address (POA), supply the material through the resubmission path described in [Apply for KYC](./apply-kyc); you usually do not need a fresh Sumsub share token.
***
## Next steps
* How to guide a user through a resubmission once you have the reject code: back to [Apply for KYC](./apply-kyc).
* Polling the ticket status yourself and parsing `errorCode`: see [Query KYC status](./query-kyc).
* Letting users supply missing documents on a hosted page: see [H5 KYC guide page](./h5-kyc-guidance).
# Updating KYC information
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/kyc/kyc-renewal
How-to task page: guide the user through re-verification after documents / KYC data expire (kycApplyMode=H5-RENEWAL / guide page type=8). Entry point is kycRenewalRequired=true; organized as a two-step flow, including the retry loop after a rejection and when the restriction is lifted.
## π Guide
Identity documents and KYC data both have an expiry. Once they expire the user must complete verification again for the account to stay compliant and keep spending normally. DCS notifies you proactively when it detects an expiry; all you have to do is guide the user to re-capture their documents and selfie on a DCS-hosted H5 page.
Renewal reuses the same two-step H5 KYC flow; the only differences are that the application mode is `H5-RENEWAL` and the guide page type is `type=8`.
## When to use it
A user needs to renew their KYC data whenever either of these holds:
* [Query a user's KYC information](./kyc-info) returns `kycRenewalRequired=true`;
* You receive the user-level [`KYC` webhook](../webhooks/events-and-schema#kyc) with `kycRenewalRequired=true`.
The webhook's `kycRenewalType` states which factors need renewing (`POI` proof of identity / `SELFIE`), which you can use to prompt for only that item in your front end.
> **Until the renewal completes, some of the user's operations (such as spending) may be restricted.** Guide the user through it as early as possible.
## How this relates to the other KYC paths
| Your situation | Use |
| - | - |
| Brand-new user, you collect the documents and submit them via API | [Apply for KYC (API mode)](./apply-kyc) |
| Brand-new user, let them fill everything in on the DCS-hosted page | [H5 KYC guide page](./h5-kyc-guidance) |
| **The user's data has expired and needs re-verification** | **This page** |
| The user already completed KYC under the DeCard-Managed model and you want to reuse it | [KYC information migration](./kyc-migration) |
## Interaction flow
A renewal can be rejected. Once rejected the ticket is terminal, but the user **still needs to renew** (`kycRenewalRequired` remains `true`); apply for a new ticket to let them retry until it passes and `kycRenewalRequired` becomes `false`.
## Step 1: Apply for the renewal ticket (you)
**`POST /open-api/kyc-ticket/v1/apply-kyc-h5`**
| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `kycTicketRef` | string | Yes | β€50 | Your own business idempotency key; you can query by it later |
| `customerId` | string | Yes | β€50 | The user whose data needs renewing |
| `kycApplyMode` | string | Yes | β€20 | Fixed `H5-RENEWAL` |
```json theme={null}
{
"kycTicketRef": "kyc-renew-20260728-0001",
"customerId": "1000000123",
"kycApplyMode": "H5-RENEWAL"
}
```
This step **synchronously checks that the user really does need to renew**. If they do not (`kycRenewalRequired` is not `true`), no ticket is created and `DAPI_KYC_RENEWAL_NOT_REQUIRED` is returned. Confirm first via [Query a user's KYC information](./kyc-info) or the webhook.
Only **one** in-progress (`INIT`) renewal ticket is allowed per user at a time; you can apply again only after the previous one reaches a terminal state (`PASSED` / `REJECTED`).
## Step 2: Exchange for the renewal guide link (you)
**`POST /open-api/card-redirect/v1/guidance-link`**
| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `type` | string | Yes | β€1 | Fixed `8` (update KYC information) |
| `customerId` | string | Yes | β€50 | Same as step 1 |
| `kycTicketId` | string | Yes | β€50 | The `kycTicketId` returned in step 1 |
| `errorRedirectUrl` | string | **Yes** | β€200 | Error page to redirect to when the renewal fails. **Required** for `type=8`; omitting it returns `DAPI_SYS_ILLEGAL_PARAM` |
| `language` | string | Yes | Length 2 | `zh` / `en`; non-zh countries are uniformly mapped to `en` |
| `theme` | string | Yes | β€10 | Such as `default` |
| `mode` | string | Yes | β€10 | `light` / `dark` |
| `userAgent` | string | Yes | β€300 | User browser UA |
| `successfulRedirectUrl` | string | No | β€200 | Redirect address on success |
| `submitAutoClosed` | string | No | β€1 | `Y` closes the page automatically after a successful submission / `N` does not |
| `primaryColor` | string | No | β€7 | Main colour, such as `#FFFFFF` |
> `type=8` does **not** need `profileId` (that is required only for `type=7`, apply for KYC-H5).
The response `data` is a string β the H5 URL you hand to the user. The link has a limited lifetime; if it expires while the ticket is still `INIT`, call this endpoint again for a fresh link β you do **not** need to apply for a new ticket.
## Step 3: Receive the result and retry if needed
Learn the review outcome from the [`KYC_TICKET` webhook](../webhooks/events-and-schema) or by calling `GET /open-api/kyc-ticket/v1/detail`:
* **`PASSED`**: the renewal is complete. You will then receive the user-level `KYC` webhook with `kycRenewalRequired=false` and the restriction is lifted.
* **`REJECTED`**: this renewal was rejected and `kycRenewalRequired` is **still `true`**. Go back to step 1 and apply for a new ticket with a **new `kycTicketRef`**.
### Ticket status (renewal mode)
In renewal mode a ticket only ever takes these three statuses β `NEED_VERIFY` and `PENDING` do **not** occur:
| Status | Meaning | Terminal | Your next step |
| - | - | :-: | - |
| `INIT` | Ticket created, waiting for the user to submit on H5; review also stays at `INIT` | No | Send the H5 link to the user; call `guidance-link` again if the link expires |
| `PASSED` | Review passed, renewal complete | Yes | Wait for the `KYC` webhook to report `kycRenewalRequired=false` |
| `REJECTED` | Review rejected | Yes | `kycRenewalRequired` is still `true`; re-apply with a new `kycTicketRef` |
## Error codes
### apply-kyc-h5 (`kycApplyMode=H5-RENEWAL`)
In addition to the common error codes listed on the [H5 KYC guide page](./h5-kyc-guidance), renewal mode can also return:
| Error code | Meaning | Trigger | Retryable |
| - | - | - | :-: |
| `DAPI_KYC_RENEWAL_NOT_REQUIRED` | customer does not need KYC renewal | This user does not currently need to renew, or their record is incomplete | β Confirm `kycRenewalRequired=true` first |
| `DAPI_TICKET_APPLY_LIMIT_EXCEEDED` | Ticket application exceeds the limit | Too many applications for the same user; the intake rate limit was hit | β
Back off and retry |
### guidance-link (`type=8`)
| Error code | Meaning | Trigger |
| - | - | - |
| `DAPI_SYS_ILLEGAL_PARAM` | System illegal param | `kycTicketId` or `errorRedirectUrl` is empty |
| `DAPI_KYC_TICKET_NOT_FOUND` | kyc ticket not found | Ticket does not exist / belongs to someone else / is not a renewal ticket |
| `DAPI_KYC_TICKET_IS_IN_PROCESSING` | KYC ticket is in processing | The ticket has already been submitted and is still being processed; the operation cannot be repeated |
| `DAPI_KYC_TICKET_ALREADY_PASSED` | KYC ticket already passed | The ticket already passed |
## Prerequisites
* You hold the Enterprise ApiKey / SecretKey β see [First steps](../../getting-started/first-steps).
* You have confirmed `kycRenewalRequired=true` for this user β see [Query a user's KYC information](./kyc-info).
* You have configured a webhook endpoint that can receive `KYC` and `KYC_TICKET` events β see [Webhook configuration](../webhooks/configuration).
## Next steps
* Check whether a user currently needs to renew β [Query a user's KYC information](./kyc-info)
* Reuse KYC the user already completed under the DeCard-Managed model β [KYC information migration](./kyc-migration)
* Webhook events and fields β [Events and data structures](../webhooks/events-and-schema)
# Query KYC
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/kyc/query-kyc
How-to task page: use the KYC ticket ID (or the idempotency key you supplied when applying) to retrieve the latest status and outcome of a KYC verification. Organized as task steps plus a minimal request/response and a who-does-what note; the status machine table and the rejection reasons follow the source, the common response envelope is explained as in the source, and the page ends with a next-steps CTA.
## π Guide
Whether you are an exchange, a wallet or a platform, you can check the progress of a KYC application at any time after submitting it β its current status, whether the user still needs to be guided through face verification, and the exact reason if it was rejected β so that you can track each user's card eligibility in your own system in real time. As a licensed card issuer, DCS has KYC outcomes reviewed jointly by its compliance team and external vendors.
You initiate the query; DCS returns the current snapshot of that KYC ticket.
> This page queries the progress of **one particular verification** (a ticket). If you want the current KYC summary for **a user** β including whether their data has expired and needs redoing β that is a different endpoint: see [Query a user's KYC information](./kyc-info). Both are called "querying KYC" and are easily confused.
### Prerequisites
* You hold the Enterprise `ApiKey` / `SecretKey`. If you have not received them yet, see [First steps](../../getting-started/first-steps).
* You have already submitted a KYC application. If not, complete [Apply for KYC](./apply-kyc) first.
### Steps
1. Have the ticket identifier ready: either the `kycTicketId` returned when you applied for KYC, or the idempotency key `kycTicketRef` you supplied at that time (your side).
2. Build the request headers as described in [Authentication](../../integration-resources/authentication) and call the query endpoint (you β DCS).
3. Read the current status from `data.status` in the response and act on it as follows (your side):
* `NEED_VERIFY`: guide the user through face verification, see [H5 KYC guide page](./h5-kyc-guidance);
* `PASSED`: you can move on to card issuance;
* `REJECTED`: read `errorCode` / `errorMessage` to determine the reason and, where appropriate, guide the user to resubmit.
### Query KYC details
**`GET /open-api/kyc-ticket/v1/detail`**
#### Query parameters
| Parameter | In | Type | Required | Description |
| - | - | - | - | - |
| `kycTicketId` | query | string | No | KYC ticket ID (returned by DCS when you applied for KYC) |
| `kycTicketRef` | query | string | No | KYC ticket idempotency field (chosen by you when applying) |
| `salt` | query | string | No | In API mode, a verification salt passed straight through to the risk center; you agree its value with the risk side, and DCS itself neither encrypts nor decrypts it. It can be ignored in H5 mode. Maximum length 64; if omitted, an empty string is passed through |
> Supply at least one of `kycTicketId` and `kycTicketRef` to locate the ticket; `kycTicketId` is preferred.
#### Request example
```
GET /open-api/kyc-ticket/v1/detail?kycTicketId=200000000456
Host:
Content-Type: application/json
X-DAPI-API-KEY:
X-DAPI-TIMESTAMP:
X-DAPI-NONCE: 12345
X-DAPI-SIGN:
```
> For how to build the full set of authentication headers (`X-DAPI-API-KEY` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE` / `X-DAPI-SIGN`, together with `Content-Type: application/json`), see [Authentication](../../integration-resources/authentication).
#### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": {
"kycTicketId": "200000000456",
"kycTicketRef": "your-kyc-ref-001",
"customerId": "100000000123",
"status": "PASSED",
"createTime": "2025-01-01T12:00:00+08:00",
"modifyTime": "2025-01-02T09:30:00+08:00",
"digest": "****1234",
"kycRenewalRequired": false,
"kycApplyMode": "H5"
}
}
```
When the ticket is rejected, `data` additionally carries `errorCode` / `errorMessage`:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": {
"kycTicketId": "200000000456",
"status": "REJECTED",
"errorCode": "INVALID_POA",
"errorMessage": "Invalid proof of address"
}
}
```
> The `errorCode` above is illustrative; for the full set of values and their meanings see [KYC reject codes](./kyc-reject-codes).
#### `data` fields
| Field | Type | Description |
| - | - | - |
| `kycTicketId` | string | KYC ticket ID |
| `kycTicketRef` | string | KYC ticket idempotency field |
| `customerId` | string | User ID |
| `status` | string | KYC ticket status; see the status machine table below for the values |
| `createTime` | string | Creation time, format `yyyy-MM-dd'T'HH:mm:ss+08:00` |
| `modifyTime` | string | Last update time, format `yyyy-MM-dd'T'HH:mm:ss+08:00` |
| `digest` | string | Encrypted (masked) document number |
| `kycRenewalRequired` | boolean | Whether a KYC renewal is required |
| `kycApplyMode` | string | Application mode: `API` (submitted via API) / `H5` (KYC applied for on the H5 page) / `H5-RENEWAL` ([updating KYC information](./kyc-renewal)) / `H5-MIGRATION` ([KYC information migration](./kyc-migration)) |
| `errorCode` | string | Rejection reason code, **returned only when the status is `REJECTED`** |
| `errorMessage` | string | Rejection reason description, **returned only when the status is `REJECTED`** |
#### KYC ticket status machine
| Status | Meaning | Terminal | Your next step |
| - | - | :-: | - |
| `INIT` | KYC information submitted | No | Wait for the system to process it |
| `NEED_VERIFY` | Waiting for face verification | No | Guide the user through face verification, see [H5 KYC guide page](./h5-kyc-guidance) |
| `PENDING` | Under review | No | Wait for the review to finish |
| `PASSED` | Approved | Yes | You can proceed to the next step, such as [issuing a card](../cards/card-issuing) |
| `REJECTED` | Rejected | Yes | Read `errorCode` / `errorMessage` and, where appropriate, submit a new KYC application |
> **About rejection reasons**: when the status is `REJECTED`, the response `data` carries `errorCode` and `errorMessage`; the same information is also pushed through the [`KYC_TICKET` webhook](../webhooks/events-and-schema). For the full classification of the codes (bucket / retryable / what the user should do / whether a resubmission is possible) see [KYC reject codes](./kyc-reject-codes).
### Common response envelope
Every `/open-api/` endpoint returns the same envelope, with this endpoint's business data in `data`:
| Field | Type | Description |
| - | - | - |
| `code` | string | System-level return code; `SYS_SUCCESS` on success |
| `message` | string \| null | Usually `null` on success (some endpoints return a `success` string); rely on `code` to judge the outcome. Carries the corresponding error message on failure |
| `messageDetail` | object \| null | Information to display to the end user (`title`/`message`/`type`/`icon`/`action`/`linkTitle`/`linkUrl`); usually `null` on success |
| `data` | object | Business data, see the field table above |
> **Please note**: this endpoint carries two layers of meaning, so evaluate them separately. Use the system-level `code` to tell whether the request was accepted, and `data.status` to tell the KYC business outcome. `code=SYS_SUCCESS` only means "the request was accepted and a ticket snapshot was returned"; it does **not** mean KYC has passed (for example, `code` is still `SYS_SUCCESS` when `status=REJECTED`). On failure, `messageDetail` is what you show the end user.
For how the codes are classified, whether they are retryable and how to handle each of them, see [KYC reject codes](./kyc-reject-codes).
## Next steps
Once the KYC status is `PASSED` you can [apply for a virtual card / issue a card](../cards/card-issuing) for the user; if the status is `NEED_VERIFY`, first use the [H5 KYC guide page](./h5-kyc-guidance) to walk the user through verification.
# Authorization report
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/reports/authorization-report
Covers the daily authorization report file itself: where it sits in the funds lifecycle, the authType / transactionType / approveFlag enumerations, how to download it, the >-delimited field layout, and how to read a sample line. For the shared mechanism used to retrieve any report, see Reconciliation overview; for a single reference covering the fields of both files, see Report field dictionary.
## Authorization report
Whether you want to review every authorization decision, check which transactions were approved or declined, or satisfy a regulatory requirement for end-to-end traceability of the authorization path, the daily authorization report gives you a complete, loadable snapshot of that day's authorization decisions. As a licensed card issuer, DCS records every authorization decision, approvals and declines alike, as standardized fields and delivers them to you in a full daily settlement file.
What the authorization report captures is **the decision and the hold placed during the authorization stage**, that is, whether the transaction was let through and how much was frozen, not where the money finally landed. It is delivered alongside the transaction report: the authorization report answers "was it approved", the transaction report answers "how much was actually charged".
***
## Where it sits in the funds lifecycle
Under the Partner-Managed model, the authorization decision belongs to you and is made by you. DCS forwards the channel's authorization request to you for an approve-or-decline verdict and records the outcome of each decision as an authorization record. Authorization, outstanding and transaction are chained together by IDs:
* Every record in the authorization report is uniquely identified by `authId` and links to its outstanding record through `outsId`.
* `originalAuthId` is used wherever a record must be traced back to an original authorization, such as multiple refunds or incremental authorizations.
* For the full set of authorization and clearing combinations (plain clearing, incremental, reversal, over- and under-clearing, forced clearing, cash withdrawal and so on), see [Authorization and clearing scenarios](../transactions/auth-and-settlement).
***
## Authorization type (authType)
`authType` tells you how an authorization record came into being, so you can separate real-time authorizations from records the system created or released on its own.
| Value | Name | Description |
| - | - | - |
| `NORMAL` | Normal authorization | The real-time authorization record produced by an authorization callback from the channel; covers purchases, incremental authorizations, reversals, refunds, cash withdrawals and balance inquiries |
| `FORCE_AUTH` | Forced authorization | An authorization record the system creates by itself, seen when clearing arrives with no matching authorization (an offline transaction, for example) or when the cleared amount differs from the held amount and the difference must be booked |
| `EXPIRED_RELEASE` | Expiry release | The authorization was never settled in time, and the channel uses the settlement file to instruct the release of the funds still on hold |
| `STATUS_DIFF_RELEASE` | Status mismatch release | The channel declined the authorization on timeout while the DCS side had approved it; the mismatch triggers a reconciliation correction |
The authoritative definitions also appear in [Authorization and clearing scenarios](../transactions/auth-and-settlement).
***
## Transaction type (transactionType)
`transactionType` tells you what kind of transaction the cardholder initiated when the authorization was requested, so you can recognize the scenario at a glance during reconciliation.
| Value | Name | Description |
| - | - | - |
| `R` | Card purchase | All card purchase transactions |
| `C` | ATM withdrawal | ATM withdrawals and other cash advance transactions |
| `Q` | Inquiry | Inquiry transactions, with no movement of funds |
| `P` | Return or refund | All transfers or refunds credited back to the card |
***
## Authorization outcome and decline reason (approveFlag / rejectReason)
| Field | Value | Description |
| - | - | - |
| `approveFlag` | `A` | Approved; the authorization was let through |
| `approveFlag` | `D` | Declined; the authorization was not let through |
| `rejectReason` | Present only when `D` | The specific reason for the decline, given as a `DAPI_*` error code; common values are listed below |
Common `rejectReason` values:
| Value | When it occurs |
| - | - |
| `DAPI_AUTH_ENTERPRISE_REJECT` | You explicitly declined in the authorization callback |
| `DAPI_AUTH_ENTERPRISE_TIMEOUT_REJECT` | You did not answer within the response window, so DCS declined as a fallback |
| `DAPI_AUTH_INTERNAL_REJECT` | Declined inside DCS (signature verification failure, forwarding failure, a response value outside the protocol, and similar) |
| `DAPI_AUTH_INSUFFICIENT_FUNDS_REJECT` | Insufficient funds; the hold or release check did not pass |
| `DAPI_CARD_STATUS_NOT_ACTIVATED` | The card is not activated |
| `DAPI_CARD_CONSUME_FROZEN` | A KYC factor has expired and the enterprise has spending restrictions enabled |
| `DAPI_CARD_VELOCITY_LIMIT_CHECK_REJECT` | The card velocity limit check declined the transaction |
| `DAPI_SYSTEM_ERROR` | Fallback for an unexpected system error |
> Because the authorization decision sits on your side, a `rejectReason` may come from your own business logic or from a DCS or channel-side check or reconciliation correction, a timeout being the obvious example. For how each code is classified and handled, see [Authorization declines and error codes](../transactions/decline-codes).
***
## How to retrieve the authorization report
Under the Partner-Managed model there is **no real-time endpoint for querying an individual authorization**; authorization records are delivered exclusively through the **full daily settlement file**. Each day DCS generates one file per partner (Enterprise) containing all of that day's authorizations.
**Endpoint (to obtain the download link)**
```
GET /open-api/enterprise/v1/settlement-file-url
```
| Parameter | Type | Required | Description |
| - | - | - | - |
| `fileType` | String | Yes | File type; pass `authorisation` for the authorization file (`transaction` for the transaction file), maximum 20 |
| `fileDate` | String | Yes | File date, format `yyyyMMdd`, for example `20251106`, maximum 8 |
Note that the `fileType` value uses the British spelling `authorisation`, not `authorization`. Pass it exactly as shown.
**Response**: inside the standard response envelope `{ code, message, messageDetail, data }`, `data` is the file's **download link** as a string (a temporary AWS S3 link with a short validity, so download it promptly).
```json theme={null}
{
"code": "...",
"message": "...",
"messageDetail": { },
"data": "https://....s3.amazonaws.com/....authorisation.....?X-Amz-..."
}
```
> **About the response envelope**: judge success or failure from `code`. `message` is a short prompt, and `messageDetail` is a structured message object meant for end-user display (carrying `title`, `type`, `action`, `linkUrl` and so on). For the envelope and the rules for interpreting it, see [API Reference overview](../../api-reference/index). For error codes, see [Authorization declines and error codes](../transactions/decline-codes).
> **Who does what**
>
> * **DCS**: generates the full authorization file for the day and stores it in S3, then issues a temporary download link on request.
> * **Partner**: calls this endpoint for a link, downloads the file while the link is valid, then loads it and checks the authorization decisions against your own records.
by default the previous day's file is generated on T+1 and can be downloaded once the job finishes. The temporary download link is valid for a fixed 120 seconds; once it expires, call the endpoint again for a new one.
For the shared retrieval mechanism behind download links and the daily reconciliation cycle, see [Reconciliation overview](./overview).
***
## Fields in the authorization report file
One authorization record per line, fields delimited by `>`, in the following order:
| Field | Description | Notes |
| - | - | - |
| `authId` | Unique authorization ID | Used to link to transaction flow, outstanding records and other downstream records |
| `direction` | Transaction direction | `OUTGOING` (funds out) / `INCOMING` (funds in) |
| `authType` | Authorization type | Matches the `authType` enumeration above; see [Authorization and clearing scenarios](../transactions/auth-and-settlement) |
| `outsId` | Outstanding ID | Links to the corresponding outstanding record |
| `originalAuthId` | Original related authorization ID | Used to trace back in linked scenarios such as multiple refunds and incremental authorizations |
| `enterpriseId` | Enterprise ID | Identifies the partner that initiated the authorization |
| `customerId` | User ID | Identifies the cardholder the authorization belongs to |
| `cardId` | Card ID | The card the transaction took place on |
| `pan` | Card number | Only the first six and last four digits are shown, to keep the number safe |
| `approveFlag` | Authorization outcome | `A` approved / `D` declined |
| `rejectReason` | Decline reason | Present only when `approveFlag` is `D` |
| `approveDate` | Processing time | Format `yyyy-MM-dd'T'HH:mm:ss+08:00` (UTC+8) |
| `currency` | Currency | ISO 3-digit currency code (settlement currency) |
| `amount` | Amount | The amount the authorization covers, in the settlement currency |
| `acquirerCurrency` | Requested currency | May differ from the actual settlement currency |
| `acquirerAmount` | Requested amount | The amount in the requested currency |
| `cardAcceptorIdentification` | Merchant ID | The merchant identifier for the transaction |
| `cardAcceptorNameLocation` | Merchant details | Merchant name and address |
| `transactionType` | Transaction type | Matches the `transactionType` enumeration above; see [Authorization and clearing scenarios](../transactions/auth-and-settlement) |
| `merchantType` | Merchant type | The MCC, four digits |
| `createTime` | Creation time | When the authorization record was created; same format as `approveDate` |
| `modifyTime` | Update time | When the authorization record was last modified; same format as above |
| `merchantCountryCode` | Merchant country code | Three-digit numeric country code |
**Sample line**
```text theme={null}
1109086179954790401>OUTGOING>NORMAL>>>1095041241881513984>1108391061086846977>1108449591919689729>4382140000003562>D>DAPI_AUTH_ENTERPRISE_TIMEOUT_REJECT>2025-03-19T11:52:33+08:00>702>0.100000000000000000>>0E-18>>>R>5399>2025-03-19T03:52:31+08:00>2025-03-19T03:52:31+08:00>840
```
> The sample above shows a declined authorization: `approveFlag=D` with `rejectReason=DAPI_AUTH_ENTERPRISE_TIMEOUT_REJECT` (the partner did not answer in time, so the authorization was declined), transaction type `R` (card purchase), settlement currency `702` (SGD), merchant country `840` (United States).
***
## Notes
### Field extensions
New fields may be appended to the authorization file in the future. Field extensions follow these compatibility commitments:
1. **Existing column indexes stay stable**: the columns already defined in this document never change their index or meaning; no column is removed and no new column is inserted in the middle;
2. **New fields are appended at the end of the line only**: every addition is announced in advance through a change notice. Partners can read the new indexes as needed; until then, simply ignore the extra trailing columns β existing parsing is unaffected;
3. **Every column keeps its placeholder**: every column (including empty ones) keeps its placeholder, so the number of separators per line always matches the column count.
We recommend parsing fields by column index and tolerating unknown trailing columns.
***
## Next steps
* Check what was finally charged: [Transaction report](./transaction-report)
* Understand the full path from hold to clearing: [Authorization and clearing scenarios](../transactions/auth-and-settlement)
* Look up the authoritative definitions of the authorization enumerations: [Authorization and clearing scenarios](../transactions/auth-and-settlement)
* Understand the shared retrieval mechanism for daily settlement files: [Reconciliation overview](./overview)
# Report field reference
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/reports/field-dictionary
This page collects the field-by-field meaning of both daily settlement files (the authorization report and the transaction report) in one place, as the reference you load your reconciliation against. For how to retrieve the reports, see Reconciliation overview; for how each file is generated and what a sample line looks like, see Authorization report and Transaction report.
## π Guide
Whether you run end-of-day reconciliation, verify user limits, or load every movement of funds on a card into a data warehouse, the settlement files DCS generates each day give you an authoritative source. As a licensed issuer with its own BINs, DCS generates two **full** files per partner every day, the authorization report and the transaction report, and stores them in AWS S3 for you to fetch a download link by date.
This page explains what every column in those two files means. Once you know the fields, you can turn any single line back into a plain statement of who spent how much at which merchant, whether it was approved or declined, and whether the money has actually moved.
## When each report is available and what it contains
| Item | Description |
| - | - |
| Frequency | Once a day, generated in **full** (not incrementally) |
| Timing | By default the previous day's file is generated on T+1; the exact run time is set on the scheduling platform |
| File types | `authorisation` (authorization report) and `transaction` (transaction report), one of each per Enterprise |
| How to retrieve | Call `GET /open-api/enterprise/v1/settlement-file-url` with `fileType` + `fileDate` (`yyyyMMdd`); it returns a download link valid for a fixed 120 seconds, and you can request a new one after it expires |
> **The two files do different jobs**: the authorization report holds every authorization decision, including the declined ones (`approveFlag = D`), and is what you use to verify authorization outcomes and the release of holds; the transaction report is the authoritative record of money actually debited or credited. The two are joined through `outsId` (the outstanding ID) and `authIds`. See [Transaction lifecycle](../../basic-concepts/transaction-lifecycle).
## File format
Both files are **plain text with `>`-delimited fields**, one record per line (not standard CSV). Column order is fixed and matches the tables below from top to bottom. An empty field appears as two consecutive delimiters.
* **Amounts**: high-precision decimals (such as `0.100000000000000000`), not integers in minor units.
* **Timestamps**: format `yyyy-MM-dd'T'HH:mm:ss+08:00`, time zone **UTC+8**.
* **Card number (`pan`)**: only the **first six and last four digits**, masked in the middle so it is safe to store.
## Authorization report fields (fileType = οΏ½13οΏ½)
> Column order matches the file.
| Column | Field | Description |
| - | - | - |
| 1 | `authId` | Unique authorization ID. Used to link to transaction flow, outstanding records and other downstream records |
| 2 | `direction` | Transaction direction: `OUTGOING` (funds out, a purchase hold) / `INCOMING` (funds in, a refund release) |
| 3 | `authType` | Authorization type: `NORMAL` / `FORCE_AUTH` / `EXPIRED_RELEASE` / `STATUS_DIFF_RELEASE`; see "Authorization type" below |
| 4 | `outsId` | Outstanding ID. Links to the corresponding outstanding record and is the bridge between authorization and clearing |
| 5 | `originalAuthId` | Original related authorization ID. Used to trace back in linked scenarios such as multiple refunds and incremental authorizations |
| 6 | `enterpriseId` | Enterprise ID. Identifies the partner that initiated the authorization |
| 7 | `customerId` | User ID. Identifies the cardholder the authorization belongs to |
| 8 | `cardId` | Card ID. The card the transaction took place on |
| 9 | `pan` | Card number, first six and last four digits only |
| 10 | `approveFlag` | Authorization outcome: `A` = approved / `D` = declined |
| 11 | `rejectReason` | Decline reason. Present only when `approveFlag = D` |
| 12 | `approveDate` | Processing time, `yyyy-MM-dd'T'HH:mm:ss+08:00` |
| 13 | `currency` | Settlement currency, ISO 3-digit currency code (numeric, such as `702`) |
| 14 | `amount` | Settlement amount (high-precision decimal) |
| 15 | `acquirerCurrency` | Requested currency. May differ from the settlement currency |
| 16 | `acquirerAmount` | Requested amount, in the requested currency |
| 17 | `cardAcceptorIdentification` | Merchant ID |
| 18 | `cardAcceptorNameLocation` | Merchant details, name and address |
| 19 | `transactionType` | Authorization transaction type: `R` = card purchase / `C` = ATM withdrawal / `Q` = inquiry / `P` = return or refund; see "Authorization transaction type" below |
| 20 | `merchantType` | Merchant type (MCC), four digits |
| 21 | `createTime` | When the authorization record was created; format as above |
| 22 | `modifyTime` | When the authorization record was last modified; format as above |
| 23 | `merchantCountryCode` | Merchant country code, three digits |
### Authorization type (`authType`)
| Value | Name | Description |
| - | - | - |
| `NORMAL` | Normal authorization | The standard record produced by a real-time authorization callback from the channel; covers every real-time scenario, including purchases, incremental authorizations, reversals, refunds, cash withdrawals and inquiries |
| `FORCE_AUTH` | Forced authorization | An authorization record the system creates by itself: clearing arrived with no matching authorization (an offline transaction, for example), or the cleared amount differs from the held amount and the difference must be booked |
| `EXPIRED_RELEASE` | Expiry release | The authorization was never settled in time, and the channel uses the settlement file to instruct the release of the funds on hold |
| `STATUS_DIFF_RELEASE` | Status mismatch release | A reconciliation correction for the case where the channel declined the authorization on timeout but the DCS side had approved it |
### Authorization transaction type (`transactionType`)
| Value | Name | Description |
| - | - | - |
| `R` | Card purchase | All card purchase transactions |
| `C` | ATM withdrawal | ATM withdrawals and other cash advance transactions |
| `Q` | Inquiry | Inquiry transactions, with no movement of funds |
| `P` | Return or refund | Transfers or refunds credited back to the card |
## Transaction report fields (fileType = οΏ½62οΏ½)
> Column order matches the file.
| Column | Field | Description |
| - | - | - |
| 1 | `transactionId` | Unique transaction ID. The unique number for each movement of funds |
| 2 | `direction` | Transaction direction: `OUTGOING` (funds out) / `INCOMING` (funds in) |
| 3 | `outsId` | Outstanding ID. Links to the corresponding outstanding record |
| 4 | `authIds` | Related authorization IDs. Multiple authorizations are separated by commas, for example `1111,2222` |
| 5 | `enterpriseId` | Enterprise ID. Identifies the partner the fund movement belongs to |
| 6 | `customerId` | User ID. Identifies the cardholder the transaction belongs to |
| 7 | `cardId` | Card ID |
| 8 | `pan` | Card number, first six and last four digits only |
| 9 | `category` | Transaction category: `RETAIL` / `CASH` / `CASH_FEES` / `PAYMENT` and others; see "Transaction category" below |
| 10 | `currency` | Settlement currency, ISO 3-digit currency code |
| 11 | `amount` | Settlement amount (high-precision decimal) |
| 12 | `acquirerCurrency` | Requested currency |
| 13 | `acquirerAmount` | Requested amount |
| 14 | `cardAcceptorIdentification` | Merchant ID |
| 15 | `cardAcceptorNameLocation` | Merchant details, name and address |
| 16 | `multiClearInd` | Clearing indicator: `O` = ordinary single clearing / `P` = multi-part clearing, not the final part / `F` = multi-part clearing complete |
| 17 | `merchantType` | Merchant type (MCC), four digits |
| 18 | `createTime` | When the transaction record was created; format as above |
| 19 | `modifyTime` | When the transaction record was last modified; format as above |
| 20 | `merchantCountryCode` | Merchant country code, three digits |
| 21 | `originalTransactionId` | Original transaction ID. For a refund-type record, the `transactionId` of the original transaction being refunded; empty for non-refund records |
### Transaction category (`category`)
| Value | Name | Description |
| - | - | - |
| `RETAIL` | Ordinary purchase | A standard card purchase transaction |
| `RETAIL_FEES` | Purchase fee | Includes DCC fees, credit adjustments, annual card fees, replacement and reissue fees and similar |
| `CASH` | Ordinary cash withdrawal | ATM withdrawals and other cash transactions |
| `CASH_FEES` | Cash withdrawal fee | Includes cash withdrawal handling fees and similar |
| `PAYMENT` | Return or refund | Returns, refunds and other incoming transactions |
| `CHARGEBACK` | Dispute chargeback | A chargeback arising from a dispute raised by the cardholder |
## Points to watch when loading the files
* **Treat the transaction report as the authority on money**: what a user was actually charged or refunded comes from the transaction report (`transaction`). The authorization report (`authorisation`) includes declined records and serves to verify authorization outcomes and the release of holds; never use it directly as evidence of a charge.
* **Joining across files**: the same piece of business lines up across the two files through `outsId`, and a single transaction line can be traced back through `authIds` to one or more authorizations (for multi-part clearing, look at `multiClearInd = P/F`).
* **Parse amounts as high-precision decimals**: parse as high-precision decimals rather than integers in minor units before storing, so you do not lose precision.
* **Currencies are numeric codes**: `currency` and its siblings are ISO 4217 numeric codes (`840` = USD, `702` = SGD), so map by numeric code on load.
## Next steps
* Not sure which clearing stage a record belongs to? Go back to [Transaction lifecycle](../../basic-concepts/transaction-lifecycle) and place the record on the authorization-to-clearing timeline.
* Want to know how to fetch a file download link by date? See [Reconciliation overview](./overview).
# Reconciliation overview
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/reports/overview
Presigned URL, valid for 120 seconds
## Reconciliation overview
Whether you need regulatory reporting, a data warehouse archive, or line-by-line reconciliation against your own ledger, the full daily files that DCS generates let you retrieve the previous day's authorization and transaction detail in a single pass. As a licensed card issuer, DCS generates standardized settlement files on a schedule according to your program configuration and, once your request is authenticated, hands back a secure download link.
A report is a file generated from platform data to convey a summary view of one subject. For financial services and account products, reports matter for compliance filing, risk decisions and operational efficiency, because what they give you is a complete snapshot as of a point in time. If you need real-time changes, use webhook notifications rather than report files.
***
## The two daily reports
Every day, DCS generates two **full** files per partner (Enterprise), one covering authorizations and one covering transaction flow:
| Report | fileType | What it covers | Details |
| - | - | - | - |
| Daily authorization report | `authorisation` | Every authorization decision made that day, including approvals and declines, decline reasons and merchant details | [Authorization report fields](./authorization-report) |
| Daily transaction report | `transaction` | Every movement of funds that day, including clearing, refunds and purchases | [Transaction report fields](./transaction-report) |
> **Authorization vs. transaction**: an authorization is the decision and the hold placed on funds at the moment the card is used; a transaction is the actual movement of money. The two are linked through the `authId` / `authIds` fields, and a single authorization may map to several clearing transactions. To join the two files for reconciliation, see the [field dictionary](./field-dictionary).
### Who does what
| Step | Owner |
| - | - |
| Generate the full files on schedule and store them in AWS S3 | DCS |
| Call the endpoint for a download link, then download and parse the file | Partner |
| Reconcile line by line against your own ledger | Partner |
> **When files are generated**: by default the two files for the previous day are generated on T+1; the exact daily run time is set on the scheduling platform. The link becomes available once the job reaches status `DONE`.
***
## Retrieving a report file
Files are held in a DCS AWS S3 bucket and are not publicly exposed. You first call the settlement file endpoint to exchange your request for a **short-lived download link**, then use that link to fetch the file.
**`GET /open-api/enterprise/v1/settlement-file-url`**
### Request parameters
| name | in | type | required | description |
| - | - | - | - | - |
| `fileType` | query | string | β | File type, maximum length 20. Values: `authorisation` (authorization file) / `transaction` (transaction file) |
| `fileDate` | query | string | β | File date, maximum length 8, format `yyyyMMdd`, for example `20260126` |
the `fileType` value for the authorization file is `authorisation` (British spelling, with an `s`), not `authorization`. Pass it exactly as documented.
### Request example
```http theme={null}
GET /open-api/enterprise/v1/settlement-file-url?fileType=authorisation&fileDate=20260126
Host:
Content-Type: application/json
X-DAPI-API-KEY:
X-DAPI-TIMESTAMP:
X-DAPI-NONCE:
X-DAPI-SIGN:
```
> For the full meaning of the authentication headers, the signature calculation and the IP whitelist requirements, see [Authentication](../../integration-resources/authentication).
### Response example
The response follows the standard response envelope. The `data` field is the temporary S3 download link for the file, returned as a string.
```json theme={null}
{
"code": "...",
"message": "...",
"messageDetail": {
"message": "...",
"title": "...",
"type": "...",
"icon": "...",
"action": "...",
"linkTitle": "...",
"linkUrl": "..."
},
"data": "https:///...presigned-url..."
}
```
### The standard response envelope
| Field | Type | Description |
| - | - | - |
| `code` | string | Business status code. Always judge success or failure from `code`; for the meaning of each error code see [Authorization declines and error codes](../transactions/decline-codes) |
| `message` | string | Short summary message |
| `messageDetail` | object | A structured prompt intended for end-user display (title, message, icon, action, link and so on), used to guide the user in your front end. It is not tightly bound to business success, so never use it to decide whether a call succeeded |
| `data` | string | Business payload. For this endpoint, the temporary download link for the file |
> For the exact `code` value on success and the complete error code dictionary, see [Authorization declines and error codes](../transactions/decline-codes), or ask the DCS team to confirm.
***
## Download link validity (important)
The link this endpoint returns is a **temporary S3 link (presigned URL) with a fixed validity of 120 seconds**. Download **immediately** after you receive it, and never cache the link for later use. Once it expires, simply call the endpoint again for a fresh link.
| Point to watch | Description |
| - | - |
| Validity | Fixed at 120 seconds; the link stops working after that, but the underlying file is retained and a new link can be issued |
| If it expires | Call `GET /open-api/enterprise/v1/settlement-file-url` again for a new link (the same `fileType` + `fileDate` can be requested as often as you need) |
| Large files | You always receive a single file; no `_part` or segment-numbered files are produced |
| File name | The file name is randomly generated (a UUID, such as `e53b723d-0b54-472f-89f9-587c7aedb07b.txt`) and carries no partner or date information. Archive it under a name of your own derived from the `fileType` + `fileDate` you requested |
| Integrity | No checksum, MD5 or SHA is supplied today. If you need integrity verification, agree it separately as part of the file transfer arrangement |
***
## File format
Each report is a text file of `>`-delimited records, one record per line, with fields in the same order as the data structure table on the corresponding report page.
Authorization report example (a single line):
```text theme={null}
1109086179954790401>OUTGOING>NORMAL>>>1095041241881513984>1108391061086846977>1108449591919689729>4382140000003562>D>DAPI_AUTH_ENTERPRISE_TIMEOUT_REJECT>2025-03-19T11:52:33+08:00>702>0.100000000000000000>>0E-18>>>R>5399>2025-03-19T03:52:31+08:00>2025-03-19T03:52:31+08:00>840
```
> Time fields use the format `yyyy-MM-dd'T'HH:mm:ss+08:00` (UTC+8). For the meaning of each field, see the corresponding report page and the [field dictionary](./field-dictionary).
***
## Next steps
* Parse the daily authorization file field by field β [Daily authorization report](./authorization-report)
* Parse the daily transaction file field by field β [Daily transaction report](./transaction-report)
* Join the two files for reconciliation β [Report field dictionary](./field-dictionary)
# Transaction report
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/reports/transaction-report
Covers the transaction report file itself: how it is generated, how to download it, the >-delimited field layout, and how to read a sample line. The concept of transaction flow and where it sits in the funds lifecycle is covered in Transaction flow and is not repeated here.
## Daily transaction report
Whether you reconcile balance movements day by day, need to reconstruct the full detail of every settled amount, or must satisfy a regulatory requirement that the flow of funds be traceable and auditable, the daily transaction report is the authoritative reconciliation record you can rely on directly. As a licensed card issuer, DCS records all of the day's settled fund movements in a standardized file, stores it in AWS S3, and delivers it to you through a single endpoint.
The transaction report captures the **actual debits and credits**, meaning the outcome of clearing. It does not include the intermediate states of the authorization stage such as holds and releases. If what you need is the authorization decision itself, use the [authorization report](./authorization-report) instead; put the two files together and you can reconstruct the complete path from hold to charge.
***
## How the file is generated
Each day DCS generates one **full** transaction file per partner (Enterprise).
| Item | Description |
| - | - |
| Scope | One full file per day, per Enterprise |
| Storage | AWS S3; a temporary download link is issued through the endpoint DCS provides |
| Format | Plain text, one record per line, fields delimited by `>` (see "File fields" below) |
| Timing | By default the previous day's file is generated on T+1; the exact run time is set on the scheduling platform, and the file can be downloaded once the status is `DONE` |
> **Who does what**
>
> * **DCS**: generates the full transaction file each day and stores it in S3, then issues a temporary download link on request.
> * **Partner**: calls the endpoint below for a link, downloads the file, then loads it and checks that balance movements match your own business records.
***
## How to retrieve the file
Under the Partner-Managed model, transaction records have **no real-time endpoint for querying an individual record**; they are delivered exclusively through the full daily file. Use the shared settlement file endpoint to obtain the link:
**`GET /open-api/enterprise/v1/settlement-file-url`**
| Parameter | In | Type | Required | Description |
| - | - | - | - | - |
| `fileType` | query | String | Yes | File type; pass `transaction` for the transaction file (`authorisation` for the authorization file), max 20 |
| `fileDate` | query | String | Yes | File date, format `yyyyMMdd`, for example `20251106`, max 8 |
**Response**: the standard response envelope `{ code, message, messageDetail, data }`, where `data` is the file's **download link** as a string (a temporary S3 link). The link is valid for a fixed 120 seconds and can be requested again once it expires. Judge success or failure from `code`; `messageDetail` exists only for front-end display and must not be used to decide the outcome. For the endpoint definition and response fields, see the Enterprise Account & Reconciliation group in the [API Reference](../../api-reference/index).
**Request example**
```
GET /open-api/enterprise/v1/settlement-file-url?fileType=transaction&fileDate=20251106
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": "https://.s3..amazonaws.com/...&X-Amz-Expires=120&..."
}
```
the temporary download link is valid for a fixed 120 seconds. Once it expires, call this endpoint again with the same `fileType` + `fileDate` to obtain a new link.
***
## File fields
One transaction record per line, fields delimited by `>`, in the following order:
| Field | Description | Notes |
| - | - | - |
| `transactionId` | Unique transaction ID | The unique number identifying each movement of funds |
| `direction` | Transaction direction | `OUTGOING` (funds out) / `INCOMING` (funds in) |
| `outsId` | Outstanding ID | Links to the corresponding outstanding record |
| `authIds` | Related authorization IDs | Multiple authorizations are separated by commas, for example `1111,2222`; use these to link back to `authId` in the [authorization report](./authorization-report) |
| `enterpriseId` | Enterprise ID | Identifies the partner the fund movement belongs to |
| `customerId` | User ID | Identifies the cardholder the transaction belongs to |
| `cardId` | Card ID | The card the transaction took place on |
| `pan` | Card number | Only the first six and last four digits are shown, to keep the number safe |
| `category` | Transaction category | The transaction category `code`, for example `RETAIL` for an ordinary purchase; see [Transaction flow](../transactions/transaction) |
| `currency` | Currency | ISO 3-digit currency code |
| `amount` | Amount | The amount the transaction covers, in the settlement currency |
| `acquirerCurrency` | Requested currency | May differ from the actual settlement currency |
| `acquirerAmount` | Requested amount | The amount in the requested currency |
| `cardAcceptorIdentification` | Merchant ID | The merchant identifier for the transaction |
| `cardAcceptorNameLocation` | Merchant details | Merchant name and address |
| `multiClearInd` | Clearing indicator | `O` ordinary single clearing / `P` multi-part clearing, not the final part / `F` multi-part clearing complete. To decide whether an authorization has finished clearing, wait until you have received `F`; see [Transaction flow, multi-part clearing indicator](../transactions/transaction) |
| `merchantType` | Merchant type | The MCC, four digits |
| `createTime` | Creation time | When the transaction record was created, format `yyyy-MM-dd'T'HH:mm:ss+08:00` (UTC+8) |
| `modifyTime` | Update time | When the transaction record was last modified; same format as above |
| `merchantCountryCode` | Merchant country code | Three-digit numeric country code |
| `originalTransactionId` | Original transaction ID | For a refund-type record, the `transactionId` of the original transaction being refunded; empty for non-refund records |
> **Time zone warning**: time fields inside the report are **UTC+8** (`+08:00`), consistent with the business time fields across the partner-facing APIs (the velocity-limit query endpoint, whose dates are UTC, is the one exception).
### Sample lines
```text theme={null}
174047198631807650000309>OUTGOING>1109862324019138561>1109862318201638913>1095041241881513984>1108391061086846977>1108449591919689729>4382140000003562>CASH>702>1.000000000000000000>>0E-18>001584054110002>>null>5399>2025-03-21T07:20:45+08:00>2025-03-21T07:20:45+08:00>840>
174047198631807650000310>OUTGOING>1109864282771689473>1109864282775883776>1095041241881513984>1108391061086846977>1108449591919689729>4382140000003562>CASH_FEES>702>1.000000000000000000>>0E-18>001584054110002>>null>5399>2025-03-21T07:24:25+08:00>2025-03-21T07:24:25+08:00>840>
174047198631807650000311>INCOMING>1109865120004237313>1109865120008431616>1095041241881513984>1108391061086846977>1108449591919689729>4382140000003562>PAYMENT>702>1.000000000000000000>>0E-18>001584054110002>>null>5399>2025-03-22T09:12:07+08:00>2025-03-22T09:12:07+08:00>840>174047198631807650000301
```
> The first two lines show a cash withdrawal: the first, `category=CASH`, is the withdrawal principal, and the second, `category=CASH_FEES`, is the withdrawal fee. Both records share the same card number and the same merchant. The third line shows a refund: an incoming record with `category=PAYMENT` whose trailing `originalTransactionId` points to the `transactionId` of the original purchase being refunded; that column is empty for non-refund records, which is why the first two lines end in `>`. An empty field (consecutive `>>`) means that column has no value.
in the settlement file, `category` is written in full as `CASH` / `CASH_FEES` (withdrawal principal / withdrawal fee). Treat those spellings as authoritative.
***
## Notes
### Field extensions
New fields may be appended to the transaction file in the future. Field extensions follow these compatibility commitments:
1. **Existing column indexes stay stable**: the columns already defined in this document never change their index or meaning; no column is removed and no new column is inserted in the middle;
2. **New fields are appended at the end of the line only**: every addition is announced in advance through a change notice. Partners can read the new indexes as needed; until then, simply ignore the extra trailing columns β existing parsing is unaffected;
3. **Every column keeps its placeholder**: every column (including empty ones) keeps its placeholder, so the number of separators per line always matches the column count.
We recommend parsing fields by column index and tolerating unknown trailing columns.
***
## Next steps
* Understand the concept of transaction flow and where it sits in the funds lifecycle: [Transaction flow](../transactions/transaction)
* Reconcile the daily file on the authorization side: [Authorization report](./authorization-report)
* Look up the authoritative definitions of the transaction category (`category`) enumeration: [Transaction flow](../transactions/transaction)
* See the full definition of the download link endpoint and its response envelope in the Enterprise Account & Reconciliation group of the [API Reference](../../api-reference/index)
# Sandbox and testing overview
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/sandbox/overview
An overview of the sandbox: which simulation capabilities the backend supports, the sandbox and production base URLs, what you need in place before you simulate, and a minimal request and response for the fund-auth simulation endpoint. For scenario-by-scenario cases, see Simulating transactions.
## π Guide
Whether you want to check that an authorization callback parses correctly or to run the whole chain from card use through authorization forwarding to posting before you go live, you can do it all in the sandbox, with no real card network, no real money, and no real cardholder ever involved. As a licensed card issuer, DCS gives partners a sandbox built to the same shape as production, so you can rehearse your authorization decision logic from the earliest days of integration.
### Does the backend support simulation?
**Yes, but only within limits.** As of the current release, the Partner-Managed API provides **one simulation endpoint**:
| Capability | Endpoint | Status |
| - | - | - |
| Simulate an authorization request (purchase / return / purchase reversal) | `POST /open-api/simulation/v1/fund-auth` | β
Available (sandbox only) |
| Simulate clearing (capture / settlement) | β | Planned |
| Simulate partial clearing / multi-part clearing | β | Planned |
| Simulate a 3DS challenge | β | Planned |
| Simulate a deposit top-up or limit change | β | Planned |
> Sandbox simulation today focuses on the authorization stage; dedicated endpoints for clearing, 3DS, deposits and the like are still planned. If your testing needs any of those scenarios, talk to the DCS team about a workable alternative.
### What the sandbox does for you
The `fund-auth` simulation endpoint triggers a **genuine authorization forwarding flow**: once DCS receives your simulated request, it pushes the authorization notification to the `authUrl` you configured over exactly the same path as production, and you return the approve-or-decline decision. That makes it a good fit for:
* checking that **RSA two-way signing plus encryption and decryption** on the authorization notification works end to end;
* checking your own **authorization decision logic** (limit checks, risk rules, timeout fallbacks);
* checking that your system transitions state correctly across all three scenarios: `EXPEND` (purchase), `REFUND` (return) and `REVERSAL` (purchase reversal).
### Base URLs
| Environment | Base URL |
| - | - |
| Sandbox | `https://api.thedecard-sandbox.com` |
| Production | `https://api.thedecard.com` |
the simulation endpoint is available in the **sandbox only**. Production blocks the path outright and returns `OPERATION_NOT_SUPPORT`, so never call a `simulation` endpoint against production.
### What you need before you simulate
| Prerequisite | Description | Owner |
| - | - | - |
| Sandbox API credentials | `api_key` + `secret_key`, used for HMAC-SHA256 authentication | Provided by DCS |
| RSA key pair | You generate and upload `external_public_key`; DCS gives you the DCS-side public key | Partner + DCS |
| `authUrl` | The callback URL that receives authorization notifications; it must be reachable from the sandbox | Partner |
| A usable card | Complete "create user, KYC, apply for card" in the sandbox first and keep the `cardId` | Partner |
| Enterprise deposit limit | The sandbox side needs available limit, otherwise authorizations are declined | Configured by DCS |
> For the full explanation of the authentication headers, RSA key generation with `openssl`, and the IP whitelists, see [Authentication](../../integration-resources/authentication).
### Minimal request
```bash theme={null}
curl -X POST https://api.thedecard-sandbox.com/open-api/simulation/v1/fund-auth \
-H "Content-Type: application/json" \
-H "X-DAPI-API-KEY: " \
-H "X-DAPI-TIMESTAMP: " \
-H "X-DAPI-NONCE: <10000-99999>" \
-H "X-DAPI-SIGN: " \
-d '{
"cardId": "",
"authType": "EXPEND",
"amount": 12.50,
"currency": "USD"
}'
```
**Request fields** (source: `POST /open-api/simulation/v1/fund-auth`):
| Field | Type | Required | Description |
| - | - | - | - |
| `cardId` | string | Yes | Card ID |
| `authType` | string | Yes | Authorization type: `EXPEND` (purchase) / `REFUND` (return) / `REVERSAL` (purchase reversal) |
| `amount` | number | Yes | Amount as a decimal, in major currency units |
| `currency` | string | Yes | Transaction currency, 3-letter ISO currency code (such as `USD` / `SGD` / `EUR` / `JPY`) |
### Minimal response
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"approved": true,
"errorCode": null
}
}
```
**Response fields**:
| Field | Type | Description |
| - | - | - |
| `data.approved` | boolean | Whether the authorization was approved, that is, the decision you returned at your `authUrl` |
| `data.errorCode` | string | The error code when declined |
> **The standard response envelope**: every endpoint returns `{ code, message, messageDetail, data }`. `code` reports the outcome of the system call (`SYS_SUCCESS`, for example), while the business outcome must be read from inside `data`, here `approved`. A `code=SYS_SUCCESS` means only that the request was accepted correctly, not that the authorization was approved, so never judge the business outcome from `code` alone. `messageDetail` is a prompt object aimed at end users and is often `null`. For the full contract of the envelope, see [Authentication](../../integration-resources/authentication).
### A suggested verification loop
1. Call `fund-auth` with `authType=EXPEND` and watch for DCS pushing an authorization notification to your `authUrl`.
2. At `authUrl`, verify the signature, decrypt, make your decision, then sign and return it.
3. Check that `data.approved` in the `fund-auth` response matches the decision you returned.
4. Repeat with `authType=REFUND` and `REVERSAL` to exercise the return and reversal paths.
## Next steps
For scenario-by-scenario simulation cases (purchase, return and reversal, plus how to handle clearing, partial and multi-part clearing, 3DS and other capabilities still to be confirmed), see [Simulating transactions](./simulating-transactions).
# Simulating transactions: authorization, clearing, refunds, partial, multi-part, 3DS
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/sandbox/simulating-transactions
Walks through the fund-auth simulation endpoint scenario by scenario: authentication and request headers, request and response fields, which scenarios each of the three authorization directions (EXPEND / REFUND / REVERSAL) can drive, a recommended regression order, and the scenarios that have no dedicated simulation entry point yet. For the sandbox itself and its prerequisites, see Sandbox and testing overview.
## Simulating transactions
Whether you have just wired up the authorization callback or are running a final regression before go-live, a single sandbox endpoint lets you exercise the whole "card use, authorization, return, reversal" sequence and confirm that your `authUrl` decision logic and your `webhookUrl` posting logic are correct, with no real money and no real card network involved at any point. DCS is a licensed card issuer, and the sandbox shares the same authorization forwarding path as production, so whatever you get working in the sandbox migrates cleanly to production.
Under the Partner-Managed model, the limit is **held** by you and the authorization is **decided** by you. That is why the real value of sandbox simulation is that it **drives a genuine authorization forwarding round trip**: once DCS receives your simulated transaction, it forwards the authorization request to your configured `authUrl` exactly as it would for a real card use, waits for your approve-or-decline answer, and then calls your `webhookUrl` accordingly.
> Who does what
>
> * **DCS**: receives the simulation request, creates the transaction, forwards the authorization to `authUrl`, then calls back `webhookUrl` based on your decision.
> * **Partner**: sends the simulation request, returns the authorization decision at `authUrl`, then receives the callback at `webhookUrl` and posts it.
***
## Endpoint: simulate an authorization request
**`POST /open-api/simulation/v1/fund-auth`**
Simulates one authorization request (purchase, return or purchase reversal), which triggers the complete authorization forwarding and webhook notification chain in the sandbox.
> This endpoint is **sandbox-only (QA/DEV)**. A call against production is blocked and returns `OPERATION_NOT_SUPPORT`.
### Authentication and request headers
As with every `/open-api/` endpoint, you must send the authentication headers and set `Content-Type: application/json`. For the full rules, see [Authentication](../../integration-resources/authentication).
| Header | Required | Description |
| - | - | - |
| `Content-Type` | Yes | Fixed value `application/json` |
| `X-DAPI-API-KEY` | Yes | The `apiKey` DCS issued to you |
| `X-DAPI-TIMESTAMP` | Yes | Request timestamp (milliseconds, UTC), used for replay protection |
| `X-DAPI-NONCE` | Yes | Random number in the range `[10000, 99999]`, for replay protection |
| `X-DAPI-SIGN` | Yes | HMAC-SHA256 signature (lowercase hexadecimal) |
### Request parameters
| Field | Type | Required | Description | Constraints |
| - | - | - | - | - |
| `cardId` | string | Yes | Card ID. The card must be in a usable state | β |
| `authType` | string | Yes | Authorization type: `EXPEND` purchase / `REFUND` return / `REVERSAL` purchase reversal | One of the three values |
| `amount` | number | Yes | Amount as a decimal, positive, in the major unit of the currency | Up to 10 integer digits, up to 2 decimal places |
| `currency` | string | Yes | Transaction currency, ISO 4217 three-letter code (`USD` / `CNY` / `SGD` / `EUR` / `JPY` and so on) | β |
> Field source: `POST /open-api/simulation/v1/fund-auth` (`APISimulationAuthRequest`). The endpoint currently does **not** accept parameters such as merchant name (`merchantName`), merchant category code (MCC) or an explicitly chosen decline reason (`declineReason`). If you need any of these later, talk to the DCS team.
### Request example (purchase)
```json theme={null}
{
"cardId": "CARD_20250101XXXX",
"authType": "EXPEND",
"amount": 50.00,
"currency": "USD"
}
```
### Response example
A successful call returns the standard response envelope, with the authorization outcome inside `data`:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"approved": true,
"errorCode": null
}
}
```
When the authorization is declined, for instance because your `authUrl` returned a decline:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"approved": false,
"errorCode": "XXXX"
}
}
```
### Response fields
| Field | Type | Description |
| - | - | - |
| `data.approved` | boolean | Whether the simulated authorization was ultimately approved |
| `data.errorCode` | string | The error code when it was not approved (see the error code dictionary for individual values) |
> About the standard response envelope `{code, message, messageDetail, data}`: `code` is the system-level status (such as `SYS_SUCCESS`), while `message` and `messageDetail` carry prompt text.
Note: **a successful HTTP call plus `code=SYS_SUCCESS` does not mean the authorization was approved.** For this endpoint, whether the authorization passed is determined by `data.approved`; an `approved=false` result comes back inside the very same `SYS_SUCCESS` envelope. Never judge the business outcome from the envelope alone. For how the error codes are classified, see [Error code dictionary](../transactions/decline-codes).
***
## What the simulation can drive
The DCS sandbox uses `authType` to distinguish three authorization directions, which together cover the core authorization forwarding loop:
| authType | Simulated action | Direction of funds | Typical use |
| - | - | - | - |
| `EXPEND` | Purchase | OUTGOING (consumes limit) | Test your `authUrl` approve or decline decision and webhook posting |
| `REFUND` | Return | INCOMING (money back) | Test the return callback and the reconciliation direction |
| `REVERSAL` | Purchase reversal | INCOMING (reverses the original authorization) | Test how you handle a reversal and the release of a hold |
### Recommended regression order
1. **Create a usable card**: complete the card issuance flow in the sandbox first (see [Virtual card application](../cards/virtual-card)) and keep the `cardId`.
2. **Configure your callback URLs**: confirm `authUrl` and `webhookUrl` are configured and reachable (see [Webhook configuration](../webhooks/configuration)).
3. **Simulate a purchase**: send `authType=EXPEND`, approve at `authUrl`, then check that `data.approved=true` and that `webhookUrl` received the matching event.
4. **Simulate a decline**: return a decline at `authUrl`, then check `data.approved=false` and the `errorCode`.
5. **Simulate a return and a reversal**: use `REFUND` and `REVERSAL` to verify the callbacks and reconciliation on the INCOMING side.
> Simulated transactions create **real transaction and authorization records** and fire **real webhooks**, built to the same shape as production, so they are suitable for end-to-end verification as they stand. For the fields on authorization and transaction records and the structure of webhook events, see [Events and data structures](../webhooks/events-and-schema) and [Authorization and clearing scenarios](../transactions/auth-and-settlement).
***
## Scenarios with no dedicated simulation yet
The DCS sandbox currently offers a single simulation entry point, `fund-auth`, covering purchase, return and reversal. The scenarios below have **no dedicated simulation endpoint**; test against the callback structures documented for them, or ask the DCS team to help construct a case:
| Scenario | Status | Description |
| - | - | - |
| Standalone clearing or settlement (settlement/capture) simulation | Not available | There is no standalone clearing simulation endpoint today; for clearing behavior, test against the real callback structures described in [Clearing scenarios: partial, over-clearing, multi-part](../transactions/capture-scenarios) |
| Incremental authorization (authorization update, such as adding a tip) | Not available | No standalone entry point for incremental authorization |
| Partial, over- and multi-part clearing | Not available | No standalone entry point; for the semantics see [Clearing scenarios](../transactions/capture-scenarios) |
| 3DS challenge (OTP/challenge) simulation | Not available | No standalone 3DS simulation endpoint; for the 3DS forwarding mechanism see [3DS forwarding](../transactions/3ds) |
| Collateral or deposit funding simulation | Not applicable | Under the Partner-Managed model the limit is held by you and the authorization is decided by you, so the sandbox offers no end-user funding simulation entry point |
> Today DCS covers the main authorization forwarding loop with a single `fund-auth` endpoint and its three `authType` values. If you need to test any of the scenarios above that lack a dedicated simulation entry point, contact the DCS team for help.
***
## Next steps
Once the simulation runs cleanly, check the posting direction of each `authType` in your own system against [Authorization and clearing scenarios](../transactions/auth-and-settlement), and complete the final regression in [Pre-go-live checks](../../customer-success/pre-go-live).
# 3DS forwarding
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/transactions/3ds
Adds the sequence diagrams and the webhook fields.
## π Guide
Whether you are an exchange, a wallet or a platform, you can let the SMS, email or in-app notification channels you already run carry the online payment verification (3DS) of your cardholders, so that the last mile of delivering a passcode goes back to the channel you know best. As a licensed issuer, DCS handles the 3DS protocol exchange with the card network, generates and validates the passcode, and forwards whatever the verification needs to the partner securely.
3DS (3D Secure) is the security protocol for online payments: it adds one extra identity check on the cardholder during a transaction to reduce fraud losses. Under Partner-Managed, DCS offers **three forms of 3DS verification**, and the partner is free to choose how much of the cardholder experience it wants to own.
## The three modes at a glance
| Mode | Who sends the passcode | Who validates it | What the partner has to do | `flowsType` triggered |
| - | - | - | - | - |
| Mode 1: DCS sends the passcode | DCS | DCS | **Nothing at all** | No webhook is sent |
| Mode 2: the partner sends the passcode (delegated OTP) | Partner | DCS | Expose a webhook to receive the OTP, decrypt it and deliver it to the user | `OTP_DELEGATE` |
| Mode 3: the partner verifies the user (OOB) | Partner, inside its own app or channel | Partner, which then returns the outcome to DCS | Expose a webhook to receive the notification, take the user through authentication, and return the outcome | `OOB` |
> Which mode applies is set by your card program configuration. **Configuring the 3DS mode: DCS.** **Building the integration: the partner.** To change modes, contact the DCS team.
## The overall 3DS forwarding flow
The diagram below shows the full round trip of a 3DS verification request between the parties: DCS receives the request from the card network and forwards it to the partner, the partner completes verification in whichever mode applies and returns the outcome back along the same path, and DCS then sends a webhook with the challenge status update. Who sends the passcode and who decides the outcome differ by mode, as detailed below.
***
## Mode 1: DCS sends the passcode
DCS sends the SMS or email passcode to the cardholder and validates it. In this mode **the partner has nothing extra to build**: DCS handles generating, sending and validating the passcode, and sends no 3DS webhook.
Suitable for partners that have not built their own messaging channels yet and want the fastest possible launch.
***
## Mode 2: the partner sends the passcode (OTP\_DELEGATE)
The partner delivers the passcode to the cardholder over its own channel and DCS validates it. In this mode the partner must expose a webhook to receive the OTP that DCS pushes, decrypt it and forward it to the user.
### Webhook fields (3DS challenge notification)
3DS webhooks carry the type `AUTHORISATION_3DS_CHALLENGE`, and `flowsType` distinguishes the mode. In mode 2, `flowsType=OTP_DELEGATE`.
| Field | Type | Description |
| - | - | - |
| `challengeId` | string | 3DS challenge ID |
| `status` | string | Challenge status: `INIT` / `NOTICED` / `RECEIVED` / `APPROVED` / `REJECTED` |
| `cardId` | string | Card ID |
| `expiryTime` | string | When the challenge expires |
| `currency` | string | Currency of the transaction |
| `amount` | number | Amount of the transaction |
| `merchantId` | string | Merchant ID |
| `merchantName` | string | Merchant name |
| `merchantCountry` | string | Merchant country |
| `mcc` | string | Merchant category code |
| `flowsType` | string | Authentication flow type: `OOB` / `OTP_DELEGATE` |
| `challengeMethodType` | string | Challenge method type: `DELEGATE_SCA_V1` / `SMS_OTP` / `EMAIL_OTP` |
| `otpPasscode` | string | OTP passcode (**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 for the AES-GCM encryption (Base64-encoded), populated only when `flowsType=OTP_DELEGATE` |
> For webhook delivery, the outer envelope and signature verification in general, see [Webhook events and schema](../webhooks/events-and-schema).
### Decrypting the OTP (partner)
`otpPasscode`, `phoneNumber` and `email` are encrypted in AES-GCM mode with the partner's `secretKey`, and the webhook carries the `iv` alongside them. On receipt, the partner decrypts with `secretKey` and `iv`.
* **Algorithm**: AES-128-GCM (NoPadding)
* **Key**: the partner's `secretKey`, Base64-encoded
* **IV**: the `iv` field of the webhook, Base64-encoded, 12 bytes
* **Auth tag**: 128 bits, appended to the ciphertext
* **Ciphertext format**: Base64-encoded, decoding to `ciphertext + authTag`
```java theme={null}
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AesGcmDecryptor {
public static String decrypt(String base64Ciphertext, String base64Key, String base64IV) {
byte[] key = Base64.getDecoder().decode(base64Key);
byte[] iv = Base64.getDecoder().decode(base64IV);
byte[] ciphertext = Base64.getDecoder().decode(base64Ciphertext);
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
}
}
// Usage example
String otpPasscode = decrypt(webhook.getOtpPasscode(), secretKey, webhook.getIv());
String phoneNumber = decrypt(webhook.getPhoneNumber(), secretKey, webhook.getIv());
String email = decrypt(webhook.getEmail(), secretKey, webhook.getIv());
```
> In mode 2 **DCS still does the validating**: the partner is only responsible for getting the decrypted passcode to the user and does not return a verification result. The final outcome arrives in a webhook carrying `status=APPROVED` or `status=REJECTED`.
***
## Mode 3: the partner verifies the user (OOB)
With OOB (out-of-band authentication), the partner takes the cardholder through authentication inside its own app or another channel, using biometrics, in-app confirmation or similar. **The partner decides** whether the check passed and returns the outcome to DCS. In this mode the webhook carries `flowsType=OOB` and `challengeMethodType=DELEGATE_SCA_V1`, and **does not include** `otpPasscode`, `phoneNumber`, `email` or `iv`.
### Returning the authentication result (partner)
Once the partner has decided, call `POST /open-api/3ds-challenge/v1/authenticate` to return the outcome to DCS.
**Minimal request:**
```json theme={null}
{
"challengeId": "",
"approveStatus": "APPROVE"
}
```
**Request fields:**
| Field | Type | Required | Description |
| - | - | :-: | - |
| `challengeId` | string | Yes | The 3DS challenge ID raised before the transaction, taken from the OOB webhook; max 50 |
| `approveStatus` | string | Yes | Challenge action: `APPROVE` / `REJECT`; max 20 |
**Response:** every `/open-api/` endpoint shares the same response envelope, `{ code, message, messageDetail, data }`. On success, `data` echoes the current state of the challenge:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"challengeId": "CHL_1a2b...",
"status": "APPROVED",
"cardId": "CARD_8f3a...",
"expiryTime": "2026-06-17T08:05:00Z",
"currency": "USD",
"amount": 100.00,
"merchantId": "M_001",
"merchantName": "Acme Store",
"merchantCountry": "SG",
"mcc": "5732"
}
}
```
| Response field | Description |
| - | - |
| `challengeId` | 3DS challenge ID |
| `status` | Challenge status: `INIT` / `NOTICED` / `RECEIVED` / `APPROVED` / `REJECTED` |
| `cardId` | Card ID |
| `expiryTime` | When the challenge expires |
| `currency` | Currency of the transaction |
| `amount` | Amount of the transaction |
| `merchantId` | Merchant ID |
| `merchantName` | Merchant name |
| `merchantCountry` | Merchant country |
| `mcc` | Merchant category code |
> About the envelope: judge success or failure by `code` alone (for example an echoed `SYS_SUCCESS`; for the error code conventions see [Authorization declines and error codes](./decline-codes)). `message` is a short developer-facing string, and `messageDetail` is an optional display object (with `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`) meant for front-end guidance only, never as the basis for deciding the outcome.
Return your result before `expiryTime`. That deadline comes from the challenge validity the 3DS provider issues; if nothing arrives in time, DCS sets the challenge to `REJECTED` and never auto-approves.
***
## Integration notes
* **Content-Type**: set `Content-Type: application/json` explicitly on every `/open-api/` request.
* **Authentication**: calling `/open-api/3ds-challenge/v1/authenticate` and verifying the signature on inbound webhooks both follow the [Authentication guide](../../integration-resources/authentication).
* **Webhook endpoint**: modes 2 and 3 both require the partner to configure, in advance, a webhook address able to receive `AUTHORISATION_3DS_CHALLENGE`. The partner supplies that address to DCS.
* **Mode 3 deadline**: the result must be returned before the `expiryTime` issued by the provider; an overdue challenge is set to `REJECTED`.
* **When delegated OTP fails**: in mode 2, DCS waits synchronously for the partner webhook for at most 7 seconds. On timeout, a failure response, a decryption failure or a delivery failure, there is no resend and no fallback to DCS sending the passcode: the current challenge goes straight to `REJECTED`. The partner must decrypt, deliver and return `phoneNumberTail` or `email` synchronously within those 7 seconds.
## Next steps
Once 3DS verification passes, the transaction moves into authorization. Go to [Authorization](./authorization) to see how the decision is forwarded to the partner, then read [Authorization and settlement](./auth-and-settlement) to understand how funds are held and finally settled.
# Authorization and settlement: every scenario
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/transactions/auth-and-settlement
Mapped to the nine Auth scenarios.
## Authorization and settlement: every scenario
Whether your cardholder is paying an online merchant, withdrawing cash at an ATM or returning goods for a refund, DCS splits every movement of funds into two clean stages for you: the **authorization stage** places or releases a hold, and the **settlement stage** performs the real debit or refund. This page lists every combination of the two so that you can map each business situation to your own books during integration. DCS is a licensed issuer and its own BIN sponsor: holds and debits are executed by DCS on the card network side, while the authorization decision stays with the partner.
> Before reading on, it helps to know how three entities relate to each other. An **authorization** records the decision to hold or release funds, an **Outstanding** tracks how much is currently held as amounts accumulate and are drawn down, and a **transaction** records the debit or refund that actually happened. See [Authorization](./authorization) and [Transaction records](./transaction).
***
## Mental model: Auth, Outstanding, Transaction
* Once an authorization is approved, DCS places the hold and creates (or reuses) an **Outstanding**, whose `amount` carries the total currently held.
* Later authorizations such as incremental authorizations and reversals use `originalAuthId` to find the `outsId` of the first authorization, then add to or subtract from the **same Outstanding**.
* When settlement arrives, DCS draws the Outstanding down to zero and creates a transaction record. If the settled amount differs from the amount held, DCS automatically back-fills a **FORCE\_AUTH** authorization to top up or release the difference.
> **Key fields**: `authId` (authorization ID), `outsId` (bill or Outstanding ID, the link between authorization and settlement), `originalAuthId` (the authorization being referenced), `direction` (`OUTGOING` holds, `INCOMING` releases) and `authType` (authorization type). The full field list is in [the authorization data structure](./authorization).
### Authorization type (authType)
| Value | Meaning | Who triggers it |
| - | - | - |
| `NORMAL` | Normal authorization, created from a real-time network callback; covers purchases, incremental authorizations, reversals, refunds, cash withdrawals and enquiries | Card network, in real time |
| `FORCE_AUTH` | Forced authorization, back-filled by DCS: settlement has no matching authorization (an offline transaction), or the settled amount differs from the amount held and needs to be trued up | DCS, automatically |
| `EXPIRED_RELEASE` | Expiry release: the authorization expired before settlement and the network instructed the release through a settlement file | Network settlement file |
| `STATUS_DIFF_RELEASE` | Status mismatch release: the network declined on timeout while DCS had approved, corrected during reconciliation | DCS reconciliation |
***
## Scenario overview
The 13 scenarios below cover every combination of authorization and settlement. The "Outstanding, final" column is 0 throughout, which is what tells you the fund lifecycle has closed.
| # | Scenario | Authorization stage (hold / release) | Settlement stage (debit / refund) | Outstanding, final |
| - | - | - | - | -: |
| 1 | Normal authorization, normal settlement | Hold 100 | Debit 100 | 0 |
| 2 | Normal authorization, incremental authorization, normal settlement | Hold 100 plus 20 more | Debit 120 | 0 |
| 3 | Normal authorization, partial reversal, normal settlement | Hold 100 minus release of 30 | Debit 70 | 0 |
| 4 | Normal authorization, full reversal | Hold 100 minus release of 100 | None | 0 |
| 5 | Normal authorization, excess capture | Hold 100 | Debit 150 (the extra 50 held by a FORCE\_AUTH) | 0 |
| 6 | Normal authorization, under capture | Hold 100 | Debit 80 (the surplus 20 released by a FORCE\_AUTH) | 0 |
| 7 | Normal authorization, partial capture, final capture | Hold 100 | Debit 60 plus debit 40 | 0 |
| 8 | Forced capture with no authorization | No hold | Direct debit of 100 | 0 |
| 9 | Forced refund with no authorization | No hold | Direct refund of 50 | 0 |
| 10 | Enquiry authorization | Amount 0, nothing held | None | 0 |
| 11 | Authorization expiry release | Hold 100, then release 100 | None | 0 |
| 12 | Authorization timeout mismatch | Hold 100, then roll back 100 | None | 0 |
| 13 | Cash withdrawal (settlement plus withdrawal fee) | Hold 110, then release 10 | Debit 100 plus fee of 10 | 0 |
> Reading convention: each action sequence is annotated as `direction / authType / outsId(change in amount)`. `A` means approved (`approveFlag=A`) and `D` means declined.
***
## Scenario detail
### Scenario 1: normal authorization, normal settlement
The base case: one hold, one settlement debit, amounts matching exactly.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL, A), `outsId1` (amount=100)
2. Settlement: OUTGOING, debit 100 USD, `transactionId1`, `outsId1` (amount=100β0)
| Step | authorization | outstanding | transaction |
| - | - | - | - |
| Authorization approved | `authId1`: OUTGOING, A, `outsId1` | `outsId1`: amount=100 | β |
| Settlement completed | β | `outsId1`: amount=0 | `transactionId1`: OUTGOING, `outsId1` |
### Scenario 2: normal authorization, incremental authorization, normal settlement
The merchant holds an additional amount, as a hotel does when adding a gratuity. Several authorizations share one Outstanding.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL), `outsId1` (amount=100)
2. Authorization: OUTGOING, hold 20 USD, `authId2` (NORMAL), `outsId1` (amount=100β120)
3. Settlement: OUTGOING, debit 120 USD, `transactionId1`, `outsId1` (amount=120β0)
The incremental authorization uses `originalAuthId` to find the `outsId` of the first authorization and adds to the same Outstanding.
### Scenario 3: normal authorization, partial reversal, normal settlement
The cardholder returns part of the purchase, the merchant sends a partial reversal, and settlement follows for the remainder.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL), `outsId1` (amount=100)
2. Authorization: INCOMING, release 30 USD, `authId2` (NORMAL), `outsId1` (amount=100β70)
3. Settlement: OUTGOING, debit 70 USD, `transactionId1`, `outsId1` (amount=70β0)
### Scenario 4: normal authorization, full reversal
The cardholder cancels, the merchant sends a full reversal, and no settlement follows.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL), `outsId1` (amount=100)
2. Authorization: INCOMING, release 100 USD, `authId2` (NORMAL), `outsId1` (amount=100β0)
Once the Outstanding is back to zero, there is nothing left to settle.
### Scenario 5: normal authorization, excess capture
The settled amount is larger than the amount held, because of an exchange-rate move or an added fee. Within the same database transaction, DCS back-fills an OUTGOING `FORCE_AUTH` for the difference.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL), `outsId1` (amount=100)
2. Settlement processing, in one transaction:
1. Authorization: OUTGOING, hold 50 USD, `authId2` (FORCE\_AUTH), `outsId1` (amount=100β150)
2. Settlement: OUTGOING, debit 150 USD, `transactionId1`, `outsId1` (amount=150β0)
### Scenario 6: normal authorization, under capture
The settled amount is smaller than the amount held. Within the same database transaction, DCS back-fills an INCOMING `FORCE_AUTH` to release the surplus.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL), `outsId1` (amount=100)
2. Settlement processing, in one transaction:
1. Authorization: INCOMING, release 20 USD, `authId2` (FORCE\_AUTH), `outsId1` (amount=100β80)
2. Settlement: OUTGOING, debit 80 USD, `transactionId1`, `outsId1` (amount=80β0)
### Scenario 7: normal authorization, partial capture, final capture
Multiple captures, identified by the `multiClearInd` field on the transaction record: every capture but the last one only draws the Outstanding down, and the difference is checked on the final one.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL), `outsId1` (amount=100)
2. Settlement: OUTGOING, debit 60 USD, `transactionId1` (multiClearInd=P), `outsId1` (amount=100β40)
3. Settlement: OUTGOING, debit 40 USD, `transactionId2` (multiClearInd=F), `outsId1` (amount=40β0)
If a difference remains after the final capture, DCS back-fills a `FORCE_AUTH` following the logic of scenarios 5 and 6.
> `multiClearInd` values: `O` for an ordinary single capture, `P` for a capture that is not the last one, `F` for the capture that completes the set. See [Capture scenarios](./capture-scenarios).
### Scenario 8: forced capture with no authorization
For an offline transaction, settlement arrives with no matching authorization, so DCS back-fills a complete Auth plus Outstanding within the same database transaction.
1. Settlement processing, in one transaction:
1. Authorization: OUTGOING, hold 100 USD, `authId1` (FORCE\_AUTH, A), `outsId1` (amount=0)
2. Settlement: OUTGOING, debit 100 USD, `transactionId1`, `outsId1` (amount=0)
The Outstanding `amount` is set straight to 0, the `FORCE_AUTH` is auto-approved, and all three records are created in the same transaction.
### Scenario 9: forced refund with no authorization
A refund settles with no matching authorization, so DCS creates a new Outstanding purely to keep the records linked.
1. Settlement processing, in one transaction:
1. Settlement: INCOMING, refund 50 USD, `transactionId1`, `outsId1` (amount=0)
Unlike a forced capture, the INCOMING direction does not back-fill an Auth; only the Outstanding is created, for linkage.
### Scenario 10: enquiry authorization
A zero-amount enquiry authorization that only checks whether the card is valid. Nothing is held.
1. Authorization: OUTGOING, hold 0 USD, `authId1` (NORMAL), `outsId1` (amount=0)
An Outstanding is still created (amount=0), but no funds are held. The partner still receives the authorization webhook, with `transactionType=Q`.
### Scenario 11: authorization expiry release
The authorization passed its validity period without settling, and the network instructs the release of the held funds through a settlement file.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL), `outsId1` (amount=100)
2. (Validity period passes, no settlement received)
3. Authorization: INCOMING, release 100 USD, `authId2` (EXPIRED\_RELEASE), `outsId1` (amount=100β0)
> The release is applied only if the amount matches the Outstanding **exactly and in the same currency**. A mismatch raises an internal DCS alert and is not processed.
### Scenario 12: authorization timeout mismatch
The network declined the authorization on timeout while DCS may already have approved it, so reconciliation has to correct the difference.
1. Authorization: OUTGOING, hold 100 USD, `authId1` (NORMAL), `outsId1` (amount=100)
2. (Network declined on timeout, DCS had approved, states disagree)
3. Authorization: INCOMING, roll back 100 USD, `authId2` (STATUS\_DIFF\_RELEASE), `outsId1` (amount rolled back)
How it is handled:
* DCS approved (`approveFlag=A`) and the network declined on timeout: a reverse authorization is created (STATUS\_DIFF\_RELEASE, INCOMING) and the Outstanding is rolled back.
* DCS declined (`approveFlag=D`): the states agree, nothing to do.
* No related authorization found: a record is created (STATUS\_DIFF\_RELEASE, `approveFlag=D`) for audit only, with no effect on funds.
### Scenario 13: cash withdrawal (normal settlement plus withdrawal fee)
The authorization stage holds the total of principal and fee. At settlement the fee portion is released first, following the under-capture logic, and the principal is debited; the fee is then debited through its own FORCE\_AUTH and settlement pair.
1. Authorization: OUTGOING, hold 110 USD, `authId1` (NORMAL), `outsId1` (amount=110)
2. Principal settlement processing, in one transaction:
1. Authorization: INCOMING, release 10 USD, `authId2` (FORCE\_AUTH), `outsId1` (amount=110β100)
2. Settlement: OUTGOING, debit 100 USD, `transactionId1` (category=CASH), `outsId1` (amount=100β0)
3. Fee forced-posting settlement processing, in one transaction:
1. Authorization: OUTGOING, hold 10 USD, `authId3` (FORCE\_AUTH), `outsId2` (amount=10)
2. Settlement: OUTGOING, debit 10 USD, `transactionId2` (category=CASH\_FEES), `outsId2` (amount=10β0)
How it is handled:
* The authorization holds 110 USD (100 principal plus 10 fee), and `outsId1` carries the total held.
* Settling the principal, DCS sees that the settled amount (100) is below the Outstanding (110), so under-capture logic back-fills an INCOMING `FORCE_AUTH` releasing the 10 difference before debiting 100.
* The fee goes through forced posting on its own: a new `outsId2` is created, a `FORCE_AUTH` holds 10, and 10 is settled and debited immediately.
> **How do you tell withdrawal principal from fee?** By the `category` field on the transaction record.
>
> The authoritative `category` values are in the [transaction category dictionary](../reports/field-dictionary): withdrawal principal is `CASH` and the withdrawal fee is `CASH_FEES`.
***
## A walkthrough: hotel pre-authorization to posting
Here is scenario 3 (partial reversal) on a real timeline β what you receive at each touchpoint, what you do, and how the books tie out. The case: your cardholder books three hotel nights with a 100 USD pre-authorization, cancels one night after check-in (the hotel reverses 30 USD), and the hotel captures 70 USD after checkout.
| Time | Event | What you receive | What you do | Outstanding `outsId1` |
| - | - | - | - | -: |
| Day 1 | Hotel pre-authorizes 100 USD | Real-time request on `authUrl`: `authId1`, `OUTGOING`, `NORMAL`, amount=100 | Decide within 2.5 s, return `00` | 0 β 100 |
| Day 1 | DCS freezes and sends the receipt | `AUTHORISATION_RESULT`: `authId1`, `approveFlag=A` | Book it: 100 frozen for this user | 100 |
| Day 3 | Hotel reverses one night, 30 USD | Real-time request on `authUrl`: `authId2`, `INCOMING`, `NORMAL`, amount=30, `originalAuthId=authId1` | Return `00` | 100 β 70 |
| Day 3 | DCS unfreezes and sends the receipt | `AUTHORISATION_RESULT`: `authId2`, `approveFlag=A` | Book it: frozen amount drops to 70 | 70 |
| Day 4 | The network settles 70 USD | (settlement sends no real-time notification) | β | 70 β 0 |
| Day 5 | Reconciliation files are generated | Two rows in the authorization report (`authId1`/`authId2`, same `outsId1`) + one row in the transaction report (`transactionId1`, 70 debited) | Tie out by `outsId1`: frozen 100 β released 30 = settled 70, closed | 0 |
Three reconciliation take-aways:
* **`outsId1` is the primary key of this timeline**: both authorizations and the settlement hang off the same Outstanding, so grouping by `outsId` matches authorizations to settlements β no guesswork needed.
* **Reversals also ask for your decision**: `INCOMING` releases are forwarded through `authUrl` too. DCS recommends simply returning `00` to approve; the only business reason to decline is an abnormal case such as a release larger than the amount currently frozen.
* **Settlement appears only in the daily files**: the capture on Day 4 produces no real-time callback, so your view of settled money comes entirely from the daily transaction report β which is exactly why end-of-day reconciliation is the source of truth for funds.
If the hotel had captured 75 or 60 instead of 70 on Day 4, DCS would automatically create a `FORCE_AUTH` to top up or release the difference, following scenarios 5/6 above; you would see that system-created record in the authorization report, with no real-time action needed on your side.
***
## What the partner does
| Step | Who does it | Description |
| - | - | - |
| Authorization decision (A/D) | **Partner** | DCS forwards the request over the authorization webhook, and the partner approves or declines based on its own limits and risk rules |
| Placing and releasing holds | **DCS** | Once the partner approves, DCS places the hold on the card network side and maintains the Outstanding |
| FORCE\_AUTH true-up | **DCS** | Back-filled automatically whenever settlement and hold disagree; no partner involvement needed |
| Settlement debit or refund | **DCS** | When network settlement arrives, DCS creates the transaction record and draws the Outstanding down |
| Reconciliation | **Partner** | Check the daily authorization report against the daily transaction report |
The request and response structures of the authorization webhook, the two-way RSA signing and the `responseCode` values (`00` approve, `01` insufficient funds, `11` transaction not allowed, `21` no response) are in [Webhook events and schema](../webhooks/events-and-schema).
***
## Next steps
* Want to verify these scenarios? Simulate an authorization in the sandbox: [Simulating transactions](../sandbox/simulating-transactions) (`POST /open-api/simulation/v1/fund-auth`, supporting `EXPEND` for a purchase, `REFUND` for a return and `REVERSAL` for a reversal).
* For the detail behind multiple and excess captures: [Capture scenarios](./capture-scenarios).
* For handling declined authorizations: [Authorization declines and error codes](./decline-codes).
# Authorization (forwarded decision)
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/transactions/authorization
Where the direction / authType and related enumerations live.
## Authorization: the decision comes back to you
Whether you want to own cardholder limits outright or apply your own risk rules transaction by transaction, authorization forwarding lets you decide, at the moment of every card payment, whether to approve or decline. DCS is a licensed issuer with its own BIN: we forward each real-time network authorization request to you securely and place or release the fund hold on your behalf. **The limits belong to the partner, and so does the authorization decision.**
An authorization happens the instant a cardholder pays. It is a real-time decision and **moves no money**. Funds actually leave the account later, at settlement (see [Transaction records](./transaction) and [Authorization and settlement](./auth-and-settlement)).
***
## Mental model: the life of one authorization
| Role | Who does it | Responsibility |
| - | - | - |
| Forward the authorization request | **DCS** | Check card status, then encrypt, sign and push the request to your `authUrl` |
| Make the authorization decision | **Partner** | Verify the signature, decrypt, and approve or decline based on the enterprise security deposit balance and your risk rules |
| Hold or release funds | **DCS** | Act on your decision and answer the card network |
> Under Partner-Managed, the decision always stays with the partner. The forwarding channel adds **two-way RSA encryption and signing**, so no business field ever travels in clear text.
***
## Transaction direction (direction)
| Value | Name | Fund action | Typical scenario |
| - | - | - | - |
| `OUTGOING` | Outgoing | Hold the corresponding amount | Cardholder makes a payment |
| `INCOMING` | Incoming | Release the corresponding amount | Reversal, refund |
> `INCOMING` authorizations β refunds with an original authorization, reversals β are forwarded through `authUrl` as well. DCS recommends that the partner simply return `00` (approve) for these `INCOMING` authorizations; the partner may still decline in abnormal cases, such as when an `INCOMING` release exceeds the amount currently held.
***
## Authorization type (authType)
`authType` tells you how an authorization record came into being, so you can separate a live authorization from a system back-fill or release.
| Value | Name | Goes through `authUrl` | Description |
| - | - | - | - |
| `NORMAL` | Normal authorization | Yes | Standard record created from a real-time channel authorization callback; covers purchases, incremental authorizations, reversals, refunds, cash withdrawals, balance enquiries and every other live scenario |
| `FORCE_AUTH` | Forced authorization | No | Record back-filled by the system: settlement arrives with no matching authorization (an offline transaction, for example), or the settled amount differs from the amount held and the difference must be trued up |
| `EXPIRED_RELEASE` | Expiry release | No | The authorization expired before settlement, and the channel instructed the release of the held funds through a settlement file |
| `STATUS_DIFF_RELEASE` | Status mismatch release | No | Reconciliation correction raised when the channel declined the authorization on timeout while DCS had already approved it |
Only `NORMAL` authorizations are forwarded through `authUrl` for the partner's real-time decision; `FORCE_AUTH` / `EXPIRED_RELEASE` / `STATUS_DIFF_RELEASE` are system-side authorizations that never go through `authUrl` and are only pushed as receipts through the `AUTHORISATION_RESULT` webhook.
The field appears both in the **authorization webhook request** and in the **daily authorization report** (see [Authorization report](../reports/authorization-report)).
the `authType` of an authorization forwarding notification and the sandbox field `APISimulationAuthRequest.authType` (`EXPEND` / `REFUND` / `REVERSAL`) are two independent enumerations, one for real authorization callbacks and one for sandbox triggers. Do not mix them.
***
## Authorization transaction type (transactionType)
`transactionType` describes the nature of the cardholder's transaction, which helps both your authorization decision and your reconciliation breakdown.
| Value | Name | Description |
| - | - | - |
| `R` | Card purchase | All card purchase transactions |
| `C` | ATM cash withdrawal | ATM withdrawals and other cash advances |
| `Q` | Enquiry | Enquiry transactions, no movement of funds |
| `P` | Return / refund | All transfers or refunds credited to the card |
***
## Authorization result (responseCode): your reply
Once the partner has decided, it returns the outcome to DCS in `responseCode`:
| `responseCode` | Meaning |
| - | - |
| `"00"` | Approve |
| `"01"` | Decline, insufficient funds |
| `"11"` | Decline, transaction not allowed |
| `"21"` | Decline, no response |
> How it is stored: the authorization forwarding notification is answered with `responseCode`, and the persisted mapping is fixed at `00 β approveFlag=A`, with `01` / `11` / `21` and any other non-`00` value mapping to `approveFlag=D`. `approveFlag` appears in the authorization result webhook and the daily authorization report. In production the synchronous response window is **2.5 seconds**; the partner must reply within it. A timeout is treated as `responseCode=21` / `DAPI_AUTH_ENTERPRISE_TIMEOUT_REJECT`, and a real-time authorization is never retried.
***
## The forwarding notification: who does what on the secure channel
An authorization request is not an ordinary webhook. It travels over a channel protected by **two-way RSA encryption and signing**, with every business field wrapped inside `encryptedData` rather than sent in clear text.
### Prerequisites (partner)
* Your enterprise has been created and both `authUrl` and `externalPublicKey` (your RSA public key) are configured. See [First steps](../../getting-started/first-steps).
* The **IP allowlist** for the authorization forwarding notification is in place.
* You have [issued a card](../cards/card-issuing) successfully.
> The sandbox DCS RSA public key can be copied straight from the [Authentication guide](../../integration-resources/authentication); for the production key, contact the DCS team.
### The round trip
| Step | Who does it | Action |
| - | - | - |
| 1. Send the notification | **DCS** | Take the business fields (`authId`, `direction`, `currency`, `amount` and so on), encrypt them with the **partner RSA public key** and Base64-encode the result into `data.encryptedData`; sign with the **DCS RSA private key** and Base64-encode the result into the `X-Auth-Signature` header; POST to your `authUrl` |
| 2. Verify | **Partner** | Verify `X-Auth-Signature` with the **DCS RSA public key**, and decline outright if verification fails |
| 3. Decrypt | **Partner** | Decrypt `data.encryptedData` with your **partner RSA private key** to recover the business fields |
| 4. Decide | **Partner** | Produce a `responseCode` from the enterprise security deposit balance and your risk rules |
| 5. Reply | **Partner** | Encrypt `{authId, responseCode}` with the **DCS RSA public key** into `encryptData`, sign it with your **partner RSA private key** into `signature`, and return both to DCS |
| 6. Execute | **DCS** | Verify and decrypt your reply, then hold funds, release funds or stop the transaction according to `responseCode` |
Signature algorithm: **RSA-SHA256 (`SHA256withRSA`)**, 2048-bit keys, with chunked encryption and decryption (245-byte encryption blocks, 256-byte decryption blocks). Full Java sample code is on the [Authentication guide](../../integration-resources/authentication) page.
> Notification URL template: `https://{domain}/xxx/v1/auth-notification` (the POST that DCS sends to your `authUrl`).
***
## Request and response structure
### Request header
| Field | Type | Description |
| - | - | - |
| `X-Auth-Signature` | string | Signature: the Base64-encoded result of signing with the DCS private key |
### Request body
| Field | Type | Description |
| - | - | - |
| `webhookId` | string | Unique webhook identifier; use it to deduplicate and avoid processing the same event twice |
| `webhookType` | string | Fixed value `AUTHORISATION` |
| `webhookSubType` | string | Subtype, for example `CREATE` or `UPDATE` |
| `businessId` | string | Identifier of the webhook subject, for example the `authId` |
| `data.authId` | string | Unique number of the authorization event |
| `data.encryptedData` | string | All authorization business fields, encrypted (RSA + Base64); see below |
| `notificationTime` | string | Notification time, `yyyy-MM-dd'T'HH:mm:ss+08:00` (UTC+8) |
**Time zone**: authorization notifications and the reconciliation files both carry UTC+8 (`+08:00`) timestamps; the one exception is the velocity-limit query endpoint, whose date fields are UTC. Store the offset explicitly so that comparisons across endpoints do not drift.
### Business fields inside `encryptedData`
| Field | Type | Description |
| - | - | - |
| `authId` | string | Unique authorization identifier, the key to later transactions and reconciliation |
| `direction` | string | Transaction direction, `OUTGOING` or `INCOMING` |
| `authType` | string | Authorization type; see the enumeration above |
| `outsId` | string | Outstanding (bill) ID, created with the first authorization, links authorization to settlement |
| `originalAuthId` | string | Original authorization being referenced; incremental and reversal scenarios reuse the same Outstanding |
| `customerId` | string | Cardholder user ID |
| `cardId` | string | ID of the card being authorized |
| `currency` | string | Settlement currency, ISO 3-character code |
| `amount` | bigdecimal | Settlement amount |
| `acquirerCurrency` | string | Currency requested by the user (may differ from the settlement currency) |
| `acquirerAmount` | bigdecimal | Amount requested by the user |
| `cardAcceptorIdentification` | string | Merchant number identifying the merchant in the transaction |
| `cardAcceptorNameLocation` | string | Merchant name and address (layout below) |
| `merchantType` | string | Merchant category code (MCC), 4 digits |
| `transactionType` | string | Transaction type `R` / `C` / `Q` / `P`; see the enumeration above |
#### `cardAcceptorNameLocation`
A fixed 40-character text; each segment is left-justified and right-padded with spaces.
##### Visa
| Position | Length | Description |
| - | - | - |
| 1β25 | 25 | Merchant name, recognisable to the cardholder |
| 26β38 | 13 | Merchant city |
| 39β40 | 2 | Merchant country, ISO 3166-1 alpha-2 two-letter code |
##### Mastercard
| Position | Length | Description |
| - | - | - |
| 1β22 | 22 | Merchant name, recognisable to the cardholder |
| 23 | 1 | Space separator |
| 24β36 | 13 | Merchant city |
| 37 | 1 | Space separator |
| 38β40 | 3 | Merchant country, ISO 3166-1 alpha-3 three-letter code |
### Response body
| Field | Type | Description |
| - | - | - |
| `signature` | string | Signature over the response: the Base64-encoded result of signing with the partner private key |
| `encryptData` | string | Encrypted response data (`authId` + `responseCode`), Base64-encoded |
Structure inside `encryptData`:
| Field | Type | Description |
| - | - | - |
| `authId` | string | Authorization ID |
| `responseCode` | string | Authorization result; see above |
the encrypted field on the request side is `data.encryptedData` (nested inside `data`), while on the response side it is `encryptData` (top level, spelled differently). The names do not match, so read each one from its own structure and never treat them as interchangeable.
***
## Minimal authorization request example
```json theme={null}
{
"webhookId": "wh_20260616_0001",
"webhookType": "AUTHORISATION",
"webhookSubType": "CREATE",
"businessId": "auth_8f3c...",
"data": {
"authId": "auth_8f3c...",
"encryptedData": "Base64(RSA(partner public key, business fields JSON))"
},
"notificationTime": "2026-06-16T14:23:05+08:00"
}
```
Response (partner to DCS):
```json theme={null}
{
"encryptData": "Base64(RSA(DCS public key, {\"authId\":\"auth_8f3c...\",\"responseCode\":\"00\"}))",
"signature": "Base64(SHA256withRSA(partner private key, encryptData))"
}
```
> This forwarding notification is a **dedicated secure channel** from DCS to your `authUrl`. It does not use the common response envelope `{code, message, messageDetail, data}` of the `/open-api/` endpoints, which applies only to the calls you initiate (see [Authentication and request conventions](../../integration-resources/authentication)).
***
## A walkthrough of one authorization
Let's tie the fields and the flow together with one realistic purchase: your cardholder carries a USD card and spends 3,000 JPY at a supermarket in Tokyo.
| Moment | What happens | What you receive / return |
| - | - | - |
| T+0ms | The cardholder taps; the card network routes the request to DCS, which checks the card state (active, not frozen) | β |
| T+\~100ms | DCS encrypts, signs and POSTs to your `authUrl` | Request: `direction=OUTGOING`, `authType=NORMAL`, `transactionType=R`, `acquirerCurrency=JPY`, `acquirerAmount=3000`, `currency=USD`, `amount=20.45`, `merchantType=5411`, `outsId=outs_71ab...` |
| Within T+2.5s | You verify the signature, decrypt, check the user's limit and your risk rules, and decide | Response: `{authId, responseCode: "00"}` (encrypted and signed) |
| T+\~2.5s | DCS freezes 20.45 USD, records 20.45 on Outstanding `outs_71ab...`, answers the network; the purchase succeeds at the till | β |
| Shortly after | DCS pushes the result receipt | `AUTHORISATION_RESULT` webhook: `approveFlag=A`, same `authId` / `outsId` |
| Next day | The authorization lands in the daily authorization report for reconciliation | Report row: `authId`, `approveFlag=A`, amount and merchant fields |
Three details worth noting:
* **The two amount pairs are not duplicates**: `acquirerAmount`/`acquirerCurrency` (3,000 JPY) is what the cardholder spent at the merchant; `amount`/`currency` (20.45 USD) is the settlement view converted at the network rate β freezing, settlement and reconciliation all follow the settlement view.
* **You only decide; you never move funds**: after you return `00`, DCS performs the freeze on the network side. If you return `01` (insufficient funds) or `11` (transaction not allowed), the flow ends at row three: the purchase fails at the till and no funds move.
* **The decision window is 2.5 seconds**: a timeout is treated as `21` (no response) and real-time authorizations are never retried, so keep your whole chain β verify, decrypt, check limits β inside the window.
At this point the money's life cycle is only half done β how the frozen 20.45 USD is actually debited on settlement day, and what happens when the settled amount differs from the frozen amount, is covered in [Authorization and settlement: every scenario](./auth-and-settlement).
***
## What authorization touches
* **Enterprise**: the partner makes the decision, and the hold or release is booked against the **enterprise security deposit** or against the cardholder limits the partner maintains.
* **Card**: every authorization request carries a `cardId`, and the card's activated, frozen or canceled state determines whether the authorization can succeed. Freezing a card triggers the release of its holds.
* **Outstanding**: the bridge between authorization and settlement. The authorization stage accumulates the held amount, and settlement draws it back down to zero. See [Authorization and settlement](./auth-and-settlement).
* **Transaction**: authorization comes first; a single transaction record can reference several `authId` values, and only approved authorizations ever produce a movement of funds.
***
## Next steps
* Want to run an authorization end to end in the sandbox without a card network? Use `POST /open-api/simulation/v1/fund-auth` from [Sandbox: simulating transactions](../sandbox/simulating-transactions) (input `cardId` / `authType` / `amount` / `currency`, returns `approved` plus `errorCode`).
* Want to know how the money settles afterwards? See [Authorization and settlement](./auth-and-settlement).
* Want to configure webhooks and their events? See [Webhooks: configuration](../webhooks/configuration).
* For decline reasons and error codes, see [Authorization declines and error codes](./decline-codes).
# Capture scenarios: partial, excess and multiple
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/transactions/capture-scenarios
Mapped to multi / partial / over-capture.
## π Guide
Once an authorization is approved, the merchant does not necessarily debit the authorized amount in one go. The final settled amount may be smaller than, equal to or larger than the authorized amount, and it may arrive in several instalments. Whichever happens, DCS aligns the money automatically on the same authorization, Outstanding and transaction model, so all you have to follow are the webhooks you receive at each step and the daily reconciliation file. You never have to correct differences yourself.
This page focuses on the three settlement deviations that come up most often and cause the most trouble, namely **partial capture, excess capture and multiple captures**, plus the closely related **under capture** and **refund** cases. All 13 combinations of authorization and settlement are in [Authorization and settlement: every scenario](./auth-and-settlement); field and enumeration definitions are in [Authorization](./authorization) and [Transaction records](./transaction).
***
## Start with the three objects
Fund reconciliation at DCS rests on three objects. Once they are clear, every scenario below reads at a glance:
| Object | Purpose | Key fields |
| - | - | - |
| **Authorization** | The limit decision taken before the transaction happens, placing or releasing a hold | `authId`, `authType`, `direction` |
| **Outstanding** | The unsettled balance, tracking held minus debited | `outsId`, `amount` |
| **Transaction** | The record the merchant's actual settlement produces, a debit or a refund | `transactionId`, `authIds`, `outsId`, `multiClearInd` |
* One authorization, or several sharing the same record, creates or adds to an Outstanding.
* Every settlement received draws the Outstanding `amount` **down** accordingly; reaching zero means the authorization is fully settled.
* Whenever the settled amount differs from the amount held, DCS **automatically back-fills a `FORCE_AUTH` authorization** to top up or release the difference, so the Outstanding can always reach zero. This step is transparent to the partner.
> **Who does what**: approve or decline at the authorization stage is the **partner's** real-time decision (see [Authorization](./authorization)); aligning funds at the settlement stage, back-filling differences and maintaining the Outstanding are all done automatically by **DCS**, and the results reach you in the daily transaction reconciliation file.
***
## Scenario 1: partial capture
**What it means**: the merchant ends up debiting only part of the authorized amount, as when a petrol station pre-authorizes 100 and the actual fill comes to 80. This is the most common deviation and is entirely normal from the partner's point of view.
**Action sequence** (authorized 100, actually settled 80):
| Step | direction | Amount | authType / identifier | Outstanding |
| - | - | -: | - | -: |
| 1. Authorization approved | OUTGOING | 100 | `authId1` (NORMAL) | `outsId1` = 100 |
| 2. Under-capture back-fill | INCOMING | 20 | `authId2` (FORCE\_AUTH) | 100 β 80 |
| 3. Settlement debit | OUTGOING | 80 | `transactionId1` | 80 β 0 |
**What the partner does**: once you read the settlement in the daily transaction reconciliation file, post the actual debited amount against its `transactionId` and release the surplus hold back to the cardholder. DCS releases the difference automatically through `FORCE_AUTH`, so you simply mirror what the reconciliation file tells you.
***
## Scenario 2: excess capture
**What it means**: the final settled amount is **larger** than the authorized amount, for example a restaurant authorizing 100 and settling 110 after a gratuity, or a cross-currency exchange-rate move. Card network rules let merchants settle above the authorized amount within defined tolerances.
**Action sequence** (authorized 100, settled 150):
| Step | direction | Amount | authType / identifier | Outstanding |
| - | - | -: | - | -: |
| 1. Authorization approved | OUTGOING | 100 | `authId1` (NORMAL) | `outsId1` = 100 |
| 2. Excess top-up hold | OUTGOING | 50 | `authId2` (FORCE\_AUTH) | 100 β 150 |
| 3. Settlement debit | OUTGOING | 150 | `transactionId1` | 150 β 0 |
Steps 2 and 3 complete inside the same database transaction: DCS sees that the settled amount (150) exceeds the Outstanding (100), back-fills an OUTGOING `FORCE_AUTH` for the difference of 50, and then settles the whole amount.
> **Limit note**: the excess consumes additional [enterprise security deposit](../../basic-concepts/fund-model) that the partner holds at DCS. A `FORCE_AUTH` true-up **does not go through the authorization forwarding decision**: it is driven by settlement and auto-approved, so the partner cannot decline at that step. To control excess exposure, keep a buffer in the limits you apply at the **authorization decision** stage.
***
## Scenario 3: multiple captures
**What it means**: one authorization is matched by **several settlements** from the merchant, as when an order ships in batches and each batch is settled separately. All the captures share one Outstanding, and the `multiClearInd` field tells you how far along the series you are.
**Action sequence** (authorized 100, settled as 60 plus 40):
| Step | direction | Amount | Identifier | `multiClearInd` | Outstanding |
| - | - | -: | - | :-: | -: |
| 1. Authorization approved | OUTGOING | 100 | `authId1` (NORMAL) | β | `outsId1` = 100 |
| 2. First capture | OUTGOING | 60 | `transactionId1` | `P` | 100 β 40 |
| 3. Final capture | OUTGOING | 40 | `transactionId2` | `F` | 40 β 0 |
The `multiClearInd` enumeration (see [Transaction records](./transaction)):
| Value | Meaning |
| - | - |
| `O` | Ordinary single capture, no series |
| `P` | **Not the last capture** of a series: draws the Outstanding down without checking the difference |
| `F` | The series is **complete**: the last capture, and the point at which the difference is checked and trued up |
**Illustration: how two captures look in the daily transaction reconciliation file** (fields taken from the [transaction records](./transaction) file schema; captures in one series share the same `authIds` and `outsId`, and `multiClearInd` distinguishes `P` from `F`):
```json theme={null}
[
{
"transactionId": "1109900000000000001",
"direction": "OUTGOING",
"outsId": "1109800000000000001",
"authIds": "1109700000000000001",
"amount": "60.000000000000000000",
"currency": "840",
"category": "RETAIL",
"multiClearInd": "P"
},
{
"transactionId": "1109900000000000002",
"direction": "OUTGOING",
"outsId": "1109800000000000001",
"authIds": "1109700000000000001",
"amount": "40.000000000000000000",
"currency": "840",
"category": "RETAIL",
"multiClearInd": "F"
}
]
```
> This is an **illustrative record** built from the fields of the transaction file (settlement has no request or response endpoint of its own and is delivered through the daily reconciliation file, see [Transaction records: how to get them](./transaction)). It shows only the key fields of the `P` record (not the last) and the `F` record (the last) under one authorization, and omits merchant, timestamps and the remaining fields.
> **What the partner does**: seeing `multiClearInd = P`, do not rush to treat the Outstanding as settled; only `F` means the authorization has settled in full. If a difference remains after `F`, DCS back-fills a `FORCE_AUTH` following the logic of scenarios 1 and 2.
***
## Related scenario: refund
**What it means**: the cardholder returns goods and the money comes back from the merchant to the card. A refund is an incoming transaction in the INCOMING direction.
* **With an original authorization**: DCS finds the original `authId` and `outsId` and creates an INCOMING refund transaction against the same Outstanding.
* **Without an original authorization** (forced or offline refund): DCS creates a new Outstanding (`amount = 0`) purely for linkage, creates the refund transaction and **does not back-fill an authorization**.
Refunds appear in the daily transaction reconciliation file as `direction = INCOMING` with `category = PAYMENT` (return or refund).
***
## Reconciliation at a glance
Put the three scenarios side by side and the pattern is obvious: **the Outstanding always converges to 0**.
| Scenario | Authorized | Settled | Difference handling | Outstanding, final |
| - | -: | -: | - | -: |
| Partial / under capture | 100 | 80 | INCOMING `FORCE_AUTH` releases 20 | 0 |
| Excess capture | 100 | 150 | OUTGOING `FORCE_AUTH` holds a further 50 | 0 |
| Multiple captures | 100 | 60 + 40 | `P` accumulates, `F` closes | 0 |
| Refund | β | INCOMING credit | New Outstanding created for linkage | 0 |
The standard approach when the partner reconciles: **use `outsId` to link authorization to settlement and `authIds` to link a transaction back to its authorizations** (with several authorizations, `authIds` is comma-separated, for example `1111,2222`), post the actual funds against `transactionId`, and finish by confirming the Outstanding has reached zero.
***
## Additional notes
* The daily transaction file uses full names for `category` (`RETAIL`, `CASH`, `CASH_FEES`, `PAYMENT` and so on); see [Report field dictionary](../reports/field-dictionary) for the complete enumeration. The `transactionType=R/C/Q/P` values on the authorization side belong to a different field, so do not mix them up.
* Excess capture differences are aligned automatically by DCS through `FORCE_AUTH`; to control excess exposure, keep a buffer at the **authorization decision** stage (see the limit note in scenario 2).
* The `OUTGOING + FORCE_AUTH` raised for a difference does travel over the authorization notification channel, but it is a notification plus a forced posting, not a fresh request for consent. Declining it does not cancel the posting.
***
## Next steps
* To see **all 13 combinations** of authorization and settlement (including incremental authorizations, reversals, expiry releases, status mismatch releases and withdrawal fees): [Authorization and settlement: every scenario](./auth-and-settlement)
* To check **fields and enumerations**: [Authorization](./authorization) and [Transaction records](./transaction)
* To **reproduce these captures** in the sandbox: [Simulating transactions](../sandbox/simulating-transactions)
# Authorization declines and error codes
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/transactions/decline-codes
Decline reasons; where the general and authorization error codes live.
## π Guide
Whether the platform you are integrating is an exchange, a wallet or something else, you can rely on one standardised set of error codes to locate a problem fast: every DCS API response uses the same format, and decline reasons under Partner-Managed come back as stable error codes that your own system can branch on and turn into user guidance. As a licensed issuer with its own BIN, DCS keeps error semantics consistent across authorization, settlement and reconciliation.
This page groups the error codes into buckets by scenario and, for each bucket, adds guidance on whether to retry, what the user should do and whether documents can be resubmitted. It helps you separate **system-level errors**, **parameter and business validation errors** and **declines under Partner-Managed**.
***
## The common response envelope
Every endpoint, successful or not, returns the same outer JSON envelope:
```json theme={null}
{
"code": "DAPI_CARD_ORDER_REF_ALREADY_EXISTS",
"message": "card order ref already exists",
"messageDetail": null,
"data": null
}
```
| Field | Type | Description |
| - | - | - |
| `code` | string | The result code: on success it is the success indicator described below, and on failure it is one of the `DAPI_*` codes enumerated on this page |
| `message` | string | A short English description of the error, written for developers and **not intended for display to end users** |
| `messageDetail` | object \| null | An optional display object (with `message` / `title` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`, all strings) meant for front-end guidance; may be `null` |
| `data` | object \| null | Business payload; usually `null` on failure |
**Judge success or failure by `code`** (`SYS_SUCCESS` on success, one of the `DAPI_*` codes on this page on failure). Do not branch on the `message` text, and do not assume `messageDetail` is always populated.
> **About the `DAPI_` prefix**: the codes on this page appear in the **`code` field of the response envelope** and always carry the `DAPI_` prefix. Two other families do **not**, because they appear inside the business payload rather than the envelope: the card order's `data.errorCode` (see [Card application error codes](../cards/card-order-codes)) and the KYC ticket's `data.errorCode` (see [KYC rejection codes](../kyc/kyc-reject-codes)). Pick the right form based on where the value comes from.
The division of labour between `messageDetail` and `message`, and whether `messageDetail` is mandatory on failure, is not yet settled in the documentation.
***
## Error codes by bucket
The tables below sort every `DAPI_*` code from the published error code list into buckets by scenario. Each bucket carries one set of handling guidance, and any code that needs a note of its own is called out inside its bucket.
### 1. System-level errors (the partner cannot fix these)
| ErrorCode | Message | Meaning |
| - | - | - |
| `DAPI_SYSTEM_ERROR` | System error | Internal system error |
| `DAPI_OPERATION_NOT_SUPPORT` | operation not support | The operation is not supported |
| `DAPI_PERMISSION_DENIED` | permission denied | Insufficient permissions |
* **Retry?** `DAPI_SYSTEM_ERROR` can be retried after a back-off; retrying `DAPI_OPERATION_NOT_SUPPORT` or `DAPI_PERMISSION_DENIED` is pointless.
* **What the user should do**: show the end user a message along the lines of "the system is busy, please try again later".
* **Resubmit documents?** Not applicable.
* **Partner action**: if it persists, contact DCS to investigate and confirm which capabilities your account has been enabled for.
### 2. Authentication and signature errors
| ErrorCode | Message | Meaning |
| - | - | - |
| `DAPI_API_KEY_EMPTY` | API Key cannot be empty | The API key header is missing |
| `DAPI_INVALID_API_KEY` | Api Key is invalid | The API key is invalid |
| `DAPI_SECRET_KEY_EMPTY` | Secret Key cannot be empty | The secret key is missing |
| `DAPI_SIGNATURE_ERROR` | signature error | Signature verification failed |
| `DAPI_CALCULATE_SIGNATURE_ERROR` | Failed to calculate hmac-sha256 | The server failed to compute the HMAC-SHA256 signature |
| `DAPI_TIMESTAMP_EMPTY` | request timestamp cannot be empty | The request timestamp is missing |
| `DAPI_TIMESTAMP_FORMAT_ERROR` | incorrectly formatted request timestamp | The timestamp format is wrong |
| `DAPI_TIMESTAMP_EXPIRED` | request timestamp has expired | The timestamp has expired (replay protection) |
| `DAPI_NONCE_ILLEGAL` | nonce illegal | The nonce is not valid (replay protection) |
* **Retry?** Yes, once the authentication headers are corrected; `DAPI_TIMESTAMP_EXPIRED` needs a fresh timestamp and a new signature.
* **What the user should do**: nothing, this is a server-side configuration issue on the partner side.
* **Resubmit documents?** Not applicable.
* **Partner action**: check the authentication headers and the signing rules item by item; see [Authentication and signing](../../getting-started/first-steps).
The nonce range is `[10000, 99999]`, and it is used together with the request timestamp for replay protection. Callers must not reuse a nonce, and should maintain a separate idempotency key for business write operations.
### 3. Parameter and permission validation errors
| ErrorCode | Message | Meaning |
| - | - | - |
| `DAPI_SYS_ILLEGAL_PARAM` | System illegal param | Invalid parameter |
| `DAPI_UPLOAD_BUSINESS_TYPE_ERROR` | Wrong business type | Wrong upload business type |
* **Retry?** Yes, once the parameters are corrected.
* **What the user should do**: nothing, this is a server-side integration issue.
* **Resubmit documents?** Not applicable.
* **Partner action**: correct the request body against the parameter table of the endpoint concerned.
### 4. User and enterprise errors
| ErrorCode | Message | Meaning |
| - | - | - |
| `DAPI_ENTERPRISE_NOT_FOUND` | enterprise not found | The enterprise does not exist |
| `DAPI_AUTHORISATION_NOT_FOUND` | `authorisation not found` | The authorization does not exist, or does not belong to your enterprise |
| `DAPI_CUSTOMER_NOT_FOUND` | customer not found | The user does not exist |
| `DAPI_CHANNEL_CUSTOMER_NOT_FOUND` | channel customer not found | The user does not exist on the channel side |
| `DAPI_CHANNEL_CUSTOMER_CREATE_FAILED` | channel customer create failed | Creating the user on the channel side failed |
| `DAPI_CUSTOMER_EMAIL_OR_PHONE_REQUIRED` | customer email or phone required | At least one of email or phone must be supplied |
| `DAPI_PHONE_INVALID` | invalid phone | The phone number format is wrong |
| `DAPI_EMAIL_INVALID` | invalid email | The email address is invalid |
| `DAPI_EMAIL_ALREADY_EXISTS` | email already exists | The email already exists |
| `DAPI_PHONE_ALREADY_EXISTS` | phone already exists | The phone number already exists |
| `DAPI_CUSTOMER_REF_NOT_UNIQUE` | customerRef not unique | The `customerRef` idempotency key is duplicated |
| `DAPI_ENTERPRISE_BALANCE_RECORD_NOT_EXISTS` | enterprise balance record not found | The enterprise balance record does not exist |
* **Retry?** Format errors (`*_INVALID`) can be retried once corrected; `*_ALREADY_EXISTS` and `*_NOT_UNIQUE` are idempotency or uniqueness conflicts, so use the existing record instead of creating another.
* **What the user should do**: if the email or phone is malformed or already taken, guide the user to change it or confirm ownership.
* **Resubmit documents?** Not applicable, these are creation-time validations.
* **Partner action**: `DAPI_CUSTOMER_REF_NOT_UNIQUE` usually comes from a duplicate submission; query the existing user instead.
### 5. KYC, ticket and EDD errors
| ErrorCode | Message | Meaning |
| - | - | - |
| `DAPI_TICKET_REF_REPEATED` | ticketRef repeated | The ticket idempotency key is duplicated |
| `DAPI_EXIST_ONGOING_TICKET_ERROR` | There is an ongoing work order | A ticket is already in progress |
| `DAPI_CARD_ORDER_KYC_INFO_INVALID` | kyc info invalid | The KYC information is invalid |
| `DAPI_CARD_ORDER_KYC_VERIFY_FAILED` | kyc verify failed | KYC verification did not pass |
| `DAPI_REQUEST_REJECTION_ERROR` | EDD application rejected | The enhanced due diligence (EDD) application was rejected |
| `DAPI_TICKET_APPLY_LIMIT_EXCEEDED` | Ticket application exceeds the limit | The KYC ticket application hit the intake rate limit |
* **Retry?** With `DAPI_EXIST_ONGOING_TICKET_ERROR`, wait for the existing ticket to close before starting another; with `DAPI_TICKET_REF_REPEATED`, use the existing ticket; `DAPI_TICKET_APPLY_LIMIT_EXCEEDED` needs a back-off before retrying, so **do not resend immediately**.
* **What the user should do**: when KYC does not pass, guide the user to supply what the rejection reason asks for (POI or POA). A failed POA does not require a new Sumsub token; use the supplementary endpoint directly.
* **Resubmit documents?** **Yes.** A KYC or EDD rejection normally lets the user supplement or update the materials and submit again.
* **Partner action**: for the KYC flow and its state machine, see [KYC flow](../../basic-concepts/compliance-kyc-flow).
KYC rejection reasons are returned through the KYC ticket's `data.errorCode` / `errorMessage`; the full mapping is in [KYC reject codes](../kyc/kyc-reject-codes), so the direction of a resubmission can only be inferred from the ticket status and its text.
### 6. Card order and card configuration errors
| ErrorCode | Message | Meaning |
| - | - | - |
| `DAPI_CARD_ORDER_REF_ALREADY_EXISTS` | card order ref already exists | The card order idempotency key is duplicated |
| `DAPI_CARD_ORDER_NOT_FOUND` | card order not found | The card order does not exist |
| `DAPI_CARD_ORDER_ID_INCORRECT_ERROR` | businessId is error, cardOrderId incorrect | The `cardOrderId` is incorrect |
| `DAPI_CARD_ORDER_PROFILE_NOT_FOUND` / `DAPI_CARD_PROFILE_NOT_FOUND` | card profile not found | The card profile does not exist |
| `DAPI_CARD_ORDER_CUSTOMER_INFO_INVALID` | customer info invalid | The user information in the card order is invalid |
| `DAPI_CARD_ORDER_CHANNEL_CUSTOMER_INVALID` | channel customer invalid | The channel-side user information is invalid |
| `DAPI_CARD_ORDER_CHANNEL_CARD_INVALID` | channel card invalid | The channel-side card information is invalid |
| `DAPI_CARD_ORDER_NOT_NEED_EXTRA_INFO` | card order does not need extra info | This card order needs no extra information |
| `DAPI_CARD_ORDER_VIRTUAL_TO_PHYSICAL_FAILED` | virtual to physical failed | Converting the virtual card to a physical card failed |
| `DAPI_CARD_PROFILE_NOT_SUPPORTING_COUNTRY_CODES` | Card profile not supporting country codes | The card profile does not support that country code |
| `DAPI_CARD_LAYOUT_NOT_FOUND` | card layout not found | The card layout does not exist |
| `DAPI_EXIST_NOT_FAILED_REPLACE_CARD_ORDER` | There are card replacement orders that are not accepted | An unfinished card replacement order exists |
| `DAPI_CARD_APPLY_LIMIT_EXCEEDED` | Card application exceeds the limit | The card application hit the intake rate limit, as when one user applies too often in a short window |
| `DAPI_REPLACE_CARD_APPLY_LIMIT_EXCEEDED` | card replacement application limit exceeded within 24 hours | Card replacement hit the 24-hour rate limit |
| `DAPI_VIRTUAL_TO_PHYSICAL_APPLY_LIMIT_EXCEEDED` | virtual to physical application limit exceeded within 24 hours | Virtual-to-physical conversion hit the 24-hour rate limit |
* **Retry?** Configuration errors (unsupported profile, layout or country code) need the configuration confirmed on the DCS side first, then a retry; for `*_REF_ALREADY_EXISTS`, use the existing order; `DAPI_CARD_APPLY_LIMIT_EXCEEDED` needs a back-off before retrying, so **do not resend immediately**.
* **What the user should do**: usually nothing. Where a region is not supported, tell the user this card type is not available in their location for now.
* **Resubmit documents?** Not applicable.
* **Partner action**: use `profileId` in customer-facing card issuance (the enterprise balance query uses the synonymous field `cardProfileId`). There is no online query endpoint today; DCS assigns the value offline. Do not use the internal alias `categoryId` in a customer-facing integration.
### 7. Card management and card status errors
| ErrorCode | Message | Meaning |
| - | - | - |
| `DAPI_CARD_NOT_FOUND` | card not found | The card does not exist |
| `DAPI_CARD_ID_INCORRECT_ERROR` | businessId is error, cardId incorrect | The `cardId` is incorrect |
| `DAPI_CARD_STATUS_NOT_ACTIVATED` | card status not activated | The card is not in the activated state |
| `DAPI_CARD_STATUS_NOT_FROZEN` | card status not frozen | The card is not in the frozen state |
| `DAPI_CARD_STATUS_NOT_WAITING_ACTIVE` | card status not waiting active | The card is not in the waiting-for-activation state |
| `DAPI_CARD_ACTIVATION_FAILED` | card activation failed | Card activation failed |
| `DAPI_CARD_NOT_EMBOSSED_ERROR` | Card not embossed: Activation not allowed before embossing | The physical card has not been embossed and cannot be activated before embossing |
| `DAPI_CARD_PIN_MUST_BE_FOUR_DIGIT_NUMBER` | The pin must be a four-digit number | The PIN must be four digits |
| `DAPI_CARD_TYPE_NOT_VIRTUAL` | card type is not virtual | The card is not a virtual card |
| `DAPI_CARD_TYPE_NOT_PHYSICAL` | card type is not physical | The card is not a physical card |
| `DAPI_ORIGINAL_CARD_STATUS_NOT_ACTIVATED` | original card status not activated | The original card is not activated (card replacement) |
| `DAPI_ORIGINAL_CARD_NOT_FOUND` | original card not found | The original card does not exist (card replacement) |
* **Retry?** State mismatch errors require moving the card into the right state before retrying; blind retries achieve nothing. The card state machine is in [Card management](../cards/card-management).
* **What the user should do**: on a PIN format error, prompt the user to enter four digits again; a card awaiting activation must be activated by the user first.
* **Resubmit documents?** Not applicable.
* **Partner action**: freeze and unfreeze share one endpoint, `POST /open-api/card/v1/freeze`, distinguished by the boolean `freeze` (`true` freezes, `false` unfreezes). Confirm the current card state before calling it.
### 8. Hosted page errors
| ErrorCode | Message | Meaning |
| - | - | - |
| `DAPI_GUIDANCE_LINK_TYPE_INVALID` | guidance link type invalid | The guidance link type is invalid |
| `DAPI_GUIDANCE_LINK_PRE_VERIFIED_INVALID` | guidance link preVerified invalid | The `preVerified` value on the guidance link is invalid |
* **Retry?** Yes, once the hosted page parameters are corrected.
* **What the user should do**: nothing, this is a parameter issue in how the partner generated the link.
* **Resubmit documents?** Not applicable.
***
## Declines under Partner-Managed (the important part)
Under Partner-Managed, **the spending limits belong to the partner and the partner makes the authorization decision in real time**. When a cardholder pays, DCS forwards the authorization request to the partner's authorization callback URL (`auth_url`) and the partner approves or declines. When an authorization is declined, the reason is returned in the `rejectReason` field of the `AUTHORISATION_RESULT` webhook and the daily authorization report. The complete enumeration:
| `rejectReason` | When it occurs |
| - | - |
| `DAPI_CARD_STATUS_NOT_ACTIVATED` | The card was not in the `ACTIVATED` state when the authorization entered processing (not activated, frozen, blocked or invalid); checked for both outgoing and incoming authorizations |
| `DAPI_CARD_CONSUME_FROZEN` | The user's KYC factor has expired, restricting spending (checked for outgoing authorizations only) |
| `DAPI_CARD_VELOCITY_LIMIT_CHECK_REJECT` | The velocity limit check did not pass |
| `DAPI_AUTH_ENTERPRISE_REJECT` | The partner returned an explicit decline in the authorization callback |
| `DAPI_AUTH_ENTERPRISE_TIMEOUT_REJECT` | The partner's authorization callback timed out without a response |
| `DAPI_AUTH_INTERNAL_REJECT` | Internal system decline: signature verification failed, the request failed or the response was invalid; the card could not be found; an incremental authorization or reversal could not find the original authorization, or the currency did not match; the hold or release flow failed abnormally |
| `DAPI_AUTH_INVALID_AUTHORIZATION` | Invalid authorization |
| `DAPI_AUTH_INSUFFICIENT_FUNDS_REJECT` | The preceding checks passed, but placing or releasing the hold on the enterprise ledger failed (insufficient balance) |
| `DAPI_SYSTEM_ERROR` | System error |
| `CARD_NOT_FOUND` | The card does not exist |
| `CARD_NOT_ACTIVE` | The card is not activated |
| `CARD_EXPIRED` | The card has expired |
| `CARD_RESTRICTED` | The card is restricted and cannot be used for this transaction |
| `ACCOUNT_RESTRICTED` | The account is restricted and cannot be used for this transaction |
| `AUTHENTICATION_FAILED` | Transaction authentication failed |
| `FRAUD_OR_RISK_REJECTED` | Declined by fraud or risk control rules |
| `LIMIT_EXCEEDED` | The transaction exceeds the applicable limit |
| `MERCHANT_RESTRICTED` | This transaction is not allowed at this merchant |
| `CURRENCY_NOT_SUPPORTED` | The transaction currency is not supported |
| `INVALID_TRANSACTION` | The transaction information is invalid |
| `DUPLICATE_TRANSACTION` | The transaction has already been processed or submitted |
| `ORIGINAL_TRANSACTION_ERROR` | The original transaction cannot be matched, or its status does not allow this transaction |
| `TRANSACTION_NOT_ALLOWED` | The transaction is not allowed |
| `ISSUER_OR_PARTNER_DECLINED` | Declined by the issuer or processor |
| `PROCESSING_ERROR` | Processing exception or system error |
The check order is fixed: **card status β KYC spending restriction β velocity limit β enterprise callback β fund hold**; when several conditions are hit at once, only the first one is returned.
* **Retry?** An authorization decline is the outcome of a single transaction; the cardholder has to start again at the merchant. **The partner must not retry the same authorization request server-side.**
* **What the user should do**: `DAPI_AUTH_ENTERPRISE_REJECT` usually means insufficient limit or a risk rule on the partner side, so the partner should explain it to the end user under its own rules (insufficient balance, over limit and so on).
* **Resubmit documents?** Not applicable.
* **Partner action**: for a timeout decline (`*_TIMEOUT_REJECT`), investigate the response latency of your authorization callback. In production the synchronous response window is **2.5 seconds**, and the partner must reply within it.
The authorization request and response fields and the signing rules are in [the authorization callback](./authorization); for where `rejectReason` sits in the report files, see [the authorization report](../reports/authorization-report). Finer business reasons such as insufficient balance, over-limit and restricted MCC are determined and recorded by the partner inside its own authorization logic.
***
## Next steps
Once you have identified the error code, return to [Transaction records](./transaction) to understand how Auth, Outstanding and Transaction relate, or go to [Sandbox simulation](../sandbox/simulating-transactions) to replay a declined authorization in the test environment.
# Transaction records
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/transactions/transaction
Where multiClearInd and the related enumerations live.
## Transaction records
Whether you need to reconcile funds line by line, reconstruct the full timeline of one purchase or satisfy a regulator's expectation that every movement of funds is traceable, the transaction record is the single source you need to rely on. As a licensed issuer, DCS persists every settled movement of funds as a tamper-proof record and delivers the complete set to the partner in a daily reconciliation file.
A transaction record captures **only real debits and credits**. It never carries the intermediate states of the authorization stage, such as holds and releases. It is the authoritative record of where the money finally went, and together with the authorization and the Outstanding it forms the complete fund lifecycle.
***
## Where it sits in the fund lifecycle
The authorization decides how much to hold, settlement decides how much is finally taken, and the transaction record is what settlement leaves behind. The three are linked by IDs:
* A transaction record links to its Outstanding through `outsId` and to one or more authorizations through `authIds` (several authorizations are comma-separated, for example `1111,2222`).
* With multiple authorizations, incremental authorizations, partial captures and similar combinations, authorizations and transaction records do not map one to one. All 13 combinations (normal settlement, incremental, reversal, excess and under capture, forced capture, cash withdrawal and the rest) are in [Authorization and settlement: every scenario](./auth-and-settlement).
***
## Transaction direction (direction)
The `direction` field separates two core cases:
| Value | Meaning | Description |
| - | - | - |
| `OUTGOING` | Outgoing | Funds leave the partner's balance account at the enterprise level, matching a cardholder payment; it closes the decision-to-execution loop opened by the hold at the authorization stage |
| `INCOMING` | Incoming | Funds arrive in the partner's balance account, matching a return or refund, which keeps every inflow traceable and reconcilable |
***
## Transaction category (category)
The `category` field classifies each record for business purposes and is the primary dimension the partner uses for analysis and cost accounting.
| Value | Name | Description |
| - | - | - |
| `RETAIL` | Ordinary purchase | Standard card purchase transaction |
| `RETAIL_FEES` | Purchase fees | Includes DCC fees, credit adjustments, card annual fees, replacement and re-issue fees |
| `CASH` | Ordinary cash withdrawal | ATM withdrawals and other cash transactions |
| `CASH_FEES` | Cash withdrawal fees | Includes cash withdrawal handling fees |
| `PAYMENT` | Return / refund | Returns, refunds and other incoming transactions |
| `CHARGEBACK` | Chargeback | Chargeback arising from a dispute raised by the cardholder |
> **How do you tell withdrawal principal from fee?** In a cash withdrawal, `category=CASH` is the principal and `category=CASH_FEES` is the fee, and each lands as its own record. The withdrawal fee goes through forced posting on its own; see [Authorization and settlement, scenario 13](./auth-and-settlement).
>
> **On cash withdrawal values**: the daily transaction file always uses the full `category` names (`RETAIL`, `CASH`, `CASH_FEES`, `PAYMENT` and so on). Codes such as `C`, `CF` and `R` are internal enumerations and never appear in the daily transaction file. The `R` / `C` / `Q` / `P` values of `transactionType` on the authorization side are a different dimension again, so do not mix them.
***
## Multiple capture indicator (multiClearInd)
One authorization may be settled several times, for instance when a merchant ships in batches or a hotel bills each line item at check-out. `multiClearInd` tells you where the current record sits in that series:
| Value | Meaning | What the partner should do |
| - | - | - |
| `O` | Ordinary single capture | One authorization, one capture; handle it as usual |
| `P` | Multiple captures, not the last one (Partial) | Draw the Outstanding down only; **do not** treat the authorization as fully settled |
| `F` | Multiple captures completed (Final) | The last one, and the point at which the difference between authorization and settlement is checked |
> With multiple captures, treat the arrival of the `F` record as the signal that the authorization has settled in full. Before `F` arrives, further `P` records may still turn up under the same `authIds`.
***
## How to get transaction records
Under Partner-Managed there is **no real-time endpoint for querying an individual transaction**. Transaction data is delivered through the **daily full reconciliation file**: each day DCS generates a complete file of that day's transaction records for every partner enterprise.
**Endpoint (fetch the download link)**
```
GET /open-api/enterprise/v1/settlement-file-url
```
| Parameter | Type | Required | Description |
| - | - | - | - |
| `fileType` | String | Yes | File type; pass `transaction` for transaction records (`authorisation` for the authorization file), max 20 |
| `fileDate` | String | Yes | File date in `yyyyMMdd`, for example `20251106`, max 8 |
**Response**: a string containing the file **download link** (a temporary S3 link with a short lifetime, so download it promptly).
> **Who does what**
>
> * **DCS**: generates the full transaction file every day and issues a temporary download link on request.
> * **Partner**: call this endpoint for the link, download the file, persist it, and check that the balance movements match your own business records.
>
> **About the files and links**: by default the system generates the previous day's authorization and transaction files on T+1, and they can be downloaded once the job status is `DONE`; the exact daily run time is configured on the job platform. A download link is valid for 120 seconds; once it expires, call the endpoint again for a fresh 120-second link. The underlying file is not deleted when a link expires.
For how download links are obtained in general and how the daily reconciliation cycle works, see [Transaction report](../reports/transaction-report).
***
## Fields in the transaction file
One record per line, fields separated by `>`, in this order:
| Field | Description | Notes |
| - | - | - |
| `transactionId` | Unique transaction record ID | The unique number of each movement of funds |
| `direction` | Transaction direction | `OUTGOING` (outgoing) or `INCOMING` (incoming) |
| `outsId` | Bill ID | Links to the matching Outstanding record |
| `authIds` | Related authorization IDs | Several are comma-separated, for example `1111,2222` |
| `enterpriseId` | Enterprise ID | Identifies the partner the movement belongs to |
| `customerId` | User ID | Identifies the cardholder behind the transaction |
| `cardId` | Card ID | The card the transaction happened on |
| `pan` | Card number | Only the first six and last four digits are shown, to keep the data safe |
| `category` | Transaction category | Matches the `category` enumeration above; see [Report field dictionary](../reports/field-dictionary) |
| `currency` | Currency | ISO 3-character currency code |
| `amount` | Amount | The amount involved, in the settlement currency |
| `acquirerCurrency` | Currency requested by the user | May differ from the actual settlement currency |
| `acquirerAmount` | Amount requested by the user | The amount in the requested currency |
| `cardAcceptorIdentification` | Merchant number | Identifies the merchant in the transaction |
| `cardAcceptorNameLocation` | Merchant details | Merchant name and address |
| `multiClearInd` | Capture indicator | `O` single, `P` multiple but not the last, `F` multiple completed |
| `merchantType` | Merchant category | The MCC, 4 digits |
| `createTime` | Creation time | When the record was created, `yyyy-MM-dd'T'HH:mm:ss+08:00` (UTC+8) |
| `modifyTime` | Update time | When the record was last modified, same format |
| `merchantCountryCode` | Merchant country code | 3-digit numeric country code |
| `originalTransactionId` | Original transaction ID | For a refund-type record, the `transactionId` of the original transaction being refunded; empty for non-refund records |
**Sample**
```text theme={null}
174047198631807650000309>OUTGOING>1109862324019138561>1109862318201638913>1095041241881513984>1108391061086846977>1108449591919689729>4382140000003562>CASH>702>1.000000000000000000>>0E-18>001584054110002>>null>5399>2025-03-21T07:20:45+08:00>2025-03-21T07:20:45+08:00>840>
174047198631807650000310>OUTGOING>1109864282771689473>1109864282775883776>1095041241881513984>1108391061086846977>1108449591919689729>4382140000003562>CASH_FEES>702>1.000000000000000000>>0E-18>001584054110002>>null>5399>2025-03-21T07:24:25+08:00>2025-03-21T07:24:25+08:00>840>
```
> The two lines above happen to illustrate a cash withdrawal: the first, `CASH`, is the withdrawal principal and the second, `CASH_FEES`, is the withdrawal fee. Both records share the same card number and the same merchant. Nothing follows the trailing `>` on either line, because non-refund records carry no `originalTransactionId`.
To make mapping into your own database easier, the first `>`-delimited line above is expanded below into an equivalent structured record, with field names matching the table above one for one:
```json theme={null}
{
"transactionId": "174047198631807650000309",
"direction": "OUTGOING",
"outsId": "1109862324019138561",
"authIds": "1109862318201638913",
"enterpriseId": "1095041241881513984",
"customerId": "1108391061086846977",
"cardId": "1108449591919689729",
"pan": "4382140000003562",
"category": "CASH",
"currency": "702",
"amount": "1.000000000000000000",
"acquirerCurrency": "",
"acquirerAmount": "0E-18",
"cardAcceptorIdentification": "001584054110002",
"cardAcceptorNameLocation": "",
"multiClearInd": "null",
"merchantType": "5399",
"createTime": "2025-03-21T07:20:45+08:00",
"modifyTime": "2025-03-21T07:20:45+08:00",
"merchantCountryCode": "840",
"originalTransactionId": ""
}
```
transaction records are **delivered only as the `>`-delimited text file shown above**. This JSON is a field-by-field illustration and does not imply that an endpoint returns JSON. In this sample `multiClearInd` is literally `null`; outside cash withdrawals it carries `O`, `P` or `F`.
***
## Next steps
* To understand every combination from hold to debit: [Authorization and settlement: every scenario](./auth-and-settlement)
* To understand how the daily reconciliation files are generated and downloaded: [Transaction report](../reports/transaction-report)
* For the authoritative definition of the transaction category enumeration: [Report field dictionary](../reports/field-dictionary)
# Create a user
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/users/create-customer
How-to task page: how a partner creates a user (customer), the first business action after integration. It sets out what DCS and the partner each do, then gives the minimum request/response and the next step.
## π Guide
Creating a user is the first step of a Partner-Managed integration. Whether your end users come from an exchange, a wallet or a project's own app, a single call registers them in DCS and lays the foundation for the KYC verification and card issuance that follow. As a licensed issuer with its own BIN, DCS assigns every user a globally unique user ID (`customerId`) at this step and stores their basic contact details securely.
### Mental model: the user is the anchor for everything that follows
Under Partner-Managed the partner owns the limits and makes the authorization decision, but the user as an identity is still managed centrally by DCS. Once a user is created, the `customerId` you receive runs through every action afterwards:
Store the `customerId` returned on success carefully: it is the required key that ties the later endpoints together.
### Who does what
| Step | Who | Action |
| - | - | - |
| 1 | You | Prepare the user's mobile number and email, and generate a `customerRef` that is unique on your side |
| 2 | You | Call the create-user endpoint with that information |
| 3 | DCS | Validate completeness and format, enforce uniqueness, assign the `customerId`, store the data securely |
| 4 | You | Save the returned `customerId` and move on to KYC and card issuance |
### Prerequisites
* You hold the Enterprise `ApiKey` / `SecretKey` and build the request headers as required by the authentication rules (including the HMAC-SHA256 signature and the nonce). If that is not set up yet, start with [First steps](../../getting-started/first-steps) and [Authentication](../../integration-resources/authentication).
* You send the request with `Content-Type: application/json`.
### Endpoint
**`POST /open-api/customer/v1/create-customer`**
#### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `phoneCountryCode` | string | Required | Country code of the mobile number as a 2-letter ISO country code (such as `SG`, `MY`, `KR`, `TH`, `AU`, `JP`, `TW`, `VN`, `PH`). Length is fixed at 2 |
| `customerRef` | string | Required | Your idempotency key for the user; you generate it and it must be unique on your side. Maximum length 50. Submitting the same `customerRef` again resolves to the same user instead of creating a duplicate |
| `phone` | string | Either/or | Mobile number in plaintext, with the country prefix and starting with `+`, length 6-30, for example `+6591234567`. Supply either this or `phoneEncryption` |
| `phoneEncryption` | string | Either/or | Mobile number as ciphertext (AES-GCM). Supply either this or `phone`; if both are sent, **the ciphertext takes precedence** |
| `email` | string | Either/or | Email in plaintext, valid email format, maximum length 100. Supply either this or `emailEncryption` |
| `emailEncryption` | string | Either/or | Email as ciphertext (AES-GCM). Supply either this or `email`; if both are sent, **the ciphertext takes precedence** |
| `encryptionIV` | string | Conditional | IV for AES-GCM, used to decrypt `phoneEncryption` / `emailEncryption`. Required whenever you use either ciphertext field |
> **Plaintext vs ciphertext (please read)**
> The mobile number and the email are each a choice between plaintext and ciphertext:
>
> * To send them in plaintext, fill in `phone` / `email` directly.
> * To add a layer of encryption on top of the transport (recommended if you are sensitive about PII), fill in `phoneEncryption` / `emailEncryption` instead and supply `encryptionIV` along with them. The algorithm is **AES-GCM**.
> * If you send both the plaintext and the ciphertext of the same field, the system **uses the ciphertext**.
> **Field note**
> `phoneCountryCode` (the country code) and `phone` (which starts with `+` and carries the country prefix) both convey the country. Both are currently required: `phoneCountryCode` acts as the 2-letter ISO region for libphonenumber and `phone` is the full E.164 number, and the two must refer to the same country.
#### Minimum request example (plaintext)
```json theme={null}
{
"phoneCountryCode": "SG",
"phone": "+6591234567",
"email": "user@example.com",
"customerRef": "ext-user-0001"
}
```
#### Minimum request example (ciphertext)
```json theme={null}
{
"phoneCountryCode": "SG",
"encryptionIV": "",
"phoneEncryption": "",
"emailEncryption": "",
"customerRef": "ext-user-0001"
}
```
#### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"customerId": "C1234567890",
"status": "SUCCEED",
"createTime": "2026-06-17T10:00:00+08:00",
"modifyTime": "2026-06-17T10:00:00+08:00"
}
}
```
#### Response fields (`data`)
| Field | Type | Description |
| - | - | - |
| `customerId` | string | Unique user ID assigned by DCS; it is the key that links the later KYC and card issuance endpoints, so store it carefully |
| `status` | string | Creation status, for example `SUCCEED`. β οΈ The literal spelling is `SUCCEED` (not the conventional `SUCCESS`/`SUCCEEDED`), so match on the value as returned |
| `createTime` | string | Creation time in the format `yyyy-MM-dd'T'HH:mm:ssXXX` (with the time zone offset, such as `+08:00`) |
| `modifyTime` | string | Time of the last update, same format |
#### Common response envelope
Every endpoint returns the same envelope, `{ code, message, messageDetail, data }`:
| Field | Description |
| - | - |
| `code` | Business status code; `SYS_SUCCESS` on success, otherwise see the error code page |
| `message` | Short status description |
| `messageDetail` | Structured prompt object (may carry `title` / `message` / `type` / `icon` / `action` / `linkTitle` / `linkUrl`), meant mainly for front-end guidance; usually `null` on success |
| `data` | Business payload; its shape depends on the endpoint |
**How to read success**: treat `code == "SYS_SUCCESS"` as the single source of truth for success, rather than inferring it from whether `messageDetail` is populated or from the wording of `data.status`. For error codes and troubleshooting, see [Authorization declines and error codes](../transactions/decline-codes).
## Next steps
Once the user is created and you hold the `customerId`, the next step is to [apply for KYC](../kyc/apply-kyc) for that user. After KYC passes you can [apply for a card](../cards/card-issuing).
# Query a user
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/users/query-customer
How-to task page: query a user's latest status by user ID (or by the idempotency key you used when creating them). Organized as task steps plus a minimum request/response and who does what, with the field table and the common response envelope filled in from the source, and a next-step call to action at the end.
## π Guide
Whether you are an exchange, a wallet or a platform, you can look up a user's latest details at any time after creating them: the user ID, the current status and the creation and update timestamps. That lets you reconcile the user record in your own system and decide whether they can move on to card issuance.
The query is initiated by **the partner**, and **DCS** returns the user's current snapshot.
### Prerequisites
* You hold the Enterprise `ApiKey` / `SecretKey`. If you have not received them yet, see [First steps](../../getting-started/first-steps).
* You have already created the user. If not, complete [Create a user](./create-customer) first.
### Steps
1. Prepare the user identifier: either the `customerId` returned when the user was created, or the idempotency key `customerRef` you supplied at creation (you).
2. Build the request headers as described in [Authentication](../../integration-resources/authentication) and call the query endpoint (you β DCS).
3. Read the user's `status` and the timestamp fields from the response `data`, and reconcile them in your own system (you).
### Query user details
**`GET /open-api/customer/v1/detail`**
#### Query parameters
| Parameter | In | Type | Required | Description |
| - | - | - | - | - |
| `customerId` | query | string | No | User ID (returned by DCS when the user was created) |
| `customerRef` | query | string | No | Your idempotency key for the user (set by you when the user was created) |
at least one of `customerId` and `customerRef` must be supplied; with neither present the user cannot be located.
#### Request example
```
GET /open-api/customer/v1/detail?customerId=100000000123
Host:
Content-Type: application/json
X-DAPI-API-KEY:
X-DAPI-TIMESTAMP: 1743044911331
X-DAPI-NONCE: 12345
X-DAPI-SIGN:
```
> For how to build the full set of authentication headers (`X-DAPI-API-KEY` / `X-DAPI-TIMESTAMP` / `X-DAPI-NONCE` / `X-DAPI-SIGN`), see [Authentication](../../integration-resources/authentication).
#### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"customerId": "100000000123",
"status": "SUCCEED",
"createTime": "2025-01-01T12:00:00+08:00",
"modifyTime": "2025-01-02T09:30:00+08:00"
}
}
```
#### `data` fields
| Field | Type | Description |
| - | - | - |
| `customerId` | string | User ID |
| `status` | string | User status, sample value `SUCCEED` (spelled as the source endpoint returns it; standardising on `SUCCESS`/`SUCCEEDED` is under consideration, see the note below) |
| `createTime` | string | Creation time in the format `yyyy-MM-dd'T'HH:mm:ss+08:00` |
| `modifyTime` | string | Time of the last update in the format `yyyy-MM-dd'T'HH:mm:ss+08:00` |
> **About the `status` value**: the sample value `SUCCEED` means the user is ready and can move on to the next stage. Documentation of the full set of status values is planned; until then, match on the values you actually receive.
### Common response envelope
Every `/open-api/` endpoint returns the same envelope, with this endpoint's payload in `data`:
| Field | Type | Description |
| - | - | - |
| `code` | string | System-level return code; `SYS_SUCCESS` on success |
| `message` | string | Return message (such as `success`) |
| `messageDetail` | object \| null | Copy meant for display to the end user (`title`/`message`/`type`/`icon`/`action`/`linkTitle`/`linkUrl`); usually `null` on success |
| `data` | object | Business payload, see the field table above |
"success" on this endpoint is signalled by the system-level `code` (`SYS_SUCCESS`), while the business outcome is carried by `data.status`. Use `code` to decide whether the call itself succeeded and the fields inside `data` to decide the user's business status; `messageDetail` is returned only when there is something to show the end user, and is usually `null` on success.
For how error codes are grouped, whether they are retryable and how to handle them, see [Error codes](../transactions/decline-codes).
## Next steps
Once you have confirmed that the user's status is healthy, you can [apply for KYC](../kyc/apply-kyc) for them, or go straight to [card issuance](../cards/card-issuing).
# Send an OTP
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/users/send-otp
How-to task page: how a partner calls POST /open-api/customer/v1/send-otp to deliver an OTP it generated itself to a user over the DCS SMS or email channel.
How-to task page: how a partner calls `POST /open-api/customer/v1/send-otp` to deliver an OTP it generated itself to a user over the DCS SMS or email channel.
This endpoint **neither generates nor verifies** the OTP. Generating and checking the OTP are the partner's job; DCS only delivers it.
## π Guide
Whether you already run your own SMS gateway or would rather reuse the DCS delivery channels, this endpoint sends a verification code to the user's mobile number or email address: you generate the OTP and hand the delivery to us. The endpoint is for Partner-Managed partners and is typically used at the points in your own flows where you need to reach a user by SMS or email.
this endpoint **does not generate or verify OTPs**. You generate the OTP, keep it and check it yourself; DCS only sends the OTP content out over the `channel` you specify.
### End-to-end flow (who does what)
| Step | Who | Action |
| - | - | - |
| 1 | You | Generate a 6-digit OTP and keep it in your own system so you can check it later |
| 2 | You | Choose the delivery channel (`SMS` or `EMAIL`) and prepare the matching recipient (mobile number or email address) |
| 3 | You | Generate a unique `otpSendRef` (your idempotency key) and call this endpoint |
| 4 | DCS | Verify your credentials and signature, then hand the OTP to the downstream SMS or email gateway for that channel |
| 5 | DCS | Apply rate limiting per recipient and record the delivery result |
| 6 | You | Read the response `data.status` to see whether the submission succeeded |
### Endpoint
**`POST /open-api/customer/v1/send-otp`**
The request headers (authentication and signature) are the same as for every other `/open-api/` endpoint: send `X-DAPI-API-KEY`, `X-DAPI-TIMESTAMP`, `X-DAPI-NONCE` and `X-DAPI-SIGN`, with `Content-Type: application/json`. See [Authentication and signing](../../integration-resources/authentication) for the details.
#### Request body fields
| Field | Type | Required | Constraints | Description |
| - | - | - | - | - |
| `otpSendRef` | string | Yes | Max 50 | Idempotency key for this send, **globally unique**; a repeat is rejected |
| `channel` | string | Yes | Max 20 | Delivery channel, one of `SMS` / `EMAIL` |
| `language` | string | Yes | β | Language of the message, one of `en` (English) / `zh` (Chinese) |
| `encryptionIV` | string | Conditional | Max 32 | IV for AES-GCM; **required whenever any encrypted field is populated**, and used to decrypt the ciphertext fields below |
| `mobileEncryption` | string | Conditional | Max 128 | Mobile number as ciphertext (AES-GCM); when `channel=SMS`, supply either this or `mobile` |
| `mobile` | string | Conditional | 6-30 | Mobile number in plaintext; when `channel=SMS`, supply either this or `mobileEncryption`; with the country prefix it must start with `+`, for example `+6591234567` |
| `emailEncryption` | string | Conditional | Max 256 | Email as ciphertext (AES-GCM); when `channel=EMAIL`, supply either this or `email` |
| `email` | string | Conditional | Max 100 | Email in plaintext; when `channel=EMAIL`, supply either this or `emailEncryption` |
| `otpEncryption` | string | Conditional | Max 64 | OTP as ciphertext (AES-GCM); supply either this or `otp` |
| `otp` | string | Conditional | Length 6 | Verification code in plaintext, 6 digits; supply either this or `otpEncryption` |
**Required combinations per channel:**
| `channel` | Required field combination |
| - | - |
| `SMS` | (`mobile` or `mobileEncryption`) + (`otp` or `otpEncryption`) |
| `EMAIL` | (`email` or `emailEncryption`) + (`otp` or `otpEncryption`) |
#### Plaintext and ciphertext
The recipient fields (`mobile` / `email`) and the OTP itself (`otp`) can each be sent in plaintext or as ciphertext. For data this sensitive we **strongly recommend ciphertext**:
* **Ciphertext**: fill in `mobileEncryption` / `emailEncryption` / `otpEncryption` using **AES-GCM**, with your **SecretKey** as the key and the IV passed in `encryptionIV`. As soon as any ciphertext field is populated, `encryptionIV` becomes mandatory.
* **Plaintext**: fill in `mobile` / `email` / `otp` and send the values as they are.
* If you supply both the plaintext and the ciphertext of the same item, **the ciphertext wins**.
#### Minimum request example (SMS, plaintext)
```json theme={null}
{
"otpSendRef": "otp-20260617-0001",
"channel": "SMS",
"language": "zh",
"mobile": "+6591234567",
"otp": "123456"
}
```
#### Minimum request example (EMAIL, ciphertext)
```json theme={null}
{
"otpSendRef": "otp-20260617-0002",
"channel": "EMAIL",
"language": "en",
"encryptionIV": "",
"emailEncryption": "",
"otpEncryption": ""
}
```
#### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": "success",
"messageDetail": null,
"data": {
"otpSendRef": "otp-20260617-0001",
"status": "SUCCESS"
}
}
```
> **Common response envelope**: every `/open-api/` endpoint returns `{ code, message, messageDetail, data }`. `code` is the business code (`SYS_SUCCESS` on success), and `messageDetail` carries human-readable copy (including `title`/`type`/`action`/`linkUrl` and so on) when there is something to show the end user, otherwise it may be empty. Decide success or failure from `code`, never from the HTTP status code or the wording of `message` alone.
#### Response fields (`data`)
| Field | Type | Description |
| - | - | - |
| `otpSendRef` | string | Echoes the idempotency key of this send |
| `status` | string | Delivery status, one of `SUCCESS` / `FAILED` |
> `status=SUCCESS` only means that **DCS handed the OTP to the downstream SMS or email gateway successfully**; it is **no guarantee that the user will receive it** (the carrier or the mailbox provider may still get in the way). When `status=FAILED`, the response `code` tells you why the downstream delivery failed.
### Rate limits
To keep the SMS and email channels from being abused, DCS rate-limits sends **to the same recipient under the same Enterprise**:
| Dimension | Limit |
| - | - |
| Same (`channel`, recipient) | Only one successful send per 60 seconds |
Exceeding the limit returns `OTP_SEND_TOO_FREQUENT`. Ask the user to **try again shortly** rather than resending immediately.
### Idempotency
`otpSendRef` is your own business idempotency key, and **the same `otpSendRef` cannot be sent twice**:
* First request: the OTP goes out and that `otpSendRef` is recorded.
* Repeat request (same `otpSendRef`): returns `OTP_SEND_REF_NOT_UNIQUE`.
Make sure every send uses a unique `otpSendRef` (deriving it from your business reference plus a timestamp works well).
### Common error codes
| Error code | Meaning | Retryable | What to do |
| - | - | - | - |
| `OTP_SEND_REF_NOT_UNIQUE` | `otpSendRef` has already been used | No (change the key) | Send again with a fresh, unique `otpSendRef` |
| `OTP_SEND_TOO_FREQUENT` | Too many sends to the same recipient | Yes (back off) | Ask the user to try again shortly and resend once the cooldown has passed |
| `ILLEGAL_ARGUMENT_ERROR` | Invalid parameter | No (fix the request) | Check the required combination, the lengths and the enum values |
| `SYSTEM_ERROR` | System error | Yes | Back off and retry; if it keeps failing, contact the customer success team |
For the full set of business and error codes, see [Authorization declines and error codes](../transactions/decline-codes).
### Prerequisites
* You hold the Enterprise **ApiKey / SecretKey** and have completed the signing and whitelist setup (see [First steps](../../getting-started/first-steps) and [Authentication and signing](../../integration-resources/authentication)).
* If you send ciphertext, your system needs **AES-GCM** support, with the Enterprise SecretKey as the key.
## Next steps
Once OTP delivery works end to end, carry on with [creating a user](./create-customer) and the KYC and card issuance flows that follow.
# Webhook configuration
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/webhooks/configuration
Fills in self-service configuration and HMAC signature verification.
## π Guide
Whether you are an exchange, a wallet or a platform, webhooks let you mirror the key DCS events, card lifecycle, transaction authorization, KYC and tickets, into your own system in real time, without the cost of polling. As a licensed issuer, DCS signs every webhook it sends, so verifying the signature is all it takes to confirm that an event really came from DCS.
This page covers two things: **how to get DCS to push events to your callback URL** (configuration and whitelisting), and **how to verify that each push is genuine** (the HMAC signature). For the event types and the field structure of each event, see [Events and data structures](./events-and-schema).
## 1. Configure the callback URLs
**Who does it**: webhook configuration is currently done by **DCS** on your behalf; there is no self-service configuration endpoint yet. Please send the information below through your integration group chat or your business contact.
There are two callback URLs to give DCS:
| URL | Purpose | What is pushed |
| - | - | - |
| `webhookUrl` | Business event notifications | Card order status, card status, KYC tickets, tickets, authorization results (asynchronous) |
| `authUrl` | Authorization forwarding notifications | Real-time authorization requests (DCS waits synchronously for you to return approve or decline) |
> `authUrl` carries the **real-time decision** under Partner-Managed and is protected by RSA two-way signing and encryption, which makes it a channel entirely separate from the business webhooks (HMAC-signed) described on this page. For the security model of `authUrl`, see [Real-time authorization](../transactions/authorization); for how to hand over the callback URLs and the public key, see [First steps](../../getting-started/first-steps).
### Requirements for the callback URLs
* They must use `https` and resolve to an IP that is **reachable from the public internet**.
* They must be registrable in the DCS webhook **whitelist**, so send DCS your **egress addresses** as well (separately for sandbox and production).
* Expose a **stable, long-lived** path; URLs that change often lead to missed deliveries.
**Who does it (you)**: DCS operates a webhook whitelist. Sandbox and production are two independent configurations, so submit the addresses for each; otherwise you will find a gap on go-live day.
## 2. Verify the signature (HMAC-SHA256)
To keep the channel secure, DCS includes `X-Signature` in the headers of **every** webhook request. The signature is produced with `HmacSHA256` using your `secretKey` as the key. Before you process the payload you must recompute the signature over the **raw payload as received** with the same algorithm and confirm that it matches `X-Signature`; only then is the request proven genuine and untampered.
### Request headers
| Header | Description |
| - | - |
| `X-Signature` | The HMAC-SHA256 signature DCS computed over the raw payload (hex encoded) |
| `Content-Type` | Always `application/json` |
**Always compute over the raw bytes**: the HMAC has to be computed over the **raw payload string** DCS sent. If you parse the JSON and regenerate it, field order, whitespace and escaping can all change, the signature will differ and verification will fail. Read and buffer the raw body for verification **before** you parse it. DCS sends compact JSON with the fields at every level sorted lexicographically by full field name, and new fields may be added in the future β none of this affects verification as long as you verify the raw payload. See [Events and schema Β· Notes](./events-and-schema#notes).
### Verification example
The Java example DCS provides (computing the HMAC-SHA256 of the payload):
```java theme={null}
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
class HmacSignature {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
String secretKey = "secret_key";
String webhookPayloadStr = "webhook_Payload_Str"; // the raw payload string as received
byte[] hmacSha256;
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKeySpec);
hmacSha256 = mac.doFinal(webhookPayloadStr.getBytes());
// compare against the X-Signature request header
System.out.println(Hex.encodeHexString(hmacSha256));
}
}
```
The check passes when `Hex(HmacSHA256(secretKey, raw payload))` and `X-Signature` are **identical character for character**. Use a constant-time comparison to avoid a timing side channel.
> `secretKey` comes from the same source as `apiKey`; together they are the integration credentials DCS issues to you. For how to retrieve them securely for production, see [Authentication](../../integration-resources/authentication). Treat `secretKey` as a secret, keep it server-side only, and never ship it to a front end or a client app.
## 3. Response and consumption conventions
So that no event is lost and none is processed twice, we recommend the following conventions:
| Item | Convention | Notes |
| - | - | - |
| Success response | Return HTTP `200` (any 2xx counts as success) | Acknowledge quickly on receipt, then process the business logic asynchronously, to avoid timing out |
| Idempotent deduplication | Key on `webhookId` | The same event may be delivered more than once, so check `webhookId` for duplicates before processing |
| Retry on failure | Up to 3 attempts for general events | Driven by a scheduled job, with the backoff interval following that job's configuration; the real-time `AUTHORISATION` channel is never retried and a timeout is declined outright |
| Out-of-order arrival | Order by `notificationTime` | The network and retries can deliver events out of order, so use the timestamp in the payload to reconstruct the true sequence |
> For the meaning of the shared fields such as `webhookId` and `notificationTime`, see [Events and data structures](./events-and-schema#common-envelope-structure).
**Avoid returning an HTML or plain-text error page**: return a structured response even when processing fails (or acknowledge with `2xx` and retry asynchronously); it makes troubleshooting and redelivery tracking far easier.
## Next steps
Once signature verification works end to end, move on to [Events and data structures](./events-and-schema), parse each event's `data` field by `webhookType`, and wire it into your business logic.
# Events and data structures
Source: https://docs.thedecard.com/en/partner-managed/how-to-use/webhooks/events-and-schema
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.
> **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`); for the values, see [Authorization declines and error codes](../transactions/decline-codes#declines-under-partner-managed-the-important-part) |
| `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).
# Authentication Guide
Source: https://docs.thedecard.com/en/partner-managed/integration-resources/authentication
Everything in one place: apiKey/secret/signing (P0 revision) + secure retrieval of production credentials + RSA key generation + the three IP whitelists
## π Guide
Whether you are an exchange, a wallet or a platform, once DCS hands you a key pair you can call every `/open-api/` endpoint with one uniform signing rule. This page gets your authentication working in a single pass. As a licensed issuer with its own BINs, DCS requires a verifiable origin and replay protection on every API call.
Three topics are covered here: **HMAC signing** for API calls, **secure retrieval** of production credentials, and the **RSA public key** used for authorization notifications.
***
## 1. Credentials and Request Headers
### What DCS gives you
| Field | Description | Provided by |
| :- | :- | :- |
| `apiKey` | Globally unique identifier for calling `/open-api/`; used for identification and request-origin tracing | DCS |
| `secretKey` | The key used to compute the request signature. **Keep it secure and never disclose it to any third party** | DCS |
> For sandbox `apiKey`/`secretKey`, contact the DCS team. Production credentials must go through the [secure retrieval process](#3-secure-retrieval-of-production-credentials).
### Headers required on every request
Every partner call to `/open-api/` must carry the following HTTP headers. A JSON request body additionally requires `Content-Type: application/json`, which the public API reference omits but whose absence causes a `415` error:
| Header | Required | Description |
| :- | :- | :- |
| `Content-Type` | REQUIRED | Fixed value `application/json`. βNeither the published header table nor the curl sample states it, but any JSON request body must carry it, otherwise the backend falls back to the Spring default and returns `415 Unsupported Media Type`. This is the single most common "copied the docs, still fails" issue in the first mile of integration |
| `X-DAPI-API-KEY` | REQUIRED | The `apiKey` issued by DCS |
| `X-DAPI-TIMESTAMP` | REQUIRED | Request timestamp (Unix epoch milliseconds, timezone-independent), used for replay protection |
| `X-DAPI-NONCE` | REQUIRED | Random number in the range `[10000, 99999]`; generate a fresh one for every request |
| `X-DAPI-SIGN` | REQUIRED | HMAC-SHA256 signature computed per the rules below (lowercase hexadecimal) |
`secretKey` is used only to compute the signature locally. Never transmit it in cleartext in a request header.
***
## 2. Request Signing (HMAC-SHA256)
### Signing rule
Use the **HmacSHA256** algorithm with `secretKey` as the key, and sign the concatenated string:
```
X-DAPI-SIGN = HmacSHA256( apiKey + timestamp + nonce + payload , secretKey )
```
The output is a lowercase hexadecimal string.
**What each segment of the concatenated string is:**
| Segment | Value |
| :- | :- |
| `apiKey` | Same as `X-DAPI-API-KEY` |
| `timestamp` | Same as `X-DAPI-TIMESTAMP` (millisecond timestamp) |
| `nonce` | Same as `X-DAPI-NONCE` |
| `payload` | **GET** requests: the URL-encoded query string; **all other methods**: the raw request body |
```text theme={null}
if method is GET:
payload = url.encodedQuery() // e.g. cardOrderRef=14
else:
payload = body // raw JSON request body string
```
> The signing algorithm is fixed to HmacSHA256. Always use it.
### Replay protection (who does what: DCS verifies / partner generates)
* **TIMESTAMP**: DCS accepts requests within a **5-second** window by default; anything later is rejected outright. Keep your local clock synchronized to UTC.
* **NONCE**: Generate a random number in `[10000, 99999]` for every request and never reuse one. Partners should still use idempotency keys for business write operations and avoid replaying an identical request inside the 5-second window.
### Signing implementation examples
**JavaScript (Postman pre-request script)**
```javascript theme={null}
const CryptoJS = require('crypto-js');
const apiSecret = '{your secret key}';
const apiKey = '{your api key}';
// GET uses the query string; other methods use the request body. Concatenate whichever applies.
const queryParams = '{your query param}'; // e.g. cardOrderRef=14
const requestBody = '{your request body}'; // raw body for POST/PUT
const nonce = 10010;
const timestamp = Date.now().toString();
const dataToSign = apiKey + timestamp + nonce + queryParams + requestBody;
const signature = CryptoJS.HmacSHA256(dataToSign, apiSecret).toString(CryptoJS.enc.Hex);
```
**Java**
```java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
public final class HmacSignature {
private static final String HMAC_SHA256 = "HmacSHA256";
public static String getSignature(String apiSecret, String data) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(apiSecret.getBytes(), HMAC_SHA256);
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(keySpec);
byte[] hmac = mac.doFinal(data.getBytes());
return Hex.encodeHexString(hmac);
}
}
```
### Full request example (GET)
```bash theme={null}
curl --location 'https://{domain}/open-api/card-order/v1/detail?cardOrderRef=14' \
--header 'Content-Type: application/json' \
--header 'X-DAPI-API-KEY: 697EA72DACF742F280943DAB211E6C2B' \
--header 'X-DAPI-TIMESTAMP: 1743044911331' \
--header 'X-DAPI-NONCE: 10100' \
--header 'X-DAPI-SIGN: b979d2ea5c70187ddac7d7a5937c40c87a58f765594b740e596d25fca96f2dcb'
```
> Here `payload = cardOrderRef=14` (the GET query string). To validate your implementation, first reproduce the same `X-DAPI-SIGN` from a set of `apiKey/timestamp/nonce/payload` values supplied by DCS, then go live.
### Uniform response envelope
Every `/open-api/` endpoint returns the same envelope:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": { }
}
```
| Field | Description |
| :- | :- |
| `code` | Business status code. `SYS_SUCCESS` on success; a specific error code on failure (see the error code page) |
| `message` | Short message, usually `null` on success |
| `messageDetail` | Detail object, usually `null` |
| `data` | Business payload |
> Judge success by `code == "SYS_SUCCESS"`. On failure, `code` carries the specific error code (see the error code page).
***
## 3. Secure Retrieval of Production Credentials
To prevent production `apiKey`/`secretKey` values from leaking in transit, **production credentials are never sent to you directly**. They are collected through a one-time secure retrieval flow.
> The sandbox does not use this flow; just contact the DCS team for sandbox credentials.
### The flow (and who does what)
1. **Partner provides**: one secure email address plus the IP that will perform the retrieval.
* The email address receives the retrieval instructions; that IP is added to the credential-retrieval whitelist.
2. **DCS sends**: an email arrives at the secure address containing a temporary secure link that is **valid exactly once**.
3. **Partner retrieves**: concatenate `extractUrl` with `extractSecretKey` and **run it from the machine at the designated IP** to receive the `apiKey`/`secretKey`.
### Fields in the email
| Field | Type | Description |
| :- | :- | :- |
| `expireTime` | string | Validity period of the retrieval security code |
| `extractSecretKey` | string | Credential retrieval security code |
| `extractUrl` | string | Credential retrieval URL |
| `howToUse` | string | Usage instructions |
| `notes` | string | Reminder that the link can be used for one retrieval only |
**Success response**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"messageDetail": null,
"data": {
"expireTime": "2024-10-21T16:52+08:00[Asia/Shanghai]",
"extractSecretKey": "4f72b1e9c49e4ac1bbc2cd12b5e44993",
"extractUrl": "https://{domain}/internal/open-api/v1/secret-extract/",
"howToUse": "Please concatenate the url with the secret-key and execute it on the specified machine.",
"notes": "This link is only valid for one AKSK extraction, if the content is not properly accessed, the AKSK may have been compromised, please contact us promptly."
}
}
```
**Failure response**
```json theme={null}
{
"code": "ERROR-CODE",
"message": "simple describe, see error-code list",
"messageDetail": null,
"data": null
}
```
**If the retrieval fails**: the link may already have been consumed while still within its validity period, which means the credentials may be compromised. Contact your DCS business contact immediately and repeat the email flow to have them reissued.
***
## 4. RSA Public Key (authorization notifications only)
Authorization forwarding notifications (DCS to the partner `auth_url`) protect sensitive data such as transaction amount and currency with **RSA-2048 mutual signing plus encryption**. This is a mechanism entirely separate from the HMAC used for API calls. Authorization notifications add no AES/IV layer: the business fields are encrypted as a whole using segmented RSA, and the signature algorithm is `SHA256withRSA`.
* **HMAC**: used when a partner calls `/open-api/` endpoints (sections 1 and 2 of this page).
* **RSA**: used when DCS pushes authorization notifications to a partner, protecting sensitive fields along the authorization path.
The partner generates its own RSA key pair, hands the **public key** (`external_public_key`) to DCS, and keeps the private key.
### How to generate (who does it: partner)
```bash theme={null}
# 1. Generate a 2048-bit RSA private key
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# 2. Derive the public key from the private key
openssl rsa -pubout -in private_key.pem -out public_key.pem
# 3. Strip the header, footer and line breaks to get the single-line public key string to submit to DCS
cat public_key.pem | sed '/BEGIN/d; /END/d' | tr -d '\n'
```
The single-line string from step 3 is your `external_public_key`. Provide it to DCS together with `auth_url`.
### DCS RSA public key (provided by DCS)
Partners use the DCS RSA public key to verify DCS notifications and to encrypt their responses.
* **Sandbox**:
```text theme={null}
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7FDHTQgX8vDvBZMlOOQm
xqoEw/gvk2bRQFa8UooAq4fWkChBpWM+bi0vu3T00rxqenbFDZoz5QE+/Mhhk+4X
Nvo47rSnhP6Nhkjivo8S8wUuHZbk20EwaqVVVpjYVvdFWSeLHkumofdj5HdbgzUI
nqslVdtAGMYJCzH0KlfWnVUuW2BnIk0wro68rNk0bqqbCkcnLLXpyElKxZCAvvo4
LRYcS81jiG78a6EzgbzLjuNvYH5vOj9dh49QevTBCyBUC35JVt85VC3RUGny9CKL
dn3x9XOLKRdndYEWGU5ZJVZdioNgWhU40TBVjveqbORSs+ApAZOWTovrKvABthXp
hQIDAQAB
-----END PUBLIC KEY-----
```
* **Production**: contact the DCS team.
### Java example for encryption, decryption and signing
The signature algorithm is `SHA256withRSA` with a 2048-bit key, and encryption and decryption run in blocks (245-byte encryption block / 256-byte decryption block):
```java theme={null}
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
private static final String ALGORITHM = "RSA";
private static final int KEY_SIZE = 2048;
private static final int MAX_ENCRYPT_BLOCK = KEY_SIZE / 8 - 11; // 245
private static final int MAX_DECRYPT_BLOCK = KEY_SIZE / 8; // 256
// Encrypt with the public key (segmented)
public static String encrypt(String data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = doSegment(cipher, data.getBytes(), MAX_ENCRYPT_BLOCK);
return Base64.getEncoder().encodeToString(encryptedBytes);
}
// Decrypt with the private key (segmented)
public static String decrypt(String encryptedData, PrivateKey privateKey) throws Exception {
byte[] dataBytes = Base64.getDecoder().decode(encryptedData);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = doSegment(cipher, dataBytes, MAX_DECRYPT_BLOCK);
return new String(decryptedBytes);
}
// Rebuild a public key from a Base64 string
public static PublicKey parsePublicKey(String base64PublicKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64PublicKey);
return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(keyBytes));
}
// Rebuild a private key from a Base64 string
public static PrivateKey parsePrivateKey(String base64PrivateKey) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(base64PrivateKey);
return KeyFactory.getInstance(ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
}
// Sign with the private key (SHA256withRSA)
public static String sign(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data.getBytes());
return Base64.getEncoder().encodeToString(signature.sign());
}
// Verify with the public key
public static boolean verify(String data, String sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes());
return signature.verify(Base64.getDecoder().decode(sign));
}
private static byte[] doSegment(Cipher cipher, byte[] data, int maxBlock) throws Exception {
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
while (offset < inputLen) {
int blockLen = Math.min(inputLen - offset, maxBlock);
out.write(cipher.doFinal(data, offset, blockLen));
offset += blockLen;
}
return out.toByteArray();
}
}
```
> For the full encryption, decryption and signature verification details of authorization notifications, see [Authorization Forwarding and Encryption](../how-to-use/transactions/authorization).
***
## 5. IP Whitelists (three types, all required)
DCS whitelists every direction of traffic. Partners must give DCS their static egress IPs **separately for sandbox and production**, covering these three directions:
| Whitelist | Direction | Purpose | Provided by |
| :- | :- | :- | :- |
| API call whitelist | Partner to DCS | Egress address used to call `/open-api/` endpoints | Partner |
| Webhook whitelist | DCS to the partner `webhook_url` | Egress address that receives card order, transaction and other event notifications | Partner |
| Authorization forwarding whitelist | DCS to the partner `auth_url` | Egress address that receives authorization forwarding requests | Partner |
> Configure sandbox and production separately and never mix them. The credential-retrieval IP (section 3) and the API call IP may differ; tell DCS which purpose each one serves.
***
## Next steps
With authentication working, head to [Quickstart](../getting-started/quickstart) to issue your first card, or go back to [First Steps](../getting-started/first-steps) to confirm that provisioning, callbacks and whitelists are in place. For the RSA encryption and decryption details of authorization notifications, see [Authorization Forwarding and Encryption](../how-to-use/transactions/authorization).
# Compliance and KYC
Source: https://docs.thedecard.com/en/corporate-card/basic-concepts/compliance-kyb-kyc
KYB for companies, KYC for employees: review flows and the shared application state machine, resubmission under the same application ID, global uniqueness and sanctions screening, custodians, freeze sources and card data protection.
## π Guide
Compliance and security are the entry ticket to card issuing, and DCS carries the heaviest parts for you: licensing and BIN ownership, the PCI data environment, KYB / KYC due diligence, risk control and sanctions screening. This page explains the due-diligence machinery and the hard rules you need to follow.
## KYB and KYC: who reviews what
| Subject | Due diligence | What is reviewed | Where to submit | Result notification |
| - | - | - | - | - |
| Company | KYB (business due diligence) | Legal name, registration number and other corporate credentials | [Company onboarding](../how-to-use/company-onboarding) | `COMPANY_CREATED` / `COMPANY_REJECTED` |
| Employee | KYC (individual due diligence) | Name screening | [Creating employees](../how-to-use/employee-onboarding) | `CUSTOMER_CREATED` / `CUSTOMER_REJECTED` |
Both reviews are **asynchronous**: the endpoint returns only the application ID + `PENDING` synchronously (accepted β approved); the final state arrives over webhooks, with the query endpoints as the polling fallback. The pre-issuing risk review is asynchronous too, and all share the same [application state machine](./states-and-freezing).
## The right path after a rejection: resubmit
After a KYB / KYC rejection, correct the key fields (company: legal name / registration number; employee: name) and **call resubmit under the same the application ID** β never reapply under a new external ID. Three hard rules:
* Resubmission is allowed only while the state is `REJECTED`; in-flight or approved applications return `STATUS_CONFLICT`;
* The partner-side external ID (`companyApplyRef` / `customerApplyRef`) cannot change;
* **An employee name change re-triggers KYC review**: with a KYC already in flight the call returns `KYC_IN_REVIEW` β retry after the result lands.
## Global uniqueness and occupation
The following details are **globally unique within one partner**, and once taken they are **never released, even after a rejection**:
| Dimension | Conflict code |
| - | - |
| Company email | `EMAIL_DUPLICATE` |
| Company registration number | `COMPANY_REGISTRATION_DUPLICATE` |
| Employee email | `EMAIL_DUPLICATE` |
| Employee phone (area code + number, unique across companies) | `PHONE_DUPLICATE` |
To change any of them, go through resubmit or the employee-update endpoint β never reapply under a new external ID.
## Sanctions screening and custodians
* **Sanctions screening**: a shipping-address country on the sanctions list returns `COUNTRY_SANCTIONED` (checked at creation and on address updates).
* **Custodian**: regulation requires every company card to name an active (ACTIVE) employee of that company as its custodian. The custodian is contacted when something is wrong with the card, and completes verification when a purchase triggers a 3DS challenge.
## The two sources of freezes
Freezes come from two sources: **partner-initiated freezes** can be lifted self-service over the API; **risk / regulatory / judicial freezes** cannot, and go through support. On granularity, freezing a card stops only that card, while freezing an employee makes every card under them decline; the 5 freeze capability domains are defined in [State machines and freezing](./states-and-freezing).
## Data security and card data protection
* **Channel security**: DCS issues each partner an AK / SK pair. The SK never appears in any request in clear text; it is used only to compute the HMAC-SHA256 signature locally, and requests carry a timestamp and nonce against replay. Your identity is established by the gateway from the AK signature check; any institution identifier carried in the body is ignored.
* **Card data protection**: regular query endpoints never return the full PAN β only the first 6 digits (BIN) and the last 4. The full PAN / CVV2 / expiry are returned only by the secure card endpoint, AES-GCM encrypted, and only to allowlisted PCI DSS-certified partners; partners without PCI DSS can integrate the DCS hosted secure card page. Details in [Secure card details](../how-to-use/secure-card-details).
## Next steps
* Endpoint details for company onboarding and resubmission: [Company onboarding and review](../how-to-use/company-onboarding)
* Endpoint details for employee creation and rename-triggered re-review: [Creating employees](../how-to-use/employee-onboarding)
# Holders and the funding model
Source: https://docs.thedecard.com/en/corporate-card/basic-concepts/identity-and-funding
"Who holds the card" and "which account pays" are two independent axes: the holder-to-owner mapping is decided by the balance mode; both funding modes are fund-first, spend-second.
## π Guide
Whether your customers prefer centralised control or budget isolation, you model their accounts and funds on the same set of endpoints. This page explains the two axes that run through every endpoint β the holder and the funding owner β and how the two funding modes differ.
## Holder vs funding owner
"Who the card is issued to" and "which account the money leaves" are two independent axes.
| | Holder | Funding owner |
| - | - | - |
| Meaning | Who the card is issued to / who it belongs to | Which account holds the money / whose balance moves |
| Parameters | `subjectType` + `subjectId` | `subjectType` + `subjectId` |
| Values | `COMPANY` / `CUSTOMER` | `COMPANY` (company pool) / `CARD` (dedicated-balance card) |
| Used by | Card management endpoints (apply / query / list) | Fund endpoints (balance / deposits / transfers / records) |
The mapping between the two is decided by the card profile, `cardProfileId` β the platform guarantees that a given profile always draws on the same funding source:
| Subject subjectType | Card profile cardProfileId | β Funding owner subjectType |
| - | - | - |
| `COMPANY` | `SHARED` (company pool) | `COMPANY` |
| `CUSTOMER` | `SHARED` (company pool) | `COMPANY` (still the pool, not the employee) |
| `CUSTOMER` | `DEDICATED` (own balance) | `CARD` (the card holds its own balance) |
| `COMPANY` | `DEDICATED` (own balance) | `CARD` |
## The funding model
**Fund first, spend second**: the company pool and dedicated-balance cards are each funded by the customer's bank transfer to their own receiving virtual account (VA), with arrival notified over webhook. A SHARED card holds no balance of its own β spending debits the company pool directly; a DEDICATED card holds its own balance and stops when it runs out. The balance acts as the real-time authorization gate.
| | Company pool mode (SHARED) | Dedicated balance mode (DEDICATED) |
| - | - | - |
| Who holds the balance | The company, shared across cards | Each card holds its own, earmarked |
| Spending debits | The company pool directly | The card's own balance, until exhausted |
| Receiving VA | Company-level, created at onboarding | Card-level, opened per card on demand |
| Valid transfer endpoint | Yes | Yes |
| Fits | Unified control, centralised bookkeeping | Budget isolation, capping, recoverable balances |
Fund transfers run between the company pool and dedicated-balance cards β same currency, no FX, closed loop within one company: allocate (pool β card) to assign budget, sweep back (card β pool) to recover balance. Cards in company pool mode hold no balance.
## Next steps
* The application, entity and card state machines: [State machines and freezing](./states-and-freezing)
* The operating path for deposits, transfers and reconciliation: [Funding and reconciliation](../how-to-use/funding-and-reconciliation)
# Limits and statements
Source: https://docs.thedecard.com/en/corporate-card/basic-concepts/limits-and-billing
Velocity rules: many-to-many binding with the strictest value winning per dimension; statements issued automatically per calendar month, with the settled statement as the final word.
## π Guide
"Who can spend, how much, and on what" is expressed by velocity rules; "how much was spent, and when it is due" is expressed by statements. This page explains the binding semantics of rules and the billing conventions of statements.
## Velocity limits
Spend control is implemented through standalone velocity-rule objects. Rules are created by the partner, belong to the company level, and apply to cards (CARD) or employees (EMPLOYEE) through targets β a many-to-many relationship. When an object has several rules bound, the system takes the minimum on every dimension, so the strictest limit always wins. The full endpoint definitions are in the API reference.
* **Control dimensions.** Purchase and cash-withdrawal limits are independent groups; each can set per-transaction and daily / monthly / quarterly / yearly caps on amount and count per currency, overlaid with allow/deny lists for transaction currency, MCC and region.
* **Binding and caps.** Rules can be bound at card application time through `ruleIds` (at most 5); a company-funded card (SHARED β paid from company funds) must have at least 1 rule bound. A single card and a single employee can each carry at most 5 rules.
* **Rule status.** A rule switches between `ACTIVE` and `INACTIVE`: an `INACTIVE` rule takes no part in transaction checks but keeps its existing targets.
## Statements and cycles
DCS issues a statement automatically per calendar month for every billing entity β no partner trigger needed. Statement data is pulled over the API.
* **Cycle and key dates**: the cycle is the calendar month (format `yyyy-MM`); each month's statement day issues the previous month's statement; any debt must be repaid by the due date (`paymentDueDate`).
* **Settled vs open statements**: a settled statement (`SETTLED`) is fixed and contains only posted transactions; the open statement (`OPEN`, `statementId` null) is a live summary of the current cycle that includes unsettled authorization holds, so its spend total falls back as transactions clear β expected behavior. Treat the settled statement as final.
* **Billing entity**: the balance mode decides who gets billed: the company pool (SHARED) yields one statement per company; dedicated balances (DEDICATED) yield one statement per card.
## Next steps
* The operating path for binding limits to cards or employees: [Setting spend limits](../how-to-use/spend-limits)
* A transaction's full journey from hold to posting: [The transaction life cycle](./transaction-lifecycle)
# Overview
Source: https://docs.thedecard.com/en/corporate-card/basic-concepts/overview
The corporate-card object model at a glance: the partner β company β employee structure, the core objects and ID conventions that run through every endpoint, and the reading path across the concept pages.
## π Guide
The concepts below decide how you model accounts, funds and controls for your customers. This page gives you the object landscape and the ID conventions; each concept page then follows one thread in depth.
## The three-tier identity model
Your business is organized as partner β company β employee: the partner is you (one API integration); a company is your business customer (KYB onboarding, funding pool); an employee is the end cardholder (issued cards after KYC). Cards can be issued to a company (a custodian is required) or to an employee.
## Core objects and ID conventions
Every ID in the contract is a JSON string, shaped as follows:
| Object | ID | Shape | Notes |
| - | - | - | - |
| Company | `companyId` | UUID-shaped string | Issued once KYB passes; every later endpoint uses it |
| Employee | `customerId` | UUID-shaped string | Issued once KYC passes |
| Card / application / event | `cardId` / `companyApplyId`, `customerApplyId`, `cardApplyId` / `webhookId` | 19-digit snowflake string | **Transmit as strings** β numeric literals get silently truncated |
| Rule / transfer / statement / transaction | `ruleId` / `transferId` / `statementId` / `transactionId` | Business strings | Pass through verbatim |
| Partner-side idempotency keys | `xxxRef` | `^[A-Za-z0-9_-]+$`, β€64 | Generated and persisted by you; reuse the same value on retry |
The meaning of a polymorphic ID (`subjectId` / `subjectId` / `subjectId`) is determined by the type field in the same structure, never inferred from its shape.
## Two orthogonal axes
"Who holds the card" (the holder) and "which account pays" (the funding owner) are independent axes, mapped to each other by the card profile `cardProfileId` β the key to understanding card types and the fund endpoints. See [Holders and the funding model](./identity-and-funding).
## Reading path
| Concept page | The question it answers |
| - | - |
| [Holders and the funding model](./identity-and-funding) | Who holds the card, which account pays, and how to choose between the two funding modes |
| [State machines and freezing](./states-and-freezing) | How applications, companies, employees and cards move through their states, and how freezing takes effect |
| [The transaction life cycle](./transaction-lifecycle) | A purchase's full journey from authorization hold to posting |
| [Compliance and KYC](./compliance-kyb-kyc) | What KYB / KYC review, what to do on rejection, and which details are globally unique |
| [Limits and statements](./limits-and-billing) | How spend control is expressed, and which statement is final |
## Next steps
* Start with the first thread of the object model: [Holders and the funding model](./identity-and-funding)
* Or go hands-on and issue your first card: [Quickstart](../getting-started/quickstart)
# State machines and freezing
Source: https://docs.thedecard.com/en/corporate-card/basic-concepts/states-and-freezing
The four state sets in one place: the application state machine shared by companies, employees and cards; the merged company/employee status view; the 7-state card machine; and the set semantics of the 5 freeze capability domains.
## π Guide
Onboarding, employee creation and issuing each have their own state machine; freezing is a capability-domain switch layered on top. This page covers them all, so the status fields in later endpoints map straight onto it.
## The application state machine
Company, employee and card applications share one state machine with three states: `PENDING` / `SUCCEED` / `REJECTED`. After a rejection, resubmit under the same the application ID (see [Compliance and KYC](./compliance-kyb-kyc)).
## The company / employee state machine
Presented as one merged status field: a life-cycle state (`ACTIVE` / `TERMINATED`) overlaid with behavioral states (`FROZEN` / `SUSPENDED` / `RESTRICTED`). Whether an entity can open cards or spend is judged by whether it is `ACTIVE`.
## The card state machine
7 states, shared by virtual and physical cards. Every state change pushes a card-status-change notification (activation is pushed separately as `CARD_ACTIVATED`); see the webhook event table in the API reference for event names.
## Freeze capability domains
5 domains, applicable to companies and employees. Adding a state freezes, removing it unfreezes β idempotent set semantics; restricted states written by risk, regulatory or judicial action cannot be lifted this way.
| Domain code | What freezing disables |
| - | - |
| `ACCOUNT_FROZEN` | The account as a whole (login, password change, phone / email change, MFA binding, account closure) |
| `CASH_IN_FROZEN` | Deposits / top-ups |
| `CASH_OUT_FROZEN` | Withdrawals / payouts / remittances |
| `PAYMENT_FROZEN` | Payments / spending (including card payments) |
| `CARD_FROZEN` | Card management actions (applying, activating, changing PIN) |
Freezing has two granularities: freezing a card stops only that card, while freezing an employee makes every card under them decline. The endpoints are in [Company status and alerts](../how-to-use/company-maintenance) and [Employee information and status](../how-to-use/employee-maintenance).
## Next steps
* A transaction's full journey from hold to posting: [The transaction life cycle](./transaction-lifecycle)
* KYB / KYC subjects, resubmission and uniqueness rules: [Compliance and KYC](./compliance-kyb-kyc)
# The transaction life cycle
Source: https://docs.thedecard.com/en/corporate-card/basic-concepts/transaction-lifecycle
A transaction's full journey from authorization hold (UNPOSTED) to settlement posting (POSTED), plus the fund semantics of reversals, refunds and chargebacks, and debt.
## π Guide
Understanding a transaction's full journey from authorization to settlement helps you reconcile correctly and present transactions and balances to your customers accurately. A transaction passes first through authorization, then through clearing / settlement, typically hours to days apart.
## Two stages and four branches
* **Authorization.** When the cardholder pays, DCS checks the entity's status, the velocity rules and the available balance in real time; on approval it holds the amount and consumes the corresponding limit quota. No money has actually left yet β the transaction appears on the statement as an authorization hold (`postStatus` = `UNPOSTED`).
* **Clearing / settlement.** When the merchant later clears, DCS debits the actual settled amount and posts it (`postStatus` = `POSTED`); the transaction lands on the statement for its cycle. The settled amount can differ from the authorized amount (tips, FX differences, or the merchant charging less).
* **Reversal / void.** If an authorization is canceled before settlement, DCS releases the held amount along the original path and restores the consumed limit quota, leaving no residue.
* **Refund / chargeback.** A refund is processed as a separate posting in the opposite direction and does not retroactively unwind historical limit counts; a chargeback is tracked through the dispute process to closure.
* **Debt.** Under delayed settlement or merchant over-capture, the available balance can be driven negative; the account then enters a debt state and the partner is notified by event. The partner repays on schedule.
## Next steps
* The payloads of the authorization and settlement events, and how to consume them: [Real-time authorization and settlement](../how-to-use/realtime-authorization)
* How limits take part in authorization, and how statements read: [Limits and statements](./limits-and-billing)
# Error code dictionary
Source: https://docs.thedecard.com/en/corporate-card/customer-success/error-codes
The complete set of error codes for the Corporate Card open API: 11 general codes, 52 business codes, and the rules for branching on them.
## π Guide
This page is the complete set of error codes for the Corporate Card open API: **11 general codes and 52 business codes**. Use it as a quick reference β the trigger conditions and request details for each code live in the guide pages named in the "Returned by" column.
Three rules for handling errors:
1. **Branch on the error code, never parse `message`**: `message` is for humans and its wording will change; the code is the contract.
2. **Never judge success by HTTP status**: business failures currently return HTTP 500 across the board. The only reliable test is `code == "SYS_SUCCESS"`.
3. **`SYS_ERROR` and `DAPI_SERVICE_UNAVAILABLE` mean the outcome is unknown**: for money-moving calls (transfers, card applications) retry with the same idempotency key or reconcile by query β do not treat them as failures outright. If you meet a code not listed here, treat that call as failed; never treat it as success.
Batch endpoints (rule bind / unbind) report errors on two levels: **batch-level** errors in the top-level `code`, and **per-item** failure reasons in `data.failTargets[].errorCode`, drawn from the same code set. See [Binding and unbinding](../how-to-use/binding-and-unbinding).
## General error codes
Independent of any specific endpoint; any call may return them. The body is the standard envelope `{ code, message, data }`.
### Response envelope
| Code | HTTP | Description |
| - | - | - |
| `SYS_SUCCESS` | 200 | Success |
| `SYS_ERROR` | 500 | System error (outcome unknown β retry money-moving calls with the same idempotency key) |
### Authentication and gateway checks
| Code | HTTP | Description |
| - | - | - |
| `DAPI_API_KEY_EMPTY` | 401 | API key missing or invalid |
| `DAPI_TIMESTAMP_EXPIRED` | 401 | Timestamp outside the window |
| `DAPI_NONCE_ILLEGAL` | 401 | Illegal nonce |
| `DAPI_NONCE_DUPLICATE` | 401 | Duplicate nonce |
| `DAPI_SIGNATURE_ERROR` | 401 | Signature verification failed |
| `DAPI_IP_NOT_ALLOWED` | 401 | IP not on the allowlist |
| `DAPI_PERMISSION_DENIED` | 401 | Access denied |
| `DAPI_PARAM_INVALID` | 400 | Invalid parameters (including structural gaps such as "none of the five control groups configured") |
| `DAPI_SERVICE_UNAVAILABLE` | 500 | Downstream unavailable (outcome unknown β handle like `SYS_ERROR`) |
## Business error codes
| Code | Description | Returned by |
| - | - | - |
| `APPLY_DUPLICATE` | Duplicate application (idempotency key already exists) | Company application, employee application, card application, virtual-to-physical |
| `APPLY_REJECTED_USE_RESUBMIT` | Application was rejected; use the resubmit endpoint instead | Company application, employee application |
| `COMPANY_REGISTRATION_DUPLICATE` | Company registration number already exists | Company application |
| `CURRENCY_NOT_ALLOWED` | Currency not on the allowed list | Company application, opening a VA |
| `EMAIL_DUPLICATE` | Email already exists | Company application, employee application |
| `APPLY_NOT_FOUND` | Application not found | Application query and resubmit (company, employee), card application query |
| `STATUS_CONFLICT` | Status conflict | Resubmit and restriction updates (company, employee) |
| `COMPANY_INVALID` | Company missing, not ACTIVE, or not owned by this partner | Most endpoints under a company |
| `COUNTRY_SANCTIONED` | Sanctioned country | Employee application, employee address update, virtual-to-physical |
| `PHONE_DUPLICATE` | Phone number already exists | Employee application |
| `CUSTOMER_INVALID` | Employee missing, not ACTIVE, or not under the given company | Employee query, the four update endpoints, restrictions |
| `KYC_IN_REVIEW` | KYC under review; updates temporarily blocked | Employee information updates |
| `CARD_LIMIT_EXCEEDED` | Card count limit exceeded | Card application |
| `CARD_PROFILE_INVALID` | Card profile unavailable | Card application |
| `CARD_RULE_REQUIRED` | A velocity rule is required for this card profile | Card application |
| `CARD_RULE_INVALID` | Rule missing, not owned by this company, or not ACTIVE (one code for all three, to prevent probing other companies' rules) | Card application |
| `CARD_RULE_DUPLICATE` | The subject is already bound to this rule | Card application, rule binding |
| `CARD_RULE_CURRENCY_NOT_MATCH` | No overlap between the rule's currencies and the card's chargeable currencies | Card application, rule binding |
| `CARD_RULE_LIMIT_EXCEEDED` | The subject already has the maximum number of rules (5) | Card application, rule binding |
| `SUBJECT_INVALID` | Subject missing, not ACTIVE, or not under the given company | Subject checks in the funds and velocity domains (including per-item results of batch calls) |
| `CARD_INVALID` | Card invalid | Most card-domain endpoints |
| `CARD_NOT_DEDICATED` | Not a dedicated-balance card | Opening a VA |
| `PCI_NOT_CERTIFIED` | No sensitive-card-data entitlement | Retrieving sensitive card data |
| `CARD_CONVERT_IN_PROGRESS` | A virtual-to-physical conversion is already in flight | Virtual-to-physical |
| `CARD_NOT_VIRTUAL` | Not a virtual card | Virtual-to-physical |
| `LIMIT_PHYSICAL_EXCEEDED` | Physical card limit exceeded | Virtual-to-physical |
| `SHIPPING_ADDRESS_REQUIRED` | Shipping address missing | Virtual-to-physical |
| `SHIPPING_CONTACT_REQUIRED` | Shipping contact phone missing | Virtual-to-physical |
| `SHIPPING_ADDRESS_INVALID` | Shipping address invalid or not usable for this card (batch shipping only) | Virtual-to-physical |
| `CARD_NOT_SHIPPED` | Card not yet shipped | Physical card activation |
| `CARD_NOT_ACTIVATED` | Card not activated | Set PIN |
| `CARD_STATE_INVALID` | The card state does not allow this operation | Most card and funds APIs |
| `PIN_DECRYPT_FAILED` | PIN ciphertext could not be decrypted | Set PIN |
| `PIN_RULE_VIOLATION` | PIN violates the PIN rules | Set PIN |
| `PIN_VERIFY_FAILED` | Identity verification factors do not match | Set PIN |
| `CURRENCY_MISMATCH` | The two sides of the transfer use different currencies | Funds transfer |
| `INSUFFICIENT_FUNDS` | Insufficient balance | Funds transfer |
| `SUBJECT_RESTRICTED` | Subject restricted (frozen / restricted state) | Funds transfer |
| `TRANSFER_DUPLICATE` | Duplicate transfer (idempotency key already exists) | Funds transfer |
| `TRANSFER_NOT_FOUND` | Transfer not found | Transfer query |
| `BALANCE_ALERT_CURRENCY_NO_POOL` | No funding pool opened for this currency | Balance alert settings |
| `VA_ALREADY_ACTIVE` | A VA for this subject and currency is already active | Opening a VA |
| `VA_REQUEST_PENDING` | The subject has an unfinished VA request in flight | Opening a VA, updating VA profile |
| `VA_PROFILE_NOT_FOUND` | The company has not submitted a VA profile (needed only in custom payee-name mode) | Opening a VA, updating VA profile |
| `VA_PROFILE_NOT_CHANGED` | The submission matches the existing profile; no new version produced | Updating VA profile |
| `VA_NOT_FOUND` | VA missing, not owned by the company, or not ACTIVE | Updating a VA, querying VA status |
| `VA_NOT_NAMED` | The VA uses the platform default payee name; there is no custom name to update | Updating VA profile |
| `VELOCITY_CURRENCY_INVALID` | Currency in an amount-limit group invalid or duplicated | Create / update velocity rule |
| `VELOCITY_LIMIT_VALUE_INVALID` | Limit value invalid (negative, malformed, over precision, or a longer period smaller than a shorter one) | Create / update velocity rule |
| `VELOCITY_LIST_CONTROL_INVALID` | List control group invalid (bad `filterType`, empty list, malformed entries) | Create / update velocity rule |
| `VELOCITY_BATCH_SIZE_EXCEEDED` | More than 200 items in one batch | Rule bind / unbind |
| `VELOCITY_LAST_PROTECTING_RULE` | The last active rule on a company-pool card cannot be unbound (per-item) | Rule unbind |
| `VELOCITY_RULE_CONTENT_EMPTY` | No control group configured | Create / update rule |
| `VELOCITY_RULE_INACTIVE` | The rule is not ACTIVE and cannot be bound (per-item) | Rule bind |
| `VELOCITY_LIST_CONTROL_EMPTY` | The list control group has an empty list | Create / update rule |
| `VELOCITY_TARGET_TYPE_INVALID` | Invalid subject type | Rule bind / unbind |
| `CARD_ALREADY_ACTIVATED` | The physical card has already been activated | Hosted-page link |
| `CARD_CUSTODIAN_INVALID` | Custodian missing or unusable | Card application, hosted-page link |
| `VELOCITY_RULE_NOT_FOUND` | Rule missing, or not owned by the given company / this partner | Rule query, update, status change, bind, unbind, bound-target query |
| `VELOCITY_RULE_STATUS_INVALID` | Rule status does not allow the operation | Rule update, status change, bind |
| `VELOCITY_RULE_VERSION_CONFLICT` | `ruleVersion` does not match the server (concurrent modification) β re-query and retry | Rule update, status change |
| `CARD_3DS_CHALLENGE_NOT_FOUND` | Challenge not found | 3DS challenge confirmation |
## Next steps
* The two-level error structure of batch bind / unbind and per-item failure handling: [Binding and unbinding](../how-to-use/binding-and-unbinding)
* Signature headers and where the `DAPI_*` codes come from: [First steps](../getting-started/first-steps)
* Resubmitting after a rejection: [Company onboarding](../how-to-use/company-onboarding) and [Employee onboarding](../how-to-use/employee-onboarding)
# FAQ
Source: https://docs.thedecard.com/en/corporate-card/customer-success/faq
The questions that come up most before and after launch: onboarding turnaround, resubmission after KYB/KYC rejection, uniqueness rules, card count caps, obtaining PANs, custodians, physical upgrades, choosing a funding mode and webhook fallbacks.
## π Guide
These questions come from partners' most frequent enquiries during integration and operations. If yours is not here, contact the DCS team.
## Frequently asked questions
**Q1. How long does company onboarding take after submission?**
Complete KYB first, then call the endpoint to create the company. Onboarding is asynchronous: the endpoint returns only the application ID and `PENDING` synchronously. The result arrives as `COMPANY_CREATED` / `COMPANY_REJECTED`, and can also be polled by application ID. Acceptance is not approval β after receiving the application ID, do not issue cards straight away; once the company is created, follow the flow in the [Quickstart](../getting-started/quickstart).
**Q2. Can a rejected KYB / KYC be submitted again?**
Yes. Use resubmit under the same application ID, with corrections. It is allowed only while the current state is `REJECTED`; an in-flight or already-approved application returns a state conflict.
**Q3. Can emails and phone numbers be reused?**
No. Within one partner, company emails, employee emails, employee phone numbers and company registration numbers are all unique, and once taken they are not released even after a rejection. To change them, go through resubmit or the employee-update endpoint β do not reapply under a new external ID.
**Q4. Does renaming an employee re-trigger KYC?**
Yes. If a KYC is already in flight the call returns `KYC_IN_REVIEW`; retry after the result lands.
**Q5. How many cards can a company / employee hold?**
There are caps at two levels β per company and per holder β counting live cards and in-flight applications together; exceeding them returns `CARD_LIMIT_EXCEEDED`. Confirm the exact numbers with your support contact during onboarding.
**Q6. When do I get the card number? Can I get the full PAN?**
Query as soon as card creation succeeds (`CARD_CREATED`), but regular endpoints return only the first 6 and last 4 digits. For the full PAN / CVV2 / expiry, use the retrieve-secure-card endpoint β which requires PCI DSS certification.
**Q7. Why must a company card name a custodian?**
The custodian is the card's contact person and must be an active employee of the company. They are contacted when something is wrong with the card, and complete verification when a purchase triggers a 3DS challenge.
**Q8. After a virtual-to-physical upgrade, do my card IDs and targets change?**
No. The upgrade keeps the same number: `cardId`, PAN, expiry and CVV2 all stay the same β it is the same card object throughout. After activation, the physical card takes effect.
**Q9. Company pool or dedicated balance β which should a customer pick?**
For "many cards spending together, centralised bookkeeping", the company pool; for "one budget per card, stop when it runs out, isolated books", dedicated-balance cards.
**Q10. How do I learn about a low balance first?**
Configure the company's low-balance alert: a threshold per currency; crossing it pushes `LOW_BALANCE` (and an email, if a mailbox is configured); once per day until the balance recovers.
**Q11. How do I get the final result of an asynchronous call?**
Use both channels: webhooks as the primary, and the query endpoints β polled by application ID or your external identifier β as the fallback. Anything that returns `PENDING` / `PROCESSING` synchronously goes into your local to-do queue for scheduled polling; a fund transfer returning `PROCESSING` must be confirmed by polling.
**Q12. What if a webhook never arrives?**
The retry window is only about 3 minutes (at most 4 deliveries); if your receiver is down longer, events will be missed β always back yourself up with the query endpoints. Events carry no ordering guarantee; go by `webhookId` (a monotonically increasing snowflake ID) plus the resource's current state.
**Q13. How do I debug a signature failure?**
Locate by error code. The most common cause is a wrongly assembled string to sign: GET uses the raw query string (empty string if none), POST uses the request body verbatim (do not re-serialise), concatenated as `apiKey + timestamp + nonce + payload`, with the HMAC in lowercase hex. Webhook verification is a separate scheme (sort field names lexicographically first, then compact-serialise).
## Next steps
* The full signing and webhook specification: [Quickstart](../getting-started/quickstart)
* The per-domain how-to guides: [Managing companies](../how-to-use/managing-companies), [Managing cards](../how-to-use/managing-cards)
# First steps
Source: https://docs.thedecard.com/en/corporate-card/getting-started/first-steps
What to prepare before integrating: the six stages from contract to launch, environment isolation and credential handling, three ground rules that run through everything, and the conventions shared by every endpoint.
## π Guide
Whether you are running a technical evaluation or starting the integration outright, walk through this page first: get the credentials, callback URL, egress IPs and shared conventions in place, then move on to the [Quickstart](./quickstart) to issue your first card. DCS, as the licensed issuer, carries issuing and settlement; you implement three things β signing, calling and receiving webhooks.
## Integration stages
From contract signing to running at scale, integration goes through six stages. Each has explicit prerequisites; do not enter a stage before the previous one is ready.
| Stage | Main work |
| - | - |
| 1 Β· Contract and scoping | Sign commercially, submit qualification and due-diligence material for the partner and the first company, confirm card types, card face, whether physical cards (and shipping range) are needed, and whether you hold PCI DSS |
| 2 Β· Environment setup | Register the callback URL and egress IPs, receive and store the keys securely, confirm the card product and card face configuration |
| 3 Β· Development | Implement request signing and run the shortest issuing path; implement the webhook receiver (signature check, idempotency, fast 2xx); implement scheduled polling of the query endpoints as fallback |
| 4 Β· Testing and certification | Verify case by case: the happy path, resubmission after rejection, idempotent repeats, callback retry and dedup, signature failures and clock skew, limit and parameter validation |
| 5 Β· Pilot | Grey-launch with a small volume in production (recommended: one company, one employee, one card) with monitoring and alerting in place |
| 6 Β· Launch and scale | Once the pilot is clean, open up issuing and transaction volume step by step into normal operations |
## Environments and credentials
* **Environment isolation**: the sandbox and production environments are fully independent deployments β domains, credentials, data and callback keys are all separate, non-interchangeable and non-migratable; switch both the domain and the credentials when switching environments. Both use the same path prefix, `/open-api-corp/`.
* Sandbox: `https://api.thedecard-sandbox.com`
* Production: `https://api.thedecard.com`
* **Credential handling**: the AK (API key) identifies the partner in request headers; the SK (secret key) is used only to compute signatures locally and doubles as the decryption key for the secure card endpoint; the webhook signing key is issued separately by DCS. Store the SK in a key-management service or encrypted configuration β never in code repositories, logs or the frontend.
## Three ground rules
β **Every create-type endpoint is accepted asynchronously.** Company onboarding, employee creation and virtual card application return only an the application ID and `status=PENDING` synchronously β meaning "accepted", not "created". For the real final state, rely on webhooks first, with polling of the query endpoints as the fallback.
β‘ **Success is judged by the response body's `code`, not the HTTP status.** The unified response envelope is `{code, message, data}`; only `code == "SYS_SUCCESS"` is success, everything else is failure. The HTTP status is informational only (authentication failures return 401).
β’ **Your identity comes from the AK, never from the request body.** DCS establishes the partner identity from the AK signature check on the request headers. Do not carry any partner or enterprise identifier in the body β if you do, it is ignored.
## Shared conventions
This section sets the ground rules shared by every endpoint; each endpoint page then only needs to describe its own parameters and fields.
| Convention | Description |
| - | - |
| Base URL | `https://{access-domain}/open-api-corp/{module}/v1/{action}`; HTTPS throughout. Test and production use separate access domains, issued by DCS during onboarding. |
| Method semantics | Query endpoints use GET (parameters in the query string); write and action endpoints use POST (parameters in a JSON body, Content-Type `application/json`). |
| Response envelope | Success or failure, the response is always `{code, message, data}`. Judge business success by `code == "SYS_SUCCESS"`; everything else is failure. HTTP status is informational (401 for authentication failure). |
| Pagination | List endpoints share `page` (from 1, default 1), `pageSize` (1β100, default 20), `total`, `result`. |
| Idempotency | Every write endpoint carries an idempotency key (`companyApplyRef` / `customerApplyRef` / `cardApplyRef` / `transferRef` / `ruleRef` and so on), generated and persisted by the partner and reused on retries. |
| Versioning | The version number is embedded in the path (currently v1). Backwards-compatible changes (new optional fields, enum values, error codes) ship within v1 β parse responses leniently and ignore unknown fields; incompatible changes ship under a new version. |
**ID conventions.** Every ID in the contract is a JSON string: `companyId` and `customerId` are UUID-shaped strings from the unified user center; `cardId`, the application ID and `webhookId` are 19-digit snowflake strings (transmit them as strings β numeric literals overflow the double-precision safe-integer range and get silently truncated); `transferId`, `ruleId`, `statementId`, `transactionId` and the rest are business strings to pass through verbatim. The meaning of a polymorphic ID (`subjectId`) is determined by the type field in the same structure, never inferred from its shape. Partner-side idempotency keys (`xxxRef` β such as `companyApplyRef` / `customerApplyRef` / `cardApplyRef` / `transferRef` / `ruleRef`) are custom strings matching `^[A-Za-z0-9_-]+$`, at most 64 characters.
## Next steps
* Once ready, head to the [Quickstart](./quickstart) and issue your first card along the shortest path.
* Understand holders, funding and state machines: [Holders and the funding model](../basic-concepts/identity-and-funding)
# Overview
Source: https://docs.thedecard.com/en/corporate-card/getting-started/overview
What the Corporate Card Open Platform is: embed card issuing into your product and issue globally accepted corporate cards to your business customers β the three-tier identity model, two funding modes, four card types and the security and compliance boundary.
## π Guide
This page gives you the full picture of the Corporate Card Open Platform: what it is, its core value, where it fits, the capability and card-type map, and the security and compliance boundary. By the end you can judge whether the product matches your business.
## What it is
**Issuing, made simple**
The Corporate Card Open Platform lets you embed card issuing directly into your own product and issue corporate cards that your business customers can use for online and offline payments worldwide β without applying for an issuing license, negotiating BIN sponsorship, building an issuing system, or integrating one by one with card networks, issuing banks and clearing institutions. DCS carries all of that underneath; you consume it through a single set of OpenAPIs.
DCS does the heavy lifting in the background β onboarding due diligence, card production and issuance, authorization processing, clearing and ledgering, compliance and risk control. You focus on the product and experience you offer your business customers, and you remain their single point of contact throughout. Licenses, BINs, PCI, card production and transaction processing are all borne by DCS; you never have to invest in them separately.
**API first: your product plus our issuing infrastructure**
The Corporate Card Open Platform's core deliverable is a set of OpenAPIs covering the full issuing life cycle β company onboarding, employee creation, card application, funding, spend controls, statements and reconciliation, and 3DS authentication. Every capability is reachable through an API call.
API-native issuing means turning card issuing into a programmable, embeddable, automatable infrastructure component. You never deal with BINs, card networks or clearing rails directly; they are encapsulated behind the API and orchestrated by DCS.
* **Issuing is no longer a redirect.** In the traditional model your customer leaves your product, fills in forms on a bank portal, applies, waits for review, receives the card, activates it β a broken, fragmented journey. Through the API the whole flow lives inside your product: your customer clicks a button in your console and the card exists within ten seconds, without ever leaving your page.
* **Data no longer has gaps.** Authorizations, settlements, balance movements, statement generation β every key event is pushed to your system over webhooks, instead of a monthly PDF statement to reconcile by hand. The transaction data your customer sees, the data you see and the data DCS sees are the same data.
* **Scaling needs no renegotiation.** A new card type? Different limit rules? Another funding mode? If DCS's capability set covers it, you get it at the API level β no new contract, no new negotiation, no repeat of the commercial process.
**A three-tier model: you own the customer experience**
Your business is organized in a partner β company β employee structure:
* The **partner** is you β the platform embedding card issuing into its own product.
* A **company** is your business customer; companies register, onboard, fund and manage budgets.
* An **employee** is the end cardholder; spending happens in their hands.
**Two card forms**
* **Virtual cards**: usable the moment they are issued, delivered in seconds β suited to online payments, e-commerce purchasing and ad spend.
* **Physical cards**: obtained by upgrading an activated virtual card under the same number β PAN, CVV, expiry and card ID all stay unchanged, so no targets need migrating. Suited to offline travel, dining and procurement.
## Core value
**Self-service issuing over APIs, end to end**
From company onboarding and employee creation to issuing, funding, spending and settlement, the entire flow is exposed through one set of OpenAPIs and can be fully embedded into your systems for self-service operation, with no manual steps or offline processes. Time-consuming stages β onboarding, employee creation, issuing β follow a synchronous-acceptance-plus-asynchronous-callback pattern: the endpoint returns an application ID immediately, the final result arrives through event callbacks, and polling is always available as a fallback. Every write endpoint supports an idempotency key, so cardNetwork retries and duplicate submissions are safe.
**Two funding modes, granularity of your choice**
Under one business customer you can combine two funding modes freely: a company funding pool (balance shared across cards, centralised bookkeeping β good for unified control) and per-card dedicated balances (earmarked funds with independent caps β good for budget isolation). Both share the same endpoints and support two-way transfers β allocate budget down to a card, or sweep a card's balance back to the pool β all closed-loop within the same currency and the same company. You arrange them around your customer's organization and budgeting habits, rather than bending to a fixed product shape.
**Event-driven across the chain, traceable and reconcilable**
Onboarding, issuing, authorization, settlement, deposits and balance movements are all pushed over webhooks, so your system tracks business progress in real time and drives its own flows. Every event carries a globally unique ID for idempotency, is signed with HMAC-SHA256 against tampering, and is retried automatically on delivery failure; every event class also has a matching query endpoint as a fallback, so nothing depends on a single channel and the books stay traceable and verifiable throughout.
## Where it fits
The value of a corporate card is turning "who can spend, how much, and on what" into programmable rules. These are the most common partner scenarios, all achievable on the same set of endpoints:
| Scenario | Description | Recommended card type |
| - | - | - |
| Online marketplaces / e-commerce | Pay platforms and marketplaces with per-transaction control over virtual cards | Company utility card / employee card (company pool) |
| Online advertising | Isolate ad-platform spend with periodic caps | Dedicated-purpose card (own balance, capped) |
| SaaS and cloud services | Recurring subscription renewals, locked by category | Company utility card or dedicated-purpose card |
| Business travel and dining | Hotels, flights and dining offline | Employee card (company pool / own balance) |
| Supplier and goods payments | One card per supplier or project, each with its own balance | Dedicated-purpose card |
| Everyday employee expenses | An expense card with its own limits per employee | Employee card |
Every scenario shares the same endpoints and the same company entity; the card type is decided solely by the type code at application time, and one employee can hold pool-funded and dedicated-balance cards at the same time. Spend controls (amount / count / currency / MCC / region) are implemented through velocity rules β see [Limits and statements](../basic-concepts/limits-and-billing) and the API reference.
**Control granularity is yours to define**
Spend control is implemented through velocity rules β per-transaction and daily / monthly / quarterly / yearly caps on amount and count, plus allow/deny lists for currency, MCC and transaction region.
Rules bind to cards or employees. When an object has several rules bound, the system automatically applies the strictest value on each dimension. That lets you satisfy company-wide control and department-level refinement at the same time, without building a rule-inheritance hierarchy.
## Capability map
**Company onboarding**
| Capability | Description |
| - | - |
| Company onboarding | KYB due diligence first; on approval the company is created with its funding pool |
| Query onboarding application | Application status, KYB result and rejection reason |
| Resubmit for review | After a KYB rejection, correct the legal name / registration number and resubmit under the same application |
| Query company detail | Company name, registration number, pool currencies, status and more |
| Status update (freeze / unfreeze) | Add or remove behavioral states per capability domain β set semantics, idempotent |
| Low-balance alert | Threshold and notification mailbox per currency; crossing the threshold triggers a webhook and an email |
**Employee management**
| Capability | Description |
| - | - |
| Create employee | Accepted asynchronously; the employee ID is issued once KYC name screening passes |
| Query / resubmit | Query applications and results; after a KYC rejection, correct the information and resubmit |
| Update employee information | Name / phone / email changed one field at a time; a change re-triggers KYC review |
| Status update (freeze / unfreeze) | Freezing an employee makes every card under that employee decline transactions |
**Card management**
| Capability | Description |
| - | - |
| Apply for a virtual card | Specify the holder and card type; risk checks and card creation run asynchronously, with the final state delivered by event callback |
| Query card / card list | First 6 and last 4 digits, company, holder, card type, cardNetwork, balance mode, currency, status |
| Open a receiving account for a dedicated-balance card | A dedicated-balance card can open a bank virtual account (VA) for inbound funds, or be funded from the company pool |
| Retrieve secure card details | Returns PAN / CVV2 / expiry as ciphertext for display in your own frontend; available only to PCI DSS-certified partners |
| Virtual-to-physical upgrade | Produce a physical card for an activated virtual card (same-number upgrade) |
**Fund management**
| Capability | Description |
| - | - |
| Query balances | Available balance and in-flight holds by funding owner (company pool / dedicated-balance card) and currency |
| Get deposit details | Receiving account number, bank code / name, account name, SWIFT and so on, for the customer's bank transfer |
| Fund transfers | Internal transfers between company pool and dedicated-balance cards β same currency, same company, closed loop, with idempotency checks |
| Query transfer results / deposit records | Transfer status by identifier; paginated deposit history for reconciliation |
## Card types
A card type is the combination of two orthogonal dimensions: the **holder** (who the card is issued to) and the **balance mode** (which account pays). You specify it with a card type code at application time.
| Business card type | Type code | Holder | Balance mode | Custodian | Typical use |
| - | - | - | - | - | - |
| Company utility card | `COMPANY_UTILITY_CARD` | Company | SHARED (company pool) | Required | Unified company spend, many cards sharing one funding entry |
| Dedicated-purpose card | `COMPANY_DEDICATED_PURPOSE_CARD` | Company | DEDICATED (own balance) | Required | A dedicated budget per card |
| Employee card (company pool) | `EMPLOYEE_CARD_CORPORATE_FUNDED` | Employee | SHARED (company pool) | β | The most common expense card: the employee spends, the pool pays |
| Employee card (own balance) | `EMPLOYEE_CARD_SELF_FUNDED` | Employee | DEDICATED (own balance) | β | Travel or ring-fenced allowance cards β spending stops when the balance runs out |
* **Custodian**: regulation requires every company card to name an active employee of that company as its custodian. The custodian is contacted when something is wrong with the card, and completes verification when a purchase triggers a 3DS challenge.
* **Embossed name**: two embossed lines are supported and can be specified when applying for the physical card.
* **Availability**: which card types can be opened, and how many, is configured by DCS per partner and per company.
* **Virtual first**: the issuing endpoint creates a virtual card that is usable immediately. If a physical card is needed, upgrade the activated virtual card in place β the same card, the same number.
## Security and compliance
Compliance and security are the entry ticket to card issuing, and DCS carries the heaviest parts for you: licensing and BIN ownership, the PCI data environment, KYB/KYC due diligence, risk control and sanctions screening. On top of that, you integrate against the following mechanisms.
* **Due diligence**: KYB for company onboarding, KYC for employees. After a rejection of either kind, correct the key fields and resubmit under the same application.
* **Risk control and freezing**: freezes come from two sources β partner-initiated freezes can be lifted self-service over the API; risk / regulatory / judicial freezes cannot, and go through support. Two granularities β freezing a card stops only that card, freezing an employee makes every card under them decline. Five capability-domain freeze codes are available (account / cash-in / cash-out / payment / card), each added or removed independently.
* **Data security**: each partner receives an AK / SK pair. The SK never appears in any request in clear text; it is used only to compute the HMAC-SHA256 signature locally, and requests carry a timestamp and nonce against replay. Your identity is established by the gateway from the AK signature check; any institution identifier carried in the request body is ignored.
* **Card data protection**: regular query endpoints never return the full PAN β only the first 6 digits (BIN) and the last 4. The full PAN / CVV2 / expiry are returned only by the secure card endpoint, AES-GCM encrypted, and only to allowlisted PCI DSS-certified partners; partners without PCI DSS can integrate our hosted secure card page.
## Next steps
* What to prepare before integrating: [First steps](./first-steps)
* Run the onboarding β funding β issuing β spending loop on the shortest path: [Quickstart](./quickstart)
* Understand holders, funding and state machines: [Holders and the funding model](../basic-concepts/identity-and-funding)
# Quickstart
Source: https://docs.thedecard.com/en/corporate-card/getting-started/quickstart
Run the onboarding β funding β issuing β spending loop in nine steps along the shortest issuing path: implement request signing, make the calls and receive the webhooks.
## π Guide
This page assumes you have obtained credentials, registered your callback URL and read the shared conventions in [First steps](./first-steps). Taking "issue an employee a virtual card paid from the company pool" as the example, the following walks the whole flow.
## The shortest issuing path
| # | Step | Endpoint | Sync/async | What you obtain |
| - | - | - | - | - |
| 1 | Obtain credentials | β | β | AK / SK and the access domain |
| 2 | Company onboarding | `POST /company/v1/apply` | Async acceptance | `companyApplyId` |
| 3 | Wait for onboarding and VA creation | Webhook `COMPANY_CREATED` (fallback query-apply) | Async | `companyId` (company ACTIVE) |
| 4 | Fund the account | Customer bank transfer to the company pool VA | Async | Deposit notification (`BANK_TRANSFER_INFO`) |
| 5 | Create the employee | `POST /customer/v1/apply` | Async acceptance | `customerApplyId` |
| 6 | Wait for the employee result | Webhook `CUSTOMER_CREATED` (fallback query-apply) | Async | `customerId` |
| 7 | Apply for the virtual card | `POST /card/v1/apply` | Async acceptance | `cardApplyId` |
| 8 | Wait for card creation | Webhook `CARD_CREATED` (fallback query-apply) | Async | `cardId` (card ACTIVE) |
| 9 | Confirm the card | `GET /card/v1/query` | Sync | Card BIN, last 4 digits, cardNetwork, status |
**The order cannot be skipped**: creating an employee requires the company to be ACTIVE (wait for step 3); applying for a virtual card requires the holder (company or employee) to be ACTIVE (wait for step 5). On rejection: a rejected company / employee KYB / KYC goes through resubmit, reusing the same the application ID.
## Request signing
Every business endpoint (paths starting with `/open-api-corp/`) must be signed; a failed signature check returns HTTP 401.
**Request headers**
| Header | Required | Description |
| - | - | - |
| `X-DAPI-API-KEY` | Y | Your AK |
| `X-DAPI-TIMESTAMP` | Y | Request timestamp, epoch milliseconds |
| `X-DAPI-NONCE` | Y | Random integer in \[10000, 99999], single use |
| `X-DAPI-SIGN` | Y | The signature, see below |
| `Content-Type` | Y | `application/json` for POST |
**Signature algorithm**
```
payload = (GET) the raw query string ("" if none)
= (POST) the request body JSON, verbatim
dataToSign = apiKey + timestamp + nonce + payload
X-DAPI-SIGN = Hex( HMAC-SHA256( SK, dataToSign ) ) # lowercase hex
```
The HMAC output is lowercase hex; the string to sign is processed as UTF-8 bytes. For POST, the payload is the request body verbatim β the bytes you sign and the bytes you send must be identical (use `--data-binary` with cURL).
* **Timestamp window**: the server checks |now β timestamp| β€ 5 seconds; outside the window it returns `DAPI_TIMESTAMP_EXPIRED`. Make sure NTP is enabled on your servers.
* **Replay protection**: the same signature is accepted only once within a short window β a repeat returns `DAPI_NONCE_DUPLICATE`; a nonce outside the range returns `DAPI_NONCE_ILLEGAL`. Generate a fresh nonce for every request.
* **The SK never appears in any request**; it is used only to compute the signature locally.
**Full example Β· company onboarding (bash)**
```bash theme={null}
BASE="https://"
AK=""; SK=""
BODY='{"companyApplyRef":"EXT-COMPANY-0001","companyName":"Example Company Limited","companyRegistrationNumber":"REG-0000000","email":"contact@example.com","fundingCurrencies":["USD"]}'
TS=$(python3 -c 'import time;print(int(time.time()*1000))')
NONCE=$(python3 -c 'import random;print(random.randint(10000,99999))')
SIGN=$(printf '%s' "${AK}${TS}${NONCE}${BODY}" | openssl dgst -sha256 -hmac "${SK}" -r | cut -d' ' -f1)
curl -sS -X POST "${BASE}/open-api-corp/company/v1/apply" \
-H "X-DAPI-API-KEY: ${AK}" -H "X-DAPI-TIMESTAMP: ${TS}" \
-H "X-DAPI-NONCE: ${NONCE}" -H "X-DAPI-SIGN: ${SIGN}" \
-H "Content-Type: application/json" --data-binary "${BODY}"
```
## Receiving webhooks
Callback flow: an HTTP POST from DCS to your server, with the unified envelope `{webhookId, webhookType, businessId, data, notificationTime}` (data}\`. Your receiver must implement four things:
| Item | Requirement |
| - | - |
| Verify the signature | `X-Signature = Hex(HMAC-SHA256(SK, sortedCompactJson(entire payload)))`. sortedCompactJson = all field names sorted lexicographically at every level + compact serialisation with whitespace stripped; recompute the same way and compare β do not verify against the raw bytes. The signing key is issued separately by DCS |
| Idempotency | Deduplicate by `webhookId` (all retries of one event reuse the same one); still return 2xx on a dedup hit |
| Return 2xx fast | A 2xx counts as delivered (DCS does not parse the response body); process business logic asynchronously |
| Assume no ordering | Go by `webhookId` (a monotonically increasing snowflake ID) plus the resource's current state; re-query when needed |
**The retry window is about 3 minutes**: a non-2xx or a timeout triggers redelivery, up to 3 retries about 1 minute apart with no backoff (at most 4 deliveries including the first). After all fail the event is marked failed and alerted. Poll the query endpoints on a schedule β they are the fallback when callbacks fail outright.
## Next steps
* Understand holders, funding and state machines: [Holders and the funding model](../basic-concepts/identity-and-funding)
* Go deeper domain by domain, starting with [Managing companies](../how-to-use/managing-companies)
# Handling 3DS Challenges
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/3ds-challenges
All fields of the AUTHORISATION_3DS_CHALLENGE event with OOB and OTP_DELEGATE payload examples; the division of work across the three authentication modes; OOB results return via the authenticate endpoint, idempotent on repeat.
## π Guide
When an online purchase triggers 3DS authentication, DCS forwards the challenge to the partner: the webhook `AUTHORISATION_3DS_CHALLENGE` is pushed, and you take part in verification or code delivery per the mode configured at onboarding (the mode is set per card BIN range on the issuing side). This page covers the challenge event's full field set with both payload examples, what each mode asks of you, and how to call the one and only result-return endpoint for OOB.
## AUTHORISATION\_3DS\_CHALLENGE: the challenge event
The event's `data` fields:
| Field | Description |
| - | - |
| `challengeId` | The challenge ID; for OOB, pass it verbatim to the authenticate endpoint |
| `status` | Challenge state (`INIT` when delivered) |
| `cardId` / `companyId` | The card that triggered the challenge and its company |
| `expiryTime` | Challenge expiry, format `yyyy-MM-dd'T'HH:mm:ssXXX` |
| `currency` / `amount` | Transaction currency (numeric ISO 4217 code, e.g. `840`) and amount |
| `merchantId` / `merchantName` / `merchantCountryCode` / `mcc` | Merchant details |
| `challengeFlowType` | Challenge mode: `OOB` / `OTP_DELEGATE` |
| `challengeMethodType` | Challenge method (e.g. `DELEGATE_SCA_V1` / `SMS_OTP`) |
| `encryptedOtpPasscode` | Encrypted one-time code (AES-GCM, Base64), **OTP\_DELEGATE only** |
| `phoneNumber` / `email` | Cardholder phone / email ciphertexts (AES-GCM, Base64), OTP\_DELEGATE only; the unconfigured one is `null` |
| `iv` | AES-GCM initialization vector (Base64), shared by all ciphertext fields in the event |
**OOB challenge payload example** (basic transaction details only β verification happens in your app):
```json theme={null}
{
"challengeId": "5211234567890123456",
"status": "INIT",
"cardId": "5136759791164443137",
"companyId": "1276574398205403138",
"expiryTime": "2026-08-13T19:31:30+08:00",
"currency": "840",
"amount": 129.99,
"merchantId": "MCT00001",
"merchantName": "EXAMPLE ONLINE STORE",
"merchantCountryCode": "IE",
"mcc": "5732",
"challengeFlowType": "OOB",
"challengeMethodType": "DELEGATE_SCA_V1"
}
```
**OTP\_DELEGATE challenge payload example** (additionally carries the code and delivery-channel ciphertexts):
```json theme={null}
{
"challengeId": "5211234567890123457",
"status": "INIT",
"cardId": "5136759791164443137",
"companyId": "1276574398205403138",
"expiryTime": "2026-08-13T19:31:30+08:00",
"currency": "344",
"amount": 500.00,
"merchantId": "MCT00002",
"merchantName": "EXAMPLE TRAVEL",
"merchantCountryCode": "HK",
"mcc": "4722",
"challengeFlowType": "OTP_DELEGATE",
"challengeMethodType": "SMS_OTP",
"encryptedOtpPasscode": "kX9β¦ (AES-GCM ciphertext, Base64)",
"phoneNumber": "aB3β¦ (AES-GCM ciphertext, Base64)",
"email": null,
"iv": "R4nd0mIVBase64=="
}
```
## Division of work across the three modes
On receiving a challenge event, handle it per the mode configured at onboarding:
| Mode | Challenge delivery | Who handles it |
| - | - | - |
| Issuer OTP | DCS sends the code to the cardholder directly (email / SMS) | The whole challenge loop closes inside DCS β the partner is not involved and receives no challenge event |
| `OTP_DELEGATE` | DCS pushes a webhook to the partner carrying the code plus email / phone ciphertexts | The partner decrypts and delivers the code to the cardholder through its own channel; the cardholder enters it on the challenge page β **no result callback** |
| `OOB` | DCS pushes a webhook to the partner carrying the basic transaction details | The partner notifies the cardholder's app β the cardholder verifies in-app β the partner calls the authenticate endpoint to return the result to DCS |
**Challenges are valid for about 300 seconds** (the event's `expiryTime` is authoritative) and time out as rejected. The result-confirmation endpoint applies to OOB mode only.
## The OOB callback: the authenticate endpoint
**`POST /open-api-corp/card3ds/v1/authenticate`** β challenge-result confirmation, returning the cardholder's verification outcome to DCS. **Callable on the OOB path only.**
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `challengeId` | String | Yes | β€64; the challenge ID, from the webhook's `data.challengeId` |
| `action` | String | Yes | β€16; the cardholder's verification result, full enum: `APPROVE` (cardholder verified) / `REJECT` (cardholder declined / verification failed) |
| `operateTime` | String | No | β€32; when the cardholder acted, format `yyyy-MM-dd'T'HH:mm:ssXXX`; recorded verbatim for audit, with no server-side fallback |
**Response data**
| Field | Type | Description |
| - | - | - |
| `challengeId` | String | The challenge ID (echoes the request's `challengeId`) |
| **Request examples** | | |
```json theme={null}
// Scenario 1: OOB challenge β return the cardholder's verification result (this endpoint is OOB-only)
{
"challengeId": "5185740066240790531",
"action": "APPROVE",
"operateTime": "2026-08-19T10:30:00+08:00"
}
// Scenario 2: OTP_DELEGATE challenges have no callback β calling this endpoint by mistake returns CARD_3DS_CHALLENGE_NOT_FOUND (see response scenario 2)
```
**Response examples**
```json theme={null}
// Scenario 1: OOB challenge accepted (repeating the same result is idempotent β the response matches the first)
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"challengeId": "5185740066240790531"
}
}
// Scenario 2: calling this endpoint against an OTP_DELEGATE challenge (no callback path β rejected)
{
"code": "CARD_3DS_CHALLENGE_NOT_FOUND",
"message": "challenge not found",
"data": null
}
```
**Error codes**
| Error code | Description |
| - | - |
| `CARD_3DS_CHALLENGE_NOT_FOUND` | Challenge does not exist / does not belong to this partner / is not an OOB challenge (e.g. calling this endpoint against an OTP\_DELEGATE challenge) β all three cases share one code, with no differentiation exposed |
**Idempotency**: repeating the same result for the same `challengeId` returns idempotently, with a response identical to the first β retries need no dedup design on your side.
**Compliance note Β· HKMA 3DS requirements**: under the Hong Kong Monetary Authority's requirements published on 14 April 2025, online credit card transactions must be verified through the bank's app. The HKMA's press release of 1 August 2024 extended the requirement to 32 banks and 10 stored-value facility (SVF) operators. Partners serving Hong Kong cardholders are advised to adopt the OOB (in-app) mode to align with this direction. References: [HKMA, "New Anti-Digital Fraud Measures: E-Banking Security ABC"](https://brdr.hkma.gov.hk/eng/doc-ldg/docId/getPdf/20250411-1-EN/20250411-1-EN.pdf), [HKAB statement](https://www.hkab.org.hk/en/news/press-release/318).
## Next steps
* The authorization and settlement main line after the challenge passes: [Real-time authorization and settlement](./realtime-authorization)
* The transaction-event landscape and authorization semantics: [Transactions Β· Overview](./authorization-and-3ds)
# Applying for a virtual card
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/applying-virtual-cards
Issue a virtual card to a company or employee with apply: the full field reference (subjectType/cardProfileId matching, company-card custodian, ruleIds binding), the complete error codes, and querying progress, single cards and card lists with query-apply / query / list.
## π Guide
Issuing always starts with a virtual card: `POST /open-api-corp/card/v1/apply` returns an `cardApplyId` on acceptance; risk checks and card creation run fully asynchronously, with the final state pushed via the webhooks `CARD_CREATED` / `CARD_REJECTED` and `GET /open-api-corp/card/v1/query-apply` as the polling fallback. Once the card is created, use the `cardId` for single-card and list queries. For where this chain sits among the card pages, see [Managing cards](./managing-cards).
## Applying for a virtual card
```http theme={null}
POST /open-api-corp/card/v1/apply
```
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `cardApplyRef` | String | Yes | β€64; your unique application identifier |
| `subjectType` | String | Yes | `COMPANY` (company card) / `CUSTOMER` (employee card) |
| `subjectId` | String | Yes | β€50; the holder's external id: `COMPANY`β`companyId`, `CUSTOMER`β`customerId`. Must exist, match the type, be ACTIVE and belong to your partnership |
| `cardProfileId` | String | Yes | The card type code β one of the four types (see the pairing table below for valid `subjectType` combinations) |
| `custodianCustomerId` | String | Conditional | β€50; required for company cards (`subjectType`β `CUSTOMER`): the `customerId` of an employee of the same company, who must be ACTIVE |
| `ruleIds` | `Array` | Conditional | β€5 entries; velocity (amount/count limit) rules bound at issue time, each element a rule external id. Required for company-balance cards (the company-pool profiles, i.e. the company utility card and the corporate-funded employee card); optional for the other types |
### The four cardProfileId types and their subjectType pairing
A card type is the combination of two orthogonal dimensions: the holder (who the card is issued to) Γ the balance mode (which account the money comes from). For the full card-type introduction see [Overview Β· Card types](../getting-started/overview).
| Card type | `cardProfileId` | `subjectType` | Balance mode | Custodian (`custodianCustomerId`) |
| - | - | - | - | - |
| Company utility card | `COMPANY_UTILITY_CARD` | `COMPANY` | company pool | Required |
| Dedicated-purpose company card | `COMPANY_DEDICATED_PURPOSE_CARD` | `COMPANY` | card's own balance balance | Required |
| Employee card (company pool) | `EMPLOYEE_CARD_CORPORATE_FUNDED` | `CUSTOMER` | SHARED company pool | Not applicable |
| Employee card (dedicated balance) | `EMPLOYEE_CARD_SELF_FUNDED` | `CUSTOMER` | DEDICATED balance | Not applicable |
Per regulatory requirements, a company card must designate a current employee of the company as its custodian. The custodian is who gets contacted when something is wrong with the card, and who completes verification when a purchase triggers a 3DS challenge.
### Binding rules with ruleIds
Every rule in `ruleIds` must simultaneously: **exist, belong to the same company, and be ACTIVE**; a rule that configures amount limits must share at least one currency with the card's settlement currencies. The array must not contain duplicates β carrying the same rule twice is rejected outright (it is not silently deduplicated), so deduplicate before submitting.
**Any failing entry rejects the whole request** (an early rejection before acceptance β no `cardApplyId` is produced); there is no partial success. After issuance you can still adjust the targets through the velocity endpoints β see [Setting spend limits](./spend-limits).
### Request examples
```json theme={null}
// Scenario 1: employee card (company pool, SHARED β ruleIds required)
{
"cardApplyRef": "ext-card-apply-0001",
"subjectType": "CUSTOMER",
"subjectId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"cardProfileId": "EMPLOYEE_CARD_CORPORATE_FUNDED",
"ruleIds": ["5136744097353943556"]
}
// Scenario 2: company utility card (non-personalized β custodianCustomerId required; SHARED β ruleIds required)
{
"cardApplyRef": "ext-card-apply-0002",
"subjectType": "COMPANY",
"subjectId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"cardProfileId": "COMPANY_UTILITY_CARD",
"custodianCustomerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"ruleIds": ["5136744097353943556"]
}
```
### Response data and example
| Field | Type | Description |
| - | - | - |
| `cardApplyId` | String | The card application ID |
| `status` | String | On successful acceptance this endpoint always returns `PENDING` (the creation result is pushed asynchronously via webhook); the full application status enum is `PENDING` / `SUCCEED` / `REJECTED` |
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardApplyId": "5136744097353943553",
"status": "PENDING"
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `CARD_PROFILE_INVALID` | Card type unavailable (e.g. invalid/disabled type code, not enabled for your partnership, or `subjectType` does not match the type's applicable holder); see message/reason for the specifics |
| `CARD_LIMIT_EXCEEDED` | Over the card cap (company-level or holder-level; live cards and in-flight applications counted together); see message/reason for which level |
| `SUBJECT_INVALID` | Company-card custodian missing, or not a valid employee of the company |
| `SUBJECT_INVALID` | The holder (company) does not exist or is invalid |
| `CUSTOMER_INVALID` | The holding/custodian employee does not exist or is invalid |
| `DAPI_PERMISSION_DENIED` | Partner does not exist or is not activated (the partner identity is derived from the AK; this is an authorization issue handled by the gateway authorization codes, HTTP 401; rarely seen) |
| `APPLY_DUPLICATE` | An in-flight or successful application already exists for this `cardApplyRef`; it cannot be resubmitted |
| `CARD_RULE_REQUIRED` | A company-balance card carried no `ruleIds` (an empty array counts as none) |
| `CARD_RULE_DUPLICATE` | The same rule appears more than once in `ruleIds`. It is not silently deduplicated β a duplicate usually means the caller's view of "which rules are actually bound" has drifted, and quietly collapsing it would leave you believing N rules were bound. Note: when the array both exceeds 5 entries and contains duplicates, this code is returned rather than `CARD_RULE_LIMIT_EXCEEDED` |
| `CARD_RULE_LIMIT_EXCEEDED` | More than 5 entries in `ruleIds` |
| `CARD_RULE_INVALID` | The rule does not exist, does not belong to this company, or is not ACTIVE β one code covers all three, so the response cannot be used to probe whether a rule exists under another company; see message for the specifics |
| `CARD_RULE_CURRENCY_NOT_MATCH` | The rule's amount-limit currencies share no currency with the card's settlement currencies |
Related webhooks: `CARD_CREATED` / `CARD_REJECTED`.
## Querying application progress
```http theme={null}
GET /open-api-corp/card/v1/query-apply?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&cardApplyId=5136744097353943553
```
The only request parameter is `cardApplyId` (String, required, β€20; the card application ID).
**Response data**:
| Field | Type | Description |
| - | - | - |
| `cardApplyId` | String | The application ID |
| `status` | String | The full application status enum: `PENDING` (in acceptance / card creation) / `SUCCEED` / `REJECTED` |
| `cardId` | String | The card ID once created (empty before creation) |
| `errorCode` | String | The rejection code: fixed at `CARD_RISK_REJECTED` (only when `REJECTED`) |
| `errorMessage` | String | The rejection reason: fixed at `risk check refused` (only when `REJECTED`; internal risk codes are not passed through) |
```json theme={null}
// Scenario 1: card created successfully
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardApplyId": "5136744097353943553",
"status": "SUCCEED",
"cardId": "5185740066240790530",
"errorCode": null,
"errorMessage": null
}
}
// Scenario 2: rejected by risk
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardApplyId": "5136744097353943553",
"status": "REJECTED",
"cardId": null,
"errorCode": "CARD_RISK_REJECTED",
"errorMessage": "risk check refused"
}
}
```
Error code: `APPLY_NOT_FOUND` β the application does not exist (or does not belong to your partnership).
## Querying a single card
```http theme={null}
GET /open-api-corp/card/v1/query?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&cardId=5185740066240790530
```
The only request parameter is `cardId` (String, required, β€20; the card ID).
**Response data**:
| Field | Type | Description |
| - | - | - |
| `cardId` | String | The card ID |
| `panFirst6` | String | First 6 digits of the PAN (the BIN; the full PAN is never returned) |
| `panLast4` | String | Last 4 digits of the PAN (the full PAN is never returned) |
| `companyId` | String | The owning company |
| `subjectType` | String | `COMPANY` / `CUSTOMER` |
| `subjectId` | String | The holder's external id |
| `cardProfileId` | String | The card product code: `COMPANY_UTILITY_CARD` / `COMPANY_DEDICATED_PURPOSE_CARD` / `EMPLOYEE_CARD_CORPORATE_FUNDED` / `EMPLOYEE_CARD_SELF_FUNDED` |
| `cardNetwork` | String | `VISA` / `MASTERCARD` / `UPI` |
| `currency` | String | `USD` / `HKD` |
| `status` | String | Card status: `ACTIVE` / `FROZEN` / `LOCKED` / `RESTRICTED` / `LOST` / `EXPIRED` / `CLOSED` |
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardId": "5185740066240790530",
"panFirst6": "520983",
"panLast4": "5492",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"subjectType": "CUSTOMER",
"subjectId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"cardProfileId": "EMPLOYEE_CARD_CORPORATE_FUNDED",
"cardNetwork": "MASTERCARD",
"currency": "USD",
"status": "ACTIVE"
}
}
```
Error code: `CARD_INVALID` β the card does not exist / does not belong to your partnership.
## Listing cards
```http theme={null}
GET /open-api-corp/card/v1/list?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&subjectType=EMPLOYEE&status=ACTIVE&page=1&pageSize=20
```
A paginated query over the cards under your partnership; all filters are optional:
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | No | β€50; filter by owning company |
| `subjectType` | String | No | `COMPANY` / `CUSTOMER` |
| `subjectId` | String | No | β€50; filter by holder |
| `status` | String | No | Filter by card status: `ACTIVE` / `FROZEN` / `LOCKED` / `RESTRICTED` / `LOST` / `EXPIRED` / `CLOSED` |
| `page` | Integer | No | Starts at 1, default 1 |
| `pageSize` | Integer | No | 1\~100, default 20 |
**Response data**: the paging envelope is `page` (current page, Integer), `pageSize` (page size, Integer), `total` (total count) and `result` (the cards); each element of `result` carries exactly the same fields as the single-card query response above (`cardId` / `panFirst6` / `panLast4` / `companyId` / `subjectType` / `subjectId` / `cardProfileId` / `cardNetwork` / `cardProfileId` / `currency` / `status`).
## Next steps
* SHARED cards need bindable velocity rules before issuance: [Setting spend limits](./spend-limits)
* Display the PAN and CVV2 once the card is created: [Retrieving secure card details](./secure-card-details)
# Transactions Β· Overview
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/authorization-and-3ds
Real-time authorization completes on the DCS side with results pushed as CARD_TRANSACTION; 3DS challenges follow one of three modes, with the authenticate endpoint returning OOB results; settlement and debt arrive as events.
## π Guide
Unlike issuing or transfers, the transaction leg needs no synchronous action from you β authorization completes on the DCS side in real time, and your job is to **consume the events well**: record authorization results, take your configured part in 3DS challenges, and update your books when settlement and debt events arrive. The life-cycle concepts are in [The transaction life cycle](../basic-concepts/transaction-lifecycle).
## Real-time authorization
When the cardholder pays, DCS checks the entity's status, the velocity rules and the available balance in real time, and pushes the webhook `CARD_TRANSACTION` (`status=APPROVED` / `DECLINED`). Key payload fields:
| Field | Description |
| - | - |
| `cardId` / `panLast4` | The card ID and last four digits |
| `status` | `APPROVED` / `DECLINED` |
| `direction` | Fund direction (`DEBIT` for purchases) |
| `originalAmount` / `originalCurrency` | Original amount and currency |
| `postAmount` / `postCurrency` | Settlement amount and currency |
| `transactionCategory` | The nature of the transaction (e.g. `PURCHASE`) |
| `mcc` / `merchantName` / `merchantCountryCode` | Merchant details |
| `transactionId` | The transaction identifier, for matching against records and statements |
## Handling 3DS challenges
When an online purchase triggers 3DS, DCS pushes the webhook `AUTHORISATION_3DS_CHALLENGE`; handle it per the mode configured at onboarding (the mode is set per card BIN range on the issuing side; the division of work is detailed in [Handling 3DS challenges](./3ds-challenges)):
| Mode | Challenge delivery | What you do |
| - | - | - |
| Issuer OTP | DCS sends the code to the cardholder directly (email / SMS) | Nothing β you are not involved |
| OTP\_DELEGATE | The webhook carries the encrypted code and the cardholder's phone / email (AES-GCM ciphertexts sharing one `iv`) | Decrypt and deliver the code through your own channel; **no result callback** |
| OOB | The webhook carries the basic transaction details | Notify the cardholder's app β they verify in-app β call authenticate with the result |
**Key challenge fields**: `challengeId`, `status`, `cardId` / `companyId`, `expiryTime`, the amount and currency, merchant details (`merchantName` / `merchantCountryCode` / `mcc`) and `challengeFlowType` (the mode); OTP\_DELEGATE additionally carries `encryptedOtpPasscode` / `phoneNumber` / `email` (ciphertexts) and `iv`.
**The OOB callback**: `POST /open-api-corp/card3ds/v1/authenticate` β send `challengeId` (from the challenge event) + `action` (`APPROVE` / `REJECT`) + optional `operateTime`. Repeating the same result is idempotent; calling it against an OTP\_DELEGATE challenge returns `CARD_3DS_CHALLENGE_NOT_FOUND`.
**Challenges are valid for about 300 seconds** and time out as rejected; the result-confirmation endpoint applies to OOB mode only.
## Settlement and debt
* **Settlement**: card settlement pushes `CARD_TRANSACTION_SETTLEMENT`; the transaction moves from an authorization hold (`UNPOSTED`) to posted (`POSTED`) and lands on the cycle's statement β records and statement queries are in [Funding and reconciliation](./funding-and-reconciliation).
* **Debt**: delayed settlement or merchant over-capture can drive the available balance negative; the account enters a debt state and `CARD_TRANSACTION_DEBT` is pushed β remind the customer to repay on schedule (repayment state in the statement's `paymentStatus`).
## Next steps
* Shape approval rates with velocity rules: [Setting spend limits](./spend-limits)
* Post-settlement records and statements: [Funding and reconciliation](./funding-and-reconciliation)
* In this group: [Real-time authorization and settlement](./realtime-authorization) Β· [Handling 3DS challenges](./3ds-challenges)
# Balances and Transfers
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/balances-and-transfers
Query a funding owner's per-currency balances (availableAmount and the authorization gate), and move funds between the company pool and dedicated-balance cards in a same-currency closed loop: submitting with transfer and polling the result with transfer-query.
## π Guide
The balance is both the precondition for spending and the real-time authorization gate. This page covers two things: reading balances (`balance`), and moving funds between the company pool and dedicated-balance cards (`transfer` + `transfer-query`).
The funding model in one sentence: SHARED cards hold no balance and spend straight from the company pool; DEDICATED cards hold their own balance and stop when it runs out. Who holds the balance and who can be a transfer endpoint is in [Holders and the funding model](../basic-concepts/identity-and-funding); how money comes in is in [Deposits](./deposits).
## Query balances
`GET /open-api-corp/fund/v1/balance` returns a funding owner's balances per currency.
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `subjectType` | String | Yes | `COMPANY` / `CARD` |
| `subjectId` | String | Yes | β€50; company ID or card ID |
| `currency` | String | No | `USD` / `HKD`; omit to return every currency |
**Response data** carries `subjectType`, `subjectId` and the per-currency list `balances`, one element per currency:
| Field | Description |
| - | - |
| `currency` | Currency: `USD` / `HKD` |
| `availableAmount` | Current available balance, paired with `currency` |
**Request example**
```http theme={null}
GET /open-api-corp/fund/v1/balance?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&subjectType=COMPANY&subjectId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"subjectType": "COMPANY",
"subjectId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"balances": [
{ "currency": "USD", "availableAmount": "1000.00" },
{ "currency": "HKD", "availableAmount": "8000.00" }
]
}
}
```
**Error codes**
| Code | Description |
| - | - |
| `SUBJECT_INVALID` | The funding owner (company / card) does not exist or is invalid |
**The balance acts as the real-time authorization gate**: a SHARED card spends straight from the company pool, a DEDICATED card from its own balance, stopping when it runs out. The authorization flow is in [Authorization and 3DS](./authorization-and-3ds).
## Transfer funds
`POST /open-api-corp/fund/v1/transfer` moves funds between the company pool and dedicated-balance cards β same currency, no FX, closed loop within one company: COMPANYβCARD funds a card (allocates budget), CARDβCOMPANY sweeps the balance back.
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `transferRef` | String | Yes | β€64; your unique transfer identifier (idempotency key), character set `^[A-Za-z0-9_-]+$` |
| `from.subjectType` | String | Yes | Source subject type: `COMPANY` / `CARD` |
| `from.subjectId` | String | Yes | β€36; `COMPANY`β`companyId`, `CARD`β`cardId` (β€32) |
| `to.subjectType` | String | Yes | Destination subject type: `COMPANY` / `CARD` (must differ from `from.subjectType`) |
| `to.subjectId` | String | Yes | β€36; same rules as `from.subjectId` |
| `amount` | BigDecimal | Yes | Greater than 0, at most 2 decimals |
| `currency` | String | Yes | `USD` / `HKD`, must match both accounts' currency |
| `remark` | String | No | β€256; remark |
**Response data**
| Field | Type | Description |
| - | - | - |
| `transferId` | String | External id of the transfer record |
| `status` | String | `SUCCESS` / `PROCESSING` / `FAILED` |
| `completeTime` | String | Completion time, ISO-8601 |
**`PROCESSING` is not a final state**: you must poll `GET /open-api-corp/fund/v1/transfer-query` with the same `transferRef` until it returns `SUCCESS` or `FAILED` before acting on the result.
**Request example**
```json theme={null}
{
"transferRef": "ext-transfer-0001",
"from": { "subjectType": "COMPANY", "subjectId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f" },
"to": { "subjectType": "CARD", "subjectId": "5185740066240790530" },
"amount": "1000.00",
"currency": "USD",
"remark": "monthly top-up"
}
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"transferId": "5185740066240790531",
"status": "SUCCESS",
"completeTime": "2026-08-19T10:30:00Z"
}
}
```
**Error codes**
| Code | Description |
| - | - |
| `TRANSFER_DUPLICATE` | The `transferRef` was already submitted |
| `SUBJECT_INVALID` | The company / card does not exist, is not yours, or the two sides belong to different companies |
| `CARD_NOT_DEDICATED` | The card is not a dedicated-balance card (a SHARED card holds no balance and cannot be a transfer endpoint) |
| `CARD_STATE_INVALID` | The card state does not allow transfers; only ACTIVE / FROZEN |
| `CURRENCY_MISMATCH` | The currency does not match the card |
| `INSUFFICIENT_FUNDS` | The sender's balance is insufficient |
If a transfer request times out on the cardNetwork and you never see a response, retry with the **same** `transferRef`: if the first attempt was accepted you get `TRANSFER_DUPLICATE`, in which case switch to `transfer-query` for the result β the funds are never moved twice.
## Query a transfer result
`GET /open-api-corp/fund/v1/transfer-query` returns the final result of one transfer, looked up by `transferRef`.
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `transferRef` | String | Yes | β€64; the unique transfer identifier used at submission |
**Response data**
| Field | Type | Description |
| - | - | - |
| `transferId` | String | External id of the transfer record |
| `status` | String | `SUCCESS` / `PROCESSING` (in progress) / `FAILED` |
| `completeTime` | String | Completion time, ISO-8601 |
**Request example**
```http theme={null}
GET /open-api-corp/fund/v1/transfer-query?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&transferRef=ext-transfer-0001
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"transferId": "5185740066240790531",
"status": "SUCCESS",
"completeTime": "2026-08-19T10:30:00Z"
}
}
```
**Error codes**
| Code | Description |
| - | - |
| `TRANSFER_NOT_FOUND` | The transfer cannot be found, or belongs to another partner (existence is not disclosed) |
## Next steps
* Fund the company pool or a dedicated-balance card: [Deposits](./deposits)
* How transfers and spending land on statements and in the records (internal allocations count into `totalDebitAmount`): [Statements and Transactions](./statements-and-transactions)
# Binding and unbinding
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/binding-and-unbinding
Bind velocity rules to cards or employees: batch bind/unbind with partial-success semantics, batch-level and per-item errors, and binding queries in both directions.
## π Guide
Rules and targets (card `CARD` / employee `CUSTOMER`) are bound many-to-many: a rule constrains nothing after creation and only takes effect once bound. Both bind and unbind are batch endpoints that address **one rule and a batch of subjects**: rule-level problems (missing / not ACTIVE / not owned by the company) **reject the whole batch**, while subject-level problems are **item-independent with partial success** β the response splits results into `successTargets` and `failTargets`, with per-item reasons in `failTargets[].errorCode`. Rule creation is covered in [Creating velocity rules](./creating-rules); the full code set is in the [Error code dictionary](../customer-success/error-codes).
Besides the `bind` endpoint on this page, rules can also be bound at issue time through the `ruleIds` field of the card application (no more than 5) β see [Applying for virtual cards](./applying-virtual-cards). Whichever path you take, **a single card or a single employee carries at most 5 rules**.
## Batch bind
**`POST /open-api-corp/velocity/v1/bind`**
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the company that owns the rule and the subjects |
| `ruleId` | String | Yes | β€50; the rule to bind (one request addresses a single rule) |
| `targets` | Array | Yes | non-empty; the subjects to bind |
| `targets[].subjectType` | Enum | Yes | `CARD` / `CUSTOMER`; any other value rejects the whole batch (`DAPI_PARAM_INVALID`) |
| `targets[].subjectId` | String | Yes | β€36; `cardId` (numeric string) for `CARD` / `customerId` for `CUSTOMER` |
### Response data
| Field | Type | Description |
| - | - | - |
| `successTargets` | Array | Successful entries |
| `successTargets[].subjectType` | Enum | `CARD` / `CUSTOMER` |
| `successTargets[].subjectId` | String | The subject's identifier |
| `successTargets[].errorCode` | String | Always `null` for successful entries |
| `successTargets[].errorMessage` | String | Always `null` for successful entries |
| `failTargets` | Array | Failed entries, same shape; `errorCode` / `errorMessage` carry the per-item failure reason |
> The endpoint returns no `successCount` / `failCount`: both equal the array lengths, and a second source of truth is avoided on purpose.
Per-item `errorCode` values for bind:
| errorCode | Description |
| - | - |
| `SUBJECT_INVALID` | Subject missing, not ACTIVE, or not under the given company |
| `CARD_RULE_DUPLICATE` | The subject is already bound to this rule (no silent dedup) |
| `CARD_RULE_CURRENCY_NOT_MATCH` | No overlap between the rule's currencies and the card's chargeable currencies (chargeable = opened VA currencies βͺ card BIN currency) |
| `CARD_RULE_LIMIT_EXCEEDED` | The subject already has the maximum number of rules (5) |
### Request example
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"ruleId": "5136744097353943556",
"targets": [
{ "subjectType": "CARD", "subjectId": "5136759791164443137" },
{ "subjectType": "CUSTOMER", "subjectId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c" },
{ "subjectType": "CARD", "subjectId": "5136765336134994434" }
]
}
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"successTargets": [
{ "subjectType": "CARD", "subjectId": "5136759791164443137",
"errorCode": null, "errorMessage": null },
{ "subjectType": "CARD", "subjectId": "5136765336134994434",
"errorCode": null, "errorMessage": null }
],
"failTargets": [
{ "subjectType": "CUSTOMER", "subjectId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"errorCode": "SUBJECT_INVALID",
"errorMessage": "target not found or not active" }
]
}
}
```
### Batch-level error codes
| Error code | Description |
| - | - |
| `COMPANY_INVALID` | Company missing, not ACTIVE, or not owned by this partner |
| `VELOCITY_RULE_NOT_FOUND` | Rule missing, or not owned by the given company β whole batch rejected |
| `VELOCITY_RULE_STATUS_INVALID` | Rule not ACTIVE, cannot be bound β whole batch rejected |
| `DAPI_PARAM_INVALID` | Any entry's `subjectType` is not `CARD` / `CUSTOMER` (whole batch rejected) |
| `VELOCITY_BATCH_SIZE_EXCEEDED` | More than 200 `targets` in one call β whole batch rejected |
## Batch unbind
**`POST /open-api-corp/velocity/v1/unbind`**
Same shape as bind: one request addresses a single rule, items are processed independently, and one failure does not affect the rest.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the company that owns the rule and the subjects |
| `ruleId` | String | Yes | β€50; the rule to unbind |
| `targets` | Array | Yes | non-empty; the subjects to unbind |
| `targets[].subjectType` | Enum | Yes | `CARD` / `CUSTOMER` |
| `targets[].subjectId` | String | Yes | β€36; `cardId` (numeric string) for `CARD` / `customerId` for `CUSTOMER` |
The response has the same shape as bind (`successTargets` / `failTargets`); per-item `errorCode` values for unbind:
| errorCode | Description |
| - | - |
| `SUBJECT_INVALID` | Subject missing, not ACTIVE, or not under the given company |
| `VELOCITY_LAST_PROTECTING_RULE` | This is the last active rule on a company-pool card; bind a replacement rule before unbinding |
Unbinding a subject that is not bound is not an error and counts in `successTargets`.
### Request example
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"ruleId": "5136744097353943556",
"targets": [
{ "subjectType": "CARD", "subjectId": "5136759791164443137" },
{ "subjectType": "CUSTOMER", "subjectId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c" },
{ "subjectType": "CARD", "subjectId": "5136765336134994434" }
]
}
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"successTargets": [
{ "subjectType": "CUSTOMER", "subjectId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"errorCode": null, "errorMessage": null },
{ "subjectType": "CARD", "subjectId": "5136765336134994434",
"errorCode": null, "errorMessage": null }
],
"failTargets": [
{ "subjectType": "CARD", "subjectId": "5136759791164443137",
"errorCode": "SUBJECT_INVALID",
"errorMessage": "target not found or not active" }
]
}
}
```
### Batch-level error codes
| Error code | Description |
| - | - |
| `COMPANY_INVALID` | Company missing, not ACTIVE, or not owned by this partner |
| `VELOCITY_RULE_NOT_FOUND` | Rule missing, or not owned by the given company |
| `DAPI_PARAM_INVALID` | Any entry's `subjectType` is not `CARD` / `CUSTOMER` (whole batch rejected) |
| `VELOCITY_BATCH_SIZE_EXCEEDED` | More than 200 `targets` in one call β whole batch rejected |
## Listing a rule's targets
**`GET /open-api-corp/velocity/v1/list-rule-targets`** β lists the cards and employees bound to a rule, paginated, newest binding first.
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50 |
| `ruleId` | String | Yes | β€50 |
| `page` | Integer | No | Page number, from 1, default 1 |
| `pageSize` | Integer | No | Page size, default 20, capped at 100 (values above are truncated to 100) |
The response `data` is a page: `total` / `page` / `pageSize` plus `result[]`, each entry carrying `subjectType` (`CARD` / `CUSTOMER`), `subjectId`, and `modifyTime` (ISO-8601).
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"total": 2,
"result": [
{ "subjectType": "CUSTOMER", "subjectId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c", "modifyTime": "2026-08-12T11:05:00Z" },
{ "subjectType": "CARD", "subjectId": "5136759791164443137", "modifyTime": "2026-08-12T11:00:00Z" }
]
}
}
```
| Error code | Description |
| - | - |
| `VELOCITY_RULE_NOT_FOUND` | Rule does not exist, or does not belong to the requesting `companyId` (no empty page is returned β an empty page would be indistinguishable from "the rule exists but has no targets") |
Returning an error instead of an **empty page** when the rule does not exist is deliberate: an empty page would be indistinguishable from "the rule exists but has no targets". When you receive an empty `result`, you can be sure the rule exists and simply has nothing bound to it yet.
## Listing the rules constraining a target
**`GET /open-api-corp/velocity/v1/list-target-rules`** β lists the rules constraining a card / employee, paginated.
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50 |
| `subjectType` | Enum | Yes | `CARD` / `CUSTOMER` |
| `subjectId` | String | Yes | β€50; `cardId` (numeric string) for `CARD` / `customerId` for `CUSTOMER` |
| `status` | Boolean | No | Default `true`: return `ACTIVE` rules only; pass `false` to include `INACTIVE` |
| `page` | Integer | No | Page number, from 1, default 1 |
| `pageSize` | Integer | No | Page size, default 20, capped at 100 (values above are truncated to 100) |
The response `data` is a page: `total` (the number of rules bound to this target) / `page` / `pageSize` plus `result[]`, each entry carrying `ruleId`, `ruleName`, `status` (`ACTIVE` / `INACTIVE`), and `modifyTime`.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"total": 2,
"result": [
{ "ruleId": "R000457", "ruleName": "Cash advance control", "status": "ACTIVE", "modifyTime": "2026-08-12T11:10:00Z" },
{ "ruleId": "R000456", "ruleName": "Employee travel", "status": "ACTIVE", "modifyTime": "2026-08-12T11:00:00Z" }
]
}
}
```
| Error code | Description |
| - | - |
| `DAPI_PARAM_INVALID` | `subjectType` is not `CARD` / `CUSTOMER` |
## Next steps
* Query the merged quota and current usage once targets take effect: [Quota query and adjustment](./quota-and-adjustment)
* How bound rules participate in authorization: [Authorization and 3DS](./authorization-and-3ds)
# Company status and alerts
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/company-maintenance
Query an onboarded company's details, freeze and unfreeze it by capability domain (idempotent set semantics), and set per-currency low-balance alerts on its funding pools (full-replacement semantics) β with field tables, examples and error codes.
## π Guide
Whether for routine operational checks, risk handling or treasury management, the three endpoints on this page maintain a company that has already been onboarded: query its details (`query`), freeze and unfreeze it (`update-state`), and configure low-balance alerts (`balance-alert-set`). All three locate the company by the `companyId` returned on successful onboarding; for the onboarding and review flow, see [Company onboarding and review](./company-onboarding).
## Query company details
**`GET /open-api-corp/company/v1/query`**
Query an onboarded company's basic details, funding-pool currencies and current status.
### Request parameters
| Field | Type | Required | Description |
| - | - | :-: | - |
| `companyId` | string | Yes | β€50; the company ID |
```http theme={null}
GET /open-api-corp/company/v1/query
```
### Response fields (`data`)
| Field | Type | Description |
| - | - | - |
| `companyId` | string | Company ID |
| `companyApplyRef` | string | Your identifier |
| `companyName` | string | Company legal name |
| `companyRegistrationNumber` | string | Registration number |
| `fundingCurrencies` | string\[] | Funding-pool currency list |
| `status` | string | Company status (merged view): life cycle `ACTIVE` / `TERMINATED`, overlaid with the behavioral states `SUSPENDED` / `FROZEN` / `RESTRICTED` |
The company status is presented as one merged field: the life cycle overlaid with behavioral states. Whether the company can issue cards or spend comes down to whether it is `ACTIVE`. For the state machine semantics, see [State machines and freezing](../basic-concepts/states-and-freezing).
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"companyApplyRef": "ext-company-001",
"companyName": "EXAMPLE COMPANY LIMITED",
"companyRegistrationNumber": "CR1234567",
"fundingCurrencies": ["USD", "HKD"],
"status": "ACTIVE"
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `COMPANY_INVALID` | The company does not exist (or does not belong to this partner) |
## Freeze and unfreeze
**`POST /open-api-corp/company/v1/update-restrictions`**
Freeze or unfreeze a company by capability domain, with idempotent set semantics: the company's behavioral state is a set β `addRestrictions` puts a capability-domain code into the set (freezing it) and `removeRestrictions` takes it out (unfreezing it), and repeating the same operation yields the same result.
### The five capability-domain codes
Freezing applies per capability domain, and one call may carry several:
| Capability-domain code | What a freeze disables |
| - | - |
| `ACCOUNT_FROZEN` | The account as a whole (login, password change, phone/email change, MFA binding, account closure) |
| `CASH_IN_FROZEN` | Pay-in / top-up |
| `CASH_OUT_FROZEN` | Pay-out / withdrawal / remittance |
| `PAYMENT_FROZEN` | Payment / spending (card payments included) |
| `CARD_FROZEN` | Card-management actions (card application, activation, PIN change) |
`addRestrictions` / `removeRestrictions` accept only the 5 open L1 capability-domain codes above; anything else (including `KYC_FROZEN` / `TRANSFER_FROZEN`, L2 fine-grained codes, and `SUSPENDED` / `RESTRICTED`) returns `DAPI_PARAM_INVALID`. Restricted states written by risk, regulatory or judicial processes cannot be lifted through this endpoint.
### Request parameters
| Field | Type | Required | Description |
| - | - | :-: | - |
| `companyId` | string | Yes | β€50; the target company |
| `addRestrictions` | string\[] | No | Freeze codes; values restricted to the 5 L1 capability-domain codes; several may be passed |
| `removeRestrictions` | string\[] | No | Unfreeze codes; same value set as `addRestrictions`; several may be passed |
| `remark` | string | No | β€256; operation remark |
### Request example: freeze (add behavioral states)
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"addRestrictions": ["PAYMENT_FROZEN", "CARD_FROZEN"],
"remark": "suspicious transactions under review"
}
```
### Request example: unfreeze (remove behavioral states)
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"removeRestrictions": ["PAYMENT_FROZEN", "CARD_FROZEN"],
"remark": "review cleared"
}
```
### Response example
An action endpoint: `data` is empty on success.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": null
}
```
After a state change, DCS pushes the company status-change notification (for the event name, see the webhook event table in the API reference); its payload carries the `addRestrictions` / `removeRestrictions` of this operation.
### Error codes
| Error code | Description |
| - | - |
| `COMPANY_INVALID` | The company does not exist (or does not belong to this partner) |
| `STATUS_CONFLICT` | The current status does not allow this operation (for example, the company is not `ACTIVE`) |
## Low-balance alerts
**`POST /open-api-corp/fund/v1/balance-alert-set`**
Set per-currency low-balance alerts on the company's funding pools.
**Full-replacement semantics**: every call resets the entire alert configuration, so always send the complete list rather than an increment.
### Request parameters
| Field | Type | Required | Description |
| - | - | :-: | - |
| `companyId` | string | Yes | β€50; the company ID |
| `balanceSettings` | array | Yes | Per-currency thresholds, at least 1 entry; no duplicate currencies (a duplicate returns `DAPI_PARAM_INVALID`); the currency must be one of the company's opened funding-pool currencies |
| `balanceSettings[].currency` | string | Yes | `USD` / `HKD` |
| `balanceSettings[].threshold` | BigDecimal | Yes | The threshold (the alert triggers when the available balance falls below it; two decimal places, non-negative) |
| `emailSettings` | string\[] | No | β€40 per entry; notification mailboxes (company-level, several allowed, each an email address); omit it to receive webhooks only |
### Request example
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"balanceSettings": [
{ "currency": "USD", "thresholdAmount": "1000.00" },
{ "currency": "HKD", "thresholdAmount": "8000.00" }
],
"emailSettings": ["finance@example.com"]
}
```
### Response example
An action endpoint: `data` is empty on success.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": null
}
```
### Trigger behavior (`LOW_BALANCE`)
* When a funding pool's available balance falls below the threshold for its currency, DCS pushes the `LOW_BALANCE` webhook (the funding-pool low-balance alert).
* Only the webhook fires by default; email is sent additionally only when `emailSettings` is configured.
* The same alert fires once a day until the balance is topped back up.
### Error codes
| Error code | Description |
| - | - |
| `SUBJECT_INVALID` | The company (the funding-pool holder) does not exist or is invalid |
| `BALANCE_ALERT_CURRENCY_NO_POOL` | The alert currency has no corresponding funding pool |
## Next steps
* Top up the funding pool and reconcile after a low-balance alert: [Funding and reconciliation](./funding-and-reconciliation)
* Review the whole flow from onboarding to maintenance: [Managing companies](./managing-companies)
# Company onboarding and review
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/company-onboarding
Submit a company onboarding application for asynchronous KYB review, query the progress and outcome, and resubmit a rejected application under the same companyApplyId β with full field tables, request/response examples and error codes.
## π Guide
Whether you serve trading firms, Web3 projects or cross-border merchants, the three endpoints on this page take your business customer through onboarding: submit the application (`apply`), query the outcome (`query-apply`), and resubmit after a rejection (`resubmit`). Onboarding is asynchronous β the synchronous response carries only the application record with status `PENDING`, and acceptance is not approval. Rely on the webhook for the final result, with the query endpoint as the polling fallback. For the overall call sequence, see the overview page [Managing companies](./managing-companies).
## Submit the onboarding application
**`POST /open-api-corp/company/v1/apply`**
Once you submit the company details, DCS runs corporate due diligence (KYB) asynchronously. Acceptance returns an `companyApplyId`, and `status` in the synchronous response is always `PENDING`.
### Request parameters
| Field | Type | Required | Description |
| - | - | :-: | - |
| `companyApplyRef` | string | Yes | β€64; your unique company identifier (the idempotency key); charset `^[A-Za-z0-9_-]+$` |
| `companyName` | string | Yes | β€256; company legal name; English letters, digits and spaces only |
| `companyRegistrationNumber` | string | Yes | β€64; company registration number |
| `email` | string | Yes | β€40; company contact email |
| `fundingCurrencies` | string\[] | Yes | Funding-pool currencies, `USD` / `HKD`, one or more (a funding-pool virtual account, VA, is created per currency); no empty elements and no duplicate currencies β empty, duplicate or non-enum values return `DAPI_PARAM_INVALID`; the set must also be a subset of the currencies allowed for your enterprise, and anything outside that range rejects the whole request (no partial opening) with `CURRENCY_NOT_ALLOWED` |
**Deduplication runs on three dimensions**: `companyApplyRef`, `email` and `companyRegistrationNumber` are each unique within one partner, and the latter two are **never released, even after a rejection**. Reapplying under an `companyApplyRef` that already has an in-flight or approved application returns `APPLY_DUPLICATE`; if only a rejected application exists, it returns `APPLY_REJECTED_USE_RESUBMIT` β the right path is `resubmit` under the same `companyApplyId`, never a fresh ID.
### Request example
```json theme={null}
{
"companyApplyRef": "ext-company-001",
"companyName": "EXAMPLE COMPANY LIMITED",
"companyRegistrationNumber": "CR1234567",
"email": "finance@example.com",
"fundingCurrencies": ["USD", "HKD"]
}
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"companyApplyId": "5136744097353943553",
"companyApplyRef": "ext-company-001",
"status": "PENDING"
}
}
```
### Response fields (`data`)
| Field | Type | Description |
| - | - | - |
| `companyApplyId` | string | The onboarding application ID; quote it in later queries and resubmissions |
| `companyApplyRef` | string | Your identifier, echoed back |
| `status` | string | Always `PENDING` (accepted; KYB in progress) |
### Error codes
| Error code | Description |
| - | - |
| `APPLY_DUPLICATE` | An in-flight or approved application already exists under this `companyApplyRef`; it cannot be submitted again |
| `APPLY_REJECTED_USE_RESUBMIT` | Only a rejected application exists under this `companyApplyRef` β use the resubmission endpoint instead (a duplicate `apply` is no longer allowed) |
| `EMAIL_DUPLICATE` | This `email` is already taken under the same partner (enterprise-wide, any status including rejected) |
| `COMPANY_REGISTRATION_DUPLICATE` | The same `companyRegistrationNumber` has already been applied for under this partner (enterprise-wide, any status including rejected) |
| `CURRENCY_NOT_ALLOWED` | A requested currency is outside the list allowed for your enterprise (`message` names the offending currency) |
## Query the onboarding application
**`GET /open-api-corp/company/v1/query-apply`**
Query the review progress and outcome of an onboarding application. Rely on webhooks first, with this endpoint as the polling fallback.
### Request parameters
| Field | Type | Required | Description |
| - | - | :-: | - |
| `companyApplyId` | string | Conditional | β€50; the onboarding application ID; pass this or `companyApplyRef` |
| `companyApplyRef` | string | Conditional | β€64; your own onboarding application reference; pass this or `companyApplyId`. It is the fallback when the create call timed out and the webhook was lost β your side only has the ref |
```http theme={null}
GET /open-api-corp/company/v1/query-apply?companyApplyId=5136744097353943553
```
### Response fields (`data`)
| Field | Type | Description |
| - | - | - |
| `companyApplyId` | string | Application ID |
| `companyApplyRef` | string | Your identifier |
| `status` | string | `PENDING` (under review; KYB in progress) / `SUCCEED` (onboarded) / `REJECTED` (refused by KYB) |
| `rejectMessage` | string | The rejection reason (present when `REJECTED`) |
| `companyId` | string | The company ID once the entity exists, present only after `SUCCEED` β persist it; every later company endpoint uses it |
### Response example: onboarded
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"companyApplyId": "5136744097353943553",
"companyApplyRef": "ext-company-001",
"status": "SUCCEED",
"rejectMessage": null,
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f"
}
}
```
### Response example: rejected
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"companyApplyId": "5136744097353943553",
"companyApplyRef": "ext-company-001",
"status": "REJECTED",
"rejectMessage": "registration number mismatch",
"companyId": null
}
}
```
The envelope's `code=SYS_SUCCESS` only means the query itself succeeded; judge the business outcome by `data.status`. When it is `REJECTED`, read `rejectMessage` for the cause, correct the details and use the resubmission endpoint below.
### Error codes
| Error code | Description |
| - | - |
| `APPLY_NOT_FOUND` | The onboarding application does not exist (or does not belong to this partner) |
## Resubmit after a rejection
**`POST /open-api-corp/company/v1/resubmit`**
After a KYB rejection, correct the details and send the same `companyApplyId` back for review; `companyApplyRef` cannot change. Resubmission is allowed only while the application's current status is `REJECTED` β in-flight or approved applications need no resubmission and are not accepted, returning `STATUS_CONFLICT`.
### Request parameters
| Field | Type | Required | Description |
| - | - | :-: | - |
| `companyApplyId` | string | Yes | β€50; the rejected onboarding application ID |
| `companyName` | string | Yes | β€256; the corrected company legal name; English letters, digits and spaces only |
| `companyRegistrationNumber` | string | Yes | β€64; the corrected company registration number |
### Request example
```json theme={null}
{
"companyApplyId": "5136744097353943553",
"companyName": "EXAMPLE COMPANY LIMITED",
"companyRegistrationNumber": "CR7654321"
}
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"companyApplyId": "5136744097353943553",
"companyApplyRef": "ext-company-001",
"status": "PENDING"
}
}
```
Once the resubmission is accepted, the application returns to `PENDING`, and you get the outcome exactly as for a first submission (webhook first, `query-apply` as fallback).
### Error codes
| Error code | Description |
| - | - |
| `APPLY_NOT_FOUND` | The onboarding application does not exist (or does not belong to this partner) |
| `STATUS_CONFLICT` | The application is not currently `REJECTED`, so it cannot be resubmitted (in-flight or approved applications need no resubmission) |
## Related webhooks
DCS pushes the onboarding outcome to you; two events cover the two terminal results:
| Event `type` | Trigger | Payload highlights |
| - | - | - |
| `COMPANY_CREATED` | KYB passed; company onboarded | Carries `companyId`; the company is `ACTIVE` and ready for the next steps |
| `COMPANY_REJECTED` | KYB refused | Carries `rejectMessage` (the rejection cause); correct the details and resubmit |
The shared event envelope (each event's business payload sits inside `data`):
```json theme={null}
{
"webhookId": "7800000000000000900",
"webhookType": "COMPANY_CREATED",
"notificationTime": 1717211400000,
"data": { }
}
```
For the envelope structure, signature verification, retries and idempotency rules, see the [Quickstart](../getting-started/quickstart).
## Next steps
* Once onboarded and holding a `companyId`, maintain the company's status and balance alerts: [Company status and alerts](./company-maintenance)
* Create cardholders once the company is `ACTIVE`: [Managing employees](./managing-employees)
# Creating velocity rules
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/creating-rules
Full field reference for creating a velocity rule: purchase and withdrawal amount/count limits, currency, MCC and region lists, ruleRef idempotency, and every error code.
## π Guide
A velocity rule is a standalone, company-owned object assembled from five control groups: `purchaseControl` (purchases), `cashAdvanceControl` (cash withdrawals), `currencyControl` (currency list), `mccControl` (merchant-category list), and `geographicControl` (region list). A newly created rule is `ACTIVE`, yet constrains nothing until it is bound to a card or an employee β binding is covered in [Binding and unbinding](./binding-and-unbinding), and the overall model in [Setting spend limits](./spend-limits).
## Create a rule
**`POST /open-api-corp/velocity/v1/create-rule`**
Configure **at least one** of the five control groups β sending none returns `DAPI_PARAM_INVALID`. **"Unlimited" is expressed by omitting the field; never send 0 or a negative number** β doing so returns `VELOCITY_LIMIT_VALUE_INVALID`.
Two different axes β do not conflate them: amount limits match on the **settlement currency**, whereas the `currencyControl` list judges the **original purchase currency**.
### Request parameters: top level
| Field | Type | Required | Description |
| - | - | - | - |
| `ruleRef` | String | Yes | β€64; client-generated idempotency key; retrying with the same company + `ruleRef` returns the first result |
| `companyId` | String | Yes | β€50; the company the rule belongs to |
| `ruleName` | String | Yes | β€128; rule name |
| `remark` | String | No | β€256; rule remark |
`ruleRef` is an idempotency key: after a cardNetwork timeout, retry with the same `ruleRef` and DCS will not create a duplicate rule β it returns the result of the first creation unchanged.
### Request parameters: purchaseControl
Omit `purchaseControl` to leave purchases unlimited.
| Field | Type | Required | Description |
| - | - | - | - |
| `purchaseControl.amountLimits` | Array | No | Purchase amount limits, one entry per currency |
| `purchaseControl.amountLimits[].currency` | String | Yes | 3; three-letter currency code (`^[A-Z]{3}$`), e.g. USD |
| `purchaseControl.amountLimits[].perTransaction` | String | No | Per-transaction limit, > 0 |
| `purchaseControl.amountLimits[].daily` | String | No | Daily cumulative limit |
| `purchaseControl.amountLimits[].monthly` | String | No | Monthly cumulative limit |
| `purchaseControl.amountLimits[].quarterly` | String | No | Quarterly cumulative limit |
| `purchaseControl.amountLimits[].yearly` | String | No | Yearly cumulative limit |
| `purchaseControl.countLimits` | Object | No | Purchase count limits, not per currency β summed across all currencies |
| `purchaseControl.countLimits.daily` | Integer | No | Daily cumulative count, > 0 |
| `purchaseControl.countLimits.monthly` | Integer | No | Monthly cumulative count |
| `purchaseControl.countLimits.quarterly` | Integer | No | Quarterly cumulative count |
| `purchaseControl.countLimits.yearly` | Integer | No | Yearly cumulative count |
### Request parameters: cashAdvanceControl
Omit `cashAdvanceControl` to leave withdrawals unlimited. The one difference from the purchase group is the group-level switch `allowed`: whenever this group is configured, `allowed` is required, and `false` bans withdrawals outright.
| Field | Type | Required | Description |
| - | - | - | - |
| `cashAdvanceControl.allowed` | Boolean | Yes | Group-level switch (required within the group); `false` bans withdrawals outright |
| `cashAdvanceControl.amountLimits` | Array | No | Withdrawal amount limits, one entry per currency |
| `cashAdvanceControl.amountLimits[].currency` | String | Yes | 3; three-letter currency code (`^[A-Z]{3}$`) |
| `cashAdvanceControl.amountLimits[].perTransaction` | String | No | Per-withdrawal limit, > 0 |
| `cashAdvanceControl.amountLimits[].daily` | String | No | Daily cumulative withdrawal limit |
| `cashAdvanceControl.amountLimits[].monthly` | String | No | Monthly cumulative withdrawal limit |
| `cashAdvanceControl.amountLimits[].quarterly` | String | No | Quarterly cumulative withdrawal limit |
| `cashAdvanceControl.amountLimits[].yearly` | String | No | Yearly cumulative withdrawal limit |
| `cashAdvanceControl.countLimits` | Object | No | Withdrawal count limits, summed across all currencies |
| `cashAdvanceControl.countLimits.daily` | Integer | No | Daily cumulative withdrawal count, > 0 |
| `cashAdvanceControl.countLimits.monthly` | Integer | No | Monthly cumulative withdrawal count |
| `cashAdvanceControl.countLimits.quarterly` | Integer | No | Quarterly cumulative withdrawal count |
| `cashAdvanceControl.countLimits.yearly` | Integer | No | Yearly cumulative withdrawal count |
### Request parameters: the three list controls
All three lists share the same shape: a `type` (`WHITELIST` / `BLACKLIST`) plus a non-empty detail list.
| Field | Type | Required | Description |
| - | - | - | - |
| `currencyControl` | Object | No | Purchase-currency list, judged against the **original purchase currency** |
| `currencyControl.type` | Enum | Yes | `WHITELIST` / `BLACKLIST` |
| `currencyControl.currencies` | Array | Yes | 3 per item; non-empty; three-letter currency codes |
| `mccControl` | Object | No | Merchant-category list |
| `mccControl.type` | Enum | Yes | `WHITELIST` / `BLACKLIST` |
| `mccControl.mccList` | Array | Yes | 4 per item; non-empty; 4-digit MCCs |
| `geographicControl` | Object | No | Transaction-region list, judged against the merchant country code |
| `geographicControl.type` | Enum | Yes | `WHITELIST` / `BLACKLIST` |
| `geographicControl.countryCodes` | Array | Yes | 2 per item; non-empty; two-letter ISO country codes (uppercase) |
### Response data
The response `data` is the complete rule object: it echoes all five control groups (an unconfigured group comes back as `null`, and so does any unconfigured field within a group), plus the following platform-managed fields:
| Field | Type | Description |
| - | - | - |
| `ruleId` | String | Rule ID (β€32), platform-generated and globally unique; all subsequent operations use it |
| `companyId` | String | Owning company |
| `status` | Enum | `ACTIVE` / `INACTIVE`; always `ACTIVE` on creation |
| `ruleVersion` | Integer | Optimistic-lock version, echoed back on update / status change; 1 on creation |
| `bindingCount` | Long | Current number of bound objects; 0 on creation |
| `createTime` | String | Creation time, ISO-8601 |
| `modifyTime` | String | Update time, ISO-8601; equals `createTime` on creation |
### Request example
```json theme={null}
{
"ruleRef": "ext-request-0001",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"ruleName": "Employee travel",
"remark": "Purchase and cash-advance limits for travel cards",
"purchaseControl": {
"amountLimits": [
{ "currency": "USD", "perTransaction": "3000.00", "daily": "10000.00",
"monthly": "80000.00", "quarterly": "200000.00", "yearly": "600000.00" },
{ "currency": "HKD", "daily": "50000.00" }
],
"countLimits": { "daily": 5, "monthly": 100 }
},
"cashAdvanceControl": {
"allowed": true,
"amountLimits": [
{ "currency": "USD", "perTransaction": "500.00", "daily": "2000.00", "monthly": "10000.00" }
]
}
}
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"ruleId": "R000456",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"ruleName": "Employee travel",
"remark": "Purchase and cash-advance limits for travel cards",
"status": "ACTIVE",
"ruleVersion": 1,
"bindingCount": 0,
"createTime": "2026-08-12T10:00:00Z",
"modifyTime": "2026-08-12T10:00:00Z",
"purchaseControl": {
"amountLimits": [
{ "currency": "USD", "perTransaction": "3000.00", "daily": "10000.00",
"monthly": "80000.00", "quarterly": "200000.00", "yearly": "600000.00" },
{ "currency": "HKD", "perTransaction": null, "daily": "50000.00",
"monthly": null, "quarterly": null, "yearly": null }
],
"countLimits": { "daily": 5, "monthly": 100, "quarterly": null, "yearly": null }
},
"cashAdvanceControl": {
"allowed": true,
"amountLimits": [
{ "currency": "USD", "perTransaction": "500.00", "daily": "2000.00",
"monthly": "10000.00", "quarterly": null, "yearly": null }
],
"countLimits": null
},
"currencyControl": null,
"mccControl": null,
"geographicControl": null
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `COMPANY_INVALID` | Company missing, not ACTIVE, or not owned by this partner |
| `DAPI_PARAM_INVALID` | None of the 5 control groups is configured (structural gaps use the general parameter error) |
| `VELOCITY_LIMIT_VALUE_INVALID` | Invalid limit value: negative, malformed, over precision, or a longer period smaller than a shorter one ("unlimited" means omitting the field, not sending 0) |
| `VELOCITY_CURRENCY_INVALID` | Currency in an amount-limit group invalid or duplicated |
| `VELOCITY_LIST_CONTROL_INVALID` | List control group invalid: bad `type`, empty list, or malformed entries (MCC not 4 digits / country code not two uppercase letters) |
The full code set is in the [Error code dictionary](../customer-success/error-codes).
## Next steps
* Bind the rule to cards or employees so it takes effect: [Binding and unbinding](./binding-and-unbinding)
* Query merged effective quotas, update or toggle rules: [Quota query and adjustment](./quota-and-adjustment)
# Deposits
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/deposits
The full deposit chain: fetch the receiving virtual account, have the customer wire the funds, get notified on arrival and reconcile the deposit records β deposit-info, the BANK_TRANSFER_INFO webhook and deposit-records.
## π Guide
The funding side has one principle: fund first, spend second. Both the company pool and dedicated-balance cards are funded by the customer wiring money to their own receiving virtual account (VA): the company-level VA is created at onboarding, a card-level VA is opened per card on demand. The funding-owner concept is in [Holders and the funding model](../basic-concepts/identity-and-funding); the end-to-end funding picture is in [Funding Β· Overview](./funding-and-reconciliation).
The three deposit steps:
| Step | Action | Endpoint / event |
| - | - | - |
| β Get the receiving account | Fetch the receiving VA details for that owner and currency | `GET /open-api-corp/fund/v1/deposit-info` |
| β‘ The customer wires the funds | The customer transfers to the VA using those details | Bank-side action, no API |
| β’ Arrival notification | DCS pushes a webhook when the deposit arrives | `BANK_TRANSFER_INFO` |
Once arrived, the funds land on the owner's balance (queries and onward transfers are in [Balances and Transfers](./balances-and-transfers)); for day-to-day reconciliation pull the deposit history with `deposit-records` and match it against bank advice.
## Get the receiving account
`GET /open-api-corp/fund/v1/deposit-info` returns the receiving account (VA) details for depositing funds, for the customer to wire against.
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `subjectType` | String | Yes | `COMPANY` / `CARD` |
| `subjectId` | String | Yes | β€50; company ID or card ID |
| `currency` | String | No | `USD` / `HKD`; omit it to return every currency opened for the subject |
**Response data**
| Field | Type | Description |
| - | - | - |
| `currency` | String | `USD` / `HKD` |
| `payeeAccountNumber` | String | Receiving virtual account number |
| `payeeBankCode` | String | Bank code, `SCB` / `DBS` |
| `payeeBankName` | String | Bank name |
| `payeeAccountName` | String | Receiving account name (beneficiary account name) |
| `payeeSwiftCode` | String | SWIFT/BIC (required for cross-border wires) |
| `payeeBankAddress` | String | Bank address |
| `payeeAddress` | String | Recipient address |
| `remark` | String | Required transfer reference (e.g. quote the `companyId`) |
**Request example**
```http theme={null}
GET /open-api-corp/fund/v1/deposit-info?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&subjectType=COMPANY&subjectId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f¤cy=USD
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"currency": "USD",
"payeeAccountNumber": "74012345678901",
"payeeBankCode": "SCB",
"payeeBankName": "Standard Chartered Bank (Hong Kong) Limited",
"payeeAccountName": "EXAMPLE COMPANY LIMITED",
"payeeSwiftCode": "SCBLHKHH",
"payeeBankAddress": "4-4A Des Voeux Road Central, Hong Kong",
"payeeAddress": "Unit 1001, 10/F, Tower 1, Harbour City, Tsim Sha Tsui, Hong Kong",
"remark": "Quote companyId e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f in the transfer reference"
}
}
```
**Error codes**
| Code | Description |
| - | - |
| `SUBJECT_INVALID` | The company / card does not exist or is invalid, or the owner has no VA bound for that currency |
Make sure the paying customer fills in the transfer reference exactly as `remark` requires; for cross-border wires, hand the customer the `payeeSwiftCode` together with the bank and recipient addresses.
## Arrival notification: BANK\_TRANSFER\_INFO
When a deposit arrives, DCS pushes the webhook event `BANK_TRANSFER_INFO`; success and failure are distinguished by `status` (`SUCCESS` means success). Payload example:
```json theme={null}
{
"webhookId": "7800000000000000951", "webhookType": "BANK_TRANSFER_INFO",
"businessId": "5185740066240791001", "notificationTime": "2026-08-25T02:30:00Z",
"data": { "depositId": "DEP20260731000000455", "amount": "20000.00", "currency": "USD",
"payeeAccountNumber": "8613300012345678", "payerName": "EXAMPLE HOLDINGS PTE LTD",
"payerAccountNumber": "1234567890", "payerBankCode": "003",
"referenceCode": "REF20260731000455", "status": "SUCCESS",
"errorCode": null, "errorMessage": null }
}
```
`data` fields:
| Field | Description |
| - | - |
| `depositId` | Deposit record ID |
| `amount` / `currency` | Deposit amount and currency |
| `payeeAccountNumber` | Deposit account number |
| `payerName` | Sender account name |
| `status` | Arrival status; `SUCCESS` means success |
| `detail` | Related ledger detail reference |
The outer layer is the site-wide webhook envelope: `webhookId`, `webhookType`, `businessId` (the `depositId` for this event) and `notificationTime`.
Judge success or failure by `status` β receiving the event itself does not mean the deposit succeeded.
## Query deposit records
`GET /open-api-corp/fund/v1/deposit-records` returns the deposit (VA inbound) records, paginated, for reconciliation.
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `subjectType` | String | No | `COMPANY` / `CARD` (paired with `subjectId`) |
| `subjectId` | String | No | β€50; company ID or card ID |
| `currency` | String | No | `USD` / `HKD` |
| `startTime` | String | Yes | β€32; range start, ISO-8601 (filters by arrival time) |
| `endTime` | String | Yes | β€32; range end, ISO-8601 |
| `page` | Integer | No | From 1, default 1 |
| `pageSize` | Integer | No | 1\~100, default 20 |
**Response data** is a page object: `page` (current page), `pageSize` (page size), `total` (total count) and `result` (the deposit records). Fields of each record:
| Field | Description |
| - | - |
| `depositId` | Deposit record ID |
| `subjectType` / `subjectId` | Funding owner the deposit belongs to: `COMPANY` / `CARD` and its ID |
| `payeeAccountNumber` | The receiving virtual account number credited |
| `amount` / `currency` | Deposit amount and currency |
| `status` | Arrival status: `SUCCESS` (arrived) / `PENDING` (processing) |
| `payerName` | Sender account name |
| `payerAccountNumber` | Sender account number |
| `payerBankCode` | Sender bank |
| `completeTime` | Arrival time, ISO-8601 (what `startTime` / `endTime` filter on) |
**Request example**
```http theme={null}
GET /open-api-corp/fund/v1/deposit-records?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&subjectType=COMPANY&subjectId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f¤cy=USD&startTime=2026-08-01T00:00:00&endTime=2026-08-19T23:59:59&page=1&pageSize=20
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"total": 2,
"result": [
{
"depositId": "5185740066240790533",
"subjectType": "COMPANY",
"subjectId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"payeeAccountNumber": "74012345678901",
"amount": "1000.00",
"currency": "USD",
"status": "SUCCESS",
"payerName": "EXAMPLE COMPANY LIMITED",
"payerAccountNumber": "12345678901234",
"payerBankCode": "HSBC",
"completeTime": "2026-08-18T15:20:00Z"
},
{
"depositId": "5185740066240790534",
"subjectType": "COMPANY",
"subjectId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"payeeAccountNumber": "74012345678901",
"amount": "500.00",
"currency": "USD",
"status": "PENDING",
"payerName": "EXAMPLE COMPANY LIMITED",
"payerAccountNumber": "12345678901234",
"payerBankCode": "HSBC",
"completeTime": "2026-08-19T10:30:00Z"
}
]
}
}
```
On statements, deposits count into the repayments / top-ups total (`totalRepaymentAmount`, transactions of type `REPAYMENT`); the statement conventions are in [Statements and Transactions](./statements-and-transactions). Configuring the pool low-balance alert (`LOW_BALANCE`) is covered in [Managing companies](./managing-companies).
## Next steps
* Query balances after arrival and move funds between the company pool and dedicated-balance cards: [Balances and Transfers](./balances-and-transfers)
* How deposits show up on statements and in transaction records, and how they offset the amount due: [Statements and Transactions](./statements-and-transactions)
# Employee information and status
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/employee-maintenance
The full call detail for maintaining existing employees: query for details (including the address array), one endpoint each for name / phone / email / address, and update-restrictions to freeze and unfreeze by capability code.
## π Guide
Day-to-day maintenance after an employee has been created is covered by six endpoints: query the details, one update endpoint each for name / phone / email / address, and freeze/unfreeze. A name change triggers KYC re-submission; the other three do not touch KYC. Freezing and unfreezing use add/remove set semantics over capability codes. Creation and resubmission are covered in [Creating employees](./employee-onboarding); the full state machine is in [State machine and freezing](../basic-concepts/states-and-freezing).
## Querying employee details
**`GET /open-api-corp/customer/v1/query`**
Returns the employee's name, contact details, status and address list (once maintained).
### Request parameters (query string)
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `customerId` | String | Yes | β€50; employee ID |
### Response `data`
| Field | Type | Description |
| - | - | - |
| `customerId` | String | Employee ID |
| `customerApplyRef` | String | Your own employee identifier (echoed) |
| `companyId` | String | The company the employee belongs to |
| `firstName` | String | First name |
| `middleName` | String | Middle name |
| `lastName` | String | Last name |
| `phoneCountryCode` | String | Country / region code of the mobile number |
| `phoneNumber` | String | Mobile number |
| `email` | String | Email address |
| `status` | String | Employee status: lifecycle `ACTIVE` / `TERMINATED`; behavioral `SUSPENDED` / `FROZEN` / `RESTRICTED` |
| `addresses` | Array | Address list, element structure below; empty when never maintained |
`addresses[]` element structure:
| Field | Type | Description |
| - | - | - |
| `addressType` | String | Address purpose, the discriminator inside the array. Current value: `SHIPPING_ADDRESS` (physical card delivery) |
| `postalCode` | String | Postal code (absent in some regions, may be empty) |
| `addressLine1` | String | Address line 1 |
| `addressLine2` | String | Address line 2 (may be empty) |
| `addressLine3` | String | Address line 3 (may be empty) |
| `city` | String | City (a district is acceptable for Hong Kong) |
| `state` | String | State / province (`Hong Kong` for Hong Kong) |
| `addressCountryCode` | String | Two-letter ISO country code (for example `HK`) |
### Request example
```http theme={null}
GET /open-api-corp/customer/v1/query?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&customerId=a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"customerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"customerApplyRef": "emp-acme-0001",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"firstName": "TAI MAN",
"middleName": null,
"lastName": "CHAN",
"phoneCountryCode": "HK",
"phoneNumber": "91234567",
"email": "taiman.chan@acme.com",
"status": "ACTIVE",
"addresses": [
{
"addressType": "SHIPPING_ADDRESS",
"postalCode": "999077",
"addressLine1": "Flat 5, 12/F, Acme Tower",
"addressLine2": "8 Connaught Road Central",
"addressLine3": "Central",
"city": "Hong Kong",
"state": "Hong Kong",
"addressCountryCode": "HK"
}
]
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `CUSTOMER_INVALID` | The employee does not exist (or does not belong to your partner account) |
## Updating employee information
Each of the four categories has its own endpoint. The path states the intent, the request carries only the fields that category needs, and the error codes returned are only the ones that category can hit:
| Endpoint | What it changes | Triggers KYC |
| - | - | - |
| `POST /open-api-corp/customer/v1/update-name` | The three name parts | Yes β resubmitted whenever the name actually changes |
| `POST /open-api-corp/customer/v1/update-phone` | Mobile country code + number | No |
| `POST /open-api-corp/customer/v1/update-email` | Email address | No |
| `POST /open-api-corp/customer/v1/update-address` | Address list | No |
All four are action endpoints: on success they return `SYS_SUCCESS` with `data` set to `null`.
### Updating the name
The name is **replaced as a whole**: the `firstName` + `middleName` + `lastName` submitted become the new legal name, with no per-part merging β KYC compares the complete name, and only a whole-name submission makes what goes to review unambiguous. Omitting `middleName` means there is no middle name, and any previous value is cleared. The call either fully succeeds or fully fails; there is no partial success.
| Field | Type | Required | Description |
| - | - | - | - |
| `customerId` | String | Yes | β€50; the target employee |
| `firstName` | String | Yes | β€64; letters, digits and spaces only |
| `middleName` | String | No | β€64; omitting it means no middle name (any previous value is cleared) |
| `lastName` | String | Yes | β€64; letters, digits and spaces only |
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"customerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"firstName": "TAI MAN",
"lastName": "CHAN"
}
```
| Error code | Description |
| - | - |
| `CUSTOMER_INVALID` | The employee does not exist or is not in a valid state |
| `KYC_IN_REVIEW` | The employee already has a KYC review in flight; retry once it completes |
### Updating the mobile number
The country code and the number must be submitted together. This number also serves as the delivery contact for physical cards.
| Field | Type | Required | Description |
| - | - | - | - |
| `customerId` | String | Yes | β€50; the target employee |
| `phoneCountryCode` | String | Yes | Exactly 2 characters; ISO-3166-1 alpha-2 uppercase (for example `HK`), and must be on the supported list |
| `phoneNumber` | String | Yes | β€15; digits only, local number without the country code and without `+` |
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"customerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"phoneCountryCode": "HK",
"phoneNumber": "91234567"
}
```
| Error code | Description |
| - | - |
| `CUSTOMER_INVALID` | The employee does not exist or is not in a valid state |
### Updating the email address
| Field | Type | Required | Description |
| - | - | - | - |
| `customerId` | String | Yes | β€50; the target employee |
| `email` | String | Yes | β€40; contact email address |
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"customerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"email": "taiman.chan@acme.com"
}
```
| Error code | Description |
| - | - |
| `CUSTOMER_INVALID` | The employee does not exist or is not in a valid state |
### Updating addresses
The update is "partial by `addressType`, whole by object": only the address types present in the request are affected, types left out are untouched, and a type that is matched has its whole object replaced. Field-level edits are not supported.
| Field | Type | Required | Description |
| - | - | - | - |
| `customerId` | String | Yes | β€50; the target employee |
| `addresses` | Array | Yes | At least one entry; element structure as in the query response above, where `addressType` / `addressLine1` / `city` / `state` / `addressCountryCode` are required |
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"customerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"addresses": [
{
"addressType": "SHIPPING_ADDRESS",
"postalCode": "999077",
"addressLine1": "Flat 5, 12/F, Acme Tower",
"addressLine2": "8 Connaught Road Central",
"addressLine3": "Central",
"city": "Hong Kong",
"state": "Hong Kong",
"addressCountryCode": "HK"
}
]
}
```
An address update only affects later virtual-to-physical applications β the address is snapshotted into the application when it is accepted, so cards already in transit or delivered are unaffected.
| Error code | Description |
| - | - |
| `CUSTOMER_INVALID` | The employee does not exist or is not in a valid state |
| `COUNTRY_SANCTIONED` | `addresses[].addressCountryCode` is on a sanctions list |
## Freezing and unfreezing (updating employee restrictions)
**`POST /open-api-corp/customer/v1/update-restrictions`**
Freeze or unfreeze an employee. The semantics match the company side exactly: adding a restriction freezes, removing it unfreezes, with idempotent set semantics that make repeated submissions safe. Only the five open L1 capability codes are accepted:
| Capability code | What it disables |
| - | - |
| `ACCOUNT_FROZEN` | The account as a whole (sign-in, password change, phone / email change, MFA binding, closure) |
| `CASH_IN_FROZEN` | Deposits / top-ups |
| `CASH_OUT_FROZEN` | Withdrawals / remittances |
| `PAYMENT_FROZEN` | Payments / spending (card payments included) |
| `CARD_FROZEN` | Card management actions (apply, activate, change PIN) |
**Once an employee is frozen, every card in their name declines transactions.** Restricted states written by risk, regulatory or judicial processes cannot be lifted through this endpoint.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `customerId` | String | Yes | β€50; the target employee |
| `addRestrictions` | Array\ | Conditional | Restrictions to add; multiple allowed. At least one of `addRestrictions` / `removeRestrictions` is required β omitting both returns `DAPI_PARAM_INVALID`. Elements must come from the five L1 capability codes above; anything else (including `KYC_FROZEN` / `TRANSFER_FROZEN` / L2 fine-grained codes / `SUSPENDED` / `RESTRICTED`) also returns `DAPI_PARAM_INVALID` |
| `removeRestrictions` | Array\ | Conditional | Restrictions to remove; same value constraints as `addRestrictions`; at least one of the two is required |
| `remark` | String | No | β€256; operation remark |
### Request examples
Freeze (add restrictions):
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"customerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"addRestrictions": ["PAYMENT_FROZEN", "CARD_FROZEN"],
"remark": "suspicious transactions"
}
```
Unfreeze (remove restrictions):
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"customerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c",
"removeRestrictions": ["PAYMENT_FROZEN", "CARD_FROZEN"],
"remark": "risk cleared"
}
```
### Error codes
| Error code | Description |
| - | - |
| `CUSTOMER_INVALID` | The employee does not exist (or does not belong to your partner account) |
| `STATUS_CONFLICT` | The current status does not allow the operation (for example the employee is not `ACTIVE`) |
Related webhook: `CUSTOMER_STATUS_CHANGED`, with `businessId` set to `customerId` and `data` carrying this change's `addRestrictions` / `removeRestrictions` and `remark`.
## Next steps
* Once the employee is `ACTIVE` and not frozen, issue and manage their cards: [Managing cards](./managing-cards)
* The full employee state machine and freezing capabilities: [State machine and freezing](../basic-concepts/states-and-freezing)
# Creating an employee
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/employee-onboarding
The full call-level detail of the employee application: every apply field with the shipping-address structure, the email/phone uniqueness rules, tracking progress with query-apply, and resubmitting after a rejection.
## π Guide
Employees are the end cardholders, and creating one is the mandatory step before any card can be issued to them. You submit the employee's details under an ACTIVE company, DCS accepts the application and runs individual due diligence (KYC name screening) asynchronously; rely on webhooks for the result with the query endpoint as fallback, and if KYC rejects the application, correct the name and resubmit under the same `customerApplyId`. This page covers all fields and examples for the three endpoints β apply, query-apply and resubmit; for the capability overview see [Managing employees](./managing-employees).
### Flow at a glance
The application state machine has only three states: `PENDING` / `SUCCEED` / `REJECTED`. After a rejection you do not start over with a new ID β you resubmit under the same `customerApplyId`.
### Prerequisites
* The company already exists and is `ACTIVE` (if not, start with [Managing companies](./managing-companies)).
* You have the employee's name, mobile number and email ready; if a physical card will be needed, also prepare a shipping address.
## Submit the application
**`POST /open-api-corp/customer/v1/apply`**
Creates an employee: submit the details, and individual due diligence (KYC name screening) runs asynchronously. Pass the shipping address as the address information.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `customerApplyRef` | String | Yes | β€64; your unique employee identifier; character set `^[A-Za-z0-9_-]+$` |
| `companyId` | String | Yes | β€50; the company the employee belongs to, must be `ACTIVE` |
| `firstName` | String | Yes | β€64; first name; English letters, digits and spaces only |
| `lastName` | String | Yes | β€64; last name; English letters, digits and spaces only |
| `middleName` | String | No | β€64; middle name; English letters, digits and spaces only |
| `phoneCountryCode` | String | Yes | Fixed length 2; mobile area code (ISO 3166-1 alpha-2 two-letter code, e.g. `HK`; must be on the supported list) |
| `phoneNumber` | String | Yes | β€15; mobile number (digits only) |
| `email` | String | Yes | β€40; email address |
| `addresses` | Array | No | Address list (structure below). If supplied at creation it is stored with the onboarding flow; if omitted, it can be added later through [updating employee addresses](./employee-maintenance) |
`addresses[]` element structure:
| Field | Required | Description |
| - | - | - |
| `addressType` | Yes | Address purpose, the discriminator inside the array. Current value: `SHIPPING_ADDRESS` (physical card delivery) |
| `postalCode` | No | Postal code (some regions have none; may be empty) |
| `addressLine1` | Yes | Address line 1, β€40 |
| `addressLine2` | No | Address line 2, β€40 |
| `addressLine3` | No | Address line 3, β€40 |
| `city` | Yes | City (for Hong Kong, the district is acceptable), β€64 |
| `state` | Yes | State / province (for Hong Kong, use `Hong Kong`), β€20 |
| `addressCountryCode` | Yes | Two-letter ISO country code (e.g. `HK`) |
**An employee without a shipping address cannot apply for a physical card.** If you only issue virtual cards you may omit `addresses` and add it later through the update endpoint when a physical card is needed.
**Uniqueness rules**: within one partner, an employee's email and phone (area code + number) are unique across companies, and **once taken they are never released β even if the application was rejected**. Duplicate submissions return `EMAIL_DUPLICATE` / `PHONE_DUPLICATE` respectively. Before submitting, make sure the contact details are not already used by another employee, including historical rejected applications.
### Request example
```json theme={null}
{
"customerApplyRef": "ext-employee-0001",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"firstName": "JOHN",
"lastName": "DOE",
"phoneCountryCode": "HK",
"phoneNumber": "12345678",
"email": "finance@example.com",
"addresses": [
{
"addressType": "SHIPPING_ADDRESS",
"postalCode": "999077",
"addressLine1": "Room 1201, Example Tower",
"addressLine2": "1 Example Street",
"addressLine3": "Central",
"city": "Hong Kong",
"state": "Hong Kong",
"addressCountryCode": "HK"
}
]
}
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"customerApplyId": "5136744097353943553",
"status": "PENDING"
}
}
```
In the response `data`, `customerApplyId` is the employee application ID (the key for the query and resubmit calls that follow β persist it), and `status` is `PENDING` on acceptance.
### Error codes
| Error code | Description |
| - | - |
| `APPLY_DUPLICATE` | An in-flight or already-successful application exists for the same `customerApplyRef`; duplicate submission is not allowed |
| `APPLY_REJECTED_USE_RESUBMIT` | Only a rejected application exists for the same `customerApplyRef` β use the resubmit endpoint on this page instead (a duplicate apply is no longer accepted) |
| `EMAIL_DUPLICATE` | The `email` is already taken under this partner (partner-wide, any state including rejected) |
| `PHONE_DUPLICATE` | The phone (area code + number) is already taken under this partner (partner-wide, any state including rejected) |
| `COMPANY_INVALID` | The company does not exist or is not `ACTIVE` |
| `COUNTRY_SANCTIONED` | The supplied `addresses[].addressCountryCode` is on the sanctions list |
Related webhooks: `CUSTOMER_CREATED` / `CUSTOMER_REJECTED`.
## Query the application progress and result
**`GET /open-api-corp/customer/v1/query-apply`**
Rely on the webhooks as your primary channel; use this endpoint as the polling fallback.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `customerApplyId` | String | Conditional | β€50; the employee application ID; pass this or `customerApplyRef` |
| `customerApplyRef` | String | Conditional | β€64; your own application reference; pass this or `customerApplyId`. It is the fallback when the create call timed out and the webhook was lost |
### Response `data`
| Field | Type | Description |
| - | - | - |
| `customerApplyId` | String | Application ID |
| `customerApplyRef` | String | Your identifier |
| `companyId` | String | The company the employee belongs to |
| `status` | String | `PENDING` / `SUCCEED` / `REJECTED` |
| `rejectMessage` | String | Rejection reason (when `REJECTED`) |
| `customerId` | String | The employee ID once the entity is created (present only after `SUCCEED`) |
### Request example
```http theme={null}
GET /open-api-corp/customer/v1/query-apply?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&customerApplyId=5136744097353943553
```
### Response example (approved)
`status=SUCCEED` and `customerId` has been issued β persist it:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"customerApplyId": "5136744097353943553",
"customerApplyRef": "ext-employee-0001",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"status": "SUCCEED",
"rejectMessage": null,
"customerId": "a3f8d2b1-6c94-4e07-b512-9d8e7f6a5b4c"
}
}
```
### Response example (KYC rejected)
`status=REJECTED`, with `rejectMessage` explaining why; use the resubmit endpoint below to try again:
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"customerApplyId": "5136744097353943553",
"customerApplyRef": "ext-employee-0001",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"status": "REJECTED",
"rejectMessage": "name mismatch",
"customerId": null
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `APPLY_NOT_FOUND` | The employee application does not exist (or does not belong to this partner) |
## Resubmit after a rejection
**`POST /open-api-corp/customer/v1/resubmit`**
After a KYC rejection, correct the name and send the application back for review under the same `customerApplyId`.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `customerApplyId` | String | Yes | β€50; the rejected employee application ID |
| `firstName` | String | Yes | β€64; first name; English letters, digits and spaces only |
| `lastName` | String | Yes | β€64; last name; English letters, digits and spaces only |
| `middleName` | String | No | β€64; middle name; English letters, digits and spaces only |
### Request example
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"customerApplyId": "5136744097353943553",
"firstName": "JOHN",
"lastName": "DOE"
}
```
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"customerApplyId": "5136744097353943553",
"customerApplyRef": "ext-employee-0001",
"status": "PENDING"
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `APPLY_NOT_FOUND` | The employee application does not exist (or does not belong to this partner) |
| `STATUS_CONFLICT` | The application is not currently `REJECTED`, so it cannot be resubmitted |
Related webhooks: `CUSTOMER_CREATED` / `CUSTOMER_REJECTED`.
To **rename** an employee after the entity has been created successfully, use the [update-name endpoint](./employee-maintenance) rather than resubmit; resubmit only serves the case where the application was rejected and no entity exists yet.
## Next steps
* Once the employee is created, maintain their details and freeze status: [Employee details and status](./employee-maintenance)
* Issue cards once the employee is `ACTIVE`: [Managing cards](./managing-cards)
# Funding Β· Overview
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/funding-and-reconciliation
Fund the company pool and dedicated-balance cards, transfer between pool and cards, query balances, and reconcile on three levels: deposit records, transaction records and statements.
## π Guide
The funding side has one principle: fund first, spend second. This page walks "deposit β transfer β balances β reconciliation"; the funding-owner concept is in [Holders and the funding model](../basic-concepts/identity-and-funding).
## Deposits
**β Get the receiving account**: `GET /open-api-corp/fund/v1/deposit-info?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&subjectType=...&subjectId=...¤cy=...` β returns the owner's (COMPANY pool / CARD dedicated balance) receiving VA for that currency: `payeeAccountNumber`, `payeeBankCode` / `payeeBankName`, `payeeAccountName`, `payeeSwiftCode` (required for cross-border wires), bank and recipient addresses, and `remark` (the required transfer reference).
**β‘ The customer wires the funds** to the VA using those details.
**β’ Arrival notification**: the webhook `BANK_TRANSFER_INFO` (success / failure distinguished by `status`, carrying `depositId` / `amount` / `currency` / sender details).
## Pool β card transfers
`POST /open-api-corp/fund/v1/transfer` β internal transfers between the company pool and dedicated-balance cards, **same currency, no FX, closed loop within one company**: COMPANYβCARD funds a card (allocates budget), CARDβCOMPANY sweeps balance back.
| Field | Required | Description |
| - | - | - |
| `transferRef` | Yes | β€64; the transfer idempotency key β a repeat returns `TRANSFER_DUPLICATE` |
| `from.subjectType` / `from.subjectId` | Yes | `COMPANY`β`companyId` / `CARD`β`cardId` |
| `to.subjectType` / `to.subjectId` | Yes | Must differ in type from the `from` side β the two ends are always one `COMPANY` and one `CARD` |
| `amount` + `currency` | Yes | Amount > 0, β€2 decimals; the currency must match both accounts (`CURRENCY_MISMATCH`) |
The response `status` is `SUCCESS` / `PROCESSING` / `FAILED`; **on `PROCESSING` you must poll `GET /fund/v1/transfer-query?transferRef=...` for the final result**. SHARED cards hold no balance and cannot be a transfer endpoint (`CARD_NOT_DEDICATED`); an underfunded sender returns `INSUFFICIENT_FUNDS`.
## Balances
`GET /open-api-corp/fund/v1/balance?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&subjectType=...&subjectId=...` β per-currency balances for a funding owner, each returning `availableAmount` (the current available balance).
## Reconciliation on three levels
| Level | Endpoint | Use |
| - | - | - |
| Deposit records | `GET /fund/v1/deposit-records` | Paginated VA deposit history (filterable by owner / currency / arrival time), matched against bank advice |
| Transaction records | `POST /statement/v1/transactions` | Transactions by time window (independent of billing cycles; **window at most six months**); omit `cardId` for the company view, pass it to narrow to one card (including deposits into that card's VA) |
| Statements | `POST /statement/v1/statements` + `statement-detail` | Statement-level reconciliation, below |
**Key transaction fields**: `transactionCategory` (`PURCHASE` / `REFUND` / `FEE` / `REPAYMENT` / `CASH_ADVANCE` / `DEPOSIT` / `TRANSFER`), `postStatus` (`POSTED` / `UNPOSTED` authorization hold / null for repayment-type rows), the original and posted amount/currency pairs (`originalAmount`/`originalCurrency`, `postAmount`/`postCurrency`), `postTime` (null while on hold) and `merchant` (`merchantName` / `mcc` / `merchantCountryCode`).
**Statement list**: `POST /statement/v1/statements` β by company (`type=SHARED`) or by card (`type=DEDICATED`, `cardId` required) over a time range, cycles in reverse order. Each statement returns `statementId` (null for the open statement), `statementPeriod`, `status` (`OPEN` / `SETTLED`), `statementDate` / `paymentDueDate`, `paymentStatus` (`CURRENT` / `AWAITING_PAYMENT` / `PAID` / `OVERDUE`, computed live) and a per-currency `summary` (purchases / refunds / fees / repayments / cash advances / amount due / overpaymentAmount totals).
**Statement detail**: `POST /statement/v1/statement-detail` β the header + per-currency summary + transaction lines (pagination applies to the lines only). For a **settled** statement pass `statementId`; for the **current open** statement pass `statementId=null` + `status=OPEN`.
**The settled statement is final**: the open statement is a live summary of the current cycle including unsettled authorization holds, so its totals fall back as transactions clear β expected behavior. The billing mechanics and key dates are in [Limits and statements](../basic-concepts/limits-and-billing).
## Related webhooks
`BANK_TRANSFER_INFO` (VA deposit arrival) / the balance-change notification / `LOW_BALANCE` (pool low-balance alert; configuration in [Managing companies](./managing-companies)).
## Next steps
* How debt arises and is notified: [Authorization and 3DS](./authorization-and-3ds)
* Open vs settled statement conventions: [Limits and statements](../basic-concepts/limits-and-billing)
* In this group: [Deposits](./deposits) Β· [Balances and transfers](./balances-and-transfers) Β· [Statements and transactions](./statements-and-transactions)
# Cards Β· Overview
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/managing-cards
Issue virtual cards to companies or employees, query cards, open receiving accounts for dedicated-balance cards, retrieve secure card details, and run the full virtual-to-physical chain (production β shipping β activation β PIN).
## π Guide
Issuing always starts virtual: the card is usable the moment the application passes; when a physical card is needed, upgrade the activated virtual card under the same number. This page walks "apply β query β display the PAN β upgrade to physical" with each endpoint's key parameters and prerequisites.
## Applying for a virtual card
`POST /open-api-corp/card/v1/apply` β acceptance returns an `cardApplyId`; risk checks and card creation run asynchronously, with the final state pushed as `CARD_CREATED` / `CARD_REJECTED` (fallback `GET /card/v1/query-apply`).
| Field | Required | Description |
| - | - | - |
| `cardApplyRef` | Yes | β€64; your unique application identifier (the idempotency key) |
| `subjectType` + `subjectId` | Yes | `COMPANY`β`companyId` / `CUSTOMER`β`customerId`; the holder must be ACTIVE |
| `cardProfileId` | Yes | One of the four card types (must match the subjectType); see [Overview Β· Card types](../getting-started/overview) |
| `custodianCustomerId` | Required for company cards | The custodian: the `customerId` of an ACTIVE employee of the same company |
| `ruleIds` | Required for SHARED cards | β€5 velocity rules bound at issue time; each must exist, belong to the same company and be ACTIVE, and amount-limit currencies must intersect the card's settlement currencies; **no duplicates in the array (not silently deduped)**; any failing entry rejects the whole request before acceptance β no `cardApplyId` is produced |
Common rejections: `CARD_RULE_REQUIRED` (a SHARED card without rules β an empty array counts as none), `CARD_RULE_DUPLICATE` (the same rule carried twice), `CARD_LIMIT_EXCEEDED` (over the card cap β live cards and in-flight applications counted together), `SUBJECT_INVALID` (custodian missing or not a valid employee of the company).
Query the application: `GET /card/v1/query-apply?cardApplyId=...` β `status=SUCCEED` returns the `cardId`; on `REJECTED` the `errorCode` is fixed at `CARD_RISK_REJECTED` (internal risk codes are not passed through).
## Querying cards
* **Single card**: `GET /open-api-corp/card/v1/query?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&cardId=...` β returns `panFirst6` / `panLast4` (never the full PAN), the company, the holder, `cardProfileId`, `cardNetwork` (VISA / MASTERCARD / UPI), `cardCurrency` and status.
* **List**: `GET /open-api-corp/card/v1/list`, filterable by `companyId` / `subjectType` / `subjectId` / `status`, paginated with `page` / `pageSize`.
## Opening a receiving account for a dedicated-balance card
`POST /open-api-corp/card/v1/open-va` β opens bank virtual accounts (VAs) per currency for a dedicated-balance card: send `cardId` + `fundingCurrencies` (USD / HKD, one or more); `data` comes back `null` β read the account numbers through [get deposit account information](./deposits). Cards funded by the company pool cannot open VAs (`CARD_NOT_DEDICATED`). A dedicated-balance card can also skip the VA and be funded from the company pool instead β see [Funding and reconciliation](./funding-and-reconciliation).
## Retrieving secure card details
`POST /open-api-corp/card/v1/retrieve-secure-card` β returns ciphertext-level `encryptedPan` / `encryptedCvv2` / `encryptedExpireDate` plus a per-call random `iv`, for decryption and display in your own frontend. **PCI DSS-allowlisted partners only** (`PCI_NOT_CERTIFIED` otherwise); partners without PCI DSS integrate the DCS hosted secure card page.
**Decryption contract**: AES/GCM/NoPadding with a 128-bit tag, keyed with your SK; the three fields share one `iv`. **CVV2 and expiry are fetched live and must never be cached**; clear text must never be persisted in any form (databases, files, logs, caches, analytics or tracing), and the frontend masks by default (first 6, last 4).
## Virtual to physical
Four steps: **upgrade application β track shipping β activate β set the PIN**. The steps are detailed in [Physical cards](./physical-cards); card states are in [State machines and freezing](../basic-concepts/states-and-freezing).
**β Upgrade application**: `POST /open-api-corp/card/v1/virtual-to-physical` β send `cardPhysicalApplyRef` (idempotency key), `cardId` (must be an ACTIVE virtual card), `cardLayoutCode` (the card-face code, values issued by DCS per partner configuration), `embossingName` (β€20, first embossed line) and optional `embossingName2`. The shipping address is read from the holder / custodian and **locked as a snapshot** β a missing address returns `SHIPPING_ADDRESS_REQUIRED`; an in-flight upgrade on the same card returns `CARD_CONVERT_IN_PROGRESS`.
**β‘ Track shipping**: `GET /open-api-corp/card/v1/shipping-info` β returns `trackingNumber` / `trackingCompanyName` (null before dispatch); dispatch pushes the webhook `CARD_SHIPPED`.
**β’ Activate**: `POST /open-api-corp/card/v1/activate` β requires the card to have shipped (`CARD_NOT_SHIPPED` otherwise); the endpoint is idempotent; success pushes `CARD_ACTIVATED`.
**β£ Set the PIN**: `POST /open-api-corp/card/v1/set-pin` β requires activation. The PIN and the identity-check fields (`encryptedPin` / `encryptedCvv2` / `encryptedExpireDate` / `encryptedPanLast4`) are AES-GCM encrypted with your SK, the four ciphertexts sharing one `iv` β the same crypto contract as retrieve-secure-card, in the opposite direction. The clear PIN must be 4 digits, no runs, no repeats (`PIN_RULE_VIOLATION`).
## Related webhooks
`CARD_CREATED` / `CARD_REJECTED` / the card status-change notification / `CARD_SHIPPED` / `CARD_ACTIVATED`.
## Next steps
* Build velocity rules before issuing SHARED cards: [Setting spend limits](./spend-limits)
* Fund a dedicated-balance card: [Funding and reconciliation](./funding-and-reconciliation)
* In this group: [Applying for a virtual card](./applying-virtual-cards) Β· [Secure card details](./secure-card-details) Β· [Physical cards](./physical-cards)
# Companies Β· Overview
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/managing-companies
Onboard and maintain your business customers: KYB application, querying and resubmission, freeze/unfreeze, low-balance alerts β with the key parameters and the idempotency and dedup rules.
## π Guide
A company is your business customer's funding and compliance entity on the platform: onboarding creates its funding pool, and every employee, card and balance hangs off it. This page walks "apply β get the result β day-to-day maintenance", showing what to call, what to send and what you get back.
## Who does what
| Step | Who | Notes |
| - | - | - |
| Submit the onboarding application | Partner | Collect the company details and call apply |
| KYB due diligence | DCS | Reviewed asynchronously; the result arrives over webhook |
| Handle the result / resubmit | Partner | Persist `companyId` on approval; correct and resubmit on rejection |
| Day-to-day maintenance | Partner | Freeze / unfreeze, low-balance alert configuration |
## Step 1: submit the application
`POST /open-api-corp/company/v1/apply` β KYB runs asynchronously after submission; acceptance returns an `companyApplyId`.
| Field | Required | Description |
| - | - | - |
| `companyApplyRef` | Yes | β€64; your unique company identifier (the idempotency key), charset `^[A-Za-z0-9_-]+$` |
| `companyName` | Yes | β€256; legal name β English letters, digits and spaces only |
| `companyRegistrationNumber` | Yes | β€64; company registration number |
| `email` | Yes | β€40; company contact email |
| `fundingCurrencies` | Yes | Pool currencies (USD / HKD, one or more β a receiving VA is created per currency); no duplicates, and must be a subset of the currencies allowed for your enterprise β anything outside rejects the whole request |
The synchronous response carries only `companyApplyId` + `status=PENDING` β accepted, not approved.
**Dedup runs on three dimensions**: `companyApplyRef`, `email` and `companyRegistrationNumber` are unique within one partner, and the latter two are **never released, even after a rejection**. Reapplying under an `companyApplyRef` with an in-flight or approved application returns `APPLY_DUPLICATE`; if only a rejected application exists it returns `APPLY_REJECTED_USE_RESUBMIT` β the right path is resubmit, never a new ID.
## Step 2: get the result
Rely on webhooks `COMPANY_CREATED` / `COMPANY_REJECTED`, with `GET /open-api-corp/company/v1/query-apply?companyApplyId=...` as the polling fallback. Key response fields:
| Field | Description |
| - | - |
| `status` | `PENDING` (KYB in progress) / `SUCCEED` (onboarded) / `REJECTED` (KYB refused) |
| `status` / `rejectMessage` | The application status and the rejection reason (`rejectMessage` only when `REJECTED`) |
| `companyId` | The company ID, present only after `SUCCEED` β persist it; every later endpoint uses it |
**Resubmission after rejection**: `POST /open-api-corp/company/v1/resubmit` with `companyApplyId` plus the corrected `companyName` / `companyRegistrationNumber`, re-reviewed under the same application; `companyApplyRef` cannot change. Allowed only while the current state is `REJECTED`, otherwise `STATUS_CONFLICT`.
## Day-to-day maintenance
**Query company detail**: `GET /open-api-corp/company/v1/query?companyId=...` β legal name, registration number, pool currencies and status (life cycle `ACTIVE` / `TERMINATED` overlaid with behavioral states `FROZEN` / `SUSPENDED` / `RESTRICTED`; semantics in [State machines and freezing](../basic-concepts/states-and-freezing)).
**Freeze / unfreeze**: `POST /open-api-corp/company/v1/update-restrictions` β idempotent set semantics: `addRestrictions` freezes, `removeRestrictions` unfreezes, values restricted to the 5 capability-domain codes `ACCOUNT_FROZEN` / `CASH_IN_FROZEN` / `CASH_OUT_FROZEN` / `PAYMENT_FROZEN` / `CARD_FROZEN` (anything else returns `DAPI_PARAM_INVALID`). A state change pushes a webhook notification.
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"addRestrictions": ["PAYMENT_FROZEN", "CARD_FROZEN"],
"remark": "suspicious transactions under review"
}
```
**Low-balance alerts**: `POST /open-api-corp/fund/v1/balance-alert-set` β a threshold per currency (`balanceSettings[].currency` + `balanceSettings[].thresholdAmount`), with optional `emailSettings`. **Full-replacement semantics** β every call resets the entire alert configuration; the webhook `LOW_BALANCE` always fires, and email is sent only if a mailbox is configured; the same alert fires once per day until the balance recovers.
## Related webhooks
`COMPANY_CREATED` (KYB passed, carries `companyId`) / `COMPANY_REJECTED` (carries `rejectMessage`) / the company status-change notification (carries `addRestrictions` / `removeRestrictions`) / `LOW_BALANCE`. Envelope and signature verification: [Quickstart](../getting-started/quickstart).
## Next steps
* Create cardholders once the company is ACTIVE: [Managing employees](./managing-employees)
* Fund the pool: [Funding and reconciliation](./funding-and-reconciliation)
* In this group: [Company onboarding and review](./company-onboarding) Β· [Company status and alerts](./company-maintenance)
# Employees Β· Overview
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/managing-employees
Create and maintain employees under an ACTIVE company: KYC submission, querying and resubmission, one update endpoint per category, freeze/unfreeze β with the uniqueness and re-review rules.
## π Guide
Employees are the end cardholders β and the source of custodians for company cards. Creating an employee runs asynchronous KYC name screening; only after it passes can you issue them cards. This page covers creation, result handling and information maintenance.
## Step 1: create the employee
`POST /open-api-corp/customer/v1/apply` (the company must be ACTIVE).
| Field | Required | Description |
| - | - | - |
| `customerApplyRef` | Yes | β€64; your unique employee identifier (the idempotency key) |
| `companyId` | Yes | The company, must be ACTIVE |
| `firstName` / `lastName` / `middleName` | Yes / Yes / No | β€64 each; English letters, digits and spaces only |
| `phoneCountryCode` + `phoneNumber` | Yes | Area code is a two-letter ISO country code (e.g. `HK`); number is digits only, β€15 |
| `email` | Yes | β€40 |
| `addresses` | No | Address list; each element carries `addressType` (currently only `SHIPPING_ADDRESS`) / `postalCode` / `addressLine1` / `addressLine2` / `addressLine3` / `city` / `state` / `addressCountryCode`. Can be added later through the update endpoint; **an employee without a shipping address cannot apply for a physical card** |
**Uniqueness**: within one partner, employee email and phone (area code + number) are unique across companies and are **never released, even after a rejection** (`EMAIL_DUPLICATE` / `PHONE_DUPLICATE`). An address country on the sanctions list returns `COUNTRY_SANCTIONED`.
## Step 2: get the KYC result
Rely on webhooks `CUSTOMER_CREATED` / `CUSTOMER_REJECTED`, with `GET /open-api-corp/customer/v1/query-apply` as fallback (by `customerApplyId` or by your own `customerApplyRef`). On `status=SUCCEED` you receive `customerId` (persist it); on `REJECTED`, `rejectMessage` explains why.
**Resubmission after rejection**: `POST /open-api-corp/customer/v1/resubmit` with `customerApplyId` plus the three corrected name parts, re-reviewed under the same application; calling outside the `REJECTED` state returns `STATUS_CONFLICT`.
## Maintaining information and status
**Query employee detail**: `GET /open-api-corp/customer/v1/query?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&customerId=...` β name, contact details, status and the address list (empty if never set).
**Update employee information**: each of the four categories has its own endpoint, and the path states the intent:
| Endpoint | Fields | Notes |
| - | - | - |
| `update-name` | `firstName` + `lastName` required, `middleName` optional | The name is **replaced as a whole**; any actual change **re-triggers KYC review**, and with a KYC already in flight the call returns `KYC_IN_REVIEW` β retry after the result lands |
| `update-phone` | `phoneCountryCode` + `phoneNumber` (as a pair) | Same uniqueness rules as creation |
| `update-email` | `email` | Same uniqueness rules as creation |
| `update-address` | The `addresses` array | Partial by `addressType`, whole-object by entry β no field-level partial update |
**Freeze / unfreeze**: `POST /open-api-corp/customer/v1/update-restrictions`, identical semantics to companies (`addRestrictions` / `removeRestrictions`, the 5 capability-domain codes only). **Freezing an employee makes every card under them decline transactions.**
## Related webhooks
`CUSTOMER_CREATED` / `CUSTOMER_REJECTED` / `CUSTOMER_STATUS_CHANGED` (carries `addRestrictions` / `removeRestrictions` and `remark`).
## Next steps
* Issue cards once the employee is ACTIVE: [Managing cards](./managing-cards)
* The in-flight KYC conflict on renames: the update table above and the [FAQ](../customer-success/faq)
* In this group: [Creating employees](./employee-onboarding) Β· [Employee information and status](./employee-maintenance)
# Physical cards
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/physical-cards
The four-step chain that upgrades an ACTIVE virtual card to a physical card under the same number: the virtual-to-physical application (cardLayoutCode, embossing names, address snapshot) β shipping-info tracking β activate (idempotent) β set-pin (AES-GCM encrypted upload).
## π Guide
A physical card comes from an activated (ACTIVE) virtual card upgraded in place through "virtual to physical". The PAN, expiry, CVV2 and `cardId` all stay the same throughout the upgrade β queries return the original virtual card's details before activation and take effect as the physical card after it β so you never need to replace the card or migrate any targets after upgrading. For the card-state background see [State machines and freezing](../basic-concepts/states-and-freezing).
The four-step chain:
The shipping address hangs off the employee's record; it is read when the virtual-to-physical application is accepted and **locked as a snapshot** β later address changes do not affect cards already in production.
## β Upgrade application: virtual to physical
```http theme={null}
POST /open-api-corp/card/v1/virtual-to-physical
```
Upgrades an ACTIVE virtual card to a physical card under the same number.
How the card ships depends on whether `shippingAddressId` is sent. **Without it** the card ships individually to the card's holder / custodian, with the address and mailing phone number taken from that employee. **With it** the card ships in bulk to the address that id points to, and cards accepted on the same day under the same id go out together as one batch.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `cardPhysicalApplyRef` | String | Yes | β€64; your unique application identifier (the idempotency key); character set `^[A-Za-z0-9_-]+$` |
| `cardId` | String | Yes | β€50; the virtual card being upgraded; must belong to your partnership / company, currently be a virtual card and be ACTIVE; the holder / custodian is derived from this card |
| `cardLayoutCode` | String | Yes | The card-face code; must be a configured face (values are issued by DCS per partner configuration β not a fixed enum) |
| `embossingName` | String | Yes | β€20; the first embossed line on the card face |
| `embossingName2` | String | No | β€20; the second embossed line (optional) |
| `shippingAddressId` | String | No | β€50; the bulk shipping address id; without it the card ships to its holder / custodian |
### Response data
| Field | Type | Description |
| - | - | - |
| `cardPhysicalApplyId` | String | The upgrade acceptance id |
### Request and response examples
```json theme={null}
// Scenario 1: individual shipping β no shippingAddressId, ships to the card's holder / custodian
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"cardPhysicalApplyRef": "ext-v2p-0001",
"cardId": "5185740066240790530",
"cardLayoutCode": "LAYOUT_CLASSIC_BLACK",
"embossingName": "DOE JOHN",
"embossingName2": "EXAMPLE HK LIMITED"
}
// Scenario 2: bulk shipping β with shippingAddressId, ships to the address that id points to
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"cardPhysicalApplyRef": "ext-v2p-0002",
"cardId": "5185740066240790531",
"cardLayoutCode": "LAYOUT_CLASSIC_BLACK",
"embossingName": "LEE SIU MING",
"embossingName2": "EXAMPLE HK LIMITED",
"shippingAddressId": "100023"
}
```
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardPhysicalApplyId": "5136744097353943557"
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `CARD_INVALID` | The card does not exist / does not belong to your partnership / is not ACTIVE |
| `CARD_NOT_VIRTUAL` | The card is already physical and cannot be converted again |
| `CARD_CONVERT_IN_PROGRESS` | The same card already has an in-flight (unfinished) upgrade application |
| `APPLY_DUPLICATE` | The same `cardPhysicalApplyRef` has already been accepted (idempotency) |
| `SHIPPING_ADDRESS_REQUIRED` | Individual shipping only: the holder / custodian has no shipping address on file |
| `SHIPPING_ADDRESS_INVALID` | Bulk shipping only: `shippingAddressId` is invalid, or that address may not be used for this card |
| `COUNTRY_SANCTIONED` | The destination country is on a sanctions list |
| `LIMIT_PHYSICAL_EXCEEDED` | Over the physical-card cap (company-level or holder-level) |
Related webhooks: `CARD_SHIPPED` (dispatched) / `CARD_ACTIVATED` (activated).
## β‘ Tracking production and shipping
```http theme={null}
GET /open-api-corp/card/v1/shipping-info
```
Queries the physical card's production and shipping progress plus the tracking number.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `cardId` | String | Yes | β€50; the card id |
### Response data
| Field | Type | Description |
| - | - | - |
| `cardId` | String | The card id |
| `trackingNumber` | String | The tracking number (empty before dispatch) |
| `trackingCompanyName` | String | The carrier |
### Response examples (shipped / not shipped)
```json theme={null}
// Scenario 1: shipped
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardId": "5185740066240790530",
"trackingNumber": "SF1234567890123",
"trackingCompanyName": "SF Express"
}
}
// Scenario 2: not yet shipped
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardId": "5185740066240790530",
"trackingNumber": null,
"trackingCompanyName": null
}
}
```
Error code: `CARD_INVALID` β the card does not exist / does not belong to your partnership.
## β’ Activating the physical card
```http theme={null}
POST /open-api-corp/card/v1/activate
```
Activates the physical card; the prerequisite is that the card **has shipped**. **The endpoint is idempotent** β repeated calls return the same success.
### Request parameters and response
Request parameters: `companyId` (String, required, β€36; the requesting company ID) and `cardId` (String, required, β€32; the card id). This is an action endpoint: `data` is `null`.
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"cardId": "5185740066240790530"
}
```
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardId": "5185740066240790530"
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `CARD_INVALID` | The card does not exist / does not belong to your partnership |
| `CARD_NOT_SHIPPED` | The card has not shipped yet and cannot be activated |
Related webhook: `CARD_ACTIVATED`.
## β£ Setting the PIN
```http theme={null}
POST /open-api-corp/card/v1/set-pin
```
Sets the physical card's PIN; the prerequisite is that the physical card **has been activated**.
The PIN and the identity-check fields are AES-GCM encrypted with your SK before upload β **the same crypto contract as `retrieve-secure-card`, in the opposite direction** (for the contract see [Retrieving secure card details](./secure-card-details): AES/GCM/NoPadding, 128-bit tag, keyed with the SK).
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `cardId` | String | Yes | β€50; the card id |
| `encryptedPin` | String | Yes | The encrypted PIN (AES-GCM under the partner key; the clear PIN must be 4 digits, no sequential runs, no all-identical digits) |
| `encryptedCvv2` | String | Yes | The encrypted CVV2 (identity check) |
| `encryptedExpireDate` | String | Yes | The encrypted expiry (identity check; plaintext format mm/yy) |
| `encryptedPanLast4` | String | Yes | The encrypted last four digits of the PAN (identity check) |
| `iv` | String | Yes | The per-call random IV (Base64); the 4 ciphertext fields share the same `iv` |
### Request and response examples
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"cardId": "5185740066240790530",
"encryptedPin": "UGluTW9ja0NpcGhlclRleHQrVGFnMTZC",
"encryptedCvv2": "Q3Z2Mk1vY2tDaXBoZXJUZXh0K1RhZzE2Qg==",
"encryptedExpireDate": "RXhwTW9ja0NpcGhlclRleHQrVGFnMTZC",
"encryptedPanLast4": "TGFzdDRNb2NrQ2lwaGVyVGV4dCtUYWcxNkI=",
"iv": "MTIzNDU2Nzg5MGFi"
}
```
The response data carries `cardId` and `success` (Boolean, whether the PIN was set):
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardId": "5185740066240790530",
"success": true
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `CARD_INVALID` | The card does not exist / does not belong to your partnership |
| `CARD_NOT_ACTIVATED` | The card is not activated; the PIN cannot be set |
| `CARD_STATE_INVALID` | The card state does not allow setting a PIN; only ACTIVE / FROZEN |
| `PIN_RULE_VIOLATION` | The PIN is not 4 digits / is a sequential run / is all-identical digits |
## Next steps
* No upgradable virtual card yet? Issue one first: [Applying for a virtual card](./applying-virtual-cards)
* Card states and the freeze capability domains: [State machines and freezing](../basic-concepts/states-and-freezing)
# Quota query and adjustment
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/quota-and-adjustment
Query a card's or employee's merged effective limits and current usage (query-quota quota objects explained), plus rule update, enable/disable, detail and list queries.
## π Guide
A subject may be constrained by several rules at once; the system merges them by taking the strictest value per dimension to produce the "effective limits". `query-quota` returns that merged result together with current usage, and the platform has already computed the remainder for you. Rules themselves are adjusted through `update-rule` (full replacement) and `change-rule-status` (enable/disable), and inspected through `query-rule` and `list-rules`. Rule creation is covered in [Creating velocity rules](./creating-rules); the merge model is introduced in [Limits and statements](../basic-concepts/limits-and-billing).
## Querying effective quota
**`GET /open-api-corp/velocity/v1/query-quota`** β the merged effective limits and current usage for a subject (card / employee).
### Request parameters (query string)
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the company the subject belongs to |
| `subjectType` | String | Yes | `CARD` (any card) / `CUSTOMER` (employee) |
| `subjectId` | String | Yes | β€50; the subject identifier matching `subjectType` |
| `currency` | String | No | `USD` / `HKD`; omit to return every currency relevant to the subject |
```
GET /open-api-corp/velocity/v1/query-quota?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&subjectType=CARD&subjectId=5185740066240790530¤cy=USD
```
### The quota object: the key to reading the response
Every period node in the response (`daily` / `monthly` / `quarterly` / `yearly`) is a "quota object". The amount side and the count side use different field names but map one to one:
| Amount quota object | Count quota object | Description |
| - | - | - |
| `limitAmount` | `limitCount` | The merged effective cap (min across rules); `null` means uncapped |
| `usedAmount` | `usedCount` | Usage in the current period; still accumulated and returned when the cap is `null` |
| `remainingAmount` | `remainingCount` | Cap minus usage; `null` when the cap is `null`; negative means the cap has been exceeded (after a mid-period reduction) |
| `utilizationRate` | `utilizationRate` | Utilization percentage, two decimals; `null` when the cap is `null` |
| `periodKey` | `periodKey` | Current period key: day `20260730` / month `202607` / quarter `2026Q3` / year `2026` |
| `sourceRuleId` | `sourceRuleId` | The rule the cap came from (the winner after taking the min) |
**The per-transaction limit is the exception**: the `perTransaction` quota object carries only `limitAmount` and `sourceRuleId` β per-transaction limits do not accumulate, so there is no usage, remainder, utilization rate or period key.
### Response data structure
| Field | Type | Description |
| - | - | - |
| `subjectType` / `subjectId` | String | Echoes the request |
| `asOfTime` | String | Reference timestamp for the quota query, ISO-8601 UTC |
| `purchase.countLimits` | Object | Purchase count quota (across currencies), four period nodes of count quota objects |
| `purchase.amountLimits` | Array | Purchase amount quota, one entry per currency |
| `purchase.amountLimits[].currency` | String | `USD` / `HKD`, the companion currency for every amount in the group |
| `purchase.amountLimits[].effective` | Boolean | Whether this currency group actually applies |
| `purchase.amountLimits[].ineffectiveReason` | String | Set only when `effective=false`. Current values: `NOT_CARD_SETTLEMENT_CURRENCY` β the currency is not among the card's settlement currencies |
| `purchase.amountLimits[].perTransaction` | Object | Per-transaction quota object; only `limitAmount` and `sourceRuleId` |
| `purchase.amountLimits[].daily` β¦ `yearly` | Object | Daily / monthly / quarterly / yearly cumulative amount quota objects |
| `cashAdvance.allowed` | Boolean | Merged cash-advance switch: **false if any rule says false** |
| `cashAdvance.sourceRuleId` | String | When `allowed=false`, names the rule that turned it off |
| `cashAdvance.countLimits` / `amountLimits` | Object / Array | Cash-advance count and amount quota, same structure as the purchase section |
| `appliedRules` | Array | The rules participating in this merge: `ruleId` / `ruleName` / `status` |
| `appliedRules[].status` | String | Always `ACTIVE` β `INACTIVE` rules do not participate in the merge and never appear here |
### Response example
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"subjectType": "CARD",
"subjectId": "5136759791164443137",
"asOfTime": "2026-08-12T15:30:00Z",
"purchase": {
"countLimits": {
"daily": { "limitCount": 5, "usedCount": 2, "remainingCount": 3, "utilizationRate": "40.00", "periodKey": "20260812", "sourceRuleId": "5136744097353943556" },
"monthly": { "limitCount": 100, "usedCount": 37, "remainingCount": 63, "utilizationRate": "37.00", "periodKey": "202608", "sourceRuleId": "5136744097353943556" },
"quarterly": { "limitCount": null, "usedCount": 61, "remainingCount": null, "utilizationRate": null, "periodKey": "2026Q3", "sourceRuleId": null },
"yearly": { "limitCount": null, "usedCount": 214, "remainingCount": null, "utilizationRate": null, "periodKey": "2026", "sourceRuleId": null }
},
"amountLimits": [
{
"currency": "USD",
"effective": true,
"ineffectiveReason": null,
"perTransaction": { "limitAmount": "3000", "sourceRuleId": "5136744097353943556" },
"daily": { "limitAmount": "10000", "usedAmount": "2400", "remainingAmount": "7600", "utilizationRate": "24.00", "periodKey": "20260812", "sourceRuleId": "5136744097353943556" },
"monthly": { "limitAmount": null, "usedAmount": "18600", "remainingAmount": null, "utilizationRate": null, "periodKey": "202608", "sourceRuleId": null },
"quarterly": { "limitAmount": "200000", "usedAmount": "45200", "remainingAmount": "154800", "utilizationRate": "22.60", "periodKey": "2026Q3", "sourceRuleId": "5136744097353943559" },
"yearly": { "limitAmount": "600000", "usedAmount": "96100", "remainingAmount": "503900", "utilizationRate": "16.02", "periodKey": "2026", "sourceRuleId": "5136744097353943556" }
},
{
"currency": "HKD",
"effective": false,
"ineffectiveReason": "NOT_CARD_SETTLEMENT_CURRENCY",
"perTransaction": null,
"daily": null, "monthly": null, "quarterly": null, "yearly": null
}
]
},
"cashAdvance": {
"allowed": true,
"sourceRuleId": null,
"countLimits": {
"daily": { "limitCount": null, "usedCount": 0, "remainingCount": null, "utilizationRate": null, "periodKey": "20260812", "sourceRuleId": null },
"monthly": { "limitCount": null, "usedCount": 1, "remainingCount": null, "utilizationRate": null, "periodKey": "202608", "sourceRuleId": null }
},
"amountLimits": [
{
"currency": "USD",
"effective": true,
"ineffectiveReason": null,
"perTransaction": { "limitAmount": "500", "sourceRuleId": "5136744097353943557" },
"daily": { "limitAmount": "2000", "usedAmount": "0", "remainingAmount": "2000", "utilizationRate": "0.00", "periodKey": "20260812", "sourceRuleId": "5136744097353943557" },
"monthly": { "limitAmount": "10000", "usedAmount": "500", "remainingAmount": "9500", "utilizationRate": "5.00", "periodKey": "202608", "sourceRuleId": "5136744097353943557" }
}
]
},
"appliedRules": [
{ "ruleId": "5136744097353943556", "ruleName": "Sales team daily cap", "status": "ACTIVE" },
{ "ruleId": "5136744097353943557", "ruleName": "Cash advance control", "status": "ACTIVE" },
{ "ruleId": "5136744097353943559", "ruleName": "Quarterly ceiling", "status": "ACTIVE" }
]
}
}
```
(The example omits some cash-advance period nodes for brevity; a real response carries all four.)
### Error codes
| Error code | Description |
| - | - |
| `DAPI_PARAM_INVALID` | The velocity domain accepts only `CARD` / `CUSTOMER` |
| `SUBJECT_INVALID` | The type is valid but the subject does not exist, is not `ACTIVE`, or does not belong to the company supplied |
## Updating a rule
**`POST /open-api-corp/velocity/v1/update-rule`** β **full replacement**: any dimension or control group absent from the request is deleted, and the change takes effect on bound subjects **immediately**.
An update is a full submission, not an incremental merge: omitting `remark` clears it; omitting a control group deletes that group (which means no limit); currency groups absent from `amountLimits` are deleted. At least one of the five control groups must still be present β to disable a whole rule use the enable/disable endpoint, do not clear its control groups. Call `query-rule` first to fetch the complete configuration, modify it, and submit the whole thing.
The request carries the same five control groups as [rule creation](./creating-rules); only the top level differs:
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50 |
| `ruleId` | String | Yes | β€50 |
| `ruleVersion` | Integer | Yes | Optimistic lock; a mismatch returns `VELOCITY_RULE_VERSION_CONFLICT` |
| `ruleName` | String | Yes | β€64; rule name (submitted in full) |
| `remark` | String | No | β€256; rule remark; omitting it clears the value |
The response `data` is the complete updated rule object (same structure as the create response) with `ruleVersion` incremented.
```json theme={null}
{
"ruleId": "5136744097353943556",
"ruleRef": "rule-20260825-0001",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"ruleName": "Sales team daily cap",
"remark": "Travel card purchase limits",
"status": "ACTIVE",
"ruleVersion": 2,
"bindingCount": "3",
"createTime": "2026-08-12T09:00:00Z",
"modifyTime": "2026-08-13T11:00:00Z",
"purchaseControl": {
"amountLimits": [
{ "currency": "USD", "perTransaction": "3000", "daily": "12000", "monthly": "80000" }
],
"countLimits": { "daily": 5, "monthly": 100 }
},
"cashAdvanceControl": null,
"currencyControl": { "filterType": "WHITELIST", "values": ["USD", "HKD"] },
"mccControl": null,
"geographicControl": null
}
```
An unconfigured control group comes back as `null`, not an empty object β an empty object would read as "an empty list was configured".
On top of the rule-creation error codes, an update can return:
| Error code | Description |
| - | - |
| `VELOCITY_RULE_NOT_FOUND` | The rule does not exist, or does not belong to the `companyId` supplied / to your partner account |
| `VELOCITY_RULE_VERSION_CONFLICT` | `ruleVersion` does not match the server; a concurrent modification happened β re-query the detail and retry |
| `VELOCITY_RULE_STATUS_INVALID` | The rule's current status does not allow modification |
## Enabling and disabling a rule
**`POST /open-api-corp/velocity/v1/change-rule-status`** β toggles between `ACTIVE` and `INACTIVE`. An `INACTIVE` rule takes no part in transaction checks but **keeps its existing bindings**, so re-enabling restores the constraints without unbinding anything.
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50 |
| `ruleId` | String | Yes | β€50 |
| `status` | String | Yes | Target status: `ACTIVE` / `INACTIVE` |
| `ruleVersion` | Integer | Yes | Optimistic lock |
The response `data` carries `ruleId`, `ruleName` (so the client can confirm it changed the right rule), `status` (after the change), `ruleVersion` (the new value, required for the next update), `bindingCount` and `modifyTime`.
Before disabling, use `bindingCount` from the response to warn the customer: this rule currently constrains N subjects, and they will no longer be controlled by it.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"ruleId": "5136744097353943556",
"ruleName": "Sales team daily cap",
"status": "INACTIVE",
"ruleVersion": 3,
"bindingCount": "3",
"modifyTime": "2026-08-13T11:00:00Z"
}
}
```
| Error code | Description |
| - | - |
| `VELOCITY_RULE_NOT_FOUND` | The rule does not exist, or does not belong to the `companyId` supplied / to your partner account |
| `VELOCITY_RULE_VERSION_CONFLICT` | `ruleVersion` does not match the server; a concurrent modification happened |
| `VELOCITY_RULE_STATUS_INVALID` | The target status is invalid, or identical to the current one |
## Querying rule details and lists
**Detail**: `GET /open-api-corp/velocity/v1/query-rule` β pass `companyId` + `ruleId` to get the complete rule object (same structure as the create response, including `ruleVersion`, `bindingCount` and the five control groups). If the rule does not exist or does not belong to the `companyId` supplied, the call returns `VELOCITY_RULE_NOT_FOUND`.
**List**: `GET /open-api-corp/velocity/v1/list-rules` β paged query of the company's rules, summaries only.
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50 |
| `status` | String | No | `ACTIVE` / `INACTIVE`; omit to return every status |
| `page` | Integer | No | Page number, starting at 1, default 1 |
| `pageSize` | Integer | No | Page size, default 20, maximum 100 |
The response `data` is the standard paging envelope: `page` / `pageSize` / `total` + `result[]`, each entry carrying `ruleId`, `ruleName`, `status`, `bindingCount` and `modifyTime`.
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"total": 2,
"result": [
{ "ruleId": "5136744097353943557", "ruleName": "Cash advance control", "status": "ACTIVE", "bindingCount": "1", "modifyTime": "2026-08-13T09:00:00Z" },
{ "ruleId": "5136744097353943556", "ruleName": "Sales team daily cap", "status": "ACTIVE", "bindingCount": "3", "modifyTime": "2026-08-13T09:00:00Z" }
]
}
}
```
`total` is a number while `bindingCount` is a string β the latter is declared as a boxed `Long` and the serializer emits it as a string. Parse them separately.
## Next steps
* Adjusting bindings (bind, unbind and the two-way queries): [Binding and unbinding](./binding-and-unbinding)
* How limits participate in the authorization path: [Transaction authorization and 3DS](./authorization-and-3ds)
# Real-Time Authorization and Settlement
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/realtime-authorization
Authorization runs a real-time check chain on the DCS side β entity status, limits, balance β and places a hold (UNPOSTED); results arrive as CARD_TRANSACTION, settlement posts via CARD_TRANSACTION_SETTLEMENT, and debt is flagged by CARD_TRANSACTION_DEBT.
## π Guide
The authorization leg needs no synchronous action from you β DCS decides every authorization in real time, and your job is to consume three transaction events well: the authorization result (`CARD_TRANSACTION`), settlement posting (`CARD_TRANSACTION_SETTLEMENT`) and the debt alert (`CARD_TRANSACTION_DEBT`). This page follows a transaction through its life cycle: the check chain DCS runs at authorization, what each event means for the money, and which field to trust when you reconcile. Life-cycle concepts are in [The transaction life cycle](../basic-concepts/transaction-lifecycle).
## Authorization: the check chain on the DCS side
When the cardholder pays, DCS runs a check chain in real time; failing any link declines the authorization:
1. **Entity status** β the company, the employee and the card must all be in a transactable state;
2. **Limit rules** β the velocity rules the card falls under are evaluated (rule setup in [Setting spend limits](./spend-limits));
3. **Available balance** β the company's fund-pool balance must fully cover the authorized amount.
Once the chain passes, DCS **places a hold on the amount and consumes the matching limit quota**. No money has actually left yet β the transaction shows on the statement as an authorization hold, with `postStatus` = `UNPOSTED`. Authorization and settlement are typically hours to days apart.
## CARD\_TRANSACTION: the authorization-result event
Authorization and release results are pushed through a single webhook, `CARD_TRANSACTION` β **approvals and declines share the event**, distinguished by `status` (`A` approved / `D` declined). The `data` fields:
| Field | Description |
| - | - |
| `cardId` | The card ID |
| `panLast4` | Last four digits of the card |
| `status` | Authorization result: `APPROVED` / `DECLINED` |
| `direction` | Fund direction (`DEBIT` for purchases) |
| `originalAmount` / `originalCurrency` | Original amount and currency |
| `postAmount` / `postCurrency` | Settlement amount and currency |
| `transactionCategory` | The nature of the transaction (e.g. `PURCHASE`) |
| `mcc` | Merchant category code |
| `merchantName` | Merchant name and location |
| `merchantCountryCode` | Merchant country code |
| `transactionId` | The transaction identifier, for matching against records and statements |
Example (an approved purchase):
```json theme={null}
{
"webhookId": "7800000000000000941", "webhookType": "CARD_TRANSACTION",
"businessId": "5185740066240791001", "notificationTime": "2026-08-25T02:30:00Z",
"data": { "cardId": "7830000000000009001", "panLast4": "4417",
"status": "APPROVED", "direction": "DEBIT",
"originalAmount": "16.45", "originalCurrency": "USD",
"postAmount": "16.45", "postCurrency": "USD",
"transactionCategory": "PURCHASE", "mcc": "5812",
"merchantName": "ONLINE MERCHANT",
"merchantCountryCode": "US", "transactionId": "TXN20260802000012345" }
}
```
## Settlement: from hold to posting
When the merchant later submits for clearing, DCS debits the **actual settled amount** and posts the transaction: `postStatus` moves from `UNPOSTED` to `POSTED`, the transaction lands on the cycle's statement, and the webhook `CARD_TRANSACTION_SETTLEMENT` is pushed.
**The settled amount can differ from the authorized amount** β tips, exchange-rate differences, or the merchant capturing less. Reconcile against the actual amount in the settlement event; never assume it equals the hold placed at authorization. Records and statement queries are in [Funding and reconciliation](./funding-and-reconciliation).
## What reversals, refunds and debt mean for the money
Settlement is not the only way forward. The following three scenarios carry different fund semantics β do not reuse one bookkeeping rule across them:
| Scenario | When it happens | Fund semantics |
| - | - | - |
| Reversal / void | After authorization, before settlement | DCS **releases the hold back along the original path** and **restores** the consumed limit quota, leaving no residual hold |
| Refund / chargeback | After settlement | A refund posts as an **independent credit in the opposite direction** and **does not unwind** historical limit counters; chargebacks are tracked through the dispute process to closure |
| Debt | Delayed settlement, merchant over-capture, etc. | The available balance can be **driven negative**; the account enters a debt state and `CARD_TRANSACTION_DEBT` is pushed β the partner must repay on schedule |
Receiving `CARD_TRANSACTION_DEBT` means the company account is already in a debt state. Top up and repay promptly β fund-pool deposits and balance management are in [Funding and reconciliation](./funding-and-reconciliation).
## Authorization to posting at a glance
## Consuming the events well
All three transaction events are HTTP POST callbacks that DCS pushes to you, sharing one envelope structure plus the same signature-verification, retry and idempotency rules:
* Deduplicate on `webhookId` β process each delivery exactly once no matter how often it is retried;
* Read the outcome from `status`, and join a transaction's authorization to its settlement via `transactionId`;
* Post to your books from the settlement event; treat the authorization event as hold display only.
Webhook receiver setup, the envelope structure and signature rules are in the [Quickstart](../getting-started/quickstart).
## Next steps
* Shape approval rates with velocity rules: [Setting spend limits](./spend-limits)
* Taking part in verification when an online purchase triggers 3DS: [Handling 3DS challenges](./3ds-challenges)
# Retrieving secure card details
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/secure-card-details
PCI DSS-allowlisted partners call retrieve-secure-card for ciphertext-level PAN / CVV2 / expiry and decrypt them in their own frontend under the AES-GCM contract; plus the cardholder data protection requirements and opening receiving accounts for dedicated-balance cards (open-va).
## π Guide
A card's full number (PAN), CVV2 and expiry date are sensitive authentication data; the ordinary card queries return only `panFirst6` / `panLast4` (see the single-card query in [Applying for a virtual card](./applying-virtual-cards)). To show the full card details to a cardholder, PCI DSS-certified partners call this page's `retrieve-secure-card` for the ciphertexts and decrypt them for display themselves; partners without PCI DSS can integrate the DCS hosted secure card page instead. This page also covers `open-va`, the receiving-account endpoint for dedicated-balance cards.
## Retrieving secure card details
```http theme={null}
POST /open-api-corp/card/v1/retrieve-secure-card
```
Returns ciphertext-level PAN / CVV2 / expiry for secure display in your frontend.
**Only PCI DSS-certified partners may call this endpoint** (`PCI_NOT_CERTIFIED` otherwise); partners without PCI DSS can integrate the platform's hosted secure card page.
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | β€50; the requesting company ID |
| `cardId` | String | Yes | β€50; the card ID; must belong to your partnership and be ACTIVE |
### Response data
| Field | Type | Description |
| - | - | - |
| `cardId` | String | The card ID |
| `encryptedPan` | String | The PAN (AES-GCM ciphertext) |
| `encryptedCvv2` | String | The CVV2 (ciphertext) |
| `encryptedExpireDate` | String | The expiry (ciphertext; plaintext format mm/yy) |
| `iv` | String | The random IV for this call (Base64), needed to decrypt |
### Decryption contract
* Algorithm: **AES/GCM/NoPadding** with a 128-bit authentication tag.
* Key: your **SK**.
* IV: the three ciphertext fields `encryptedPan` / `encryptedCvv2` / `encryptedExpireDate` **share the same `iv`** (randomly generated per call, returned Base64-encoded).
**CVV2 and expiry are fetched live and must never be cached** β call this endpoint again for fresh ciphertexts every time they need to be displayed.
### Request and response examples
```json theme={null}
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"cardId": "5185740066240790530"
}
```
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"cardId": "5185740066240790530",
"encryptedPan": "UGFuTW9ja0NpcGhlclRleHRCYXNlNjQrVGFnMTZC",
"encryptedCvv2": "Q3Z2Mk1vY2tDaXBoZXJUZXh0K1RhZzE2Qg==",
"encryptedExpireDate": "RXhwTW9ja0NpcGhlclRleHQrVGFnMTZC",
"iv": "MTIzNDU2Nzg5MGFi"
}
}
```
### Error codes
| Error code | Description |
| - | - |
| `PCI_NOT_CERTIFIED` | Secure card data access not enabled (not on the PCI allowlist) |
| `CARD_INVALID` | The card does not exist / does not belong to your partnership |
| `CARD_STATE_INVALID` | The card state does not allow viewing sensitive details; only ACTIVE / FROZEN |
## PCI DSS Β· Cardholder data protection
Under PCI DSS, the full PAN, CVV2 and expiry date are sensitive authentication data and must be handled end-to-end under the minimum-exposure principle:
* **Mask by default**: the frontend shows only the masked PAN by default β first 6 and last 4 digits preserved (e.g. 531993 β’β’β’β’ β’β’β’β’ 8888); CVV2 and expiry are not shown by default.
* **Temporary display only when needed**: decrypt and display the plaintext only when there is a genuine business need and the cardholder's identity has been verified.
* **Plaintext never persisted**: during display the plaintext must not be stored in any form β databases, files, logs, caches, analytics and tracing must all keep none of it; CVV2 must not be retained after authorization either.
* **Keys in a KMS**: decryption keys must be held in a key management service (KMS) and rotated regularly.
## Opening a receiving account for a dedicated-balance card
```http theme={null}
POST /open-api-corp/card/v1/open-va
```
Opens a top-up account (VA) per currency for a dedicated-balance card; you can then fund the card either by bank transfer or by allocation from the company pool (for funding and reconciliation see [Funding and reconciliation](./funding-and-reconciliation)).
### Request parameters
| Field | Type | Required | Description |
| - | - | - | - |
| `cardId` | String | Yes | β€50; the card ID; must belong to your partnership and be a dedicated-balance card |
| `fundingCurrencies` | `Array` | Yes | The currencies to open VAs for (at least one; values `USD` / `HKD`; one VA per currency); no empty elements, no duplicate currencies β empty/duplicate/non-enum values β `DAPI_PARAM_INVALID`; the set must also be a subset of the company's permitted currencies, and anything out of range rejects the whole request (no partial opening) β `CURRENCY_NOT_ALLOWED` |
### Response
An action endpoint: `data` is `null`. To read the account numbers that were opened, call [get deposit account information](./deposits) with `subjectType=CARD` β opening a VA and reading its account number are two separate endpoints, and this one only opens.
### Request and response examples
```json theme={null}
{
"cardId": "5185740066240790531",
"fundingCurrencies": ["USD", "HKD"]
}
```
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": null
}
```
### Error codes
| Error code | Description |
| - | - |
| `CARD_INVALID` | The card does not exist / does not belong to your partnership |
| `CARD_NOT_DEDICATED` | Not a dedicated-balance card; VAs cannot be opened |
| `CURRENCY_NOT_ALLOWED` | A requested currency is not in the company's permitted list (the message names the currency) |
| `SUBJECT_INVALID` | The card's ledger holder (account\_holder) is missing, or the cardholder's KYB/KYC name is missing (both holder-side problems that prevent opening a VA) |
## Next steps
* Fund a dedicated-balance card by transfer or company-pool allocation, and reconcile: [Funding and reconciliation](./funding-and-reconciliation)
* Upgrade an activated virtual card to a physical card under the same number: [Physical cards](./physical-cards)
# Limits Β· Overview
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/spend-limits
Control spending and withdrawals with velocity rules: rule creation (five control groups), full-replacement updates with optimistic locking, batch bind/unbind with partial-success semantics, and querying merged effective quotas.
## π Guide
Velocity rules are standalone objects: owned by a company, bound many-to-many to cards or employees, with the strictest value winning per dimension. This page walks "create β bind β query quota β adjust"; the concepts are in [Limits and statements](../basic-concepts/limits-and-billing).
## Creating a rule
`POST /open-api-corp/velocity/v1/create-rule` β send `ruleRef` (idempotency key: the same company + `ruleRef` on retry returns the first result), `companyId`, `ruleName`, and **at least one** of the five control groups (none at all returns `DAPI_PARAM_INVALID`):
| Control group | Content |
| - | - |
| `purchaseControl` | Purchase limits: `amountLimits[]` per currency (`perTransaction` / `daily` / `monthly` / `quarterly` / `yearly`, all > 0) + `countLimits` (counts, summed across currencies) |
| `cashAdvanceControl` | Withdrawal limits: same structure, plus the group switch `allowed` (required within the group; `false` bans withdrawals outright) |
| `currencyControl` | Purchase-currency list: `type` (WHITELIST / BLACKLIST) + `currencies[]` (judged against the **original purchase currency**) |
| `mccControl` | Merchant-category list: `type` + `mccList[]` (4 digits) |
| `geographicControl` | Region list: `type` + `countryCodes[]` (two-letter ISO, uppercase; judged against the merchant country code) |
**"Unlimited" is expressed by omitting the field β never send 0 or a negative number** (`VELOCITY_LIMIT_VALUE_INVALID`). Amount limits match on the **settlement currency**, while `currencyControl` judges the original purchase currency β two different axes.
A new rule is always `ACTIVE`, but **constrains nothing until it is bound** to a card or employee. The response returns `ruleId`, `ruleVersion` (optimistic lock), `bindingCount` and more.
```json theme={null}
{
"ruleRef": "ext-request-0001",
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"ruleName": "Employee travel",
"purchaseControl": {
"amountLimits": [
{ "currency": "USD", "perTransaction": "3000.00", "daily": "10000.00", "monthly": "80000.00" }
],
"countLimits": { "daily": 5, "monthly": 100 }
},
"cashAdvanceControl": {
"allowed": true,
"amountLimits": [ { "currency": "USD", "perTransaction": "500.00", "daily": "2000.00" } ]
}
}
```
## Binding and unbinding
**Bind**: `POST /open-api-corp/velocity/v1/bind` β one request addresses **one rule** (top-level `ruleId`) and a batch of subjects in `targets[]` (`subjectType` CARD / CUSTOMER + `subjectId`). Rule-level problems (missing / not ACTIVE) **reject the whole batch**; subject-level problems are **item-independent with partial success**: the response splits into `successTargets` and `failTargets`, with per-item reasons such as `SUBJECT_INVALID` / `CARD_RULE_DUPLICATE` / `CARD_RULE_CURRENCY_NOT_MATCH`. Rules can also be bound at issue time through `ruleIds`. A single card or employee carries at most 5 rules.
**Unbind**: `POST /open-api-corp/velocity/v1/unbind` β batch, partial success as above; unbinding a subject that is not bound is not an error and counts in `successTargets`.
**Query targets**: `list-rule-targets` (objects bound to a rule) and `list-target-rules` (rules constraining a card / employee; ACTIVE only by default).
## Querying effective quota
`GET /open-api-corp/velocity/v1/query-quota` β the **merged** effective limits and current usage for a card / employee, from which you compute the remainder. Key points:
* Each quota node returns `limitAmount` / `usedAmount` / `remainingAmount` / `utilizationRate` / `periodKey` / `sourceRuleId` β `sourceRuleId` names the rule the strictest value came from; `limit=null` means the dimension is uncapped (`usedAmount` still accumulates).
* `perTransaction` carries only `limitAmount` and `sourceRuleId` β per-transaction limits do not accumulate.
* Each currency group carries an `effective` flag: `false` + `NOT_CARD_SETTLEMENT_CURRENCY` means the currency is not among the card's settlement currencies and does not apply.
* `cashAdvance.allowed` is the merged switch: **false if any rule says false**, with `sourceRuleId` naming which one.
* `appliedRules` lists the rules in the merge (always ACTIVE β INACTIVE rules never participate).
## Adjusting rules
**Update**: `POST /open-api-corp/velocity/v1/update-rule` β **full-replacement semantics**: any dimension or control group absent from the request is deleted; `ruleVersion` must be echoed back, and a mismatch returns `VELOCITY_RULE_VERSION_CONFLICT` (concurrent modification β re-query and retry). Changes take effect **immediately** on bound objects.
**Enable / disable**: `POST /open-api-corp/velocity/v1/change-rule-status` β toggles `ACTIVE` β `INACTIVE` (also versioned). An `INACTIVE` rule takes no part in checks but keeps its targets; the response echoes `bindingCount` β before disabling, warn the customer that the rule currently constrains N objects.
**Detail / list**: `query-rule` (full configuration) and `list-rules` (paginated summaries, filterable by status).
## Next steps
* Bind rules at issue time: [Managing cards](./managing-cards)
* How limits participate in authorization: [Authorization and 3DS](./authorization-and-3ds)
* In this group: [Creating velocity rules](./creating-rules) Β· [Binding and unbinding](./binding-and-unbinding) Β· [Quota and adjustment](./quota-and-adjustment)
# Statements and Transactions
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/statements-and-transactions
Pull monthly statements by company or by card (statements / statement-detail) and transaction records unbounded by billing cycles (transactions): the statement header, per-currency summary and transaction-line fields, plus how amount due and overpaymentAmount are computed.
## π Guide
DCS issues a statement for every billing subject automatically, per calendar month, with nothing for the partner to trigger; the data is pulled over the API. The balance mode decides the billing subject: the company pool (SHARED) gets one statement per company, dedicated card balances (DEDICATED) get one statement per card. The billing cycle and its key dates are in [Limits and statements](../basic-concepts/limits-and-billing).
The three query endpoints divide the work:
| Endpoint | Use |
| - | - |
| `GET /open-api-corp/statement/v1/statements` | Statement list: statements over a time range, by company or by card |
| `GET /open-api-corp/statement/v1/statement-detail` | Statement detail: header + per-currency summary + transaction lines |
| `GET /open-api-corp/statement/v1/transactions` | Transaction list: records by time window, unbounded by billing cycles |
**The settled statement is final**: a settled statement (`SETTLED`) is fixed and contains only posted transactions; the open statement (`OPEN`, `statementId` is null) is a live summary of the current cycle that includes unsettled authorization holds, so its purchase total falls back as transactions clear β expected behavior.
## Statement list
`GET /open-api-corp/statement/v1/statements` returns statements by company or by card, paged with the most recent cycle first. To query by time range use the transaction list endpoint.
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | Company ID |
| `cardId` | String | No | β€50; pass it to bill by card, omit it to bill by company. Only dedicated-balance cards have their own statement |
| `page` | Integer | No | Default 1 |
| `pageSize` | Integer | No | Default 20, at most 100 |
**Response data** is the standard paging envelope `page` / `pageSize` / `total` + `result[]`, and no longer echoes the request. Each `result[]` entry is a statement header plus per-currency summary. The total already includes the open statement. Each `result[]` entry is a statement header plus a per-currency summary:
| Field | Description |
| - | - |
| `statementId` | Statement ID; null for the open statement |
| `statementPeriod` | Billing cycle, yyyy-MM |
| `status` | `OPEN` (not yet issued) / `SETTLED` (issued) |
| `statementDate` | Statement date, yyyy-MM-dd; the expected statement date for the open statement |
| `paymentDueDate` | Payment due date, yyyy-MM-dd; the expected due date for the open statement |
| `paymentStatus` | Repayment status, computed live, see below |
| `summary` | Per-currency summary array, one element per currency, see below |
The four `paymentStatus` states:
| Value | Meaning |
| - | - |
| `CURRENT` | Current cycle, in good standing |
| `AWAITING_PAYMENT` | Awaiting payment |
| `PAID` | Paid off |
| `OVERDUE` | Overdue |
`summary` fields per currency:
| Field | Description |
| - | - |
| `currency` | Grouping currency, e.g. `USD` / `HKD` |
| `creditLimitAmount` | Nominal limit, configured per currency by the platform |
| `availableCreditLimitAmount` | Available deposit balance (the overpaymentAmount shown as a positive number, 0.00 when there is none) |
| `totalPurchaseAmount` | Purchases total (+) |
| `totalRefundAmount` | Refunds and chargebacks total (β) |
| `totalFeeAmount` | Fees total (+) |
| `totalRepaymentAmount` | Repayments / top-ups total (β) |
| `totalCashAdvanceAmount` | Cash advances total (net, +) |
| `totalDebitAmount` | Internal allocations total; not shown among the lines, always 0.00 for SHARED (transfers are in [Balances and Transfers](./balances-and-transfers)) |
| `totalDueAmount` | Amount due (+) |
| `overpaymentAmount` | Overpayment (β) |
**Request example**
```http theme={null}
# By company, without cardId
GET /open-api-corp/statement/v1/statements?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&page=1&pageSize=20
# By dedicated-balance card
GET /open-api-corp/statement/v1/statements?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&cardId=5185740066240790530&page=1&pageSize=20
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"total": 3,
"result": [{
"statementId": "6410000000000000001",
"statementPeriod": "2026-03",
"status": "SETTLED",
"statementDate": "2026-04-01",
"paymentDueDate": "2026-04-27",
"paymentStatus": "AWAITING_PAYMENT",
"summary": [{
"currency": "USD",
"creditLimitAmount": "500.00",
"availableCreditLimitAmount": "0.00",
"totalPurchaseAmount": "120.00",
"totalRefundAmount": "-20.00",
"totalFeeAmount": "1.50",
"totalRepaymentAmount": "-80.00",
"totalCashAdvanceAmount": "0.00",
"totalDebitAmount": "0.00",
"totalDueAmount": "21.50",
"overpaymentAmount": "0.00"
}]
}]
}
}
```
## How amount due and overpaymentAmount are computed
The result fields of `summary` derive from one net figure S β purchases, fees and cash advances are positive, repayments / top-ups and refunds are negative; a positive net is debt, a negative net is overpaymentAmount:
```text theme={null}
S = totalRepaymentAmount + totalPurchaseAmount + totalRefundAmount
+ totalFeeAmount + totalCashAdvanceAmount + totalDebitAmount
totalDueAmount = max(0, S) amount due, debt shown as a positive number
overpaymentAmount = min(0, S) overpaymentAmount, shown as a negative number
availableCreditLimitAmount = S < 0 ? -S : 0 available deposit balance, overpaymentAmount shown positive
```
Checked against the response example above: S = β80.00 + 120.00 + (β20.00) + 1.50 + 0 + 0 = 21.50, so `totalDueAmount` is 21.50 and `overpaymentAmount` is 0.00.
## Statement detail
`GET /open-api-corp/statement/v1/statement-detail` returns the statement header + the per-currency summary + the transaction lines (pagination applies to the lines only).
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | Company ID |
| `cardId` | String | No | β€50; pass it to bill by card, omit it to bill by company. Only dedicated-balance cards have their own statement |
| `statementId` | String | No | Required when querying a settled statement; pass null for the open statement |
| `status` | String | No | Only takes `OPEN`: pass it when querying the open statement; omit it for a settled one |
| `page` | Integer | No | Default 1, applies to the lines only |
| `pageSize` | Integer | No | Default 20, at most 100 |
Two ways to query: for a **settled statement** pass `statementId` and omit `status`; for the **current open statement** pass `statementId=null` + `status=OPEN`.
**Response data** is the standard paging envelope `page` / `pageSize` / `total` + `result[]`; it no longer returns the statement header or `summary` β call the statement list for those. Each `result[]` entry is one transaction line:
| Field | Description |
| - | - |
| `currency` | Posting currency, also the grouping currency of the lines |
| `transactionId` | Transaction reference |
| `transactionCategory` | Transaction category, seven values, see below |
| `transactionTime` | Transaction time |
| `remark` | Line remark / transaction description, free text |
| `cardId` | Card ID; null for cardless rows such as deposits / transfers |
| `panLast4` | Last four digits of the card number; null for cardless rows |
| `postStatus` | Posting status, three states, see below |
| `originalCurrency` / `originalAmount` | Original transaction currency and amount |
| `postCurrency` / `postAmount` | Posting currency and amount |
| `postTime` | Posting / arrival time; null while on authorization hold |
| `merchant` | Merchant details: `merchantName` / `mcc` / `merchantCountryCode`; null for repayment-type rows |
The seven `transactionCategory` values:
| Value | Meaning |
| - | - |
| `PURCHASE` | Purchase |
| `REFUND` | Refund |
| `DEPOSIT` | Deposit |
| `TRANSFER` | Transfer |
| `CASH_ADVANCE` | Cash advance |
| `FEE` | Fee |
| `REPAYMENT` | Repayment / top-up |
The three `postStatus` states:
| Value | Meaning |
| - | - |
| `POSTED` | Posted |
| `UNPOSTED` | On authorization hold (`postTime` is null) |
| null | Not meaningful for repayment-type rows |
**Request example**
```http theme={null}
# A settled statement: pass statementId
GET /open-api-corp/statement/v1/statement-detail?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&statementId=6410000000000000001&page=1&pageSize=20
# The current open statement: omit statementId, pass status=OPEN
GET /open-api-corp/statement/v1/statement-detail?companyId=e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f&status=OPEN&page=1&pageSize=20
```
**Response example**
```json theme={null}
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"total": 2,
"result": [
{
"transactionId": "6510000000000000001",
"transactionCategory": "REPAYMENT",
"transactionTime": "2026-03-10 16:00:12",
"remark": "REPAYMENT",
"cardId": null,
"panLast4": null,
"postStatus": null,
"currency": "USD",
"originalCurrency": "USD",
"originalAmount": "-80",
"postCurrency": "USD",
"postAmount": "-80",
"postTime": "2026-03-10T08:00:12Z",
"merchant": null
},
{
"transactionId": "6510000000000000002",
"transactionCategory": "PURCHASE",
"transactionTime": "2026-03-05 20:03:41",
"remark": "ACME ONLINE STORE",
"cardId": "5185740066240790530",
"panLast4": "5492",
"postStatus": "POSTED",
"currency": "USD",
"originalCurrency": "JPY",
"originalAmount": "18000",
"postCurrency": "USD",
"postAmount": "121.35",
"postTime": "2026-03-06T02:10:05Z",
"merchant": {
"merchantName": "ACME ONLINE STORE",
"mcc": "5732",
"merchantCountryCode": "JP"
}
}
]
}
}
```
## Transaction list
`GET /open-api-corp/statement/v1/transactions` returns transaction records by time window, unbounded by billing cycles, in reverse time order.
**Request parameters**
| Field | Type | Required | Description |
| - | - | - | - |
| `companyId` | String | Yes | Company ID |
| `cardId` | String | No | Omit = the company view; pass = narrow to that card (including deposits into its bound virtual account) |
| `startTime` | String | Yes | Range start, ISO-8601 |
| `endTime` | String | Yes | Range end, ISO-8601 |
| `page` | Integer | No | Default 1 |
| `pageSize` | Integer | No | Default 20, at most 100 |
The `startTime`β`endTime` window must not exceed six months; query older records in segments.
**Response data** is the standard paging envelope (`page` / `pageSize` / `total` / `result`); each `result` element is ordered by `transactionTime` descending, with exactly the same fields as the statement-detail lines above.
**Request example**
```json theme={null}
// Request: by card
{
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"cardId": "7830000000000009001",
"startTime": "2026-01-01T00:00:00Z",
"endTime": "2026-06-30T23:59:59Z",
"page": 1,
"pageSize": 20
}
```
**Response example**
```json theme={null}
// Response: one purchase on authorization hold
{
"code": "SYS_SUCCESS",
"message": null,
"data": {
"total": 152,
"result": [{
"currency": "USD",
"transactionId": "178289884146286860002494",
"transactionCategory": "PURCHASE",
"transactionTime": "2026-03-05T12:03:41Z",
"remark": "EXAMPLE MERDOET",
"cardId": "7830000000000009001",
"panLast4": "8180",
"postStatus": "UNPOSTED",
"originalCurrency": "USD",
"originalAmount": "50.00",
"postCurrency": "USD",
"postAmount": "50.00",
"postTime": null,
"merchant": {
"merchantName": "EXAMPLE MERDOET",
"mcc": "4511",
"merchantCountryCode": "JP"
}
}]
}
}
```
## Next steps
* How debt arises, and how authorizations hold and clear: [Authorization and 3DS](./authorization-and-3ds)
* Repayment is a deposit β the funding path and arrival notification: [Deposits](./deposits)
# Webhook configuration
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/webhooks/configuration
How to register the callback URL, recompute and verify X-Signature with sortedCompactJson, deduplicate by webhookId, and what the delivery and retry windows are.
## π Guide
Company onboarding, employee creation, card issuing, transactions and deposits are all asynchronous: the platform pushes the final state to the callback URL you registered as soon as the event happens, so you never have to poll. Your receiver only needs **one** endpoint β every event shares the same envelope, so you read `webhookType` and then parse `data`. The full event list and each event's `data` structure are in [Events and data structures](./events-and-schema).
## 1. Registering the callback URL
The callback URL is configured for you by platform operations; there is no self-service endpoint, so contact your DCS representative to change it. **Sandbox and production are configured separately** and must both be registered.
With no callback URL configured, any path that needs your participation fails outright β a 3DS challenge cannot be forwarded to you and the transaction is declined. Configure the callback URL before going live with OOB or with `otpSendMode=ENTERPRISE`.
### Requirements for the callback URL
| Requirement | Detail |
| :- | :- |
| Protocol | HTTPS, with a certificate that validates publicly |
| Method | Accept `POST` with a JSON body |
| Response | HTTP `2xx` counts as a successful delivery; the platform **does not parse the response body** |
| Latency | Return quickly β persist first and process asynchronously, never do long-running work inside the callback |
| Idempotency | The same `webhookId` may arrive more than once; deduplicate on it and **still return `2xx`** after deduplicating |
## 2. Verifying the signature (HMAC-SHA256)
Every delivery carries `X-Signature`:
```
X-Signature = Hex( HMAC-SHA256( SK, sortedCompactJson(entire payload) ) )
```
`SK` is the webhook signing key issued to you specifically, isolated per partner. It is **not** the same key you use for API request signing.
### Headers
| Header | Detail |
| :- | :- |
| `X-Signature` | The signature, lowercase hexadecimal |
| `Accept` | `application/json` |
The event type, timestamp and IDs all live in the body β **never in headers**.
**You cannot verify against the raw bytes.** `sortedCompactJson` means: sort the fields at **every level** of the payload by field name, then serialise compactly with no whitespace. You must re-serialise the same way before comparing, otherwise the signature will never match.
### Verification example
```python theme={null}
import hmac, hashlib, json
def sorted_compact_json(obj):
# every level sorted by field name, then compact serialisation
return json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
def verify(payload_dict: dict, header_signature: str, sk: str) -> bool:
body = sorted_compact_json(payload_dict).encode('utf-8')
expected = hmac.new(sk.encode('utf-8'), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header_signature.lower())
```
Discard and alert on a failed verification; never process it as business data.
## 3. Delivery, retries and ordering
| Item | Contract |
| :- | :- |
| Success | HTTP `2xx`; the response body is not parsed |
| Retry trigger | Any non-`2xx`, or a timeout |
| Retry cadence | Queued for redelivery and swept roughly once a minute; at most **3** retries, about 1 minute apart, **no exponential backoff** |
| Total window | First attempt plus retries is at most **4** deliveries, finishing within about **3 minutes** |
| Still failing | The platform alerts and follows up manually |
| Idempotency key | `webhookId` is globally unique and **reused across every retry** of the same event |
| Ordering | **Not guaranteed.** Order by `webhookId` (a monotonically increasing snowflake ID) plus the resource's current state; never assume arrival order |
| Fallback | If nothing arrives within the window, reconcile through the corresponding query endpoint |
The retry window is only about 3 minutes. A receiver that is down longer than that will miss events, so you **must** reconcile through the query endpoints β do not rely on webhooks alone.
## Next steps
* The full event list, envelope fields and each event's `data`: [Events and data structures](./events-and-schema)
* Receiving and replying to 3DS challenges: [3DS challenges](../3ds-challenges)
# Events and data structures
Source: https://docs.thedecard.com/en/corporate-card/how-to-use/webhooks/events-and-schema
The envelope fields, businessId mapping and per-event data structures for corporate card outbound webhook events; configuration and signature verification are covered in Webhook configuration.
## π Guide
Company onboarding, employee creation, card issuing, card status, 3DS challenges, the three transaction events and the deposit alert make up five groups of outbound events. Every event shares one envelope β read `webhookType`, then parse the matching `data`. Your receiver needs a single endpoint.
> **Prerequisites**: register the callback URL and be ready to recompute `X-Signature` with your `SK`. Setup steps and verification code are in [Webhook configuration](./configuration).
## The envelope
```json theme={null}
{
"webhookId": "7800000000000000900",
"webhookType": "COMPANY_CREATED",
"businessId": "7810000000000000123",
"data": { },
"notificationTime": "2026-08-25T02:30:00Z"
}
```
| Field | Type | Description |
| :- | :- | :- |
| `webhookId` | String | The webhook's unique ID. **The idempotency key** β reused across every retry of the same event |
| `webhookType` | String | Event type code, uppercase with underscores; the full set is below |
| `businessId` | String | The ID of the object this event is about; its meaning varies with `webhookType` (e.g. `companyApplyId` / `cardId` / `transactionId`). Per-event mapping below |
| `data` | Object | Business data; the fields vary by `webhookType` |
| `notificationTime` | String | Delivery timestamp, ISO-8601 UTC. **It is the time of this delivery, so retries of one event carry different values** |
The envelope matches the common webhook structure used by the platform's other product lines, so one consumer can handle events from all of them. Every ID field is a string; amounts are strings and trailing zeros are not guaranteed.
## Event overview
| Group | `webhookType` | `businessId` is | Trigger |
| :- | :- | :- | :- |
| Company | `COMPANY_CREATED` | `companyApplyId` | KYB passed, company created |
| | `COMPANY_REJECTED` | `companyApplyId` | KYB rejected |
| | `COMPANY_STATUS_CHANGED` | `companyId` | Company restrictions changed (added / removed) |
| Employee | `CUSTOMER_CREATED` | `customerApplyId` | KYC passed, employee created |
| | `CUSTOMER_REJECTED` | `customerApplyId` | KYC rejected |
| | `CUSTOMER_STATUS_CHANGED` | `customerId` | Employee restrictions changed (added / removed) |
| Card | `CARD_CREATED` | `cardApplyId` | Risk checks passed and the card was created |
| | `CARD_REJECTED` | `cardApplyId` | Risk declined, or card creation failed |
| | `CARD_SHIPPED` | `cardId` | The physical card was produced and dispatched (once, when the tracking number is generated) |
| | `CARD_ACTIVATED` | `cardId` | The physical card was activated |
| | `CARD_STATUS_CHANGED` | `cardId` | Card status changed (freeze / unfreeze / lock / unlock / lost / close); virtual and physical alike |
| | `AUTHORISATION_3DS_CHALLENGE` | `challengeId` | A cardholder transaction triggered a 3DS challenge needing your verification or OTP delivery |
| Transaction | `CARD_TRANSACTION` | `transactionId` | Card authorization / release (approval and decline share one event, distinguished by `data.status`) |
| | `CARD_TRANSACTION_SETTLEMENT` | `transactionId` | Card settlement |
| | `CARD_TRANSACTION_DEBT` | `transactionId` | The card entered a debt state |
| Funds | `BANK_TRANSFER_INFO` | `depositId` | A VA deposit landed (both success and failure, distinguished by `data.status`) |
| | `BALANCE_CHANGE` | `transactionId` | Balance changed |
| | `LOW_BALANCE` | `companyId` | Funding pool balance below the alert threshold |
| | `VA_STATUS_CHANGED` | `companyId` | Async result of opening or updating a VA (sent on both success and failure, distinguished by `data.status`) |
Every event's `data` carries `companyId` except `COMPANY_REJECTED` (the entity does not exist yet) and the three transaction events (attribution is derivable from `cardId`), so you never have to look up ownership.
***
## Company events
### `COMPANY_CREATED`
| Field | Type | Description |
| :- | :- | :- |
| `companyId` | String | Company ID |
| `companyApplyRef` | String | Your own company application reference (echoed) |
| `companyApplyId` | String | Company application ID |
### `COMPANY_REJECTED`
| Field | Type | Description |
| :- | :- | :- |
| `companyApplyRef` | String | Your own company application reference |
| `companyApplyId` | String | Company application ID |
| `rejectMessage` | String | The KYB rejection reason |
This event **carries no `companyId`** β the company entity is only persisted once KYB passes, so it does not exist at rejection time. Match on `companyApplyRef` or `companyApplyId`.
### `COMPANY_STATUS_CHANGED`
Mirrors [updating company restrictions](../company-maintenance); pushed once the change is committed.
| Field | Type | Description |
| :- | :- | :- |
| `companyId` | String | Company ID |
| `restrictions` | Array\ | The **full set** of restrictions after the change (not a delta); an empty array means no restriction |
| `addRestrictions` | Array\ | Restrictions added by this change |
| `removeRestrictions` | Array\ | Restrictions removed by this change |
| `remark` | String | Operation remark |
***
## Employee events
### `CUSTOMER_CREATED`
| Field | Type | Description |
| :- | :- | :- |
| `customerId` | String | Employee ID |
| `customerApplyRef` | String | Your own employee identifier (echoed) |
| `companyId` | String | The company the employee belongs to |
| `customerApplyId` | String | Employee application ID |
### `CUSTOMER_REJECTED`
| Field | Type | Description |
| :- | :- | :- |
| `customerApplyRef` | String | Your own employee identifier |
| `companyId` | String | The company the employee belongs to |
| `customerApplyId` | String | Employee application ID |
| `rejectMessage` | String | The KYC rejection reason |
### `CUSTOMER_STATUS_CHANGED`
Mirrors [updating employee restrictions](../employee-maintenance).
| Field | Type | Description |
| :- | :- | :- |
| `customerId` | String | Employee ID |
| `companyId` | String | The company the employee belongs to |
| `restrictions` | Array\ | The **full set** of restrictions after the change (not a delta); an empty array means no restriction |
| `addRestrictions` | Array\ | Restrictions added by this change |
| `removeRestrictions` | Array\ | Restrictions removed by this change |
| `remark` | String | Operation remark |
***
## Card events
### `CARD_CREATED`
| Field | Type | Description |
| :- | :- | :- |
| `cardId` | String | Card ID |
| `companyId` | String | The company the card belongs to |
| `cardApplyId` | String | Card application ID |
| `cardApplyRef` | String | Your own application identifier (echoed) |
### `CARD_REJECTED`
| Field | Type | Description |
| :- | :- | :- |
| `companyId` | String | The company the card belongs to |
| `cardApplyId` | String | Card application ID |
| `cardApplyRef` | String | Your own application identifier (echoed) |
| `rejectMessage` | String | Why it failed (risk decline or a card-creation error) |
### `CARD_SHIPPED`
Pushed when the tracking number is generated, **exactly once**.
| Field | Type | Description |
| :- | :- | :- |
| `cardId` | String | Card ID |
| `companyId` | String | The company the card belongs to |
| `trackingNumber` | String | Tracking number |
| `trackingCompanyName` | String | Courier |
| `trackingNumberDate` | String | Waybill date, `yyyy-MM-dd` |
### `CARD_ACTIVATED`
| Field | Type | Description |
| :- | :- | :- |
| `cardId` | String | Card ID |
| `companyId` | String | The company the card belongs to |
### `CARD_STATUS_CHANGED`
| Field | Type | Description |
| :- | :- | :- |
| `cardId` | String | Card ID |
| `companyId` | String | The company the card belongs to |
| `fromStatus` | String | Card status before the change (one of 7) |
| `toStatus` | String | Card status after the change (one of 7) |
This event covers neither **activation** nor **expiry**: activation is pushed as `CARD_ACTIVATED`, and expiry currently has no trigger. The 7 card statuses are listed in [Managing cards](../managing-cards).
### `AUTHORISATION_3DS_CHALLENGE`
The business flow and how to reply are covered in [3DS challenges](../3ds-challenges).
| Field | Type | Description |
| :- | :- | :- |
| `challengeId` | String | Challenge ID, echoed back when you reply. **Deduplicate on it** |
| `cardId` | String | The card that triggered the challenge |
| `companyId` | String | The company the card belongs to |
| `expiryTime` | String | When the challenge expires, ISO-8601 UTC (about 300 seconds by default) |
| `currency` | String | Transaction currency, ISO-4217; paired with `amount` |
| `amount` | String | Transaction amount |
| `merchantId` | String | Merchant identifier |
| `merchantName` | String | Merchant name |
| `merchantCountryCode` | String | Merchant country, ISO-3166-1 alpha-2 uppercase |
| `mcc` | String | Merchant category code, four digits |
| `challengeFlowType` | String | `OOB` / `OTP_DELEGATE` β tells you whether to parse the encrypted fields below |
| `challengeMethodType` | String | `DELEGATE_SCA_V1` / `SMS_OTP` / `EMAIL_OTP` |
The following four fields are populated **only when `challengeFlowType=OTP_DELEGATE` and the partner is configured with `otpSendMode=ENTERPRISE`**:
| Field | Type | Description |
| :- | :- | :- |
| `encryptedOtpPasscode` | String | The OTP, encrypted |
| `encryptedPhoneNumber` | String | The cardholder's mobile number, encrypted |
| `encryptedEmail` | String | The cardholder's email address, encrypted |
| `iv` | String | AES-GCM initialisation vector (Base64), shared by the three encrypted fields above |
Decryption: `AES/GCM/NoPadding` with a 128-bit authentication tag; the key is your `SK` (the same one used for request signing), following the same contract as retrieving secure card details.
`merchantCountryCode` changes from alpha-3 (three letters) to **alpha-2 (two uppercase letters)** β that is a value conversion, not just a rename, so your parsing needs to change with it.
***
## Transaction events
The three transaction events describe the same facts as statement line items and reuse the statement domain's vocabulary: `panLast4` / `transactionTime` / `transactionCategory` / `originalAmount`+`originalCurrency` (the original transaction amount) / `postAmount`+`postCurrency` (the posted amount) / `merchantName` / `mcc` / `merchantCountryCode`.
**Transaction events carry no `companyId`**: attribution is derivable from `cardId` β a card's company is fixed at issue time and never changes, and you already received it in `CARD_CREATED`. Transaction events are far higher in volume than lifecycle events, so they carry only what is necessary. To aggregate by company, use [Statements and transactions](../statements-and-transactions).
### `CARD_TRANSACTION`
Authorization and release share one event; approval versus decline is given by `status`.
| Field | Type | Description |
| :- | :- | :- |
| `transactionId` | String | Transaction ID, identical to the statement line's, so it reconciles directly |
| `originalTransactionId` | String | The original transaction's ID. Present only for release / reversal / refund events |
| `cardId` | String | Card ID |
| `panLast4` | String | Last four digits of the card number |
| `status` | String | Authorization result: `APPROVED` / `DECLINED` |
| `direction` | String | Debit or credit: `DEBIT` / `CREDIT` / `DEDUCT`. Absent entirely when upstream sends no indicator |
| `originalAmount` / `originalCurrency` | String | The original transaction amount and currency |
| `postAmount` / `postCurrency` | String | The posted (accounting) amount and currency |
| `transactionTime` | String | Transaction time in the acquirer's local time, `yyyy-MM-dd HH:mm:ss` |
| `transactionCategory` | String | `SALES` / `CASH_ADVANCE` / `PAYMENT` / `INSTALLMENT` / `INQUIRY` / `TRANSFER_OUT` |
| `merchantName` | String | Merchant name, passed through from upstream and may include an address |
| `merchantCountryCode` | String | Merchant country, alpha-2 uppercase |
| `mcc` | String | Merchant category code, four digits |
```json theme={null}
{
"webhookId": "7800000000000001201",
"webhookType": "CARD_TRANSACTION",
"businessId": "5185740066240791001",
"notificationTime": "2026-08-25T02:30:00Z",
"data": {
"transactionId": "5185740066240791001",
"cardId": "5136759791164443137",
"panLast4": "4821",
"status": "APPROVED",
"direction": "DEBIT",
"originalAmount": "128.50",
"originalCurrency": "HKD",
"postAmount": "16.47",
"postCurrency": "USD",
"transactionTime": "2026-08-25 10:30:00",
"transactionCategory": "SALES",
"merchantName": "STARBUCKS TSIM SHA TSUI HK",
"merchantCountryCode": "HK",
"mcc": "5812"
}
}
```
### `CARD_TRANSACTION_SETTLEMENT`
| Field | Type | Description |
| :- | :- | :- |
| `transactionId` | String | Transaction ID, identical to the authorization event's and the statement line's |
| `cardId` | String | Card ID |
| `panLast4` | String | Last four digits of the card number |
| `direction` | String | `DEBIT` / `CREDIT`. Memo transactions do not push this event |
| `originalAmount` / `originalCurrency` | String | The original transaction amount and currency |
| `postAmount` / `postCurrency` | String | The settled amount and currency |
| `transactionTime` | String | Transaction time in the acquirer's local time |
| `transactionCategory` | String | `RETAIL` / `CASH` / `RETAIL_FEES` / `CASH_FEES` / `PAYMENT` / `DISPUTE_REGISTER` / `DISPUTE_RELEASE` / `RETAIL_INTEREST` / `CASH_ADVANCE_INTEREST` / `CARD_ANNUAL_FEE` / `CARD_PHYSICAL_FEE` / `CARD_REPLACEMENT_FEE` |
| `merchantName` | String | Merchant name |
| `merchantCountryCode` | String | Merchant country, alpha-2 |
| `mcc` | String | Merchant category code |
`transactionCategory` has **three different value domains**: six values on the authorization event (`SALES`, `CASH_ADVANCE`, β¦), twelve on the settlement event (`RETAIL`, `CASH`, β¦) and seven on statement lines (`PURCHASE`, `REFUND`, β¦). Parse them per event type; do not share one enum.
### `CARD_TRANSACTION_DEBT`
Pushed when the card enters a debt state.
| Field | Type | Description |
| :- | :- | :- |
| `transactionId` | String | Transaction ID |
| `cardId` | String | The card the debt belongs to |
| `debtAmount` / `debtCurrency` | String | The debt amount and currency (transaction basis) |
| `postAmount` / `postCurrency` | String | The posted amount and currency (settlement basis) |
| `transactionTime` | String | Transaction time in the acquirer's local time |
| `merchantName` | String | Merchant name |
This payload holds two amounts with different meanings (the debt and the posting), so each carries a qualifier instead of a bare `amount`.
***
## Fund events
### `LOW_BALANCE`
Raised when the funding pool falls below the threshold. Threshold setup is covered in [Company maintenance](../company-maintenance).
| Field | Type | Description |
| :- | :- | :- |
| `companyId` | String | The company that triggered the alert |
| `currency` | String | Funding pool currency: `USD` / `HKD` |
| `availableAmount` | String | Current available balance, paired with `currency` |
| `thresholdAmount` | String | The threshold that triggered the alert |
```json theme={null}
{
"webhookId": "7800000000000001001",
"webhookType": "LOW_BALANCE",
"businessId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"notificationTime": "2026-08-25T02:30:00Z",
"data": {
"companyId": "e7c9a1f0-3b52-4d8e-9a67-1f2b3c4d5e6f",
"currency": "USD",
"availableAmount": "850.25",
"thresholdAmount": "1000"
}
}
```
### `BANK_TRANSFER_INFO`
A VA deposit landed. Both success and failure are pushed, distinguished by `status`.
| Field | Type | Description |
| :- | :- | :- |
| `depositId` | String | The platform's deposit record ID, matching the deposit-records query |
| `companyId` | String | The company that owns the receiving pool |
| `amount` / `currency` | String | Deposit amount and currency: `USD` / `HKD` |
| `payeeAccountNumber` | String | The receiving virtual account number |
| `referenceCode` | String | The bank's reference number |
| `payerName` | String | Payer name |
| `payerAccountNumber` | String | Payer account number |
| `payerBankCode` | String | Payer bank code |
| `status` | String | Posting status: `SUCCESS` / `FAILED` |
| `errorCode` / `errorMessage` | String | Returned only on `FAILED`, always as a pair |
### `BALANCE_CHANGE`
**This event's contract is not finalised and its fields may change at any time.** The current implementation forwards the ledger change log as-is, including internal fields that do not follow the conventions in this documentation. Confirm with the platform before integrating; this page does not define its `data` structure.
Three things remain open: which balance dimensions it covers (company pool balance / free balance / a dedicated-balance card's own balance), how the subject and balance type are identified, and whether every ledger movement is pushed.
### `VA_STATUS_CHANGED`
The result of opening or updating a virtual receiving account (VA). These are accepted asynchronously: `open-va`, `update-va` and `update-va-profile` return `PENDING` synchronously and the final state arrives through this event.
| Field | Type | Description |
| :- | :- | :- |
| `companyId` | String | The company the VA belongs to |
| `subjectType` | String | Funding subject type: `COMPANY` (company pool) / `CARD` (dedicated-balance card) |
| `subjectId` | String | The subject identifier matching `subjectType` |
| `payeeAccountNumber` | String | The virtual account number; may be an empty string while `PENDING` or on failure |
| `currency` | String | VA currency: `USD` / `HKD` |
| `payeeNameMode` | String | Payee-name mode: `DEFAULT` (platform default) / `CUSTOM` |
| `operationType` | String | The operation that triggered this change |
| `status` | String | Result: `SUCCESS` / `FAILURE` |
| `statusDescription` | String | Status description; carries the reason on failure |
## Next steps
* Registering the callback URL, verification and the retry contract: [Webhook configuration](./configuration)
* Receiving and replying to 3DS challenges: [3DS challenges](../3ds-challenges)
* Reconciling through query endpoints: [Statements and transactions](../statements-and-transactions)