# Webhook events

The complete set of events cryptoinfra sends — 9 of them, generated from `docs/openapi.yaml` v1.0.0. Nothing outside this list is ever emitted. See [Webhooks](webhooks) for signature verification, retries and the polling rules.

| Event | Fires when |
| --- | --- |
| [`deposit.credited`](#deposit-credited) | A deposit cleared confirmations and was credited |
| [`withdrawal.confirmed`](#withdrawal-confirmed) | A payout confirmed on-chain |
| [`withdrawal.failed`](#withdrawal-failed) | A payout failed and the hold was refunded |
| [`card.kyc`](#card-kyc) | A card holder's KYC status changed |
| [`card.status`](#card-status) | A card's status changed (also emitted when an issue order settles) |
| [`card.topup`](#card-topup) | A card top-up order reached a terminal state (confirmed or failed) |
| [`card.withdraw`](#card-withdraw) | A card→wallet withdrawal order reached a terminal state (confirmed or failed) |
| [`card.transaction`](#card-transaction) | A card transaction was created |
| [`card.wallet_otp`](#card-wallet-otp) | A live 3DS wallet OTP is available (notification only — the code is NOT in the payload) |

## deposit.credited

**A deposit cleared confirmations and was credited**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: deposit.credited` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

Sent to your registered endpoint. Headers: `x-cryptoinfra-event: deposit.credited`
and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`. Verify the HMAC
over the raw bytes before parsing.

#### Payload

Schema `WebhookDepositCredited`.

| Field | Type | Description |
| --- | --- | --- |
| `depositId` | `integer` | — |
| `userExternalId` | `string` | — |
| `assetId` | `string` | — |
| `chain` | `string` | The chain an address or asset is on. TRON is the only network the platform serves: `btc`, `eth`, `bsc` and `sol` were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. _(one of `tron`)_ |
| `amount` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `fee` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `amountCredited` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `txid` | `string` | — |
| `address` | `string` | — |
| `fromAddress` | `string` | Who paid it. For a token (USDT, USDC), the `from` of the Transfer event that credited the user's address — not the transaction's signer, which for an exchange's batch or proxy payout is an operator address. For TRX, the transaction's sender. `null` only for a deposit recorded before 19 September 2026 whose sender could not be read. |

```json
{
  "depositId": 1,
  "userExternalId": "string",
  "assetId": "string",
  "chain": "tron",
  "amount": "1.5",
  "fee": "1.5",
  "amountCredited": "1.5",
  "txid": "string",
  "address": "string",
  "fromAddress": "string"
}
```

## withdrawal.confirmed

**A payout confirmed on-chain**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: withdrawal.confirmed` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

#### Payload

Schema `WebhookWithdrawalConfirmed`.

| Field | Type | Description |
| --- | --- | --- |
| `withdrawalId` | `integer` | — |
| `userExternalId` | `string` | — |
| `assetId` | `string` | — |
| `chain` | `string` | The chain an address or asset is on. TRON is the only network the platform serves: `btc`, `eth`, `bsc` and `sol` were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. _(one of `tron`)_ |
| `amount` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `fee` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `feeAssetId` | `string` | The asset `fee` is charged in — always equals `assetId` (the option that could make it differ was retired on 2026-09-05; the field stays so an older consumer keeps working). |
| `platformFee` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `toAddress` | `string` | — |
| `txid` | `string | null` | — |

```json
{
  "withdrawalId": 1,
  "userExternalId": "string",
  "assetId": "string",
  "chain": "tron",
  "amount": "1.5",
  "fee": "1.5",
  "feeAssetId": "string",
  "platformFee": "1.5",
  "toAddress": "string",
  "txid": "string"
}
```

## withdrawal.failed

**A payout failed and the hold was refunded**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: withdrawal.failed` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

#### Payload

Schema `WebhookWithdrawalFailed`.

| Field | Type | Description |
| --- | --- | --- |
| `withdrawalId` | `integer` | — |
| `userExternalId` | `string` | — |
| `assetId` | `string` | — |
| `chain` | `string` | The chain an address or asset is on. TRON is the only network the platform serves: `btc`, `eth`, `bsc` and `sol` were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. _(one of `tron`)_ |
| `amount` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `fee` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `feeAssetId` | `string` | The asset `fee` is charged in — always equals `assetId` (see withdrawal.confirmed). |
| `platformFee` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `toAddress` | `string` | — |
| `refunded` | `boolean` | Always true — every hold, fee included, was refunded in full. |
| `error` | `string` | Why. `rejected from the dashboard` when an owner or finance member refused an awaiting_approval send. |

```json
{
  "withdrawalId": 1,
  "userExternalId": "string",
  "assetId": "string",
  "chain": "tron",
  "amount": "1.5",
  "fee": "1.5",
  "feeAssetId": "string",
  "platformFee": "1.5",
  "toAddress": "string",
  "refunded": true,
  "error": "string"
}
```

## card.kyc

**A card holder's KYC status changed**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: card.kyc` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

Signed like every tenant webhook: `x-cryptoinfra-event: card.kyc` and
`x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

#### Payload

Schema `WebhookCardKyc`.

| Field | Type | Description |
| --- | --- | --- |
| `userExternalId` | `string` | — |
| `provider` | `string` | — |
| `kycStatus` | `string` | Legacy issuer-KYC lifecycle on a card holder. Distinct from `KycStatus`, the cryptoinfra-owned verdict at /v1/kyc/*. _(one of `none`, `pending`, `approved`, `rejected`)_ |
| `remark` | `string | null` | — |

```json
{
  "userExternalId": "string",
  "provider": "string",
  "kycStatus": "none",
  "remark": "string"
}
```

## card.status

**A card's status changed (also emitted when an issue order settles)**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: card.status` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

#### Payload

Schema `WebhookCardStatus`.

| Field | Type | Description |
| --- | --- | --- |
| `cardId` | `integer` | — |
| `userExternalId` | `string` | — |
| `provider` | `string` | Present on the generic path only. |
| `status` | `integer | string` | Provider RAW status code (integer) on a card-lifecycle change; a CardOrderStatus string on the issue-order variant. |
| `providerStatus` | `string` | — |
| `reason` | `string` | Optional; present on card status-change events. |
| `refundAmount` | `string` | Informational remaining-balance figure on card close; no ledger entry is posted for it in v1. |
| `orderId` | `integer` | Present only on the issue-order settlement variant. |
| `kind` | `string` | `close_fee` is the close fee charged to the cardholder on DELETE /v1/cards/{id}: `amount` is 0, `fee` is the charge. _(one of `topup`, `withdraw`, `issue_fee`, `close_fee`)_ |
| `assetId` | `string` | Present on the issue-order settlement variant. |
| `amount` | `string` | Present on the issue-order settlement variant. |
| `fee` | `string` | Present on the issue-order settlement variant. |
| `gas` | `string` | Issue-order variant — gas for the on-chain send. |
| `merchantFee` | `string` | Issue-order variant — your own fee. |
| `total` | `string` | Issue-order variant — everything the cardholder was debited; mirror this |
| `error` | `string` | Present on the issue-order settlement variant when it failed. |

```json
{
  "cardId": 1,
  "userExternalId": "string",
  "provider": "string",
  "status": 1,
  "providerStatus": "string",
  "reason": "string",
  "refundAmount": "string",
  "orderId": 1,
  "kind": "topup",
  "assetId": "string",
  "amount": "1.5",
  "fee": "1.5",
  "gas": "1.5",
  "merchantFee": "1.5",
  "total": "1.5",
  "error": "string"
}
```

## card.topup

**A card top-up order reached a terminal state (confirmed or failed)**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: card.topup` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

#### Payload

Schema `WebhookCardOrder`.

| Field | Type | Description |
| --- | --- | --- |
| `orderId` | `integer` | — |
| `kind` | `string` | `close_fee` is the close fee charged to the cardholder on DELETE /v1/cards/{id}: `amount` is 0, `fee` is the charge. _(one of `topup`, `withdraw`, `issue_fee`, `close_fee`)_ |
| `txid` | `string | null` | The hash of the on-chain send behind the order, as on GET /v1/cards/orders/{id}; null when there was none. |
| `userExternalId` | `string` | — |
| `cardId` | `integer | null` | — |
| `assetId` | `string` | — |
| `amount` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `fee` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `gas` | `string` | The network fee actually CHARGED for the on-chain send behind the order — what the cardholder was debited, not the quoted figure. |
| `merchantFee` | `string` | Your own fee on this order, credited to your balance. |
| `total` | `string` | Everything the cardholder was debited — amount + fee + gas + merchantFee. |
| `amountCredited` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `amountCreditedBase` | `string` | Exact integer amount in the asset's smallest unit. _(pattern `^[0-9]+$`)_ |
| `status` | `string` | `funding` = the cardholder has been charged and the money is on its way to the card rail on-chain. `requested` = confirmed on-chain, waiting for the rail to show the credit. `sent` = the rail has been asked, outcome still pending. `confirmed` = settled. `failed` = definitively failed; the WHOLE charge — `amount`, `fee`, `gas` and `merchant_fee`, i.e. `total` — is refunded to the balance it was taken from, the cardholder's, in the same moment (top-up / issue only — a withdraw holds nothing). Nothing goes anywhere else: not to the card, not to your treasury. When the on-chain send behind a load reverted (`provider_status: send_failed`), no coins left the cardholder's address either, so on-chain and ledger agree. `reversed` is reserved for manual operator reversals of a confirmed order. _(one of `funding`, `requested`, `sent`, `confirmed`, `failed`, `reversed`)_ |
| `error` | `string` | — |
| `providerFee` | `string` | — |

```json
{
  "orderId": 1,
  "kind": "topup",
  "txid": "string",
  "userExternalId": "string",
  "cardId": 1,
  "assetId": "string",
  "amount": "1.5",
  "fee": "1.5",
  "gas": "1.5",
  "merchantFee": "1.5",
  "total": "1.5",
  "amountCredited": "1.5",
  "amountCreditedBase": "1500000000",
  "status": "funding",
  "error": "string",
  "providerFee": "string"
}
```

## card.withdraw

**A card→wallet withdrawal order reached a terminal state (confirmed or failed)**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: card.withdraw` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

#### Payload

Schema `WebhookCardOrder`.

| Field | Type | Description |
| --- | --- | --- |
| `orderId` | `integer` | — |
| `kind` | `string` | `close_fee` is the close fee charged to the cardholder on DELETE /v1/cards/{id}: `amount` is 0, `fee` is the charge. _(one of `topup`, `withdraw`, `issue_fee`, `close_fee`)_ |
| `txid` | `string | null` | The hash of the on-chain send behind the order, as on GET /v1/cards/orders/{id}; null when there was none. |
| `userExternalId` | `string` | — |
| `cardId` | `integer | null` | — |
| `assetId` | `string` | — |
| `amount` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `fee` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `gas` | `string` | The network fee actually CHARGED for the on-chain send behind the order — what the cardholder was debited, not the quoted figure. |
| `merchantFee` | `string` | Your own fee on this order, credited to your balance. |
| `total` | `string` | Everything the cardholder was debited — amount + fee + gas + merchantFee. |
| `amountCredited` | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `amountCreditedBase` | `string` | Exact integer amount in the asset's smallest unit. _(pattern `^[0-9]+$`)_ |
| `status` | `string` | `funding` = the cardholder has been charged and the money is on its way to the card rail on-chain. `requested` = confirmed on-chain, waiting for the rail to show the credit. `sent` = the rail has been asked, outcome still pending. `confirmed` = settled. `failed` = definitively failed; the WHOLE charge — `amount`, `fee`, `gas` and `merchant_fee`, i.e. `total` — is refunded to the balance it was taken from, the cardholder's, in the same moment (top-up / issue only — a withdraw holds nothing). Nothing goes anywhere else: not to the card, not to your treasury. When the on-chain send behind a load reverted (`provider_status: send_failed`), no coins left the cardholder's address either, so on-chain and ledger agree. `reversed` is reserved for manual operator reversals of a confirmed order. _(one of `funding`, `requested`, `sent`, `confirmed`, `failed`, `reversed`)_ |
| `error` | `string` | — |
| `providerFee` | `string` | — |

```json
{
  "orderId": 1,
  "kind": "topup",
  "txid": "string",
  "userExternalId": "string",
  "cardId": 1,
  "assetId": "string",
  "amount": "1.5",
  "fee": "1.5",
  "gas": "1.5",
  "merchantFee": "1.5",
  "total": "1.5",
  "amountCredited": "1.5",
  "amountCreditedBase": "1500000000",
  "status": "funding",
  "error": "string",
  "providerFee": "string"
}
```

## card.transaction

**A card transaction was created**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: card.transaction` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

#### Payload

Schema `WebhookCardTransaction`.

| Field | Type | Description |
| --- | --- | --- |
| `cardId` | `integer` | — |
| `userExternalId` | `string` | — |
| `currency` | `string | null` | — |
| `createAt` | `integer | null` | Provider epoch timestamp. |
| `transaction` | `object` | — |
| `transaction.recordNo` | `string | null` | — |
| `transaction.transactionId` | `string | null` | — |
| `transaction.originalTransactionId` | `string | null` | On a refund (171) |
| `transaction.transType` | `integer | null` | 170 payment, 171 refund, 172 card fee, 173 ATM withdrawal, 174 pre-authorization. |
| `transaction.transStatus` | `integer | null` | 1 processing, 2 success, 3 failed. |
| `transaction.merchantName` | `string | null` | — |
| `transaction.merchantCategoryCode` | `string | null` | — |
| `transaction.localCurrency` | `string | null` | — |
| `transaction.localCurrencyAmt` | `string | null` | — |
| `transaction.transCurrency` | `string | null` | — |
| `transaction.transCurrencyAmt` | `string | null` | — |
| `transaction.feeAmount` | `string | null` | — |
| `transaction.feeCurrency` | `string | null` | — |
| `transaction.balance` | `string | null` | — |
| `transaction.respCodeDesc` | `string | null` | — |

```json
{
  "cardId": 1,
  "userExternalId": "string",
  "currency": "string",
  "createAt": 1,
  "transaction": {
    "recordNo": "string",
    "transactionId": "string",
    "originalTransactionId": "string",
    "transType": 1,
    "transStatus": 1,
    "merchantName": "string",
    "merchantCategoryCode": "string",
    "localCurrency": "string",
    "localCurrencyAmt": "string",
    "transCurrency": "string",
    "transCurrencyAmt": "string",
    "feeAmount": "string",
    "feeCurrency": "string",
    "balance": "string",
    "respCodeDesc": "string"
  }
}
```

## card.wallet_otp

**A live 3DS wallet OTP is available (notification only — the code is NOT in the payload)**

Delivered as `POST` to each of your enabled endpoints with `x-cryptoinfra-event: card.wallet_otp` and `x-cryptoinfra-signature: <hex hmac-sha256 of the raw body>`.

#### Payload

Schema `WebhookCardWalletOtp`.

| Field | Type | Description |
| --- | --- | --- |
| `cardId` | `integer` | — |
| `userExternalId` | `string` | — |
| `provider` | `string` | — |
| `providerCardId` | `string` | — |
| `event` | `string` |  _(one of `wallet_otp_available`)_ |
| `name` | `string` | Provider event name. |
| `otpKind` | `string` | Which code it is — branch on this, not on `name`. _(one of `payment_3ds`, `wallet_provisioning`)_ |
| `retrievable` | `boolean` | True while the code can be fetched. |
| `retrievalPath` | `string` | — |
| `expiresInSeconds` | `integer` | — |

```json
{
  "cardId": 1,
  "userExternalId": "string",
  "provider": "string",
  "providerCardId": "string",
  "event": "wallet_otp_available",
  "name": "string",
  "otpKind": "payment_3ds",
  "retrievable": true,
  "retrievalPath": "/v1/cards/102/otp",
  "expiresInSeconds": 600
}
```
