Skip to content
Ask the docs

Find answers across the QairoPay docs.

Type a question and we'll synthesize an answer from the docs with citations back to the source pages.

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
Full path of a card transaction. The dashed arrows are out-of-band webhooks; the solid arrows are synchronous.

Prerequisites

Card programs require KYB approval in live (sandbox is auto-approved). Before you issue your first live card:

  1. Complete tenant onboarding in the dashboard (entity formation docs, UBO disclosure).
  2. Sign the Spend Card program order form. This identifies the sponsor bank and locks the cardholder-agreement template.
  3. Wait for the program.activated webhook. 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.