Card

Issue a virtual card to your own account or to a PF sub-account, fund it from the holder's wallet and manage it, step by step.

Overview

The Hodle card is a virtual card issued by Hodle's card issuer partner. Its balance is a USD stablecoin balance (USDC) that the holder tops up from their own Hodle wallet, and it pays anywhere the card network is accepted.

Who holds the card. Every card has exactly one holder:

You call withThe card belongs to
no subAccountIdthe account that owns the API key (you).
subAccountId: "<id>"that sub-account's holder: a person (PF) you created with POST /api/subaccount.

Each holder has its own sign-in, its own onboarding, its own card and its own balance. A card is never visible through another holder: not through the parent call without subAccountId, not through a sibling sub-account, and never through another integrator's key (404).

The holder's wallet signs. Two things are signed by the holder's own Hodle wallet:

  1. the sign-in that opens the card session (step 1);
  2. each top-up that moves stablecoins from the wallet to the card (funding).

You authorize both exactly like a payout: send the holder's walletPin (and, optionally, the protectedSymmetricKey from POST /api/wallet/keys), and Hodle unlocks the wallet and signs. Same PIN rules as payouts: a wrong PIN answers 400 INVALID_PIN, and three wrong PINs in a row lock that wallet for 24 hours (429 RATE_LIMITED). A holder who signs with an external wallet they control can send the signature instead.

The card number is never returned by the API. No route returns the card number (PAN), CVV or expiry. The holder sees them only inside Hodle's secure card-details iframe (PSE) in the Hodle app, which talks to the card issuer partner directly. The API returns an opaque cardId and the last four digits.

Limits

WhatLimit
Holders per accountone card account for you, plus one per PF sub-account
Top-up amountgreater than 0, up to 10000, up to 6 decimals, in the source asset
Top-ups in flightone per holder; finish (or let expire) the current one before the next
Top-up quote validity60 seconds between POST /api/card/fund and its submit
New quotesone every 3 seconds per holder (replaying a requestId is not limited)
REQUEST_OTP, REQUEST_PHONE_CODEonce per 60 seconds per holder
Other onboarding actionsonce per 2 seconds per action and holder, one action at a time
Card session1 hour; unfreezing needs a session signed in the last 5 minutes
Balance freshnessGET /api/card serves a balance up to 30 seconds old; a top-up refreshes it

Prerequisites

  1. Production plane with the card enabled. Your account's production plane must carry the card capability. There are two levels; ask support to enable them:

    CapabilityUnlocks
    card identitysign-in, GET /api/card, and the identity steps: REQUEST_OTP, SIGN_UP, ACCEPT_TERMS, START_KYC
    cardeverything above, plus the card steps, funding, transactions and freeze

    The capability is read from your account (the API-key owner) for your card and for every sub-account's card. Without it every card route answers 403 CARD_NOT_ENABLED.

  2. A Hodle wallet for the holder, created with POST /api/wallet/create. It is the card's signer:

    HolderWalletwalletPin you send
    youyour default walletyour platform wallet PIN
    a sub-accountthe sub-account's default wallet (subAccountId)the sub-account wallet's own secret when it was created with pinScope: "WALLET"; otherwise your platform PIN, which it inherits

    The card is bound to that wallet on the first sign-in. Fund the card from the same holder's wallet.

  3. Individuals only (PF). A sub-account must be a person verified with a valid CPF (KYC) and have an email. The card's identity check is bound to the sub-account's own CPF, never to yours. A company (PJ) sub-account gets 400 SUBACCOUNT_NOT_INDIVIDUAL.

  4. A webhook endpoint for CARD_KYC_APPROVED, CARD_KYC_REJECTED and CARD_ISSUED (Webhooks). Optional, but it saves polling.

The onboarding state machine

GET /api/card and every onboarding response carry stage. Each stage has one way forward, listed in nextStep.actions; nextStep.humanStep says when a person has to act.

AUTH_REQUIRED ──sign in──▶ SIGN_UP ──REQUEST_OTP + SIGN_UP(otp)──▶ TERMS
                                                                     │
                                                              ACCEPT_TERMS
                                                                     ▼
              ┌──────────── KYC_ACTION_REQUIRED ◀──(issuer asks)── KYC ◀─┐
              │                     │                               │    │
              └──START_KYC (kycUrl)─┴───────────────────────────────┘    │
                                                                         │
     KYC approved ──▶ SOURCE_OF_FUNDS ──SUBMIT_SOURCE_OF_FUNDS──▶ PHONE   │
                                                                  │      │
                              REQUEST_PHONE_CODE + VERIFY_PHONE(code)    │
                                                                  ▼      │
                         SAFE ──DEPLOY_SAFE──▶ CARD ──ISSUE_VIRTUAL_CARD──▶ READY
StagenextStep.actionsnextStep.humanStepWhat happens
AUTH_REQUIRED—SIGN_INNo card session. Sign in with the holder wallet.
SIGN_UPREQUEST_OTP, SIGN_UPEMAIL_CODEREQUEST_OTP emails a 6-digit code to the holder; send it back with SIGN_UP.
TERMSACCEPT_TERMS—Accepts the issuer's current terms (terms[] lists them with their URL).
KYCSTART_KYCHOSTED_KYCReturns kycUrl, the hosted identity check the holder opens and completes.
KYC_ACTION_REQUIREDSTART_KYCHOSTED_KYCThe issuer needs more from the holder; a fresh kycUrl reopens the check.
SOURCE_OF_FUNDSSUBMIT_SOURCE_OF_FUNDSSOURCE_OF_FUNDS_ANSWERSAnswer every question in sourceOfFundsQuestions with one of its listed answers.
PHONEREQUEST_PHONE_CODE, VERIFY_PHONESMS_CODESends an SMS to the holder's phone; the holder types the code, you send it with VERIFY_PHONE.
SAFEDEPLOY_SAFE—Creates the card's on-chain account.
CARDISSUE_VIRTUAL_CARD—Issues the virtual card. CARD_ISSUED fires.
READY——The card can be funded and used.

An action for a stage already passed is a no-op that returns the current status. An action for a later stage is refused with 400 (CARD_REQUEST_FAILED): the flow never skips ahead.

Never guess a human step. The email code and the SMS code are typed by the holder, and the hosted KYC is done by the holder in their browser. Your integration relays them; it never invents them.

kycStatus is one of NOT_STARTED, PENDING, ACTION_REQUIRED, APPROVED, REJECTED. REJECTED is terminal: once the card issuer partner rejects a holder, kycStatus stays REJECTED and CARD_KYC_REJECTED is recorded once. For a sub-account whose KYC follows the KYC order (CARD, then DOCUMENTS, then HOSTED), a REJECTED card KYC closes the CARD step and the order can move the sub-account to the next provider.

Tutorial: your own card (account holder)

Every request uses your API key: Authorization: Bearer $API_KEY. Omit subAccountId.

1. Sign in with the holder wallet

Send the holder's wallet PIN. Hodle unlocks the wallet, builds the sign-in, signs it with the wallet and opens the session, in one call.

curl --request POST \
  --url https://api.hodle.com.br/api/card/session \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "walletPin": "$WALLET_PIN", "protectedSymmetricKey": "$PSK" }'
FieldRequiredDescription
walletPinyesThe holder wallet's PIN or secret (see prerequisites). Never echoed or logged.
protectedSymmetricKeynoFrom POST /api/wallet/keys. Defaults to the signer wallet's own.
walletIdnoMust be the holder's card signer wallet; anything else answers 404 WALLET_NOT_FOUND.
subAccountIdnoFor a sub-account's card.

For a holder whose signer is an external wallet they control (not a Hodle wallet), ask for the sign-in message, sign it on the holder's side (EIP-191 personal_sign) and send it back:

curl --request POST \
  --url https://api.hodle.com.br/api/card/session/prepare \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "eoaAddress": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" }'
200
{
  "success": true,
  "data": {
    "walletId": "6a8f0c2e1b7d4a0012ab34cd",
    "requiresSignerAddress": false,
    "message": "app.hodle.com.br wants you to sign in with your Ethereum account:\n0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\n\n…\n\nURI: https://app.hodle.com.br\nVersion: 1\nChain ID: 100\nNonce: 8f2k1x9q0w\nIssued At: 2026-11-05T12:00:00.000Z",
    "eoaAddress": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
    "expiresAt": "2026-11-05T12:05:00.000Z",
    "subAccountId": null
  }
}

eoaAddress is only taken when the wallet has no signer on record yet; a wallet that already has one answers with its own. Then:

curl --request POST \
  --url https://api.hodle.com.br/api/card/session \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "message": "<message from prepare>", "signature": "0x<65-byte signature>" }'
200 — stage before: AUTH_REQUIRED, after: SIGN_UP
{
  "success": true,
  "data": {
    "subAccountId": null,
    "sessionExpiresAt": "2026-11-05T13:00:00.000Z",
    "status": {
      "stage": "SIGN_UP",
      "registered": false,
      "termsAccepted": false,
      "kycStatus": "NOT_STARTED",
      "kycUrl": null,
      "sourceOfFundsAnswered": false,
      "phoneValidated": false,
      "safeDeployed": false,
      "safeAddress": null,
      "activeCardCount": 0,
      "terms": [],
      "sourceOfFundsQuestions": [],
      "nextStep": { "actions": ["REQUEST_OTP", "SIGN_UP"], "humanStep": "EMAIL_CODE" }
    }
  }
}

The session lasts one hour. When a later call answers 409 CARD_SESSION_REQUIRED (or GET /api/card is back at AUTH_REQUIRED), repeat this same call: it renews the session. The response never carries the PIN, the key or the signature.

2. Sign up (email code)

curl --request POST \
  --url https://api.hodle.com.br/api/card/onboarding \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "action": "REQUEST_OTP" }'

The card issuer partner emails a 6-digit code to the holder's email. The holder reads it and gives it to you; then:

--data '{ "action": "SIGN_UP", "input": { "otp": "123456" } }'
200 — stage before: SIGN_UP, after: TERMS
{
  "success": true,
  "data": {
    "subAccountId": null,
    "action": "SIGN_UP",
    "status": {
      "stage": "TERMS",
      "registered": true,
      "termsAccepted": false,
      "kycStatus": "NOT_STARTED",
      "terms": [
        { "type": "general-tos", "currentVersion": "v1", "accepted": false, "url": "https://…/terms" }
      ],
      "nextStep": { "actions": ["ACCEPT_TERMS"], "humanStep": null }
    }
  }
}

3. Accept the terms

Show terms[].url to the holder, then:

--data '{ "action": "ACCEPT_TERMS" }'

Stage before: TERMS, after: KYC.

4. Identity check (hosted KYC)

--data '{ "action": "START_KYC" }'
200 — stage stays KYC until the holder finishes
{
  "success": true,
  "data": {
    "subAccountId": null,
    "action": "START_KYC",
    "status": {
      "stage": "KYC",
      "kycStatus": "NOT_STARTED",
      "kycUrl": "https://…/kyc/session/…",
      "nextStep": { "actions": ["START_KYC"], "humanStep": "HOSTED_KYC" }
    }
  }
}

Send kycUrl to the holder, who completes the identity check in their own browser (document and selfie). Then poll GET /api/card (or wait for CARD_KYC_APPROVED). kycStatus moves to PENDING, then APPROVED (stage SOURCE_OF_FUNDS) or REJECTED (terminal, CARD_KYC_REJECTED). If the stage becomes KYC_ACTION_REQUIRED, call START_KYC again for a fresh kycUrl.

5. Source of funds

status.sourceOfFundsQuestions lists each question with its allowed answers. Ask the holder and send one answer per question:

--data '{
  "action": "SUBMIT_SOURCE_OF_FUNDS",
  "input": {
    "sourceOfFunds": [
      { "question": "What is your occupation?", "answer": "Employed" },
      { "question": "What is the source of your funds?", "answer": "Salary" }
    ]
  }
}'

Stage before: SOURCE_OF_FUNDS, after: PHONE.

6. Phone (SMS code)

--data '{ "action": "REQUEST_PHONE_CODE", "input": { "phoneNumber": "+5511987654321" } }'

The holder receives an SMS and types the code; send it:

--data '{ "action": "VERIFY_PHONE", "input": { "phoneCode": "4821" } }'

Stage before: PHONE, after: SAFE.

7. Create the card account and issue the card

--data '{ "action": "DEPLOY_SAFE" }'
--data '{ "action": "ISSUE_VIRTUAL_CARD" }'

Stage SAFE → CARD → READY. CARD_ISSUED is recorded once.

8. Read the card

curl --url https://api.hodle.com.br/api/card \
  --header "Authorization: Bearer $API_KEY"
200
{
  "success": true,
  "data": {
    "subAccountId": null,
    "stage": "READY",
    "registered": true,
    "termsAccepted": true,
    "kycStatus": "APPROVED",
    "kycUrl": null,
    "sourceOfFundsAnswered": true,
    "phoneValidated": true,
    "safeDeployed": true,
    "safeAddress": "0x4d62c475826F0614d4BfC82F5cDFd74dFB24C50a",
    "activeCardCount": 1,
    "terms": [],
    "sourceOfFundsQuestions": [],
    "nextStep": { "actions": [], "humanStep": null },
    "cards": [
      {
        "cardId": "R25vc2lzUGF5TWFuYWdlZENhcmQ6Y2FyZC0x",
        "lastFourDigits": "4242",
        "virtual": true,
        "statusCode": 1000,
        "activatedAt": "2026-11-05T12:30:00.000Z"
      }
    ],
    "balance": {
      "currency": "USDC",
      "total": "25",
      "spendable": "20",
      "pending": "5",
      "onChain": "25",
      "requiresAuthentication": false,
      "checkedAt": "2026-11-05T12:31:00.000Z"
    }
  }
}

cards and balance are filled from stage CARD on. spendable is what the next purchase can use; pending is held by authorizations not yet settled. statusCode: 1000 is an active card.

Tutorial: a sub-account's card

Same steps, same bodies, plus subAccountId on every call: in the JSON body for POST/DELETE, in the query string for GET (GET /api/card?subAccountId=…).

Before step 1, make sure the sub-account:

  1. was created by your key (POST /api/subaccount) with an email (the card sign-up code goes there);
  2. is a PF whose KYC recorded a valid CPF; the card is bound to that CPF;
  3. has its own default wallet (POST /api/wallet/create with subAccountId), which signs.
1. sign in with the sub-account wallet
curl --request POST \
  --url https://api.hodle.com.br/api/card/session \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "subAccountId": "a928c33c-1ad2-4bf9-b4ce-1f83b403f1e4", "walletPin": "$SUB_ACCOUNT_WALLET_SECRET" }'

walletPin is the sub-account wallet's own secret when it was created with pinScope: "WALLET", otherwise your platform PIN. Your own PIN never opens a pinScope: "WALLET" wallet: it answers INVALID_PIN and counts toward that wallet's lockout.

200
{
  "success": true,
  "data": {
    "subAccountId": "a928c33c-1ad2-4bf9-b4ce-1f83b403f1e4",
    "sessionExpiresAt": "2026-11-05T13:00:00.000Z",
    "status": { "stage": "SIGN_UP", "nextStep": { "actions": ["REQUEST_OTP", "SIGN_UP"], "humanStep": "EMAIL_CODE" } }
  }
}
2+. onboarding, one action at a time
--data '{ "subAccountId": "a928c33c-…", "action": "REQUEST_OTP" }'
--data '{ "subAccountId": "a928c33c-…", "action": "SIGN_UP", "input": { "otp": "123456" } }'
--data '{ "subAccountId": "a928c33c-…", "action": "ACCEPT_TERMS" }'
--data '{ "subAccountId": "a928c33c-…", "action": "START_KYC" }'

The code goes to the sub-account's email, the kycUrl is for the sub-account's holder, and the SMS goes to the phone the holder gives. Webhooks for this card carry its subAccountId.

Refusals specific to sub-accounts:

StatuscodeWhen
404SUBACCOUNT_NOT_FOUNDThe sub-account is not yours (or does not exist).
400SUBACCOUNT_NOT_INDIVIDUALThe sub-account is a company (PJ) or its CPF is invalid.
400SUBACCOUNT_TAX_ID_REQUIREDNo verified CPF yet: finish the sub-account's KYC first.
400SUBACCOUNT_EMAIL_REQUIREDThe sub-account has no email.
400SUBACCOUNT_UNSUPPORTEDA legacy sub-account, created before sub-account references.

Funding the card

A top-up moves a stablecoin from the holder's Hodle wallet to the card balance. Sources (sourceKey): USDC_BASE, USDC_POLYGON, USDT_POLYGON, BRLA_POLYGON. The card must be READY.

POST /api/card/fund ──▶ POST /api/card/fund/{fundingId}/submit (walletPin: Hodle signs)
       (QUOTED)                       (PENDING)
                                                                          │
                       GET /api/card/fund/{fundingId}  ◀── poll ──────────┘  ──▶ COMPLETED

1. Prepare

curl --request POST \
  --url https://api.hodle.com.br/api/card/fund \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "sourceKey": "USDC_BASE", "amount": "20", "requestId": "5f0e2a8c-4b1d-4c7e-9a3f-2d6b8e1c0a47" }'
200
{
  "success": true,
  "data": {
    "fundingId": "5f0e2a8c-4b1d-4c7e-9a3f-2d6b8e1c0a47",
    "subAccountId": null,
    "status": "QUOTED",
    "sourceKey": "USDC_BASE",
    "amount": "20",
    "expectedAmount": "19.8",
    "minimumAmount": "19.7",
    "nativeFee": "0",
    "feeUsd": "0.2",
    "durationSeconds": 45,
    "expiresAt": "2026-11-05T12:41:00.000Z",
    "signingHash": "0xabababababababababababababababababababababababababababababababab",
    "signingWalletId": "6a8f0c2e1b7d4a0012ab34cd",
    "txHash": null,
    "receivingTxHash": null
  }
}

requestId (a UUID you generate) is the idempotency key and becomes fundingId. Repeating the same requestId with the same body returns the same funding; with a different body it is refused. minimumAmount is the least the card receives.

2. Submit before expiresAt

Authorize it with the source wallet's PIN, like a payout; Hodle signs signingHash with the wallet signingWalletId and broadcasts.

curl --request POST \
  --url https://api.hodle.com.br/api/card/fund/5f0e2a8c-4b1d-4c7e-9a3f-2d6b8e1c0a47/submit \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "walletPin": "$WALLET_PIN", "protectedSymmetricKey": "$PSK" }'

protectedSymmetricKey is optional (defaults to the source wallet's own). A wrong PIN answers 400 INVALID_PIN and counts toward the same 3-strike lockout as payouts.

Sign signingHash as a raw digest (no message prefix) with the external signer, prefix the 65-byte signature with 12 zero bytes and send it:

const raw = await account.sign({ hash: funding.signingHash })
const signature = `0x${'0'.repeat(24)}${raw.slice(2)}`
--data '{ "signature": "0x000000000000000000000000<130 hex chars>" }'

status becomes PENDING. The operation is broadcast at most once: a retried submit answers the funding's current state and never sends again.

3. Track it

GET /api/card/fund/{fundingId} refreshes the funding from the chains (at most every 5 seconds) until COMPLETED. FAILED means nothing reached the card; RECONCILIATION_REQUIRED means support is checking the delivery. Once completed, GET /api/card shows the new balance.

Card management

Transactions

curl --url "https://api.hodle.com.br/api/card/transactions?offset=0" \
  --header "Authorization: Bearer $API_KEY"
200
{
  "success": true,
  "data": {
    "subAccountId": null,
    "transactions": [
      {
        "createdAt": "2026-11-05T12:45:00.000Z",
        "clearedAt": null,
        "isPending": true,
        "kind": "Payment",
        "status": "Approved",
        "merchant": "Coffee Shop",
        "billingAmount": "1.5",
        "billingCurrency": "USD",
        "billingCurrencyDecimals": 6,
        "billingCurrencySymbol": "$",
        "amount": "7.5",
        "currency": "BRL",
        "currencyDecimals": 2,
        "currencySymbol": "R$"
      }
    ],
    "hasNextPage": false
  }
}

kind is Payment, Refund or Reversal. Pages hold 20; pass the next offset.

Before the card is issued, card management calls answer 409 CARD_NOT_ISSUED with the holder's stage:

409
{ "success": false, "error": "The card is not issued yet: … Current stage: SIGN_UP.", "code": "CARD_NOT_ISSUED", "errorCode": "CARD_NOT_ISSUED", "stage": "SIGN_UP" }

Freeze and unfreeze

curl --request POST \
  --url https://api.hodle.com.br/api/card/freeze \
  --header "Authorization: Bearer $API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "cardId": "R25vc2lzUGF5TWFuYWdlZENhcmQ6Y2FyZC0x", "frozen": true, "requestId": "freeze-2026-11-05-0001" }'
200
{ "success": true, "data": { "subAccountId": null, "cardId": "R25vc2lzUGF5TWFuYWdlZENhcmQ6Y2FyZC0x", "frozen": true } }

requestId (16-80 letters, digits or -) makes the call idempotent. Freezing works with any open session. Unfreezing needs a session signed in the last 5 minutes: sign in again first, or the call answers 409 CARD_SESSION_REQUIRED.

Card number, CVV and expiry

Never through the API. The holder opens them in the Hodle app, inside Hodle's secure card-details iframe (PSE), served by the card issuer partner. Your systems never receive them.

Sign out

DELETE /api/card/session (with subAccountId for a sub-account) ends the card session.

Webhooks

Register CARD_KYC_APPROVED, CARD_KYC_REJECTED and CARD_ISSUED like any other webhook. Each event is recorded once per holder, the first time Hodle sees it. Failed deliveries are retried in the background, including after an interrupted status request. Recorded acknowledgements skip endpoints that accepted the event while other endpoints retry. Deliveries have at-least-once semantics.

CARD_KYC_APPROVED
{
  "event": "CARD_KYC_APPROVED",
  "data": {
    "subAccountId": "a928c33c-1ad2-4bf9-b4ce-1f83b403f1e4",
    "occurredAt": "2026-11-05T12:20:00.000Z",
    "kycStatus": "APPROVED"
  }
}
CARD_KYC_REJECTED
{
  "event": "CARD_KYC_REJECTED",
  "data": {
    "subAccountId": "a928c33c-1ad2-4bf9-b4ce-1f83b403f1e4",
    "occurredAt": "2026-11-05T12:20:00.000Z",
    "kycStatus": "REJECTED"
  }
}
CARD_ISSUED
{
  "event": "CARD_ISSUED",
  "data": {
    "subAccountId": null,
    "occurredAt": "2026-11-05T12:30:00.000Z",
    "activeCardCount": 1
  }
}

subAccountId is null for your own card. Hodle notices a milestone when it reads the card status (any card call or the Hodle app), so call GET /api/card while you wait for one.

MCP

The Hodle MCP server exposes the card to agents:

ToolKindWhat it does
hodle_card_statusReadGET /api/card: stage, kycStatus, cards (last four digits only) and balance.
hodle_card_transactionsReadGET /api/card/transactions.
hodle_card_sessionWriteSigns your own card in (or renews it) with the server's HODLE_WALLET_PIN; not for sub-accounts.
hodle_card_onboardingWriteOne onboarding action. Asks for the holder's email code, SMS code or answers instead of guessing, and hands back kycUrl for the holder.
hodle_card_fundWritePrepare (sourceKey, amount) or submit (fundingId, signed with the server PIN or an external signature) a top-up. Capped per call by HODLE_MCP_CARD_FUND_MAX_USD, default 50 USD.
hodle_card_freezeWriteFreeze or unfreeze a card by cardId.

Write tools need HODLE_MCP_ALLOW_WRITES=true and confirm: true. hodle_card_session and a hodle_card_fund submit without signature use the wallet PIN the MCP server holds for payouts (HODLE_WALLET_PIN, plus HODLE_PROTECTED_SYMMETRIC_KEY when set): the account holder's only, so sub-account cards are signed from your integration. The PIN is never a tool argument nor returned.

Errors

Every refusal is { "success": false, "error": "<message>", "code": "<CODE>", "errorCode": "<CODE>" } (errorCode repeats code, as on the other routes). error is a human-readable message (often in Portuguese) for logs and support; branch on code.

StatuscodeMeaning
400CARD_INVALID_REQUESTThe body or query failed validation (error names the field).
400CARD_REQUEST_FAILEDThe step was refused: wrong stage, invalid code, expired quote, insufficient balance…
400SUBACCOUNT_*See sub-account refusals.
401—Missing or invalid API key.
403CARD_NOT_ENABLEDThe production plane lacks the card capability (or only has card identity for a card step).
404SUBACCOUNT_NOT_FOUNDThe sub-account is not yours.
404CARD_NOT_FOUNDThe funding (or account) does not exist for this holder.
400INVALID_PINWrong walletPin (same code as payouts); counts toward the wallet lockout.
404WALLET_NOT_FOUNDNo card signer wallet for this holder, or walletId is not it.
429RATE_LIMITEDThree wrong PINs: the wallet is locked for 24 hours (same as payouts).
409CARD_SESSION_REQUIREDNo card session (or it expired), the holder wallet changed, or unfreeze needs a fresh sign-in: sign in again.
409CARD_NOT_ISSUEDThe session is valid but the card is not issued yet (cards, transactions, freeze, top-ups). stage says where the holder is; finish the onboarding.
429CARD_RATE_LIMITEDA cooldown is running (see limits); retry later.

Endpoints

MethodPathCapability
POST/api/card/session/preparecard identity
POST/api/card/sessioncard identity
DELETE/api/card/sessioncard identity
GET/api/cardcard identity
POST/api/card/onboardingcard identity for REQUEST_OTP, SIGN_UP, ACCEPT_TERMS, START_KYC; card for the rest
POST/api/card/fundcard
POST/api/card/fund/{fundingId}/submitcard
GET/api/card/fund/{fundingId}card
GET/api/card/transactionscard
POST/api/card/freezecard

All accept subAccountId. Full schemas are in the API reference.