# The card rail with Stripe Issuing

> Bind a virtual card to an agent and the card issuer asks Immiscible in real time before money moves. No receipt, no payment. Every authorisation, approved or declined, is evidence.

Source: https://immiscible.fly.dev/docs/guides/card-rail

An agent that holds a card number can spend without asking anyone. The card rail turns that around: the agent holds a card that **only works against a receipt**. Stripe Issuing (or any issuer through the generic adapter) asks Immiscible during authorisation, and Immiscible approves only when the agent holds an unused receipt that covers the charge. The approval uses the receipt up.

```text
agent asks the gate ──> allow + receipt ──> agent pays with its card
                                                  │
                         Stripe Issuing asks ─────┘
                         Immiscible: is there an unused receipt for this
                         payee and currency covering this amount?  yes: approve, spend it
                                                                   no:  decline
```

## Rules at the card

Several conditions hold at the card itself, whatever the agent's receipt says:

- Cash, transfer and gambling merchant codes never pay.
- A grocery receipt pays only under grocery merchant codes.
- A tolerance over the receipt applies only where tips happen.
- Captures add up; an over-capture freezes the agent under an incident hold.
- A force post with no authorisation freezes the agent.
- Replays get their first answer.
- Anything that cannot be verified, read or decided declines. A refusal is always a `200` with `approved: false`, because an error status would hand the decision to the issuer's own stand-in, which may approve.

Clearing, not the agent's own report, settles the action: what the issuer says was captured is what the ledger records.

## 1. Create the Stripe webhook

In the Stripe dashboard, for your Issuing account:

1. Add a webhook endpoint at `https://immiscible.fly.dev/issuing/<workspace id>/stripe`. The console shows the exact URL under **Agents**, **Cards**.
2. Subscribe to `issuing_authorization.request`, `issuing_authorization.created`, `issuing_authorization.updated` and `issuing_transaction.created`.
3. Turn on real-time authorisations for the endpoint, and copy its **signing secret** (`whsec_...`).

> **Warning**
> Set the issuer's timeout default to **decline**. If Immiscible cannot be reached in time, nothing should be approved without it. When Stripe's stand-in does approve something, Immiscible records it as a stand-in approval, not as its own.

## 2. Connect the issuer

An owner saves the signing secret. Connecting an issuer widens what money can move, so with anyone else in the workspace it is a **proposal a second owner confirms**; alone in a workspace, the owner acts alone.

curl:

```bash
curl -X PUT "https://immiscible.fly.dev/api/w/$IMMISCIBLE_WORKSPACE/issuing/stripe" \
  -H "cookie: __Host-sid=$IMMISCIBLE_SESSION" -H "x-immiscible-csrf: 1" \
  -H "content-type: application/json" \
  -d '{ "secret": "whsec_...", "overPct": 0 }'
```

Node:

```ts
const res = await fetch('https://immiscible.fly.dev/api/w/$IMMISCIBLE_WORKSPACE/issuing/stripe', {
  method: 'PUT',
  credentials: 'include',
  headers: { 'content-type': 'application/json', 'x-immiscible-csrf': '1' },
  body: JSON.stringify({ secret: process.env.STRIPE_ISSUING_WHSEC, overPct: 0 }),
});
// 200 when you are alone in the workspace; 202 with { proposal } when a second owner must confirm
```

A second owner confirms with [`POST /api/w/:wid/issuing/proposals/:pid/confirm`](https://immiscible.fly.dev/docs/api/post-api-w-wid-issuing-proposals-pid-confirm.md). A proposal lapses if its proposer stops being an owner.

## 3. Bind a card to an agent

Create a virtual card for the agent in Stripe, then bind its id to the agent (also dual control in a team):

```bash
curl -X POST "https://immiscible.fly.dev/api/w/$IMMISCIBLE_WORKSPACE/agents/agt_4f2c91a7/cards" \
  -H "cookie: __Host-sid=$IMMISCIBLE_SESSION" -H "x-immiscible-csrf: 1" \
  -H "content-type: application/json" \
  -d '{ "issuer": "stripe", "cardRef": "ic_1Q2w3E4r5T6y", "last4": "4242" }'
```

Removing a card narrows what money can move, so it never waits for a second owner: [`DELETE /api/w/:wid/cards/:cid`](https://immiscible.fly.dev/docs/api/delete-api-w-wid-cards-cid.md).

## 4. Pay

The agent asks the gate first, exactly as in the [quickstart](https://immiscible.fly.dev/docs/quickstart.md). On `allow` it holds a receipt for that payee, currency and amount; it then pays with its card, and the authorisation succeeds only because the receipt exists. Without asking first, the card declines.

Every authorisation is under **Cards** in the console and at [`GET /api/w/:wid/card-authorizations`](https://immiscible.fly.dev/docs/api/get-api-w-wid-card-authorizations.md).

## Any other issuer

The generic adapter takes two signed calls:

```http
POST /issuing/$IMMISCIBLE_WORKSPACE/generic/authorize HTTP/1.1
immiscible-signature: t=1790444901,v1=5d41402abc4b2a76b9719d911017c592...
content-type: application/json

{ "authRef": "auth_881", "cardRef": "card_19", "amount": 4200, "currency": "GBP", "merchant": { "name": "Grocer", "domain": "grocer.example", "mcc": "5411" }, "eventId": "evt_77" }
```

```http
POST /issuing/$IMMISCIBLE_WORKSPACE/generic/clear HTTP/1.1
immiscible-signature: t=1790445002,v1=7c211433f02071597741e6ff5a8ea34...
content-type: application/json

{ "kind": "capture", "authRef": "auth_881", "cardRef": "card_19", "amount": 4200, "currency": "GBP", "eventId": "evt_78" }
```

The signature is an HMAC-SHA256 of `<t>.<raw body>` with the issuer secret you saved for `generic`, within five minutes. `kind` is `capture`, `refund` or `closed`. The answer to an authorisation is always `200` with `{ "approved": true }` or `{ "approved": false, "reason": "..." }`. A capture Immiscible could not record is answered `503` so the issuer sends it again, and is recorded as an incident.
