Ledger
Two histories: your balance, and what happened on a card. Reconcile against these rather than against a rate card.
Ledger row kinds#
GET /ledger is the movement history of your own balance. Five kinds are possible and the column is constrained to exactly those, so an unknown value never reaches you. Summing amount_usd across every page equals float.balance_usd on GET /me.
| Kind | Sign | What it is |
|---|---|---|
funding | Positive, or negative on a reversal | Money we credited after you sent funds. One submitted with POST /funding is referenced funding-<NETWORK>-<tx_hash>, and a reversal of one funding-reversal-<NETWORK>-<tx_hash> |
fee | Negative, or positive on a reversal | A load fee on a funding, referenced as the funding with :fee appended (a reversal returns it as a positive row), or a monthly card fee. The card id for a card fee is in the reference, not in card_id |
card_topup | Negative | One row per top-up. The reference is topup: followed by your idempotency key |
refund | Positive | A balance returned on closing a card, or a top-up the card processor definitively rejected |
adjustment | Zero | A bookkeeping marker. Always zero, and not counted in your balance |
One row per purchase#
A purchase appears once. It starts as pending: its amount_usd has already been charged to the card, fee included, but the merchant can still settle a different amount, and settlement takes a median of around 22 hours. Refunds and reversals do not add a row: the original row changes status, and its amount_usd is the card processor's current figure, so a reversal lowers it. Take the exact returned amount from the `card.transaction` webhook, matched on transaction_id.
Fees on a transaction#
fee_usdis exactlyprovider_fee_usdplusclient_fee_usd.provider_fee_usdis the fee insideamount_usd, as the card processor itemizes it, to the cent.client_fee_usdis your markup, computed onfee_basis_usd; it is not part ofamount_usd.fee_breakdownitemises our charge only, sofx_usdplusatm_usdplusdeposit_usdequalsprovider_fee_usdexactly and does not sum tofee_usd.- Never check a rate against
original_amount: it is in the cardholder's purchase currency, so multiplying it by a USD rate is wrong by the exchange rate.fee_basis_usdis in USD but built from cent-rounded figures, so it is too coarse for a small purchase. Useamount_usdandfee_usdon the purchase'scard.authorizedevent for that. - Every fee field on a declined row is zero. Do not book revenue or cost from a decline.
- Row amounts and fees are the card processor's figures rounded to the cent, each fee component rounded separately, so a fee under half a cent reads
0. The exact fee is on the purchase'scard.authorizedevent. Store amounts in a decimal type and sum before you round.
decline_reason maps the card processor's raw code to a closed set of our own, falling back to unknown, so an exhaustive switch is safe and no processor vocabulary reaches you. retryable means the same purchase can succeed on a straight retry; card_blocked means the card is unusable until we unblock it.
Returns one row for every event that changed your prepaid balance, newest first. Each row carries a signed USD amount, a kind, and a reference that ties it back to the call or the charge that produced it. Page with limit and page until page equals total_pages.
limitintegerpageintegerentriesarrayamount_usdnumberkindstringreferencestring | nullcard_idstring | nulldescriptionstring | nullcreated_atstringcountintegerpageintegertotalintegertotal_pagesintegerBAD_REQUESTUNAUTHORIZEDFORBIDDENBAD_REQUESTRATE_LIMITEDUNAVAILABLE- Summing
amount_usdacross every page equalsfloat.balance_usdon GET /me. amount_usdcarries up to ten decimal places. Parse it as an exact decimal, not as cents.adjustmentrows are bookkeeping markers, always zero, and are not counted in your balance.
curl -sS -X GET 'https://virtual.ktap.io/api/partner/v1/ledger?limit=50' \
-H 'X-API-Key: ktap_live_...'{
"data": {
"entries": [
{
"amount_usd": 43.2,
"kind": "refund",
"reference": "close:7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
"card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
"description": "Balance returned on closing ••••7640",
"created_at": "2026-08-19T11:02:44.560Z"
},
{
"amount_usd": 0,
"kind": "adjustment",
"reference": "topup:order-8842:done",
"card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
"description": "Top-up confirmed by processor",
"created_at": "2026-08-04T16:41:12.913Z"
},
{
"amount_usd": -200,
"kind": "card_topup",
"reference": "topup:order-8842",
"card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
"description": "Top-up card ••••7640",
"created_at": "2026-08-04T16:41:11.402Z"
},
{
"amount_usd": -2,
"kind": "fee",
"reference": "cardfee:7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u:2026-08-01",
"card_id": null,
"description": "Card fee 2026-08",
"created_at": "2026-08-04T16:41:09.774Z"
},
{
"amount_usd": -100,
"kind": "fee",
"reference": "wire-20260801-0042:fee",
"card_id": null,
"description": "Load fee on $10000 funding",
"created_at": "2026-08-01T09:14:02.118Z"
},
{
"amount_usd": 10000,
"kind": "funding",
"reference": "wire-20260801-0042",
"card_id": null,
"description": "Float funding",
"created_at": "2026-08-01T09:14:02.118Z"
}
],
"count": 6,
"page": 1,
"total": 6,
"total_pages": 1
}
}Returns purchase activity for one card, one row per purchase, with the fee breakdown, the derived status and a decline reason where the purchase was refused. Only the PURCHASE operation is queried, so approvals, declines, refunds and reversals all appear as lifecycle outcomes on the original purchase row rather than as separate rows. Amounts and the processor's fees are reported to the cent, as the card processor itemizes them. client_fee_usd, and so fee_usd, is computed unrounded from those figures. Fees are zero on every declined row.
cardIdstringlimitintegerpageintegerfrom_datestringto_datestringcard_idstringtransactionsarrayidstringtransaction_idstring | nullpartner_refstring | nulldatetimestringstatusstringmerchantstring | nullamount_usdnumberoriginal_amountnumber | nulloriginal_currencystring | nullfee_basis_usdnumberexchange_ratenumber | nullprovider_fee_usdnumberclient_fee_usdnumberfee_usdnumberfx_usdnumberatm_usdnumberdeposit_usdnumberdecline_reasonobject | nullcodestringmessagestringretryablebooleancard_blockedbooleancountintegerpageintegertotal_pagesintegertotalintegerBAD_REQUESTBAD_REQUESTUNAUTHORIZEDFORBIDDENNOT_FOUNDBAD_REQUESTRATE_LIMITEDUNAVAILABLEUNAVAILABLEUPSTREAM_AUTHlimitis capped at 25 here rather than 100. Page withpagerather than asking for more rows.- A row whose detail lookup fails is dropped from the response, while
totalstill counts it. On any page before the last, acountbelow yourlimitmeans rows are missing and the page is worth retrying. pendingis the normal state for a recent purchase. Itsamount_usdhas already been charged to the card, fee included and rounded to the cent here, but the merchant can still settle a different amount.
curl -sS -X GET 'https://virtual.ktap.io/api/partner/v1/cards/7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u/transactions?limit=25' \
-H 'X-API-Key: ktap_live_...'{
"data": {
"card_id": "7Kd2mQ4rTz9XbW1sYh6vN0pLcE3fJa8u",
"transactions": [
{
"id": "a1c4e7f0",
"transaction_id": "tid_01a07c3e-4f5a-7b6c-9d8e-0f1a2b3c4d5e",
"partner_ref": "your-user-8842",
"datetime": "2026-08-19T14:22:41",
"status": "settled",
"merchant": "CORNER CAFE",
"amount_usd": 25.5,
"original_amount": 25,
"original_currency": "USD",
"fee_basis_usd": 25,
"exchange_rate": 1.02,
"provider_fee_usd": 0.5,
"client_fee_usd": 0.5,
"fee_usd": 1,
"fee_breakdown": {
"fx_usd": 0.5,
"atm_usd": 0,
"deposit_usd": 0
},
"decline_reason": null
},
{
"id": "b2d5f8a1",
"transaction_id": "tid_01a0b2d9-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"partner_ref": "your-user-8842",
"datetime": "2026-08-19T12:51:08",
"status": "pending",
"merchant": "UBER EATS",
"amount_usd": 10.2,
"original_amount": 10,
"original_currency": "USD",
"fee_basis_usd": 10,
"exchange_rate": 1.02,
"provider_fee_usd": 0.2,
"client_fee_usd": 0.2,
"fee_usd": 0.4,
"fee_breakdown": {
"fx_usd": 0.2,
"atm_usd": 0,
"deposit_usd": 0
},
"decline_reason": null
},
{
"id": "c3e6a9b2",
"transaction_id": "tid_01a0b2e7-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
"partner_ref": "your-user-8842",
"datetime": "2026-08-19T11:07:03",
"status": "declined",
"merchant": "STEAMGAMES.COM",
"amount_usd": 12.24,
"original_amount": 12,
"original_currency": "USD",
"fee_basis_usd": 0,
"exchange_rate": 1.02,
"provider_fee_usd": 0,
"client_fee_usd": 0,
"fee_usd": 0,
"fee_breakdown": {
"fx_usd": 0,
"atm_usd": 0,
"deposit_usd": 0
},
"decline_reason": {
"code": "insufficient_card_balance",
"message": "The card balance did not cover the purchase.",
"retryable": false,
"card_blocked": false
}
}
],
"count": 3,
"page": 1,
"total_pages": 29,
"total": 87
}
}