---
title: "Event reference"
description: "Every event type we send, with its payload and an example."
kind: reference
url: /docs/webhooks/events
canonical_url: "https://virtual.ktap.io/docs/webhooks/events"
---

> Part of the KTAP Cards API documentation.
> Index of every page: https://virtual.ktap.io/llms.txt
> Machine-readable schema: https://virtual.ktap.io/api/partner/v1/openapi.json

# Event reference

Every event type we send, with its payload and an example.

Every type we send today. Treat the list as open: ignore a type you do not recognise and return `2xx`.

A purchase produces at least two events: `card.authorized` when it is authorised, then a `card.transaction` for each later leg, whether it clears, reverses or refunds. They all carry the same `transaction_id`. A decline stands alone. See [Receiving events](https://virtual.ktap.io/docs/webhooks.md) for signature verification and retries.

### `card.issued`

A virtual card was created for you and recorded against your account

**Sent when** POST /api/partner/v1/cards succeeds. Emitted after the card exists at the issuer, after the cards row is inserted, and after the first month is billed. It is emitted outside any transaction, so it is queued only once the card genuinely exists.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card id. Use it in every /cards/{id} path |
| `partner_ref` | string, nullable | optional | The partner_ref you sent on the issue request, null when you sent none |
| `last4` | string, nullable | optional | Last four digits of the PAN, null when the issuer did not return them on create |
| `card_holder_name` | string | optional | The name on the card as the issuer stored it: uppercased, at most 26 characters, accents already converted to their ASCII form |

```json
{
  "id": "0f4c2c1e-9a3d-4a67-8b21-5b8e3d1c7a90",
  "type": "card.issued",
  "created_at": "2026-08-22T00:32:35.412Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "user_8812",
    "last4": "4417",
    "card_holder_name": "MARIA GONZALEZ"
  }
}
```

- Arrives once per card. A retry that replays an existing card under the same idempotency key emits nothing.

### `card.topped_up`

Money moved from your float onto one of your cards

**Sent when** POST /api/partner/v1/cards/{id}/topup succeeds. Emitted, after the float ledger has committed and after the issuer confirmed the credit.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card that was credited |
| `partner_ref` | string, nullable | optional | Your reference stored on the card |
| `amount_requested_usd` | number | optional | The amount_usd you asked for, at most 2 decimal places |
| `fee_usd` | number | optional | Always 0 on the float model: the load fee is charged when you fund the float, not when you move money to a card |
| `credited_usd` | number | optional | What landed on the card. Equal to amount_requested_usd while fee_usd is 0 |
| `partner_balance_usd` | number | optional | Your float BALANCE after the debit, not your remaining spend-limit headroom |

```json
{
  "id": "6b1a08d3-2f4c-4d19-9f77-c0a1e2b4d556",
  "type": "card.topped_up",
  "created_at": "2026-08-22T09:14:02.087Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "user_8812",
    "amount_requested_usd": 250,
    "fee_usd": 0,
    "credited_usd": 250,
    "partner_balance_usd": 18750.25
  }
}
```

- A top-up whose processor call fails ambiguously emits nothing, so the absence of this event does not prove the card was not funded.

### `card.authorized`

A purchase was authorized and its amount, fee included, was charged to the card. The merchant can still settle a different amount

**Sent when** The issuer sends a card event with event_type 'authorized' for a card that belongs to you.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card the purchase happened on |
| `partner_ref` | string, nullable | optional | Your reference stored on the card |
| `transaction_id` | string, nullable | optional | The purchase id. Join on it to pair this authorization with its later settlement, and with the transaction_id field (not id) on GET /cards/{id}/transactions rows |
| `event` | string | optional | Always "authorized" on this event type |
| `status` | string | optional | Always "pending" on this event type |
| `is_pending` | boolean | optional | Always true on this event type |
| `delivery_status` | string | optional | The processor's own status on the source event, lowercased. One of pending, success, failed, canceled, time_canceled, manual_check. Kept for support, do not branch on it |
| `merchant` | string, nullable | optional | Merchant name, taken from merchant_name and falling back to merchant_data.name |
| `mcc_code` | string, nullable | optional | Merchant category code, passed through from merchant_data.mcc_code |
| `mcc_category` | string, nullable | optional | Merchant category label, passed through from merchant_data.mcc_category |
| `merchant_country` | string, nullable | optional | Merchant country, passed through from merchant_data.country |
| `amount_usd` | number, nullable | optional | The total charged to the card at authorization, in the billing currency, fee included. The merchant can still settle a different amount |
| `original_amount` | number, nullable | optional | The amount in the currency the cardholder actually paid in |
| `original_currency` | string, nullable | optional | Currency of original_amount, read from transaction_currency_code and falling back to transaction_currency |
| `bill_currency` | string, nullable | optional | The currency the card was debited in, read from bill_currency_code and falling back to bill_currency. This is what amount_usd is denominated in |
| `exchange_rate` | number, nullable | optional | Close to amount_usd divided by original_amount, but not an exact identity, so do not assert it. amount_usd includes the fee, so this rate includes it too and is not a market rate. It can carry more than 6 decimal places |
| `fee_usd` | number | optional | The fee inside amount_usd, at full precision. This is the purchase's fee and the settlement event does not repeat it, so keep it. Six decimals appear here in practice |
| `fee_breakdown` | object | optional | { "fx_usd": number, "atm_usd": number }, the two components of fee_usd |
| `decline_reason` | object, nullable | optional | Always null on this event type |
| `occurred_at` | string, nullable | optional | When the purchase happened, UTC with a Z suffix, from the source event's created_at and falling back to datetime. Distinct from the envelope created_at, which is when the event entered our outbox |

```json
{
  "id": "c74e1b6a-3d55-4b8f-9a2e-1c6d0f5a8b41",
  "type": "card.authorized",
  "created_at": "2026-08-22T14:07:54.118Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "user_8812",
    "transaction_id": "tid_01a09a51-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
    "event": "authorized",
    "status": "pending",
    "is_pending": true,
    "delivery_status": "success",
    "merchant": "CORNER CAFE",
    "mcc_code": "5814",
    "mcc_category": "Fast Food Restaurants",
    "merchant_country": "USA",
    "amount_usd": 25.5,
    "original_amount": 25,
    "original_currency": "USD",
    "bill_currency": "USD",
    "exchange_rate": 1.02,
    "fee_usd": 0.5,
    "fee_breakdown": { "fx_usd": 0.5, "atm_usd": 0 },
    "decline_reason": null,
    "occurred_at": "2026-08-22T14:07:51.906Z"
  }
}
```

- This is the first event for one purchase. Every later leg arrives as `card.transaction`, and they all carry the same `transaction_id`.
- Settlement is slow: the median gap to the settled event is around 22 hours. Show the purchase as pending with this event's `amount_usd` and `fee_usd` straight away. When a settlement arrives with the same `original_amount`, keep those figures as the charge. If a settlement arrives first, do not book its figures: wait for this event.

### `card.transaction`

A purchase settled, or a refund or reversal posted. A settlement does not repeat the fee taken at authorization, so its amount is not the total charged

**Sent when** The issuer sends a card event with event_type 'cleared', 'refund' or 'reversal' for one of your cards.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card the movement happened on |
| `partner_ref` | string, nullable | optional | Your reference stored on the card |
| `transaction_id` | string, nullable | optional | The purchase id: the same value as on its card.authorized, and as the transaction_id field (not id) on GET /cards/{id}/transactions rows. Resolved by matching the event timestamp against purchases from the previous 45 days, so a settlement that clears days after the hold still pairs. Null only when that lookup fails or more than one purchase matched: we send the event without an id rather than guess and merge two purchases |
| `event` | string | optional | Which movement this is: "cleared", "refund" or "reversal". Branch on this, not on the event type |
| `status` | string | optional | Always "settled" on this event type |
| `is_pending` | boolean | optional | Always false on this event type |
| `delivery_status` | string | optional | The processor's own status on the source event, lowercased. One of pending, success, failed, canceled, time_canceled, manual_check |
| `merchant` | string, nullable | optional | Merchant name, from merchant_name falling back to merchant_data.name |
| `mcc_code` | string, nullable | optional | Merchant category code from merchant_data.mcc_code |
| `mcc_category` | string, nullable | optional | Merchant category label from merchant_data.mcc_category |
| `merchant_country` | string, nullable | optional | Merchant country from merchant_data.country |
| `amount_usd` | number, nullable | optional | On a settlement (event cleared): the amount the merchant settled, in the billing currency, without the fee taken at authorization, so on a purchase that settled what it authorized it sits below the authorization's amount_usd. On a refund or reversal: the amount released back. Branch on event for the direction |
| `original_amount` | number, nullable | optional | The amount in the currency the cardholder paid in |
| `original_currency` | string, nullable | optional | Currency of original_amount |
| `bill_currency` | string, nullable | optional | The currency the card was debited in |
| `exchange_rate` | number, nullable | optional | Rate between original_amount and amount_usd on this event. It usually differs from the authorization's rate, because the authorization's amount_usd includes the fee and this one does not |
| `fee_usd` | number | optional | Not the purchase's fee. The fee is taken at authorization and reported on card.authorized, so on a settlement this is normally 0. Keep the authorization's fee_usd rather than replacing it with this |
| `fee_breakdown` | object | optional | { "fx_usd": number, "atm_usd": number } |
| `decline_reason` | object, nullable | optional | Always null on this event type |
| `occurred_at` | string, nullable | optional | When the movement happened, UTC with a Z suffix |

```json
{
  "id": "e1907c22-4a6b-49d8-b0f3-5d2c8e7a1140",
  "type": "card.transaction",
  "created_at": "2026-08-23T12:41:19.006Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "user_8812",
    "transaction_id": "tid_01a09a51-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
    "event": "cleared",
    "status": "settled",
    "is_pending": false,
    "delivery_status": "success",
    "merchant": "CORNER CAFE",
    "mcc_code": "5814",
    "mcc_category": "Fast Food Restaurants",
    "merchant_country": "USA",
    "amount_usd": 25,
    "original_amount": 25,
    "original_currency": "USD",
    "bill_currency": "USD",
    "exchange_rate": 1,
    "fee_usd": 0,
    "fee_breakdown": { "fx_usd": 0, "atm_usd": 0 },
    "decline_reason": null,
    "occurred_at": "2026-08-23T12:41:17.882Z"
  }
}
```

- This is the second event for a purchase that already arrived as `card.authorized`. Pair them on `transaction_id` and tell them apart by `status`.
- A refund or reversal is its own `card.transaction` sharing the original `transaction_id`, so one transaction can produce three or more events. Key your ledger on the event `id` for deduplication and on `transaction_id` for grouping.
- A settlement does not repeat the fee taken at authorization: its `fee_usd` is normally `0`. Keep the authorization's `amount_usd` and `fee_usd` as the charge while `original_amount` is unchanged. If it changed, treat the charge as open and reconcile it with us by `transaction_id`.

### `card.declined`

A purchase was declined at the point of sale

**Sent when** The issuer sends a card event with event_type 'declined' for one of your cards.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card the attempt was made on |
| `partner_ref` | string, nullable | optional | Your reference stored on the card |
| `transaction_id` | string, nullable | optional | The declined attempt's purchase id, resolved the same way as a settlement and null only when that lookup fails. A decline never pairs with another event, so use it to find the attempt on GET /cards/{id}/transactions (its transaction_id field) |
| `event` | string | optional | Always "declined" on this event type |
| `status` | string | optional | Always "settled". A decline is final but moved no money, so do not read "settled" here as "money moved" |
| `is_pending` | boolean | optional | Always false on this event type |
| `delivery_status` | string | optional | The processor's own status on the source event, lowercased |
| `merchant` | string, nullable | optional | Merchant name |
| `mcc_code` | string, nullable | optional | Merchant category code. Use it with mcc_category to recognise a blocked or high-risk category |
| `mcc_category` | string, nullable | optional | Merchant category label |
| `merchant_country` | string, nullable | optional | Merchant country |
| `amount_usd` | number, nullable | optional | The amount that was attempted, in the billing currency. Nothing was charged |
| `original_amount` | number, nullable | optional | The attempted amount in the cardholder's currency |
| `original_currency` | string, nullable | optional | Currency of original_amount |
| `bill_currency` | string, nullable | optional | The currency the card would have been debited in |
| `exchange_rate` | number, nullable | optional | Rate that would have applied |
| `fee_usd` | number | optional | Always 0 on a decline, forced to zero so this event and GET /cards/{id}/transactions cannot disagree |
| `fee_breakdown` | object | optional | { "fx_usd": 0, "atm_usd": 0 } on a decline |
| `decline_reason` | object, nullable | optional | The reason object: { "code": string, "message": string, "retryable": boolean, "card_blocked": boolean }. code is one of a fixed set we control: insufficient_card_balance, issuer_funds_unavailable, daily_limit_exceeded, weekly_limit_exceeded, monthly_limit_exceeded, single_purchase_limit_exceeded, velocity_limit_exceeded, card_blocked, fraud_risk_declined, security_check_failed, merchant_category_blocked, processing_error, authorization_timeout, unknown. An unrecognised upstream code maps to unknown. Branch on code; message is display text we author |
| `occurred_at` | string, nullable | optional | When the attempt happened, UTC with a Z suffix |

```json
{
  "id": "3c8d5f10-b7a2-4e69-8c11-9f0e2d4a6b73",
  "type": "card.declined",
  "created_at": "2026-08-22T18:22:41.503Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "user_8812",
    "transaction_id": "tid_01a0b3e1-77c2-7d04-8e5f-6a1b2c3d4e5f",
    "event": "declined",
    "status": "settled",
    "is_pending": false,
    "delivery_status": "success",
    "merchant": "STEAMGAMES.COM",
    "mcc_code": "5816",
    "mcc_category": "Digital Goods: Games",
    "merchant_country": "LU",
    "amount_usd": 59.99,
    "original_amount": 59.99,
    "original_currency": "USD",
    "bill_currency": "USD",
    "exchange_rate": 1,
    "fee_usd": 0,
    "fee_breakdown": { "fx_usd": 0, "atm_usd": 0 },
    "decline_reason": {
      "code": "insufficient_card_balance",
      "message": "The card balance did not cover the purchase.",
      "retryable": false,
      "card_blocked": false
    },
    "occurred_at": "2026-08-22T18:22:40.115Z"
  }
}
```

- A decline stands alone. It never pairs with a `card.authorized` or a `card.transaction`.
- `decline_reason.card_blocked` means the card is unusable until we unblock it. Stop retrying and contact us.

### `card.3ds_challenge`

A 3DS one-time code for one of your cards, for you to deliver to the cardholder

**Sent when** The issuer sends a card event with event_type 'notification_3ds_authorization' for one of your cards. Every card delivers its 3DS codes this way: none are sent by text message.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card being challenged |
| `partner_ref` | string, nullable | optional | Your reference stored on the card, so you can route the code to the right end user without a lookup |
| `code` | string, nullable | optional | The challenge's identifier. It is not a code for the cardholder to type: send it back with POST /cards/{cardId}/3ds/respond and approve true or false. Null when the source event carried no code |
| `auto_approved` | boolean | optional | True when we already approved this challenge because auto-approve is on for your account. Nothing more to do. False means the purchase waits for your answer |
| `merchant` | string, nullable | optional | Merchant requesting the challenge, from merchant_name |
| `merchant_country` | string, nullable | optional | Merchant country, from merchant_country_code |
| `amount` | number, nullable | optional | The transaction amount being challenged, from transaction_amount. Not converted to USD |
| `currency` | string, nullable | optional | Currency of amount, from transaction_currency |
| `occurred_at` | string, nullable | optional | When the challenge was raised, from transaction_timestamp |

```json
{
  "id": "7d0b3a99-1e42-4c07-a5f8-6b3d9c2e0417",
  "type": "card.3ds_challenge",
  "created_at": "2026-08-22T15:03:11.271Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "user_8812",
    "code": "481902",
    "merchant": "BOOKING.COM",
    "merchant_country": "NL",
    "amount": 412.5,
    "currency": "EUR",
    "occurred_at": "2026-08-22T15:03:10.804Z"
  }
}
```

- Time critical. Acknowledge it immediately and deliver the code to your cardholder: a failed first attempt costs at least a minute of backoff, by which point the challenge has usually expired.
- Sent for every card: 3DS codes are delivered by webhook only, never by text message. Keep your webhook registered and fast.

### `card.closed`

A card was closed and any balance left on it returned to your float

**Sent when** POST /api/partner/v1/cards/{id}/close succeeds. Emitted, after the sweep, after the issuer deleted the card, and after the local cards row is marked DELETED.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card that was closed |
| `partner_ref` | string, nullable | optional | Your reference stored on the card |
| `returned_usd` | number | optional | The balance swept off the card back to your float. 0 when the card was empty. On a retry of a partly completed close this reports the amount recorded by the original sweep, not the card's current zero balance |

```json
{
  "id": "a2d9f7b1-55c8-4e02-8a3b-71f4c9e0d233",
  "type": "card.closed",
  "created_at": "2026-08-22T11:40:55.900Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "user_8812",
    "returned_usd": 12.34
  }
}
```

- Closing does not return card allowance. Your lifetime issuance count still includes the closed card.

### `deposit.credited`

A crypto payment matched one of your deposit quotes and the card was credited (quote-matched variant)

**Sent when** The watcher confirms an on-chain transfer that matches a pending partner_deposits row on asset, exact amount and destination address. Emitted, after the issuer confirmed the card credit. Available only to partners with deposits_enabled true. It is also sent when the watcher sees a confirmed transfer to an address derived for a specific card, on EVM chains, BTC or TRON. Emitted, after the issuer confirmed the credit. Available only to partners with deposits_enabled true; a transfer to a float-only partner's card is held for manual handling and emits nothing.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `deposit_id` | string | optional | The deposit quote this payment settled. Present only on this variant |
| `card_id` | string | optional | The card that was credited |
| `credited_usd` | number | optional | USD placed on the card, net of fees, rounded to cents |
| `tx_hash` | string | optional | The on-chain transaction hash that funded it |
| `partner_ref` | string, nullable | optional | Card-address variant only. Your id for the cardholder, as set at issuance |
| `asset` | string | optional | Card-address variant only. The asset code the transfer arrived in |
| `amount_received` | number | optional | Card-address variant only. The on-chain amount received, in crypto units of asset, not USD |
| `fee_usd` | number | optional | Card-address variant only. Everything deducted before the credit. It can exceed ktap_fee_usd plus partner_fee_usd because it absorbs the sub-cent truncation remainder |
| `ktap_fee_usd` | number | optional | Card-address variant only. Our share of the fee |
| `partner_fee_usd` | number | optional | Card-address variant only. Your markup. Accrues to your commission balance |

```json
{
  "id": "b58c1d7e-90a3-4f21-b6c4-2e7f8a3d0159",
  "type": "deposit.credited",
  "created_at": "2026-08-22T20:11:44.630Z",
  "data": {
    "deposit_id": "41f6d2c8-7b09-4a35-9e18-c3d5b0a7f264",
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "credited_usd": 98.4,
    "tx_hash": "0x9c1f3a7d4e2b6058ab77c0d9e3f1425a6b8c0d2e4f6a8b1c3d5e7f9012345678"
  }
}
```

- Two payload shapes ship under this type. Tell them apart by presence: `deposit_id` marks one, `partner_ref` with `asset` marks the other. Read every field defensively.

### `float.low`

Your spendable float fell below the low-balance threshold

**Sent when** The watcher's partner float check runs on every tick. It emits this when available_usd falls below the threshold, once per drop.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `available_usd` | number | optional | What you can actually spend: the lower of your balance and your remaining spend limit. The same figure GET /me reports as float.available_usd. This is the number that gates a top-up |
| `balance_usd` | number | optional | Your raw float balance, ignoring any spend limit. It can be far higher than available_usd when a limit binds |
| `threshold_usd` | number | optional | The threshold that fired, $500 by default |
| `message` | string | optional | Fixed display text: "Your balance is running low. Send funds to avoid card top-ups being declined." |

```json
{
  "id": "5a1e8c73-2b90-4f16-8d47-e0c9b3f5a218",
  "type": "float.low",
  "created_at": "2026-08-22T07:00:19.442Z",
  "data": {
    "available_usd": 412.8,
    "balance_usd": 50412.8,
    "threshold_usd": 500,
    "message": "Your balance is running low. Send funds to avoid card top-ups being declined."
  }
}
```

- About your account, not a card. It carries no `card_id` and no `partner_ref`.
- Sent once when your available balance (`available_usd`) falls below the threshold, not repeated while it stays there. It comes again, with a new event id, only after your available balance has recovered to a fifth above the threshold and fallen below it again.

### `float.credited`

A transaction hash you submitted with POST /funding was credited to your balance

**Sent when** A submission to POST /api/partner/v1/funding becomes credited: the transaction was verified on-chain, the funds arrived (on BNB Chain and Base, once the transaction is final on its network), and the funding and its load fee were written to your ledger. The ledger rows, the credited status and this event are recorded together, so none exists without the others. Also when we credit a submission after review. Sent once per submission. Not sent for a submission credited separately, nor for a reversal.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `funding_id` | string | optional | The id POST /funding returned for this submission |
| `network` | string | optional | The network the transaction was made on One of: `POLYGON`, `BNB`, `BASE`. |
| `tx_hash` | string | optional | The transaction hash, lowercased |
| `token` | string | optional | The stablecoin sent One of: `USDC`, `USDT`. |
| `amount_usd` | number | optional | The amount credited before the load fee: normally the amount sent, what the transaction paid into your funding address; after a review, if less reached us than was sent, no more than reached us. It can therefore be lower than amount_usd on GET /funding/{fundingId}, which is always the amount sent |
| `fee_usd` | number | optional | The load fee on your agreement, charged on amount_usd |
| `credited_usd` | number | optional | amount_usd minus fee_usd: what reached your balance |
| `balance_usd` | number | optional | Your balance straight after this credit |

```json
{
  "id": "6f3b8d2a-9c14-4e70-b5a8-1d7e3c9f2b46",
  "type": "float.credited",
  "created_at": "2026-09-23T10:17:40.618Z",
  "data": {
    "funding_id": "3b9e6f1a-7c42-4d85-a1e0-5f2c8d9b4e63",
    "network": "POLYGON",
    "tx_hash": "0x4a7e2c9d1b8f3e6a5c0d7b2e9f4a1c8d3e6b0f5a2d9c7e4b1a8f6d3c0e5b2a91",
    "token": "USDC",
    "amount_usd": 2500.25,
    "fee_usd": 25.0025,
    "credited_usd": 2475.2475,
    "balance_usd": 12475.2475
  }
}
```

- Sent once per submission, when it becomes `credited`. `funding_id` is the `id` that [POST /funding](/docs/api/account#submitFunding) returned. A submission we close as `credited` with reason `credited_separately` after a review sends none: those funds were already credited to your balance by other means. A reversal sends none either. Its `amount_usd` is the amount credited before the load fee: normally the amount sent, but after a review no more than reached us, so it can be below `amount_usd` on [GET /funding/{fundingId}](/docs/api/account#getFunding).
- About your account, not a card. It carries no `card_id` and no `partner_ref`.
- `balance_usd` is your balance straight after the credit. A top-up is checked against `float.available_usd` on [GET /me](/docs/api/account#getMe), which a limit can hold below it.

### `card.unloaded`

Money moved off one of your cards back into your balance

**Sent when** POST /api/partner/v1/cards/{id}/unload succeeds. Emitted (unloadPartnerCard), after the processor subtracted from the card and the balance credit committed.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card that was unloaded |
| `partner_ref` | string, nullable | optional | Your reference stored on the card |
| `amount_usd` | number | optional | What came off the card and returned to your balance |
| `partner_balance_usd` | number | optional | Your balance after the credit |

```json
{
  "id": "8c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
  "type": "card.unloaded",
  "created_at": "2026-08-24T10:15:00.000Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "your-user-8842",
    "amount_usd": 50,
    "partner_balance_usd": 9995
  }
}
```

- Money left the card and returned to your balance. Pairs with a `POST /cards/{id}/unload` you made; there is no processor-initiated unload.

### `card.status_changed`

A card's status changed, whether you changed it or we did

**Sent when** The card processor blocks a card on velocity rules, or you freeze or unfreeze one via POST /cards/{id}/freeze. All status changes arrive on this one event so a consumer maintains card state from a single stream.

| Field | Type |  | Description |
| --- | --- | --- | --- |
| `card_id` | string | optional | The card whose status changed |
| `partner_ref` | string, nullable | optional | Your reference stored on the card |
| `status` | string | optional | The status the card is now in One of: `ACTIVE`, `FROZEN`, `BLOCKED`. |
| `reason` | string | optional | Why it changed. velocity means the processor blocked it after repeated limit breaches; partner_freeze and partner_unfreeze are your own freeze calls, mirrored here so your webhook consumer sees them without coordinating with the caller One of: `velocity`, `partner_freeze`, `partner_unfreeze`. |
| `action_required` | string, nullable | optional | contact_support when only we can restore the card, as with a velocity block. null when no action is needed |
| `occurred_at` | string, nullable | optional | When the change happened, UTC |

```json
{
  "id": "3e2a1b0c-9d8e-7f6a-5b4c-3d2e1f0a9b8c",
  "type": "card.status_changed",
  "created_at": "2026-08-24T10:20:00.000Z",
  "data": {
    "card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
    "partner_ref": "your-user-8842",
    "status": "BLOCKED",
    "reason": "velocity",
    "action_required": "contact_support",
    "occurred_at": "2026-08-24T10:19:58.412Z"
  }
}
```

- One stream for every status change, whether you made it (freeze, unfreeze) or we did (velocity block). `reason` tells you which.
- A `BLOCKED` card cannot be restored through the API. `action_required: "contact_support"` means exactly that.

Something arriving that is not described here, or an event you expected and did not get: email [dev@ktap.io](mailto:dev@ktap.io) with the delivery id and the time in UTC.
