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
issuertenant. 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:
Example response for an issuer that has not joined:
And once joined:
Save the tenant_id — several endpoints below take it in the path:
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:
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:
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:
| 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:
{
"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_pendingis 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_pendingis 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.
-
Credit Card Invoicing
The concepts behind cutoffs, allocation order, and reconciliation controls.
Related¶
- Ledger Accounts API reference — interactive endpoint documentation
- QPay Ledger Core — chart of accounts and accounting principles
- Create a Card — the Issuer API path, for products and layouts
- Ledger roles — which role each endpoint requires