#Cards
Crypto debit cards — holders, KYC, issuance, top-up/withdraw, reveal, and delivery
32 endpoints. Generated from docs/openapi.yaml v1.0.0.
| Endpoint | Summary |
|---|---|
GET /v1/cards/holders | List your card holders |
POST /v1/cards/holders | Register a card holder |
GET /v1/cards/summary | Card counts at a glance |
GET /v1/cards/orders | Card-order ledger |
GET /v1/cards/kyc/countries | KYC country catalogue |
POST /v1/cards/kyc/upload | Upload a KYC document |
POST /v1/cards/kyc | Submit the KYC dossier |
GET /v1/cards/kyc/status | Authoritative KYC status |
POST /v1/kyc/start | Start (or resume) identity verification |
GET /v1/kyc/status | Current KYC verdict (reconciled at read time) |
GET /v1/cards/products | Card product catalogue |
GET /v1/cards | List cards |
POST /v1/cards | Issue a card |
GET /v1/cards/topup-quote | Preview a top-up's fee and total |
GET /v1/cards/issue-quote | Preview 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}/balance | Live card balance |
POST /v1/cards/{id}/reveal | Reveal the full card number |
GET /v1/cards/{id}/otp | Fetch the card's live one-time code |
POST /v1/cards/{id}/topup | Top up a card |
GET /v1/cards/{id}/close-quote | What closing this card would charge the cardholder |
GET /v1/merchant/card-fees | Your 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}/withdraw | Retired — moving part of a card balance back to the wallet |
POST /v1/cards/{id}/freeze | Freeze a card |
POST /v1/cards/{id}/unfreeze | Unfreeze a card |
GET /v1/cards/{id}/transactions | Card transaction history |
GET /v1/cards/delivery/regions | Physical-card delivery regions |
POST /v1/cards/delivery/address | Add 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
| Status | Body | Meaning |
|---|---|---|
200 | object | Your card holders. |
401 | Error | Missing, malformed or unknown API key. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
holders required | object[] | — |
holders[].id required | integer | — |
holders[].user_id required | string | Your externaluserid. |
holders[].rail required | string | Opaque, 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 required | string | The holder's display name as registered. |
holders[].program required | string | — |
holders[].email required | string | The email the holder was registered with. |
holders[].kyc_status required | string | (one of none, pending, approved, rejected) |
holders[].card_count required | integer | — |
holders[].created_at required | string<date-time> | — |
{
"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
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
| Field | Type | Description |
|---|---|---|
user_id required | string | Your externaluserid. (minLength 1; maxLength 255) |
email required | string<email> | (maxLength 255) |
provider | string | Rail 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_name | string | (maxLength 64) |
last_name | string | (maxLength 64) |
phone | string | E.164 |
date_of_birth | string<date> | — |
country | string | (minLength 2; maxLength 2) |
zip_code | string | (maxLength 16) |
address | string | (maxLength 128) |
city | string | (maxLength 64) |
state | string | (maxLength 64) |
#Responses
| Status | Body | Meaning |
|---|---|---|
201 | CardHolder | Holder registered (or already existed). |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown externaluserid for this tenant. |
409 | Error | A 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). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
user_id required | string | Your externaluserid. |
rail required | string | Opaque, 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 required | string | — |
kyc_status required | string | Legacy 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 required | string | null | Provider remark (e.g. a rejection reason); null when none. |
{
"user_id": "string",
"rail": "rail_a",
"email": "string",
"kyc_status": "none",
"kyc_remark": "string"
}#Example request
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
core_id | query | string | no | Count one programme's cards only. |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | The counts. |
401 | Error | Missing, malformed or unknown API key. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
cards required | integer | — |
by_status required | object | Keys are issuing/active/frozen/suspended/closed/expired. |
holders required | integer | — |
on_cards | string | Sum of live card balances (≤60s cache); null when none were readable. |
processing | string | Sum of pending holds. |
balances_read | integer | — |
{
"cards": 1,
"by_status": {},
"holders": 1,
"on_cards": "string",
"processing": "string",
"balances_read": 1
}#Example request
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
card_id | query | integer | no | — (min 1) |
core_id | query | string | no | One programme's orders. |
limit | query | integer | no | — (min 1; max 500) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | The orders. |
401 | Error | Missing, malformed or unknown API key. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
orders required | object[] | — |
orders[].id | integer | — |
orders[].kind | string | close_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[].status | string | — |
orders[].amount | string | — |
orders[].fee | string | — |
orders[].amount_credited | string | — |
orders[].card_id | integer | — |
orders[].card_last4 | string | — |
orders[].rail | string | Opaque, 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_id | string | — |
orders[].error | string | — |
orders[].txid | string | The on-chain send's hash; null until broadcast or when the rail already held the money. |
orders[].created_at | string<date-time> | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
name | query | string | no | Optional local case-insensitive filter on name/en_name. |
provider | query | string | no | Rail id (a product's rail); omit unless two enabled rails could both answer. |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Supported countries. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
countries required | object[] | — |
countries[].name | string | — |
countries[].en_name | string | — |
countries[].value | string | ISO alpha-3 code. |
countries[].prof_value | string | — |
countries[].code | string | — |
countries[].telephone_code | string | — |
countries[].need_certificate | boolean | true → this country additionally needs filetype/fileurl/face_url in POST /v1/cards/kyc. |
{
"countries": [
{
"name": "string",
"en_name": "string",
"value": "string",
"prof_value": "string",
"code": "string",
"telephone_code": "string",
"need_certificate": true
}
]
}#Example request
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
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
provider | string | Opaque, 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 required | string | (minLength 1; maxLength 255) |
content_base64 required | string | Base64 of the document (≤5 MB decoded). |
#Responses
| Status | Body | Meaning |
|---|---|---|
201 | object | Uploaded. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | No matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
file_url required | string | An 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}$) |
{
"file_url": "string"
}#Example request
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
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
provider | string | Opaque, 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 required | string | (minLength 1; maxLength 128) |
last_name required | string | (minLength 1; maxLength 128) |
id_no required | string | (minLength 1; maxLength 64) |
email required | string<email> | (maxLength 255) |
nation_code required | string | Nationality, ISO alpha-3 (the value from GET /v1/cards/kyc/countries). (minLength 3; maxLength 3) |
cert_type required | integer | 1 = ID card, 2 = passport, 3 = driver's licence. (one of 1, 2, 3) |
id_url required | string | FRONT document photo — the kycfile_… handle from POST /v1/cards/kyc/upload, or a URL you host. (maxLength 1024) |
birthday required | string | YYYY-MM-DD. (pattern ^\d{4}-\d{2}-\d{2}$) |
country_code required | string | Country of residence, ISO alpha-3. (minLength 3; maxLength 3) |
area_code required | string | Dialing code without '+' (e.g. '86'). (minLength 1; maxLength 8) |
phone required | string | (minLength 3; maxLength 32) |
id_back_url | string | BACK document photo (handle or URL, as idurl); REQUIRED for certtype 1 (ID card) and 3 (driver's licence). (maxLength 1024) |
file_type | integer | (min 1; max 100) |
file_url | string | Handle from the upload, or a URL you host. (maxLength 1024) |
face_url | string | Handle from the upload, or a URL you host. (maxLength 1024) |
selfie_url | string | Handle from the upload, or a URL you host. (maxLength 1024) |
reference_id | string | (maxLength 128) |
reference_type | string | (maxLength 64) |
#Responses
| Status | Body | Meaning |
|---|---|---|
202 | CardHolder | KYC accepted; the holder's status is now pending. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | No matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
user_id required | string | Your externaluserid. |
rail required | string | Opaque, 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 required | string | — |
kyc_status required | string | Legacy 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 required | string | null | Provider remark (e.g. a rejection reason); null when none. |
{
"user_id": "string",
"rail": "rail_a",
"email": "string",
"kyc_status": "none",
"kyc_remark": "string"
}#Example request
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
user_id | query | string | yes | — |
provider | query | string | no | Rail 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
| Status | Body | Meaning |
|---|---|---|
200 | CardHolder | The holder with its current KYC status. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | No matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
user_id required | string | Your externaluserid. |
rail required | string | Opaque, 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 required | string | — |
kyc_status required | string | Legacy 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 required | string | null | Provider remark (e.g. a rejection reason); null when none. |
{
"user_id": "string",
"rail": "rail_a",
"email": "string",
"kyc_status": "none",
"kyc_remark": "string"
}#Example request
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
| Field | Type | Description |
|---|---|---|
user_id required | string | The tenant's user ref (same one that scopes card reads). (minLength 1; maxLength 255) |
language | string | Optional ISO language hint for the hosted flow. (minLength 2; maxLength 8) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Session created or resumed (or already approved). |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown externaluserid for this tenant. |
503 | Error | KYC is disabled or not configured. |
200 response body
| Field | Type | Description |
|---|---|---|
status required | string | cryptoinfra-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) |
url | string | Hosted verification URL; absent when already approved. |
{
"status": "none",
"url": "string"
}#Example request
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
user_id | query | string | yes | — |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | The current verdict. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown externaluserid for this tenant. |
200 response body
| Field | Type | Description |
|---|---|---|
status required | string | cryptoinfra-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) |
{
"status": "none"
}#Example request
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
core_id | query | string | no | Show one programme's shelf, priced for it. Omit to see every programme on offer. |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Products. |
401 | Error | Missing, malformed or unknown API key. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
asset_id required | string | — |
gas | string | The network line inside every issue_total below, priced for this response. |
gas_dynamic | boolean | true = priced live now; false = the static fallback is inside the totals. |
gas_priced_at | string<date-time> | — |
issue_fee | string | Deprecated — virtual-card fee. Use products[].issue_fee. |
issue_fee_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
products required | object[] | — |
products[].id required | integer | null | Numeric id, kept for back-compat; null when the product code is not numeric. |
products[].rail required | string | Opaque, 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_id | string | The 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_name | string | What this programme is called, for display. e.g. Core-1. |
products[].code required | string | Provider-scoped product code — pass as product_code to POST /v1/cards. |
products[].card_nature required | string | (one of VIRTUAL, PHYSICAL) |
products[].ccy required | string | Card currency (only USD is supported for money operations). |
products[].region | string | null | — |
products[].card_bin | string | null | — |
products[].card_brand | string | null | — |
products[].card_mode | string | null | — |
products[].title | string | null | — |
products[].requires_kyc | boolean | Whether 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_limit | string | null | Minimum FIRST top-up that activates a virtual card (decimal string); null when none. |
products[].recharge_min_limit | string | null | Minimum single top-up (decimal string); null when the provider imposes none. |
products[].withdraw_min_limit | string | null | Minimum 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 required | string | OUR issue fee for THIS product. |
products[].issue_fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
products[].merchant_issue_fee | string | Your own issue fee (PUT /v1/merchant/card-fees/issue). "0" until you set one. |
products[].issue_total | string | Everything 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_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
products[].charged_to | string | (one of cardholder) |
products[].load_fee_bps required | integer | Our load fee for cards of this product, basis points of the amount loaded. |
products[].load_fee_min required | string | The floor on that load fee, in USD. |
products[].load_fee_flat required | string | A flat platform fee on every load, in USD, on top of the percentage (or its floor). |
products[].close_fee | string | The 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_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
products[].cardholder_fees required | object | null | What 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_transaction | string | USD |
products[].cardholder_fees.three_d_secure | string | USD |
products[].cardholder_fees.foreign_currency_pct | string | Percent |
products[].cardholder_fees.atm_flat | string | USD |
products[].cardholder_fees.atm_pct | string | Percent |
products[].cardholder_fees.cancellation | string | USD |
products[].cardholder_fees.dispute | string | USD |
products[].cardholder_fees.monthly_service | string | null | Recurring holding fee; null when there is none. |
products[].limits required | object | null | Ceilings the cardholder hits, per BIN. null with cardholder_fees. |
products[].limits.per_transaction | string | — |
products[].limits.daily | string | — |
products[].limits.monthly | string | — |
products[].limits.atm_single | string | — |
products[].limits.atm_per_day | integer | — |
products[].limits.atm_monthly | string | — |
products[].limits.cards_per_user | integer | How many of this card one rail account may hold. Not a ceiling on your customer — when it is reached |
products[].limits.validity_years | integer | Years from issue. |
products[].min_opening_balance | string | Smallest 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_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
user_id | query | string | no | — |
core_id | query | string | no | One programme's cards only. |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | The cards. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | No matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
user_id | string | Echoed only on the one-user form. |
cards required | object[] | — |
cards[].id required | integer | — |
cards[].rail required | string | Opaque, 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 required | string | null | The 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 required | string | null | The 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 required | string | null | Provider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged. |
cards[].product_code required | string | null | Provider-scoped product code; null when the card was listed untagged. |
cards[].card_nature required | string | (one of VIRTUAL, PHYSICAL) |
cards[].status required | integer | The provider's RAW status code, verbatim — interpret via status_normalized. |
cards[].status_normalized required | string | Provider-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 required | string | null | — |
cards[].last4 required | string | null | — |
cards[].name_on_card required | string | null | The cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic. |
cards[].issued_at required | string<date-time> | — |
cards[].user_id | string | The cardholder's externaluserid. |
cards[].cardholder_name | string | null | — |
cards[].program | string | null | — |
cards[].balance | string | null | Live available balance (≤60 s cache); null when unreadable. |
cards[].pending | string | null | Holds in flight; null when the rail does not expose them or the read failed. |
{
"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
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
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
core_id | string | Which 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) |
provider | string | Deprecated 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_id | integer | Numeric product id, kept for back-compat. One of productid / productcode is required. (min 1) |
product_code | string | Provider-scoped product code (preferred; wins when both are sent). (minLength 1; maxLength 64) |
delivery_address_id | integer | Required for PHYSICAL products (from POST /v1/cards/delivery/address). (min 1) |
idempotency_key required | string | Required; replay-safe. (minLength 1; maxLength 255) |
name_on_card | string | Embossed/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 required | string | Required. 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
| Status | Body | Meaning |
|---|---|---|
201 | CardIssueResult | Card issued. card.status_normalized is issuing — it becomes active once the opening balance lands (see the description). |
202 | CardIssueResult | Application accepted, still resolving (card is null). The usual answer: order.status is funding while the opening balance travels on-chain to the rail. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | No matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant. |
409 | Error | A 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). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
order required | object | A 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 required | integer | — |
order.kind required | string | close_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 required | string | funding = 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.txid | string | null | The 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 required | string | — |
order.amount required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
order.fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
order.gas | string | The 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_fee | string | Your own fee on this order, credited to your balance. |
order.total | string | Everything the cardholder was debited for this order. |
order.sent_on_chain | string | What 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 required | string | null | WITHDRAW 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 required | string | null | Integer-string base units of amount_credited (null until a withdraw confirms). |
order.card_id required | integer | null | — |
order.request_order_id required | string | null | Server-minted correlation handle (ct-/cw-/ci- prefix). |
order.provider_status required | string | null | Where 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 required | string | null | — |
order.idempotency_key required | string | null | — |
order.created_at required | string<date-time> | — |
card required | object | null | The issued card, or null while the application is still resolving (202). |
card.id required | integer | — |
card.rail required | string | Opaque, 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 required | string | null | The 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 required | string | null | The 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 required | string | null | Provider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged. |
card.product_code required | string | null | Provider-scoped product code; null when the card was listed untagged. |
card.card_nature required | string | (one of VIRTUAL, PHYSICAL) |
card.status required | integer | The provider's RAW status code, verbatim — interpret via status_normalized. |
card.status_normalized required | string | Provider-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 required | string | null | — |
card.last4 required | string | null | — |
card.name_on_card required | string | null | The cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic. |
card.issued_at required | string<date-time> | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
amount | query | string | yes | Decimal amount to top up (≤6 decimal places). |
provider | query | string | no | Rail id (a product's rail); the quote then uses that rail's fee row — the same one the charge uses. |
card_id | query | integer | no | Price for THIS card — its rail, BIN and product. Send with user_id. (min 1) |
user_id | query | string | no | The card's holder (your external user id). Send with card_id. |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | The fee preview — every component the cardholder is debited. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
asset_id required | string | — |
amount required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee required | string | Our fee. |
fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
gas required | string | The network fee for the on-chain send behind this load. |
gas_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
merchant_fee required | string | Your own fee (PUT /v1/merchant/card-fees/load). "0" until you set one. |
merchant_fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
credited required | string | What reaches the card — amount itself, since the fees are charged on top. The figure to show next to "on your card". |
credited_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
total required | string | What the cardholder's balance is debited — amount + fee + gas + merchant_fee. |
total_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
charged_to required | string | (one of cardholder) |
dynamic | boolean | true 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. |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
product_code | query | string | yes | A code from GET /v1/cards/products. |
core_id | query | string | no | The programme to issue from. Optional with one programme; send it when you hold several. |
initial_amount | query | string | no | Opening balance (≤6 decimal places). Omitted = the product's min_opening_balance; below it is a 400 invalid_amount. |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | The preview — every component the cardholder is debited. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Record not found (or belongs to another tenant). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
asset_id required | string | — |
core_id required | string | — |
product_code required | string | — |
initial_amount required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
min_opening_balance required | string | A 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 required | string | Our fee — the issue fee plus our load fee on the opening balance. |
fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
gas required | string | The network fee for the on-chain send behind the opening balance. |
gas_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
merchant_fee required | string | Your own fees on this issue (PUT /v1/merchant/card-fees/issue and /load). "0" until you set one. |
merchant_fee_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
opening_credited required | string | What the opening balance puts on the card — initial_amount itself; its load fee and gas are charged on top. |
opening_credited_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
total required | string | initialamount + fee + gas + merchantfee — what the cardholder's balance is debited. |
total_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
charged_to required | string | (one of cardholder) |
dynamic | boolean | true 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). |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
order_id | path | integer | yes | The order id returned by issue / top-up / withdraw. (min 1) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | CardOrder | The card order in its current (possibly terminal) state. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
id required | integer | — |
kind required | string | close_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 required | string | funding = 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) |
txid | string | null | The 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 required | string | — |
amount required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
gas | string | The 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_fee | string | Your own fee on this order, credited to your balance. |
total | string | Everything the cardholder was debited for this order. |
sent_on_chain | string | What 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 required | string | null | WITHDRAW 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 required | string | null | Integer-string base units of amount_credited (null until a withdraw confirms). |
card_id required | integer | null | — |
request_order_id required | string | null | Server-minted correlation handle (ct-/cw-/ci- prefix). |
provider_status required | string | null | Where 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 required | string | null | — |
idempotency_key required | string | null | — |
created_at required | string<date-time> | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
user_id | query | string | yes | — |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | CardDetails | Card details (possibly degraded — see CardDetails). |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
card required | object | — |
card.id required | integer | — |
card.rail required | string | Opaque, 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 required | string | null | The 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 required | string | null | The 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 required | string | null | Provider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged. |
card.product_code required | string | null | Provider-scoped product code; null when the card was listed untagged. |
card.card_nature required | string | (one of VIRTUAL, PHYSICAL) |
card.status required | integer | The provider's RAW status code, verbatim — interpret via status_normalized. |
card.status_normalized required | string | Provider-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 required | string | null | — |
card.last4 required | string | null | — |
card.name_on_card required | string | null | The cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic. |
card.issued_at required | string<date-time> | — |
balance required | object | null | Live card balance, or null when the provider read failed (never a fake zero). |
balance.available required | string | Spendable balance |
balance.pending required | string | null | Held amount; null (NOT 0) when the provider does not expose holds. |
balance.currency required | string | — |
currency required | string | null | — |
card_pan_url required | string | null | Link 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 required | string | null | YYYY-MM. |
masked_number | string | null | Masked PAN as the issuer renders it. |
name_on_card | string | null | The name given at issue, as we stored it — not a claim about what is printed on the card. null means none was supplied. |
cardholder_name | string | null | — |
bin | string | null | — |
decline_count | integer | null | Declines counted by the issuer; null when it exposes none. |
decline_limit | integer | null | Declines allowed before the issuer acts; null when it exposes none. |
otp_available | boolean | True 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_waiting | object | Present only while otp_available is true: what the waiting code is for. Never the code itself. |
otp_waiting.kind required | string | (one of payment_3ds, wallet_provisioning) |
otp_waiting.merchant_name | string | — |
otp_waiting.amount | string | — |
otp_waiting.currency | string | — |
otp_waiting.expires_in_seconds required | integer | (min 0) |
provider_read_failed | boolean | Present and true only when the live provider read failed (response degraded to stored state). |
note | string | Present alongside providerreadfailed. |
reveal_unavailable | boolean | Present and true when the card read succeeded but no card-number link is available (see reveal_note for which of the two reasons). |
reveal_note | string | Present alongside reveal_unavailable. |
{
"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
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.
- 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_progresswithdetails.card_idanddetails.order_id. The card is not closed yet — a 202 is not a closed card. Until it is,GET /v1/cards/{id}carriesclose_in_progress: true. - 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.statuswebhook confirms it, and the card readsstatus_normalized: closed. The fee shows up as aclose_feeorder. You may also call DELETE again yourself once the order is confirmed (card.withdrawwebhook or GET /v1/cards/orders/{order_id}); that finishes it at once, and a DELETE on a card we already closed answers200with 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
#Request body
application/json · required · schema CardUserRef
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Card closed; the close fee was debited from the cardholder. |
202 | object | card_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. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
409 | Error | A 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). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
card required | object | — |
card.id required | integer | — |
card.rail required | string | Opaque, 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 required | string | null | The 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 required | string | null | The 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 required | string | null | Provider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged. |
card.product_code required | string | null | Provider-scoped product code; null when the card was listed untagged. |
card.card_nature required | string | (one of VIRTUAL, PHYSICAL) |
card.status required | integer | The provider's RAW status code, verbatim — interpret via status_normalized. |
card.status_normalized required | string | Provider-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 required | string | null | — |
card.last4 required | string | null | — |
card.name_on_card required | string | null | The cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic. |
card.issued_at required | string<date-time> | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
user_id | query | string | yes | — |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | CardBalance | The card's balance. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
409 | Error | A 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). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
available required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
pending required | string | null | Held amount (decimal string); null (NOT 0) when the provider does not expose holds. |
pending_base required | string | null | — |
currency required | string | — |
{
"available": "1.5",
"available_base": "1500000000",
"pending": "string",
"pending_base": "string",
"currency": "string"
}#Example request
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,cvvandexpiry(MM/YY) are filled andreveal_urlisnull. - Hosted page — the issuer reveals only through its own page:
panandcvvcome backnullandreveal_urlcarries 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
#Request body
application/json · required · schema CardUserRef
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | CardSecrets | The card details, returned once. Sent with Cache-Control: no-store — do not cache, log, or persist any field. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
409 | Error | A 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). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
pan required | string | null | Full card number; null when the issuer reveals via reveal_url instead. |
cvv required | string | null | Security code; null when the issuer reveals via reveal_url instead. |
expiry required | string | null | MM/YY; null when the issuer does not disclose it here. |
reveal_url required | string | null | Short-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. |
{
"pan": "string",
"cvv": "string",
"expiry": "string",
"reveal_url": "string"
}#Example request
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_availableonGET /v1/cards/{id}(a peek that does not consume) or listen for thecard.wallet_otpwebhook. 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_secondsis 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
user_id | query | string | no | Optional 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
| Status | Body | Meaning |
|---|---|---|
200 | object | The code, returned once. Sent with Cache-Control: no-store — do not cache, log, or persist it. |
401 | Error | Missing, malformed or unknown API key. |
404 | object | No code is available. Uniform by design — no code arrived, it was already read, it expired, or the card is not yours. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
card_id required | integer | — |
code required | string | The one-time code, exactly as the issuer sent it. |
kind required | string | payment_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_name | string | payment_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. |
amount | string | payment_3ds only, when sent: the amount of that payment, as a decimal string. |
currency | string | payment_3ds only, when sent: the currency of amount. |
expires_in_seconds required | integer | (min 0) |
{
"card_id": 1,
"code": "441524",
"kind": "payment_3ds",
"merchant_name": "EXAMPLE STORE",
"amount": "12.50",
"currency": "USD",
"expires_in_seconds": 477
}#Example request
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
#Request body
application/json · required · schema CardMoneyRequest
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
amount required | string | Decimal amount in major units (USD/USDT), at most 6 decimal places. (pattern ^\d+(\.\d+)?$) |
idempotency_key required | string | Required; replay-safe. (minLength 1; maxLength 255) |
#Responses
| Status | Body | Meaning |
|---|---|---|
202 | CardOrder | Top-up accepted; provider outcome pending. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
409 | Error | A 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). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
id required | integer | — |
kind required | string | close_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 required | string | funding = 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) |
txid | string | null | The 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 required | string | — |
amount required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
gas | string | The 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_fee | string | Your own fee on this order, credited to your balance. |
total | string | Everything the cardholder was debited for this order. |
sent_on_chain | string | What 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 required | string | null | WITHDRAW 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 required | string | null | Integer-string base units of amount_credited (null until a withdraw confirms). |
card_id required | integer | null | — |
request_order_id required | string | null | Server-minted correlation handle (ct-/cw-/ci- prefix). |
provider_status required | string | null | Where 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 required | string | null | — |
idempotency_key required | string | null | — |
created_at required | string<date-time> | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
user_id | query | string | yes | — |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | The close preview. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
200 response body
| Field | Type | Description |
|---|---|---|
asset_id required | string | — |
fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
gas | string | The live network figure for returning the balance to the holder's address on-chain. Part of total. |
gas_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
dynamic | boolean | true when gas was priced live now; false on the fallback. |
merchant_fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
total required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
charged_to required | string | Where 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 required | string | Always 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_fee | string | What 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_base | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
minimum_card_balance_base required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
refund_quote | object | What 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 required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
refund_quote.issuer_fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
refund_quote.rail_return_fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
refund_quote.close_fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
refund_quote.gas required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
refund_quote.merchant_fee required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
refund_quote.refund required | string | A 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 required | string | Exact integer amount in the asset's smallest unit. (pattern ^[0-9]+$) |
refund_quote.live required | boolean | — |
refund_eta_typical | string | What a refund usually takes once the card is closed (e.g. "24–48 hours"). |
refund_eta_max_days | integer | The 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 required | integer | The card's balance reaches the holder's own address on-chain within this many working days of the close. |
{
"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
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
| Status | Body | Meaning |
|---|---|---|
200 | object | Our prices and yours, per operation. |
401 | Error | Missing, malformed or unknown API key. |
200 response body
| Field | Type | Description |
|---|---|---|
max_percent_bps | integer | — |
charged_to | string | (one of cardholder) |
ours | object | — |
ours.issue | object | — |
ours.issue.flat | string | A 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_physical | string | A 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 | object | — |
ours.load.percent_bps | integer | — |
ours.load.min | string | A 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.gas | string | The 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_dynamic | boolean | true = priced live now; false = the live price could not be read and a conservative fallback applies. |
ours.load.gas_priced_at | string<date-time> | When this figure was priced — the moment of this response. |
ours.close | object | — |
ours.close.flat | string | A 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 | object[] | — |
yours[].operation | string | (one of issue, load, close) |
yours[].percent_bps | integer | 100 = 1%. |
yours[].flat | string | A 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_at | string<date-time> | null | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
operation | path | string | yes | — (one of issue, load, close) |
#Request body
application/json · required
| Field | Type | Description |
|---|---|---|
percent_bps | integer | (min 0; max 1000) |
flat | string | (pattern ^\d+(\.\d+)?$) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | The fee as saved. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
409 | Error | no_own_account — open an own-balance account first. |
200 response body
| Field | Type | Description |
|---|---|---|
operation | string | (one of issue, load, close) |
percent_bps | integer | — |
flat | string | A 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. |
{
"operation": "issue",
"percent_bps": 1,
"flat": "1.5"
}#Example request
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
operation | path | string | yes | — (one of issue, load, close) |
#Responses
| Status | Body | Meaning |
|---|---|---|
204 | — | Removed; the cardholder pays exactly our rate again. |
401 | Error | Missing, malformed or unknown API key. |
#Example request
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
#Request body
application/json · required · schema CardMoneyRequest
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
amount required | string | Decimal amount in major units (USD/USDT), at most 6 decimal places. (pattern ^\d+(\.\d+)?$) |
idempotency_key required | string | Required; replay-safe. (minLength 1; maxLength 255) |
#Responses
| Status | Body | Meaning |
|---|---|---|
401 | Error | Missing, malformed or unknown API key. |
409 | Error | A 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). |
410 | Error | Retired — card_unload_retired. Close the card instead. |
503 | Error | Temporarily 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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
#Request body
application/json · required · schema CardUserRef
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Card frozen. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
409 | Error | A 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). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
card required | object | — |
card.id required | integer | — |
card.rail required | string | Opaque, 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 required | string | null | The 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 required | string | null | The 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 required | string | null | Provider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged. |
card.product_code required | string | null | Provider-scoped product code; null when the card was listed untagged. |
card.card_nature required | string | (one of VIRTUAL, PHYSICAL) |
card.status required | integer | The provider's RAW status code, verbatim — interpret via status_normalized. |
card.status_normalized required | string | Provider-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 required | string | null | — |
card.last4 required | string | null | — |
card.name_on_card required | string | null | The cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic. |
card.issued_at required | string<date-time> | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
#Request body
application/json · required · schema CardUserRef
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Card unfrozen. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
409 | Error | A 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). |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
card required | object | — |
card.id required | integer | — |
card.rail required | string | Opaque, 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 required | string | null | The 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 required | string | null | The 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 required | string | null | Provider-scoped product CODE (kept as an alias of product_code); null when the card was listed untagged. |
card.product_code required | string | null | Provider-scoped product code; null when the card was listed untagged. |
card.card_nature required | string | (one of VIRTUAL, PHYSICAL) |
card.status required | integer | The provider's RAW status code, verbatim — interpret via status_normalized. |
card.status_normalized required | string | Provider-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 required | string | null | — |
card.last4 required | string | null | — |
card.name_on_card required | string | null | The cardholder name you gave at issue — our record, whether or not the BIN prints it on the plastic. |
card.issued_at required | string<date-time> | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | yes | The card's numeric id (from GET /v1/cards). (min 1) |
user_id | query | string | yes | — |
limit | query | integer | no | Rows per page. (min 1; max 100) |
page | query | integer | no | 1-based page number. (min 1; max 100000) |
page_size | query | integer | no | Deprecated alias of limit; limit wins when both are sent. (min 1; max 100) |
page_num | query | integer | no | Deprecated alias of page; page wins when both are sent. (min 1; max 100000) |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Transactions. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | Unknown card (card_not_found), or an unknown payer user (user_not_found), for this tenant. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
transactions required | object[] | — |
transactions[].id required | string | — |
transactions[].occurred_at | string | null | — |
transactions[].amount | string | null | Decimal string; null when the provider omits it. |
transactions[].amount_base | string | null | — |
transactions[].currency | string | null | — |
transactions[].fee | string | null | — |
transactions[].fee_base | string | null | — |
transactions[].merchant_name | string | null | — |
transactions[].mcc | string | null | — |
transactions[].type | string | null | Transaction code: 170 payment, 171 refund, 172 card fee, 173 ATM withdrawal, 174 pre-authorization, 150 top-up, 140 card issue. |
transactions[].status | string | null | 1 processing, 2 success, 3 failed. |
transactions[].authorization_code | string | null | — |
transactions[].related_authorization_code | string | null | — |
{
"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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
local | query | string | no | Display locale (default enUS). (one of zh_CN, zh_HK, en_US, ko_KR)_ |
#Responses
| Status | Body | Meaning |
|---|---|---|
200 | object | Regions. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
regions required | object[] | — |
regions[].id | string | Region id; pass its numeric value as countryregionid to POST /v1/cards/delivery/address. |
regions[].country_region | string | — |
regions[].country_region_zh | string | — |
regions[].national_code | string | Dialing code. |
regions[].price | string | null | Postage as a decimal string; currency unspecified. null = unknown (NOT free). |
{
"regions": [
{
"id": "string",
"country_region": "string",
"country_region_zh": "string",
"national_code": "string",
"price": "string"
}
]
}#Example request
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
| Field | Type | Description |
|---|---|---|
user_id required | string | (minLength 1; maxLength 255) |
country_region_id required | integer | Region id from GET /v1/cards/delivery/regions. (min 1) |
country required | string | (minLength 1; maxLength 128) |
city required | string | (minLength 1; maxLength 128) |
receiver_name required | string | (minLength 1; maxLength 128) |
receiver_mobile required | string | (minLength 1; maxLength 32) |
receiver_address required | string | (minLength 1; maxLength 512) |
post_code required | string | (minLength 1; maxLength 32) |
#Responses
| Status | Body | Meaning |
|---|---|---|
201 | object | Address created. |
400 | Error | Request 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. |
401 | Error | Missing, malformed or unknown API key. |
404 | Error | No matching card holder (holder_not_found), or an unknown user/payer (user_not_found), for this tenant. |
503 | Error | Temporarily 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
| Field | Type | Description |
|---|---|---|
delivery_address_id required | integer | — |
{
"delivery_address_id": 1
}#Example request
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"
}'