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

# Cartevo Webhooks

> Documentation of Cartevo webhook events and their payloads.

## Overview

Cartevo webhooks notify your system in near real‑time about account events (card creation, funding, withdrawals, errors, etc.).

* **Method**: POST
* **Format**: JSON
* **Headers**: `Content-Type: application/json`
* **Expected response**: any HTTP 2xx is treated as success (response body is ignored)

Your webhook URL is configured on your company account. For every event, Cartevo sends an HTTP request with the following shape:

```json theme={null}
{
  "event": "<event_name>",
  "data": {
    /* event-specific payload */
  }
}
```

### Webhook Headers

```
Content-Type: application/json
X-Cartevo-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
```

* `Content-Type` is always `application/json`.
* `X-Cartevo-Signature` is present **only when webhook signing is enabled** for your company (a signing secret is configured). When signing is disabled, no signature header is sent.
* No other custom headers (e.g. `User-Agent`, `X-Webhook-Id`, `X-Webhook-Timestamp`) are guaranteed — do not depend on them.

## Retry policy

If delivery fails (non‑2xx response or network error), Cartevo automatically retries. There is **1 initial attempt + 3 retries = 4 total deliveries**, spaced **5 seconds** apart.

| Attempt     | Delay | Elapsed |
| ----------- | ----- | ------- |
| 1 (initial) | 0s    | 0s      |
| 2 (retry 1) | 5s    | 5s      |
| 3 (retry 2) | 5s    | 10s     |
| 4 (retry 3) | 5s    | 15s     |

* **Max retries:** 3 (4 total attempts including the initial delivery)
* **Timeout:** 10 seconds per attempt
* **Logging:** every attempt is logged; after the final retry fails, delivery is marked permanently failed.

## Verifying webhook signatures

When webhook signing is enabled for your company, every delivery carries an `X-Cartevo-Signature` header so you can confirm the request genuinely came from Cartevo and was not tampered with.

**Header format:**

```
X-Cartevo-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
```

| Part | Description                                               |
| ---- | --------------------------------------------------------- |
| `t`  | Unix timestamp (seconds) when the signature was generated |
| `v1` | Lowercase hex-encoded HMAC-SHA256 signature               |

**How the signature is computed:**

1. Take the **raw request body** exactly as received — do not re-serialize the JSON.
2. Build the signed string `"{t}.{rawBody}"` — the timestamp, a literal dot (`.`), then the raw body.
3. Compute `HMAC-SHA256(signed_string, your_signing_secret)` and hex-encode it. That value is `v1`.

Your signing secret is a 64-character hex string available in your company webhook settings. The signature is generated **once** when the event is enqueued and replayed verbatim on retries, so the same `t`/`v1` may arrive on more than one delivery attempt.

**Verification (Node.js):**

```javascript theme={null}
import crypto from "crypto";

function verifyCartevoSignature(rawBody, header, secret, toleranceSeconds = 300) {
  // header example: "t=1736337600,v1=9a1c..."
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=").map((s) => s.trim()))
  );
  const { t, v1 } = parts;
  if (!t || !v1) return false;

  // Optional: reject stale timestamps to mitigate replay attacks
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  try {
    return crypto.timingSafeEqual(
      Buffer.from(v1, "hex"),
      Buffer.from(expected, "hex")
    );
  } catch {
    return false;
  }
}
```

<Warning>
  Always verify against the **raw, unparsed body**. If your framework parses JSON before you can read the raw bytes, configure it to expose the raw body (e.g. Express: `express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })`). Re-serializing the parsed object changes the bytes and the signature will not match.
</Warning>

## Field naming convention

All webhook payload keys use **`snake_case`** (e.g. `transaction_id`, `card_id`, `created_at`). A small number of legacy events still emit a few `camelCase` keys (e.g. `cardId`, `transactionId`); these are flagged inline in each event's field table and will be migrated to `snake_case` in a future release. Always consult the field table for the canonical key spelling per event.

🚨 Tip: treat webhooks as **at‑least‑once** deliveries. Implement **idempotency/deduplication** on your receiver using stable business identifiers (e.g., `transaction_id`, `card.id`, `wallet_id`).

## Setting Up Webhooks

### Configure Your Webhook URL

Set your webhook URL in your company settings:

## Supported Events

### Payment Events

#### payment.collect

Emitted when a payment collection (pay‑in) is initiated. Note: this fires when collection begins; the eventual outcome (`SUCCESS` / `FAILED`) reaches you via the polled `GET /payment/transactions/{id}/status` endpoint or via your `notify_url` if one was supplied.

```json theme={null}
{
  "event": "payment.collect",
  "data": {
    "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
    "external_id": "your-idempotency-key-001",
    "status": "PENDING",
    "amount": 1000,
    "currency": "XAF",
    "country": "CM",
    "operator": "mtn",
    "phone_number": "237600000000",
    "initiated_at": "2025-01-08T12:00:00.000Z"
  }
}
```

**Fields:**

| Field            | Type   | Description                                                       |
| ---------------- | ------ | ----------------------------------------------------------------- |
| `transaction_id` | string | Cartevo-side transaction identifier (UUID)                        |
| `external_id`    | string | The idempotency key you supplied at collection initiation, if any |
| `status`         | string | One of `PENDING`, `PROCESSING`, `SUCCESS`, `FAILED`               |
| `amount`         | number | Amount in `currency` units                                        |
| `currency`       | string | ISO 4217 currency code (e.g. `XAF`, `XOF`, `CDF`, `USD`)          |
| `country`        | string | ISO 3166-1 alpha-2 country code                                   |
| `operator`       | string | Mobile-money operator code (e.g. `mtn`, `orange`, `moov`)         |
| `phone_number`   | string | The end-user phone number being charged                           |
| `initiated_at`   | string | ISO 8601 timestamp when the collection was initiated (UTC)        |

### Card Events

#### card.created

Emitted when a new card is successfully created for a customer.

**Payload:**

```json theme={null}
{
  "event": "card.created",
  "data": {
    "card": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "customer_id": "8d4f2a1b-5c3e-4d7f-9a6b-2e1c8f9d0a3b",
      "brand": "VISA",
      "masked_number": "4111********1111",
      "balance": 100.0,
      "currency": "USD"
    },
    "card_issuance": {
      "status": "SUCCESS",
      "transaction_id": "txn_3f8a9b2c4d6e7f8a9b0c1d2e",
      "amount": 2.0,
      "description": "Card issuance fee"
    },
    "card_fund": {
      "status": "SUCCESS",
      "transaction_id": "txn_5b7c9d2e4f6a8b1c3d5e7f9a",
      "amount": 100.0,
      "description": "Initial card funding"
    }
  }
}
```

**`card` fields:**

| Field           | Type   | Description                                                |
| --------------- | ------ | ---------------------------------------------------------- |
| `id`            | string | Unique card identifier (UUID)                              |
| `customer_id`   | string | Customer who owns the card (UUID)                          |
| `brand`         | string | Card brand. One of `VISA`, `MASTERCARD`                    |
| `masked_number` | string | Card number with all but the last 4 digits replaced by `*` |
| `balance`       | number | Card balance in `currency` units                           |
| `currency`      | string | Card currency (always `USD`)                               |

**`card_issuance` fields:**

| Field            | Type   | Description                                            |
| ---------------- | ------ | ------------------------------------------------------ |
| `status`         | string | Issuance status. One of `SUCCESS`, `FAILED`, `PENDING` |
| `transaction_id` | string | Transaction ID for the issuance fee                    |
| `amount`         | number | Issuance fee in USD                                    |
| `description`    | string | Human-readable description of the issuance transaction |

**`card_fund` fields:**

| Field            | Type   | Description                                           |
| ---------------- | ------ | ----------------------------------------------------- |
| `status`         | string | Funding status. One of `SUCCESS`, `FAILED`, `PENDING` |
| `transaction_id` | string | Transaction ID for the initial card funding           |
| `amount`         | number | Initial funding amount in USD                         |
| `description`    | string | Human-readable description of the funding transaction |

#### card.fund

Emitted when a card is funded with money from a wallet.

**Payload:**

```json theme={null}
{
  "event": "card.fund",
  "data": {
    "transaction_id": "txn_ghi789klm345",
    "card_id": "card_abc123xyz789",
    "amount": 50.0,
    "currency": "USD",
    "fee_amount": 0.0
  }
}
```

**Fields:**

| Field            | Type   | Description                         |
| ---------------- | ------ | ----------------------------------- |
| `transaction_id` | string | Transaction reference               |
| `card_id`        | string | Card being funded                   |
| `amount`         | number | Amount added to card                |
| `currency`       | string | Transaction currency                |
| `fee_amount`     | number | Fee charged for funding (usually 0) |

#### card.withdraw

Emitted when money is withdrawn from a card back to the wallet.

**Payload:**

```json theme={null}
{
  "event": "card.withdraw",
  "data": {
    "transaction_id": "txn_nop678qrs234",
    "card_id": "card_abc123xyz789",
    "amount": 30.0,
    "currency": "USD",
    "fee_amount": 0.0
  }
}
```

**Fields:**

| Field            | Type   | Description                            |
| ---------------- | ------ | -------------------------------------- |
| `transaction_id` | string | Transaction reference                  |
| `card_id`        | string | Card being withdrawn from              |
| `amount`         | number | Amount withdrawn                       |
| `currency`       | string | Transaction currency                   |
| `fee_amount`     | number | Fee charged for withdrawal (usually 0) |

#### card.withdraw\.failed

Emitted when a card withdrawal attempt fails.

**Payload:**

```json theme={null}
{
  "event": "card.withdraw.failed",
  "data": {
    "transaction_id": "txn_tuv901wxy567",
    "card_id": "card_abc123xyz789",
    "amount": 200.0,
    "error": "Insufficient balance or provider error"
  }
}
```

**Fields:**

| Field            | Type   | Description                             |
| ---------------- | ------ | --------------------------------------- |
| `transaction_id` | string | Failed transaction reference            |
| `card_id`        | string | Card attempted to withdraw from         |
| `amount`         | number | Amount that failed to withdraw          |
| `error`          | string | Error message describing failure reason |

#### card.terminated

Emitted when a card is permanently terminated and any remaining balance is refunded to the company's USD wallet.

**Payload:**

```json theme={null}
{
  "event": "card.terminated",
  "data": {
    "cardId": "550e8400-e29b-41d4-a716-446655440000",
    "cardNumber": "****1234",
    "status": "TERMINATED",
    "terminatedAt": "2025-01-08T12:00:00.000Z",
    "refundAmount": 50.0,
    "refundProcessed": true,
    "reason": "Card terminated"
  }
}
```

**Fields:**

| Field             | Type    | Description                                                                 |
| ----------------- | ------- | --------------------------------------------------------------------------- |
| `cardId`          | string  | Unique identifier of the terminated card (UUID)                             |
| `cardNumber`      | string  | Masked card number showing only the last 4 digits (e.g., `****1234`)        |
| `status`          | string  | Always `"TERMINATED"` for this event                                        |
| `terminatedAt`    | string  | ISO 8601 timestamp when the card was terminated                             |
| `refundAmount`    | number  | Amount refunded to the company's USD wallet (0 if card had no balance)      |
| `refundProcessed` | boolean | Indicates whether the refund was successfully processed (`true` or `false`) |
| `reason`          | string  | Reason for termination (typically `"Card terminated"`)                      |

**Important Notes:**

* When a card is terminated, any remaining balance is automatically refunded to the company's USD wallet
* The card balance is set to zero and the card becomes unusable
* This action is **irreversible** - once terminated, a card cannot be reactivated
* Webhook failures do not prevent card termination from completing
* A termination may be **customer/company-initiated** or **provider-initiated** (e.g. a fraud/risk block). For provider-initiated terminations the payload is lighter and carries `reason: "provider_terminated"` and `previousBalance` instead of the refund fields:

```json theme={null}
{
  "event": "card.terminated",
  "data": {
    "cardId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "TERMINATED",
    "reason": "provider_terminated",
    "previousBalance": 1.12
  }
}
```

#### card.verification.completed

Emitted when a card account is successfully verified by a merchant — a \$0.00 verification hold some merchants place when a card is added (e.g. to a wallet or subscription).

**Payload:**

```json theme={null}
{
  "event": "card.verification.completed",
  "data": {
    "cardId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "VERIFIED",
    "message": "Card account verified",
    "merchant": {
      "name": "Google TEMPORARY HOLD"
    },
    "reference": "eebade9a-2437-4ecb-8dc6-c75ed1ba7eac"
  }
}
```

**Fields:**

| Field       | Type   | Description                                                            |
| ----------- | ------ | ---------------------------------------------------------------------- |
| `cardId`    | string | Verified card identifier (UUID, **camelCase — legacy**)                |
| `status`    | string | Always `"VERIFIED"` for this event                                     |
| `message`   | string | Verification message from the network (e.g. `"Card account verified"`) |
| `merchant`  | object | Merchant that performed the verification (`name`), or `null`           |
| `reference` | string | Provider event reference for this verification                         |

### Transaction Events

#### transaction.settlement.completed

Emitted when a transaction is settled/cleared.

```json theme={null}
{
  "event": "transaction.settlement.completed",
  "data": {
    "transactionId": "txn_abc123",
    "cardId": "card_xyz789",
    "amount": 25.0,
    "currency": "USD",
    "type": "SETTLEMENT",
    "status": "SUCCESS",
    "createdAt": "2025-01-06T13:00:00Z",
    "merchant": {
      "name": "Merchant Ltd",
      "country": "US",
      "city": "Seattle"
    }
  }
}
```

#### transaction.authorization.created

Emitted when a card payment authorization succeeds.

**Payload:**

```json theme={null}
{
  "event": "transaction.authorization.created",
  "data": {
    "transactionId": "txn_abc123",
    "cardId": "card_xyz789",
    "amount": 25.0,
    "currency": "USD",
    "type": "AUTHORIZATION",
    "status": "SUCCESS",
    "createdAt": "2025-01-06T13:00:00Z",
    "merchant": {
      "name": "Amazon.com",
      "country": "US",
      "city": "Seattle"
    }
  }
}
```

**Fields:**

| Field               | Type   | Description                                |
| ------------------- | ------ | ------------------------------------------ |
| `transactionId`     | string | Transaction identifier                     |
| `cardId`            | string | Card used for transaction                  |
| `amount`            | number | Transaction amount                         |
| `currency`          | string | Transaction currency                       |
| `type`              | string | Transaction type (`AUTHORIZATION`)         |
| `status`            | string | Transaction status (`SUCCESS`)             |
| `createdAt`         | string | ISO 8601 timestamp                         |
| `merchant`          | object | Merchant information (name, country, city) |
| `mcc`               | string | Merchant Category Code (optional)          |
| `mid`               | string | Merchant ID (optional)                     |
| `approvalCode`      | string | Authorization approval code (optional)     |
| `cardBalanceBefore` | number | Card balance before (optional)             |
| `cardBalanceAfter`  | number | Card balance after (optional)              |

#### transaction.authorization.declined

Emitted when a payment authorization is declined.

**Payload:**

```json theme={null}
{
  "event": "transaction.authorization.declined",
  "data": {
    "transactionId": "txn_def456",
    "cardId": "card_xyz789",
    "customerId": "cust_def456",
    "amount": 150.0,
    "currency": "USD",
    "type": "AUTHORIZATION",
    "status": "FAILED",
    "createdAt": "2025-01-06T13:00:00Z",
    "merchant": {
      "name": "Expensive Store",
      "country": "US"
    },
    "declineReason": "Insufficient funds"
  }
}
```

**Fields:**

| Field           | Type   | Description                        |
| --------------- | ------ | ---------------------------------- |
| `transactionId` | string | Transaction identifier             |
| `cardId`        | string | Card used for transaction          |
| `customerId`    | string | Customer identifier                |
| `amount`        | number | Transaction amount                 |
| `currency`      | string | Transaction currency               |
| `type`          | string | Transaction type (`AUTHORIZATION`) |
| `status`        | string | Transaction status (`FAILED`)      |
| `createdAt`     | string | ISO 8601 timestamp                 |
| `merchant`      | object | Merchant information               |
| `declineReason` | string | Reason for decline                 |

#### transaction.preauthorization.created

Emitted when a **pre-authorization** hold is placed on a card — an amount is reserved at the merchant but not yet finally captured (common for hotels, car rentals, fuel pumps, etc.). The hold is later either settled (`transaction.settlement.completed`) or released/reversed (`transaction.reversal.completed`).

**Payload:**

```json theme={null}
{
  "event": "transaction.preauthorization.created",
  "data": {
    "transactionId": "pre-67367357",
    "cardId": "card_xyz789",
    "amount": 1.0,
    "currency": "USD",
    "type": "PRE_AUTHORIZATION",
    "status": "PENDING",
    "merchant": {
      "name": "BOOKING.COM"
    }
  }
}
```

**Fields:**

| Field           | Type   | Description                                       |
| --------------- | ------ | ------------------------------------------------- |
| `transactionId` | string | Pre-authorization transaction identifier          |
| `cardId`        | string | Card the hold was placed on                       |
| `amount`        | number | Amount held (reserved)                            |
| `currency`      | string | Transaction currency                              |
| `type`          | string | Always `"PRE_AUTHORIZATION"` for this event       |
| `status`        | string | Always `"PENDING"` (the hold is not yet captured) |
| `merchant`      | object | Merchant information (name, country, city)        |

#### transaction.reversal.completed

Emitted when a transaction is reversed (usually within 24 hours).

**Payload:**

```json theme={null}
{
  "event": "transaction.reversal.completed",
  "data": {
    "transactionId": "txn_pqr678",
    "cardId": "card_xyz789",
    "amount": 25.0,
    "currency": "USD",
    "type": "REVERSAL",
    "status": "SUCCESS",
    "merchant": {
      "name": "Amazon.com",
      "country": "US"
    },
    "reversalReason": "Customer cancelled order",
    "createdAt": "2025-01-06T13:00:00Z"
  }
}
```

**Fields:**

| Field            | Type   | Description                    |
| ---------------- | ------ | ------------------------------ |
| `transactionId`  | string | Transaction identifier         |
| `cardId`         | string | Card used for transaction      |
| `amount`         | number | Reversed amount                |
| `currency`       | string | Transaction currency           |
| `type`           | string | Transaction type (`REVERSAL`)  |
| `status`         | string | Transaction status (`SUCCESS`) |
| `merchant`       | object | Merchant information           |
| `reversalReason` | string | Reason for reversal            |
| `createdAt`      | string | ISO 8601 timestamp             |

#### transaction.refund.completed

Emitted when a refund is processed (usually after settlement).

**Payload:**

```json theme={null}
{
  "event": "transaction.refund.completed",
  "data": {
    "transactionId": "txn_stu901",
    "cardId": "card_xyz789",
    "amount": 25.0,
    "currency": "USD",
    "type": "REFUND",
    "status": "SUCCESS",
    "merchant": {
      "name": "Amazon.com",
      "country": "US"
    },
    "refundReason": "Product returned",
    "createdAt": "2025-01-06T13:00:00Z"
  }
}
```

**Fields:**

| Field           | Type   | Description                    |
| --------------- | ------ | ------------------------------ |
| `transactionId` | string | Transaction identifier         |
| `cardId`        | string | Card used for transaction      |
| `amount`        | number | Refunded amount                |
| `currency`      | string | Transaction currency           |
| `type`          | string | Transaction type (`REFUND`)    |
| `status`        | string | Transaction status (`SUCCESS`) |
| `merchant`      | object | Merchant information           |
| `refundReason`  | string | Reason for refund              |
| `createdAt`     | string | ISO 8601 timestamp             |

#### transaction.funding.completed

Emitted when a card funding transaction completes.

```json theme={null}
{
  "event": "transaction.funding.completed",
  "data": {
    "transactionId": "txn_fund001",
    "cardId": "card_xyz789",
    "amount": 50.0,
    "currency": "USD",
    "type": "FUND",
    "status": "SUCCESS",
    "createdAt": "2025-01-06T12:00:00Z"
  }
}
```

#### transaction.withdrawal.completed

Emitted when a withdrawal from card completes.

```json theme={null}
{
  "event": "transaction.withdrawal.completed",
  "data": {
    "transactionId": "txn_wd001",
    "cardId": "card_xyz789",
    "amount": 30.0,
    "currency": "USD",
    "type": "WITHDRAW",
    "status": "SUCCESS",
    "createdAt": "2025-01-06T12:30:00Z"
  }
}
```

#### transaction.crossborder.charged

Emitted when a cross‑border charge related to a purchase is recorded.

```json theme={null}
{
  "event": "transaction.crossborder.charged",
  "data": {
    "transactionId": "txn_cb001",
    "cardId": "card_xyz789",
    "amount": 25.0,
    "currency": "USD",
    "type": "CROSS_BORDER_CHARGE",
    "status": "SUCCESS",
    "createdAt": "2025-01-06T13:00:00Z",
    "feeAmount": 1.13
  }
}
```

#### transaction.terminated

Emitted when a termination‑type transaction is recorded for a card.

```json theme={null}
{
  "event": "transaction.terminated",
  "data": {
    "transactionId": "txn_term001",
    "cardId": "card_xyz789",
    "amount": 0,
    "currency": "USD",
    "type": "TERMINATION",
    "status": "SUCCESS",
    "createdAt": "2025-01-06T15:00:00Z"
  }
}
```

### Fee Events

#### fee.payment\_failure.charged

Emitted when a fee is charged for a failed payment transaction.

**Payload:**

```json theme={null}
{
  "event": "fee.payment_failure.charged",
  "data": {
    "type": "payment_failure",
    "amount": 0.5,
    "currency": "USD",
    "transactionId": "txn_ghi789",
    "description": "Payment failure fee - Expensive Store - 150.00 USD - 01/06/2025, 02:00 PM",
    "createdAt": "2025-01-06T14:00:05Z"
  }
}
```

#### fee.crossborder.charged

Emitted when a cross-border transaction fee is charged.

**Payload:**

```json theme={null}
{
  "event": "fee.crossborder.charged",
  "data": {
    "type": "crossborder",
    "amount": 1.13,
    "currency": "USD",
    "transactionId": "txn_jkl012",
    "cardId": "card_xyz789",
    "description": "Cross-border fee - Amazon.com - 25.00 USD - 01/06/2025, 01:00 PM",
    "createdAt": "2025-01-06T13:00:20Z"
  }
}
```

### Debt Events

#### debt.recovery.pending

Notifies you in real time when a payment fee debt has been created because the fee could not be collected from the card or wallet at the time of the transaction. Triggered in two scenarios:

* **Payment failure fee** — A card payment fails, and the company wallet cannot cover the failure fee.
* **Cross-border fee** — A cross-border transaction occurs, and the company wallet cannot cover the fee.

**Payload:**

```json theme={null}
{
  "event": "debt.recovery.pending",
  "data": {
    "amount": 0.5,
    "currency": "USD",
    "type": "payment_failure_fee",
    "cardId": "56dd86c7-255f-4f76-bbbb-fdb33c55fb6c",
    "transactionId": "4e3f4fc1-08d9-49a6-b1af-b6495f0a5c2c",
    "originalAmount": 150.0
  }
}
```

**Fields:**

| Field            | Type   | Description                                                                          |
| ---------------- | ------ | ------------------------------------------------------------------------------------ |
| `amount`         | number | Outstanding fee amount in USD                                                        |
| `currency`       | string | Always `"USD"`                                                                       |
| `type`           | string | One of `payment_failure_fee`, `crossborder_fee`                                      |
| `cardId`         | string | Card associated with the underlying transaction (UUID, **camelCase — legacy**)       |
| `transactionId`  | string | Underlying transaction ID (UUID, **camelCase — legacy**)                             |
| `originalAmount` | number | Amount of the underlying transaction that produced this fee, in transaction currency |

**Purpose:**

* Maintain transparency throughout the debt recovery process.

**Cross-border fee calculation:**

```
Fee = (Transaction Amount × 2.5%) + $0.50

Example:
Transaction: $25.00
Fee: ($25.00 × 2.5%) + $0.50 = $0.625 + $0.50 = $1.13
```

**Fee collection priority:**

1. **Card balance** — Fees are first deducted from the available card balance.
2. **Wallet balance** — If the card balance is insufficient, the company USD wallet is debited.
3. **Debt creation** — If both are insufficient, the remaining amount is recorded as a debt to be settled, and `debt.recovery.pending` is sent.

### Customer Events

#### customer.created

Emitted when a new customer is created in the system.

**Payload:**

```json theme={null}
{
  "event": "customer.created",
  "data": {
    "id": "8d4f2a1b-5c3e-4d7f-9a6b-2e1c8f9d0a3b",
    "first_name": "John",
    "last_name": "Doe",
    "email": "john.doe@example.com",
    "phone_number": "+237600000000",
    "country": "Cameroon",
    "created_at": "2025-01-07T11:00:00.000Z"
  }
}
```

**Fields:**

| Field          | Type   | Description                                       |
| -------------- | ------ | ------------------------------------------------- |
| `id`           | string | Unique customer identifier (UUID)                 |
| `first_name`   | string | Customer's first name                             |
| `last_name`    | string | Customer's last name                              |
| `email`        | string | Customer's email address                          |
| `phone_number` | string | Customer's phone number in E.164 format           |
| `country`      | string | Customer's country (full name, e.g. `"Cameroon"`) |
| `created_at`   | string | ISO 8601 timestamp (UTC)                          |

## Best Practices

### Response Handling

Always return `200 OK` within 10 seconds, before processing:

```typescript theme={null}
// GOOD
app.post("/webhooks", async (req, res) => {
  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Process async
  processWebhook(req.body.event, req.body.data).catch((error) =>
    console.error("Webhook error:", error)
  );
});

// BAD
app.post("/webhooks", async (req, res) => {
  // Processing delays response
  await processWebhook(req.body.event, req.body.data);
  res.status(200).json({ received: true });
});
```

### Idempotency

Webhooks may be delivered more than once. Always check for duplicates using `transaction_id`, `card.id`, or `wallet_id`:

```typescript theme={null}
const processedWebhooks = new Set<string>();

async function processWebhook(event: string, data: any) {
  const webhookId = data.transactionId || data.cardId || data.card?.id;

  if (processedWebhooks.has(webhookId)) {
    console.log("Duplicate webhook ignored:", webhookId);
    return;
  }

  processedWebhooks.add(webhookId);
  await handleEvent(event, data);
}
```

### Security

* **Use HTTPS only** - HTTP endpoints are not supported
* **Verify webhook signatures** (when enabled) using the `X-Cartevo-Signature` header — see [Verifying webhook signatures](#verifying-webhook-signatures)
* **Validate request format** - Ensure payload matches expected structure
* **Implement rate limiting** - Protect your endpoint from abuse
* **IP whitelisting** - Whitelist Cartevo IP addresses (when available)

### Error Handling

Handle errors gracefully without exposing internal details:

```typescript theme={null}
app.post("/webhooks", async (req, res) => {
  // Always respond with 200 first
  res.status(200).json({ received: true });

  try {
    await processWebhook(req.body.event, req.body.data);
  } catch (error) {
    // Log error internally only
    console.error("Webhook processing error:", {
      event: req.body.event,
      error: error.message,
    });
    // Alert monitoring system
  }
});
```

### Logging

Record important fields for supportability:

```typescript theme={null}
app.post("/webhooks", (req, res) => {
  const { event, data } = req.body;

  // Log webhook (use business identifiers; there is no webhook-id header)
  console.log({
    timestamp: new Date().toISOString(),
    event,
    transactionId: data.transactionId || data.transaction_id,
    cardId: data.cardId || data.card_id || data.card?.id,
    walletId: data.walletId || data.wallet_id,
  });

  res.status(200).json({ received: true });
  processWebhook(event, data);
});
```

## Sample Receiver Response

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json

{"received": true}
```

## Troubleshooting

| Issue              | Solution                                                                                              |
| ------------------ | ----------------------------------------------------------------------------------------------------- |
| Receiving nothing  | Verify the configured webhook URL and public reachability                                             |
| 4xx errors         | Your receiver rejected the request (auth/validation). Adjust your endpoint to accept the format above |
| 5xx errors         | Your receiver errored; Cartevo will retry up to 3 times (4 total attempts)                            |
| Timeout errors     | Respond within 10 seconds. Process webhooks asynchronously                                            |
| Duplicate webhooks | Implement idempotency checks using transaction IDs                                                    |

## Complete Event Reference

### Payment Events (1)

* `payment.collect` — Payment collection (pay-in) initiated.

### Card Events (6)

* `card.created` — Card successfully created.
* `card.verification.completed` — Card account verified by a merchant (\$0 hold).
* `card.fund` — Card funded from a wallet.
* `card.withdraw` — Money withdrawn from a card to a wallet.
* `card.withdraw.failed` — Card withdrawal failed.
* `card.terminated` — Card terminated (customer- or provider-initiated); remaining balance refunded.

### Transaction Events (10)

* `transaction.authorization.created` — Card payment authorized at a merchant.
* `transaction.authorization.declined` — Card payment authorization declined.
* `transaction.preauthorization.created` — Pre-authorization hold placed on a card.
* `transaction.reversal.completed` — Authorization reversed (typically within 24h).
* `transaction.refund.completed` — Refund processed (after settlement).
* `transaction.settlement.completed` — Authorized transaction cleared.
* `transaction.funding.completed` — Card funding transaction completed.
* `transaction.withdrawal.completed` — Card withdrawal transaction completed.
* `transaction.crossborder.charged` — Cross-border charge recorded against a card.
* `transaction.terminated` — Termination-type transaction recorded.

### Fee Events (2)

* `fee.payment_failure.charged` — Failure-payment fee charged (\$0.50).
* `fee.crossborder.charged` — Cross-border fee charged (2.5% + \$0.50).

### Debt Events (1)

* `debt.recovery.pending` — Fee debt created because the fee could not be deducted from the card or wallet.

### Customer Events (1)

* `customer.created` — Customer created.

**Total: 21 webhook events**
