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

> ## Agent Instructions
> Bitnob API requests authenticate with an `X-Auth-Client` API key header against https://api.bitnob.com.
> Always use the sandbox environment for testing. Never put live API keys in sample code.
> Money amounts are integers in the smallest unit of the currency (satoshis for BTC, cents for fiat). Never use floats.
> Every state-changing request takes a client-supplied `reference` and must be treated as idempotent on retry.
> Quotes expire. Re-quote rather than reusing a stale quote id.

# Issue your first card

> Take one card through its whole life: verify the cardholder, create and fund the card, handle spends and refunds, then freeze or terminate it.

In this tutorial you take one card through its whole life: verify the cardholder, create the card, fund it, see a transaction land, handle a refund, and shut the card down. By the end you have every integration point a production card program needs.

## Prerequisites

* Your API credentials and a working signing helper; see [Authentication](https://www.bitnob.dev/api-reference/authentication/authentication.md).
* A reachable webhook endpoint that [verifies signatures](https://www.bitnob.dev/api-reference/webhooks.md#verify-every-delivery).
* Enough in your company balance to cover the card's opening amount plus fees. The \$2.00 card in this tutorial takes \$4.00: the \$2.00 load, a \$1.00 top-up fee, and a \$1.00 creation fee. See [What creating a card costs](https://www.bitnob.dev/docs/card-issuing/card-issuance.md#what-creating-a-card-costs).

Amounts throughout are integers in micro-units: 1 USD = 1,000,000, so `2000000` is \$2.00.

**1. Verify the cardholder**

Every card starts with a verified cardholder. Submit their identity details to [Card KYC](https://www.bitnob.dev/api-reference/cards/card-kyc.md). The response returns the `customer_id` you pass to Create card. Identity details are captured here, so you don't send them again when you create the card. [Card KYC](https://www.bitnob.dev/docs/card-issuing/card-kyc.md) covers document rules, statuses, and rejections.

```http theme={null}
POST /api/cards/kyc
```

```json theme={null}
{
  "customer": {
    "customer_type": "individual",
    "first_name": "Amaka",
    "last_name": "Eze",
    "date_of_birth": "1992-05-14",
    "id_type": "bvn",
    "id_number": "22200000000",
    "email": "amaka.eze.6788f4@example.com",
    "phone_number": "8000000000",
    "dial_code": "+234",
    "line1": "12 Admiralty Way",
    "city": "Lagos",
    "state": "Lagos",
    "postal_code": "101233",
    "country": "NGA"
  },
  "occupation": "software_engineer",
  "employment_status": "employed",
  "account_purpose": "online subscriptions",
  "annual_salary": "12000000",
  "expected_monthly_volume": "500000",
  "place_of_birth": "Enugu",
  "terms_of_service_accepted": true
}
```

With a BVN, approval can come back in the same response:

```json theme={null}
{
  "success": true,
  "message": "Card KYC initiated successfully",
  "data": {
    "customer_id": "01a14942-c667-783d-8e42-9d9d4828bc60",
    "status": "approved",
    "completion_link": "",
    "email": "amaka.eze.6788f4@example.com",
    "first_name": "Amaka",
    "last_name": "Eze",
    "normalized_status": "approved"
  },
  "timestamp": "2026-10-17T09:47:55.452509813Z"
}
```

> [!NOTE]
>
> KYC can be asynchronous. Wait until the customer is approved before you issue a card. If `status` comes back pending, the outcome arrives on the `virtualcard.user.kyc.complete` or `virtualcard.user.kyc.failed` webhook. If the cardholder has a step to finish, the response also carries `verification_link`, the page they must complete, with `verification_link_expires_at`.

If a customer with that `id_number` or phone number already exists under a different email, Card KYC returns `400` with that customer's ID in the `detail`. Resubmit with `customer_id` in place of the `customer` object to verify the existing customer.

Store the `customer_id`. It's the input to the next step.

**2. Create the card**

Issue the card against the approved `customer_id`. `name` is optional and defaults to the customer's name. Full schema: [Create card](https://www.bitnob.dev/api-reference/cards/create-card.md).

```http theme={null}
POST /api/cards
```

```json theme={null}
{
  "card_type": "virtual",
  "currency": "USD",
  "amount": 2000000,
  "card_brand": "visa",
  "customer_id": "01a14942-c667-783d-8e42-9d9d4828bc60",
  "reference": "CARD_CREATE_7e685abc-40bc-4ca2-b7aa-209fad4777d6"
}
```

`webhook_url` and `contactless_payment` are optional; see [Create card](https://www.bitnob.dev/api-reference/cards/create-card.md) for every field.

The response returns the card in status `pending`, with no card number yet:

```json theme={null}
{
  "success": true,
  "message": "Card created successfully",
  "data": {
    "card": {
      "id": "01a14943-44cf-7b97-832e-5a70803c54b9",
      "company_id": "00000000-0000-4000-8000-000000000001",
      "customer_id": "01a14942-c667-783d-8e42-9d9d4828bc60",
      "card_type": "virtual",
      "status": "pending",
      "created_status": "processing",
      "card_brand": "visa",
      "name": "Amaka Eze",
      "preferred_name": "Amaka Eze",
      "first_six_digit": "000000",
      "masked_pan": "000000******",
      "balance_amount": "0",
      "balance_currency": "USD",
      "display_amount": 0,
      "contactless_payment": false,
      "card_limits": {},
      "reference": "CARD_CREATE_7e685abc-40bc-4ca2-b7aa-209fad4777d6",
      "card_program": "prepaid",
      "nickname": "",
      "business_spend": null,
      "available_actions": [],
      "balance_breakdown": null,
      "user_metadata": "{}",
      "billing_address": {
        "line1": "3401 N. Miami, Ave. Ste 230",
        "line2": "",
        "city": "Miami",
        "state": "Florida",
        "postal_code": "33127",
        "country": "United States"
      },
      "created_by": "00000000-0000-4000-8000-000000000001",
      "created_at": "2026-10-17T09:48:27Z",
      "updated_at": "2026-10-17T09:48:27Z"
    }
  },
  "timestamp": "2026-10-17T09:48:27.910453852Z"
}
```

The opening `amount` and both fees are taken from your company balance at this point. If it can't cover them, the call fails with `400` and `extensions.error_code` `COMPANY_INSUFFICIENT_BALANCE`.

Store the card `id`. Every later call uses it.

**3. Wait for the activation webhook**

The issuer confirms creation with `virtualcard.created.completed`:

```json theme={null}
{
  "event_id": "7f3a9c2b-4d1e-4f5a-8b9c-1d2e3f4a5b6c",
  "event": "virtualcard.created.completed",
  "data": {
    "id": "01a14943-44cf-7b97-832e-5a70803c54b9",
    "companyId": "00000000-0000-4000-8000-000000000001",
    "status": "active",
    "reference": "CARD_CREATE_7e685abc-40bc-4ca2-b7aa-209fad4777d6",
    "createdStatus": "completed"
  }
}
```

This is the signal to show the card in your UI. Fetch the PAN, CVV, and expiry with [Get secure card details](https://www.bitnob.dev/api-reference/cards/get-secure-card-details.md), display them once, and do not store the CVV. If you've registered a card encryption key, they arrive encrypted; see [Decrypt card details](https://www.bitnob.dev/docs/card-issuing/decrypt-card-details.md). If creation fails you get `virtualcard.created.failed` with a `failureReason`.

You can also check with [Get card](https://www.bitnob.dev/api-reference/cards/get-card.md). Once the card is `active`, it shows the last four digits and the opening balance:

```json theme={null}
{
  "success": true,
  "message": "Card retrieved successfully",
  "data": {
    "card": {
      "id": "01a14943-44cf-7b97-832e-5a70803c54b9",
      "status": "active",
      "created_status": "completed",
      "last_four_digit": "7725",
      "masked_pan": "000000******7725",
      "balance_amount": "2000000",
      "display_amount": 2,
      "available_actions": [
        "FUND",
        "WITHDRAW",
        "FREEZE",
        "UPDATE_LIMITS",
        "TERMINATE"
      ]
    }
  }
}
```

**4. Fund the card**

Load USD from your balance with [Fund or withdraw balance](https://www.bitnob.dev/api-reference/cards/fund-or-withdraw-balance.md). `type: "fund"` credits the card; `type: "withdraw"` moves value back.

```http theme={null}
POST /api/cards/{cardId}/balance
```

```json theme={null}
{
  "amount": 2000000,
  "reference": "FUND_TYU039847383",
  "type": "fund"
}
```

`reference` is your idempotency key: retrying with the same reference returns the original transaction instead of double-funding. Confirmation arrives as `virtualcard.topup.completed`; a balance or card status problem is refused straight away with a `400`. If a top-up fails later during processing, you get `virtualcard.topup.failed` with a `reason`.

**5. Handle a spend**

When the cardholder pays a merchant, the settled charge reaches you as `virtualcard.transaction.settlement`:

```json theme={null}
{
  "event_id": "06cd8fab-da0b-4c4d-87e8-0a1b2c3d4e5f",
  "event": "virtualcard.transaction.settlement",
  "data": {
    "cardId": "01a14943-44cf-7b97-832e-5a70803c54b9",
    "id": "tx_debit_a1b2c3d4e5f6",
    "companyId": "00000000-0000-4000-8000-000000000001",
    "amount": 10000000,
    "display_amount": 10.0,
    "currency": "USD",
    "transaction_type": "settlement",
    "reference": "spend_txn_001",
    "narrative": "Settlement at spotify",
    "timestamp": "2026-05-16T14:22:10Z",
    "status": "completed",
    "reason": null,
    "merchant_name": "spotify",
    "merchant_mcc": "5815",
    "merchant_country": "SWE"
  }
}
```

Branch on `event` first, then on `transaction_type`; several events share a name across pending and settled states. The full catalog is on [Webhook events](https://www.bitnob.dev/docs/card-issuing/webhooks.md). The same activity is queryable with [List card transactions](https://www.bitnob.dev/api-reference/cards/list-card-transactions-by-card.md).

**6. Handle a refund or reversal**

Merchants return money two ways, and both arrive as webhooks: `virtualcard.transaction.refund` after settlement, `virtualcard.transaction.reversed` when an authorization is canceled before settlement. Credit your user's view of the balance from these events. Each event has its own `reference`, so match it to the original spend by card and merchant.

**7. Freeze, unfreeze, or terminate**

Give users a freeze button, and keep termination for endings:

```http theme={null}
POST /api/cards/{cardId}/status
{ "status": "frozen" }     # or "active" to unfreeze

DELETE /api/cards/{cardId}
{ "reason": "Card no longer needed" }
```

Freezing blocks new authorizations immediately and is reversible via [Freeze or unfreeze](https://www.bitnob.dev/api-reference/cards/freeze-or-unfreeze.md). [Terminate](https://www.bitnob.dev/api-reference/cards/terminate-card.md) is permanent, sweeps the remaining balance back to your wallet, and cannot be called within 24 hours of creation. Details and the automatic transitions are in [Freeze, unfreeze, or terminate a card](https://www.bitnob.dev/docs/card-issuing/card-status-management.md).

## Production checklist

* Idempotency keys on card creation and every top-up.
* Webhook handler verifies the signature, de-duplicates on `event_id`, and returns `200` fast.
* Card status tracked in your database and reflected in your UI.
* CVV never stored; `masked_pan` shown only after user authentication.
* Decline handling wired: repeated declines carry fees and can terminate the card. See [Fees and decline rules](https://www.bitnob.dev/docs/card-issuing/card-fees.md).

## Next

> [card] [Fund a card](https://www.bitnob.dev/docs/card-issuing/card-funding.md)
>
> Load the card you just issued.

> [card] [Receive card webhooks](https://www.bitnob.dev/docs/card-issuing/receive-card-webhooks.md)
>
> Learn what the card is doing.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.