Kryxo Cards API · v1

API Documentation

Installation guide and endpoint reference for partners who build their own screens on top of Kryxo. One API for issuing, balances and secure card display.

Full card number, CVV and PIN never leave the API. They are rendered by the secure display layer directly on your page. Your servers never receive them.
  • REST + JSON, always over HTTPS.
  • Calls are made from the partner's server, never from the customer's browser. The key gives access to the whole partner account.
  • Every response contains only the data of the partner that owns the key.
  • The rules defined in your agreement apply equally to the API and the dashboard: enabled functions, limits, deposit networks and BINs.

Getting access

API access is granted under a partner agreement. Contact sales to request your credentials, base URL and sandbox: contact@kryxo.xyz. Credentials are delivered privately and never published on this page.

Version

All routes are prefixed with /v1. Breaking changes ship under a new prefix; /v1 stays available while you migrate. Every response carries a Kryxo-Version header and a Kryxo-Request-Id you can quote to support.

Limits

ItemRule
Rate120 calls per minute per key. Above that, 429 rate_limited.
Pagination?page=1&limit=50, maximum 200. Responses carry data, total, page and limit.
AmountsUS dollars, numbers with a decimal point (20.5). Card load is an integer.
DatesISO 8601 with offset, Swiss time (Europe/Zurich): 2026-10-22T03:58:41+02:00.
ConcurrencyOne issuance or top-up at a time per account. A second one returns 409 in_progress; wait for the event.

HTTP methods

MethodUse
GETRead. Parameters in the query string. Never changes state.
POSTCreate or execute. Body in JSON. Always send Idempotency-Key.

Common headers

HeaderValueWhen
AuthorizationBearer pk_…Every call
Content-Typeapplication/jsonEvery POST
Idempotency-KeyUUID v4Every POST

There is no request signing. The key is the secret: keep it on the server, restrict it to your IPs, and rotate it on suspicion.

Idempotency

Send Idempotency-Key: <uuid> in every POST. Repeating the same key within 24 hours returns the first response, with "repeated": true, without executing again. After a 5xx, retry with the same key.

API keys

KeyFormatWhat it does
Testpk_test_<id>.<secret>Reads real account data. Writes are validated in full but not executed: "simulated": true, nothing charged.
Productionpk_live_<id>.<secret>Executes for real: deposits, issuance, top-ups and blocks.

Each key has a scope: read (read only) or write (read and execute). The secret is shown once, at creation. If lost, ask for revocation and a new key. A key can be restricted to a list of server IPs.

Keep the key in an environment variable or a secrets vault. Never in source code, never in the browser.

# .env on the server, outside the repository
API_KEY=pk_test_xxxxxxxxxxxx.yyyyyyyyyyyyyyyy
API_SECRET=            # stays empty: the secret is inside the key, after the dot
API_URL=https://api.your-base-url   # base URL delivered with your credentials

First call

curl -s "$API_URL/v1/account" -H "Authorization: Bearer $API_KEY"
{
  "partner": "Green Store",
  "mode": "test",
  "scope": "write",
  "status": "active",
  "permissions": ["manage_customers", "manage_cards", "customer_deposit", "customer_issue"],
  "limits": {"max_customers": null, "cards_per_customer": null, "max_topup": 50, "daily_topup": null, "daily_limit": 5000},
  "networks": ["polygon", "sol", "base", "tron"],
  "customers": 1
}

In limits, null means no limit.

Examples

// Node.js 18+
const api = async (method, route, body) => {
  const r = await fetch(`${process.env.API_URL}/v1${route}`, {
    method,
    headers: {
      'Authorization': `Bearer ${process.env.API_KEY}`,
      'Content-Type': 'application/json',
      ...(method === 'POST' ? {'Idempotency-Key': crypto.randomUUID()} : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const j = await r.json();
  if (!r.ok) throw Object.assign(new Error(j.message), {code: j.code, status: r.status});
  return j;
};
const dep = await api('POST', '/customers/CLI2041/deposits', {network: 'polygon', amount: 20});
$ch = curl_init(getenv('API_URL') . '/v1/customers');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('API_KEY')],
]);
$customers = json_decode(curl_exec($ch), true)['data'];
import json, os, urllib.request, uuid

def api(method, route, body=None):
    h = {'Authorization': 'Bearer ' + os.environ['API_KEY'], 'Content-Type': 'application/json'}
    if method == 'POST':
        h['Idempotency-Key'] = str(uuid.uuid4())
    data = json.dumps(body).encode() if body else None
    r = urllib.request.Request(os.environ['API_URL'] + '/v1' + route, data, h, method=method)
    return json.loads(urllib.request.urlopen(r).read())

Error codes

Every error has the same shape. Show message to the user and always branch on code, which never changes.

{"message": "human-readable text", "code": "stable_code"}
HTTPcodeWhen
400invalid_dataMissing field or wrong format
401invalid_keyKey missing, wrong or revoked
402insufficient_balanceCustomer balance does not cover amount plus fees
403function_disabledFunction not enabled in the agreement, or limit reached
403read_only_scopeWrite with a read-only key
403code_requiredAction without a ticket, or ticket expired, used, or for another email
400wrong_code, code_blockedWrong code; blocked after 5 attempts
403ip_not_allowedIP outside the key's allow-list
403account_suspendedPartner account suspended
400below_costFee below your cost (carries type and your_cost)
400below_minimumDeposit or withdrawal below the minimum (deposit carries minimum)
409open_withdrawalA withdrawal is already awaiting payment
409in_progressAn issuance or top-up is already running for this account; wait for the event
503network_unavailableThe chosen deposit network is unavailable right now; offer another
404not_found, unknown_routeResource of another partner, or nonexistent
409already_exists, card_blockedConflict with current state
429rate_limitedMore than 120 calls in a minute
5xxfailed, internal_errorSystem failure, never a data error: retry with the same Idempotency-Key

Constants

Deposit status · status

ValueMeaning
pendingAddress issued, nothing received yet (valid 30 min)
detectedTransfer seen on the network, confirmations pending
confirmedCredited to the customer balance, net of the deposit fee. Carries credited, fee, net
expiredWindow closed without a transfer

Issuance status · GET /v1/issuances/{ord_id}

ValueMeaning
processingOrder accepted, card being created
completedCard created; response carries card
failedIssuance failed; balance is returned

Card status · status

ValueMeaning
activeCard can transact
blockedBlocked by the customer or by Kryxo; unblock with /block
closedClosed; cannot be reopened

Transaction · type, status, reason

FieldValues
typepurchase purchase · refund refund · decline declined
statusauthorized · settled · declined
reasonOn declines, e.g. insufficient_balance, card_blocked
amount / currencyAmount in the merchant currency (e.g. 42.00 EUR)
amount_usdWhat left the card in US$, converted by the network. Use this one to sum

Withdrawal status · GET /v1/withdrawals

ValueMeaning
requestedRequested, awaiting payment
paidPaid; carries the transfer hash
refusedRefused; carries reason

Functions & limits

GroupKeys
Account permissionsmanage_customers, manage_cards, customer_deposit, customer_issue, plus "Results" for commission withdrawals
Per-customer functionsdeposit, issue, topup, data, block, support
Limitsmax_customers, cards_per_customer, max_topup, daily_topup, daily_limit; null = no limit
Value rangesmin_load, max_load, min_topup, max_topup, daily_topup, cards_per_customer
Fee typesissuance (standard card) · issuance_wallet (card with digital wallet) · topup · deposit

Identifiers

ObjectFormat
CustomerCLI0000
Cardcrt_…
Issuance orderord_…
Depositdep_…
Transactiontrn_…
Code noticentc_…
Withdrawalwdr_…

Secure display · card data on your page

Number, expiry, CVV and PIN are drawn on your own card element by the secure display layer, inside a transparent overlay fitted over it. Your page and your server never receive the values.

Prepare once

  1. Include the script: <script src="https://secure.kryxo.xyz/visor.js"></script>
  2. In your site's CSP, allow frame-src https://secure.kryxo.xyz and add https://secure.kryxo.xyz to script-src.

Layer "on the card"

// When the cards screen opens (or on "Card details", before asking for the code): the layer loads hidden over the card
const layer = SecureCard.mount(document.querySelector('#card-4242'), null, {
  font: 'widescreen', color: '#fff', fields: ['number', 'expiry', 'cvv', 'pin'],
  onOpen: () => {}, onHide: reason => {}, onError: err => {}, onTick: s => {}
});
// After the customer confirms the code, your server calls POST /v1/cards/{crt_id}/details and returns only the url:
layer.show(url);    // values appear over the card instantly
layer.hide();       // hide early (optional)
layer.unmount();    // remove the layer when leaving the screen

Card + number line, one click

const layer = SecureCard.mount([
  {el: document.querySelector('#card-4242')},                                        // digits over the card
  {el: document.querySelector('#title-4242'), format: 'line', fields: ['number']}  // "400000 •••• 4242" next to the card
], null, {font: 'widescreen', color: '#fff', onHide: reason => {}});
layer.show(url);   // one link, one code: both open and close together

Code input & Copy button

// "Copy" click: request the code (your server POSTs to send the email), mount the layer and the code boxes
const layer = SecureCard.mount([{el: card}, {el: title, format: 'line', fields: ['number']}], null, {
  font: 'widescreen', color: '#fff',
  onOpen: () => layer.copyButton(copyBtn, {label: 'Copy', font: 'widescreen-ex', color: '#1a1a1a', background: '#fff', border: '#1a1a1a'}),
  onCopy: (field, automatic) => toast('Card number copied'),
  onCopyFailed: () => toast('Click Copy to copy the card number'),
  onCodeError: err => toast(err.message)
});
layer.codeInput(document.querySelector('#boxes-4242'), {
  copy: 'number',                                   // 'all' = number, expiry, CVV and PIN
  border: '#1a1a1a', background: '#fff', color: '#1a1a1a',
  onCode: async code => (await myServer.cardData(crt_id, code)).url,   // your server verifies the code and calls POST /v1/cards/{crt_id}/details
  onCancel: () => {}                                // Esc inside the boxes
});

Window (alternative): SecureCard.open(url) opens a "Card details" window over the page. The frame only renders on your domain's sites, has no cache or indexing, and your page has no access to its contents.

Endpoints

GET/v1/account

Mode, scope, enabled functions, limits, networks and values: min_load, max_load, min_topup, max_topup, daily_topup, cards_per_customer. Validate on your screen before calling.

GET/v1/networks?customer={id}

Enabled deposit networks: id, name, token (PoS, SPL, TRC20), currency, minimum, and default_network (the suggested network). minimum is the minimum deposit for that customer, deposit fee included; without customer, it applies to a new customer. Always show this value: deposits below it are refused.

GET/v1/bins

BINs you can issue: bin, brand, category, wallets (apple_pay, google_pay; empty list = no digital wallet), fee (issuance or issuance_wallet) and issuance_fee (what your account pays per card on that BIN).

GET/v1/fees

version, types, your_cost, customer_default and per_customer. Keep version: it changes when the fees.changed event arrives. Issuance has two fees: issuance (standard card) and issuance_wallet (card with digital wallet).

POST/v1/fees

{"target": "default", "values": {"issuance": 50}}
{"target": "CLI2041", "values": {"topup": 4}}

Nothing below your cost (below_cost). Changing the default also moves the customers who follow it. Returns version and what changed; every change becomes a fees.changed event.

POST/v1/code/send

Actions that require a code: block/unblock and card data. Sign-up uses the entry code (below). Top-up does not ask for a code. A 6-digit code goes to the customer's email; the customer types it on your page and you exchange it for a ticket (ticket).

{"email": "customer@example.com"}

Valid 5 minutes; resend after 60 s; up to 10 per day per email. For an email without an account yet, sending requires the sign-up function enabled.

POST/v1/code/verify

{"email": "customer@example.com", "code": "123456"}
→ {"ticket": "9c1e7a2b4d6f8e0a3b5c7d9e1f2a4b6c", "valid_s": 60, "signup_s": 1800}

The ticket is valid for 60 seconds and one action, only for the email that received the code. Send it in the action body as "ticket". Up to 5 attempts per code. Never store the ticket or the code.

GET/v1/customers

Paginated list: id, email, name, phone, address, since, balance.

GET/v1/customers/{id}

One customer. balance rises by confirmed deposit or by a credit posted by our team; both show here.

GET/v1/customers/{id}/rules

Functions and limits that apply to the customer (functions, limits), the ones set only for them (own), the account ones (from_partner) and their value ranges (values).

POST/v1/customers/{id}/rules

{"functions": {"issue": false}, "limits": {"max_topup": 500}}

Only restricts what your account has: turns a function off or lowers a limit. null removes a customer-specific limit. Functions: deposit, issue, topup, data, block, support. Limits: cards_per_customer, max_topup, daily_topup.

POST/v1/customers

{
  "email": "customer@example.com",
  "ticket": "9c1e7a2b4d6f8e0a3b5c7d9e1f2a4b6c",
  "name": "Full Name",
  "country_code": "+1",
  "phone": "212 555 0100",
  "postal_code": "10001",
  "street": "Main Street",
  "number": "100",
  "random": false
}

Opens the account; the customer gets the default fees. Only email is required. If the customer confirmed the entry code within the last 30 minutes, sign-up passes without another code. After that, send the ticket of a new code. With "random": true, empty fields are filled with placeholder name, phone and address; sent fields are kept. The response returns the record as stored.

POST/v1/customers/{id}/deposits

{"network": "polygon", "amount": 20}
→ {"id": "dep_…", "address": "0x…", "amount_to_send": 20.00, "valid_until": "2026-10-22 14:32:00"}

Returns address, amount_to_send and valid_until (30 min). Show the address and amount to the customer, with a QR code if you like.

GET/v1/deposits?customer={id}

List with status. A confirmed deposit carries credited (what arrived), fee (deposit fee) and net (what entered the balance). Show all three to the customer.

GET/v1/deposits/{id}

One deposit, to follow until confirmed. After that, GET /v1/customers/{id} shows the new balance.

POST/v1/customers/{id}/cards

{"bin": "400000", "load": 10}
→ {"id": "ord_…", "status": "processing"}

Issues the card. Debits from the customer balance the load, the issuance fee and the percentage. Returns the order (ord_…). A BIN with a digital wallet (wallets not empty) charges issuance_wallet; others charge issuance. Show the price of each BIN before issuing.

GET/v1/issuances/{ord_id}

Follows the issuance: processing, completed or failed. When complete, carries card.

GET/v1/cards?customer={id}

id, last4, brand, expiry, balance, limit, status, billing_address.

GET/v1/cards/{crt_id}

One card.

POST/v1/cards/{crt_id}/topups

{"amount": 25}

Tops up from the customer's account balance. Does not ask for a code.

POST/v1/cards/{crt_id}/block

{"block": true, "ticket": "…"}

true blocks, false unblocks. Requires the code ticket.

POST/v1/cards/{crt_id}/details

{"ticket": "…"}
→ {"url": "https://secure.kryxo.xyz/v/…", "expires_s": 60}

Do not show or open the url: pass it to layer.show(url) (see Secure display). Valid once, for 60 s. Requires the code ticket.

GET/v1/topups?customer={id}

Top-up history.

GET/v1/transactions?customer={id}

Purchases, refunds and declines: merchant, amount and currency, amount_usd, type, status, declined, reason.

GET/v1/notices?customer={id}

Card code notices: last4, card, reason, when, recent.

{"id": "ntc_…", "customer": "CLI2187", "card": "crt_…", "last4": "4242",
 "reason": "Purchase at AMAZON · USD 25.00", "sent_to_email": true,
 "when": "2026-10-19 14:02:11", "recent": true}
In some online purchases (3-D Secure) and when adding the card to Apple Pay or Google Pay, the card's bank sends a confirmation code. That code goes only to the customer's email and never passes through the API. The API tells you it was sent so your page can show a notice right away. recent stays true for 5 minutes. Suggested text: Code for card ending 4242 · Purchase at AMAZON · USD 25.00 · We sent the code to your email. Do not share it. If this was not you, block the card. Never ask the customer for this code, not even through support.

GET/v1/commission

Your commission is computed and recorded by us on every completed operation of your customers: fee charged to the customer − your cost, on issuance, top-up and deposit. Returns available, pending, paid, credited, min_withdrawal (200), terms_h, network_fee per network, open_withdrawal, negative_margin.

GET/v1/commission/statement

One line per operation: when, operation, customer, reference, charged, your_cost, commission, negative_margin.

POST/v1/withdrawals

// to a wallet: paid within 48 h, network fee deducted; response carries net and eta
{"destination": "wallet", "network": "polygon", "wallet": "0x…", "amount": 200, "ticket": "…"}
// to a customer of yours (own use): credited within 24 h, no network fee
{"destination": "card", "customer": "CLI2003", "amount": 200, "ticket": "…"}

GET/v1/withdrawals

Requests with status: requested, paid (with hash) or refused (with reason).

GET/v1/withdrawals/{wdr_id}

One request.

Events

GET/v1/events?after={cursor}

Returns what happened after the cursor, in order, up to 200 events per call. Keep the cursor and poll every 30 to 60 seconds. Webhooks are planned for v2.

{
  "data": [
    {"when": "2026-10-17 13:41:37", "type": "deposit.confirmed", "object": {"id": "dep_…", "credited": 48.50}},
    {"when": "2026-10-17 14:20:05", "type": "card.issued", "object": {"id": "crt_…", "last4": "4242"}}
  ],
  "cursor": "2026-10-17 14:20:05"
}

Event types

TypeWhen
deposit.confirmedDeposit credited to the customer balance
card.issuedCard created
card.code_noticeBank sent a confirmation code to the customer's email
topup.completedTop-up applied to the card
transaction.recordedPurchase, refund or decline recorded
commission.creditedCommission recorded for an operation
commission.negative_marginAn operation was charged below your cost
withdrawal.requested · withdrawal.paid · withdrawal.refusedWithdrawal lifecycle
fees.changedFees changed. Carries version, scope (your_cost, customer_default, customer or types), customer when single, changes ({"issuance": {"from": 50, "to": 55}}) and origin. Re-read GET /v1/fees.
customer.rules_changedPer-customer functions or limits changed

Typical flow

  1. POST /v1/code/send and POST /v1/code/verify: confirm the customer's email and receive the ticket.
  2. POST /v1/customers (up to 30 min after entry, no new code): open the customer account.
  3. POST /v1/customers/{id}/deposits: show the customer address and amount_to_send.
  4. Follow GET /v1/deposits/{id} or the events until deposit.confirmed.
  5. POST /v1/customers/{id}/cards: issue the card.
  6. Follow GET /v1/issuances/{ord_id} until completed.
  7. Use GET /v1/cards day to day. Block, unblock and card data ask for a new code on each action. Top-up does not.

Security checklist

Certification

  1. Integrate everything with the test key: write responses carry "simulated": true.
  2. Validate the typical flow and the error handling.
  3. Request the production key, with scope and IPs defined.
  4. Run a first real operation of low value, followed by our team.

Contact

SubjectChannel
Keys, certification and technical questionscontact@kryxo.xyz
Agreements and partnershipskryxo.xyz
Suspected key leakagecontact@kryxo.xyz · we revoke immediately