Issue virtual cards against a customer wallet and authorise card payments in real time. Cards are issued through Stripe Issuing with credentials configured per Account; every payment attempt reaches Truust as a synchronous webhook, is approved or declined against the wallet balance, and is recorded as an order tagged with the Authorization category.
Overview
Stripe Authorization turns a customer wallet into a spendable balance: the customer gets a virtual card, and every purchase is authorised in real time against the funds held in that wallet. The flow has two halves:- Issuing — you create a cardholder, a wallet and a virtual card through the API. The card details (PAN, CVC, expiry) are never stored by Truust; they are revealed directly in the browser through Stripe.js.
- Authorising — when the cardholder pays, Stripe calls Truust within a 2 second deadline. Truust checks the available balance and answers approve or decline. Money only moves when the payment is later captured.
404.
The issuing currency is fixed to EUR. A wallet in any other currency is rejected.
Setup
Credentials are configured per Account, not per platform, so different Accounts can issue against different Stripe accounts. In the Dashboard: Source → Edit → Add Other Gateway → Stripe Authorization.
If an Account has no Stripe Authorization gateway, every operation fails with
422 and the message “Stripe Authorization is not configured for this account”. There is no fallback to a global configuration.
Stripe Issuing uses two webhook endpoints — the real-time authorization one and the asynchronous events one — and each has its own signing secret. Put both in
webhook_secret, separated by commas.How It Works
1. The customer becomes a cardholder
A customer created withtype: "STRIPE" is registered as a Stripe cardholder as soon as it is created. This requires a complete KYC block — see Cardholder requirements. The resulting cardholder id (ich_...) is stored in metadata.stripe_cardholder_id.
2. A wallet funds the card
Each card is backed by exactly one EUR wallet. The wallet balance is the spendable limit of the card: there is no credit line, no overdraft.3. The card is issued
POST /2.0/customers/{uuid}/cards with a wallet_id issues a virtual card. The response never contains the PAN — only the last four digits in alias and the Stripe card id in gateway_reference_id.
4. Every payment attempt is authorised in real time
When the cardholder pays, Stripe sendsissuing_authorization.request and waits up to 2 seconds for the answer. Truust:
- Resolves the card and rejects it if it is unknown or inactive.
- Takes a lock on the wallet, so two simultaneous authorisations cannot spend the same money.
- Computes the available balance: cached wallet balance minus the amount already held by live authorisations.
- Answers
approved: trueif the available balance covers the amount,falseotherwise. - Records the attempt as an order and a payin — declines included.
5. Capture moves the money
Stripe sendsissuing_transaction.created when the merchant captures, usually seconds to days later. Only then is the amount transferred from the customer wallet to the Account wallet, and a transaction is recorded.
Cardholder Requirements
Stripe requires a full identity for the cardholder. A customer of typeSTRIPE cannot be created without these fields:
If any is missing the request fails with
422 and lists exactly what is missing:
Endpoints
Issuing a Card
Response:
Revealing Card Details
The PAN and CVC never travel through Truust servers. They are rendered by Stripe.js inside secure iframes, and unlocking them takes a nonce generated in the browser plus an ephemeral key obtained from the API:GET /2.0/cards/{uuid}?nonce= adds two fields to the response:
The nonce is single-use and tied to that browser session: an ephemeral key requested with a stale nonce is useless.
The Authorization Webhook
Point both Stripe Issuing webhook endpoints at:webhook_secret configured across Accounts, so a single URL serves all of them. An unsigned or unrecognised request gets 400.
Everything is idempotent on the Stripe authorisation id, so a retried event never duplicates an order, a payin or a transfer.
Orders and Payins
Every authorisation attempt produces a visible order, so declines are auditable too. TheAuthorization category is what separates these orders from ordinary ones. The Dashboard uses it to split the two sections: /authorizations lists the orders that carry it, /orders the ones that do not. The same rule applies to their payins.
The order uses the regular lifecycle statuses:
Order fields worth knowing:
And on the payin:
Merchant data
merchant_data describes where the card was used, and is stored verbatim:
Available Balance
The decision is not taken against the raw wallet balance but against:AUTHORIZED state on that wallet. A wallet holding 19.35 € with a 5.00 € authorisation awaiting capture will decline anything above 14.35 €.
The wallet balance is cached and refreshed from the ledger when it is older than 5 seconds, which keeps the decision inside the 2s deadline without serving a stale figure.
In the Dashboard
Issued cards are deliberately kept out of the customer’s Cards tab, which only lists tokenized cards.
Testing
A full run, from an empty Account to a captured payment.1. Configure the gateway
Source → Edit → Add Other Gateway → Stripe Authorization, with the keys of your Stripe test account. Use the Issuing test keys, not the live ones.2. Point the webhooks at your environment
In the Stripe Dashboard, create the two Issuing endpoints (real-time authorizations and events) against/2.0/stripe/issuing/webhook, and copy both signing secrets into the gateway, comma-separated.
Locally, the Stripe CLI does the same job:
whsec_... — add it to the gateway too.
3. Create the cardholder
metadata.stripe_cardholder_id. If it does not, the cardholder was not registered.
4. Create the wallet and issue the card
customer_id and wallet_id are numeric ids, as in every POST; UUIDs are only used in URLs.
5. Fund the wallet
Move funds into the wallet and confirm the balance is visible on Wallets before going further. An unfunded wallet declines everything, which looks like a broken integration but is the correct answer.6. Trigger an authorisation
From the Stripe Dashboard, open the card under Issuing → Cards and use Create test purchase, or with the CLI:{"approved": true}, and Authorizations shows a new order named after the merchant, in PENDING_RELEASE. The wallet balance has not changed yet — the amount is on hold.
7. Capture it
RELEASED, the payin to CONFIRMED with a reference_id, the customer wallet drops by the amount and the Account wallet rises by the same figure.
8. Check the edge cases
9. Verify the card details
Open BaaS → Issued Cards and use View card data. The PAN, expiry and CVC must render inside the card. If they do not, the usual cause is apublishable key that does not belong to the same Stripe account as the secret.
Notes
- Issuing is EUR only. A wallet in another currency is rejected when the card is issued.
- One card per wallet: issuing a second card from the Dashboard creates a new wallet for it.
- Declines create an order too. This is intentional — a payment attempt that failed for lack of funds is auditable.
- Approving does not move money; only capture does. Reconcile against the ledger transfer, not against the authorisation.
reference_dataholds the raw Stripe payload, whose shape belongs to Stripe. Do not build logic on top of its keys.- Cancelling a card cannot be undone. Issue a new one instead.
- See the Create customer endpoint, Create wallet endpoint and Get card endpoint for the full request schemas.
