AltScore
Credit Management System (CMS)

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

{
  "id": "fd44532b-f5f6-493b-baab-8dda053c9cff",
  "clientId": "0187f2d0-471e-40ea-90a2-64d559a40577",
  "productId": "dbb59052-955c-456c-9be5-6041df34f60e",
  "referenceId": "partner-acct-001",
  "status": "enabled",
  "creditLimit": { "amount": "10000.00", "currency": "USD" },
  "currency": "USD",
  "currentStatementId": "712043dc-24c6-45b1-9a58-fd72548fe4dc",
  "outstanding": { "amount": "6255.09", "currency": "USD" },
  "creditBalance": { "amount": "0.00", "currency": "USD" },
  "availableCredit": { "amount": "3744.91", "currency": "USD" },
  "expiresOn": "2029-02-06",
  "terms": { "billingPeriod": { "unit": "day", "count": 1, "monthDays": [15] }, "termDays": 10 },
  "version": 7
}

Attributes

AttributeDescriptionType
idCard account idUUID
clientIdThe client the card belongs toUUID
productIdCard product (plan) the terms were copied fromUUID
referenceIdPartner's own id — creation idempotency keyString
statusSee Card StatusString
creditLimitCredit limit (cupo máximo), enforced on every purchaseMoney
outstandingTotal owed: unpaid billed statements + the open cycle's chargesMoney
creditBalanceMoney in the cardholder's favor (overpayment / reversal excess)Money
availableCreditcreditLimit − outstanding + creditBalance — derived, never storedMoney
expiresOnCard validity date (openedOn + terms.validityYears)Date
currentStatementIdThe single open statement (current cycle)UUID
termsFrozen copy of the plan's terms — see Card ProductsObject

Card Status

StatusMeaning
pendingCreated, not yet activated. No purchases, no payments.
enabledOperating: purchases and payments allowed.
disabledPurchases rejected; payments still accepted. Billing and interest continue.
closedTerminal. 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: its dueAmount and minDue freeze, 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

StatusMeaning
openCurrent cycle, nothing due yet
closedBilled at cut-off, awaiting payment
overduePast the due date with money still owing
paidRemaining due reached zero (or the cycle billed nothing)

Money Rules

  • Credit is revolving: a payment reduces outstanding immediately, 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 in applications.
  • 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.

OperationKey
Open accountreferenceId (optional)
Purchase / paymentreferenceId (required)
Purchase reversalone reversal per purchase
Interest / late feeone 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.

POST /v1/card-accounts

Body:

FieldDescriptionType
clientIdClient identifier — see Which client id to sendString
creditLimitCredit limitMoney
productIdPlan to use. Omitted ⇒ the tenant's default planUUID
referenceIdPartner's id for the account (idempotency)String
termsOverridePer-account overrides of the plan termsObject
openedOnBackdates the account for historical imports (YYYY-MM-DD)Date
metadataFree-formObject

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:

IdentifierWho uses it
idaltledger's own id, minted when the client is created here
legacy_idthe legacy Mongo _id — this is what borrower-central carries in borrower.cmsClientIds[0], so it is what the panel sends
external_idthe partner's own code (PULPO-0003)
cms_client_ida 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

GET /v1/card-accounts?clientId=&status=&limit=&offset=
GET /v1/card-accounts/{id}

Change the Credit Limit

PUT /v1/card-accounts/{id}/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

POST /v1/card-accounts/{id}/enable
POST /v1/card-accounts/{id}/disable    {"reason": "..."}
POST /v1/card-accounts/{id}/close      {"reason": "..."}

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.

POST /v1/card-accounts/{id}/authorizations
GET  /v1/card-accounts/{id}/authorizations?status=&limit=&offset=
GET  /v1/card-accounts/{id}/authorizations/{authId}
POST /v1/card-accounts/{id}/authorizations/{authId}/capture
POST /v1/card-accounts/{id}/authorizations/{authId}/void

Authorize — body:

FieldDescriptionType
referenceIdRequired. Your authorization id; idempotent per card (a replay returns 200 with the same authorization)String
amountAmount to retainMoney
creditLineIdCompany mode: the line it retains on (required when the card has live lines)UUID
expiresAtRFC 3339. Default now + 7 days, at most 30 days aheadString
metadataFree-formObject
201
{ "id": "6f1d6a1e-0000-4000-8000-000000000020", "accountId": "…",
  "creditLineId": "…", "referenceId": "pulpo-auth-8842",
  "amount": { "amount": "300.00", "currency": "USD" },
  "status": "active", "expiresAt": "2026-10-22T21:43:01Z", "createdAt": "…" }

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 with force).
  • 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 after expiresOn is still 422 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-Key is accepted but not needed. A second capture with a different referenceId, or capturing a voided authorization, is 412. 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).

StatusMeaning
activeRetaining cupo
capturedBooked as a purchase (capturedTransactionId)
voidedReleased without a purchase
expiredPast 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

POST /v1/card-accounts/{id}/purchases
FieldDescriptionType
referenceIdRequired idempotency keyString
amountPurchase amountMoney
dateBusiness date. Omitted ⇒ today (YYYY-MM-DD)Date
metadatadescription is shown on the statementObject
forceBook it even if it breaks a cap (see below)Boolean
creditLineIdCompany mode: the credit line that spentUUID

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

POST /v1/card-accounts/{id}/purchases/{txId}/reverse

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

POST /v1/card-accounts/{id}/payments

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:

{
  "transactionId": "…",
  "type": "payment",
  "amount": { "amount": "800.00", "currency": "USD" },
  "applications": [
    { "statementId": "…jul", "amount": { "amount": "300.00", "currency": "USD" } },
    { "statementId": "…aug", "amount": { "amount": "400.00", "currency": "USD" } }
  ]
}

(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

POST /v1/card-accounts/{id}/payments/{txId}/reverse

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 date cannot be earlier than the payment's.
  • Not on a closed card (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

GET /v1/card-accounts/{id}/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

GET /v1/card-accounts/{id}/statements
GET /v1/card-accounts/{id}/statements/current
GET /v1/card-accounts/{id}/transactions
GET /v1/card-statements/{id}
GET /v1/card-statements/{id}/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.

{
  "startDate": "2026-07-15", "closeDate": "2026-08-14", "dueDate": "2026-08-24",
  "status": "overdue",
  "dueAmount": { "amount": "4824.00", "currency": "USD" },
  "charged": {
    "purchases": { "amount": "4500.00", "currency": "USD" },
    "interest":  { "amount": "99.00",   "currency": "USD" },
    "penalties": { "amount": "225.00",  "currency": "USD" }
  }
}

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)

GET /v1/card-statements/{id}/document
GET /v1/card-statements/{id}/document.csv
GET /v1/card-statements/{id}/document.pdf?lang=es

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}.

{
  "issuer":     { "tenant": "…", "name": "…", "taxId": "…" },
  "cardholder": { "clientId": "…", "name": "…", "taxId": "…", "address": "…" },
  "card":       { "tail": "0539", "creditLimit": {}, "availableCredit": {}, "expiresOn": "2029-07-01" },
  "period":     { "startDate": "2026-07-15", "closeDate": "2026-08-14", "dueDate": "2026-08-24", "minDueMet": false },
  "totals":     { "previousBalance": {}, "purchases": {}, "interest": {}, "penalties": {},
                  "interestTax": {}, "penaltyTax": {}, "taxes": {},
                  "payments": {}, "reversals": {}, "appliedReversals": {}, "paymentReversals": {},
                  "creditApplied": {}, "dueAmount": {}, "paidAmount": {}, "remainingDue": {},
                  "minDue": {}, "minDueRemaining": {},
                  "byCreditLine": [ { "creditLineId": "…", "label": "Flota Norte",
                                      "purchases": {}, "reversals": {}, "net": {} } ] },
  "lines": [
    { "date": "2026-07-25", "type": "penalty", "concept": "Late fee",
      "reference": "pen:6f1d6a1e-0000-4000-8000-000000000014",
      "amount": { "amount": "225.00", "currency": "USD" }, "tax": {}, "debit": true,
      "sourceStatementId": "6f1d6a1e-0000-4000-8000-000000000014" },
    { "date": "2026-08-05", "type": "purchase", "concept": "Purchase", "reference": "pos-000123",
      "amount": { "amount": "1500.00", "currency": "USD" }, "tax": {}, "debit": true,
      "creditLineId": "6f1d6a1e-0000-4000-8000-000000000001", "creditLineLabel": "Flota Norte" }
  ]
}

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 carries sourceStatementId, 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, and dueAmount − paidAmount = remainingDue. creditApplied is the credit balance the cut-off consumed.
  • Each line carries the raw reference. A company-card purchase or reversal also carries creditLineId and creditLineLabel; interest and late fees carry no line — they are charged to the card.
  • totals.byCreditLine is 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:

TotalWhat it is
interest / penaltiesGross: tax included, because that is what the cardholder owes and what dueAmount is built from
interestTax / penaltyTaxThe tax included in each of the two above
taxesinterestTax + 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

GET /v1/card-accounts/{id}/events
GET /v1/card-accounts/{id}/events?type=card.account.limit-updated

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.

{
  "id": 918273,
  "type": "card.account.limit-updated",
  "occurredAt": "2026-09-30T02:53:43Z",
  "data": {
    "previous":    { "amount": "1000.00", "currency": "USD" },
    "creditLimit": { "amount": "3000.00", "currency": "USD" },
    "actor": { "type": "user", "id": "…", "name": "Ana Operaciones" }
  }
}

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:

RuleError (422)
Not after todaycard_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 purchasecard_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 issuedcard_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

PermissionGrants
card.account.readaccounts, summary, statements, transactions, statement document, history
card.account.writeopen an account, change the limit, enable/disable/close
card.purchase.createregister purchases and reverse them
card.payment.createregister payments and reverse them

Sending X-Test-Mode: true operates on the tenant's sandbox, fully partitioned from production data.

Error Handling

StatusWhen
400Validation: missing referenceId, non-positive amount, currency mismatch, bad dates
404Unknown account, statement or transaction
412Wrong state: purchase on a non-enabled card, closing with debt, reversing a non-purchase
422 insufficient_creditThe purchase exceeds the available credit
422 card_tx_date_in_future / card_tx_before_opening / card_tx_before_purchase / card_expiredThe date does not make sense for the card — see Transaction Dates
422 card_client_has_cardOne-card-per-client mode, and the client already has a live card
409 concurrent_modificationAnother operation changed the same card at the same time; retry after Retry-After
422 (lines)See Credit Lines
200 on replayA duplicate referenceId returns the existing record instead of failing