Issuing a Spend Card
The Spend Card is QairoPay’s debit-style card product. Issued via a sponsor bank, settled in USDC on Aptos, accepted anywhere the underlying card network is accepted. You provide a balance; we provide the card and the rails.
What happens on a card swipe
When a cardholder taps or swipes, five systems coordinate in under a second. Knowing the order matters when you’re debugging declines or designing real-time spending controls.
sequenceDiagram autonumber participant CH as Cardholder participant Merchant participant Network as Card network participant Bank as Issuing bank participant QP as QairoPay participant You as Your endpoint CH->>Merchant: Tap or swipe Merchant->>Network: Authorization request Network->>Bank: Auth request Bank->>QP: Auth and cardholder context QP->>QP: Apply spending controls alt Real-time auth opt-in (Enterprise) QP->>You: card.transaction.authorization_request Note over You,QP: 1.5s budget — falls back to static rules You-->>QP: approve or decline plus reason end QP-->>Bank: Decision Bank-->>Network: Approve or decline Network-->>Merchant: Result Merchant-->>CH: Receipt QP--)You: card.transaction.authorized webhook Note over Merchant,QP: Later (often days), capture Merchant->>Bank: Capture Bank->>QP: Capture QP--)You: card.transaction.captured webhook
Prerequisites
Card programs require KYB approval in live (sandbox is auto-approved). Before you issue your first live card:
- Complete tenant onboarding in the dashboard (entity formation docs, UBO disclosure).
- Sign the Spend Card program order form. This identifies the sponsor bank and locks the cardholder-agreement template.
- Wait for the
program.activatedwebhook. From there, you can issue live cards.
In sandbox you can skip all of this — every tenant is approved at signup.
1. Create a cardholder
The cardholder is the end user who will spend the card. They are KYC’d through QairoPay’s KYC partner (Persona) the first time they receive a card.
const cardholder = await qp.cardholders.create( { name: "Jane Doe", email: "jane@example.com", phone: "+1-415-555-0100", date_of_birth: "1992-04-12", address: { line1: "1 Market St", city: "San Francisco", state: "CA", postal_code: "94111", country: "US", }, }, { idempotencyKey: crypto.randomUUID() },);The response contains a kyc.status field. If it’s verified, you can issue. If it’s pending, the user has been emailed a Persona verification link; you’ll receive a cardholder.kyc.completed webhook on completion.
2. Issue a virtual card
Virtual cards activate instantly and can be provisioned into Apple Pay or Google Pay.
const card = await qp.cards.create( { cardholder_id: cardholder.id, program: "default", type: "virtual", spending_controls: { daily_limit_cents: 50000, monthly_limit_cents: 500000, blocked_categories: ["gambling", "adult_content"], }, }, { idempotencyKey: crypto.randomUUID() },);
console.log(card.last4); // "4092"console.log(card.expiry_month, card.expiry_year);The full PAN, CVC, and expiry are never returned by this endpoint. To surface the full card to the cardholder, fetch a short-lived display token and render it client-side using our hosted iframe:
const token = await qp.cards.displayToken(card.id, { ttl_seconds: 60 });// then in the browser:// <iframe src={`https://card.qairopay.com/display?token=${token}`} />The iframe is PCI-scoped at QairoPay; the PAN never enters your environment.
3. Provision into a wallet
To provision the card into Apple Pay or Google Pay from your iOS/Android app, request a provisioning payload:
const payload = await qp.cards.provisioningPayload(card.id, { device: { platform: "ios", device_id: "...", wallet_id: "..." },});Pass the payload to PKPaymentPass.requestSecureProvisioning() (iOS) or TapAndPayClient.pushTokenize() (Android). The wallet completes the rest.
4. Issue a physical card
const physical = await qp.cards.create( { cardholder_id: cardholder.id, program: "default", type: "physical", shipping: { service: "standard", address: cardholder.address, }, }, { idempotencyKey: crypto.randomUUID() },);You’ll receive card.shipped when the card leaves the printer (with a tracking number) and card.activated when the cardholder activates it.
5. Handle authorizations
When the card is used, you receive card.transaction.authorized (and, later, card.transaction.captured when the merchant captures). You don’t approve or decline — the sponsor bank does that — but you can set spending controls that the network enforces in real time.
await qp.cards.update(card.id, { spending_controls: { daily_limit_cents: 100000, // bumped from 50000 blocked_merchants: ["mid_01HZX"], // block a specific merchant id },});For programs that need bespoke decisioning beyond static rules, opt into real-time authorization webhooks (see Webhooks → Events). Your endpoint receives the authorization with a 1.5-second response budget and returns approve/decline. We strongly recommend you don’t need this — static rules cover most cases — but it’s available.
6. Disputes
When a cardholder reports an unauthorized or incorrect transaction, open a dispute:
await qp.disputes.create({ transaction_id: "txn_01HZX", reason: "fraudulent", evidence: { customer_communication: "..." },});The sponsor bank’s chargeback workflow handles the network filings. You’ll receive dispute.updated as the case progresses. Dispute outcomes update the underlying transaction’s disposition field.
What to read next
- USDC settlement — how spend resolves to USDC on Aptos.
- Webhooks → Event catalog — every event your card program emits.
- Errors →
card_declined— handling network declines.