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

# Issued Cards (Beta)

> Check out on a single-purpose virtual card, paid for by your buyer's card

<Warning>
  **Issued cards are in beta.** Everything on this page, including issued
  checkout and the `card.issue`, `card.retrieve`, `card.close` and
  `card.reveal` endpoints, is enabled per account and may change. To request access, email
  [support@henrylabs.ai](mailto:support@henrylabs.ai). Until access is
  enabled, these endpoints return `403`.
</Warning>

## What it is

With issued cards, a checkout doesn't send your buyer's card to the merchant.
Henry Labs charges your buyer's tokenized card (collect it with the
[Card Element](/v1/sdk/client/elements/card-element)), issues a **virtual card** for
that amount, and checks out on the virtual card instead.

* **The buyer is charged once, by Henry Labs.** Their statement reads
  `HENRY* <MERCHANT>`, for example `HENRY* ZARA`.
* **The buyer doesn't need to be present.** The charge uses the card saved when
  it was tokenized, so no CVC is needed, even days later.
* **The merchant never sees your buyer's card.** Each issued card has a hard
  spending limit and locks to the merchant that charges it.
* **Unspent money goes back automatically.** When the card settles, the buyer is
  refunded whatever it didn't spend, and later merchant refunds on returns are
  passed back too.

There are two ways to use it:

<CardGroup cols={2}>
  <Card title="Issued checkout" icon="bolt" href="#issued-checkout">
    Recommended. Henry Labs sizes, charges and issues the card for you inside
    `cart.checkout.purchase`, from a checkout quote.
  </Card>

  <Card title="Issue a card yourself" icon="credit-card" href="#issue-a-card-yourself">
    Call `card.issue` with an amount you choose, then pay with the returned
    card token.
  </Card>
</CardGroup>

<Warning>
  **Get your buyer's permission for future charges.** Both flows charge the
  buyer's card without them present, which card networks treat as a
  merchant-initiated charge. When your buyer enters their card, tell them it
  may be charged later for their orders, and keep a record that they agreed.
</Warning>

## Issued checkout

For apps using issued checkout, every headless purchase pays with an issued
card. It takes one extra field: the checkout quote the card is sized from.

<Steps>
  <Step title="Quote the cart">
    Run checkout details for the buyer's shipping address. You get one `refId`
    per merchant in the cart.

    ```ts theme={null}
    const details = await henry.cart.checkout.details(cartId, {
      buyer: { shippingAddress },
      mode: "sync",
    });

    const refIds = details.data.jobs.map((job) => job.refId);
    const totals = details.data.jobs.map((job) => job.result?.data?.costs.total);
    ```

    Show your buyer the total before you charge them.
  </Step>

  <Step title="Purchase with the quote">
    Pass every `refId` as `checkoutDetailsRefIds`, within **30 minutes** of the
    quote, with the same items and shipping address.

    ```ts theme={null}
    const purchase = await henry.cart.checkout.purchase(cartId, {
      buyer: {
        name: { firstName: "Ada", lastName: "Lovelace" },
        email: "ada@example.com",
        phone: "+12125551234",
        shippingAddress,
        card: { details: { cardToken: buyerCardToken } },
      },
      checkoutDetailsRefIds: refIds,
    });
    ```

    Henry Labs charges `buyerCardToken`, issues the card and starts the
    checkout on it. Poll or use webhooks as for any purchase.
  </Step>
</Steps>

### What the buyer is charged

Each merchant's **quoted total, plus padding**, rounded up to the whole dollar.
Padding absorbs small changes between the quote and the merchant's final
total, such as tax recalculated at the last step. The default is **\$5 per
merchant**; email [support@henrylabs.ai](mailto:support@henrylabs.ai) to
change it for your app.

| Cart | Quote | Charged | Issued card |
| - | - | - | - |
| One merchant | \$54.12 | $60 ($59.12 rounded up) | Locked to that merchant |
| Two merchants | $80.00 + $40.00 | $85 + $45 = **\$130**, in one charge | One card usable at both |

Whatever the merchants don't take is refunded to the buyer when the card
settles (see [Refunds](#refunds)). For a single-merchant cart you can set the
amount yourself with `capCents`, as long as it covers the quoted total.

### When a quote is refused

Nothing is charged in any of these cases. Fix the quote and retry.

| Problem | Response |
| - | - |
| No `checkoutDetailsRefIds` | `400` |
| A merchant in the cart has no quote, or has two | `400` |
| A `refId` that doesn't exist or isn't your app's | `404` |
| A quote that's still running, failed, or is over 30 minutes old | `409` |
| Items, quantities or the shipping address changed since the quote | `409` |
| The amount is over your per-card maximum or open-card limit | `402` |
| The buyer's card was declined | `402`, with Stripe's `stripeCode` and `declineCode` |

Retrying a purchase with the same `checkoutDetailsRefIds` is safe: it returns
the same order and never charges twice.

<Note>
  Issued checkout currently applies to headless purchases. Hosted checkout
  orders pay with the buyer's card directly, as before.
</Note>

## Issue a card yourself

Use `card.issue` when you want to choose the amount, or use the card outside a
Henry cart.

<Steps>
  <Step title="Issue">
    ```ts theme={null}
    const { data: card } = await henry.card.issue(
      {
        capCents: 3000,
        fundingCardToken: buyerCardToken,
        merchantHost: "zara.com",
        ttlDays: 7,
      },
      { idempotencyKey: "order-1234" },
    );
    // card.cardToken → "card_live_…"
    ```

    * `capCents` is a **lifetime** limit: the most the card can ever be charged
      in total. It's rounded up to the whole dollar ($30.40 becomes $31). Size
      it to cover tax and shipping.
    * The buyer is charged the rounded amount before the card exists.
    * Send an idempotency key. Retrying with the same key returns the same card
      and never charges twice.
  </Step>

  <Step title="Pay with it">
    Use the token anywhere a tokenized card works.

    ```ts theme={null}
    await henry.cart.checkout.purchase(cartId, {
      buyer: {
        // …name, email, phone, shipping address…
        card: { details: { cardToken: card.cardToken } },
      },
    });
    ```

    Issued cards don't need CVC re-collection: their security code stays valid
    until the card expires.
  </Step>

  <Step title="Check on it">
    `card.retrieve(cardToken)` returns the card's limit, expiry, live state, and
    what has been charged to it.
  </Step>

  <Step title="Close it">
    `card.close(cardToken)` once the purchase has fully completed. The card
    then settles within about 15 minutes. Closing is optional: a card you leave
    alone settles when it expires.
  </Step>
</Steps>

## Reveal the card number

If you run the checkout yourself instead of through `cart.checkout.purchase`,
`card.reveal` returns an issued card's full number, expiry and security code.

```ts theme={null}
const { data } = await henry.card.reveal(card.cardToken);
// data.cardNumber, data.expiryMonth, data.expiryYear, data.cvv
```

<Warning>
  **Receiving a full card number puts your systems in PCI DSS scope**
  (typically SAQ D). Don't log, store or display it. If you can, pay with
  `cart.checkout.purchase` and the `cardToken` instead: that never exposes the
  number.
</Warning>

* **Separate beta access.** Reveal is enabled on its own, separately from card
  issuance. Email [support@henrylabs.ai](mailto:support@henrylabs.ai) to
  request it; until then it returns `403`.
* **Issued cards only.** Only cards issued to your app can be revealed. A
  tokenized buyer card never can (`404`).
* **Open cards only.** A card that's closed or expired returns `409`, as does a
  card whose state couldn't be confirmed (safe to retry).
* **Rate limited.** 30 reveals per minute per app (`429` above that). Every
  reveal is recorded with the app and card.

## Refunds

A card **settles** when it's closed or expires. At settlement, the buyer is
refunded the difference between what they were charged and what the merchant
actually took, usually within 15 minutes.

| Situation | Refunded |
| - | - |
| Charged $60, merchant took $54.12 | \$5.88 |
| The checkout failed and the card was never charged | Everything |
| The merchant hasn't captured yet (some charge when they ship) | Settlement waits for the capture |
| The buyer returns an item later | The merchant's refund is passed to the buyer, for up to 120 days after settlement |
| The buyer disputed the charge with their bank | Nothing, so they aren't paid twice; the dispute decides |

<Warning>
  Don't close a card the merchant may still charge. A merchant that charges at
  shipping can't capture on a closed card. If you're unsure, leave the card
  open and let it settle when it expires.
</Warning>

## Limits

| Limit | What it means |
| - | - |
| Per-card maximum | The largest amount one card can be issued for |
| Open-card limit | The total of all your cards that haven't settled yet |
| Expiry | Cards expire after your app's default number of days unless you set `ttlDays` (up to 90) |
| Currency | Cards are issued in US dollars |

Henry Labs sets these for your app during the beta; email
[support@henrylabs.ai](mailto:support@henrylabs.ai) to change them. A request
over either limit returns `402` and charges nothing.


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