> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.thepennyinc.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.thepennyinc.com/_mcp/server.

# Issuing a Card

This walks through issuing a virtual card to a new individual cardholder, drawing funds from an existing Capital Account, and putting a spend control on it. See [Cards & Cardholders](/issuing/cards-and-cardholders) first if these terms are new.

All requests go to Penny Issuing (`https://issuing.sandbox.api.thepennyinc.com` in sandbox), except creating the spend control, which lives on Penny Banking. Assigning a spend control to a card or cardholder happens on Penny Issuing.

## 1. Create a cardholder (optional)

Every card needs a cardholder. Skip this step if you already have a `cardholder_id`. You can also skip it when creating a new cardholder and card together: in the `POST /cards/virtual` request in step 3, replace `cardholder_id` with an inline `cardholder` object. For an individual, add this object alongside the other card fields:

```json
{
  "cardholder": {
    "individual": {
      "given_name": "Jordan",
      "family_name": "Rivera",
      "email": "jordan@example.com",
      "address": {
        "line_1": "1 Market St",
        "city": "San Francisco",
        "state_or_province": "CA",
        "post_code": "94105",
        "country": "US"
      }
    }
  }
}
```

Supply exactly one of `cardholder_id` or `cardholder`. The examples in step 3 use the ID returned by the separate cardholder request below.

The address takes `line_1`, an optional `line_2`, `city`, `state_or_province` (an ISO 3166-2 code, required for countries that use subdivisions), `post_code`, and `country` (ISO 3166-1 alpha-2). You can also pass an optional `phone_number`. Cardholder responses return these as `phone` and, in the address, `zip_code`.

The cardholder is created in your [default program](/get-started/platform-concepts/programs#your-default-program) and business entity, and the response includes the `program_id` and `business_entity_id` it was assigned.

**`cURL`**

```bash title="cURL"
curl -X POST https://issuing.sandbox.api.thepennyinc.com/cardholders/individual \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "given_name": "Jordan",
    "family_name": "Rivera",
    "email": "jordan@example.com",
    "address": {
      "line_1": "1 Market St",
      "city": "San Francisco",
      "state_or_province": "CA",
      "post_code": "94105",
      "country": "US"
    }
  }'
```

**`Python`**

```python title="Python"
import requests

ISSUING = "https://issuing.sandbox.api.thepennyinc.com"
headers = {"Authorization": f"Bearer {access_token}"}

cardholder = requests.post(
    f"{ISSUING}/cardholders/individual",
    headers=headers,
    json={
        "given_name": "Jordan",
        "family_name": "Rivera",
        "email": "jordan@example.com",
        "address": {
            "line_1": "1 Market St",
            "city": "San Francisco",
            "state_or_province": "CA",
            "post_code": "94105",
            "country": "US",
        },
    },
).json()
cardholder_id = cardholder["cardholder_id"]
```

**`JavaScript`**

```javascript title="JavaScript"
const ISSUING = "https://issuing.sandbox.api.thepennyinc.com";
const headers = {
  Authorization: `Bearer ${accessToken}`,
  "Content-Type": "application/json",
};

const cardholder = await fetch(`${ISSUING}/cardholders/individual`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    given_name: "Jordan",
    family_name: "Rivera",
    email: "jordan@example.com",
    address: {
      line_1: "1 Market St",
      city: "San Francisco",
      state_or_province: "CA",
      post_code: "94105",
      country: "US",
    },
  }),
}).then((res) => res.json());
const cardholderId = cardholder.cardholder_id;
```

## 2. Create a spend control (optional)

Skip this if you're attaching an existing one. This example limits debit transactions to between \$10 and \$500 — see [Spend Controls](/spend-controls) for the full set of rule types.

**`cURL`**

```bash title="cURL"
curl -X POST https://sandbox.api.thepennyinc.com/spend_controls/ \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rule_configs": [{
      "rule_type": "transaction_amount",
      "applicable_transaction_authorizations": ["debit"],
      "applicable_card_issuance_modes": ["managed"],
      "configuration": {"USD": {"minimum": "10.00", "maximum": "500.00"}}
    }]
  }'
```

**`Python`**

```python title="Python"
BANKING = "https://sandbox.api.thepennyinc.com"

spend_control = requests.post(
    f"{BANKING}/spend_controls/",
    headers=headers,
    json={
        "rule_configs": [{
            "rule_type": "transaction_amount",
            "applicable_transaction_authorizations": ["debit"],
            "applicable_card_issuance_modes": ["managed"],
            "configuration": {"USD": {"minimum": "10.00", "maximum": "500.00"}},
        }]
    },
).json()
spend_control_id = spend_control["spend_control_id"]
```

**`JavaScript`**

```javascript title="JavaScript"
const BANKING = "https://sandbox.api.thepennyinc.com";

const spendControl = await fetch(`${BANKING}/spend_controls/`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    rule_configs: [{
      rule_type: "transaction_amount",
      applicable_transaction_authorizations: ["debit"],
      applicable_card_issuance_modes: ["managed"],
      configuration: { USD: { minimum: "10.00", maximum: "500.00" } },
    }],
  }),
}).then((res) => res.json());
const spendControlId = spendControl.spend_control_id;
```

## 3. Issue the card

Cards are issued with `POST /cards/virtual`. The request needs a `card_product_id`, a `capital_account_id`, and a cardholder (`cardholder_id` or an inline `cardholder`). Optional fields include `control_account_id`, `spend_control_id`, `alias`, `description`, `expiration_date`, and `active`.

If the card product's `usage_type` is `single_reload`, also send `load_amount`: the card's lifetime budget, as a decimal string in USD (for example `"250.00"`). It's required for `single_reload` products and rejected with a `422` for any other usage type. See [Usage types](/issuing/cards-and-cardholders#usage-types) for how each type is enforced.

The examples below use the `cardholder_id` returned by step 1. If you skipped that step, use an existing ID or the inline `cardholder` payload shown there.

The card number is never part of the request. Penny chooses the BIN and BIN range from the card product and generates the card number, including the `last_four` you'll see on the card object afterward.

**`cURL`**

```bash title="cURL"
curl -X POST https://issuing.sandbox.api.thepennyinc.com/cards/virtual \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "card_product_id": "card_product_01937f5d-a015-7a55-818b-a015a5518b97",
    "capital_account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
    "cardholder_id": "cardholder_01937e19-4ebb-75bd-8250-4ebb5bd25085",
    "spend_control_id": "spend_control_019379d9-a170-796b-867a-a17096b67a21",
    "alias": "Jordan'"'"'s Card"
  }'
```

**`Python`**

```python title="Python"
card = requests.post(
    f"{ISSUING}/cards/virtual",
    headers=headers,
    json={
        "card_product_id": "card_product_01937f5d-a015-7a55-818b-a015a5518b97",
        "capital_account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
        "cardholder_id": cardholder_id,
        "spend_control_id": spend_control_id,
        "alias": "Jordan's Card",
    },
).json()
card_id = card["card_id"]
```

**`JavaScript`**

```javascript title="JavaScript"
const card = await fetch(`${ISSUING}/cards/virtual`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    card_product_id: "card_product_01937f5d-a015-7a55-818b-a015a5518b97",
    capital_account_id: "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
    cardholder_id: cardholderId,
    spend_control_id: spendControlId,
    alias: "Jordan's Card",
  }),
}).then((res) => res.json());
const cardId = card.card_id;
```

A new card is `active` as soon as it's issued and can authorize transactions straight away. Pass `"active": false` to issue it `deactivated` and activate it later.

To change the card's spend control later, call `PUT /cards/{card_id}/spend_control` on Penny Issuing with `{"spend_control_id": "..."}`, or `DELETE /cards/{card_id}/spend_control` to remove it.

## 4. Confirm it

Read the card back and check its `status`:

**`cURL`**

```bash title="cURL"
curl https://issuing.sandbox.api.thepennyinc.com/cards/$CARD_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

**`Python`**

```python title="Python"
requests.get(f"{ISSUING}/cards/{card_id}", headers=headers).json()
```

**`JavaScript`**

```javascript title="JavaScript"
await fetch(`${ISSUING}/cards/${cardId}`, { headers }).then((res) => res.json());
```

## Revealing full card details (optional)

The card object returned above, like every other card read, includes the `expiration_date`, `bin`, and `last_four`, but never the full card number or CVV. If you need the unmasked PAN (for example, to display it once to the cardholder), call the dedicated reveal endpoint. It requires the `card:reveal` permission, which wildcards don't grant:

**`cURL`**

```bash title="cURL"
curl -X POST https://issuing.sandbox.api.thepennyinc.com/cards/$CARD_ID/reveal \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

**`Python`**

```python title="Python"
payment_details = requests.post(
    f"{ISSUING}/cards/{card_id}/reveal", headers=headers
).json()
# payment_details: card_id, pan, cvv, expiration_month, expiration_year
```

**`JavaScript`**

```javascript title="JavaScript"
const paymentDetails = await fetch(`${ISSUING}/cards/${cardId}/reveal`, {
  method: "POST",
  headers,
}).then((res) => res.json());
```

This is a separate, highly sensitive, separately audited operation — deliberately kept apart from the `reveal_sensitive` query parameter used elsewhere in the API (see [Cards & Cardholders](/issuing/cards-and-cardholders)), so that reading a raw card number always requires an explicit, dedicated call rather than a flag on a routine read. Call it only when you need the full number.

## Next steps

* [Setting Spend Controls](/spend-controls/setting-spend-controls) — update limits after the fact, or share one control across many cards
* [Reading Transactions & Ledger Entries](/banking/reading-transactions) — see the card's activity as it happens
* [Subscribing to Webhooks](/platform/subscribing-to-webhooks) — get a `card.issued` event the moment a card like this one comes online, instead of polling for it