Skip to content

Guides

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.

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.

Output
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_...).

#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 -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 }'

A second owner confirms with POST /api/w/:wid/issuing/proposals/:pid/confirm. 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):

Shell
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.

#4. Pay

The agent asks the gate first, exactly as in the quickstart. 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.

#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.