Skip to main content

Cards

An OPay Tap card is linked to a wallet. It holds no stored value, so its reach is the whole wallet balance, and its spend caps are what contain the loss if it is stolen.

The card object

{
"id": "2b7d9e10-4c3f-4a55-9e6b-1d2c3b4a5f60",
"userId": "1f2e3d4c-5b6a-4790-8123-456789abcdef",
"cardNumber": null,
"last4": "4417",
"cardType": 1,
"cardTypeName": "STANDARD",
"printedName": "ANA MENDEZ",
"status": "ACTIVE",
"frozen": false,
"issuedAt": "2026-08-01T10:00:00.0000000Z",
"activatedAt": "2026-08-03T12:30:00.0000000Z",
"frozenAt": null,
"perTxnCap": 250.00,
"dailyCap": 800.00,
"lifetimeCap": null
}

cardNumber is null except for the holder reading their own card. Treat it as a payment credential: never log, cache or store it.

StatusMeaning
UNISSUEDCreated, not yet activated by the holder
ACTIVEUsable, unless frozen is true
BLOCKEDTerminal. The holder cannot reverse it

frozen is separate from status. A frozen card is still ACTIVE but cannot spend.

List and read

GET /api/v1/cards

Scope: core:cards:read. Returns the signed-in user's cards.

{ "data": { "cards": [ { "id": "...", "last4": "4417" } ] } }

GET /api/v1/cards/{cardId}

Scope: core:cards:read. Returns 404 when the card is not the caller's.

Issue

POST /api/v1/cards

Scope: core:cards:create. Administrative.

FieldTypeNotes
userIdstringWho the card belongs to
cardTypeintegerCard product
printedNamestringName printed on the card, up to 64 characters

The response carries the full card number exactly once, for the print pipeline. It is never returned again.

Activate

POST /api/v1/cards/{cardId}/activate

Scope: core:cards:activate

{ "cardNumber": "6000 0000 0000 4417" }

The holder proves possession by entering the printed number. Spacing and dashes are tolerated. The card must be UNISSUED.

Freeze and unfreeze

POST /api/v1/cards/{cardId}/freeze

Scope: core:cards:freeze. No step-up. Freezing is the emergency control and must always be one tap away.

POST /api/v1/cards/{cardId}/unfreeze

Scope: core:cards:freeze. Requires step-up verification, because unfreezing restores spending.

Spend caps

Three optional caps, all in BZD, all nullable. null means no cap.

CapApplies to
perTxnCapA single payment
dailyCapRolling 24 hours
lifetimeCapThe card's whole life

PUT /api/v1/cards/{cardId}/limits

Scope: core:cards:limits

{ "perTxnCap": 250.00, "dailyCap": 800.00, "lifetimeCap": null }

This replaces all three caps. A field you leave out is sent as null, which removes that cap. Always send the full set.

Tightening and loosening

A change loosens if any single cap loosens.

CurrentRequestedLoosening
nullnullNo, nothing changed
nulla valueNo, this adds a bound
a valuenullYes, this removes a bound
a valuelargerYes
a valueequal or smallerNo
ChangeWhat it takes
TighteningNothing extra. Always allowed, never prompts
Loosening, user enrolled in 2FAA valid step-up grant
Loosening, user not enrolledRefused with core::gatekeeper::enrol::required. The user must enrol first

Loosening a cap is the one action where an unenrolled user is refused rather than let through. It is a privilege increase, and refusing costs the user nothing because the card keeps working at its current caps. See Step-up verification.

Cap validation

RuleMessage
Caps must be greater than zeroA cap of 0 is rejected. To stop all spending, freeze the card
perTxnCap cannot exceed dailyCap
dailyCap cannot exceed lifetimeCap
perTxnCap cannot exceed lifetimeCap

Failures are 400 with core::tessera::limits::invalid. The message is written for the cardholder, so it is safe to show as-is.

Validation runs before the step-up check, so an invalid change is never worth a code prompt. Apply the same order in your client.

Block

POST /api/v1/cards/{cardId}/block

Scope: core:cards:manage. Administrative and permanent. A blocked card cannot be unblocked; issue a replacement instead.

Error codes

CodeStatusMeaning
core::tessera::not_found404Unknown card, or not the caller's
core::tessera::request::invalid409The card's state does not allow this change
core::tessera::limits::invalid400Cap combination rejected
core::tessera::unusable400Card cannot be used to pay, for example frozen
core::tessera::update::fail500Write failed, nothing changed