Skip to content

How to Use the QPay Ledger

This guide walks through onboarding onto QPay's internal ledger — from joining it, through creating and KYC-approving a cardholder, to issuing a credit card with an approved limit. It is the alternative to running your own ledger, and it is what the sandbox uses by default.

Read QPay Ledger Core first if you have not — it explains the chart of accounts and why accounts appear when they do.

Time to complete: ~15 minutes
API: Ledger Accounts API — base path /api/issuer/ledger


Prerequisites

  • An access token for an issuer tenant. See Authentication.
  • Your client must hold the ledger roles used below. See Ledger roles.
  • An existing card product with a layout and an approved embosser, if you intend to issue a card in Step 8. See Manage Card Products.

Set your token and base URLs:

export TOKEN="eyJhbGci..."
export BASE="https://sandbox.qpay.quecto.com.br"
export LEDGER="$BASE/api/issuer/ledger"

Currencies are numeric codes

Every currency on the ledger is an ISO 4217 numeric code: 986 for BRL, 840 for USD. Those two are the only ones available today.


Overview

flowchart TD
    A[Check membership] --> B[Join the ledger]
    B --> C[Activate another currency]
    C --> D[Fund the pool account]
    D --> E[Create a cardholder]
    E --> F[Approve KYC per currency]
    F --> G[Deposit to the checking account]
    G --> H[Create a card with credit limits]
    H --> I[Read balances and limits]
Step What it creates Role
2 Tenant + the full tenant-level account set for one currency manage-ledger-tenant
3 The tenant-level account set for a further currency manage-ledger-tenant
4 A pool-account movement manage-ledger-pool
5 A cardholder record — no accounts manage-ledger-cardholder
6 The cardholder's Checking_Account approve-ledger-kyc
7 A deposit into that checking account deposit-ledger-funds
8 The card and its credit accounts create-ledger-card
9 Nothing — read only manage-ledger-cardholder
10 A credit-limit adjustment manage-ledger-credit-limit

Step 1 — Check whether you have already joined

Before anything else, find out where your issuer stands:

curl -s $LEDGER/accounts/tenants/me \
  -H "Authorization: Bearer $TOKEN" | jq .

Example response for an issuer that has not joined:

{
  "joined": false,
  "currencies": []
}

And once joined:

{
  "joined": true,
  "tenant_id": "0192f3a1-7c4e-7000-8a1b-2c3d4e5f6071",
  "currencies": [986]
}

Save the tenant_id — several endpoints below take it in the path:

export TENANT_ID="0192f3a1-7c4e-7000-8a1b-2c3d4e5f6071"

Step 2 — Join the ledger

Joining creates your tenant and, atomically, the complete tenant-level account set for one currency — pool, corporate, the three floats, revenue and expense. Either all of it is created, or none of it is.

curl -s -X POST $LEDGER/accounts/tenants/join \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"currency_numeric_code": 986}' | jq .

Example response:

{
  "tenant_id": "0192f3a1-7c4e-7000-8a1b-2c3d4e5f6071"
}

You can only join once

A second join request for the same tenant is rejected with 409, not silently accepted. To add a currency later, use Step 3 — never join again.


Step 3 — Activate a further currency

Each additional currency gets its own complete, isolated set of tenant-level accounts.

curl -s -X POST $LEDGER/accounts/tenants/$TENANT_ID/currencies \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"currency_numeric_code": 840}' | jq .

Activating a currency that is already active returns 409.


Step 4 — Record money arriving in your bank account

The Pool_Account_Asset mirrors the physical bank account holding your cardholders' funds. When money lands there for real, record it on the ledger so the two stay in parity.

curl -s -X POST $LEDGER/accounts/tenants/$TENANT_ID/pool-account/986/fundings \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "100000",
    "idempotency_key": "3f2b9c10-5d41-4e77-9a0e-8b1c2d3e4f50"
  }' | jq .

Example response:

{
  "pool_account_id": "0192f3a1-7c4e-7000-8a1b-2c3d4e5f6072",
  "currency_numeric_code": 986,
  "amount": "100000",
  "transfer_id": "0192f3a2-1111-7000-8a1b-2c3d4e5f6073",
  "pool_balance": "100000",
  "unallocated_balance": "100000"
}
Field Meaning
amount Minor units as a decimal string — "100000" is R$ 1,000.00
idempotency_key Your own UUID. Retrying with the same key and amount never double-applies; reusing it for a different amount is a 409
pool_balance Must match your physical bank balance
unallocated_balance Funds that have arrived but are not yet allocated to any cardholder — the ceiling on the next deposit

Use .../withdrawals with the same body shape to record money leaving.

No counterparty, by design

Pool movements take no counterparty account. Both sides are resolved from your own tenant record, which makes it structurally impossible to fund your corporate account from regulated cardholder money.


Step 5 — Create a cardholder

This establishes identity only. No account is created — a cardholder can exist with none at all.

curl -s -X POST $LEDGER/accounts/cardholders \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "legal_name": "JOAO DA SILVA",
    "tax_id": "12345678900",
    "date_of_birth": "1985-04-17",
    "contact_email": "joao.silva@example.com",
    "contact_phone": "+5511987654321",
    "currencies": [986]
  }' | jq .

Example response:

{
  "cardholder_id": "0192f3b0-8a2d-7000-9c4e-1f2a3b4c5d60"
}
export CARDHOLDER_ID="0192f3b0-8a2d-7000-9c4e-1f2a3b4c5d60"

The currencies list names which currencies this cardholder should be evaluated for. Every one starts with KYC not approved.

Identity fields are immutable

legal_name, tax_id and date_of_birth cannot be changed after creation — altering them would invalidate a KYC decision without a re-review. PATCH /accounts/cardholders/{cardholder_id} accepts only contact_email and contact_phone; sending any other field is rejected with 422.

curl -s -X PATCH $LEDGER/accounts/cardholders/$CARDHOLDER_ID \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"contact_email": "joao.silva@newmail.example.com"}'

Step 6 — Approve KYC for a currency

This is the step that creates the cardholder's Checking_Account. Nothing exists before it, and it is per currency — a cardholder can be approved for BRL while still pending for USD.

The KYC analysis itself happens in your own systems. The ledger only records the outcome and reacts to it.

curl -s -X POST $LEDGER/accounts/cardholders/$CARDHOLDER_ID/kyc/986/approve \
  -H "Authorization: Bearer $TOKEN" | jq .

Example response:

{
  "checking_account_id": "0192f3b1-0000-7000-9c4e-1f2a3b4c5d61"
}
Failure Status Cause
Currency not active for your tenant 422 Run Step 3 first
Already approved 409 Approval is one-way per currency
Cardholder belongs to another issuer 404 Never 403 — existence is not disclosed

Step 7 — Deposit funds into the checking account

For prepaid products, credit the cardholder's checking account. The amount is capped by your tenant's unallocated (Float_Inbound) balance from Step 4 — you cannot allocate money that has not arrived.

curl -s -X POST $LEDGER/accounts/cardholders/$CARDHOLDER_ID/checking-account/986/deposits \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "15000",
    "idempotency_key": "7c1a5e90-2b34-4d68-8f01-9a2b3c4d5e6f"
  }' | jq .

Example response:

{
  "checking_account_id": "0192f3b1-0000-7000-9c4e-1f2a3b4c5d61",
  "currency_numeric_code": 986,
  "amount": "15000",
  "transfer_id": "0192f3b2-2222-7000-9c4e-1f2a3b4c5d62",
  "balance": "15000"
}

The response carries the resulting balance, so no follow-up read is needed to confirm the credit landed.


Step 8 — Create a card

Creating a card at the ledger is what instantiates its credit accounts. The ledger reads the product's card template, and for every application whose account type is CREDIT it creates a Card_Invoice_Asset plus the three shadow limit accounts, seeded so the Golden Limit Equation holds from the first instant. Applications that are not CREDIT reuse the checking account from Step 6.

The ledger then calls the Issuer API on your behalf, so this single request also produces the physical card and its embossing job.

curl -s -X POST $LEDGER/accounts/cards \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "b4d9f210-6e83-4a15-9c27-0d1e2f3a4b5c",
    "product_id": 12,
    "cardholder_id": "'"$CARDHOLDER_ID"'",
    "currency_numeric_code": 986,
    "credit_limits": { "A0000003330101": 500000 },
    "layout_id": 7,
    "reference_id": "CARD-REF-00001",
    "card_holder_name": "JOAO DA SILVA",
    "billing_address": {
      "name": "JOAO DA SILVA",
      "address_line1": "Av. Paulista 1000",
      "city": "Sao Paulo",
      "state": "SP",
      "country": "BR",
      "postal_code": "01310100"
    },
    "shipping_address": {
      "name": "JOAO DA SILVA",
      "address_line1": "Av. Paulista 1000",
      "city": "Sao Paulo",
      "state": "SP",
      "country": "BR",
      "postal_code": "01310100"
    }
  }' | jq .

Example response:

{
  "card_id": 5821,
  "embosser_id": 3,
  "status": "PRODUCTION_STATUS_APPROVED",
  "embossing_status": "EMBOSSING_PENDING"
}
Field Required Notes
idempotency_key Yes Reuse verbatim to retry the same intent safely
product_id Yes The product whose card template declares the applications
product_embosser_id No Defaults to the product's default embosser
cardholder_id Yes Must be KYC-approved for currency_numeric_code
credit_limits For credit AID → approved limit in minor units, one entry per CREDIT application
pin No Omit to have the HSM generate a random PIN

Onboarding order is enforced

If the cardholder is not KYC-approved for that currency, or the currency is not active for your tenant, card creation fails before any account or card is created. The ledger never creates a card against a state it has not already established.

Consult the exact billing_address and shipping_address field list, and the pin shape, in the Ledger Accounts API reference.


Step 9 — Read balances and credit limits

Checking-account balance:

curl -s $LEDGER/accounts/cardholders/$CARDHOLDER_ID/checking-account/986/balance \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "checking_account_id": "0192f3b1-0000-7000-9c4e-1f2a3b4c5d61",
  "currency_numeric_code": 986,
  "balance": "15000",
  "credits_posted": "15000",
  "debits_posted": "0",
  "credits_pending": "0",
  "debits_pending": "0"
}

balance is the settled figure. Authorizations that have not cleared show up in debits_pending — subtract them to get the cardholder's spendable amount.

Credit limits:

curl -s $LEDGER/accounts/cards/5821/credit-limits \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "card_id": 5821,
  "credit_limits": [
    {
      "card_application_id": 9001,
      "aid": "A0000003330101",
      "currency_numeric_code": 986,
      "card_invoice_asset_id": "0192f3c0-3333-7000-9c4e-1f2a3b4c5d63",
      "total_granted": "500000",
      "available": "480000",
      "used": "20000",
      "pending_available_credits": "3500",
      "pending_used_debits": "3500",
      "available_after_pending": "476500",
      "used_including_pending": "23500"
    }
  ]
}

total_granted always equals available + used. A card with no CREDIT application returns an empty list.

available and used are settled figures. An authorization that has not cleared yet moves neither — it is held as pending_available_credits (and its mirror image pending_used_debits, the same amount seen from the drawn side). So:

  • available_after_pending is what the cardholder can actually still spend, and it is the exact figure the authorizer compares an incoming transaction against. Use this one to answer "will the next purchase go through?". It is not clamped at zero — holds standing after a limit cut can push it negative.
  • used_including_pending is everything consumed, cleared or not.

The two pending_* fields report the same holds from opposite sides and are equal in a healthy ledger; if they ever differ, the ledger's own reconciliation controls flag it.


Step 10 — Change a credit limit

Credit limits are set to an absolute value, not adjusted by a delta — state the new approved limit and the ledger derives the movement, so no prior read is required.

curl -s -X PUT $LEDGER/accounts/cards/5821/credit-limits/9001 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "total_granted": "800000",
    "idempotency_key": "e1f2a3b4-c5d6-4e7f-8a90-1b2c3d4e5f60"
  }' | jq .

Send "0" to revoke the limit entirely. If the limit already matches the requested value the response omits transfer_id — nothing needed posting.

The response carries the same limit fields as Step 9, including the pending ones, so there is no need to follow up with a GET. Note that a limit change never touches outstanding holds: the pending_* figures come back unchanged, and available_after_pending moves by the same amount as available.

Cutting a limit below what the cardholder has already drawn is allowed: available goes negative while used stays truthful, and the Golden Limit Equation still holds.


What's next?

Your cardholder now has a card and a credit line. The next thing they need is a statement.

  • Run a Billing Cycle


    Configure a cycle, close statements, take payments, and accrue finance charges.

    Read guide

  • Credit Card Invoicing


    The concepts behind cutoffs, allocation order, and reconciliation controls.

    Learn more