Credit Cards API
This API manages revolving credit cards: a credit limit, billing cycles with a cut-off date, statements with a due date, configurable daily interest on unpaid balances, a minimum payment with a late fee, and purchase reversals.
It is a product line of its own, separate from DPA flows and debts — the only shared entity is the client. The CMS Base URL is used (see introduction).
No card number, no security code
The tail you see in responses is a display label derived from the account id
so support and the cardholder can name the same card. It is not a PAN and
cannot be used as a payment instrument — issuing and tokenization belong to a
card processor.
The Card Account Object
Attributes
| Attribute | Description | Type |
|---|---|---|
id | Card account id | UUID |
clientId | The client the card belongs to | UUID |
productId | Card product (plan) the terms were copied from | UUID |
referenceId | Partner's own id — creation idempotency key | String |
status | See Card Status | String |
creditLimit | Credit limit (cupo máximo), enforced on every purchase | Money |
outstanding | Total owed: unpaid billed statements + the open cycle's charges | Money |
creditBalance | Money in the cardholder's favor (overpayment / reversal excess) | Money |
availableCredit | creditLimit − outstanding + creditBalance — derived, never stored | Money |
expiresOn | Card validity date (openedOn + terms.validityYears) | Date |
currentStatementId | The single open statement (current cycle) | UUID |
terms | Frozen copy of the plan's terms — see Card Products | Object |
Card Status
| Status | Meaning |
|---|---|
pending | Created, not yet activated. No purchases, no payments. |
enabled | Operating: purchases and payments allowed. |
disabled | Purchases rejected; payments still accepted. Billing and interest continue. |
closed | Terminal. Requires a settled book (outstanding − creditBalance exactly zero). |
Billing Cycles
- The account always has exactly one open statement. Purchases land there and are never due mid-cycle — that is the grace period.
- At the cut-off (
closeDate, inclusive) the statement is billed: itsdueAmountandminDuefreeze, and the next cycle opens. Cycle dates always chain from the previous cut-off, so cycles stay contiguous even if a sweep runs late. - The due date is
closeDate + termDays. - Unpaid balances are not re-billed into later cycles: every statement keeps
its own
dueAmount/paidAmount. Interest and late fees charged against a statement post on the current open statement (they appear on the next bill, like a real card) and therefore never compound.
Statement Status
| Status | Meaning |
|---|---|
open | Current cycle, nothing due yet |
closed | Billed at cut-off, awaiting payment |
overdue | Past the due date with money still owing |
paid | Remaining due reached zero (or the cycle billed nothing) |
Money Rules
- Credit is revolving: a payment reduces
outstandingimmediately, so the available credit is restored in the same transaction — no waiting for the cut-off. - Payments apply oldest-unpaid-first across billed statements; the remainder
becomes
creditBalance. The payment records the split inapplications. - Purchases over the available credit are rejected with
422 insufficient_credit, checked under a row lock so concurrent purchases cannot jointly overrun the limit. - Interest is daily, from the day after a statement's due date on its unpaid balance, until it is paid. The base is the statement's own remaining due, so interest never accrues on interest.
- The late fee is one-shot per statement, charged only when payments through
the due date did not reach
minDue. Paying at or above the minimum avoids the fee; interest still accrues on the rest.
Idempotency
Every write is idempotent — a replay returns the existing record with 200
instead of creating a twin.
| Operation | Key |
|---|---|
| Open account | referenceId (optional) |
| Purchase / payment | referenceId (required) |
| Purchase reversal | one reversal per purchase |
| Interest / late fee | one per statement per day / one per statement |
The Idempotency-Key header is additionally honored on purchases, payments and
reversals.
Endpoints
Open a Card Account
Creates the account in pending with its first open statement.
Body:
| Field | Description | Type |
|---|---|---|
clientId | Client identifier — see Which client id to send | String |
creditLimit | Credit limit | Money |
productId | Plan to use. Omitted ⇒ the tenant's default plan | UUID |
referenceId | Partner's id for the account (idempotency) | String |
termsOverride | Per-account overrides of the plan terms | Object |
openedOn | Backdates the account for historical imports (YYYY-MM-DD) | Date |
metadata | Free-form | Object |
Returns 201 with the Card Account; a replay of the same referenceId returns
200 with the existing one.
Which client id to send
clientId is resolved, never taken at face value, through the same chain
every other {id} surface uses:
native id > legacy_id > external_id
It matters because one customer can carry up to four identifiers and the caller rarely holds the native one:
| Identifier | Who uses it |
|---|---|
id | altledger's own id, minted when the client is created here |
legacy_id | the legacy Mongo _id — this is what borrower-central carries in borrower.cmsClientIds[0], so it is what the panel sends |
external_id | the partner's own code (PULPO-0003) |
cms_client_id | a third id, for clients imported with one |
A UUID is not necessarily the native id
For a client minted in altledger, the UUID the panel sends is its legacy_id.
A well-formed UUID used to be assumed to be the native id, so the same client
failed with client <uuid> not found in tenant by UUID while it worked by
external_id. Both paths — create and GET /v1/card-accounts?clientId= — now
resolve the same way.
cms_client_id is not in the chain, and nothing is lost by that: all 1,151
production rows that have a value carry the literal "0", so the column holds
no real identifier. The CMS-facing id callers actually hold is the legacy Mongo
_id, and that is the legacy_id.
Returns 404 when nothing matches and 412 when the value matches more than one
client only via a reference column.
Get / List Card Accounts
Change the Credit Limit
Body: { "creditLimit": { "amount": "15000.00", "currency": "USD" } }.
Lowering it below the current usage is allowed — the available credit goes
negative and purchases are rejected until the cardholder pays down.
Activate, Suspend or Close
A suspended card still accepts payments. Closing requires a settled book.
Authorizations (hold cupo before the purchase)
For a processor that authorizes a purchase and compensates it later. An authorization can be approved and never compensated (timeouts, expiry), so the purchase is not booked at authorization: the authorization only retains cupo, and the purchase is booked when the compensation arrives. The statement only ever shows purchases that were compensated.
Authorize — body:
| Field | Description | Type |
|---|---|---|
referenceId | Required. Your authorization id; idempotent per card (a replay returns 200 with the same authorization) | String |
amount | Amount to retain | Money |
creditLineId | Company mode: the line it retains on (required when the card has live lines) | UUID |
expiresAt | RFC 3339. Default now + 7 days, at most 30 days ahead | String |
metadata | Free-form | Object |
The checks are a purchase's, without force: the card must be enabled and not
expired, the line enabled, and the amount must fit the line and the card
(both already net of other active holds), refused with the same
422 insufficient_line_credit / insufficient_credit and details.scope. It
creates no transaction and no debt.
Capture — body (all optional): referenceId (the purchase's key; default
the authorization's), amount (what the compensation settled; default the
authorized amount), date, metadata, force. It answers 201 with the
purchase, which carries metadata.authorizationId.
- The hold is released before the purchase is checked, in the same operation, so the reserved cupo is never counted twice.
- Up to the authorized amount it books without a cap check: that cupo was
reserved and the money already moved. If interest ate the card's room
meanwhile, it still books and is marked
overLimit. Only the part beyond the authorized amount must fit (or come withforce). - A card or line disabled after authorizing still captures; a closed one does not.
- A card that expired after authorizing still captures: expiry is judged on
the day the authorization was issued (
createdAt, UTC), not on the capture date, because the processor settles days later. An authorization can only be issued while the card is valid, and a direct purchase afterexpiresOnis still422 card_expired. - An expired authorization can still be captured (a late compensation), but nothing was reserved anymore, so the whole amount is checked like a new purchase.
- Replaying the same capture (same
referenceId, or none) returns the same purchase (200), so a retry never books it twice:Idempotency-Keyis accepted but not needed. A second capture with a differentreferenceId, or capturing a voided authorization, is412. A capture refused by the caps leaves the hold in place.
Void — body { "reason" } (optional). Releases the hold. Voiding twice is a
no-op; a captured authorization cannot be voided (412: reverse the purchase).
| Status | Meaning |
|---|---|
active | Retaining cupo |
captured | Booked as a purchase (capturedTransactionId) |
voided | Released without a purchase |
expired | Past expiresAt without being captured: retains nothing |
List — GET …/authorizations?status=active returns only the ones still
retaining; status takes any value of the table above (omit it for all, any
other value is 400). Newest first, paged with limit / offset.
Where holds show up: each credit line carries held, already subtracted
from availableCredit and effectiveAvailable (consumed stays purchases
only); GET …/summary carries the card's total held, already subtracted from
availableCredit and not part of outstanding — a hold is not debt.
Register a Purchase
| Field | Description | Type |
|---|---|---|
referenceId | Required idempotency key | String |
amount | Purchase amount | Money |
date | Business date. Omitted ⇒ today (YYYY-MM-DD) | Date |
metadata | description is shown on the statement | Object |
force | Book it even if it breaks a cap (see below) | Boolean |
creditLineId | Company mode: the credit line that spent | UUID |
412 if the card is not enabled; 422 insufficient_credit if it exceeds the
available credit. The date must make sense for the card — see
Transaction Dates.
force: true books a purchase that does not fit — for purchases a processor
already authorized and settled, where refusing would only keep real money out of
the ledger. The card goes into overdraft (availableCredit negative) and the
purchase is stamped metadata.overLimit: true with overLimitAmount, plus a
card.purchase.over-limit event. A purchase that fits is never marked, even with
force. It skips the cap, never the card's status nor its expiry.
On a company card with credit lines, the purchase must carry creditLineId and
fit both the line and the card. See Credit Lines.
Reverse a Purchase
Body: optional amount (omit for a full reversal) and reason. Transactions are
immutable, so the reversal is a new compensating transaction — one per
purchase, full or partial:
- If the purchase's statement is still open, the reversal voids it inside the cycle and the purchase never gets billed.
- If the statement was already billed, the reversal applies against that
statement like a targeted payment (and can mark it
paid, stopping its daily interest). Any excess becomes credit balance.
Interest or late fees already charged for past days are not retro-reverted. The
reversal's date cannot be earlier than the purchase's (422 card_tx_before_purchase).
Register a Payment
Same body as a purchase. Applies oldest-unpaid-first; statements it zeroes become
paid, which stops their daily interest from that date. The response carries the
split:
(the remaining 100.00 became credit balance).
The order across statements is the plan's paymentAllocation (oldest cycle
first by default) and when the paid cupo is usable again is its
creditRestoration — see Payment policies.
On a company card the payment also frees cupo on the lines, and an optional
creditLineId directs that cupo to one line — see
Credit Lines. The diagrams of
every flow are in Card Flows.
Reverse a Payment
Body (both optional): { "reason": "duplicated by the reconciliation file", "date": "2026-10-02" }.
For a payment loaded by mistake, duplicated, or imputed to the wrong card. It
undoes exactly what the payment did: each statement it settled gets the
amount back as owed (a statement it had left paid goes back to closed, or to
overdue if the reversal is past its due date), and what had gone to credit
balance is taken back. The result is a new payment_reversal transaction.
- Always the whole payment. A payment that should have been smaller is reversed and registered again with the right amount.
- One reversal per payment: repeating the call returns the existing reversal.
- The
datecannot be earlier than the payment's. - Not on a
closedcard (412). 422 payment_credit_consumed— part of the payment went to credit balance and a later cut-off already used it.422 payment_has_charge_corrections— the payment arrived late and gave back interest or late fees when it was registered.- Interest and late fees are not recalculated retroactively: the debt counts as owed again from the reversal day on.
Account Summary
The cardholder's position right now: creditLimit, outstanding,
creditBalance, availableCredit, dueNow, minDueNow, oldestDueDate,
daysPastDue, cycleStartDate, nextCutoffDate, nextDueDate, cycleSpend
(this period's consumption) and expiresOn.
Statements and Transactions
Transaction types: purchase, payment, interest, penalty (late fee),
reversal and payment_reversal. Interest and late fees carry
metadata.sourceStatementId pointing at the cycle that owed the money; reversals
carry metadata.sourceTransactionId. On a company card, a purchase and its
reversal carry creditLineId.
Each statement in the list carries charged: what that cycle charged, the
same figures as its statement document.
Charges count where they are charged, not where they come from
Interest and late fees generated by an unpaid cycle are charged on a
later one. In the example, the 99.00 of interest and the 225.00 late fee
were generated by the July cycle and charged on this one: 4,500 + 99 + 225 =
4,824. Summing charges by sourceStatementId answers a different question
and does not add up to dueAmount.
Statement Document (estado de cuenta)
The composed statement of one cycle in a single payload — issuer, cardholder,
card, period, totals and the itemized lines — so a renderer never has to stitch
calls or re-derive money. .csv returns the same document as a downloadable
attachment (debits positive, credits negative, so summing the amount column
reconciles). .pdf returns it as a ready-to-send PDF with the same layout the
panel downloads: header, amount due and due date, the two blocks that reconcile
the totals, and the movements, paged. ?lang=es (default) or en; any other
value is 400. Both files are named estado-de-cuenta-{tail}-{closeDate}.
How to read it:
- Debits are what this cycle charged: purchases, interest, late fees and
same-cycle voids posted on it — the same set the cut-off freezes into
dueAmount. Interest and late fees are charged on the cycle that is open when they accrue (the one after the overdue cycle that generated them); their line carriessourceStatementId, the cycle that generated them. - Credits are the payments and reversals applied to this cycle.
- On every billed cycle the block closes exactly:
purchases + interest + penalties − reversals − creditApplied = dueAmount, anddueAmount − paidAmount = remainingDue.creditAppliedis the credit balance the cut-off consumed. - Each line carries the raw
reference. A company-card purchase or reversal also carriescreditLineIdandcreditLineLabel; interest and late fees carry no line — they are charged to the card. totals.byCreditLineis the cycle's consumption per line (purchases, reversals, net). Interest and late fees are never split per line.
previousBalance is what the older cycles still owed as of this cut-off. It is
computed when the document is built (unpaid balances are never re-billed) and it
is informational — it is not part of this cycle's dueAmount.
Tax (VAT) on interest and late fees
When the plan sets interestTax (and optionally penaltyTax), every interest or
late-fee charge stores its own tax and the document breaks it out:
| Total | What it is |
|---|---|
interest / penalties | Gross: tax included, because that is what the cardholder owes and what dueAmount is built from |
interestTax / penaltyTax | The tax included in each of the two above |
taxes | interestTax + penaltyTax for the period |
Each interest/late-fee line also carries its own tax; a line's net charge is
amount − tax. Every other line (purchases, payments, reversals) carries a zero
tax.
Tax is rounded per charge
Interest accrues daily, so the tax is computed and rounded per charge (half-even). The period total is the sum of the lines — the way a line-level VAT invoice adds up. Re-applying the rate to the aggregate interest can differ by cents: the document reports the sum, not the recomputation.
The payment reference is still missing
The document does not yet include a payment reference or coupon. It is deliberately not derived: a number the API invents is registered with no collection network and cannot be paid. The shape will be an opaque per-account reference set by the partner, plus the tenant's configured payment instructions.
Account History
The card's full audit trail, oldest first: statements, purchases, payments,
accruals and every credit-line change. It is the only place to see the
credit-limit history: each card.account.limit-updated keeps the previous
and the new limit, and every event says who did it.
actor.type is user (a person), service (a service account) or system
(the billing and accrual sweeps). Events written before 2026-09-30 carry no
actor. Setting a value that does not change anything writes no event.
Transaction Dates
A purchase, payment or reversal carries an optional date (YYYY-MM-DD,
today when omitted). It has to make sense for the card:
| Rule | Error (422) |
|---|---|
| Not after today | card_tx_date_in_future |
| A purchase or payment: not before the card opened (the first day of its first cycle) | card_tx_before_opening |
| A purchase reversal: not before the purchase | card_tx_before_purchase |
A purchase: not after the card's expiresOn — force does not skip it. A capture is judged on the day its authorization was issued | card_expired |
A card expires on its opening date plus the plan's validityYears (3 by
default). An expired card takes no purchases but still takes payments and
reversals: its debt keeps being collected and corrected.
Backdated purchases inside the card's life are accepted
A purchase dated in an already closed cycle (a late presentment) is accepted on purpose: it books into the open cycle with its original date.
Permissions
| Permission | Grants |
|---|---|
card.account.read | accounts, summary, statements, transactions, statement document, history |
card.account.write | open an account, change the limit, enable/disable/close |
card.purchase.create | register purchases and reverse them |
card.payment.create | register payments and reverse them |
Sending X-Test-Mode: true operates on the tenant's sandbox, fully partitioned
from production data.
Error Handling
| Status | When |
|---|---|
400 | Validation: missing referenceId, non-positive amount, currency mismatch, bad dates |
404 | Unknown account, statement or transaction |
412 | Wrong state: purchase on a non-enabled card, closing with debt, reversing a non-purchase |
422 insufficient_credit | The purchase exceeds the available credit |
422 card_tx_date_in_future / card_tx_before_opening / card_tx_before_purchase / card_expired | The date does not make sense for the card — see Transaction Dates |
422 card_client_has_card | One-card-per-client mode, and the client already has a live card |
409 concurrent_modification | Another operation changed the same card at the same time; retry after Retry-After |
422 (lines) | See Credit Lines |
200 on replay | A duplicate referenceId returns the existing record instead of failing |