KTAP Cards API
API reference

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.

KindSignWhat it is
fundingPositive, or negative on a reversalMoney 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>
feeNegative, or positive on a reversalA 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_topupNegativeOne row per top-up. The reference is topup: followed by your idempotency key
refundPositiveA balance returned on closing a card, or a top-up the card processor definitively rejected
adjustmentZeroA 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_usd is exactly provider_fee_usd plus client_fee_usd.
  • provider_fee_usd is the fee inside amount_usd, as the card processor itemizes it, to the cent. client_fee_usd is your markup, computed on fee_basis_usd; it is not part of amount_usd.
  • fee_breakdown itemises our charge only, so fx_usd plus atm_usd plus deposit_usd equals provider_fee_usd exactly and does not sum to fee_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_usd is in USD but built from cent-rounded figures, so it is too coarse for a small purchase. Use amount_usd and fee_usd on the purchase's card.authorized event 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's card.authorized event. 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.

GET/api/partner/v1/ledger#

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.

Scope users:read
Query parameters
limitinteger
Entries per page
1 to 100. Out of range returns A value above 100 returns 400. A value that is not a run of digits, including a negative number, a decimal, an empty value or any text, returns 400. The value 0 passes validation and is clamped up to 1·Defaults to 50
pageinteger
1-based page number, offset as (page - 1) * limit
1 or greater, no upper bound. Out of range returns A value that is not a run of digits returns 400. The value 0 passes validation and is clamped up to 1. A page past the end returns an empty entries array, not an error·Defaults to 1
Response
entriesarray
The page of rows, ordered by created_at descending and then by internal id descending
amount_usdnumber
Signed USD. Positive adds to your balance, negative draws it down. Carries up to 10 decimal places
kindstring
What the row is. The column is constrained to these five values in the database
funding · fee · card_topup · refund · adjustment
referencestring | null
The identifier that ties the row to its cause. See notes for the exact shape per kind
card_idstring | null
The card the row concerns. Null on funding rows, load fee rows, monthly card fee rows, top-up reversal rows and card order claims
descriptionstring | null
Human-readable label for the row. On a card order claim this field holds the id of the card the order produced
created_atstring
ISO 8601 UTC with milliseconds and a Z suffix
countinteger
Rows on this page
pageinteger
The page returned, after clamping
totalinteger
Rows across all pages for your account. 0 when the requested page is past the end
total_pagesinteger
Pages at the requested limit. Never below 1
Errors
400BAD_REQUEST
limit or page contains anything other than digits, or limit is greater than 100
Fix the parameter and resend. Retrying the same request produces the same error
401UNAUTHORIZED
No key was presented, or the key is unknown, revoked, or belongs to a suspended account
Do not retry. Check the key, then contact KTAP
403FORBIDDEN
The key is valid but does not carry users:read
Do not retry. Ask KTAP to add the scope, or use a key that has it
405BAD_REQUEST
Any method other than GET is used on /ledger
Do not retry. Use GET
429RATE_LIMITED
The key exceeded its per-minute allowance
Wait the number of seconds in Retry-After, which is always 60, then resend
500UNAVAILABLE
An unhandled fault on the KTAP side, for example a database error
Retry after a short pause. The endpoint is read-only, so a retry cannot duplicate anything
Notes
  • Summing amount_usd across every page equals float.balance_usd on GET /me.
  • amount_usd carries up to ten decimal places. Parse it as an exact decimal, not as cents.
  • adjustment rows 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
  }
}
GET/api/partner/v1/cards/{cardId}/transactions#

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.

Scope cards:read
Path parameters
cardIdstring
The card id returned by POST /cards
Opaque and case-sensitive. Live ids are 32 characters of letters and digits. Match is exact and scoped to your partner account
Query parameters
limitinteger
Rows per page
1 to 25. Out of range returns Anything that is not a run of digits gives 400 BAD_REQUEST. A value above 25 gives 400 BAD_REQUEST and is never clamped. A value of 0 is accepted and coerced to 1·Defaults to 25
pageinteger
Page number, 1-based
1 or greater. Out of range returns Anything that is not a run of digits gives 400 BAD_REQUEST. A value of 0 is accepted and coerced to 1. A page beyond total_pages returns 200 with an empty transactions array·Defaults to 1
from_datestring
Earliest calendar date to include, YYYY-MM-DD
Must match ^\d{4}-\d{2}-\d{2}$ and be a real date. 2026-02-30 and 2026-13-01 are refused. Out of range returns 400 BAD_REQUEST. Also 400 when from_date is later than to_date. When omitted, no lower bound is sent to the processor
to_datestring
Latest calendar date to include, YYYY-MM-DD
Must match ^\d{4}-\d{2}-\d{2}$ and be a real date. Out of range returns 400 BAD_REQUEST. Also 400 when from_date is later than to_date. When omitted, no upper bound is sent to the processor
Response
card_idstring
The card id you requested, as stored
transactionsarray
Transaction objects for this page
idstring
This row's own movement id, for example a1c4e7f0. Stable, but no webhook carries it. Join webhooks on transaction_id instead
transaction_idstring | null
The purchase id. Equal to transaction_id on the card.authorized, card.transaction and card.declined webhooks for the same purchase, so this is the field that pairs pulled history with delivered events
partner_refstring | null
Your own reference for the cardholder. Copied from the CARD record, so it is identical on every row and null when the card was issued without one
datetimestring
UTC timestamp with no offset and no Z, for example 2026-08-19T14:22:41. Append Z when parsing
statusstring
Derived by us. pending until the merchant settles; settled once a settlement has posted, whatever amount it settled, including a partial reversal followed by a settlement of the rest; declined when the purchase was refused; refunded, partially_refunded, reversed or partially_reversed from the purchase's outcome
pending · settled · declined · refunded · partially_refunded · reversed · partially_reversed
merchantstring | null
Merchant name as the processor reports it. Null when the processor sends none
amount_usdnumber
The purchase's current total as the card processor reports it, fee included, to the cent. It is not zeroed on a decline, where nothing left the card. A reversal lowers it
original_amountnumber | null
Amount in the currency the cardholder was charged in. Not USD unless original_currency is USD
original_currencystring | null
ISO currency of original_amount, for example CAD
fee_basis_usdnumber
amount_usd minus the fees inside it. Both are rounded to the cent, so this can be a cent away from what the merchant charged. Never negative, and 0 on a declined row. Too coarse to check a small purchase's fee against a rate
exchange_ratenumber | null
The card processor's rate between original_amount and the fee-inclusive amount, so on a USD purchase it sits above 1 and is not a market rate. amount_usd is rounded to the cent, so dividing it by original_amount may not reproduce this. Null when the processor sends none
provider_fee_usdnumber
The fees charged on this purchase, as the card processor itemizes them. Each component is rounded to the cent separately, so this can be a cent above or below the exact fee on card.authorized, and a fee under half a cent reads 0. 0 on a declined row
client_fee_usdnumber
Your markup: fee_basis_usd multiplied by your configured transaction markup, unrounded. It inherits the cent rounding of fee_basis_usd. 0 when no markup is set and 0 on a declined row
fee_usdnumber
provider_fee_usd + client_fee_usd exactly. 0 on a declined row
fx_usdnumber
Combined provider-side and client-side FX fee. USD purchase pricing is applied through the exchange rate and lands here, so a non-zero value does not prove a currency conversion happened
atm_usdnumber
Combined provider-side and client-side ATM fee
deposit_usdnumber
Combined provider-side and client-side deposit fee
decline_reasonobject | null
Null unless status is declined. Object with code, message, retryable and card_blocked
codestring
Closed KTAP enum of 14 values. Branch on this
insufficient_card_balance · issuer_funds_unavailable · daily_limit_exceeded · weekly_limit_exceeded · monthly_limit_exceeded · single_purchase_limit_exceeded · velocity_limit_exceeded · card_blocked · fraud_risk_declined · security_check_failed · merchant_category_blocked · processing_error · authorization_timeout · unknown
messagestring
KTAP-authored explanation, always present, safe to show a cardholder. Wording can change, so never branch on it
retryableboolean
True when the same purchase can succeed on a straight retry with no action
card_blockedboolean
True when the card is unusable until KTAP unblocks it. Stop retrying and contact support
countinteger
Rows in this page. Can be lower than limit even mid-history when a row could not be read, so compare it against total
pageinteger
The page returned, 1-based, after coercion of page=0 to 1
total_pagesinteger
Total pages for this filter, from the processor
totalinteger
Total purchases matching this filter across all pages, from the processor
Errors
400BAD_REQUEST
limit or page is not a run of digits, limit is above 25, from_date or to_date is not a real YYYY-MM-DD date, or from_date is later than to_date
No. Fix the parameter
400BAD_REQUEST
The card processor rejected the search with its own 400 or 412. A deterministic rejection, mapped to 400 so you do not retry it forever
No
401UNAUTHORIZED
No key, unknown key, revoked key, or your partner account is suspended
No. Fix the credential
403FORBIDDEN
The key is valid but lacks the cards:read scope
No. Ask for the scope
404NOT_FOUND
The card id does not exist, or belongs to another partner. Card status is not checked, so a closed card still returns its history
No
405BAD_REQUEST
Any method other than GET. Produced before authentication, so it is not rate limited or logged against your key
No
429RATE_LIMITED
Your key exceeded rate_limit_per_minute in a rolling one-minute window. The default allowance is 120 per minute
Yes, after the Retry-After value of 60 seconds
500UNAVAILABLE
The processor call failed at the transport level, including the 20 second client timeout, or an internal fault occurred. This is the shape a genuinely unreachable processor takes, not 502
Yes, shortly
502UNAVAILABLE
The processor returned an error status that is neither 401 nor 400 nor 412, including its 5xx responses and a 429 whose retries were exhausted after 5 attempts
Yes, shortly
503UPSTREAM_AUTH
The processor returned 401 to KTAP. A configuration problem on the KTAP side, never yours
Yes, shortly, then contact support
Notes
  • limit is capped at 25 here rather than 100. Page with page rather than asking for more rows.
  • A row whose detail lookup fails is dropped from the response, while total still counts it. On any page before the last, a count below your limit means rows are missing and the page is worth retrying.
  • pending is the normal state for a recent purchase. Its amount_usd has 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
  }
}
Ledger · KTAP Cards API