cryptoinfraDocs

#Cards

Crypto debit cards — holders, KYC, issuance, top-up/withdraw, reveal, and delivery

Open as MarkdownFor AI assistants

32 endpoints. Generated from docs/openapi.yaml v1.0.0.

EndpointSummary
GET /v1/cards/holdersList your card holders
POST /v1/cards/holdersRegister a card holder
GET /v1/cards/summaryCard counts at a glance
GET /v1/cards/ordersCard-order ledger
GET /v1/cards/kyc/countriesKYC country catalogue
POST /v1/cards/kyc/uploadUpload a KYC document
POST /v1/cards/kycSubmit the KYC dossier
GET /v1/cards/kyc/statusAuthoritative KYC status
POST /v1/kyc/startStart (or resume) identity verification
GET /v1/kyc/statusCurrent KYC verdict (reconciled at read time)
GET /v1/cards/productsCard product catalogue
GET /v1/cardsList cards
POST /v1/cardsIssue a card
GET /v1/cards/topup-quotePreview a top-up's fee and total
GET /v1/cards/issue-quotePreview what issuing a card costs the cardholder
GET /v1/cards/orders/{order_id}Read a card order's status (the authoritative poll)
GET /v1/cards/{id}Secure card details
DELETE /v1/cards/{id}Close (terminate) a card
GET /v1/cards/{id}/balanceLive card balance
POST /v1/cards/{id}/revealReveal the full card number
GET /v1/cards/{id}/otpFetch the card's live one-time code
POST /v1/cards/{id}/topupTop up a card
GET /v1/cards/{id}/close-quoteWhat closing this card would charge the cardholder
GET /v1/merchant/card-feesYour own card fees, on top of ours
PUT /v1/merchant/card-fees/{operation}Set your fee on one card operation
DELETE /v1/merchant/card-fees/{operation}Remove your fee on one card operation
POST /v1/cards/{id}/withdrawRetired — moving part of a card balance back to the wallet
POST /v1/cards/{id}/freezeFreeze a card
POST /v1/cards/{id}/unfreezeUnfreeze a card
GET /v1/cards/{id}/transactionsCard transaction history
GET /v1/cards/delivery/regionsPhysical-card delivery regions
POST /v1/cards/delivery/addressAdd a physical-card delivery address

#GET/v1/cards/holders

List your card holders · Auth: bearer API key · operationId: listCardHolders

One row per provider identity, newest first, with a card count and KYC state.

#Responses

StatusBodyMeaning
200objectYour card holders.
401ErrorMissing, malformed or unknown API key.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
holders requiredobject[]—
holders[].id requiredinteger—
holders[].user_id requiredstringYour externaluserid.
holders[].rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
holders[].name requiredstringThe holder's display name as registered.
holders[].program requiredstring—
holders[].email requiredstringThe email the holder was registered with.
holders[].kyc_status requiredstring(one of none, pending, approved, rejected)
holders[].card_count requiredinteger—
holders[].created_at requiredstring<date-time>—
json
{
  "holders": [
    {
      "id": 1,
      "user_id": "string",
      "rail": "rail_a",
      "name": "string",
      "program": "string",
      "email": "string",
      "kyc_status": "none",
      "card_count": 1,
      "created_at": "2026-08-31T09:15:00.000Z"
    }
  ]
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/holders' \
  -H 'Authorization: Bearer ck_live_your_key'

#POST/v1/cards/holders

Register a card holder · Auth: bearer API key · operationId: createCardHolder

The first step of the card flow: registers your user at the card provider and persists the holder. Idempotent per (user, rail) — replaying returns the existing holder untouched (still 201). Pass provider only when two enabled rails could both take the holder.

The same customer may hold a card at any number of merchants with the same email address: the rail's account is opened under a tagged copy of it, so one merchant's holder never blocks another's. 409 email_taken (with details.email) is answered only when no tag is free for that address — send a different one; it is not a retryable failure.

#Request body

application/json · required

FieldTypeDescription
user_id requiredstringYour externaluserid. (minLength 1; maxLength 255)
email requiredstring<email>(maxLength 255)
providerstringRail id (a product's rail), for the identity to register. Omit it unless two enabled rails could both take this holder. (minLength 1; maxLength 64)
first_namestring(maxLength 64)
last_namestring(maxLength 64)
phonestringE.164
date_of_birthstring<date>—
countrystring(minLength 2; maxLength 2)
zip_codestring(maxLength 16)
addressstring(maxLength 128)
citystring(maxLength 64)
statestring(maxLength 64)

#Responses

StatusBodyMeaning
201CardHolderHolder registered (or already existed).
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown externaluserid for this tenant.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

201 response body — CardHolder

FieldTypeDescription
user_id requiredstringYour externaluserid.
rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
email requiredstring—
kyc_status requiredstringLegacy issuer-KYC lifecycle on a card holder. Distinct from KycStatus, the cryptoinfra-owned verdict at /v1/kyc/*. (one of none, pending, approved, rejected)
kyc_remark requiredstring | nullProvider remark (e.g. a rejection reason); null when none.
json
{
  "user_id": "string",
  "rail": "rail_a",
  "email": "string",
  "kyc_status": "none",
  "kyc_remark": "string"
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/holders' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string",
  "email": "string",
  "phone": "+6591234567"
}'

#GET/v1/cards/summary

Card counts at a glance · Auth: bearer API key · operationId: cardsSummary

Totals for a dashboard header — cards by normalized status, plus how many holders exist.

#Parameters

NameInTypeRequiredDescription
core_idquerystringnoCount one programme's cards only.

#Responses

StatusBodyMeaning
200objectThe counts.
401ErrorMissing, malformed or unknown API key.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
cards requiredinteger—
by_status requiredobjectKeys are issuing/active/frozen/suspended/closed/expired.
holders requiredinteger—
on_cardsstringSum of live card balances (≤60s cache); null when none were readable.
processingstringSum of pending holds.
balances_readinteger—
json
{
  "cards": 1,
  "by_status": {},
  "holders": 1,
  "on_cards": "string",
  "processing": "string",
  "balances_read": 1
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/summary' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/cards/orders

Card-order ledger · Auth: bearer API key · operationId: listCardOrders

Every issue, top-up, card withdrawal and close fee on your account, newest first. card_id filters to one card, core_id to one programme.

#Parameters

NameInTypeRequiredDescription
card_idqueryintegerno— (min 1)
core_idquerystringnoOne programme's orders.
limitqueryintegerno— (min 1; max 500)

#Responses

StatusBodyMeaning
200objectThe orders.
401ErrorMissing, malformed or unknown API key.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
orders requiredobject[]—
orders[].idinteger—
orders[].kindstringclose_fee is the close fee charged to the cardholder on DELETE /v1/cards/{id}: amount is 0, fee is the charge. (one of topup, withdraw, issue_fee, close_fee)
orders[].statusstring—
orders[].amountstring—
orders[].feestring—
orders[].amount_creditedstring—
orders[].card_idinteger—
orders[].card_last4string—
orders[].railstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
orders[].user_idstring—
orders[].errorstring—
orders[].txidstringThe on-chain send's hash; null until broadcast or when the rail already held the money.
orders[].created_atstring<date-time>—
json
{
  "orders": [
    {
      "id": 1,
      "kind": "topup",
      "status": "string",
      "amount": "string",
      "fee": "string",
      "amount_credited": "string",
      "card_id": 1,
      "card_last4": "string",
      "rail": "rail_a",
      "user_id": "string",
      "error": "string",
      "txid": "string",
      "created_at": "2026-08-31T09:15:00.000Z"
    }
  ]
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/orders' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/cards/kyc/countries

KYC country catalogue · Auth: bearer API key · operationId: getCardKycCountries

The provider's supported KYC countries. value is the ISO alpha-3 code to pass as nation_code / country_code in POST /v1/cards/kyc.

#Parameters

NameInTypeRequiredDescription
namequerystringnoOptional local case-insensitive filter on name/en_name.
providerquerystringnoRail id (a product's rail); omit unless two enabled rails could both answer.

#Responses

StatusBodyMeaning
200objectSupported countries.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
countries requiredobject[]—
countries[].namestring—
countries[].en_namestring—
countries[].valuestringISO alpha-3 code.
countries[].prof_valuestring—
countries[].codestring—
countries[].telephone_codestring—
countries[].need_certificatebooleantrue → this country additionally needs filetype/fileurl/face_url in POST /v1/cards/kyc.
json
{
  "countries": [
    {
      "name": "string",
      "en_name": "string",
      "value": "string",
      "prof_value": "string",
      "code": "string",
      "telephone_code": "string",
      "need_certificate": true
    }
  ]
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/kyc/countries' \
  -H 'Authorization: Bearer ck_live_your_key'

#POST/v1/cards/kyc/upload

Upload a KYC document · Auth: bearer API key · operationId: uploadCardKycDocument

Passes a KYC document through to the provider and returns its hosted URL (feed that URL into POST /v1/cards/kyc). The bytes live only in this request — never stored on disk or in the DB. Max 5 MB decoded; the extension must be .png/.jpg/.jpeg/.pdf.

#Request body

application/json · required

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)
providerstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
filename requiredstring(minLength 1; maxLength 255)
content_base64 requiredstringBase64 of the document (≤5 MB decoded).

#Responses

StatusBodyMeaning
201objectUploaded.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorNo matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

201 response body

FieldTypeDescription
file_url requiredstringAn opaque handle for the uploaded document, valid for 24 hours and only on your account. Pass it as idurl / idbackurl / fileurl / faceurl / selfieurl in POST /v1/cards/kyc. It is not a link to open. (pattern ^kycfile_[A-Za-z0-9_-]{16,64}$)
json
{
  "file_url": "string"
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/kyc/upload' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string",
  "provider": "rail_a",
  "filename": "string",
  "content_base64": "string"
}'

#POST/v1/cards/kyc

Submit the KYC dossier · Auth: bearer API key · operationId: submitCardKyc

Submits KYC to the provider and flips the holder to pending. We keep no PII — the fields go straight to the provider. Resubmission after a rejection is allowed; an already-approved holder is rejected (400 invalid_request). id_back_url is required for cert_type 1 (ID card) and 3 (driver's licence).

#Request body

application/json · required · schema CardKycApply

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)
providerstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
first_name requiredstring(minLength 1; maxLength 128)
last_name requiredstring(minLength 1; maxLength 128)
id_no requiredstring(minLength 1; maxLength 64)
email requiredstring<email>(maxLength 255)
nation_code requiredstringNationality, ISO alpha-3 (the value from GET /v1/cards/kyc/countries). (minLength 3; maxLength 3)
cert_type requiredinteger1 = ID card, 2 = passport, 3 = driver's licence. (one of 1, 2, 3)
id_url requiredstringFRONT document photo — the kycfile_… handle from POST /v1/cards/kyc/upload, or a URL you host. (maxLength 1024)
birthday requiredstringYYYY-MM-DD. (pattern ^\d{4}-\d{2}-\d{2}$)
country_code requiredstringCountry of residence, ISO alpha-3. (minLength 3; maxLength 3)
area_code requiredstringDialing code without '+' (e.g. '86'). (minLength 1; maxLength 8)
phone requiredstring(minLength 3; maxLength 32)
id_back_urlstringBACK document photo (handle or URL, as idurl); REQUIRED for certtype 1 (ID card) and 3 (driver's licence). (maxLength 1024)
file_typeinteger(min 1; max 100)
file_urlstringHandle from the upload, or a URL you host. (maxLength 1024)
face_urlstringHandle from the upload, or a URL you host. (maxLength 1024)
selfie_urlstringHandle from the upload, or a URL you host. (maxLength 1024)
reference_idstring(maxLength 128)
reference_typestring(maxLength 64)

#Responses

StatusBodyMeaning
202CardHolderKYC accepted; the holder's status is now pending.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorNo matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

202 response body — CardHolder

FieldTypeDescription
user_id requiredstringYour externaluserid.
rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
email requiredstring—
kyc_status requiredstringLegacy issuer-KYC lifecycle on a card holder. Distinct from KycStatus, the cryptoinfra-owned verdict at /v1/kyc/*. (one of none, pending, approved, rejected)
kyc_remark requiredstring | nullProvider remark (e.g. a rejection reason); null when none.
json
{
  "user_id": "string",
  "rail": "rail_a",
  "email": "string",
  "kyc_status": "none",
  "kyc_remark": "string"
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/kyc' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string",
  "provider": "rail_a",
  "first_name": "string",
  "last_name": "string",
  "id_no": "string",
  "email": "string",
  "nation_code": "string",
  "cert_type": 1,
  "id_url": "string",
  "birthday": "string",
  "country_code": "string",
  "area_code": "string",
  "phone": "string"
}'

#GET/v1/cards/kyc/status

Authoritative KYC status · Auth: bearer API key · operationId: getCardKycStatus

Polls the rail and syncs the holder row before answering. Pass provider only when the user holds LIVE identities on two rails at once.

#Parameters

NameInTypeRequiredDescription
user_idquerystringyes—
providerquerystringnoRail id (a holder's or product's rail). Omit it: we follow the rail this user's own identity is on, and only ask when the user has live identities on two rails at once.

#Responses

StatusBodyMeaning
200CardHolderThe holder with its current KYC status.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorNo matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body — CardHolder

FieldTypeDescription
user_id requiredstringYour externaluserid.
rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
email requiredstring—
kyc_status requiredstringLegacy issuer-KYC lifecycle on a card holder. Distinct from KycStatus, the cryptoinfra-owned verdict at /v1/kyc/*. (one of none, pending, approved, rejected)
kyc_remark requiredstring | nullProvider remark (e.g. a rejection reason); null when none.
json
{
  "user_id": "string",
  "rail": "rail_a",
  "email": "string",
  "kyc_status": "none",
  "kyc_remark": "string"
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/kyc/status?user_id=string' \
  -H 'Authorization: Bearer ck_live_your_key'

#POST/v1/kyc/start

Start (or resume) identity verification · Auth: bearer API key · operationId: startKyc

cryptoinfra-owned KYC, independent of the card rail. Creates (or RESUMES) a verification session for user_id and returns the hosted URL to send the user to. approved is terminal — a verified user is never sent around again (no url, status: approved). Call this when a product's requires_kyc is true before issuing it.

Server-to-server only: authenticate with the tenant key and proxy from your backend — never expose this to a browser (the tenant key operates every card on the tenant). The verdict itself is always re-read from the provider's decision endpoint; poll GET /v1/kyc/status to observe it.

#Request body

application/json · required

FieldTypeDescription
user_id requiredstringThe tenant's user ref (same one that scopes card reads). (minLength 1; maxLength 255)
languagestringOptional ISO language hint for the hosted flow. (minLength 2; maxLength 8)

#Responses

StatusBodyMeaning
200objectSession created or resumed (or already approved).
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown externaluserid for this tenant.
503ErrorKYC is disabled or not configured.

200 response body

FieldTypeDescription
status requiredstringcryptoinfra-owned identity verdict for /v1/kyc/*. Issuance of a requires_kyc product needs approved. none = never started. (one of none, pending, approved, declined, review, expired)
urlstringHosted verification URL; absent when already approved.
json
{
  "status": "none",
  "url": "string"
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/kyc/start' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string"
}'

#GET/v1/kyc/status

Current KYC verdict (reconciled at read time) · Auth: bearer API key · operationId: getKycStatus

The user's verdict, re-read from the provider while it can still change (pending/review); terminal states are not re-fetched. none when the user has never started verification.

#Parameters

NameInTypeRequiredDescription
user_idquerystringyes—

#Responses

StatusBodyMeaning
200objectThe current verdict.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown externaluserid for this tenant.

200 response body

FieldTypeDescription
status requiredstringcryptoinfra-owned identity verdict for /v1/kyc/*. Issuance of a requires_kyc product needs approved. none = never started. (one of none, pending, approved, declined, review, expired)
json
{
  "status": "none"
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/kyc/status?user_id=string' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/cards/products

Card product catalogue · Auth: bearer API key · operationId: listCardProducts

Every issuable product, across every card programme you can issue from — the source of truth for the product_code and core_id you pass to POST /v1/cards. Read both from here; never hard-code either. A programme that is closed to new cards simply does not appear, so an empty list means nothing is currently on sale rather than that the feature is broken.

Each row carries the programme it belongs to (core_id, core_name) and is PRICED FOR THAT PROGRAMME. The same BIN under two programmes can cost different amounts, so compare prices by (core_id, card_bin) — never by BIN alone.

Each row also carries what the user pays: issue_fee (our one-time issuance fee for THAT product, on THAT programme), min_opening_balance — the smallest initial_amount the issue will accept — and issue_total, everything the cardholder is debited to open the card at that minimum: our issue fee, the opening balance priced as a load (our load fee + gas), and your own fees if set. Budget from issue_total (or total on GET /v1/cards/issue-quote for another opening balance); never add issue_fee and initial_amount yourself.

The envelope's top-level issue_fee is deprecated (virtual-card fee off the default row only) — use products[].issue_fee.

#Parameters

NameInTypeRequiredDescription
core_idquerystringnoShow one programme's shelf, priced for it. Omit to see every programme on offer.

#Responses

StatusBodyMeaning
200objectProducts.
401ErrorMissing, malformed or unknown API key.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
asset_id requiredstring—
gasstringThe network line inside every issue_total below, priced for this response.
gas_dynamicbooleantrue = priced live now; false = the static fallback is inside the totals.
gas_priced_atstring<date-time>—
issue_feestringDeprecated — virtual-card fee. Use products[].issue_fee.
issue_fee_basestringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
products requiredobject[]—
products[].id requiredinteger | nullNumeric id, kept for back-compat; null when the product code is not numeric.
products[].rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
products[].core_idstringThe card programme this row belongs to — pass it as core_id to POST /v1/cards. Every price on this row is resolved FOR this programme, so compare prices by (core_id, card_bin).
products[].core_namestringWhat this programme is called, for display. e.g. Core-1.
products[].code requiredstringProvider-scoped product code — pass as product_code to POST /v1/cards.
products[].card_nature requiredstring(one of VIRTUAL, PHYSICAL)
products[].ccy requiredstringCard currency (only USD is supported for money operations).
products[].regionstring | null—
products[].card_binstring | null—
products[].card_brandstring | null—
products[].card_modestring | null—
products[].titlestring | null—
products[].requires_kycbooleanWhether getting this card requires an ID check (cryptoinfra-owned KYC via /v1/kyc/*). Per-BIN; false while KYC is disabled. Show it on the shelf ("ID check required") so a customer is not refused at issuance — a card whose requires_kyc is true rejects POST /v1/cards with 409 kyc_not_approved until the user's status is approved.
products[].active_min_limitstring | nullMinimum FIRST top-up that activates a virtual card (decimal string); null when none.
products[].recharge_min_limitstring | nullMinimum single top-up (decimal string); null when the provider imposes none.
products[].withdraw_min_limitstring | nullMinimum single withdraw/unload the issuer will process (decimal string); null when the provider does not declare one. A residual balance below this cannot be withdrawn — surface it so a user can be warned before trying to take the last few dollars off a card.
products[].issue_fee requiredstringOUR issue fee for THIS product.
products[].issue_fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
products[].merchant_issue_feestringYour own issue fee (PUT /v1/merchant/card-fees/issue). "0" until you set one.
products[].issue_totalstringEverything the cardholder is debited to issue this card at min_opening_balance: our issue fee, the opening balance priced as a load (our load fee + gas), and your own fees. A card opened with more pays the load parts on more.
products[].issue_total_basestringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
products[].charged_tostring(one of cardholder)
products[].load_fee_bps requiredintegerOur load fee for cards of this product, basis points of the amount loaded.
products[].load_fee_min requiredstringThe floor on that load fee, in USD.
products[].load_fee_flat requiredstringA flat platform fee on every load, in USD, on top of the percentage (or its floor).
products[].close_feestringThe flat part of OUR fee to close a card of this product — our own fee plus the issuer's cancellation fee billed to us — debited from the cardholder's balance when the card terminates. The live network figure for returning the balance on-chain is added on top; GET /v1/cards/{id}/close-quote prices a specific card, gas and all. The issuer's own cancellation charge is separate and stays on the card — see cardholder_fees.cancellation.
products[].close_fee_basestringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
products[].cardholder_fees requiredobject | nullWhat the CARDHOLDER pays when the card is used, per BIN — separate from issue_fee / load_fee_bps, which are charged when you issue and load. Surface them, or a $3.86 purchase that took $4.11 becomes a support ticket. null for a BIN whose terms we have not transcribed.
products[].cardholder_fees.per_transactionstringUSD
products[].cardholder_fees.three_d_securestringUSD
products[].cardholder_fees.foreign_currency_pctstringPercent
products[].cardholder_fees.atm_flatstringUSD
products[].cardholder_fees.atm_pctstringPercent
products[].cardholder_fees.cancellationstringUSD
products[].cardholder_fees.disputestringUSD
products[].cardholder_fees.monthly_servicestring | nullRecurring holding fee; null when there is none.
products[].limits requiredobject | nullCeilings the cardholder hits, per BIN. null with cardholder_fees.
products[].limits.per_transactionstring—
products[].limits.dailystring—
products[].limits.monthlystring—
products[].limits.atm_singlestring—
products[].limits.atm_per_dayinteger—
products[].limits.atm_monthlystring—
products[].limits.cards_per_userintegerHow many of this card one rail account may hold. Not a ceiling on your customer — when it is reached
products[].limits.validity_yearsintegerYears from issue.
products[].min_opening_balancestringSmallest initial_amount this product's issue accepts, in USD. Read it rather than hard-coding a minimum: it tracks platform policy, the issuer's floor and the product's own activation minimum, whichever is highest.
products[].min_opening_balance_basestringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
json
{
  "asset_id": "usdt_trc20",
  "gas": "1.5",
  "gas_dynamic": true,
  "gas_priced_at": "2026-08-31T09:15:00.000Z",
  "issue_fee": "1.5",
  "issue_fee_base": "1500000000",
  "products": [
    {
      "id": 1,
      "rail": "rail_a",
      "core_id": "string",
      "core_name": "string",
      "code": "string",
      "card_nature": "VIRTUAL",
      "ccy": "string",
      "region": "string",
      "card_bin": "string",
      "card_brand": "string",
      "card_mode": "string",
      "title": "string",
      "requires_kyc": true,
      "active_min_limit": "string",
      "recharge_min_limit": "string",
      "withdraw_min_limit": "string",
      "issue_fee": "1.5",
      "issue_fee_base": "1500000000",
      "merchant_issue_fee": "1.5",
      "issue_total": "1.5",
      "issue_total_base": "1500000000",
      "charged_to": "cardholder",
      "load_fee_bps": 1,
      "load_fee_min": "1.5",
      "load_fee_flat": "1.5",
      "close_fee": "1.5",
      "close_fee_base": "1500000000",
      "cardholder_fees": {
        "per_transaction": "string",
        "three_d_secure": "string",
        "foreign_currency_pct": "string",
        "atm_flat": "string",
        "atm_pct": "string",
        "cancellation": "string",
        "dispute": "string",
        "monthly_service": "string"
      },
      "limits": {
        "per_transaction": "string",
        "daily": "string",
        "monthly": "string",
        "atm_single": "string",
        "atm_per_day": 1,
        "atm_monthly": "string",
        "cards_per_user": 1,
        "validity_years": 1
      },
      "min_opening_balance": "5.000000",
      "min_opening_balance_base": "1500000000"
    }
  ]
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/products' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/cards

List cards · Auth: bearer API key · operationId: listCards

With user_id: that user's cards across all their provider identities (404 when the user has no card holder). Without it: every card on your account, newest first — each row additionally carries user_id, cardholder_name, the holder's program, and a cached (≤60 s) live balance / pending (null when unreadable, never a fake zero), so any per-card endpoint can be called from a row. core_id narrows either form to one programme.

#Parameters

NameInTypeRequiredDescription
user_idquerystringno—
core_idquerystringnoOne programme's cards only.

#Responses

StatusBodyMeaning
200objectThe cards.
401ErrorMissing, malformed or unknown API key.
404ErrorNo matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
user_idstringEchoed only on the one-user form.
cards requiredobject[]—
cards[].id requiredinteger—
cards[].rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
cards[].core_id requiredstring | nullThe programme this card was issued from — the pair (core_id, bin) is what prices its loads and close. A programme holds no money; the cardholder pays. null on cards issued before programmes existed.
cards[].bin requiredstring | nullThe card's BIN. Read it WITH core_id, never alone: a BIN number is unique only inside its issuer, so the same number under a different programme is a different card with different fees, limits and validity.
cards[].product_id requiredstring | nullProvider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged.
cards[].product_code requiredstring | nullProvider-scoped product code; null when the card was listed untagged.
cards[].card_nature requiredstring(one of VIRTUAL, PHYSICAL)
cards[].status requiredintegerThe provider's RAW status code, verbatim — interpret via status_normalized.
cards[].status_normalized requiredstringProvider-neutral status. A just-issued card is issuing; it becomes active once the opening balance has landed at the issuer. (one of issuing, active, frozen, suspended, closed, expired)
cards[].currency requiredstring | null—
cards[].last4 requiredstring | null—
cards[].name_on_card requiredstring | nullThe cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic.
cards[].issued_at requiredstring<date-time>—
cards[].user_idstringThe cardholder's externaluserid.
cards[].cardholder_namestring | null—
cards[].programstring | null—
cards[].balancestring | nullLive available balance (≤60 s cache); null when unreadable.
cards[].pendingstring | nullHolds in flight; null when the rail does not expose them or the read failed.
json
{
  "user_id": "string",
  "cards": [
    {
      "id": 1,
      "rail": "rail_a",
      "core_id": "string",
      "bin": "string",
      "product_id": "string",
      "product_code": "string",
      "card_nature": "VIRTUAL",
      "status": 1,
      "status_normalized": "issuing",
      "currency": "string",
      "last4": "string",
      "name_on_card": "string",
      "issued_at": "2026-08-31T09:15:00.000Z",
      "user_id": "string",
      "cardholder_name": "string",
      "program": "string",
      "balance": "string",
      "pending": "string"
    }
  ]
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards' \
  -H 'Authorization: Bearer ck_live_your_key'

#POST/v1/cards

Issue a card · Auth: bearer API key · operationId: issueCard

A card is issued FUNDED. initial_amount is required and must be at least the product's min_opening_balance from GET /v1/cards/products (read that value, do not hard-code a minimum — it tracks platform policy and the issuer's own floor, whichever is higher). The cardholder is debited total from GET /v1/cards/issue-quote — our issue fee, the opening balance priced as a load (our load fee + gas), and your own fees if set — at request time, and refunded together if the application definitively fails. Read issue_total on the shelf or total on the quote; never add the parts yourself.

Pass one of product_id / product_code, taken from GET /v1/cards/products rather than hard-coded. provider is almost never needed: the product code names its own rail. delivery_address_id (from POST /v1/cards/delivery/address) is required for PHYSICAL products. Requires an approved holder. idempotency_key is required and replay-safe. The holder pays: our issue fee, the opening balance priced as a load, gas, and your own fees if you set any — see issue_total on GET /v1/cards/products.

201 = the card exists (card populated). 202 = the application is still resolving (card is null; a card.status webhook or the reconciler links it later). Treat 202 as PENDING.

The normal answer is 202. The opening balance is sent on-chain to the rail first, so the response is 202 with order.status: funding (then requested/sent); the card is applied for once the rail shows the credit and arrives on the card.status webhook, usually within minutes.

A 201 card is not spendable yet. It is returned with status_normalized: issuing, and turns active only once the opening balance has landed at the issuer — usually minutes. An integrator that polls for active immediately after the 201 will see issuing first; that is the normal path, not an error. Watch GET /v1/cards/{id} or the card.status webhook for the flip.

#Request body

application/json · required

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)
core_idstringWhich card programme to issue from — the core_id from the product in GET /v1/cards/products. Send it. It names both the balance that pays and the BIN that prices, so it is the only thing that stays correct once you hold a second programme. With exactly one programme open it may be omitted; with two, an omitted core_id is refused with card_core_required (the acceptable values ride along in the error) rather than charged to a balance you did not mean. (minLength 1; maxLength 64)
providerstringDeprecated in favour of core_id. Which card rail to issue on — the rail value from the product in GET /v1/cards/products. Ignored when core_id is sent. (minLength 1; maxLength 64)
product_idintegerNumeric product id, kept for back-compat. One of productid / productcode is required. (min 1)
product_codestringProvider-scoped product code (preferred; wins when both are sent). (minLength 1; maxLength 64)
delivery_address_idintegerRequired for PHYSICAL products (from POST /v1/cards/delivery/address). (min 1)
idempotency_key requiredstringRequired; replay-safe. (minLength 1; maxLength 255)
name_on_cardstringEmbossed/printed cardholder name. Uppercased on the way to the issuer. Not every card is PRINTED with one. BIN 493 has no cardholder name on the plastic — its issuer has no such field. We keep it regardless. The name you send is stored against the card and returned by every card read, because a list of cards has to be readable and last4 does not tell one cardholder from another. It reaches the issuer only on a rail that can emboss it. (minLength 2; maxLength 26; pattern ^[A-Za-z][A-Za-z .'-]*$)
initial_amount requiredstringRequired. Opening balance in USD (whole cents), on the card the moment it opens. Cards are issued FUNDED: an empty card is not a usable card, and on some rails it does not even activate until a first load lands. The minimum is min_opening_balance from GET /v1/cards/products — read it, do not hard-code it; it moves with platform policy and with the issuer's own floor. The cardholder is debited issue_total: our issue fee, the opening balance priced as a load (our load fee + gas), and your own fees if you set any. Read issue_total from GET /v1/cards/products or total from GET /v1/cards/issue-quote rather than adding issue_fee and initial_amount. (There is no netting: an issuer's own mandatory opening credit is NOT discounted out of the fee.) Below min_opening_balance the issue is refused with 400 invalid_request; the message names the required amount. min_opening_balance is computed per product and already includes the product's own activation minimum (active_min_limit), so opening at it is enough for the card to come alive. (pattern ^\d+(\.\d+)?$)

#Responses

StatusBodyMeaning
201CardIssueResultCard issued. card.status_normalized is issuing — it becomes active once the opening balance lands (see the description).
202CardIssueResultApplication accepted, still resolving (card is null). The usual answer: order.status is funding while the opening balance travels on-chain to the rail.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorNo matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

201 response body — CardIssueResult

FieldTypeDescription
order requiredobjectA money order against a card (top-up, card→wallet withdraw, or issue fee). See CardOrderStatus for the lifecycle; a card.topup / card.withdraw / card.status webhook (or the reconciler) settles it.
order.id requiredinteger—
order.kind requiredstringclose_fee is the close fee charged to the cardholder on DELETE /v1/cards/{id}: amount is 0, fee is the charge. (one of topup, withdraw, issue_fee, close_fee)
order.status requiredstringfunding = the cardholder has been charged and the money is on its way to the card rail on-chain. requested = confirmed on-chain, waiting for the rail to show the credit. sent = the rail has been asked, outcome still pending. confirmed = settled. failed = definitively failed; the WHOLE charge — amount, fee, gas and merchant_fee, i.e. total — is refunded to the balance it was taken from, the cardholder's, in the same moment (top-up / issue only — a withdraw holds nothing). Nothing goes anywhere else: not to the card, not to your treasury. When the on-chain send behind a load reverted (provider_status: send_failed), no coins left the cardholder's address either, so on-chain and ledger agree. reversed is reserved for manual operator reversals of a confirmed order. (one of funding, requested, sent, confirmed, failed, reversed)
order.txidstring | nullThe hash of the on-chain send behind a load or an issue's opening balance — link your receipt to it the way you do for a deposit or a payout. Null until the send is broadcast, and for an order the rail served from its float. A failed order keeps the hash of the send that reverted, the proof of what happened; after a re-send it is the current send's hash and the reverted one is named in error. Always null on a withdraw order.
order.asset_id requiredstring—
order.amount requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
order.amount_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
order.fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
order.fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
order.gasstringThe network fee actually CHARGED for the on-chain send behind a load or issue — the figure the cardholder was debited, fixed when the order was created. Not the quote's figure: reconcile a quote against this.
order.merchant_feestringYour own fee on this order, credited to your balance.
order.totalstringEverything the cardholder was debited for this order.
order.sent_on_chainstringWhat the on-chain send behind this order carried ("0" when the rail already held it). It is the attempt, not the outcome: on a failed order whose send reverted, this still reads the amount that was sent for and nothing moved.
order.amount_credited requiredstring | nullWITHDRAW only: the NET actually credited after the provider's unload tariff, which differs per rail. null until a withdraw settles confirmed, and always null for top-ups / issue fees. Credit your user from THIS (or amount_credited_base), never from amount — the requested gross — or you over-credit by the tariff on every unload.
order.amount_credited_base requiredstring | nullInteger-string base units of amount_credited (null until a withdraw confirms).
order.card_id requiredinteger | null—
order.request_order_id requiredstring | nullServer-minted correlation handle (ct-/cw-/ci- prefix).
order.provider_status requiredstring | nullWhere a failed order failed: send_failed = the on-chain send to the rail reverted (our side; the charge was refunded, nothing left the address); anything else is the rail's own word for its refusal. Informational — act on status.
order.error requiredstring | null—
order.idempotency_key requiredstring | null—
order.created_at requiredstring<date-time>—
card requiredobject | nullThe issued card, or null while the application is still resolving (202).
card.id requiredinteger—
card.rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
card.core_id requiredstring | nullThe programme this card was issued from — the pair (core_id, bin) is what prices its loads and close. A programme holds no money; the cardholder pays. null on cards issued before programmes existed.
card.bin requiredstring | nullThe card's BIN. Read it WITH core_id, never alone: a BIN number is unique only inside its issuer, so the same number under a different programme is a different card with different fees, limits and validity.
card.product_id requiredstring | nullProvider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged.
card.product_code requiredstring | nullProvider-scoped product code; null when the card was listed untagged.
card.card_nature requiredstring(one of VIRTUAL, PHYSICAL)
card.status requiredintegerThe provider's RAW status code, verbatim — interpret via status_normalized.
card.status_normalized requiredstringProvider-neutral status. A just-issued card is issuing; it becomes active once the opening balance has landed at the issuer. (one of issuing, active, frozen, suspended, closed, expired)
card.currency requiredstring | null—
card.last4 requiredstring | null—
card.name_on_card requiredstring | nullThe cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic.
card.issued_at requiredstring<date-time>—
json
{
  "order": {
    "id": 1,
    "kind": "topup",
    "status": "funding",
    "txid": "string",
    "asset_id": "usdt_trc20",
    "amount": "1.5",
    "amount_base": "1500000000",
    "fee": "1.5",
    "fee_base": "1500000000",
    "gas": "1.5",
    "merchant_fee": "1.5",
    "total": "1.5",
    "sent_on_chain": "1.5",
    "amount_credited": "1.5",
    "amount_credited_base": "1500000000",
    "card_id": 1,
    "request_order_id": "string",
    "provider_status": "string",
    "error": "string",
    "idempotency_key": "string",
    "created_at": "2026-08-31T09:15:00.000Z"
  },
  "card": {
    "id": 1,
    "rail": "rail_a",
    "core_id": "string",
    "bin": "string",
    "product_id": "string",
    "product_code": "string",
    "card_nature": "VIRTUAL",
    "status": 1,
    "status_normalized": "issuing",
    "currency": "string",
    "last4": "string",
    "name_on_card": "string",
    "issued_at": "2026-08-31T09:15:00.000Z"
  }
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string",
  "idempotency_key": "string",
  "initial_amount": "string"
}'

#GET/v1/cards/topup-quote

Preview a top-up's fee and total · Auth: bearer API key · operationId: cardTopupQuote

Read-only, holds nothing. The fees are charged ON TOP of amount: the card receives exactly amount (so credited equals it), and total is what the cardholder's balance is debited — amount plus fee, gas and merchant_fee. Show total wherever you tell someone what a load will cost, and check it against the balance before you send. Pass card_id and user_id together to price a specific card (its rail, BIN and product); otherwise the rail's default row prices it.

#Parameters

NameInTypeRequiredDescription
amountquerystringyesDecimal amount to top up (≤6 decimal places).
providerquerystringnoRail id (a product's rail); the quote then uses that rail's fee row — the same one the charge uses.
card_idqueryintegernoPrice for THIS card — its rail, BIN and product. Send with user_id. (min 1)
user_idquerystringnoThe card's holder (your external user id). Send with card_id.

#Responses

StatusBodyMeaning
200objectThe fee preview — every component the cardholder is debited.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
asset_id requiredstring—
amount requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
amount_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
fee requiredstringOur fee.
fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
gas requiredstringThe network fee for the on-chain send behind this load.
gas_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
merchant_fee requiredstringYour own fee (PUT /v1/merchant/card-fees/load). "0" until you set one.
merchant_fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
credited requiredstringWhat reaches the card — amount itself, since the fees are charged on top. The figure to show next to "on your card".
credited_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
total requiredstringWhat the cardholder's balance is debited — amount + fee + gas + merchant_fee.
total_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
charged_to requiredstring(one of cardholder)
dynamicbooleantrue when gas was priced NOW from the live energy rate and TRX price (the same bill as a payout's network_fee); false when the live price could not be read and gas is the static fallback. Label the figure "live" or "approx" from this.
json
{
  "asset_id": "usdt_trc20",
  "amount": "1.5",
  "amount_base": "1500000000",
  "fee": "1.5",
  "fee_base": "1500000000",
  "gas": "1.5",
  "gas_base": "1500000000",
  "merchant_fee": "1.5",
  "merchant_fee_base": "1500000000",
  "credited": "1.5",
  "credited_base": "1500000000",
  "total": "1.5",
  "total_base": "1500000000",
  "charged_to": "cardholder",
  "dynamic": true
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/topup-quote?amount=string' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/cards/issue-quote

Preview what issuing a card costs the cardholder · Auth: bearer API key · operationId: cardIssueQuote

Read-only, holds nothing. total is what the cardholder's balance is debited by POST /v1/cards — our issue fee, yours, initial_amount, and the load fee and gas on that opening balance, charged on top of it. The card opens with opening_credited, which is initial_amount itself. Priced by the same code that charges, so the preview and the charge cannot differ. A card is issued FOR one of your users and only from what that user holds: when their balance is short, POST /v1/cards is refused with 409 insufficient_balance before anything is charged or applied for. Read total; adding the parts yourself is how a preview ends up smaller than the charge.

#Parameters

NameInTypeRequiredDescription
product_codequerystringyesA code from GET /v1/cards/products.
core_idquerystringnoThe programme to issue from. Optional with one programme; send it when you hold several.
initial_amountquerystringnoOpening balance (≤6 decimal places). Omitted = the product's min_opening_balance; below it is a 400 invalid_amount.

#Responses

StatusBodyMeaning
200objectThe preview — every component the cardholder is debited.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorRecord not found (or belongs to another tenant).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
asset_id requiredstring—
core_id requiredstring—
product_code requiredstring—
initial_amount requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
initial_amount_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
min_opening_balance requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
fee requiredstringOur fee — the issue fee plus our load fee on the opening balance.
fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
gas requiredstringThe network fee for the on-chain send behind the opening balance.
gas_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
merchant_fee requiredstringYour own fees on this issue (PUT /v1/merchant/card-fees/issue and /load). "0" until you set one.
merchant_fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
opening_credited requiredstringWhat the opening balance puts on the card — initial_amount itself; its load fee and gas are charged on top.
opening_credited_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
total requiredstringinitialamount + fee + gas + merchantfee — what the cardholder's balance is debited.
total_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
charged_to requiredstring(one of cardholder)
dynamicbooleantrue when gas was priced NOW from the live energy rate and TRX price; false when it is the static fallback. Always true when there is no opening balance (no send, no gas).
json
{
  "asset_id": "usdt_trc20",
  "core_id": "core-1",
  "product_code": "string",
  "initial_amount": "1.5",
  "initial_amount_base": "1500000000",
  "min_opening_balance": "1.5",
  "fee": "1.5",
  "fee_base": "1500000000",
  "gas": "1.5",
  "gas_base": "1500000000",
  "merchant_fee": "1.5",
  "merchant_fee_base": "1500000000",
  "opening_credited": "1.5",
  "opening_credited_base": "1500000000",
  "total": "1.5",
  "total_base": "1500000000",
  "charged_to": "cardholder",
  "dynamic": true
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/issue-quote?product_code=string' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/cards/orders/{order_id}

Read a card order's status (the authoritative poll) · Auth: bearer API key · operationId: getCardOrder

Return a card order by its id. This is the authoritative outcome read behind a "webhooks are hints, the poll is truth" reconcile model: a top-up / withdrawal / issue-fee order is created 202 processing (status: funding, requested or sent) and reaches confirmed or failed as the platform's reconciler and provider webhooks settle it. Poll this when a card.topup / card.withdraw webhook was missed (delivery exhausted, or your endpoint was down) so a debited operation can always be resolved without a manual investigation. Tenant-scoped: another tenant's order returns 404 and is never disclosed.

Refund discipline (unchanged): refund the customer only on status: failed (definitive — the money did not move); never on requested/sent (outcome still unknown).

#Parameters

NameInTypeRequiredDescription
order_idpathintegeryesThe order id returned by issue / top-up / withdraw. (min 1)

#Responses

StatusBodyMeaning
200CardOrderThe card order in its current (possibly terminal) state.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body — CardOrder

FieldTypeDescription
id requiredinteger—
kind requiredstringclose_fee is the close fee charged to the cardholder on DELETE /v1/cards/{id}: amount is 0, fee is the charge. (one of topup, withdraw, issue_fee, close_fee)
status requiredstringfunding = the cardholder has been charged and the money is on its way to the card rail on-chain. requested = confirmed on-chain, waiting for the rail to show the credit. sent = the rail has been asked, outcome still pending. confirmed = settled. failed = definitively failed; the WHOLE charge — amount, fee, gas and merchant_fee, i.e. total — is refunded to the balance it was taken from, the cardholder's, in the same moment (top-up / issue only — a withdraw holds nothing). Nothing goes anywhere else: not to the card, not to your treasury. When the on-chain send behind a load reverted (provider_status: send_failed), no coins left the cardholder's address either, so on-chain and ledger agree. reversed is reserved for manual operator reversals of a confirmed order. (one of funding, requested, sent, confirmed, failed, reversed)
txidstring | nullThe hash of the on-chain send behind a load or an issue's opening balance — link your receipt to it the way you do for a deposit or a payout. Null until the send is broadcast, and for an order the rail served from its float. A failed order keeps the hash of the send that reverted, the proof of what happened; after a re-send it is the current send's hash and the reverted one is named in error. Always null on a withdraw order.
asset_id requiredstring—
amount requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
amount_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
gasstringThe network fee actually CHARGED for the on-chain send behind a load or issue — the figure the cardholder was debited, fixed when the order was created. Not the quote's figure: reconcile a quote against this.
merchant_feestringYour own fee on this order, credited to your balance.
totalstringEverything the cardholder was debited for this order.
sent_on_chainstringWhat the on-chain send behind this order carried ("0" when the rail already held it). It is the attempt, not the outcome: on a failed order whose send reverted, this still reads the amount that was sent for and nothing moved.
amount_credited requiredstring | nullWITHDRAW only: the NET actually credited after the provider's unload tariff, which differs per rail. null until a withdraw settles confirmed, and always null for top-ups / issue fees. Credit your user from THIS (or amount_credited_base), never from amount — the requested gross — or you over-credit by the tariff on every unload.
amount_credited_base requiredstring | nullInteger-string base units of amount_credited (null until a withdraw confirms).
card_id requiredinteger | null—
request_order_id requiredstring | nullServer-minted correlation handle (ct-/cw-/ci- prefix).
provider_status requiredstring | nullWhere a failed order failed: send_failed = the on-chain send to the rail reverted (our side; the charge was refunded, nothing left the address); anything else is the rail's own word for its refusal. Informational — act on status.
error requiredstring | null—
idempotency_key requiredstring | null—
created_at requiredstring<date-time>—
json
{
  "id": 1,
  "kind": "topup",
  "status": "funding",
  "txid": "string",
  "asset_id": "usdt_trc20",
  "amount": "1.5",
  "amount_base": "1500000000",
  "fee": "1.5",
  "fee_base": "1500000000",
  "gas": "1.5",
  "merchant_fee": "1.5",
  "total": "1.5",
  "sent_on_chain": "1.5",
  "amount_credited": "1.5",
  "amount_credited_base": "1500000000",
  "card_id": 1,
  "request_order_id": "string",
  "provider_status": "string",
  "error": "string",
  "idempotency_key": "string",
  "created_at": "2026-08-31T09:15:00.000Z"
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/orders/1' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/cards/{id}

Secure card details · Auth: bearer API key · operationId: getCard

Local card + live balance + a link to this card's hosted card-number page (card_pan_url — drop it into an <iframe src>; it works for about ten minutes, so fetch it fresh each time you show the number). This endpoint never returns PAN/CVV — the full number is a separate, explicit call: POST /v1/cards/{id}/reveal. A live provider read failure DEGRADES to stored state (provider_read_failed: true, null balance — never fake zeros) rather than failing the call. When the read succeeded but there is no card-number page, reveal_unavailable: true is set.

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)
user_idquerystringyes—

#Responses

StatusBodyMeaning
200CardDetailsCard details (possibly degraded — see CardDetails).
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body — CardDetails

FieldTypeDescription
card requiredobject—
card.id requiredinteger—
card.rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
card.core_id requiredstring | nullThe programme this card was issued from — the pair (core_id, bin) is what prices its loads and close. A programme holds no money; the cardholder pays. null on cards issued before programmes existed.
card.bin requiredstring | nullThe card's BIN. Read it WITH core_id, never alone: a BIN number is unique only inside its issuer, so the same number under a different programme is a different card with different fees, limits and validity.
card.product_id requiredstring | nullProvider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged.
card.product_code requiredstring | nullProvider-scoped product code; null when the card was listed untagged.
card.card_nature requiredstring(one of VIRTUAL, PHYSICAL)
card.status requiredintegerThe provider's RAW status code, verbatim — interpret via status_normalized.
card.status_normalized requiredstringProvider-neutral status. A just-issued card is issuing; it becomes active once the opening balance has landed at the issuer. (one of issuing, active, frozen, suspended, closed, expired)
card.currency requiredstring | null—
card.last4 requiredstring | null—
card.name_on_card requiredstring | nullThe cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic.
card.issued_at requiredstring<date-time>—
balance requiredobject | nullLive card balance, or null when the provider read failed (never a fake zero).
balance.available requiredstringSpendable balance
balance.pending requiredstring | nullHeld amount; null (NOT 0) when the provider does not expose holds.
balance.currency requiredstring—
currency requiredstring | null—
card_pan_url requiredstring | nullLink to this card's hosted card-number page — put it straight in an <iframe src> (or open it) to show the number to the cardholder, for clients that would rather never handle a PAN. To render your own card face instead, use POST /v1/cards/{id}/reveal. It is a link on OUR domain that redirects to the page: the URL carries a one-time handle, works for about ten minutes, and is a credential in its own right — never log, cache or persist it, and fetch a fresh one from this endpoint each time you need to show the number. null when this card has no hosted page (see reveal_unavailable). This response itself never carries PAN/CVV.
expire_date requiredstring | nullYYYY-MM.
masked_numberstring | nullMasked PAN as the issuer renders it.
name_on_cardstring | nullThe name given at issue, as we stored it — not a claim about what is printed on the card. null means none was supplied.
cardholder_namestring | null—
binstring | null—
decline_countinteger | nullDeclines counted by the issuer; null when it exposes none.
decline_limitinteger | nullDeclines allowed before the issuer acts; null when it exposes none.
otp_availablebooleanTrue when a live wallet/3DS code is waiting for this card. A PEEK — reading this does not consume the code. Fetch the code itself from GET /v1/cards/{id}/otp, which does consume it.
otp_waitingobjectPresent only while otp_available is true: what the waiting code is for. Never the code itself.
otp_waiting.kind requiredstring(one of payment_3ds, wallet_provisioning)
otp_waiting.merchant_namestring—
otp_waiting.amountstring—
otp_waiting.currencystring—
otp_waiting.expires_in_seconds requiredinteger(min 0)
provider_read_failedbooleanPresent and true only when the live provider read failed (response degraded to stored state).
notestringPresent alongside providerreadfailed.
reveal_unavailablebooleanPresent and true when the card read succeeded but no card-number link is available (see reveal_note for which of the two reasons).
reveal_notestringPresent alongside reveal_unavailable.
json
{
  "card": {
    "id": 1,
    "rail": "rail_a",
    "core_id": "string",
    "bin": "string",
    "product_id": "string",
    "product_code": "string",
    "card_nature": "VIRTUAL",
    "status": 1,
    "status_normalized": "issuing",
    "currency": "string",
    "last4": "string",
    "name_on_card": "string",
    "issued_at": "2026-08-31T09:15:00.000Z"
  },
  "balance": {
    "available": "string",
    "pending": "string",
    "currency": "USD"
  },
  "currency": "string",
  "card_pan_url": "string",
  "expire_date": "string",
  "masked_number": "string",
  "name_on_card": "string",
  "cardholder_name": "string",
  "bin": "string",
  "decline_count": 1,
  "decline_limit": 1,
  "otp_available": true,
  "otp_waiting": {
    "kind": "payment_3ds",
    "merchant_name": "string",
    "amount": "string",
    "currency": "string",
    "expires_in_seconds": 0
  },
  "provider_read_failed": true,
  "note": "string",
  "reveal_unavailable": true,
  "reveal_note": "string"
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/1?user_id=string' \
  -H 'Authorization: Bearer ck_live_your_key'

#DELETE/v1/cards/{id}

Close (terminate) a card · Auth: bearer API key · operationId: closeCard

Terminates the card. One call is enough.

  1. If the card still holds a balance, this call returns the residual to us (a card→wallet withdrawal order) and answers 202 card_close_in_progress with details.card_id and details.order_id. The card is not closed yet — a 202 is not a closed card. Until it is, GET /v1/cards/{id} carries close_in_progress: true.
  2. Within about a minute of that order settling we finish the close ourselves: the close fee — ours plus yours, see GET /v1/cards/{id}/close-quote — is debited from the cardholder and the card is terminated. The card.status webhook confirms it, and the card reads status_normalized: closed. The fee shows up as a close_fee order. You may also call DELETE again yourself once the order is confirmed (card.withdraw webhook or GET /v1/cards/orders/{order_id}); that finishes it at once, and a DELETE on a card we already closed answers 200 with the card.

The close is always accepted: it never fails because a refund cannot be paid yet. Read close_refund.state on GET /v1/cards/{id} for where the money is (waiting → approved → sending → paid, or held with a reason).

A card holding less than the issuer's cancellation fee, or nothing at all, closes like any other (since 20 September 2026): the issuer keeps what is on the card, there is nothing to return, and 409 card_close_underfunded is no longer raised. minimum_card_balance on the close quote is always 0.

Money still pending on the card blocks the close with 409 card_state (the error names the amount) — wait for it to settle.

The card's balance goes back to the holder's own address on-chain, from a wallet cryptoinfra funds by hand, within 7 working days of the issuer releasing it. The close is never refused while that is arranged (409 card_refund_wallet_empty is retired): read close_refund.state on GET /v1/cards/{id} — waiting, approved, sending, paid, or held with a reason. Closing an already-closed or expired card also answers 409 card_state. If the balance cannot be read the close is refused (400) — an unreadable balance is never treated as zero.

Residual-on-close rails refund the residual as part of the close. The settlement that credits that residual back to the user arrives in a later release; until then close for these providers answers 400 not_supported_by_provider.

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)

#Request body

application/json · required · schema CardUserRef

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)

#Responses

StatusBodyMeaning
200objectCard closed; the close fee was debited from the cardholder.
202objectcard_close_in_progress — the balance is being returned; details.order_id is the withdrawal order to watch. The card is NOT closed yet; we close it within about a minute of that order settling (or call again yourself once it is confirmed). Watch card.status, or GET /v1/cards/{id} until close_in_progress is gone and status_normalized is closed.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
card requiredobject—
card.id requiredinteger—
card.rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
card.core_id requiredstring | nullThe programme this card was issued from — the pair (core_id, bin) is what prices its loads and close. A programme holds no money; the cardholder pays. null on cards issued before programmes existed.
card.bin requiredstring | nullThe card's BIN. Read it WITH core_id, never alone: a BIN number is unique only inside its issuer, so the same number under a different programme is a different card with different fees, limits and validity.
card.product_id requiredstring | nullProvider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged.
card.product_code requiredstring | nullProvider-scoped product code; null when the card was listed untagged.
card.card_nature requiredstring(one of VIRTUAL, PHYSICAL)
card.status requiredintegerThe provider's RAW status code, verbatim — interpret via status_normalized.
card.status_normalized requiredstringProvider-neutral status. A just-issued card is issuing; it becomes active once the opening balance has landed at the issuer. (one of issuing, active, frozen, suspended, closed, expired)
card.currency requiredstring | null—
card.last4 requiredstring | null—
card.name_on_card requiredstring | nullThe cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic.
card.issued_at requiredstring<date-time>—
json
{
  "card": {
    "id": 1,
    "rail": "rail_a",
    "core_id": "string",
    "bin": "string",
    "product_id": "string",
    "product_code": "string",
    "card_nature": "VIRTUAL",
    "status": 1,
    "status_normalized": "issuing",
    "currency": "string",
    "last4": "string",
    "name_on_card": "string",
    "issued_at": "2026-08-31T09:15:00.000Z"
  }
}

#Example request

bash
curl -X DELETE 'https://api.cryptoinfra.build/v1/cards/1' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string"
}'

#GET/v1/cards/{id}/balance

Live card balance · Auth: bearer API key · operationId: getCardBalance

pending is null (not 0) when the provider does not expose holds.

While the card is still being issued (status_normalized: issuing, the minute or two after POST /v1/cards) this answers 409 card_issuing — poll GET /v1/cards/{id} or wait for the card.status webhook, then retry.

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)
user_idquerystringyes—

#Responses

StatusBodyMeaning
200CardBalanceThe card's balance.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body — CardBalance

FieldTypeDescription
available requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
available_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
pending requiredstring | nullHeld amount (decimal string); null (NOT 0) when the provider does not expose holds.
pending_base requiredstring | null—
currency requiredstring—
json
{
  "available": "1.5",
  "available_base": "1500000000",
  "pending": "string",
  "pending_base": "string",
  "currency": "string"
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/1/balance?user_id=string' \
  -H 'Authorization: Bearer ck_live_your_key'

#POST/v1/cards/{id}/reveal

Reveal the full card number · Auth: bearer API key · operationId: revealCard

Full card details for the merchant that OWNS the card, on explicit request. POST (never GET) so the card id never lands in an access-log query string, and the body is the only place the secrets ever appear.

The issuer decides the shape of the answer, and exactly ONE of the two forms is populated — always branch on which:

  • Raw secrets — pan, cvv and expiry (MM/YY) are filled and reveal_url is null.
  • Hosted page — the issuer reveals only through its own page: pan and cvv come back null and reveal_url carries a URL to open. The link is SHORT-LIVED (about a minute) and single-purpose: hand it straight to the cardholder's browser. Do not cache it, store it, put it in an email, or re-serve it later — a stale link is simply dead, and mint a fresh one by calling this endpoint again.

A client that reads only pan will therefore show a blank card number for every card on a hosted-reveal product. Handle both.

While the card is still being issued (status_normalized: issuing) this answers 409 card_issuing instead of a body with neither form — poll GET /v1/cards/{id} or wait for the card.status webhook, then retry.

Handling rules (both forms). The response is sent with Cache-Control: no-store. Never log, persist, or forward these values — not to your database, not to analytics, not into a support ticket. Render them to the cardholder and drop them.

This is the way to show a card number. Both enabled rails answer it with raw secrets, so you can render your own card face — you do not need to embed anything. card_pan_url on GET /v1/cards/{id} remains for clients that would rather not handle a PAN at all, and reveal_url here covers the rare product whose issuer insists on showing the number itself. A rail with no reveal at all answers 400 not_supported_by_provider.

Scoped exactly like every other per-card call: the card must belong to this tenant AND to the user_id in the body, or the call 404s.

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)

#Request body

application/json · required · schema CardUserRef

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)

#Responses

StatusBodyMeaning
200CardSecretsThe card details, returned once. Sent with Cache-Control: no-store — do not cache, log, or persist any field.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body — CardSecrets

FieldTypeDescription
pan requiredstring | nullFull card number; null when the issuer reveals via reveal_url instead.
cvv requiredstring | nullSecurity code; null when the issuer reveals via reveal_url instead.
expiry requiredstring | nullMM/YY; null when the issuer does not disclose it here.
reveal_url requiredstring | nullShort-lived link to a hosted page showing the number, populated INSTEAD of pan/cvv. A link on OUR domain that redirects; open it immediately, do not cache or store it, and call this endpoint again for a fresh one.
json
{
  "pan": "string",
  "cvv": "string",
  "expiry": "string",
  "reveal_url": "string"
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/1/reveal' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string"
}'

#GET/v1/cards/{id}/otp

Fetch the card's live one-time code · Auth: bearer API key · operationId: getCardOtp

Returns the verification code the ISSUER relayed for this card — either a 3DS code that approves a payment, or a wallet code that adds the card to Apple/Google Pay.

The code never reaches the cardholder's own phone or email. For an issued-on-your-program card the issuer sends it to us, so whichever channel the wallet or checkout offers your customer, the code arrives here. This endpoint is how you hand it to them.

Three properties to build against:

  • Single read. Fetching CONSUMES the code — a second call returns 404. Never poll this endpoint; call it when your user is ready to type the code. To know a code is waiting, read otp_available on GET /v1/cards/{id} (a peek that does not consume) or listen for the card.wallet_otp webhook. Your API key and your dashboard each have their own read: fetching a code from your server does not take it away from someone looking at the card in the dashboard, and the other way round.
  • Short-lived. Codes expire in about ten minutes; expires_in_seconds is the remaining life at the moment of the read.
  • Never stored. The code is held in memory only — not in the database, not in a webhook payload, not in a log. If it is lost, the customer must trigger a new one.

A 404 is deliberately uniform: it does not distinguish "no code arrived" from "already read", "expired", or "not your card".

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)
user_idquerystringnoOptional second proof of ownership. When supplied it must match the end-user the code was bound to at receipt, or the read returns 404.

#Responses

StatusBodyMeaning
200objectThe code, returned once. Sent with Cache-Control: no-store — do not cache, log, or persist it.
401ErrorMissing, malformed or unknown API key.
404objectNo code is available. Uniform by design — no code arrived, it was already read, it expired, or the card is not yours.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
card_id requiredinteger—
code requiredstringThe one-time code, exactly as the issuer sent it.
kind requiredstringpayment_3ds interrupts a checkout; wallet_provisioning gates adding the card to Apple/Google Pay. Surface them differently — they reach the customer at different moments. (one of payment_3ds, wallet_provisioning)
merchant_namestringpayment_3ds only, when the issuer sent it: the shop the code approves a payment at. Show it so the cardholder can check the code is for their own checkout.
amountstringpayment_3ds only, when sent: the amount of that payment, as a decimal string.
currencystringpayment_3ds only, when sent: the currency of amount.
expires_in_seconds requiredinteger(min 0)
json
{
  "card_id": 1,
  "code": "441524",
  "kind": "payment_3ds",
  "merchant_name": "EXAMPLE STORE",
  "amount": "12.50",
  "currency": "USD",
  "expires_in_seconds": 477
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/1/otp' \
  -H 'Authorization: Bearer ck_live_your_key'

#POST/v1/cards/{id}/topup

Top up a card · Auth: bearer API key · operationId: cardTopup

Debits total from the cardholder's balance immediately (see GET /v1/cards/topup-quote: amount + our fee + gas + your fee), sends what the rail needs on-chain, and funds the card once that send is credited. Returns 202 with the order in funding; it moves to requested when the send confirms, sent when the rail is asked, confirmed when the card is funded — a card.topup webhook or the reconciler tells you. A definitive failure at any stage refunds the charge in full. Treat 202 as pending; refund your user ONLY on a definitive failure (409 provider_rejected, or a webhook with order status failed), never on the 202 itself. idempotency_key is required.

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)

#Request body

application/json · required · schema CardMoneyRequest

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)
amount requiredstringDecimal amount in major units (USD/USDT), at most 6 decimal places. (pattern ^\d+(\.\d+)?$)
idempotency_key requiredstringRequired; replay-safe. (minLength 1; maxLength 255)

#Responses

StatusBodyMeaning
202CardOrderTop-up accepted; provider outcome pending.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

202 response body — CardOrder

FieldTypeDescription
id requiredinteger—
kind requiredstringclose_fee is the close fee charged to the cardholder on DELETE /v1/cards/{id}: amount is 0, fee is the charge. (one of topup, withdraw, issue_fee, close_fee)
status requiredstringfunding = the cardholder has been charged and the money is on its way to the card rail on-chain. requested = confirmed on-chain, waiting for the rail to show the credit. sent = the rail has been asked, outcome still pending. confirmed = settled. failed = definitively failed; the WHOLE charge — amount, fee, gas and merchant_fee, i.e. total — is refunded to the balance it was taken from, the cardholder's, in the same moment (top-up / issue only — a withdraw holds nothing). Nothing goes anywhere else: not to the card, not to your treasury. When the on-chain send behind a load reverted (provider_status: send_failed), no coins left the cardholder's address either, so on-chain and ledger agree. reversed is reserved for manual operator reversals of a confirmed order. (one of funding, requested, sent, confirmed, failed, reversed)
txidstring | nullThe hash of the on-chain send behind a load or an issue's opening balance — link your receipt to it the way you do for a deposit or a payout. Null until the send is broadcast, and for an order the rail served from its float. A failed order keeps the hash of the send that reverted, the proof of what happened; after a re-send it is the current send's hash and the reverted one is named in error. Always null on a withdraw order.
asset_id requiredstring—
amount requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
amount_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
gasstringThe network fee actually CHARGED for the on-chain send behind a load or issue — the figure the cardholder was debited, fixed when the order was created. Not the quote's figure: reconcile a quote against this.
merchant_feestringYour own fee on this order, credited to your balance.
totalstringEverything the cardholder was debited for this order.
sent_on_chainstringWhat the on-chain send behind this order carried ("0" when the rail already held it). It is the attempt, not the outcome: on a failed order whose send reverted, this still reads the amount that was sent for and nothing moved.
amount_credited requiredstring | nullWITHDRAW only: the NET actually credited after the provider's unload tariff, which differs per rail. null until a withdraw settles confirmed, and always null for top-ups / issue fees. Credit your user from THIS (or amount_credited_base), never from amount — the requested gross — or you over-credit by the tariff on every unload.
amount_credited_base requiredstring | nullInteger-string base units of amount_credited (null until a withdraw confirms).
card_id requiredinteger | null—
request_order_id requiredstring | nullServer-minted correlation handle (ct-/cw-/ci- prefix).
provider_status requiredstring | nullWhere a failed order failed: send_failed = the on-chain send to the rail reverted (our side; the charge was refunded, nothing left the address); anything else is the rail's own word for its refusal. Informational — act on status.
error requiredstring | null—
idempotency_key requiredstring | null—
created_at requiredstring<date-time>—
json
{
  "id": 1,
  "kind": "topup",
  "status": "funding",
  "txid": "string",
  "asset_id": "usdt_trc20",
  "amount": "1.5",
  "amount_base": "1500000000",
  "fee": "1.5",
  "fee_base": "1500000000",
  "gas": "1.5",
  "merchant_fee": "1.5",
  "total": "1.5",
  "sent_on_chain": "1.5",
  "amount_credited": "1.5",
  "amount_credited_base": "1500000000",
  "card_id": 1,
  "request_order_id": "string",
  "provider_status": "string",
  "error": "string",
  "idempotency_key": "string",
  "created_at": "2026-08-31T09:15:00.000Z"
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/1/topup' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string",
  "amount": "string",
  "idempotency_key": "string"
}'

#GET/v1/cards/{id}/close-quote

What closing this card would charge the cardholder · Auth: bearer API key · operationId: cardCloseQuote

Read-only. Our flat close fee, the live network figure for the on-chain refund, plus your own fee if you set one — total is the holder's debit on DELETE /v1/cards/{id}.

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)
user_idquerystringyes—

#Responses

StatusBodyMeaning
200objectThe close preview.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.

200 response body

FieldTypeDescription
asset_id requiredstring—
fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
gasstringThe live network figure for returning the balance to the holder's address on-chain. Part of total.
gas_basestringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
dynamicbooleantrue when gas was priced live now; false on the fallback.
merchant_fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
merchant_fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
total requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
total_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
charged_to requiredstringWhere the close is paid from. Every cost — ours, the network figure, yours and the rail's release charge — comes out of the balance being returned, never out of the holder's wallet balance. They receive refund_quote.refund. (one of refund)
minimum_card_balance requiredstringAlways 0 since 20 September 2026: a close is no longer refused because the card holds little or nothing. Kept so existing clients do not break.
issuer_feestringWhat the issuer keeps on the card as its cancellation charge when the card holds that much. A card holding less simply closes, and the issuer keeps whatever is there.
issuer_fee_basestringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
minimum_card_balance_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
refund_quoteobjectWhat the holder actually receives, on the card's balance right now — the figure to show them before they close. card_balance is read live from the rail; when it cannot be read it is null, live is false and the other lines are 0. The issuer's cancellation fee comes off the card, and the rail's charge for releasing it, our close fee, the network cost of the send and your own fee all come off what is returned. Nothing is taken from the holder's wallet balance.
refund_quote.card_balance requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
refund_quote.card_balance_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
refund_quote.issuer_fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
refund_quote.issuer_fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
refund_quote.rail_return_fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
refund_quote.rail_return_fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
refund_quote.close_fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
refund_quote.close_fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
refund_quote.gas requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
refund_quote.gas_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
refund_quote.merchant_fee requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
refund_quote.merchant_fee_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
refund_quote.refund requiredstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
refund_quote.refund_base requiredstringExact integer amount in the asset's smallest unit. (pattern ^[0-9]+$)
refund_quote.live requiredboolean—
refund_eta_typicalstringWhat a refund usually takes once the card is closed (e.g. "24–48 hours").
refund_eta_max_daysintegerThe issuer's outside limit. Cancelling a card opens a settlement period during which the balance is not released, and the issuer states it can run to this many days. It has been far quicker in practice, but this is the figure to quote a customer rather than a promise of your own.
refund_eta_working_days requiredintegerThe card's balance reaches the holder's own address on-chain within this many working days of the close.
json
{
  "asset_id": "usdt_trc20",
  "fee": "1.5",
  "fee_base": "1500000000",
  "gas": "1.5",
  "gas_base": "1500000000",
  "dynamic": true,
  "merchant_fee": "1.5",
  "merchant_fee_base": "1500000000",
  "total": "1.5",
  "total_base": "1500000000",
  "charged_to": "refund",
  "minimum_card_balance": "1.5",
  "issuer_fee": "1.5",
  "issuer_fee_base": "1500000000",
  "minimum_card_balance_base": "1500000000",
  "refund_quote": {
    "card_balance": "1.5",
    "card_balance_base": "1500000000",
    "issuer_fee": "1.5",
    "issuer_fee_base": "1500000000",
    "rail_return_fee": "1.5",
    "rail_return_fee_base": "1500000000",
    "close_fee": "1.5",
    "close_fee_base": "1500000000",
    "gas": "1.5",
    "gas_base": "1500000000",
    "merchant_fee": "1.5",
    "merchant_fee_base": "1500000000",
    "refund": "1.5",
    "refund_base": "1500000000",
    "live": true
  },
  "refund_eta_typical": "string",
  "refund_eta_max_days": 1,
  "refund_eta_working_days": 1
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/1/close-quote?user_id=string' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/merchant/card-fees

Your own card fees, on top of ours · Auth: bearer API key · operationId: merchantCardFeesGet

Ours is the floor and is always charged; what you set here is ADDED to it, charged to the cardholder, and credited to your own balance in the same journal as the charge. Nothing to move, nothing to pay out. Works with an API key or a dashboard login.

#Responses

StatusBodyMeaning
200objectOur prices and yours, per operation.
401ErrorMissing, malformed or unknown API key.

200 response body

FieldTypeDescription
max_percent_bpsinteger—
charged_tostring(one of cardholder)
oursobject—
ours.issueobject—
ours.issue.flatstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
ours.issue.flat_physicalstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
ours.loadobject—
ours.load.percent_bpsinteger—
ours.load.minstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
ours.load.gasstringThe network line of every load, priced when you ask — the live cost of the two TRON transfers behind a load, the same way a payout's network_fee is priced. Never set by hand, never stored: it moves with energy prices and the TRX price. If you mirror this figure, show gas_dynamic beside it and re-read it, do not print it as a fixed charge.
ours.load.gas_dynamicbooleantrue = priced live now; false = the live price could not be read and a conservative fallback applies.
ours.load.gas_priced_atstring<date-time>When this figure was priced — the moment of this response.
ours.closeobject—
ours.close.flatstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
yoursobject[]—
yours[].operationstring(one of issue, load, close)
yours[].percent_bpsinteger100 = 1%.
yours[].flatstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
yours[].updated_atstring<date-time> | null—
json
{
  "max_percent_bps": 1000,
  "charged_to": "cardholder",
  "ours": {
    "issue": {
      "flat": "1.5",
      "flat_physical": "1.5"
    },
    "load": {
      "percent_bps": 1,
      "min": "1.5",
      "gas": "1.5",
      "gas_dynamic": true,
      "gas_priced_at": "2026-08-31T09:15:00.000Z"
    },
    "close": {
      "flat": "1.5"
    }
  },
  "yours": [
    {
      "operation": "issue",
      "percent_bps": 1,
      "flat": "1.5",
      "updated_at": "2026-08-31T09:15:00.000Z"
    }
  ]
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/merchant/card-fees' \
  -H 'Authorization: Bearer ck_live_your_key'

#PUT/v1/merchant/card-fees/{operation}

Set your fee on one card operation · Auth: bearer API key · operationId: merchantCardFeeSet

A percentage of the amount (percent_bps, 100 = 1%, at most 1000), a flat amount (flat, in the card asset), or both. Omitted fields keep their current value. Refused with 409 no_own_account when your account has no balance of its own for the fee to land in.

#Parameters

NameInTypeRequiredDescription
operationpathstringyes— (one of issue, load, close)

#Request body

application/json · required

FieldTypeDescription
percent_bpsinteger(min 0; max 1000)
flatstring(pattern ^\d+(\.\d+)?$)

#Responses

StatusBodyMeaning
200objectThe fee as saved.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
409Errorno_own_account — open an own-balance account first.

200 response body

FieldTypeDescription
operationstring(one of issue, load, close)
percent_bpsinteger—
flatstringA monetary value as a decimal string. Always paired with a *_base field that carries the exact integer base-unit value; compute with the _base string.
json
{
  "operation": "issue",
  "percent_bps": 1,
  "flat": "1.5"
}

#Example request

bash
curl -X PUT 'https://api.cryptoinfra.build/v1/merchant/card-fees/issue' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{}'

#DELETE/v1/merchant/card-fees/{operation}

Remove your fee on one card operation · Auth: bearer API key · operationId: merchantCardFeeClear

#Parameters

NameInTypeRequiredDescription
operationpathstringyes— (one of issue, load, close)

#Responses

StatusBodyMeaning
204—Removed; the cardholder pays exactly our rate again.
401ErrorMissing, malformed or unknown API key.

#Example request

bash
curl -X DELETE 'https://api.cryptoinfra.build/v1/merchant/card-fees/issue' \
  -H 'Authorization: Bearer ck_live_your_key'

#POST/v1/cards/{id}/withdraw

Retired — moving part of a card balance back to the wallet · Deprecated. · Auth: bearer API key · operationId: cardWithdraw

Retired on 2026-09-14. Always answers 410 card_unload_retired. The only way value leaves a card is DELETE /v1/cards/{id}, which returns the whole balance to the holder's own address on-chain. An unload would credit the holder's balance with coins that never reach their address.

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)

#Request body

application/json · required · schema CardMoneyRequest

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)
amount requiredstringDecimal amount in major units (USD/USDT), at most 6 decimal places. (pattern ^\d+(\.\d+)?$)
idempotency_key requiredstringRequired; replay-safe. (minLength 1; maxLength 255)

#Responses

StatusBodyMeaning
401ErrorMissing, malformed or unknown API key.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
410ErrorRetired — card_unload_retired. Close the card instead.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/1/withdraw' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string",
  "amount": "string",
  "idempotency_key": "string"
}'

#POST/v1/cards/{id}/freeze

Freeze a card · Auth: bearer API key · operationId: freezeCard

Freezes an active card (409 card_state otherwise). The authoritative status is confirmed by a card.status webhook.

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)

#Request body

application/json · required · schema CardUserRef

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)

#Responses

StatusBodyMeaning
200objectCard frozen.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
card requiredobject—
card.id requiredinteger—
card.rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
card.core_id requiredstring | nullThe programme this card was issued from — the pair (core_id, bin) is what prices its loads and close. A programme holds no money; the cardholder pays. null on cards issued before programmes existed.
card.bin requiredstring | nullThe card's BIN. Read it WITH core_id, never alone: a BIN number is unique only inside its issuer, so the same number under a different programme is a different card with different fees, limits and validity.
card.product_id requiredstring | nullProvider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged.
card.product_code requiredstring | nullProvider-scoped product code; null when the card was listed untagged.
card.card_nature requiredstring(one of VIRTUAL, PHYSICAL)
card.status requiredintegerThe provider's RAW status code, verbatim — interpret via status_normalized.
card.status_normalized requiredstringProvider-neutral status. A just-issued card is issuing; it becomes active once the opening balance has landed at the issuer. (one of issuing, active, frozen, suspended, closed, expired)
card.currency requiredstring | null—
card.last4 requiredstring | null—
card.name_on_card requiredstring | nullThe cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic.
card.issued_at requiredstring<date-time>—
json
{
  "card": {
    "id": 1,
    "rail": "rail_a",
    "core_id": "string",
    "bin": "string",
    "product_id": "string",
    "product_code": "string",
    "card_nature": "VIRTUAL",
    "status": 1,
    "status_normalized": "issuing",
    "currency": "string",
    "last4": "string",
    "name_on_card": "string",
    "issued_at": "2026-08-31T09:15:00.000Z"
  }
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/1/freeze' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string"
}'

#POST/v1/cards/{id}/unfreeze

Unfreeze a card · Auth: bearer API key · operationId: unfreezeCard

Unfreezes a frozen card (409 card_state otherwise).

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)

#Request body

application/json · required · schema CardUserRef

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)

#Responses

StatusBodyMeaning
200objectCard unfrozen.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
409ErrorA conflicting card state. error is one of: kycnotapproved (details.kycstatus), cardstate, cardissuing (the card is still being issued — under two minutes; details.card_id; poll GET /v1/cards/{id} until statusnormalized is active, or wait for the card.status webhook, then retry), providerrejected (the rail refused — DEFINITIVE, so retrying the identical call cannot help; details.order_id on a money call, where any charge is already reversed, and details.provider_code otherwise), emailtaken (details.email — the rail already holds an account on that address; send a different one), idempotencyconflict, insufficientbalance, noownaccount (you have set a card fee of your own but your account has no balance of its own to credit it to — ask us to open one).
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
card requiredobject—
card.id requiredinteger—
card.rail requiredstringOpaque, stable id of the card rail a product, holder or card sits on — and the value to pass as provider when a call needs disambiguating. It tells you WHICH rail, never who runs it: two cards on different rails are different arrangements of ours, and that is all you need to route a call correctly. Read it from the resource and echo it back; never hard-code one, and never parse it — the string is an identifier, not a description.
card.core_id requiredstring | nullThe programme this card was issued from — the pair (core_id, bin) is what prices its loads and close. A programme holds no money; the cardholder pays. null on cards issued before programmes existed.
card.bin requiredstring | nullThe card's BIN. Read it WITH core_id, never alone: a BIN number is unique only inside its issuer, so the same number under a different programme is a different card with different fees, limits and validity.
card.product_id requiredstring | nullProvider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged.
card.product_code requiredstring | nullProvider-scoped product code; null when the card was listed untagged.
card.card_nature requiredstring(one of VIRTUAL, PHYSICAL)
card.status requiredintegerThe provider's RAW status code, verbatim — interpret via status_normalized.
card.status_normalized requiredstringProvider-neutral status. A just-issued card is issuing; it becomes active once the opening balance has landed at the issuer. (one of issuing, active, frozen, suspended, closed, expired)
card.currency requiredstring | null—
card.last4 requiredstring | null—
card.name_on_card requiredstring | nullThe cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic.
card.issued_at requiredstring<date-time>—
json
{
  "card": {
    "id": 1,
    "rail": "rail_a",
    "core_id": "string",
    "bin": "string",
    "product_id": "string",
    "product_code": "string",
    "card_nature": "VIRTUAL",
    "status": 1,
    "status_normalized": "issuing",
    "currency": "string",
    "last4": "string",
    "name_on_card": "string",
    "issued_at": "2026-08-31T09:15:00.000Z"
  }
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/1/unfreeze' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string"
}'

#GET/v1/cards/{id}/transactions

Card transaction history · Auth: bearer API key · operationId: listCardTransactions

Normalized live passthrough from the provider (nothing is stored in v1).

#Parameters

NameInTypeRequiredDescription
idpathintegeryesThe card's numeric id (from GET /v1/cards). (min 1)
user_idquerystringyes—
limitqueryintegernoRows per page. (min 1; max 100)
pagequeryintegerno1-based page number. (min 1; max 100000)
page_sizequeryintegernoDeprecated alias of limit; limit wins when both are sent. (min 1; max 100)
page_numqueryintegernoDeprecated alias of page; page wins when both are sent. (min 1; max 100000)

#Responses

StatusBodyMeaning
200objectTransactions.
401ErrorMissing, malformed or unknown API key.
404ErrorUnknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
transactions requiredobject[]—
transactions[].id requiredstring—
transactions[].occurred_atstring | null—
transactions[].amountstring | nullDecimal string; null when the provider omits it.
transactions[].amount_basestring | null—
transactions[].currencystring | null—
transactions[].feestring | null—
transactions[].fee_basestring | null—
transactions[].merchant_namestring | null—
transactions[].mccstring | null—
transactions[].typestring | nullTransaction code: 170 payment, 171 refund, 172 card fee, 173 ATM withdrawal, 174 pre-authorization, 150 top-up, 140 card issue.
transactions[].statusstring | null1 processing, 2 success, 3 failed.
transactions[].authorization_codestring | null—
transactions[].related_authorization_codestring | null—
json
{
  "transactions": [
    {
      "id": "string",
      "occurred_at": "string",
      "amount": "string",
      "amount_base": "string",
      "currency": "string",
      "fee": "string",
      "fee_base": "string",
      "merchant_name": "string",
      "mcc": "string",
      "type": "string",
      "status": "string",
      "authorization_code": "string",
      "related_authorization_code": "string"
    }
  ]
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/1/transactions?user_id=string' \
  -H 'Authorization: Bearer ck_live_your_key'

#GET/v1/cards/delivery/regions

Physical-card delivery regions · Auth: bearer API key · operationId: listCardDeliveryRegions

Shipping regions for physical cards. Supported on one rail in v1 — returns 400 not_supported_by_provider on the others.

#Parameters

NameInTypeRequiredDescription
localquerystringnoDisplay locale (default enUS). (one of zh_CN, zh_HK, en_US, ko_KR)_

#Responses

StatusBodyMeaning
200objectRegions.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

200 response body

FieldTypeDescription
regions requiredobject[]—
regions[].idstringRegion id; pass its numeric value as countryregionid to POST /v1/cards/delivery/address.
regions[].country_regionstring—
regions[].country_region_zhstring—
regions[].national_codestringDialing code.
regions[].pricestring | nullPostage as a decimal string; currency unspecified. null = unknown (NOT free).
json
{
  "regions": [
    {
      "id": "string",
      "country_region": "string",
      "country_region_zh": "string",
      "national_code": "string",
      "price": "string"
    }
  ]
}

#Example request

bash
curl -X GET 'https://api.cryptoinfra.build/v1/cards/delivery/regions' \
  -H 'Authorization: Bearer ck_live_your_key'

#POST/v1/cards/delivery/address

Add a physical-card delivery address · Auth: bearer API key · operationId: addCardDeliveryAddress

Creates a mailing address at the provider AND records its ownership locally. The returned delivery_address_id is passed as delivery_address_id when issuing a PHYSICAL card (issuance only accepts an id recorded for the same holder).

#Request body

application/json · required

FieldTypeDescription
user_id requiredstring(minLength 1; maxLength 255)
country_region_id requiredintegerRegion id from GET /v1/cards/delivery/regions. (min 1)
country requiredstring(minLength 1; maxLength 128)
city requiredstring(minLength 1; maxLength 128)
receiver_name requiredstring(minLength 1; maxLength 128)
receiver_mobile requiredstring(minLength 1; maxLength 32)
receiver_address requiredstring(minLength 1; maxLength 512)
post_code requiredstring(minLength 1; maxLength 32)

#Responses

StatusBodyMeaning
201objectAddress created.
400ErrorRequest validation failed or a domain rule was violated. error is one of: validationerror, invalidrequest, invalidamount, cardcorerequired, cardproviderrequired, unknownprovider, notsupportedby_provider. card_core_required carries details.cores — the card programmes this call will accept as core_id. It appears when you hold more than one programme that could serve the request. We refuse rather than pick, because picking would spend a balance that did not pay for the card and nothing in the response would say so. Send core_id. card_provider_required carries details.rails — the rail ids this call will accept as provider. It only ever appears when two enabled rails could both serve the request; naming a core_id, or having cards on just one rail, resolves it without your having to choose.
401ErrorMissing, malformed or unknown API key.
404ErrorNo matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant.
503ErrorTemporarily unable to serve this card call — your request was valid. Retry; nothing has been charged. error is one of: provider_unavailable — the rail did not answer, or answered unreadably. Transient. card_rail_unavailable — this card's own rail is not reachable (it may have been closed to new business). Other rails are unaffected, and the card itself is fine. cards_disabled — no card rail is enabled on this deployment at all. rail_unfunded — the platform cannot fund the on-chain send behind this load right now. Nothing was charged; retry later. 409 card_coins_not_on_chain — the holder's coins are not at their own address yet (a close refund or a deposit still on its way): a load pays its own way on-chain from that address and waits for them. details carry needed. Nothing was charged; try again once they have arrived.

201 response body

FieldTypeDescription
delivery_address_id requiredinteger—
json
{
  "delivery_address_id": 1
}

#Example request

bash
curl -X POST 'https://api.cryptoinfra.build/v1/cards/delivery/address' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "user_id": "string",
  "country_region_id": 1,
  "country": "string",
  "city": "string",
  "receiver_name": "string",
  "receiver_mobile": "string",
  "receiver_address": "string",
  "post_code": "string"
}'