KTAP Cards API
API reference

Account and balance

Your account, how you fund it, what you can spend, and what you are charged.

How your balance works#

You send us funds and we credit your balance, charging one load fee at that point. Everything after that draws on the balance: a top-up moves value onto a card at 1:1 with no further fee, and closing a card sweeps what is left back. GET /me reports it under float.

Two figures constrain a top-up and available_usd is the smaller of them. balance_usd is what you have funded, less everything drawn down. limit_usd is a ceiling we may set on how much card value you can have outstanding at once, and it is null when no ceiling applies. Fees reduce your balance but never consume limit headroom, because the limit is measured against card value alone.

Read available_usd, do not derive it

The figure the limit arm subtracts is not returned by this endpoint, so you cannot recompute available_usd from the payload. A top-up above it returns 402 rather than clamping.

Your rates are the ones on your agreement. Read them from GET /fees rather than hardcoding them, so a change reaches you without a deploy. The transaction rates there are a published rate card: reconcile against the fee each purchase actually carried: exactly on its card.authorized event, or to the cent on its row in GET /cards/{id}/transactions. Its settlement event does not repeat the fee.

Fund your balance#

  1. Before your first transfer, tell us through your contact or the support channel which wallet or wallets you will send from. We register them to your account, and the same address counts on every network. A wallet can belong to one account only, so never name an exchange's hot wallet or a wallet that relays or solves bridge transfers; our own wallets and card deposit addresses cannot be registered either. Registration is handled by us and is not visible through the API.
  2. Read your funding address from GET /funding/addresses just before you send. It lists each network you can fund on (Polygon, BNB Chain and Base) with the tokens accepted there (USDC and USDT), and is refreshed at least every 10 minutes. Never copy an address from these pages or send to a card address.
  3. Send a listed token from a registered wallet, directly to the address listed for the network you send on, at least 10 USD per transfer. Credit is automatic only when exactly one wallet paid into your funding address in that transaction, that wallet is registered to your account (or, if your account is set to accept any wallet, from any wallet, such as an exchange), and it paid directly: the wallet itself sent the transaction straight to the token contract, or it is a smart wallet, such as a multisig, executing its own transfer. A gasless, bundled (ERC-4337) or relayed transfer, or a payment routed through another contract such as a router, batcher, bridge or solver, is not credited automatically. A transfer under 10 USD is never picked up, so it cannot be credited: it is rejected with reason below_minimum.
  4. Submit the transaction hash and its network to POST /funding as soon as the transaction is broadcast. We verify it on-chain immediately.
  5. We credit your balance once the funds arrive: the amount sent on-chain, minus the load fee on your agreement. On Polygon this is usually within minutes. On BNB Chain and Base the funds are routed first and we credit only once your transaction is final on its network, so it can take several hours. Follow it with GET /funding/{fundingId} or the `float.credited` webhook.
StatusMeaning
pendingReceived. Either not yet found on its network, and re-checked every couple of minutes for 24 hours, or verified and waiting for the funds to arrive
creditedAdded to your balance and announced with float.credited. GET /me reflects it, and GET /ledger has a funding row referenced funding-<NETWORK>-<tx_hash> plus its load fee row with :fee appended. With reason credited_separately, it was credited to your balance by other means after a review: fee_usd and credited_usd are null, amount_usd is still the amount sent, and there is no webhook for it
rejectedCannot be credited as submitted, or a credit we reversed. reason says why. Only not_found can be corrected, by submitting the hash again with the right network
reviewVerified, but not credited automatically: a person decides. reason says why. We resolve it: it then becomes credited, with the same webhook and ledger rows (for the amount sent, or no more than arrived when the two differ), credited with reason credited_separately, or rejected with reason rejected_by_operator. There is nothing to resubmit
ReasonStatusMeaning
transaction_failedrejectedThe transaction failed on-chain
not_a_funding_transferrejectedNothing was transferred to your funding address in that transaction on that network
unsupported_tokenrejectedA transfer reached your funding address, but in a token not accepted on that network
mixed_tokensrejectedThe transaction sent both USDC and USDT to your funding address
below_minimumrejectedLess than 10 USD. It is never picked up, so it cannot be credited
unregistered_senderrejectedThe only wallet that paid is registered to another account, so it is that account's money. Wallets registered to other accounts are rejected whether or not your account is set to accept any wallet
bridge_fillrejectedOn any network: the transaction delivered bridged funds into the funding address, the arrival of a deposit made elsewhere. The depositor claims their own transaction on the network they sent from. If you bridged the funds yourself, contact us with both hashes
before_automatic_fundingrejectedSent before automatic funding started. Those transfers were settled separately
before_account_openedrejectedThe transaction predates your account
not_foundrejectedNot found on that network within 24 hours of your first submission, or still unreadable a day later. Submit the hash again, with the right network if it was wrong: it is looked up again
rejected_by_operatorrejectedWe reviewed it and it cannot be credited
reversed_by_operatorrejectedIt was credited, and we reversed the credit because it was not your money. See below
unregistered_senderreviewThe wallet that sent it is registered to no account. Tell us if it is yours. This does not arise if your account is set to accept any wallet, unless the wallet is registered to another account (which is rejected instead)
operator_senderreviewOne of our own wallets paid into the funding address in that transaction
multiple_sendersreviewMore than one wallet paid into the funding address in that transaction, even when one of them is registered to another account
routed_transferreviewThe wallet did not pay directly: the transfer was gasless, bundled (ERC-4337) or relayed, or routed through another contract such as a router, batcher, bridge or solver
precisionreviewThe amount has more than 6 decimal places
unlisted_tokenreviewThat token is not listed on that network at the moment
not_arrivedreviewVerified, but no matching arrival within 72 hours of the transaction. Reviewed shortly after that window
ambiguous_matchreviewOn Polygon: more than one arrival was reported for the same transaction
amount_mismatchreviewThe arrival reported for it differs from what the chain shows in token, amount or sending wallet
receipt_changedreviewOn BNB Chain or Base: the transaction is no longer on its chain as it was verified, or has been missing from it for over 2 hours
not_finalreviewOn BNB Chain or Base: an arrival matched, but the transaction did not become final on its network within 6 hours
check_failedreviewThe chain could not be read for this submission long after its window
already_creditedreviewThe same funds were already credited to a balance by other means
held_by_operatorreviewWe held it, your account is suspended, or your account was credited for the same transaction before and that credit was reversed
credited_separatelycreditedAfter a review: the funds were credited to your balance by other means

Whose money it is is decided first, then the amount. A transaction that one of our own wallets paid into goes to review with reason operator_sender, and one that more than one wallet paid into goes to review with reason multiple_senders. Otherwise, when the only wallet that paid is registered to another account, it is rejected with reason unregistered_sender: it is that account's money. One from a wallet registered to nobody, or not paid directly, goes to review, and only then are the decimals and the token checked. Chain facts are read only from sources we trust, or confirmed by independent sources that agree, before anything is credited.

Each transaction can be credited once. POST /funding needs no Idempotency-Key: submitting the same hash with the same network returns the same record, so a retry is always safe. If you named the wrong network, submit the hash again with the right one while it is still unverified (pending with a null amount_usd, or rejected with reason not_found): the same submission, under the same id, is looked up on that network. Resubmitting a hash rejected as not_found with the same network looks it up again too. The 24 hours run from your first submission. Once the transaction is verified, its network is fixed and a resubmission returns the same record. At most 5 of your submissions can be unverified at once: past that, a new submission answers 429 TOO_MANY_PENDING, which carries no Retry-After, so wait until some are found or rejected as not_found. A correction of your own is still accepted.

A network GET /funding/addresses does not list at the moment answers 400, and nothing is stored. When the BNB Chain and Base addresses cannot be read, GET /funding/addresses answers 503, and so does a submission on BNB Chain or Base; Polygon submissions keep working.

A hash belongs to the account whose registered wallet sent the money, when it is the only wallet that paid and it was registered before any other account claimed the hash. That account's submission replaces any other account's claim on the same hash that has not been paid, whether pending, in review or rejected, including one we rejected. Failing that, the first account to submit the hash on the right network keeps it, and another account's claim that named another network and never verified there is replaced by one that verifies. A replaced claim disappears: it no longer appears in GET /funding, and GET /funding/{fundingId} answers 404 for it. A paid claim is never displaced. When another account's claim on the hash could still give way to yours (it is still being looked up, or is verified on your network but not paid) and yours cannot be read on its network yet, you get 503 UNAVAILABLE: retry shortly. Otherwise you get 409 ALREADY_SUBMITTED, and retrying does not help: if the transaction is yours, check the network and that your sending wallet is registered to your account, then contact us. A transaction already credited to a balance by other means answers 409 ALREADY_CREDITED. Funding on any network other than Polygon, BNB Chain and Base is settled by arrangement with us.

If a credited submission turns out not to be your money, we can reverse it. Its status becomes rejected with reason reversed_by_operator, and fee_usd and credited_usd keep their original figures. GET /ledger gains a negative funding row referenced funding-reversal-<NETWORK>-<tx_hash> and a positive fee row with :fee appended, which returns the load fee, so your balance drops by credited_usd and can go below zero. No webhook is sent for a reversal.

GET/api/partner/v1/me#

Returns the account behind the key, the scopes the key carries, your lifetime card counts, your float broken down, a summary of your load rate, and the commission you have earned. This is the call to make first when wiring up an integration, and again whenever you need a current spendable balance. Everything it returns is scoped to your own account.

Scope users:read
Response
idstring
Your account id, a UUID
namestring | null
Your registered business name
contact_emailstring | null
The address we hold for your account
created_atstring | null
ISO 8601 UTC timestamp of when your account was created
namestring
The label we gave this key when we issued it
prefixstring
The public half of the key, in the form ktap_live_ plus 8 hex characters
scopesarray of string
Exactly what this key may do. A call outside these returns 403
users:read · users:write · cards:read · cards:write · cards:pan · deposits:read · deposits:write
modestring
Always live. Every key we issue reaches the real card processor
live · test
rate_limit_per_minuteinteger
Requests this key may make per rolling minute, 120 unless we agreed otherwise
totalinteger
Cards you have ever ordered, closed ones included. This is the number the allowance is measured against
activeinteger
Cards whose status is not DELETED. Frozen and blocked cards count here
limitinteger | null
Your lifetime issuance allowance. Null means no cap
funded_usdnumber
Everything you have sent us, before any fee
spent_usdnumber
Everything drawn down: load fees, monthly card fees, and net card top-ups
balance_usdnumber
funded_usd minus spent_usd
limit_usdnumber | null
A ceiling we set on card value. Null means no ceiling
available_usdnumber
What a top-up can draw on right now, never below 0
ktap_pctnumber
Your load rate on float funding, as a fraction
your_markup_pctnumber
Your own markup, set with PUT /fees. It applies to crypto deposits only
total_pctnumber
fees.ktap_pct plus fees.your_markup_pct. No single charge equals this sum
ktap_fee_usdnumber
The KTAP load fee on $100 funded from float
your_fee_usdnumber
Your markup applied to $100
lands_on_card_usdnumber
$100 less both figures above
accrued_usdnumber
Every positive commission row, ever
paid_out_usdnumber
Every payout we have recorded against you
owed_usdnumber
accrued_usd minus paid_out_usd, the amount still payable
last_payout_atstring | null
ISO 8601 UTC timestamp of the most recent payout row, null if you have never been paid
Errors
401UNAUTHORIZED
The key is missing, unknown, revoked, or your account is suspended. All four are reported identically
No. Fix the credential
403FORBIDDEN
The key is valid but does not carry users:read
No. Ask us for a key with the scope
405BAD_REQUEST
You send POST, PUT, PATCH or DELETE to /me
No. Use GET
429RATE_LIMITED
Your key made rate_limit_per_minute requests in the trailing minute
Yes, after the 60 seconds Retry-After names
500UNAVAILABLE
An internal fault, such as a database error
Yes, shortly
Notes
  • float.available_usd is the only figure a top-up is checked against. It is the smaller of your balance and any spending limit we have set, floored at zero.
  • You cannot recompute available_usd from this payload, because the figure it subtracts is not returned here. Read it, do not derive it. When float.limit_usd is null it equals balance_usd.
  • cards.total counts every card you have ever ordered. Closing a card does not return capacity, so your remaining allowance is cards.limit minus cards.total.
curl -sS -X GET 'https://virtual.ktap.io/api/partner/v1/me' \
  -H 'X-API-Key: ktap_live_...'
Response
{
  "data": {
    "partner": {
      "id": "7c1f4b2e-9a63-4d18-b0f5-2ea84c9d1367",
      "name": "Acme Ltd",
      "contact_email": "ops@acme.example",
      "created_at": "2026-03-14T09:22:41.518Z"
    },
    "key": {
      "name": "Production server",
      "prefix": "ktap_live_ef27ebea",
      "scopes": [
        "users:read",
        "users:write",
        "cards:read",
        "cards:write"
      ],
      "mode": "live",
      "rate_limit_per_minute": 120
    },
    "cards": {
      "total": 128,
      "active": 121,
      "limit": 250
    },
    "float": {
      "funded_usd": 50000,
      "spent_usd": 18495.55,
      "balance_usd": 31504.45,
      "limit_usd": 20000,
      "available_usd": 1854.45
    },
    "fees": {
      "ktap_pct": 0.01,
      "your_markup_pct": 0.02,
      "total_pct": 0.03,
      "example_on_100_usd": {
        "ktap_fee_usd": 1,
        "your_fee_usd": 2,
        "lands_on_card_usd": 97
      }
    },
    "earnings": {
      "accrued_usd": 412.5,
      "paid_out_usd": 300,
      "owed_usd": 112.5,
      "last_payout_at": "2026-08-01T10:00:00.000Z"
    }
  }
}
GET/api/partner/v1/funding/addresses#

Returns the funding address for each network you can fund your balance on, with the tokens it accepts there, refreshed at least every 10 minutes. Only networks whose transaction hashes POST /funding can verify and credit are listed, and only addresses that are currently enabled. Send only a token listed here, from a wallet you have registered with us, to the address listed for the network you send on, and at least 10 USD per transfer.

Scope users:read
Response
addressesarray
One entry per network you can fund on. A network that is not listed cannot be credited through POST /funding
networkstring
Name this network when you submit the hash to POST /funding
POLYGON · BNB · BASE
addressstring
Where to send on this network. Compare addresses case-insensitively
tokensarray
The stablecoins this address accepts on this network, each USDC or USDT. Send nothing else
Errors
401UNAUTHORIZED
The key is missing, unknown, revoked, or your account is suspended
No. Fix the credential
403FORBIDDEN
The key is valid but does not carry users:read
No. Ask us for a key with the scope
405BAD_REQUEST
Any method other than GET is used on this path
No. Use GET
429RATE_LIMITED
Your key exceeded its per-minute allowance
Yes, after the 60 seconds Retry-After names
503UNAVAILABLE
The funding addresses, or only the BNB Chain and Base ones, could not be read. We never answer with a guessed address instead
Yes, shortly. Do not send funds until it answers
Notes
  • Read the addresses before each transfer rather than storing them. They are refreshed at least every 10 minutes, and a network or token that is not listed at that moment cannot be credited through POST /funding.
  • Send at least 10 USD per transfer, from a wallet you have registered with us. A smaller transfer is never picked up, so it cannot be credited: POST /funding rejects it with reason below_minimum.
  • A 503 returns no address at all: wait and retry, and do not send until it answers. It never falls back to a guessed address, and it also answers 503 when only the BNB Chain and Base addresses cannot be read. The address in the example is illustrative: never send funds to an address copied from this documentation.
curl -sS -X GET 'https://virtual.ktap.io/api/partner/v1/funding/addresses' \
  -H 'X-API-Key: ktap_live_...'
{
  "data": {
    "addresses": [
      {
        "network": "POLYGON",
        "address": "0x5c1E8A3D7f24B690e1a4C8d3F7B25e906A1D4c8E",
        "tokens": ["USDC", "USDT"]
      },
      {
        "network": "BNB",
        "address": "0x5c1E8A3D7f24B690e1a4C8d3F7B25e906A1D4c8E",
        "tokens": ["USDC", "USDT"]
      },
      {
        "network": "BASE",
        "address": "0x5c1E8A3D7f24B690e1a4C8d3F7B25e906A1D4c8E",
        "tokens": ["USDC", "USDT"]
      }
    ]
  }
}
POST/api/partner/v1/funding#

Send USDC or USDT to the funding address GET /funding/addresses returns for the network you send on, then submit the transaction hash here. We verify it on-chain immediately, then credit your balance once the funds arrive: the amount sent on-chain, minus the load fee on your agreement. On Polygon this is usually within minutes. On BNB Chain and Base the funds are routed first and we credit only once your transaction is final on its network, so it can take several hours. Before your first transfer, tell us through your contact or the support channel which wallet or wallets you send from. A transfer is credited automatically only when exactly one wallet paid into your funding address in that transaction, that wallet is registered to your account (or, if your account is set to accept any wallet, from any wallet, such as an exchange), and it paid directly: the wallet itself sent the transaction straight to the token contract, or it is a smart wallet, such as a multisig, executing its own transfer. A gasless, bundled (ERC-4337) or relayed transfer goes to review. Anything else goes to review or is rejected. Accepted: USDC and USDT on Polygon, BNB Chain and Base, as GET /funding/addresses lists them, at least 10 USD per transfer, always sent to your funding address and never to a card address. No Idempotency-Key is needed: the call is idempotent on the transaction hash. Each transaction can be credited once.

Scope users:write
Headers
Content-Typestring
application/json. The body is parsed as JSON whatever you send
Body
tx_hashstringrequired
The hash of the transaction that sent the funds to your funding address
0x followed by 64 hex characters, in any case. Stored lowercase
networkstringrequired
The network the transaction was made on. BSC is accepted as another name for BNB, and case does not matter
POLYGON · BNB · BASE
Response
idstring
The submission's id, a UUID. Read it back with GET /funding/{fundingId}. It is funding_id on the float.credited webhook
networkstring
The network the hash is checked on. BSC is stored as BNB. It can be corrected while the submission is unverified, and is fixed once the transaction is verified
POLYGON · BNB · BASE
tx_hashstring
The transaction hash, lowercased
statusstring
pending: received, and either not yet found on its network (re-checked every couple of minutes for 24 hours) or verified and waiting for the funds to arrive. credited: added to your balance, and float.credited is sent. rejected: cannot be credited as submitted, or a credit we reversed, see reason; only not_found can be corrected, by submitting the hash again with the right network. review: verified but not credited automatically, see reason; a person decides. We resolve it: it then becomes credited (with reason credited_separately if the funds were credited to your balance by other means), or rejected with reason rejected_by_operator. There is nothing to resubmit
pending · credited · rejected · review
reasonstring | null
Why a submission is rejected, in review or credited separately, null otherwise. Whose money it is is decided first, then the amount. Rejected: transaction_failed (it failed on-chain), not_a_funding_transfer (nothing was transferred to your funding address in that transaction on that network), unsupported_token (a transfer reached your funding address, but in a token that is not accepted on that network), mixed_tokens (it sent both USDC and USDT to your funding address), below_minimum (less than 10 USD, which is never picked up), unregistered_sender (the only wallet that paid is registered to another account, so it is that account's money), bridge_fill (on any network, the transaction delivered bridged funds into the funding address, the arrival of a deposit made elsewhere; the depositor claims their own transaction on the network they sent from, and if you bridged the funds yourself, contact us with both hashes), before_automatic_funding (sent before automatic funding started; those transfers were settled separately), before_account_opened (it predates your account), not_found (not found on that network within 24 hours of your first submission, or still unreadable a day later; submitting it again looks it up again), rejected_by_operator (we reviewed it and it cannot be credited), reversed_by_operator (it was credited and we reversed the credit because it was not your money; fee_usd and credited_usd keep their figures). Review: unregistered_sender (the wallet that sent it is registered to no account), operator_sender (one of our own wallets paid into the funding address in that transaction), multiple_senders (more than one wallet paid into the funding address in that transaction, even when one of them is registered to another account), routed_transfer (the wallet did not pay directly: the transfer was gasless, bundled (ERC-4337) or relayed, or routed through another contract such as a router, batcher, bridge or solver), precision (the amount has more than 6 decimal places), unlisted_token (that token is not listed on that network at the moment), not_arrived (verified, but no matching arrival within 72 hours of the transaction), ambiguous_match (on Polygon, more than one arrival was reported for the same transaction), amount_mismatch (the arrival reported for it differs from what the chain shows in token, amount or sending wallet), receipt_changed (on BNB Chain or Base, the transaction is no longer on its chain as it was verified, or has been missing from it for over 2 hours), not_final (on BNB Chain or Base, an arrival matched but the transaction did not become final on its network within 6 hours), check_failed (the chain could not be read for this submission long after its window), already_credited (the same funds were already credited to a balance by other means), held_by_operator (we held it, your account is suspended, or your account was credited for the same transaction before and that credit was reversed). Credited: credited_separately (after a review, the funds were credited to your balance by other means; fee_usd and credited_usd are null, amount_usd is still the amount sent, and no webhook is sent)
transaction_failed · not_a_funding_transfer · unsupported_token · mixed_tokens · below_minimum · bridge_fill · before_automatic_funding · before_account_opened · not_found · rejected_by_operator · reversed_by_operator · precision · multiple_senders · operator_sender · unregistered_sender · routed_transfer · unlisted_token · not_arrived · ambiguous_match · amount_mismatch · receipt_changed · not_final · check_failed · already_credited · held_by_operator · credited_separately
tokenstring | null
The stablecoin sent. Null until the transaction has been read on-chain
USDC · USDT
amount_usdnumber | null
What the transaction paid into your funding address, as read on-chain: always the amount sent, even when a review credits less. Several transfers of one token in the same transaction are summed. The ledger keeps up to 10 decimal places, so an amount with more than 6 goes to review with reason precision rather than being rounded. Null until verified
fee_usdnumber | null
The load fee on your agreement, charged on the amount credited: amount_usd, or after a review no more than arrived if less arrived. The same figure as fee_usd on float.credited. Null until credited, and on a credited_separately submission. A reversal keeps it
credited_usdnumber | null
The amount credited minus fee_usd: what reached your balance, the same figure as credited_usd on float.credited. Null until credited, and on a credited_separately submission. A reversal keeps it, and takes this amount back off your balance
created_atstring
When this submission was first recorded, ISO 8601 UTC. A network correction keeps it
credited_atstring | null
When it was credited, ISO 8601 UTC. Null until then
Errors
400BAD_REQUEST
The body is not valid JSON or not a JSON object, tx_hash is missing or not 0x plus 64 hex characters, network is missing or not one of POLYGON, BNB, BSC and BASE, or GET /funding/addresses does not list that network at the moment
Nothing was recorded. Fix the request and send it again
401UNAUTHORIZED
The key is missing, unknown, revoked, or your account is suspended
No. Fix the credential
403FORBIDDEN
The key is valid but does not carry users:write
No. Ask us for a key with the scope
405BAD_REQUEST
You sent PUT, PATCH or DELETE to /funding
No. POST submits a hash and GET lists your submissions
409ALREADY_SUBMITTED
Another account holds this transaction hash and keeps it: its claim has been paid, it submitted the hash first on the same network and the money did not come from a wallet registered to you alone, or its claim names another network and your transaction cannot be credited on yours
No, retrying does not help. If the transaction is yours, check the network and that your sending wallet is registered to your account, then contact us with the hash
409ALREADY_CREDITED
The transaction was already credited to a balance by other means
No. It has been credited once; find it on GET /ledger
429RATE_LIMITED
Your key exceeded its per-minute allowance
Yes, after the 60 seconds Retry-After names
429TOO_MANY_PENDING
5 of your submissions are still not found on their network. A correction of your own submission is still accepted
After some are found, or rejected as not_found 24 hours after their first submission. There is no Retry-After header
500UNAVAILABLE
An internal fault. A network that is slow or cannot be read leaves the submission pending instead
Yes, with the same hash. A repeat returns the stored record if the first call got that far
503UNAVAILABLE
The funding addresses could not be read just now, so the hash could not be checked. When only the BNB Chain and Base addresses cannot be read, only a submission on those networks gets this and Polygon keeps working. Also when another account's claim on the hash could still give way to yours (it is still being looked up, or is verified on your network but not paid) and your submission cannot be read on its network yet, so it could not be verified to replace that claim. Nothing was recorded
Yes, with the same hash, shortly
Notes
  • A 200 means the hash is recorded, not that it is credited. Read status, then poll GET /funding/{fundingId} or listen for `float.credited`. Tell us which wallets you send from before your first transfer: credit is automatic only when exactly one wallet paid into your funding address, it is registered to your account (or, if your account is set to accept any wallet, from any wallet, such as an exchange), and it paid directly: it sent the transaction straight to the token contract itself or, as a smart wallet such as a multisig, executed its own transfer. When the only wallet that paid is another account's registered wallet, it is rejected with reason unregistered_sender; one from an unregistered wallet, or gasless, bundled (ERC-4337), relayed or routed through another contract (routed_transfer), goes to review.
  • No Idempotency-Key is needed: submitting is idempotent on the hash, so retry a timeout or a 5xx with the same hash and network. Named the wrong network? While your submission is unverified (pending with a null amount_usd, or rejected with reason not_found), submit the same hash with the right network: the same submission, same id, is looked up there. The 24 hours run from your first submission, so correct it early. Once the transaction is verified its network is fixed. At most 5 of your submissions can be unverified at once: past that, a new submission answers 429 TOO_MANY_PENDING, with no Retry-After; a correction is still accepted.
  • Send only a token GET /funding/addresses lists for the network, only to the address it lists, at least 10 USD per transfer, and never to a card address, then submit as soon as the transaction is broadcast. A hash belongs to the account whose registered wallet alone sent the money (registered before any other account claimed the hash), and its submission replaces another account's unpaid claim; failing that, the first account to submit it on the right network keeps it. A paid claim is never displaced. 503 UNAVAILABLE means another account's claim on the hash could still give way to yours (it is still being looked up, or is verified on your network but not paid) and yours could not be verified yet: retry shortly. Retrying 409 ALREADY_SUBMITTED does not help: check the network and that your sending wallet is registered, then contact us. A transaction that delivered bridged funds into the funding address, on any network, is rejected with reason bridge_fill: if you bridged them yourself, contact us with both hashes.
curl -sS -X POST 'https://virtual.ktap.io/api/partner/v1/funding' \
  -H 'X-API-Key: ktap_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"tx_hash":"0x4a7e2c9d1b8f3e6a5c0d7b2e9f4a1c8d3e6b0f5a2d9c7e4b1a8f6d3c0e5b2a91","network":"POLYGON"}'
{
  "data": {
    "id": "3b9e6f1a-7c42-4d85-a1e0-5f2c8d9b4e63",
    "network": "POLYGON",
    "tx_hash": "0x4a7e2c9d1b8f3e6a5c0d7b2e9f4a1c8d3e6b0f5a2d9c7e4b1a8f6d3c0e5b2a91",
    "status": "pending",
    "reason": null,
    "token": "USDC",
    "amount_usd": 2500.25,
    "fee_usd": null,
    "credited_usd": null,
    "created_at": "2026-09-23T10:15:02.114Z",
    "credited_at": null
  }
}
GET/api/partner/v1/funding/{fundingId}#

Returns one submission to POST /funding and where it stands. Poll it until status is credited or rejected, or listen for the float.credited webhook instead. A submission in review is resolved by us and then becomes one of those two. A credited submission that was not your money can later be reversed by us, and then reads rejected with reason reversed_by_operator.

Scope users:read
Path parameters
fundingIdstring
The id POST /funding returned. A submission belonging to another account returns 404, the same as an id that does not exist
A UUID. Anything else returns 404
Response
idstring
The submission's id, a UUID. Read it back with GET /funding/{fundingId}. It is funding_id on the float.credited webhook
networkstring
The network the hash is checked on. BSC is stored as BNB. It can be corrected while the submission is unverified, and is fixed once the transaction is verified
POLYGON · BNB · BASE
tx_hashstring
The transaction hash, lowercased
statusstring
pending: received, and either not yet found on its network (re-checked every couple of minutes for 24 hours) or verified and waiting for the funds to arrive. credited: added to your balance, and float.credited is sent. rejected: cannot be credited as submitted, or a credit we reversed, see reason; only not_found can be corrected, by submitting the hash again with the right network. review: verified but not credited automatically, see reason; a person decides. We resolve it: it then becomes credited (with reason credited_separately if the funds were credited to your balance by other means), or rejected with reason rejected_by_operator. There is nothing to resubmit
pending · credited · rejected · review
reasonstring | null
Why a submission is rejected, in review or credited separately, null otherwise. Whose money it is is decided first, then the amount. Rejected: transaction_failed (it failed on-chain), not_a_funding_transfer (nothing was transferred to your funding address in that transaction on that network), unsupported_token (a transfer reached your funding address, but in a token that is not accepted on that network), mixed_tokens (it sent both USDC and USDT to your funding address), below_minimum (less than 10 USD, which is never picked up), unregistered_sender (the only wallet that paid is registered to another account, so it is that account's money), bridge_fill (on any network, the transaction delivered bridged funds into the funding address, the arrival of a deposit made elsewhere; the depositor claims their own transaction on the network they sent from, and if you bridged the funds yourself, contact us with both hashes), before_automatic_funding (sent before automatic funding started; those transfers were settled separately), before_account_opened (it predates your account), not_found (not found on that network within 24 hours of your first submission, or still unreadable a day later; submitting it again looks it up again), rejected_by_operator (we reviewed it and it cannot be credited), reversed_by_operator (it was credited and we reversed the credit because it was not your money; fee_usd and credited_usd keep their figures). Review: unregistered_sender (the wallet that sent it is registered to no account), operator_sender (one of our own wallets paid into the funding address in that transaction), multiple_senders (more than one wallet paid into the funding address in that transaction, even when one of them is registered to another account), routed_transfer (the wallet did not pay directly: the transfer was gasless, bundled (ERC-4337) or relayed, or routed through another contract such as a router, batcher, bridge or solver), precision (the amount has more than 6 decimal places), unlisted_token (that token is not listed on that network at the moment), not_arrived (verified, but no matching arrival within 72 hours of the transaction), ambiguous_match (on Polygon, more than one arrival was reported for the same transaction), amount_mismatch (the arrival reported for it differs from what the chain shows in token, amount or sending wallet), receipt_changed (on BNB Chain or Base, the transaction is no longer on its chain as it was verified, or has been missing from it for over 2 hours), not_final (on BNB Chain or Base, an arrival matched but the transaction did not become final on its network within 6 hours), check_failed (the chain could not be read for this submission long after its window), already_credited (the same funds were already credited to a balance by other means), held_by_operator (we held it, your account is suspended, or your account was credited for the same transaction before and that credit was reversed). Credited: credited_separately (after a review, the funds were credited to your balance by other means; fee_usd and credited_usd are null, amount_usd is still the amount sent, and no webhook is sent)
transaction_failed · not_a_funding_transfer · unsupported_token · mixed_tokens · below_minimum · bridge_fill · before_automatic_funding · before_account_opened · not_found · rejected_by_operator · reversed_by_operator · precision · multiple_senders · operator_sender · unregistered_sender · routed_transfer · unlisted_token · not_arrived · ambiguous_match · amount_mismatch · receipt_changed · not_final · check_failed · already_credited · held_by_operator · credited_separately
tokenstring | null
The stablecoin sent. Null until the transaction has been read on-chain
USDC · USDT
amount_usdnumber | null
What the transaction paid into your funding address, as read on-chain: always the amount sent, even when a review credits less. Several transfers of one token in the same transaction are summed. The ledger keeps up to 10 decimal places, so an amount with more than 6 goes to review with reason precision rather than being rounded. Null until verified
fee_usdnumber | null
The load fee on your agreement, charged on the amount credited: amount_usd, or after a review no more than arrived if less arrived. The same figure as fee_usd on float.credited. Null until credited, and on a credited_separately submission. A reversal keeps it
credited_usdnumber | null
The amount credited minus fee_usd: what reached your balance, the same figure as credited_usd on float.credited. Null until credited, and on a credited_separately submission. A reversal keeps it, and takes this amount back off your balance
created_atstring
When this submission was first recorded, ISO 8601 UTC. A network correction keeps it
credited_atstring | null
When it was credited, ISO 8601 UTC. Null until then
Errors
401UNAUTHORIZED
The key is missing, unknown, revoked, or your account is suspended
No. Fix the credential
403FORBIDDEN
The key is valid but does not carry users:read
No. Ask us for a key with the scope
404NOT_FOUND
The id is unknown, is not a UUID, belongs to another account, or the submission was replaced by another account's claim
No. Check the id
405BAD_REQUEST
Any method other than GET is used on this path
No. Use GET
429RATE_LIMITED
Your key exceeded its per-minute allowance
Yes, after the 60 seconds Retry-After names
Notes
  • Once status is credited, credited_usd is what reached your balance and GET /me already reflects it. amount_usd here is always the amount sent on-chain, while float.credited carries the amount credited, which after a review can be lower; fee_usd and credited_usd match the webhook.
  • A review status is ours to resolve: the submission verified but could not be credited automatically, and reason says why. It then becomes credited, with a float.credited webhook; credited with reason credited_separately if the funds were credited to your balance by other means (no webhook, null fee_usd and credited_usd, amount_usd still the amount sent); or rejected with reason rejected_by_operator. Do not resubmit it.
  • A credited submission that was not your money can later become rejected with reason reversed_by_operator. fee_usd and credited_usd keep their figures, GET /ledger gains a negative funding row referenced funding-reversal-<NETWORK>-<tx_hash> and a positive :fee row, and your balance drops by credited_usd. No webhook is sent.
curl -sS -X GET 'https://virtual.ktap.io/api/partner/v1/funding/3b9e6f1a-7c42-4d85-a1e0-5f2c8d9b4e63' \
  -H 'X-API-Key: ktap_live_...'
{
  "data": {
    "id": "3b9e6f1a-7c42-4d85-a1e0-5f2c8d9b4e63",
    "network": "POLYGON",
    "tx_hash": "0x4a7e2c9d1b8f3e6a5c0d7b2e9f4a1c8d3e6b0f5a2d9c7e4b1a8f6d3c0e5b2a91",
    "status": "credited",
    "reason": null,
    "token": "USDC",
    "amount_usd": 2500.25,
    "fee_usd": 25.0025,
    "credited_usd": 2475.2475,
    "created_at": "2026-09-23T10:15:02.114Z",
    "credited_at": "2026-09-23T10:17:40.502Z"
  }
}
GET/api/partner/v1/funding#

Returns your 50 most recent submissions to POST /funding, newest first, each with its status. There is no paging.

Scope users:read
Response
fundingsarray
At most 50 submissions, ordered by created_at descending
idstring
The submission's id, a UUID. Read it back with GET /funding/{fundingId}. It is funding_id on the float.credited webhook
networkstring
The network the hash is checked on. BSC is stored as BNB. It can be corrected while the submission is unverified, and is fixed once the transaction is verified
POLYGON · BNB · BASE
tx_hashstring
The transaction hash, lowercased
statusstring
pending: received, and either not yet found on its network (re-checked every couple of minutes for 24 hours) or verified and waiting for the funds to arrive. credited: added to your balance, and float.credited is sent. rejected: cannot be credited as submitted, or a credit we reversed, see reason; only not_found can be corrected, by submitting the hash again with the right network. review: verified but not credited automatically, see reason; a person decides. We resolve it: it then becomes credited (with reason credited_separately if the funds were credited to your balance by other means), or rejected with reason rejected_by_operator. There is nothing to resubmit
pending · credited · rejected · review
reasonstring | null
Why a submission is rejected, in review or credited separately, null otherwise. Whose money it is is decided first, then the amount. Rejected: transaction_failed (it failed on-chain), not_a_funding_transfer (nothing was transferred to your funding address in that transaction on that network), unsupported_token (a transfer reached your funding address, but in a token that is not accepted on that network), mixed_tokens (it sent both USDC and USDT to your funding address), below_minimum (less than 10 USD, which is never picked up), unregistered_sender (the only wallet that paid is registered to another account, so it is that account's money), bridge_fill (on any network, the transaction delivered bridged funds into the funding address, the arrival of a deposit made elsewhere; the depositor claims their own transaction on the network they sent from, and if you bridged the funds yourself, contact us with both hashes), before_automatic_funding (sent before automatic funding started; those transfers were settled separately), before_account_opened (it predates your account), not_found (not found on that network within 24 hours of your first submission, or still unreadable a day later; submitting it again looks it up again), rejected_by_operator (we reviewed it and it cannot be credited), reversed_by_operator (it was credited and we reversed the credit because it was not your money; fee_usd and credited_usd keep their figures). Review: unregistered_sender (the wallet that sent it is registered to no account), operator_sender (one of our own wallets paid into the funding address in that transaction), multiple_senders (more than one wallet paid into the funding address in that transaction, even when one of them is registered to another account), routed_transfer (the wallet did not pay directly: the transfer was gasless, bundled (ERC-4337) or relayed, or routed through another contract such as a router, batcher, bridge or solver), precision (the amount has more than 6 decimal places), unlisted_token (that token is not listed on that network at the moment), not_arrived (verified, but no matching arrival within 72 hours of the transaction), ambiguous_match (on Polygon, more than one arrival was reported for the same transaction), amount_mismatch (the arrival reported for it differs from what the chain shows in token, amount or sending wallet), receipt_changed (on BNB Chain or Base, the transaction is no longer on its chain as it was verified, or has been missing from it for over 2 hours), not_final (on BNB Chain or Base, an arrival matched but the transaction did not become final on its network within 6 hours), check_failed (the chain could not be read for this submission long after its window), already_credited (the same funds were already credited to a balance by other means), held_by_operator (we held it, your account is suspended, or your account was credited for the same transaction before and that credit was reversed). Credited: credited_separately (after a review, the funds were credited to your balance by other means; fee_usd and credited_usd are null, amount_usd is still the amount sent, and no webhook is sent)
transaction_failed · not_a_funding_transfer · unsupported_token · mixed_tokens · below_minimum · bridge_fill · before_automatic_funding · before_account_opened · not_found · rejected_by_operator · reversed_by_operator · precision · multiple_senders · operator_sender · unregistered_sender · routed_transfer · unlisted_token · not_arrived · ambiguous_match · amount_mismatch · receipt_changed · not_final · check_failed · already_credited · held_by_operator · credited_separately
tokenstring | null
The stablecoin sent. Null until the transaction has been read on-chain
USDC · USDT
amount_usdnumber | null
What the transaction paid into your funding address, as read on-chain: always the amount sent, even when a review credits less. Several transfers of one token in the same transaction are summed. The ledger keeps up to 10 decimal places, so an amount with more than 6 goes to review with reason precision rather than being rounded. Null until verified
fee_usdnumber | null
The load fee on your agreement, charged on the amount credited: amount_usd, or after a review no more than arrived if less arrived. The same figure as fee_usd on float.credited. Null until credited, and on a credited_separately submission. A reversal keeps it
credited_usdnumber | null
The amount credited minus fee_usd: what reached your balance, the same figure as credited_usd on float.credited. Null until credited, and on a credited_separately submission. A reversal keeps it, and takes this amount back off your balance
created_atstring
When this submission was first recorded, ISO 8601 UTC. A network correction keeps it
credited_atstring | null
When it was credited, ISO 8601 UTC. Null until then
countinteger
Rows returned, at most 50
Errors
401UNAUTHORIZED
The key is missing, unknown, revoked, or your account is suspended
No. Fix the credential
403FORBIDDEN
The key is valid but does not carry users:read
No. Ask us for a key with the scope
429RATE_LIMITED
Your key exceeded its per-minute allowance
Yes, after the 60 seconds Retry-After names
500UNAVAILABLE
An internal fault, such as a database error
Yes, shortly. The endpoint is read-only
Notes
  • Returns your 50 most recent submissions and does not page. Keep the id each POST /funding returns and read older submissions back by id.
  • A submission replaced by another account's claim, because that account's registered wallet sent the money or its claim verified on the right network, disappears from this list, and reading it by id answers 404.
curl -sS -X GET 'https://virtual.ktap.io/api/partner/v1/funding' \
  -H 'X-API-Key: ktap_live_...'
Response
{
  "data": {
    "fundings": [
      {
        "id": "3b9e6f1a-7c42-4d85-a1e0-5f2c8d9b4e63",
        "network": "POLYGON",
        "tx_hash": "0x4a7e2c9d1b8f3e6a5c0d7b2e9f4a1c8d3e6b0f5a2d9c7e4b1a8f6d3c0e5b2a91",
        "status": "credited",
        "reason": null,
        "token": "USDC",
        "amount_usd": 2500.25,
        "fee_usd": 25.0025,
        "credited_usd": 2475.2475,
        "created_at": "2026-09-23T10:15:02.114Z",
        "credited_at": "2026-09-23T10:17:40.502Z"
      },
      {
        "id": "8d4a2c7e-1f93-4b6d-9e05-c3a7b1f8d240",
        "network": "BNB",
        "tx_hash": "0x1f8c3e5a7d2b9046c1e8a3f5d7b2c9e4a6f1d8b3c5e7a2f9d4b6c1e8a3f50246",
        "status": "rejected",
        "reason": "not_a_funding_transfer",
        "token": null,
        "amount_usd": null,
        "fee_usd": null,
        "credited_usd": null,
        "created_at": "2026-09-22T16:40:11.308Z",
        "credited_at": null
      }
    ],
    "count": 2
  }
}
GET/api/partner/v1/fees#

Returns your load rates, your own markup, and the card usage rates. Read your rates from here rather than hardcoding them, so a change on our side reaches you without a deploy. GET and PUT return the identical payload shape.

Scope users:read
Response
ktap_pctnumber
Your load rate when you fund your float, as a fraction. Identical to load_from_float_pct
load_from_float_pctnumber
The rate we charge once, when you send us funds. Card top-ups are then 1:1
load_from_user_deposit_pctnumber
The rate on a load your end user pays us in crypto directly. Applies only if deposits are enabled on your account
your_markup_pctnumber
Your own markup on loads, set with PUT /fees. Charged on the deposit route only
total_pctnumber
load_from_float_pct plus your_markup_pct. This sum is not charged on any single route
ktap_fee_usdnumber
$100 times load_from_float_pct
your_fee_usdnumber
$100 times your_markup_pct
lands_on_card_usdnumber
$100 less both figures above
usd_purchase_pctnumber
Rate on a USD purchase, as a fraction
fx_purchase_pctnumber
Rate on a purchase in any currency other than USD, as a fraction
high_risk_surcharge_pctnumber
Added to the applicable purchase rate at a high-risk merchant category, not charged instead of it
atm_pctnumber
Rate on an ATM withdrawal, as a fraction
visa_direct_pctnumber
Rate on a push-to-card payout, as a fraction
monthly_per_card_usdnumber
Charged per month for every card that exists, frozen ones included. No proration and no separate issuance fee
your_markup_pctnumber
Your own markup on card transactions. We set this for you; PUT /fees does not change it
Errors
401UNAUTHORIZED
The key is missing, unknown, revoked, or your account is suspended
No. Fix the credential
403FORBIDDEN
The key is valid but does not carry users:read
No. Ask us for a key with the scope
405BAD_REQUEST
You send POST, PATCH or DELETE to /fees
No. Use GET or PUT
429RATE_LIMITED
Your key made rate_limit_per_minute requests in the trailing minute
Yes, after the 60 seconds Retry-After names
500UNAVAILABLE
An internal fault, such as a database error
Yes, shortly
Notes
  • Rates are fractions, not percentages. A value of 0.02 means 2%.
  • The rates under transaction are a published rate card, not the figures any one movement is charged at. What a purchase was actually charged is on its card.authorized event, not on its settlement event. Its row in GET /cards/{id}/transactions shows the card processor's figures rounded to the cent. Reconcile against those, not against this response.
curl -sS -X GET 'https://virtual.ktap.io/api/partner/v1/fees' \
  -H 'X-API-Key: ktap_live_...'
Response
{
  "data": {
    "ktap_pct": 0.01,
    "load_from_float_pct": 0.01,
    "load_from_user_deposit_pct": 0.02,
    "your_markup_pct": 0.02,
    "total_pct": 0.03,
    "example_on_100_usd": {
      "ktap_fee_usd": 1,
      "your_fee_usd": 2,
      "lands_on_card_usd": 97
    },
    "transaction": {
      "usd_purchase_pct": 0.01,
      "fx_purchase_pct": 0.02,
      "high_risk_surcharge_pct": 0.01,
      "atm_pct": 0.04,
      "visa_direct_pct": 0.02,
      "monthly_per_card_usd": 1,
      "your_markup_pct": 0.02
    }
  }
}
PUT/api/partner/v1/fees#

Sets the rate you add on top of the KTAP load fee, as a fraction. The change takes effect on the next deposit quoted, and the response is the full GET /fees payload with the new rate applied. Any key holding users:write can change this rate for the whole account, with no approval step.

Scope users:write
Headers
Content-Typestring
application/json. The body is parsed as JSON regardless of what you send
Body
markup_pctnumberrequired
Your markup as a fraction, so 0.025 means 2.5%. Required: omitting it returns 400
A JSON number, finite, 0 to 0.5 inclusive. Stored at 5 decimal places
Response
ktap_pctnumber
Your load rate when you fund your float, unchanged by this call
load_from_float_pctnumber
The same figure, named explicitly
load_from_user_deposit_pctnumber
The rate on a load your end user pays us in crypto directly, unchanged by this call
your_markup_pctnumber
The markup now in force, read back from storage after rounding to 5 decimal places
total_pctnumber
load_from_float_pct plus your_markup_pct. This sum is not charged on any single route
ktap_fee_usdnumber
$100 times load_from_float_pct
your_fee_usdnumber
$100 times the new your_markup_pct
lands_on_card_usdnumber
$100 less both figures above
usd_purchase_pctnumber
Rate on a USD purchase, unchanged by this call
fx_purchase_pctnumber
Rate on a non-USD purchase, unchanged by this call
high_risk_surcharge_pctnumber
Added to the applicable purchase rate at a high-risk merchant category, unchanged by this call
atm_pctnumber
Rate on an ATM withdrawal, unchanged by this call
visa_direct_pctnumber
Rate on a push-to-card payout, unchanged by this call
monthly_per_card_usdnumber
Per card per month, unchanged by this call
your_markup_pctnumber
Your markup on card transactions. A different rate in a different column, and this call does not touch it
Errors
400BAD_REQUEST
The request body is not valid JSON. Message: Request body must be valid JSON
No. Fix the body
400BAD_REQUEST
The body parses but is null or an array. Message: Request body must be a JSON object
No. Send a JSON object
400BAD_REQUEST
markup_pct is absent or is not a JSON number, including a numeric string such as "0.025". Message: markup_pct must be a number, e.g. 0.025 for 2.5%
No. Send a bare number
400BAD_REQUEST
markup_pct is negative, above 0.5, or not finite. Message: Markup must be between 0 and 0.5 (50%)
No. Send a value from 0 to 0.5
401UNAUTHORIZED
The key is missing, unknown, revoked, or your account is suspended
No. Fix the credential
403FORBIDDEN
The key is valid but does not carry users:write
No. Ask us for a key with the scope
405BAD_REQUEST
You send POST, PATCH or DELETE to /fees
No. Use GET or PUT
429RATE_LIMITED
Your key made rate_limit_per_minute requests in the trailing minute
Yes, after the 60 seconds Retry-After names
500UNAVAILABLE
An internal fault, such as a database error
Yes, shortly
Notes
  • Send markup_pct as a fraction. 0.025 is 2.5%. Sending 2.5 returns 400.
  • The accepted range is 0 to 0.5 inclusive, stored to five decimal places. A value with more precision is rounded on write and the response echoes the rounded figure.
  • This rate applies to direct card funding only. On a prepaid balance it changes nothing you are billed.
curl -sS -X PUT 'https://virtual.ktap.io/api/partner/v1/fees' \
  -H 'X-API-Key: ktap_live_...' \
  -H 'Content-Type: application/json' \
  -d '{"markup_pct":0.025}'
{
  "data": {
    "ktap_pct": 0.01,
    "load_from_float_pct": 0.01,
    "load_from_user_deposit_pct": 0.02,
    "your_markup_pct": 0.02,
    "total_pct": 0.03,
    "example_on_100_usd": {
      "ktap_fee_usd": 1,
      "your_fee_usd": 2,
      "lands_on_card_usd": 97
    },
    "transaction": {
      "usd_purchase_pct": 0.01,
      "fx_purchase_pct": 0.02,
      "high_risk_surcharge_pct": 0.01,
      "atm_pct": 0.04,
      "visa_direct_pct": 0.02,
      "monthly_per_card_usd": 1,
      "your_markup_pct": 0.02
    }
  }
}
Account and balance · KTAP Cards API