Skip to content
Cryptonera

Developers

API overview

One JSON REST API for Crypto Billing and the Crypto Splitter: Bearer API keys, cursor pagination, idempotent money movements and HMAC-signed webhooks. Start with the quick start, then create your first invoice.

Product 1 of 2

Crypto Billing API #

Issue invoices priced in crypto or fiat, let the payer choose one of the allowed assets on the hosted checkout (or in the widget on your site), and get your share settled to your balance or your wallets. Partial, late and over payments wait for your decision; refunds go back to the address that paid; payouts pay your customers from a separate payout wallet.

1. Quick start #

Everything both products share: where to send requests, how to authenticate, the limits, how to retry safely and how lists page.

Base URL #

https://cryptonera.com/api/v1

JSON in and out (Content-Type: application/json). Amounts are decimal strings in the asset's own unit ("59.400000"), never numbers. Ids are ULIDs; an invoice also has a transaction_id (TRX-… live, TST-… sandbox). Timestamps are ISO 8601 in UTC. New keys are added to answers without notice: ignore the ones you do not know.

Sandbox and live #

The API key chooses the environment, never the URL. A ck_sandbox_ key works on test networks with test coins; a ck_live_ key with real money. Nothing is shared between them: keys, ids, transaction ids, balances, webhooks and settings are separate, and a sandbox key never sees a live object. See sandbox (test mode).

Authentication and scopes #

Create API keys in the dashboard (Developers → API keys). A key looks like this:

ck_<env>_<prefix>_<secret>

e.g. ck_sandbox_Ab12Cd34Ef56_<40–64 alphanumeric characters>

Send it as a Bearer token from your server, never from a browser. Lost it? Show key in the dashboard displays it again after you confirm your password; revoke a key at once if it leaks.

Authorization: Bearer ck_sandbox_Ab12Cd34Ef56_…

Each key carries scopes; a request outside them gets 403 forbidden (with required_scope). Give a key only the scopes it needs:

ScopeAllowsEndpoints
assets:read Read the supported assets.
GET /assets
fees:read Read your commission per asset.
GET /fee-policy
wallets:read Read wallets.
GET /wallets
wallets:write Create wallets.
POST /wallets
balances:read Read balances and withdrawals.
GET /withdrawals
GET /withdrawals/{id}
GET /balances
balances:withdraw
moves money
Withdraw your balance at once, without an approval: to your settlement wallets (any address while the settlement lock is off).
POST /withdrawals
POST /withdrawals/{id}/cancel
splitters:read Read splitters.
GET /splitters
GET /splitters/{id}
splitters:write Create, change, pause and resume splitters.
POST /splitters
PATCH /splitters/{id}
POST /splitters/{id}/pause
POST /splitters/{id}/resume
sessions:read Read split sessions.
GET /split-sessions
GET /split-sessions/{id}
sessions:write Create and close split sessions.
POST /split-sessions
POST /split-sessions/{id}/close
invoices:read Read invoices and the checkout designs.
GET /checkout-themes
GET /invoices
GET /invoices/{id}
invoices:write
moves money
Create and cancel invoices; accept partial, late or extra payments.
POST /invoices
POST /invoices/{id}/accept
POST /invoices/{id}/cancel
activity:read Read deposits, and settlements with their transfers.
GET /settlements
GET /settlements/{id}
GET /deposits
refunds:read Read refunds.
GET /refunds
GET /refunds/{id}
refunds:write
moves money
Refund invoices to the paying customer; cancel queued refunds.
POST /refunds
POST /refunds/{id}/cancel
webhooks:read Read webhook endpoints and events.
GET /webhook-endpoints
GET /webhook-endpoints/{id}
GET /webhook-events
webhooks:write Manage webhook endpoints; replay events.
POST /webhook-endpoints
PATCH /webhook-endpoints/{id}
DELETE /webhook-endpoints/{id}
POST /webhook-events/{id}/replay
payouts:read Read payouts and payout wallets.
GET /payout-wallets
GET /payouts
GET /payouts/{id}
payouts:write
moves money
Pay customers from the payout wallet; create payout wallets.
POST /payout-wallets
POST /payouts
POST /payouts/{id}/cancel

Store-restricted keys. A key restricted to one store creates, lists, shows and cancels only that store's invoices, split sessions and refunds (another store: 403 store_not_allowed), and is refused on the account-wide money endpoints (payouts, payout wallets, withdrawals). Balances, settlements, deposits and webhooks stay readable account-wide. IP allowlist: a key may be limited to listed addresses; requests from elsewhere are 401.

Rate limits. Over a limit the answer is 429 with Retry-After:

  • 120 requests per minute per key, and 600 per minute per account and environment over all its keys (rate_limited);
  • 60 requests per minute per IP address with a key that does not work (a valid key is never refused by it);
  • 120 requests per minute per key refused for a missing scope (403 forbidden), counted on their own;
  • new invoices: 60 per minute live, 30 sandbox, API and dashboard together (invoice_rate_limited);
  • open invoices: at most 2000 live, 200 sandbox (open_invoice_limit). The operator can change them per account.

Idempotency #

Idempotency-Key is required on these endpoints, which move money (400 idempotency_key_required without it), and optional on the other endpoints that create or act (the Idempotency-Key column of the endpoint tables; — there means the header is ignored, e.g. on PATCH and DELETE /webhook-endpoints/{id}, so a retry there runs again instead of being replayed). Required on:

  • POST /invoices/{id}/accept
  • POST /refunds
  • POST /payouts
  • POST /withdrawals

Use a new UUID for every request and send the same key only to retry that request after a timeout. The same key with the same body returns the stored answer (Idempotent-Replayed: true) instead of running twice; the same key with another body is 409 idempotency_key_reused; while the first request still runs, 409 request_in_progress. Keys are kept 48 hours per account and environment (and per store for a store-restricted key: another store's key sending the same key and body gets 409 idempotency_key_reused, never that store's answer); a malformed key is 400 idempotency_key_invalid. Some answers are never stored (the key stays free: fix the cause, then send the same request again): the Key column of the errors table says which. Never use your order number as the key: send it as order_id (duplicate protection).

Idempotency-Key: 6f0b5c1e-2d4a-4b8e-9a37-1c5e8f2d7a90

Lists and pagination #

List endpoints return a page and a cursor, newest first. Pass ?limit= (at most 100) and ?cursor= to page; filters are exact.

{
  "object": "list",
  "data": [ … ],
  "next_cursor": "eyJpZCI6…",
  "has_more": true,
  "request_id": "req_…"
}

Every answer carries request_id (and the X-Request-Id header): quote it to support.

Your first payment in four steps #

  1. Switch the dashboard to Sandbox and create a key with invoices:write (Developers → API keys).
  2. Save your settlement wallets and tick the store's payment assets (simple integration).
  3. POST /invoices (create an invoice) and send the payer to checkout_url, or show it in the widget.
  4. Ship the order when your server receives the signed invoice.paid or invoice.overpaid webhook (an overpaid invoice sends only invoice.overpaid; webhooks).

2. Create an invoice #

POST https://cryptonera.com/api/v1/invoices
Scope invoices:write · Idempotency-Key optional · answers 201 (created) or 200 (the open invoice of the same order_id)

Headers #

HeaderRequiredValue
AuthorizationyesBearer ck_… (API keys)
Content-Typeyesapplication/json
Idempotency-KeynoA new UUID per request, the same one only to retry it (idempotency). Your order number goes in order_id.
Acceptnoapplication/json

Parameters #

Only pricing_mode, currency and amount are required; with the dashboard set up (simple integration) that and order_id are all you send. merchant_reference was renamed to order_id (sending it is a 422).

FieldAllowed valuesDefaultNotes
pricing_mode
string · Required
crypto, fiat — crypto: priced in one asset. fiat: priced in a currency, converted per allowed asset at creation.
currency
string · Required
crypto: an asset identifier; fiat: an ISO-4217 code the price source quotes (fiat currencies) — Case-insensitive. Crypto pricing: on an invoice that settles to a wallet, for a store with payment assets one of them that can be paid in this environment (another payment option: asset_not_in_store); a split-settled invoice is paid in the splitter's asset.
amount
string · Required
A positive decimal string, e.g. "49.99" — A JSON string, never a number. Crypto pricing: at most the asset's decimals. At least the asset's minimum deposit and more than twice the fixed fee per invoice.
order_id
string · Optional
Any text up to 120 characters after trimming; control characters are removed null Your order number (duplicate protection). While an invoice of this order is open in the store, the same order with the same terms (pricing_mode, currency, amount, and the allowed assets only when the request chose them) returns that invoice unchanged (200, "created": false; every other field is ignored); other terms: 409 order_id_in_use; an order whose invoice received money: 409 order_already_paid.
customer_id
string · Optional
Any text up to 120 characters null Your customer reference: find the customer's invoices and refunds by it.
customer_email
string · Optional
An email address up to 160 characters null Stored in lower case. Never shown to the payer.
description
string · Optional
Up to 255 characters; HTML tags are removed null Shown to the payer.
lines
array · Optional
Up to 100 objects {description, quantity, amount} null Shown on the payer's receipt; display only, never added up.
allowed_assets
array · Optional
Asset identifiers The store's Payment assets (fiat), [currency] (crypto), the splitter's asset Ignored on an invoice that settles to a wallet when its store has payment assets (still checked to be a list of 1-20 strings): those are enforced. A store without them lets the request decide: crypto pricing exactly one, equal to currency; fiat pricing needs it (allowed_assets_missing). An empty list is an error.
expires_in_seconds
integer · Optional
300–2592000 (clamped, never refused) 3600 The store's Invoice expiry wins when it sets one; expires_at in the response is the expiry that applies.
return_url
string · Optional
An absolute https:// URL null Link on the checkout after payment.
cancel_url
string · Optional
An absolute https:// URL null Link on the checkout to go back.
metadata
object · Optional
A JSON object up to 4096 bytes null Returned as sent; the keys settlement_address, settlement_addresses and settlement_asset_addresses are reserved.
settlement_type
string · Optional
wallet, splitter The store's default: its saved settlement wallets (wallet), or its splitter splitter when settlement_target_id is sent, wallet when an address is sent. Wallet invoices settle to your settlement wallets on the per_invoice and daily schedules, into your balance on on_request (the default).
settlement_target_id
string · Optional
An active splitter id of this account The store's default splitter Refused while the settlement lock is on (422 settlement_address_locked).
settlement_address
string · Optional
An address valid for every allowed asset's network; never an address of this platform The saved settlement wallets Refused while the settlement lock is on (422 settlement_address_locked). Used by the per_invoice and daily schedules; on on_request (the default) the share goes to your balance.
settlement_addresses
object · Optional
Keys: evm, bitcoin, bitcoincash, tron (network families); never an address of this platform The saved settlement wallets Refused while the settlement lock is on (422 settlement_address_locked). Used by the per_invoice and daily schedules; on on_request (the default) the share goes to your balance.
store_id
string · Optional
A store id of this account The API key's store, else your first store A key restricted to a store may only use that store (403 store_not_allowed).
checkout_theme
string · Optional
modern, classic, minimal, midnight, vivid The store's design, else modern Only the look changes (designs).

Asset identifiers (allowed_assets) #

Send these exact identifiers in allowed_assets (and in currency when the invoice is priced in crypto). An identifier names the coin and its network, so USDT alone is refused: say USDT_TRON or USDT_ETHEREUM. Upper or lower case both work, and the same identifier is used in sandbox and live. The list below is read from this platform as you open the page: when the operator switches an asset off it disappears here and POST /invoices answers 422 for it.

A store's payment assets, ticked once in the dashboard (Stores → Edit → Payment assets), are enforced on every invoice of that store that settles to a wallet, from the API and the dashboard: a fiat-priced invoice offers exactly those that can be paid in your key's environment, and any allowed_assets the request sends is ignored. A crypto-priced invoice must name one of them as its currency; another coin gets 422 with "reason": "asset_not_in_store". A splitter invoice is paid in the splitter's asset. For a store without payment assets each request chooses: send allowed_assets (a crypto-priced invoice uses its currency, a splitter invoice the splitter's asset). If no payment asset is left for the invoice, POST /invoices answers 422 with "reason": "allowed_assets_missing" and a link to the store page.

"allowed_assets": ["USDT_TRON", "USDC_BASE", "BTC_BITCOIN"]
IdentifierAssetNetwork (live / sandbox)settlement_addresses keyDecimalsMinimumSandboxLive
BTC_BITCOIN BTC · Bitcoin
Native coin
Bitcoin
Bitcoin Testnet
bitcoin
Bitcoin
8 0.0001 BTC Available Available
BCH_BITCOINCASH BCH · Bitcoin Cash
Native coin
Bitcoin Cash
Bitcoin Cash Testnet
bitcoincash
Bitcoin Cash
8 0.0001 BCH Available Available
ETH_ETHEREUM ETH · Ether
Native coin
Ethereum
Ethereum Sepolia
evm
Ethereum-type (0x…)
18 0.001 ETH Available Available
TRX_TRON TRX · Tron
Native coin
Tron
Tron Nile Testnet
tron
Tron (T…)
6 1 TRX Available Available
USDT_ETHEREUM USDT · Tether USD (ERC-20)
ERC-20 token
Ethereum
Ethereum Sepolia
evm
Ethereum-type (0x…)
6 1 USDT No public test token: test it in live Available
USDT_TRON USDT · Tether USD (TRC-20)
TRC-20 token
Tron
Tron Nile Testnet
tron
Tron (T…)
6 1 USDT No public test token: test it in live Available
USDC_ETHEREUM USDC · USD Coin (Ethereum)
ERC-20 token
Ethereum
Ethereum Sepolia
evm
Ethereum-type (0x…)
6 1 USDC Available Available
USDC_BASE USDC · USD Coin (Base)
ERC-20 token
Base
Base Sepolia
evm
Ethereum-type (0x…)
6 1 USDC Available Available
USDC_ARBITRUM USDC · USD Coin (Arbitrum One)
ERC-20 token
Arbitrum One
Arbitrum Sepolia
evm
Ethereum-type (0x…)
6 1 USDC Available Available
USDC_POLYGON USDC · USD Coin (Polygon PoS)
ERC-20 token
Polygon PoS
Polygon Amoy
evm
Ethereum-type (0x…)
6 1 USDC Available Available
  • Decimals: the most digits after the point an amount may have for a crypto-priced invoice. Minimum: the smallest amount an option may ask for.
  • The settlement_addresses key is the kind of network: one address per key covers every asset with that key (one 0x… for all evm assets), see where settlements go.
  • Live also needs your account's live access. Fiat-priced invoices need the operator's price source. Contracts, confirmations and fees: supported assets. Your key's own view: GET /assets (assets:read), entries with capabilities.invoice_payment_option: true.

Fiat currencies (currency) #

For "pricing_mode": "fiat" send an ISO-4217 code in currency (USD, EUR, JPY …; upper or lower case). The amount is converted at a locked quote from the operator's price source, so the code must also be one that source quotes; any other code is refused with 422 on currency before a quote is taken.

  • Sandbox: Coinbase, 149 of the 155 currencies below.
  • Live: Coinbase, 149 of the 155 currencies below.
All 155 currencies
CodeCurrencyUsed inSandboxLive
AED UAE Dirham United Arab Emirates Priced Priced
AFN Afghani Afghanistan Priced Priced
ALL Lek Albania Priced Priced
AMD Armenian Dram Armenia Priced Priced
AOA Kwanza Angola Priced Priced
ARS Argentine Peso Argentina Priced Priced
AUD Australian Dollar Australia Priced Priced
AWG Aruban Florin Aruba Priced Priced
AZN Azerbaijan Manat Azerbaijan Priced Priced
BAM Convertible Mark Bosnia and Herzegovina Priced Priced
BBD Barbados Dollar Barbados Priced Priced
BDT Taka Bangladesh Priced Priced
BHD Bahraini Dinar Bahrain Priced Priced
BIF Burundi Franc Burundi Priced Priced
BMD Bermudian Dollar Bermuda Priced Priced
BND Brunei Dollar Brunei Priced Priced
BOB Boliviano Bolivia Priced Priced
BRL Brazilian Real Brazil Priced Priced
BSD Bahamian Dollar Bahamas Priced Priced
BTN Ngultrum Bhutan Priced Priced
BWP Pula Botswana Priced Priced
BYN Belarusian Ruble Belarus Priced Priced
BZD Belize Dollar Belize Priced Priced
CAD Canadian Dollar Canada Priced Priced
CDF Congolese Franc DR Congo Priced Priced
CHF Swiss Franc Switzerland Priced Priced
CLP Chilean Peso Chile Priced Priced
CNY Yuan Renminbi China Priced Priced
COP Colombian Peso Colombia Priced Priced
CRC Costa Rican Colon Costa Rica Priced Priced
CUP Cuban Peso Cuba Priced Priced
CVE Cabo Verde Escudo Cabo Verde Priced Priced
CZK Czech Koruna Czechia Priced Priced
DJF Djibouti Franc Djibouti Priced Priced
DKK Danish Krone Denmark Priced Priced
DOP Dominican Peso Dominican Republic Priced Priced
DZD Algerian Dinar Algeria Priced Priced
EGP Egyptian Pound Egypt Priced Priced
ERN Nakfa Eritrea Priced Priced
ETB Ethiopian Birr Ethiopia Priced Priced
EUR Euro Euro area Priced Priced
FJD Fiji Dollar Fiji Priced Priced
FKP Falkland Islands Pound Falkland Islands Priced Priced
GBP Pound Sterling United Kingdom Priced Priced
GEL Lari Georgia Priced Priced
GHS Ghana Cedi Ghana Priced Priced
GIP Gibraltar Pound Gibraltar Priced Priced
GMD Dalasi Gambia Priced Priced
GNF Guinean Franc Guinea Priced Priced
GTQ Quetzal Guatemala Priced Priced
GYD Guyana Dollar Guyana Priced Priced
HKD Hong Kong Dollar Hong Kong Priced Priced
HNL Lempira Honduras Priced Priced
HTG Gourde Haiti Priced Priced
HUF Forint Hungary Priced Priced
IDR Rupiah Indonesia Priced Priced
ILS New Israeli Sheqel Israel Priced Priced
INR Indian Rupee India Priced Priced
IQD Iraqi Dinar Iraq Priced Priced
IRR Iranian Rial Iran Priced Priced
ISK Iceland Krona Iceland Priced Priced
JMD Jamaican Dollar Jamaica Priced Priced
JOD Jordanian Dinar Jordan Priced Priced
JPY Yen Japan Priced Priced
KES Kenyan Shilling Kenya Priced Priced
KGS Som Kyrgyzstan Priced Priced
KHR Riel Cambodia Priced Priced
KMF Comorian Franc Comoros Priced Priced
KPW North Korean Won North Korea No No
KRW Won South Korea Priced Priced
KWD Kuwaiti Dinar Kuwait Priced Priced
KYD Cayman Islands Dollar Cayman Islands Priced Priced
KZT Tenge Kazakhstan Priced Priced
LAK Lao Kip Laos Priced Priced
LBP Lebanese Pound Lebanon Priced Priced
LKR Sri Lanka Rupee Sri Lanka Priced Priced
LRD Liberian Dollar Liberia Priced Priced
LSL Loti Lesotho Priced Priced
LYD Libyan Dinar Libya Priced Priced
MAD Moroccan Dirham Morocco Priced Priced
MDL Moldovan Leu Moldova Priced Priced
MGA Malagasy Ariary Madagascar Priced Priced
MKD Denar North Macedonia Priced Priced
MMK Kyat Myanmar Priced Priced
MNT Tugrik Mongolia Priced Priced
MOP Pataca Macao Priced Priced
MRU Ouguiya Mauritania Priced Priced
MUR Mauritius Rupee Mauritius Priced Priced
MVR Rufiyaa Maldives Priced Priced
MWK Malawi Kwacha Malawi Priced Priced
MXN Mexican Peso Mexico Priced Priced
MYR Malaysian Ringgit Malaysia Priced Priced
MZN Mozambique Metical Mozambique Priced Priced
NAD Namibia Dollar Namibia Priced Priced
NGN Naira Nigeria Priced Priced
NIO Cordoba Oro Nicaragua Priced Priced
NOK Norwegian Krone Norway Priced Priced
NPR Nepalese Rupee Nepal Priced Priced
NZD New Zealand Dollar New Zealand Priced Priced
OMR Rial Omani Oman Priced Priced
PAB Balboa Panama Priced Priced
PEN Sol Peru Priced Priced
PGK Kina Papua New Guinea Priced Priced
PHP Philippine Peso Philippines Priced Priced
PKR Pakistan Rupee Pakistan Priced Priced
PLN Zloty Poland Priced Priced
PYG Guarani Paraguay Priced Priced
QAR Qatari Rial Qatar Priced Priced
RON Romanian Leu Romania Priced Priced
RSD Serbian Dinar Serbia Priced Priced
RUB Russian Ruble Russia Priced Priced
RWF Rwanda Franc Rwanda Priced Priced
SAR Saudi Riyal Saudi Arabia Priced Priced
SBD Solomon Islands Dollar Solomon Islands Priced Priced
SCR Seychelles Rupee Seychelles Priced Priced
SDG Sudanese Pound Sudan Priced Priced
SEK Swedish Krona Sweden Priced Priced
SGD Singapore Dollar Singapore Priced Priced
SHP Saint Helena Pound Saint Helena Priced Priced
SLE Leone Sierra Leone No No
SOS Somali Shilling Somalia Priced Priced
SRD Surinam Dollar Suriname Priced Priced
SSP South Sudanese Pound South Sudan Priced Priced
STN Dobra São Tomé and Príncipe No No
SVC El Salvador Colon El Salvador Priced Priced
SYP Syrian Pound Syria Priced Priced
SZL Lilangeni Eswatini Priced Priced
THB Baht Thailand Priced Priced
TJS Somoni Tajikistan Priced Priced
TMT Turkmenistan New Manat Turkmenistan Priced Priced
TND Tunisian Dinar Tunisia Priced Priced
TOP Pa’anga Tonga Priced Priced
TRY Turkish Lira Türkiye Priced Priced
TTD Trinidad and Tobago Dollar Trinidad and Tobago Priced Priced
TWD New Taiwan Dollar Taiwan Priced Priced
TZS Tanzanian Shilling Tanzania Priced Priced
UAH Hryvnia Ukraine Priced Priced
UGX Uganda Shilling Uganda Priced Priced
USD US Dollar United States Priced Priced
UYU Peso Uruguayo Uruguay Priced Priced
UZS Uzbekistan Sum Uzbekistan Priced Priced
VED Bolívar Soberano Venezuela No No
VES Bolívar Soberano Venezuela Priced Priced
VND Dong Vietnam Priced Priced
VUV Vatu Vanuatu Priced Priced
WST Tala Samoa Priced Priced
XAF CFA Franc BEAC Central Africa Priced Priced
XCD East Caribbean Dollar Eastern Caribbean Priced Priced
XCG Caribbean Guilder Curaçao, Sint Maarten No No
XOF CFA Franc BCEAO West Africa Priced Priced
XPF CFP Franc French Pacific territories Priced Priced
YER Yemeni Rial Yemen Priced Priced
ZAR Rand South Africa Priced Priced
ZMW Zambian Kwacha Zambia Priced Priced
ZWG Zimbabwe Gold Zimbabwe No No

Duplicate protection: order_id #

Send your order number as order_id. Per store and environment, compared exactly (case-sensitive). Only the terms of the order are compared:

  • pricing_mode;
  • currency, in any case (usd = USD);
  • amount, as a number (25 = 25.00);
  • allowed_assets, as a set (order and case do not matter), and only when your request chose them. The store's payment assets and its default splitter are settings: changing them never refuses the same order.
The order already has…Answer
no invoice in this store201, a new invoice, "created": true
an open invoice with the same terms. Open means awaiting payment and not past its expires_at, confirming, or partially paid.200 with that invoice, "created": false: nothing new, no webhook
an open invoice with other terms, partially paid ones included409 order_id_in_use with existing_invoice and differences
an invoice that received money: paid, overpaid, paid late, underpaid, refund pending, partially refunded or refunded, or expired or cancelled with confirmed money409 order_already_paid: do not charge the order twice
only invoices that expired or were cancelled without confirmed money, or one still awaiting payment after its expires_at201, a new invoice

On a repeat every other field is ignored: description, lines, metadata, customer ID and email, return and cancel URLs, expiry, checkout theme and settlement (type, splitter, addresses). You get the original invoice back unchanged, and its expiry is not extended. A splitter you name counts only when it decides the allowed assets (fiat pricing without allowed_assets): then its asset is compared. To change anything else, cancel the open invoice and create a new one.

So a timeout is harmless: send the same request again. Without order_id every request is a new invoice (an Idempotency-Key then protects the retry).

Example #

A 49.99 USD invoice for order 1042, payable in the store's payment assets and settled the way your settlement schedule says (into your balance on on_request, the default; to your settlement wallets on per_invoice and daily):

curl -X POST https://cryptonera.com/api/v1/invoices \
  -H "Authorization: Bearer $CRYPTONERA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "pricing_mode": "fiat",
    "currency": "USD",
    "amount": "49.99",
    "order_id": "order-1042",
    "customer_email": "[email protected]",
    "expires_in_seconds": 3600
  }'
<?php
$body = [
    'pricing_mode' => 'fiat',
    'currency' => 'USD',
    'amount' => '49.99',            // a string, never a float
    'order_id' => 'order-1042',     // your order number
    'customer_email' => '[email protected]',
];
$ch = curl_init('https://cryptonera.com/api/v1/invoices');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 20,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.getenv('CRYPTONERA_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($raw === false) {
    // network error: send the same request again (order_id makes it safe)
}
$invoice = json_decode((string) $raw, true);
if ($status === 201 || $status === 200) {
    // 200: this order already has an open invoice with the same terms ("created": false)
    header('Location: '.$invoice['checkout_url']);
} else {
    error_log($invoice['error']['code'].': '.$invoice['error']['message']);
}
// Node.js 18+ (server side only: never put your API key in a browser)
const res = await fetch('https://cryptonera.com/api/v1/invoices', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CRYPTONERA_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    pricing_mode: 'fiat',
    currency: 'USD',
    amount: '49.99', // a string, never a number
    order_id: 'order-1042',
    customer_email: '[email protected]',
  }),
});
const invoice = await res.json();
if (res.status === 201 || res.status === 200) {
  // send the payer to invoice.checkout_url; keep invoice.id and invoice.transaction_id
} else {
  console.error(invoice.error.code, invoice.error.message);
}

Response 201 Created:

{
  "id": "01J9Z3A1B2C3D4E5F6G7H8J9KM",
  "object": "invoice",
  "transaction_id": "TST-7K3M-9QX2-H4PD",
  "number": "INV-MAIN-000123",
  "order_id": "order-1042",
  "store_id": "01J9Z2K7QH4M8N3P5R6S7T8V9W",
  "environment": "sandbox",
  "status": "awaiting_payment",
  "settlement_status": "not_started",
  "pricing_mode": "fiat",
  "currency": "USD",
  "amount": "49.99",
  "amount_asset": null,
  "amount_paid": "0",
  "amount_pending": "0",
  "amount_due": null,
  "needs_decision": false,
  "decision": null,
  "description": null,
  "lines": null,
  "customer_id": null,
  "customer_email": "[email protected]",
  "allowed_assets": ["USDT_TRON", "USDC_BASE"],
  "settlement_type": "wallet",
  "rate_source": "coingecko",
  "payment_options": [
    {
      "id": "01J9Z3A1B2C3D4E5F6G7H8J9KN",
      "asset": "USDT_TRON",
      "status": "pending_address",
      "required_amount": "49.990000",
      "received_confirmed": "0.000000",
      "received_unconfirmed": "0.000000",
      "address": null,
      "quote_expires_at": "2026-10-12T11:00:00+00:00"
    },
    {
      "id": "01J9Z3A1B2C3D4E5F6G7H8J9KP",
      "asset": "USDC_BASE",
      "status": "pending_address",
      "required_amount": "49.990000",
      "received_confirmed": "0.000000",
      "received_unconfirmed": "0.000000",
      "address": null,
      "quote_expires_at": "2026-10-12T11:00:00+00:00"
    }
  ],
  "payer_addresses": [],
  "refund": null,
  "metadata": {},
  "return_url": null,
  "cancel_url": null,
  "checkout_url": "https://cryptonera.com/pay/i/<public-token>",
  "checkout_theme": "modern",
  "expires_at": "2026-10-12T11:00:00+00:00",
  "paid_at": null,
  "cancelled_at": null,
  "created_at": "2026-10-12T10:00:00+00:00",
  "created": true,
  "request_id": "req_01J9Z3A1B2C3D4E5F6G7H8J9KQ"
}

The same order again while this invoice is open, with the same terms: 200, the same object and "created": false. With other terms:

{
  "error": {
    "code": "order_id_in_use",
    "message": "Order \"order-1042\" already has open invoice INV-MAIN-000123 (TST-7K3M-9QX2-H4PD) in this store with a different amount. Cancel it first, or use another order_id.",
    "retryable": false,
    "request_id": "req_01J9Z3A1B2C3D4E5F6G7H8J9KR",
    "order_id": "order-1042",
    "existing_invoice": {
      "id": "01J9Z3A1B2C3D4E5F6G7H8J9KM",
      "transaction_id": "TST-7K3M-9QX2-H4PD",
      "number": "INV-MAIN-000123",
      "status": "awaiting_payment",
      "amount": "49.99",
      "currency": "USD",
      "allowed_assets": ["USDT_TRON", "USDC_BASE"],
      "checkout_url": "https://cryptonera.com/pay/i/<public-token>",
      "created_at": "2026-10-12T10:00:00+00:00"
    },
    "differences": ["amount"]
  }
}

Send the payer to checkout_url (or show it in the widget); keep id and transaction_id with your order. A payment option's address is allocated when the payer chooses it, so payment_options[].address is null until then. To settle through a splitter instead of your wallets, send "settlement_type": "splitter" and "settlement_target_id".

Response: the invoice object #

Every key is always present (null when empty), in this order. POST /invoices adds created; every answer adds request_id.

FieldTypeMeaning
id string Payment ID (ULID, 26 characters).
object string Always invoice.
transaction_id string or null Platform reference TRX-XXXX-XXXX-XXXX (sandbox TST-…): quote it to support; GET /invoices/{id} takes it too.
number string Invoice number shown to the payer (INV-MAIN-000123), unique per store and environment.
order_id string or null Your Merchant order ID, as sent (cleaned).
store_id string or null The store (also data-store of the website widget).
environment string The environment of the object (the API key's).
status string See invoice statuses.
settlement_status string Where your share is: see settlement status.
pricing_mode string crypto: priced in one asset; fiat: priced in a currency and quoted per option.
currency string The asset identifier (crypto) or ISO-4217 code (fiat) of amount.
amount string The price as sent (decimal string in currency).
amount_asset string or null The asset of the four amounts below: the option paid, else the only option; null while an invoice of several assets is unpaid.
amount_paid string Confirmed money received in amount_asset (late, excess and held payments included; "0" without an asset). Decimal string in the asset's units.
amount_pending string Payments seen, not confirmed yet. Decimal string in the asset's units.
amount_due string or null Still missing while open: 0 once paid or accepted; null without amount_asset. Decimal string in the asset's units.
needs_decision boolean true while held money waits for your accept (POST /invoices/{id}/accept) or refund (POST /refunds).
decision object or null The decision about held money; null when it never needed one.
description string or null Shown to the payer.
lines array of objects or null Receipt lines as sent (display only).
customer_id string or null Your customer reference, as sent.
customer_email string or null Lower-cased; never shown to the payer.
allowed_assets array of strings The assets the payer may choose from.
settlement_type string or null wallet: to your settlement wallets (or balance); splitter: through a splitter.
rate_source string or null Fiat invoices: the price source of the quotes.
payment_options array of PaymentOption One per allowed asset: see payment option.
payer_addresses array of PayerAddress Addresses its confirmed payments came from (largest first): where a refund goes back to.
refund object or null The invoice's refund (one per invoice), or null.
metadata object Returned as sent (reserved settlement keys removed).
return_url string or null Link on the checkout after payment.
cancel_url string or null Link on the checkout to go back.
checkout_url string The payer's page (/pay/i/{token}): keep it out of analytics and shared URLs.
checkout_theme string The design the payer sees: see checkout designs.
expires_at string The expiry that applies (the store's Invoice expiry, else expires_in_seconds, else one hour); later payments are held as late.
paid_at string or null When it became paid.
cancelled_at string or null When it was cancelled.
created_at string When it was created.

The payment option object #

FieldTypeMeaning
id string Payment option id.
asset string The asset the payer may pay with (identifier, e.g. USDT_TRON).
status string The option's state.
required_amount string What the payer must send with this option. Decimal string in the asset's units.
received_confirmed string Confirmed so far. Decimal string in the asset's units.
received_unconfirmed string Seen, not confirmed yet. Decimal string in the asset's units.
address string or null The deposit address; null until the payer chooses this option on the checkout.
quote_expires_at string or null Fiat invoices: when the quoted rate expires.

Which payment is which #

Every invoice is one customer payment. Keep these identifiers next to your order:

IdentifierWhere you get itUse it for
Transaction ID transaction_id (TRX-7K3M-9QX2-H4PD; sandbox TST-…)POST /invoices; every invoice, deposit, settlement and refund object and their webhooks; a payout to a past payer carries the paid invoice's as matched_transaction_id (its payout.* events carry matched_invoice_id); shown to the payer on the checkout and the receiptQuote it to support and to your customer; search it anywhere in the dashboard; GET /invoices/{transaction_id}; ?transaction_id= (any case, dashes optional)
Payment ID: the invoice id (26 characters)The response; resource.id of invoice.*; invoice_id elsewhere; widget eventsAPI look-ups; the widget's data-invoice-id
Merchant order ID order_idYou send itMatching to your orders; duplicate protection; ?order_id=
Invoice number (INV-MAIN-000123)The checkout and the receiptTalking to the payer; unique per store and environment
customer_id, customer_emailYou send themFinding refunds; allowing payouts to known customers
payer_addressDeposits, the invoice's payer_addresses (when the network source names the sender; not on Bitcoin Cash)Where refunds go; payouts to past payers
Payment tx_hash + explorer_urlDeposits, deposit.*Proof the customer paid (several partial payments are possible)
Transfer tx_hash + explorer_urltransfers[] of settlements; tx_hash and explorer_url of refunds and payouts; settlement.transfer_confirmed, refund.completed, payout.completedProof the money left: a daily settlement or a withdrawal pays many invoices in one transfer

In the dashboard, Payments → Invoices and Money → Settlements, Refunds and Payouts find a payment by any of these.

Where settlements go (settlement wallets) #

A wallet-settled invoice ("settlement_type": "wallet") on the per_invoice and daily settlement schedules pays the confirmed amount, minus the platform fee, to your address on the network the customer paid on (daily: with the next daily settlement). On on_request, the default schedule, your share is credited to your balance instead and this address is not used: a withdrawal goes to the settlement wallet saved when you withdraw (or, while the lock is off, to the destination_address you send). Either way an invoice needs one address for every allowed asset. Save them once in the dashboard (Payments → Settlement wallets) and send none: with the settlement lock on (the default) those are the only addresses used. The address is the first of these that has one (1 and 2 only while the lock is off; on on_request an address sent in the request is used only if a payment has to be settled on its own because your collection address is not ready yet):

#SourceExample
1lock off settlement_addresses in the request: one address per network key of the allowed assets"settlement_addresses": {"evm": "0x…", "tron": "T…", "bitcoin": "bc1…"}
2lock off settlement_address in the request: one address for every allowed asset (one kind of network)"settlement_address": "T…"
3Your saved address for that asset: Dashboard → Payments → Settlement wallets → "a different address for one asset"USDC on Base to an exchange account
4Your saved address for that kind of network (same page)one 0x… for every EVM asset

Settlement wallets are kept separately for sandbox and live: switch the dashboard to the environment of your API key (sidebar → Environment) before saving. The address is copied into the invoice when it is created, so on the per_invoice and daily schedules later changes never move that invoice's settlement; on on_request money already in your balance goes wherever your withdrawal goes. An address of this platform (a payment, collection or payout wallet address it holds the keys for) is never accepted as a settlement address, saved or sent: the money would stay in the platform's custody instead of reaching you. A request that sends no settlement_type (and no splitter or address) gets the store's default: its saved settlement wallets, or its splitter (Dashboard → Stores → Edit → Default settlement for invoices: "Settlement wallets" or "Splitter rule"). "settlement_type": "wallet" settles to the saved wallets even when the store's default is a splitter. "settlement_type": "splitter" without an id uses the store's default splitter (a store set to Settlement wallets has none: 422 store_settlement_invalid); naming one with "settlement_target_id" works only while the lock is off. Invoices created in the dashboard always use the saved settlement wallets (they never name an address, lock on or off), or a splitter chosen on the form.

When an allowed asset has no address from any of these, POST /invoices answers 422 with the fields and a hint saying exactly where to add it (the same in sandbox, plus a note about test networks there):

{
  "error": {
    "code": "validation_failed",
    "message": "The request is invalid.",
    "retryable": false,
    "request_id": "req_…",
    "fields": {
      "settlement_addresses.tron": [
        "No settlement wallet is saved for USDT_TRON: save a Tron (T…) address under Settlement wallets."
      ]
    },
    "reason": "settlement_destination_missing",
    "environment": "sandbox",
    "settlement_lock": true,
    "missing": [
      {
        "family": "tron",
        "label": "Tron (T…)",
        "assets": ["USDT_TRON"]
      }
    ],
    "hint": "This invoice has no settlement address for USDT_TRON … open Payments → Settlement wallets and save the address …",
    "dashboard_url": "https://cryptonera.com/dashboard/settlement-wallets",
    "docs_url": "https://cryptonera.com/developers#billing-settlement-lock"
  }
}

This error does not use up the Idempotency-Key: save the address, then retry the same request with the same key. (With the lock off the message also offers sending settlement_addresses, and docs_url ends in #billing-settlement.)

Settlement lock #

Every account starts with the settlement lock on, separately for sandbox and live (Dashboard → Payments → Settlement wallets). While it is on, invoices are settled only to the settlement wallets saved in the dashboard, and a request that names a settlement address or a splitter is refused. So if someone changes the code of your website, or gets hold of an API key, they still cannot send your payments to their own address. Only the account owner can turn the lock off (password confirmed; recorded in the audit log).

While the lock is on, POST /invoices refuses settlement_address, settlement_addresses and settlement_target_id (empty values count as not sent) with 422, before anything is created. missing lists the allowed assets that also have no saved wallet yet (never the saved addresses):

{
  "error": {
    "code": "validation_failed",
    "message": "The request is invalid.",
    "retryable": false,
    "request_id": "req_…",
    "fields": {
      "settlement_address": [
        "Not accepted: this account pays out only to its saved settlement wallets (Dashboard → Payments → Settlement wallets). Remove this field."
      ]
    },
    "reason": "settlement_address_locked",
    "environment": "live",
    "settlement_lock": true,
    "missing": [],
    "hint": "Settlements of this account are locked to the settlement wallets saved in the dashboard, so \"settlement_address\" cannot be sent. Remove it: …",
    "dashboard_url": "https://cryptonera.com/dashboard/settlement-wallets",
    "docs_url": "https://cryptonera.com/developers#billing-settlement-lock"
  }
}

Remove the field and retry; the Idempotency-Key is not used up. The lock covers invoices: PATCH /splitters/{id} is refused the same way (field recipients) for a splitter that settles invoices (a store's default settlement, or the target of an invoice not settled yet), and a store's default splitter is used only for invoices of its own environment. Withdrawals of your balance (POST /withdrawals) go only to the saved settlement wallets while the lock is on. Refunds created through the API go back only to an address that paid the invoice (another address only when no paying address is known and Refunds → Refund settings → "Allow a refund address from the API" is on), and they are sent at once unless "Refunds from the API need approval" is on. Payouts from the API go only to the destinations Payouts → Payout settings allow (by default, addresses that paid you before), and they wait for a finance member's approval only while "API payouts need approval" is on. Split sessions and new splitters created through the API name their own recipients: give an API key only the scopes it needs (an invoice integration needs invoices:write, not refunds:write, payouts:write, splitters:write or sessions:write).

Checkout designs (checkout_theme) #

The hosted checkout (checkout_url) comes in 5 designs. Only the look changes: every design shows the same amounts, address, QR code, warnings and live status. Choose one per invoice with "checkout_theme" when you create it; without it the invoice uses its store's default design (Dashboard → Stores), else modern. The invoice response echoes the design in checkout_theme.

Modern"checkout_theme": "modern"

Clean, centred payment card with a progress bar and steps. The default.

Preview: choose coin · pay · confirming · paid · expired

Classic"checkout_theme": "classic"

Conservative and wide, with a solid header in your brand colour.

Preview: choose coin · pay · confirming · paid · expired

Minimal"checkout_theme": "minimal"

Plain white page, thin lines and plenty of space.

Preview: choose coin · pay · confirming · paid · expired

Midnight"checkout_theme": "midnight"

Dark mode with soft glows, easy on the eyes at night.

Preview: choose coin · pay · confirming · paid · expired

Vivid"checkout_theme": "vivid"

Your brand colours as a bold gradient behind a glass card.

Preview: choose coin · pay · confirming · paid · expired

List the designs with their preview links from the API (any key with invoices:read):

GET https://cryptonera.com/api/v1/checkout-themes
Authorization: Bearer ck_sandbox_Ab12Cd34Ef56_…

{
  "object": "list",
  "default": "modern",
  "data": [
    {
      "object": "checkout_theme",
      "key": "modern",
      "name": "Modern",
      "description": "Clean, centred payment card with a progress bar and steps. The default.",
      "default": true,
      "preview_url": "https://cryptonera.com/pay/preview/modern/pay",
      "preview_urls": {
        "choose": "…",
        "pay": "…",
        "confirming": "…",
        "paid": "…",
        "expired": "…"
      }
    },
    {
      "object": "checkout_theme",
      "key": "classic",
      "name": "Classic",
      "description": "Conservative and wide, with a solid header in your brand colour.",
      "default": false,
      "preview_url": "https://cryptonera.com/pay/preview/classic/pay",
      "preview_urls": {
        "choose": "…",
        "pay": "…",
        "confirming": "…",
        "paid": "…",
        "expired": "…"
      }
    },
    …
  ],
  "request_id": "req_…"
}

To show one invoice in another design without creating it again, add ?theme=<key> to its checkout_url (display only; an unknown key is ignored). The same designs are available in the embedded checkout widget.

3. Simple integration with the widget #

Set once in the dashboard what does not change per order. Your server then sends only the price and your order number (plus, optionally, expires_in_seconds), and your page shows the checkout in the widget. Nothing else is needed, because:

  • Settlements go to your settlement wallets. With the settlement lock on (the default) they are the only addresses used, so your code never names one.
  • The coins offered are the store's Payment assets (identifiers), enforced on every invoice of the store, so the request needs no allowed_assets (one sent is ignored).
  • The design is the store's default checkout design, so the request needs no checkout_theme.
  • How long the invoice stays open is one hour unless you say otherwise: the example sends expires_in_seconds with that default, and a store's Invoice expiry applies instead when one is set.

1. Set up once in the dashboard

  1. Payments → Settlement wallets: an address for each kind of network you accept (one 0x… address covers every Ethereum-type coin, one T… address USDT and TRX on Tron). Sandbox and live are saved separately: switch the environment in the sidebar first.
  2. Stores → Edit → Payment assets: tick the coins the store accepts; every invoice of the store offers exactly these. A coin stays greyed out until it has a settlement wallet in that environment.
  3. Stores → Edit → Website widget: the domain of your site, e.g. example.com (localhost:3000 works for sandbox only).
  4. Stores → Edit → Invoice expiry (optional): one expiry for every invoice of the store, whatever the request sends.
  5. Developers → API keys: a ck_sandbox_ key with invoices:write (and invoices:read to read invoices back). With several stores, restrict the key to the store: a key without a store creates invoices for your first store.
  6. Developers → Webhooks: an endpoint that receives invoice.paid and invoice.overpaid (or invoice.*).

Dashboard → Developers → Checkout widget shows this checklist for each store, in the environment you are in.

2. Create the invoice on your server

Exactly this (your API key stays on the server). Send your order number as order_id: if the request is sent again while that order's invoice is open, you get the same invoice back (200, "created": false), never a second one. No Idempotency-Key is needed.

curl -X POST https://cryptonera.com/api/v1/invoices \
  -H "Authorization: Bearer $CRYPTONERA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "pricing_mode": "fiat",
    "currency": "USD",
    "amount": "49.99",
    "order_id": "order-1042",
    "expires_in_seconds": 3600
  }'

amount is a JSON string ("49.99", never the number 49.99), and so is order_id, also for numeric order numbers. No allowed_assets, settlement address or design is sent: the store's Payment assets apply whatever the request says. For a price in crypto send "pricing_mode": "crypto" with "currency": "USDT_TRON", one of the store's Payment assets: the invoice then offers that one coin (another coin gets asset_not_in_store). expires_in_seconds is optional: 3600 (one hour) is the default. If the store sets an Invoice expiry (Stores → Edit), the store's Invoice expiry applies and the value sent is ignored; the response's expires_at is the expiry that applies. Response (201 Created, shortened):

{
  "id": "01J9Z3A1B2C3D4E5F6G7H8J9KM",
  "object": "invoice",
  "transaction_id": "TST-7K3M-9QX2-H4PD",
  "number": "INV-MAIN-000123",
  "order_id": "order-1042",
  "store_id": "YOUR_STORE_ID",
  "status": "awaiting_payment",
  "environment": "sandbox",
  "pricing_mode": "fiat",
  "currency": "USD",
  "amount": "49.99",
  "allowed_assets": ["USDT_TRON", "USDC_BASE"],
  "settlement_type": "wallet",
  "checkout_url": "https://cryptonera.com/pay/i/<token>",
  "expires_at": "2026-09-22T11:00:00+00:00",
  "created_at": "2026-09-22T10:00:00+00:00",
  "created": true,
  …
}

Hand checkout_url, id and store_id to the page. Keep transaction_id: it is the reference support asks for.

3. Show the widget on your page

Load the script once per page with data-store set to the store_id of the response, and give the button the invoice's checkout_url and id (HTML-escaped, as every value you print into a page):

<script
  src="https://cryptonera.com/widget/v1.0.0/cryptonera.js"
  integrity="sha384-TAdsG7ASjU0jWvpWxxPDKatsUF4TLIjr3axGD3/yeIYi/rzHadjBioiah0olsZyX"
  crossorigin="anonymous"
  data-store="YOUR_STORE_ID"
  async></script>

<button
  data-cryptonera-checkout="CHECKOUT_URL"
  data-invoice-id="INVOICE_ID"
  data-cryptonera-button="dark">Pay with crypto</button>

The button opens the checkout as a pop-up (a bottom sheet on phones). Other layouts, an inline box, the designs and the JavaScript API: Embedded checkout widget.

4. Ship the order from your server

Mark the order paid only from your server. Events from the widget (onPaid) update your page; anyone can fake them in their own browser. Mark an order paid only when your server receives the signed invoice.paid or invoice.overpaid webhook (order_id is your order number; verify the signature) or reads the invoice with GET /api/v1/invoices/{id} (status paid or overpaid, amount, currency and environment). An overpaid invoice is paid in full and sends only invoice.overpaid, never invoice.paid; its excess is held for your decision.

5. When something is not set up

POST /invoices answers 422 validation_failed with error.reason naming the setting, a hint in plain words and (except for price_source_missing) a dashboard_url to fix it, one at a time in the order the checks run (the same rows as in the errors table):

ReasonWhat it meansHow to fix it
allowed_assets_missingThe store has no payment assets that can be paid in the key's environment (none ticked, or none available there).dashboard_url opens the store: tick its Payment assets.
asset_not_in_storeOnly for a price in crypto: currency is not one of the store's Payment assets (store_assets lists them).Send one of store_assets as currency, or open dashboard_url (the store) and tick the coin.
settlement_address_lockedOnly when the request names settlement_address, settlement_addresses or settlement_target_id (a withdrawal: destination_address) while the settlement lock is on (example). It comes before the wallets are checked: its missing already lists the coins without a saved settlement wallet.Remove those fields: the saved wallets are used (save the ones in missing first).
settlement_wallet_invalidA saved settlement wallet is not a valid settlement address for a coin offered any more (or is an address of this platform, saved before those were refused): asset names the coin, environment where.Save a valid address under Payments → Settlement wallets in that environment.
settlement_destination_missingA coin offered has no settlement wallet: missing names the kind of network and the coins, environment where (example).Save an address under Payments → Settlement wallets in that environment, or untick the coin.
store_settlement_invalidOnly for a store that settles through its default splitter, in place of the wallet reasons above: the store has no default splitter, it is not active (e.g. paused), it belongs to the other environment, or it is paid in another coin than the invoice asks for.dashboard_url opens the store: choose an active splitter of the key's environment (or Settlement wallets) as its default settlement, or resume the splitter.
price_source_missingA price in fiat, and the platform has no price source for the key's environment.Price the invoice in crypto, or ask the platform operator.
No API error: an empty frame, or "The checkout cannot be shown on this page"Your website is not listed on the store, or data-store names another store than the invoice's. The browser console says which.List the website under Stores → Edit → Website widget; set data-store to the response's store_id.

For example, before any payment asset is ticked:

{
  "error": {
    "code": "validation_failed",
    "message": "The request is invalid.",
    "retryable": false,
    "request_id": "req_…",
    "fields": {
      "allowed_assets": [
        "Send allowed_assets, or choose payment assets for store \"Main store\" (Stores → Edit → Payment assets)."
      ]
    },
    "reason": "allowed_assets_missing",
    "environment": "sandbox",
    "store_id": "YOUR_STORE_ID",
    "hint": "This invoice names no allowed_assets and store \"Main store\" has no payment assets chosen. Either send \"allowed_assets\" … or ask the account owner to tick the coins this store accepts: Dashboard → Stores → Edit \"Main store\" → Payment assets … retry with the same key.",
    "dashboard_url": "https://cryptonera.com/dashboard/stores/YOUR_STORE_ID/edit",
    "docs_url": "https://cryptonera.com/developers#allowed-assets"
  }
}

Sending it again. After a timeout or a 5xx (for example 503 price_source_unavailable while the price source is down; retryable: true, Retry-After), send the same request again: the open invoice of the order comes back. After a 422 with a reason, fix that setting and send it again. A 422 without reason means the request itself is wrong (for example amount as a JSON number): fix it first. All codes: Errors.

Embedded checkout widget #

Instead of sending the payer to the checkout page, show the same checkout inside your website: as a pop-up, a side panel, a bottom sheet (phones) or an inline box, in any of the 5 designs. It is the hosted checkout in a secure frame: amounts, address, QR code, live confirmations and every warning stay exactly as on the checkout page, and your page never needs your API key.

1. Allow your website

Dashboard → Stores → your store → Website widget: type the domain of your site, e.g. example.com (https://, www. and a path are optional); it covers www.example.com and every subdomain such as shop.example.com. On a shared platform list your own site (yourshop.myshopify.com), never the platform alone. localhost:3000, *.localhost or an IP address work for test-mode invoices only. Any other site that tries gets an empty frame. Your store id for the script tag is YOUR_STORE_ID (shown on that page).

2. Create the invoice on your server

Exactly as for the hosted checkout (your API key stays on the server). Hand checkout_url and id to the page.

curl -X POST https://cryptonera.com/api/v1/invoices \
  -H "Authorization: Bearer $CRYPTONERA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "pricing_mode": "fiat",
    "currency": "USD",
    "amount": "49.99",
    "order_id": "order-1042",
    "expires_in_seconds": 3600,
    "checkout_theme": "midnight"
  }'

allowed_assets is optional: a store's Payment assets are enforced on its invoices, so with them the request needs none (one sent is ignored; simple integration); send it only for a store without Payment assets. checkout_theme is optional too (the store's default design), and so is expires_in_seconds (default 3600; a store's Invoice expiry wins over it). Settlements go to your saved settlement wallets. Response (201 Created, shortened):

{
  "id": "01J9Z3A1B2C3D4E5F6G7H8J9KM",
  "object": "invoice",
  "status": "awaiting_payment",
  "checkout_url": "https://cryptonera.com/pay/i/<token>",
  "checkout_theme": "midnight",
  …
}
3. Add the script and a button

Load the script once per page, from this checkout domain, with your store id. The pinned address never changes, so you can protect it with integrity:

<script
  src="https://cryptonera.com/widget/v1.0.0/cryptonera.js"
  integrity="sha384-TAdsG7ASjU0jWvpWxxPDKatsUF4TLIjr3axGD3/yeIYi/rzHadjBioiah0olsZyX"
  crossorigin="anonymous"
  data-store="YOUR_STORE_ID"
  async></script>

<!-- or always the newest 1.x (no integrity):
     https://cryptonera.com/widget/v1/cryptonera.js -->

Then any element with data-cryptonera-checkout opens the checkout:

<button
  data-cryptonera-checkout="CHECKOUT_URL"
  data-invoice-id="INVOICE_ID"
  data-cryptonera-button="dark"
  data-layout="modal"
  data-theme="midnight">Pay with crypto</button>

<!-- inline, in the page -->
<div
  data-cryptonera-inline="CHECKOUT_URL"
  data-invoice-id="INVOICE_ID"
  data-theme="minimal"></div>

Or from your own script (e.g. after your server created the invoice):

const pay = Cryptonera.open(invoice.checkout_url, {
  // the widget closes if the frame shows another invoice
  invoiceId: invoice.id,
  // auto (pop-up, bottom sheet on phones) · modal · drawer · sheet
  layout: 'auto',
  // modern · classic · minimal · midnight · vivid (omit: the invoice's design)
  theme: 'vivid',
  // close 4 s after the payment is confirmed (omit: stay open)
  closeOnPaid: 4000,
  // e: { id, invoice, status, livemode, preview, theme }
  onReady: (e) => {},
  onStatus: (e) => updateBadge(e.status),
  // a hint for your page only, see below
  onPaid: (e) => showThankYou(),
  onExpired: (e) => offerNewInvoice(),
  onClose: () => {},
});

// inline, grows with its content
Cryptonera.mount('#pay', invoice.checkout_url, {
  invoiceId: invoice.id,
  theme: 'minimal',
});

Cryptonera.close();

The same events reach the page as DOM events (cryptonera:ready, cryptonera:status, cryptonera:paid, cryptonera:expired, cryptonera:close, cryptonera:error; event.detail as above). They bubble from the data-cryptonera-* element that opened the checkout, from the mount() target, or, for Cryptonera.open(), from document unless you pass eventTarget: element. Elements added to the page later are picked up by Cryptonera.init() (safe to call again; add data-autobind="false" to the script tag to bind only when you call it). Button styles: data-cryptonera-button="dark|light|outline|brand" (data-color="#0b7a5c" for brand, data-pill for round ends).

Ship orders from your server, not from onPaid. Events from the widget update your page; anyone can fake them in their own browser. Mark an order paid only when your server receives the signed invoice.paid or invoice.overpaid webhook (an overpaid invoice sends only invoice.overpaid) or reads the invoice with GET /api/v1/invoices/{id} (status, amount, currency and environment). Widget 1.0.0 also reports paid_late (the full amount arrived after the expiry) as paid: that money is held until you accept or refund it, so fulfil from invoice.paid, which follows your accept, or from the API.
4. If your site sends a Content-Security-Policy
script-src … https://cryptonera.com;
frame-src  … https://cryptonera.com;

Nothing else: the widget adds no inline styles or scripts and sets no cookies. If the frame is refused (website not listed, another store's invoice, or this policy without frame-src), the payer sees "The checkout cannot be shown on this page. Please contact the shop." and onError reports not_shown; only a very slow connection gets a link to open the checkout in a new tab.

Try it (sample data)

Every design in every layout, on preview data (not payable). Previews are opened with data-preview / { preview: true }; real invoices never need it.

DesignPop-upSide panelBottom sheet
Modern modern
Classic classic
Minimal minimal
Midnight midnight
Vivid vivid
Button styles (each opens the Vivid design)

Inline, minimal design:

4. Other endpoints #

Every Crypto Billing endpoint and the ones both products share. The scope and the Idempotency-Key rule come from the platform's routes.

EndpointScopeIdempotency-KeyPurpose
Invoices
GET /checkout-themes invoices:read — List the checkout designs
GET /invoices invoices:read — List invoices
POST /invoices invoices:write Optional Create an invoice
GET /invoices/{id} invoices:read — Read an invoice
POST /invoices/{id}/accept invoices:write Required Accept held money
POST /invoices/{id}/cancel invoices:write Optional Cancel an invoice
Refunds
GET /refunds refunds:read — List refunds
POST /refunds refunds:write Required Refund an invoice
GET /refunds/{id} refunds:read — Read a refund
POST /refunds/{id}/cancel refunds:write Optional Cancel a refund
Payouts
GET /payout-wallets payouts:read — List the payout wallets
POST /payout-wallets payouts:write Optional Create a payout wallet
GET /payouts payouts:read — List payouts
POST /payouts payouts:write Required Pay a customer
GET /payouts/{id} payouts:read — Read a payout
POST /payouts/{id}/cancel payouts:write Optional Cancel a payout
Settlements
GET /settlements activity:read — List settlements
GET /settlements/{id} activity:read — Read a settlement
GET /withdrawals balances:read — List withdrawals
POST /withdrawals balances:withdraw Required Withdraw your balance
GET /withdrawals/{id} balances:read — Read a withdrawal
POST /withdrawals/{id}/cancel balances:withdraw Optional Cancel a withdrawal
Balances
GET /balances balances:read — Read your balances
Activity
GET /deposits activity:read — List deposits
Assets
GET /assets assets:read — List the assets
GET /fee-policy fees:read — Read your fee policy
Webhooks
GET /webhook-endpoints webhooks:read — List webhook endpoints
POST /webhook-endpoints webhooks:write Optional Create a webhook endpoint
GET /webhook-endpoints/{id} webhooks:read — Read a webhook endpoint
PATCH /webhook-endpoints/{id} webhooks:write — Change a webhook endpoint
DELETE /webhook-endpoints/{id} webhooks:write — Disable a webhook endpoint
GET /webhook-events webhooks:read — List webhook events
POST /webhook-events/{id}/replay webhooks:write Optional Replay a webhook event
Wallets
GET /wallets wallets:read — List wallets
POST /wallets wallets:write Optional Create a wallet

Find, cancel and accept invoices #

GET /invoices lists invoices newest first; GET /invoices/{id} and the actions take the Payment ID or the transaction_id (any case, dashes optional). Filters of the list:

FilterTypeAllowed valuesNotes
?status=stringawaiting_payment, confirming, partially_paid, paid, overpaid, expired, underpaid, paid_late, cancelled, refund_pending, partially_refunded, refundedOnly this status.
?store_id=stringOnly this store (a key restricted to a store lists only its own; another store: 403 store_not_allowed).
?order_id=stringMerchant order ID (exact).
?transaction_id=stringTransaction ID (any case, dashes optional; not a transaction id: no results).
?number=stringInvoice number.
?customer_id=stringCustomer ID.
?customer_email=stringCustomer email (any case).
?needs_decision=booleantrue: only invoices whose held money waits for your decision; false: the others (1/0 too).

POST /invoices/{id}/cancel cancels an open invoice (a partly paid one becomes underpaid, its confirmed money held for your decision):

FieldAllowed valuesDefaultNotes
reason
string · Optional
up to 255 characters — Why (audit log, invoice.cancelled). Default: api.

Partial, late and extra payments. Only confirmed money counts. An invoice that closes with a partial payment becomes underpaid; money that arrives after it closed is held and the invoice becomes underpaid or paid_late; an excess, or a payment at a second option, is held too. Held money waits for your decision (needs_decision: true, decision.status: needed, webhooks invoice.underpaid, invoice.late_payment, invoice.paid_late, invoice.payment_held): accept it with POST /invoices/{id}/accept, or refund it. Accepting settles it like a payment with the invoice's fee (an underpaid or paid-late invoice becomes paid: invoice.accepted, then invoice.paid); it answers 202 and is planned within about a minute (invoice.accept_failed when it cannot be). An open, partly paid invoice can be accepted early (it closes at once). Body:

FieldAllowed valuesDefaultNotes
asset
string · Optional
up to 40 characters — The asset; required when money is held in several assets.
expected_amount
string · Optional
up to 64 characters — The held amount you saw (decision.held[].amount); 409 held_amount_changed when it differs. Recommended.
reason
string · Optional
up to 500 characters — Your note (audit log).

The decision object #

FieldTypeMeaning
status string Where the decision stands.
reason string or null Why money is held.
needed_since string or null Since when it needs your decision.
held array of HeldAmount What an accept or a refund would move, per asset.
actions array of strings What you can do now (empty unless needed).
acceptance object or null The latest accept, or null.

Refunds #

  • Only to an address that paid the invoice (by default the payer of its largest confirmed payment); another address only when none is known and the owner allows it.
  • Once per invoice, at most what was paid. A fiat-priced invoice is refunded in its currency, converted at the current rate (rounded down; the quote is stored); another token sent to its address (no payment option of the invoice) in that token.
  • Paid from the invoice's held money when it covers the refund and its network fee, else from your balance (or, if you ask, your payout wallet). The customer receives the exact amount; the network fee is charged to you on top. No commission.
  • Not enough in your balance: 202 with status queued (refund.queued and an email); it is sent automatically, oldest first, once the balance suffices. You may cancel it until then. If the invoice's confirmed payment no longer covers it by then (a payment reorganized away), it fails with paid_amount_changed and nothing is taken.
  • With "Refunds from the API need approval" on, an API refund waits as pending_approval for a finance user in the dashboard.
FieldAllowed valuesDefaultNotes
invoice_id
string · Optional
up to 64 characters — The invoice (Payment ID); or transaction_id.
transaction_id
string · Optional
up to 64 characters — The invoice's Transaction ID (any case, dashes optional); or invoice_id.
amount
string · Optional
up to 64 characters — In the invoice currency (fiat: its minor unit; crypto: the asset's decimals); an asset a fiat invoice has no payment option for (another token sent to its address) in that asset, up to what was received. Default: the most refundable.
asset
string · Optional
up to 40 characters — Which payment, when the invoice was paid in several assets.
destination
string · Optional
up to 128 characters — An address that paid the invoice (payer_addresses). Default: the payer of its largest payment.
destination_memo
string · Optional
up to 64 characters — Memo or tag.
funding_source
string · Optional
balance, payout_wallet — Pay it from your balance (default) or your payout wallet; ignored when the invoice's held payment pays it.
reason
string · Optional
up to 500 characters — Your note.
# a new UUID for every refund; the same one only to retry it
curl -X POST https://cryptonera.com/api/v1/refunds \
  -H "Authorization: Bearer $CRYPTONERA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f1c2a9e-8d4b-4c6f-9e21-7a5b0c3d4e6f" \
  -d '{
    "transaction_id": "TST-7K3M-9QX2-H4PD",
    "amount": "20.00",
    "reason": "Order 1042 returned"
  }'

Response 201 Created:

{
  "id": "01J9Z4B2C3D4E5F6G7H8J9KMNP",
  "object": "refund",
  "status": "processing",
  "status_reason": null,
  "environment": "sandbox",
  "invoice_id": "01J9Z3A1B2C3D4E5F6G7H8J9KM",
  "transaction_id": "TST-7K3M-9QX2-H4PD",
  "invoice_number": "INV-MAIN-000123",
  "order_id": "order-1042",
  "customer_id": null,
  "customer_email": "[email protected]",
  "asset": "USDT_TRON",
  "amount": "20.000000",
  "fiat_currency": "USD",
  "fiat_amount": "20.00",
  "quote": {
    "rate": "1.000000",
    "source": "coingecko",
    "fetched_at": "2026-10-14T09:00:00+00:00"
  },
  "network_fee": "1.100000",
  "network_fee_final": true,
  "total_charged": "21.100000",
  "funding_source": "balance",
  "destination": "<payer-tron-address>",
  "destination_memo": null,
  "destination_type": "payer_address",
  "reason": "Order 1042 returned",
  "created_via": "api",
  "settlement_id": "01J9Z4B2C3D4E5F6G7H8J9KMNQ",
  "tx_hash": null,
  "explorer_url": null,
  "funds_returned": false,
  "created_at": "2026-10-14T09:00:00+00:00",
  "approved_at": null,
  "funded_at": "2026-10-14T09:00:00+00:00",
  "completed_at": null,
  "failed_at": null,
  "cancelled_at": null,
  "request_id": "req_01J9Z4B2C3D4E5F6G7H8J9KMNR"
}

The refund object #

FieldTypeMeaning
id string Refund id.
object string Always refund.
status string See refund statuses.
status_reason string or null Why it waits, failed or was cancelled: insufficient_balance, network_fee_unavailable, no_collection_address, payout_wallet_missing, held_funds_on_several_addresses, held_funds_changed, paid_amount_changed (at funding or approval the invoice's confirmed payment no longer covered it, e.g. a payment reorganized away; nothing was taken), transfer_failed, transfer_failed_funds_returned, cancelled_by_merchant, superseded (an older refund of the invoice from before one refund per invoice), or a transfer wait.
environment string The environment of the object (the API key's).
invoice_id string The invoice refunded.
transaction_id string or null Its Transaction ID.
invoice_number string or null Its number.
order_id string or null Its Merchant order ID.
customer_id string or null Its customer ID.
customer_email string or null Its customer email.
asset string The asset sent back.
amount string What the payer receives (exactly). Decimal string in the asset's units.
fiat_currency string or null Fiat invoices: the invoice currency.
fiat_amount string or null Fiat invoices: the amount refunded in that currency (amount = fiat_amount / quote.rate, rounded down).
quote object or null Fiat invoices: the current rate it was converted at when created.
network_fee string or null Charged to you on top (null before funding): a token's fee, or a coin's reserve until the transfer confirmed, then its actual cost. Decimal string in the asset's units.
network_fee_final boolean network_fee is final.
total_charged string or null amount + network_fee: what left the funding account. Decimal string in the asset's units.
funding_source string Where the money comes from.
destination string Where it goes: an address that paid the invoice.
destination_memo string or null Memo or tag of the destination.
destination_type string Why this destination.
reason string or null Your note.
created_via string Where it was created.
settlement_id string or null The settlement that sends it (GET /settlements/{id}, kind refund).
tx_hash string or null The transfer's transaction hash, once sent.
explorer_url string or null Block explorer link of tx_hash.
funds_returned boolean A failed refund whose money went back where it came from: the invoice can be refunded again.
created_at string or null When it was created.
approved_at string or null When it was approved.
funded_at string or null When its money was set aside.
completed_at string or null When its transfer confirmed.
failed_at string or null When it failed.
cancelled_at string or null When it was cancelled.

GET /refunds filters (exact):

FilterTypeAllowed valuesNotes
?status=stringpending_approval, queued, processing, completed, failed, cancelledOnly this status.
?asset=stringup to 40 charactersAsset identifier.
?invoice_id=stringThe invoice (Payment ID).
?transaction_id=stringThe invoice's Transaction ID (any case, dashes optional).
?order_id=stringMerchant order ID.
?customer_id=stringCustomer ID.
?customer_email=stringCustomer email (any case).
?destination=stringup to 128 charactersDestination address.
?tx_hash=stringup to 128 charactersThe refund transfer's hash.

POST /refunds/{id}/cancel cancels a refund that is still queued or pending_approval.

Payouts to your customers #

A payout pays one of your customers (a cashback, winnings, a refund outside an invoice) from your payout wallet: one address per chain family and environment (one for every EVM network), created with POST /payout-wallets and funded by sending money to it or with "Move from balance" in the dashboard. On EVM and Tron it holds tokens only: ETH, POL or TRX sent there fund your gas budget. Your settings (Payouts → Settings) decide where payouts may go: by default only addresses that paid you before; also customers with a paid invoice by customer_id or customer_email, or any address; payouts from the API on or off; approval in the dashboard. The customer receives the exact amount; the network fee is charged on top. Not enough in the payout wallet: 409 insufficient_balance and nothing is created (payouts are never queued).

FieldAllowed valuesDefaultNotes
asset
string · Required
up to 40 characters — Asset identifier (tokens on EVM and Tron; BTC, BCH).
amount
string · Required
up to 64 characters — What the customer receives (at most the asset's decimals).
destination
string · Required
up to 128 characters — The customer's address.
destination_memo
string · Optional
up to 64 characters — Memo or tag.
customer_id
string · Optional
up to 120 characters — Your customer's id (allows any address of a customer with a paid invoice, when that basis is on).
customer_email
string · Optional
up to 160 characters; an email address — Your customer's email (the same rule by email).
order_id
string · Optional
up to 120 characters — Your Merchant order ID for this payout.
note
string · Optional
up to 255 characters — Your note.
# a new UUID for every payout; the same one only to retry it
curl -X POST https://cryptonera.com/api/v1/payouts \
  -H "Authorization: Bearer $CRYPTONERA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9b2e4c71-5a3d-4f08-b6e2-1c7d9a0f3e54" \
  -d '{
    "asset": "USDT_TRON",
    "amount": "15.00",
    "destination": "<customer-tron-address>",
    "customer_id": "cust-77",
    "note": "Cashback October"
  }'

Response 201 Created:

{
  "id": "01J9Z5C3D4E5F6G7H8J9KMNPQR",
  "object": "payout",
  "status": "processing",
  "status_reason": null,
  "environment": "sandbox",
  "asset": "USDT_TRON",
  "amount": "15.000000",
  "network_fee": "1.100000",
  "network_fee_final": true,
  "total_charged": "16.100000",
  "destination": "<customer-tron-address>",
  "destination_memo": null,
  "destination_basis": "past_payer",
  "matched_invoice_id": "01J9Z3A1B2C3D4E5F6G7H8J9KM",
  "matched_transaction_id": "TST-7K3M-9QX2-H4PD",
  "customer_id": "cust-77",
  "customer_email": null,
  "order_id": null,
  "note": "Cashback October",
  "created_via": "api",
  "settlement_id": "01J9Z5C3D4E5F6G7H8J9KMNPQS",
  "tx_hash": null,
  "explorer_url": null,
  "funds_returned": false,
  "created_at": "2026-10-14T10:00:00+00:00",
  "approved_at": null,
  "funded_at": "2026-10-14T10:00:00+00:00",
  "completed_at": null,
  "failed_at": null,
  "cancelled_at": null,
  "request_id": "req_01J9Z5C3D4E5F6G7H8J9KMNPQT"
}

The payout object #

FieldTypeMeaning
id string Payout id.
object string Always payout.
status string See payout statuses.
status_reason string or null Why it failed or waits: transfer_failed, transfer_failed_funds_returned, cancelled_by_merchant, or a transfer wait.
environment string The environment of the object (the API key's).
asset string The asset paid.
amount string What the customer receives (exactly). Decimal string in the asset's units.
network_fee string or null Charged on top: a token's fee (0 when your own gas budget pays), a coin's reserve until completed, then its actual cost. Decimal string in the asset's units.
network_fee_final boolean network_fee is final.
total_charged string or null amount + network_fee. Decimal string in the asset's units.
destination string Where it goes.
destination_memo string or null Memo or tag of the destination.
destination_basis string Why the destination was allowed (Payouts → Settings).
matched_invoice_id string or null The paid invoice that made it eligible.
matched_transaction_id string or null Its Transaction ID.
customer_id string or null Your customer's id, as sent.
customer_email string or null Your customer's email (lower case).
order_id string or null Your Merchant order ID for this payout.
note string or null Your note.
created_via string Where it was created.
settlement_id string or null The settlement that sends it (kind payout); null while it waits for approval.
tx_hash string or null The transfer's transaction hash, once sent.
explorer_url string or null Block explorer link of tx_hash.
funds_returned boolean A failed payout whose money went back to the payout wallet.
created_at string or null When it was created.
approved_at string or null When it was approved.
funded_at string or null When its money was set aside.
completed_at string or null When its transfer confirmed.
failed_at string or null When it failed.
cancelled_at string or null When it was cancelled.

The payout wallet object #

GET /payout-wallets lists one per chain family with an enabled network; POST /payout-wallets with network_family creates one (202, allocating; 200 when it exists).

FieldTypeMeaning
object string Always payout_wallet.
network_family string The chain family (one address for every EVM network).
status string Whether it exists.
address string or null Its address (send money here to top it up); null until created.
networks array of strings The networks it receives on.
environment string The environment of the object (the API key's).
balances array of PayoutWalletBalance Its money per asset.

Settlements and withdrawals #

A settlement sends you your share of a payment. When depends on your settlement schedule (the platform's default, or your choice under Balances when the operator lets merchants choose):

ScheduleWhat happens to your share
on_requestThe default. It goes into your balance (credited, "In balance"); you withdraw when you want, and may keep part for refunds and payouts.
per_invoiceSent to your settlement wallet for each invoice.
dailyCollected and sent once a day, at the hour the operator sets.

The platform commission and the fixed fee per invoice are taken from every invoice whatever the schedule. On EVM and Tron, on request roughly halves the network cost per invoice (one transfer into your balance instead of two; commissions are collected in batches); a withdrawal is one more transfer. On Bitcoin and Bitcoin Cash it costs about the same as per invoice.

GET /settlements lists settlements with their transfers (refunds and payouts are listed under their own endpoints). Filters:

FilterTypeAllowed valuesNotes
?status=stringplanned, funds_reserved, awaiting_resources, submitting, settling, pooled, completed, partially_completed, reconciliation_required, held, credited, requested, cancelledOnly this status.
?kind=stringinvoice, daily, split_session, splitter, balance_release, withdrawalOnly this kind.
?asset=stringup to 40 charactersAsset identifier.
?owner_id=stringup to 26 charactersThe owner's id.
?invoice_id=stringup to 26 charactersOne invoice's settlements and the daily settlement that paid it.
?transaction_id=stringup to 40 charactersThe invoice or split session with this Transaction ID.
?order_id=stringup to 120 charactersInvoices and split sessions with this Merchant order ID.
?tx_hash=stringup to 128 charactersA customer payment's or a transfer's hash.

The settlement object #

FieldTypeMeaning
id string Settlement id.
object string Always settlement.
kind string What is settled: see settlement kinds.
status string See settlement statuses.
hold_reason string or null Why it waits or was cancelled: a code (e.g. below_minimum_payout, fee_estimate_unavailable, insufficient_native_gas_budget, settlement_destination_missing, cancelled_by_merchant, unwound), or replanned: <id of the new settlement> / replanned: nothing left / replanned: settled outside; liability_short: the platform operator reviews money that is no longer in your pending balance. Never the provider's own error text; an unknown code means the platform operator reviews it.
environment string The environment of the object (the API key's).
asset string The asset.
owner object What the settlement belongs to.
invoice_id string or null The invoice (Payment ID) this belongs to; null for splitters, withdrawals and daily settlements.
invoice_number string or null Its invoice number.
transaction_id string or null The Transaction ID of that invoice or split session (TRX-…, sandbox TST-…).
order_id string or null The Merchant order ID of that invoice or split session.
gross string What is settled. Decimal string in the asset's units.
platform_fee string The platform commission, the fixed fee included. Decimal string in the asset's units.
fixed_fee string The fixed fee per invoice in platform_fee. Decimal string in the asset's units.
network_fee string Token gas of every transfer, charged in the token before the split (0 for coins and when your own gas budget pays). Decimal string in the asset's units.
network_fee_detail object or null How network_fee was estimated; null when none was charged.
execution_cost_reserve string Coins: the network cost reserved from the payment. Decimal string in the asset's units.
actual_network_cost string or null Coins: what the network charged. Decimal string in the asset's units.
residual_retained string or null The unused reserve kept (null until known). Decimal string in the asset's units.
distributable string What reaches you or the recipients: gross − platform_fee − network_fee − execution_cost_reserve. Decimal string in the asset's units.
cost_funding string or null separate_gas: tokens, gas from a gas budget; same_asset_reserve: coins, reserved from the payment.
fee_snapshot_id string or null The fee snapshot it was planned with.
gas_paid object or null Tokens: the gas its transfers actually spent (null until known).
settlement_schedule string or null Invoice settlements: the schedule it was planned under; null for the other kinds.
settlement_batch_id string or null Daily schedule: the batch that settles it.
withdrawal object or null Withdrawals only: what was asked for; null for every other kind.
transfers array of SettlementTransfer Its on-chain transfers, in order.
created_at string When it was created.
completed_at string or null When it completed (or was credited).

The transfer object #

FieldTypeMeaning
id string Transfer id.
object string Always settlement_transfer.
settlement_id string Its settlement.
kind string Where it goes: see transfer kinds.
status string See transfer statuses.
hold_reason string or null Why it waits: a code (e.g. deposit_reorged, below_minimum_payout_accumulating).
asset string The asset sent.
amount string What is sent. Decimal string in the asset's units.
destination string or null The receiving address.
memo string or null Memo or tag.
label string or null The destination for people.
network_fee string or null What the network charged, in network_fee_asset (decimal string); null until known, and on the 2nd+ output of a shared Bitcoin transaction.
network_fee_asset string or null The coin that pays the network fee (e.g. TRX_TRON for USDT_TRON).
tx_hash string or null Transaction hash once sent.
explorer_url string or null Block explorer link of tx_hash: proof the money left.
attempts integer Send attempts.
error string or null Why it waits or failed, in one short sentence in the platform's own words (e.g. waiting for the platform's network fee wallet; retried automatically). Never the provider's error text. Null when there is nothing to say.
submitted_at string or null When it was sent.
confirmed_at string or null When it confirmed.

Withdrawals. POST /withdrawals takes an amount of one asset from your balance (default: everything available) for your saved settlement wallet; it answers 202 with a settlement of kind withdrawal, status requested, planned within about a minute (settlement.planned) and sent as one transfer. Its network cost comes out of the amount. Cancel it with POST /withdrawals/{id}/cancel while it is requested.

FieldAllowed valuesDefaultNotes
asset
string · Required
up to 40 characters — Asset identifier.
amount
string · Optional
up to 64 characters — At most the asset's decimals and what is available. Default: everything available.
destination_address
string · Optional
up to 128 characters — Only while the settlement lock is off. Default: your saved settlement wallet. Never an address of this platform (a payment, collection or payout wallet address it holds the keys for: 422 validation_failed).
destination_memo
string · Optional
up to 64 characters — Memo or tag.
note
string · Optional
up to 255 characters — Your note (withdrawal.note).
# a new UUID for every withdrawal; the same one only to retry it
curl -X POST https://cryptonera.com/api/v1/withdrawals \
  -H "Authorization: Bearer $CRYPTONERA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5c8a1f3d-7e2b-4a96-8d10-2f6b9e4c7a31" \
  -d '{
    "asset": "USDT_TRON",
    "amount": "90.00"
  }'

Response 202 Accepted:

{
  "id": "01J9Z6D4E5F6G7H8J9KMNPQRST",
  "object": "settlement",
  "kind": "withdrawal",
  "status": "requested",
  "hold_reason": null,
  "environment": "sandbox",
  "asset": "USDT_TRON",
  "owner": {
    "type": "withdrawal",
    "id": "01J9Z6D4E5F6G7H8J9KMNPQRST"
  },
  "invoice_id": null,
  "invoice_number": null,
  "transaction_id": null,
  "order_id": null,
  "gross": "90.000000",
  "platform_fee": "0.000000",
  "fixed_fee": "0.000000",
  "network_fee": "0.000000",
  "network_fee_detail": null,
  "execution_cost_reserve": "0.000000",
  "actual_network_cost": null,
  "residual_retained": "0.000000",
  "distributable": "0.000000",
  "cost_funding": "separate_gas",
  "fee_snapshot_id": "01J9Z6D4E5F6G7H8J9KMNPQRSV",
  "gas_paid": null,
  "settlement_schedule": null,
  "settlement_batch_id": null,
  "withdrawal": {
    "requested_amount": "90.000000",
    "destination": {
      "address": "<your-tron-settlement-wallet>",
      "memo": null,
      "label": "Tron wallet",
      "source": "settlement_wallet"
    },
    "estimate": {
      "network_fee": "1.100000",
      "execution_cost_reserve": null,
      "net": "88.900000",
      "basis": "kept"
    },
    "channel": "api",
    "note": null,
    "cancellable": true,
    "cancel_reason": null
  },
  "transfers": [],
  "created_at": "2026-10-14T11:00:00+00:00",
  "completed_at": null,
  "request_id": "req_01J9Z6D4E5F6G7H8J9KMNPQRSW"
}

Balances #

GET /balances gives your money per asset, with a USD estimate at the rate the platform keeps (an estimate only: never used for any payment, and assets are never converted into each other). pending_detail says why pending waits, in parts that add up to it exactly (the codes).

{
  "object": "list",
  "environment": "sandbox",
  "data": [
    {
      "object": "balance",
      "asset": "USDT_TRON",
      "symbol": "USDT",
      "decimals": 6,
      "available": "128.400000",
      "pending": "24.250000",
      "pending_detail": [
        {
          "code": "on_its_way",
          "label": "On its way into your balance (network confirmations)",
          "amount": "18.500000",
          "amount_atomic": "18500000",
          "count": 1
        },
        {
          "code": "open_invoice",
          "label": "Waiting for the rest of the payment",
          "amount": "5.750000",
          "amount_atomic": "5750000",
          "count": 1
        }
      ],
      "held": "0.000000",
      "reserved": "0.000000",
      "payout_wallet": null,
      "retained": "0.000000",
      "deficit": "0.000000",
      "total": "152.650000",
      "minimum_withdrawal": "1.000000",
      "usd": {
        "rate": "1.00",
        "rate_source": "coingecko",
        "rate_at": "2026-10-14T11:50:00+00:00",
        "stale": false,
        "available": "128.40",
        "total": "152.65"
      }
    }
  ],
  "usd_totals": {
    "available": "128.40",
    "total": "152.65",
    "oldest_rate_at": "2026-10-14T11:50:00+00:00",
    "complete": true,
    "missing": [],
    "note": "Estimate at the listed rates; never used for any payment. Assets are never converted."
  },
  "request_id": "req_01J9Z7E5F6G7H8J9KMNPQRSTVW"
}

The balance object #

FieldTypeMeaning
object string Always balance.
asset string Asset identifier.
symbol string Its symbol.
decimals integer Its decimals.
available string In your balance and not set aside: what POST /withdrawals can take. Decimal string in the asset's units.
pending string Confirmed payments on their way into the balance (or to your wallet on the other schedules); pending_detail says why each part waits. Decimal string in the asset's units.
pending_detail array of PendingPart What pending is made of and why it waits, one part per reason in a fixed order; the amounts add up to pending exactly. Empty when nothing is pending. From stored records only (no network call).
held string Waiting for your decision (late, underpaid, extra payments). Decimal string in the asset's units.
reserved string Set aside for withdrawals and refunds on their way. Decimal string in the asset's units.
payout_wallet string or null What the payout wallet can pay out now; null without payout-wallet money in this asset. Decimal string in the asset's units.
retained string Unused network-fee reserves. Decimal string in the asset's units.
deficit string What you owe after a payment was reversed; taken from the next payments. Decimal string in the asset's units.
total string available + pending + held + reserved + payout_wallet + retained − deficit (signed decimal string).
minimum_withdrawal string The least a withdrawal must deliver after its network cost. Decimal string in the asset's units.
usd object or null USD estimate; null without a usable rate.

Deposits #

GET /deposits lists the payments seen on the network (both products). Filters:

FilterTypeAllowed valuesNotes
?status=stringOnly this status.
?asset=stringAsset identifier.
?owner_id=stringThe owner's id.
?tx_hash=stringTransaction hash.
?invoice_id=stringOne invoice's payments.
?order_id=stringInvoices with this Merchant order ID.
?transaction_id=stringThe invoice with this Transaction ID (a split session's payments: ?owner_id= the session id).

The deposit object #

FieldTypeMeaning
id string Deposit id.
object string Always deposit.
status string See deposit statuses.
hold_reason string or null Why it is held.
classification string or null What kind of money it is.
environment string The environment of the object (the API key's).
asset string or null The asset.
amount string or null The amount. Decimal string in the asset's units.
tx_hash string The payment's transaction hash.
explorer_url string or null Block explorer link of tx_hash: proof the customer paid.
block_number integer or null Its block (null while unconfirmed).
confirmations integer or null Confirmations so far.
address string or null The receiving address.
payer_address string or null Who sent it (canonical form), where a refund goes back to; null when the source names no sender (Bitcoin Cash, block scans).
owner object What the address belongs to.
settlement_id string or null The settlement that took it.
invoice_id string or null The invoice (Payment ID) this belongs to; null for splitters, withdrawals and daily settlements.
invoice_number string or null Its invoice number.
transaction_id string or null The Transaction ID of that invoice or split session (TRX-…, sandbox TST-…).
order_id string or null The Merchant order ID of that invoice or split session.
observed_at string or null First seen.
confirmed_at string or null Confirmed.
accepted_at string or null Accepted into a settlement.

Assets, the fee policy and wallets: see the endpoint table above. Webhook endpoints are explained under webhooks.

5. Statuses #

Every status the Crypto Billing objects can have. The split session's are under Crypto Splitter.

Invoice status (Invoice.status) #

Ship the order on paid or overpaid only. underpaid and paid_late need your decision: accept or refund.

ValueMeaning
awaiting_paymentOpen: waiting for the payer; payments seen before expires_at count.
confirmingPayments that cover the invoice wait for the network's confirmations; it does not expire meanwhile.
partially_paidOpen: part of the amount is confirmed; the payer may send the rest before expires_at.
paidPaid in full with confirmed money, or a partial or late payment you accepted (decision.status: accepted). Ship the order.
overpaidPaid, with more than asked: ship the order; the excess is held for you to refund or accept.
expiredClosed at expires_at without any confirmed payment.
underpaidClosed (expired or cancelled) with a confirmed partial payment: held until you accept it (the invoice becomes paid) or refund it.
paid_lateIt had expired or been cancelled when confirmed money covering it arrived: held until you accept (paid) or refund it.
cancelledCancelled by you before anything was paid.
refund_pendingIts refund is being sent.
partially_refundedPart of the payment was refunded.
refundedThe payment was refunded in full.

Invoice settlement status (Invoice.settlement_status) #

ValueMeaning
not_startedNo accepted payment yet.
pendingPaid; the settlement is being prepared.
processingIts transfers are being sent.
scheduledDaily schedule: collected, sent with the next daily settlement.
settledYour share reached your settlement wallet or splitter.
heldWaiting: see the settlement's hold_reason.
creditedOn-request schedule: your share is in your balance ("In balance"), to withdraw when you want.

Invoice decision status (InvoiceDecision.status) #

ValueMeaning
neededHeld money waits for you: accept or refund it.
acceptingAn accept waits to be planned.
acceptedYou accepted it (the latest accept planned).
refundedIt was refunded.
resolvedNothing is held any more (for example a held payment was reorganised away).

Payment option status (PaymentOption.status) #

ValueMeaning
pending_addressNo deposit address yet: it is allocated when the payer chooses this option.
availableThe address is ready; the payer may pay with this option.
lockedThe option the payer chose (or paid with).
paidPaid in full with this option.
supersededAnother option was paid.

Refund status (Refund.status) #

ValueMeaning
pending_approvalCreated from the API while "Refunds from the API need approval" is on: approve or cancel it in the dashboard.
queuedWaits for money (status_reason); sent automatically, oldest first, when the money suffices. You may cancel it.
processingFunded; its transfer is on its way.
completedSent: tx_hash and explorer_url prove it.
failedThe transfer failed (status_reason); the operator retries it or returns the money (funds_returned).
cancelledCancelled before anything was sent.

Payout status (Payout.status) #

ValueMeaning
pending_approvalCreated from the API while "API payouts need approval" is on; its money is set aside in the payout wallet.
processingFunded; its transfer is on its way.
completedSent: tx_hash and explorer_url prove it.
failedThe transfer failed (status_reason); the operator retries it or returns the money to the payout wallet (funds_returned).
cancelledCancelled before anything was sent.

Settlement status (Settlement.status) #

ValueMeaning
plannedRecorded with its fees; not funded yet.
funds_reservedFunded; its transfers will be sent.
awaiting_resourcesWaits for gas or another resource (hold_reason); continues by itself.
submittingIts transfers are being sent.
settlingTransfers broadcast, waiting for confirmations.
pooledDaily schedule: on your collection address, sent with the next daily settlement.
completedEvery transfer confirmed.
partially_completedSome transfers confirmed, others failed: the operator reviews it.
reconciliation_requiredNeeds operator review (a failed transfer or a reversed payment).
heldWaits (hold_reason), for example below the minimum payout.
creditedOn-request schedule: your share is in your balance.
requestedA withdrawal you asked for: the amount is set aside and planned within about a minute.
cancelledA withdrawal (or another transfer of yours) cancelled or refused before anything was sent (hold_reason), or a held invoice settlement replaced by a new plan of the same payment (hold_reason replanned: <id of the new settlement>, or replanned: nothing left), or recorded by the platform operator as paid to you outside the platform (replanned: settled outside).

Settlement kind (Settlement.kind) #

ValueMeaning
invoiceAn invoice payment.
dailyA daily settlement: one transfer for the invoices collected that day.
split_sessionA split session payment.
splitterA deposit to a splitter.
balance_releaseA balance release (before 2026-10-12; no longer created).
refundA refund's transfer (listed under refunds, not among settlements).
withdrawalA withdrawal of your balance.
payoutA payout's transfer (listed under payouts, not among settlements).
payout_wallet_fundingA move from your balance into the payout wallet.

Transfer status (SettlementTransfer.status) #

ValueMeaning
plannedRecorded, not sent yet.
reservedFunds set aside for it.
submittedHanded to the wallet provider.
broadcastOn the network, not in a block yet.
confirmingIn a block, waiting for confirmations.
confirmedConfirmed: final.
rejectedRefused by the provider before it was sent.
failedFailed on the chain.
replacedReplaced by a new transfer (fee bump).
unknown_outcomeSent; its outcome is being verified with the provider. Never sent twice.
heldWaiting: see hold_reason.

Transfer kind (SettlementTransfer.kind) #

ValueMeaning
recipientTo you (your settlement wallet) or a splitter recipient.
platform_feeThe platform commission (shown for transparency).
consolidationTo your collection address (daily schedule, or on request: into your balance).
refundA refund to the paying customer.
balance_releaseA balance release (before 2026-10-12).
withdrawalA withdrawal of your balance to your settlement wallet.
customer_payoutA payout to your customer.
payout_fundingFrom your collection address into your payout wallet.

Deposit status (Deposit.status) #

ValueMeaning
observedSeen on the network, not confirmed yet.
confirmingIn a block, waiting for confirmations.
confirmedConfirmed: counted.
droppedLeft the chain before it confirmed.
conflictedTwo sources disagree about it; checked again.
reorgedUndone by a chain reorganisation after it confirmed.
unsupportedAn asset or contract the platform does not accept.
heldKept aside (hold_reason): late, extra, below the minimum, or waiting for your decision.

Why a balance is pending (PendingPart.code) #

The pending_detail of GET /balances: why each part of pending waits. The parts add up to pending exactly.

ValueMeaning
waiting_for_gasPlanned; its token transfer waits for network fee (gas) funds: the platform's gas wallet (the operator is notified) or, when your own gas budget pays your transfers, that budget. It continues by itself once they are topped up.
pausedPlanned; transfers are paused by the platform (all of them, a network, a provider or your account; live transfers not activated yet). It continues by itself when the pause is lifted.
splitter_pausedPlanned; your own splitter is paused. Resume it (Splitters) and its transfers continue.
operator_reviewStopped: a transfer failed or needs a check, the money is held for a reason only the platform operator can clear (an archived splitter included), or the operator decides about money from before 2026-10-12. The operator reviews it.
on_its_wayPlanned and sent, or about to be sent: waiting for network confirmations. Into your balance on the on-request schedule, to your wallet on the others.
replanningNot planned yet (no network fee estimate at the time, unwound by the operator, below the minimum payout, or a settlement wallet saved since): the platform plans it again by itself.
too_smallToo small to move on its own (the network cost would take most or all of it, or, for an invoice paid out through a splitter, a recipient's share is below the minimum payout): it waits until the platform operator plans it again.
no_destinationA paid invoice with nowhere to settle to: save a settlement wallet for its network (an older invoice without an address of its own), or its splitter has no current version. It moves on by itself once there is one.
open_invoiceConfirmed part-payments of invoices that are still open: they wait for the rest of the payment.
split_waitingPayments to a splitter or split session that wait for more: its threshold, the minimum payout or the next payment.
scheduled_dailyDaily schedule: collected, and paid to your wallet by the next daily payout.
otherAnything else not settled yet (for example money from an older release, or a settlement in a state not listed here). Ask support if it stays.

6. Errors #

Every error has the same envelope:

{
  "error": {
    "code": "validation_failed",
    "message": "The request is invalid.",
    "retryable": false,
    "request_id": "req_01J9Z3A1B2C3D4E5F6G7H8J9KS",
    "fields": {
      "amount": ["The amount field must be a string."]
    }
  }
}
  • retryable: true: retry later (honour Retry-After) with the same request.
  • reason: a setting or a detail. hint explains it; dashboard_url and docs_url point to the fix.
  • The Key column says what happens to your Idempotency-Key: stored (the same key replays this answer: fix the request and use a new key), free (nothing stored: send the same request with the same key once fixed), or — (answered before the key is read).
HTTPCodeMeaningWhat to doKey
400 idempotency_key_required This endpoint moves money and the request has no Idempotency-Key. Send a new UUID as Idempotency-Key; send the same one only to retry that same request. —
400 idempotency_key_invalid Idempotency-Key is longer than 255 characters or has other characters than letters, digits and - _ : .. Use a UUID. —
401 unauthenticated No valid API key: missing, mistyped, revoked, expired, from an address outside its IP allowlist, or the account is not active (header WWW-Authenticate: Bearer). Check the key and its environment (ck_sandbox_ or ck_live_) under Developers → API keys. —
403 forbidden The key lacks the scope this endpoint needs (required_scope).
Also: required_scope
Create a key with that scope. —
403 store_not_allowed The key is restricted to one store and the request names another store, a record of another store, or an account-wide money endpoint (payouts, payout wallets, withdrawals).
Also: store_id, hint
Leave store_id out, or use a key without a store. stored
403 payouts_api_disabled "Payouts allowed from the API" is off for this account. The owner turns it on (Payouts → Settings), or create the payout in the dashboard. free
404 not_found No such object in this account and environment (a sandbox key never sees live objects). Check the id (an invoice also takes its transaction_id). stored
405 http_405 The method is not allowed on this path (other transport errors answer http_<status> too). Check the method in the endpoint table. —
409 idempotency_key_reused The Idempotency-Key was used before with a different request (or by a key restricted to another store). Send the new request with a new key. —
409 request_in_progress
retryable
A request with this Idempotency-Key is still being processed (Retry-After: 2). Retry after the delay with the same key. —
409 order_id_in_use The order_id already has an open invoice in this store with other terms: another pricing mode, currency, amount or allowed assets (differences).
Also: order_id, existing_invoice, differences
Use that invoice (existing_invoice.checkout_url), cancel it first, or send another order_id (duplicate protection). free
409 order_already_paid The order_id already has an invoice with money in this store.
Also: order_id, existing_invoice
Do not charge the order twice; for a new payment use another order_id. free
409 insufficient_balance Not enough available money. A withdrawal: your balance must cover the amount itself (its network cost comes out of the amount); details asset and available. A payout: the payout wallet must cover the amount plus its network fee; details asset, available, amount, network_fee and needed. Refunds are queued instead (202).
Also: asset, available, amount, network_fee, needed
Lower the amount (a withdrawal can take at most available), wait for more payments to reach your balance, or top up the payout wallet; then send the same request again. free
409 refund_exists The invoice already has a refund (one per invoice).
Also: refund
Read it (refund.id); cancel it while it is queued or waits for approval. free
409 refund_not_cancellable Only a refund that is queued or pending_approval can be cancelled.
Also: status
Read the refund: it is being sent or is finished. free
409 payout_not_cancellable Only a payout that is pending_approval can be cancelled.
Also: status
Read the payout: it is being sent or is finished. free
409 withdrawal_not_cancellable Only a withdrawal that is requested can be cancelled.
Also: status
Read the withdrawal: it is planned, sent or finished. free
409 nothing_to_accept Nothing is held for this invoice, or it is still open and not partly paid: awaiting_payment, or confirming (a payment that covers it is still confirming; it becomes paid once that confirms).
Also: status, needs_decision
Read the invoice (status, decision). free
409 payment_confirming The invoice is partly paid (partially_paid: an early accept) and another payment to it is still confirming.
Also: status
Accept once the payment confirmed or the invoice closed. free
409 accept_in_progress An accept of this invoice waits to be planned (within about a minute).
Also: acceptance
Wait for invoice.accepted; to refund instead, cancel the accept in the dashboard first. free
409 refund_in_progress A refund of this invoice is waiting for approval, queued or being sent.
Also: refund
Cancel the refund first, or let it finish. free
409 held_amount_changed expected_amount is not what is held now.
Also: asset, amount, amount_atomic
Read the invoice again (decision.held) and accept what is held now. free
409 operator_review_required Part of the money held in that asset cannot be accepted (another token sent to the address, a transfer mined again after a reorganization, money at another payment option); a transfer mined again after a reorganization cannot be refunded either.
Also: asset, reason
Another token: refund it. A transfer mined again: ask the platform operator. free
422 validation_failed The request is invalid: fields names each problem. With a reason (rows below) a setting is missing instead, and the key stays free.
Also: fields, reason, hint, dashboard_url, docs_url, environment
Without reason: fix the request and send it with a new key. With reason: fix the setting (hint, dashboard_url), then send the same request again. stored
422 validation_failed
reason: allowed_assets_missing
The store has no payment assets that can be paid in the key's environment (none ticked, or none available there). dashboard_url opens the store: tick its Payment assets. free
422 validation_failed
reason: asset_not_in_store
Only for a price in crypto: currency is not one of the store's Payment assets (store_assets lists them). Send one of store_assets as currency, or open dashboard_url (the store) and tick the coin. free
422 validation_failed
reason: settlement_address_locked
Only when the request names settlement_address, settlement_addresses or settlement_target_id (a withdrawal: destination_address) while the settlement lock is on (example). It comes before the wallets are checked: its missing already lists the coins without a saved settlement wallet. Remove those fields: the saved wallets are used (save the ones in missing first). free
422 validation_failed
reason: settlement_wallet_invalid
A saved settlement wallet is not a valid settlement address for a coin offered any more (or is an address of this platform, saved before those were refused): asset names the coin, environment where. Save a valid address under Payments → Settlement wallets in that environment. free
422 validation_failed
reason: settlement_destination_missing
A coin offered has no settlement wallet: missing names the kind of network and the coins, environment where (example). Save an address under Payments → Settlement wallets in that environment, or untick the coin. free
422 validation_failed
reason: store_settlement_invalid
Only for a store that settles through its default splitter, in place of the wallet reasons above: the store has no default splitter, it is not active (e.g. paused), it belongs to the other environment, or it is paid in another coin than the invoice asks for. dashboard_url opens the store: choose an active splitter of the key's environment (or Settlement wallets) as its default settlement, or resume the splitter. free
422 validation_failed
reason: price_source_missing
A price in fiat, and the platform has no price source for the key's environment. Price the invoice in crypto, or ask the platform operator. free
422 invalid_argument A value the platform cannot use, for example an unknown asset identifier or no platform commission configured. Fix the value; the message says which. stored
422 asset_not_supported The asset cannot be used here (reason).
  • not_available: The asset is not available in the key's environment.
  • gas_coin_on_account_chain: ETH, POL or TRX: on EVM and Tron a payout wallet holds tokens only (a coin sent there funds your gas budget).
Also: asset, reason, hint
Use an asset of GET /assets; pay customers in tokens on EVM and Tron. free
422 amount_below_minimum What would arrive after the network fee is below the asset's minimum transfer (on Bitcoin and Bitcoin Cash also the destination's dust limit).
Also: asset, minimum, estimated_network_cost, available, amount
Send more (see minimum and estimated_network_cost). free
422 refund_not_allowed This invoice cannot be refunded now (reason).
  • invoice_status: Only paid, overpaid, paid_late and underpaid invoices are refunded.
  • payment_confirming: A payment to the invoice is still confirming.
  • nothing_paid: Nothing confirmed was paid in that asset.
  • asset_required: The invoice was paid in several assets: name one (asset).
  • no_payment_rate: The rate the fiat invoice was paid at is unknown.
  • held_funds_on_several_addresses: The held money sits on several payment addresses.
  • unheld_partial_payment: A payment that is neither held nor in your balance yet.
Also: reason, invoice_status, asset, assets
See reason; a confirming payment can be refunded once it is confirmed. free
422 refund_amount_too_large More than may be refunded: more than the customer paid (a fiat invoice: the fiat value paid), or more than the invoice's held payment covers with its network fee (reason: held_funds_changed).
  • held_funds_changed: The invoice's held payment no longer covers the refund and its network fee.
Also: max_amount, max_crypto, asset, funding_source, reason
Refund at most max_amount (invoice currency) or max_crypto. free
422 refund_destination_not_payer Refunds go to an address that paid this invoice (reason).
  • not_a_payer: The address did not pay this invoice.
  • no_payer_address_known: No payer address is known and the API may not name one (Refunds → Settings).
Also: reason, payer_addresses
Leave destination out (the payer is used), or enter the address in the dashboard; see payer_addresses. free
422 payout_destination_not_allowed Your payout settings do not allow this destination (reason; allowed lists what is on).
  • not_a_past_payer: The address never paid you.
  • customer_unknown: No paid invoice names this customer_id or customer_email.
  • platform_address: An address this platform holds the keys for (or its fee wallet), not a customer's; addresses a tenant merely watches are not refused.
  • no_destination_allowed: Every destination basis is switched off.
Also: reason, allowed
Change the destination, or the policy under Payouts → Settings. free
422 payout_wallet_missing No payout wallet for this network family yet.
Also: network_family, hint
POST /payout-wallets with network_family, then fund it. free
422 asset_not_acceptable The money is held in an asset that is none of the invoice's payment options.
Also: asset
Refund it. free
422 amount_too_small_to_settle The platform fee is not less than the held amount.
Also: asset, amount, platform_fee
Refund it instead. free
429 rate_limited
retryable
Too many requests: per key, per account and environment, or invalid keys per IP address (Retry-After). Wait Retry-After seconds. —
429 invoice_rate_limited
retryable
Too many new invoices per minute in this account and environment (API and dashboard together; Retry-After).
Also: environment, limit
Wait Retry-After seconds; the platform operator can raise the limit. free
429 open_invoice_limit
retryable
Too many open invoices in this account and environment (open of limit).
Also: environment, limit, open
Cancel unused invoices or let them expire. free
500 internal_error
retryable
An unexpected error. Retry with the same request and key; quote the request_id to support if it persists. free
502 price_source_error The price source cannot price an asset of this fiat invoice.
Also: fields
Price the invoice in crypto, or in another currency. free
502 provider_error The wallet or chain provider refused the request. Contact the platform operator with the request_id. free
503 price_source_unavailable
retryable
The price source is down or its rate is stale (Retry-After).
Also: fields, hint
Retry later with the same request. free
503 provider_unavailable
retryable
The wallet or chain provider is unreachable (Retry-After when known). Retry later with the same request. free
503 network_fee_unavailable
retryable
No network fee estimate right now (Retry-After: 60). Retry in a minute with the same request. free
503 schema_update_pending
retryable
This feature needs a database update the platform operator has not applied yet (Retry-After). Retry later, or ask the operator to apply the update (Admin → System). free
503 not_installed
retryable
The platform is not installed yet. Ask the platform operator. —

Ignore keys you do not know; new detail keys may be added.

7. Webhooks #

Endpoints and subscriptions #

Register an https endpoint with POST /webhook-endpoints (or Developers → Webhooks in the dashboard). event_types takes *, a prefix such as invoice.* (prefixes: invoice, deposit, settlement, fee, refund, split_session, splitter, payout ) or exact event names. The signing secret is shown once; PATCH with rotate_secret creates a new one (the old one keeps signing for a while). Sandbox and live endpoints are separate.

Payload #

Every delivery is a POST with this body (schema_version 2026-10-12):

{
  "id": "01J9Z8F6G7H8J9KMNPQRSTVWXY",
  "schema_version": "2026-10-12",
  "type": "invoice.paid",
  "occurred_at": "2026-10-12T10:07:12+00:00",
  "delivery_attempt_id": "01J9Z8F6G7H8J9KMNPQRSTVWXZ",
  "environment": "sandbox",
  "resource": {
    "type": "invoice",
    "id": "01J9Z3A1B2C3D4E5F6G7H8J9KM"
  },
  "data": {
    "number": "INV-MAIN-000123",
    "deposit_id": "01J9Z8F6G7H8J9KMNPQRSTVWY0",
    "excess": "0.000000",
    "excess_atomic": "0",
    "accepted": false,
    "accepted_kind": null,
    "acceptance_id": null,
    "status": "paid",
    "asset": "USDT_TRON",
    "amount_required": "49.990000",
    "amount_required_atomic": "49990000",
    "amount_paid": "49.990000",
    "amount_paid_atomic": "49990000",
    "amount_pending": "0.000000",
    "amount_pending_atomic": "0",
    "amount_due": "0.000000",
    "amount_due_atomic": "0",
    "amount_held": "0.000000",
    "amount_held_atomic": "0",
    "needs_decision": false,
    "invoice_id": "01J9Z3A1B2C3D4E5F6G7H8J9KM",
    "invoice_number": "INV-MAIN-000123",
    "transaction_id": "TST-7K3M-9QX2-H4PD",
    "order_id": "order-1042"
  }
}

data carries the references invoice_id, invoice_number, transaction_id and order_id whenever the event concerns a payment (unknown ones are left out). Every invoice.* event except invoice.settlement_* carries the invoice's amount block: status, asset, amount_required, amount_paid, amount_pending, amount_due, amount_held (decimal strings, each with an *_atomic sibling in the smallest unit) and needs_decision. Amounts are decimal strings everywhere. Events are small: read the object again before acting. For invoice, refund, payout, settlement, split_session and splitter events that is GET /invoices|refunds|payouts|settlements|split-sessions|splitters/{resource.id}; a settlement_transfer (settlement.transfer_*, fee.collected) has no endpoint of its own: read GET /settlements/{data.settlement_id} and its transfers[]; a deposit neither: GET /deposits?tx_hash={data.tx_hash} (deposit.held has no tx_hash: list the payment's deposits with ?invoice_id=, or ?owner_id= the split session) and pick the one whose id is resource.id. GET /webhook-events is also a polling feed.

Events #

Only confirmed money is reported as paid: invoice.partially_paid, invoice.underpaid, invoice.late_payment and invoice.paid_late are sent at confirmation, never at first sight.

EventSent whenresource.typedata
Invoices
invoice.created An invoice was created (also from the dashboard). invoice number, amount, currency, expires_at
+ references
invoice.confirming Payments that cover the invoice were seen and wait for confirmations. invoice deposit_id
+ references
invoice.payment_dropped The payment it was confirming left the chain and nothing else covers it: open again until it expires. invoice deposit_id, tx_hash, reason
+ references
invoice.partially_paid A confirmed payment covers part of the amount (sent at confirmation only). invoice deposit_id
+ references
invoice.paid Paid in full with confirmed money, or your accept made it paid: ship the order. invoice number, deposit_id, excess, excess_atomic, accepted, accepted_kind, acceptance_id
+ references
invoice.overpaid Paid with more than asked: ship the order; the excess is held for your decision. invoice number, deposit_id, excess, excess_atomic, accepted, accepted_kind, acceptance_id
+ references
invoice.underpaid It closed with a confirmed partial payment (or its amounts changed): held for your decision. invoice trigger, expires_at, cancelled_at, deposit_id
+ references
invoice.late_payment A payment that arrived after it closed confirmed and is held. invoice deposit_id, tx_hash, deposit_asset, deposit_amount, deposit_amount_atomic, held
+ references
invoice.paid_late Confirmed money after it closed now covers it (or, already paid late, an on-time payment was held too): held for your decision (accept or refund). invoice deposit_id, trigger
+ references
invoice.payment_held A payment was held for another reason (reason: after it was paid, on a second option, below the minimum, another asset). invoice deposit_id, tx_hash, deposit_asset, deposit_amount, deposit_amount_atomic, reason
+ references
invoice.accepted Your accept of held money was planned (then invoice.paid for an underpaid or paid-late invoice). invoice acceptance_id, kind, accepted_asset, accepted_amount, accepted_amount_atomic, status_before, settlement_id, platform_fee, platform_fee_atomic, network_fee, network_fee_atomic, via
+ references
invoice.accept_failed Your accept could not be planned; nothing moved and the decision is needed again. invoice acceptance_id, accepted_asset, accepted_amount, accepted_amount_atomic, reason, message
+ references
invoice.expired It closed at expires_at without any confirmed payment. invoice number, trigger
+ references
invoice.cancelled You cancelled it. invoice reason
+ references
invoice.settlement_processing Its settlement is sending your share. invoice settlement_id, status
+ references
invoice.settlement_scheduled Daily schedule: collected, sent with the next daily settlement. invoice settlement_id, status
+ references
invoice.settlement_credited On-request schedule: your share is in your balance. invoice settlement_id, status
+ references
invoice.settlement_settled Your share reached your wallet or splitter. invoice settlement_id, status
+ references
invoice.settlement_held Its settlement waits (see the settlement's hold_reason). invoice settlement_id, status
+ references
Refunds
refund.created A refund was created. refund refund_id, status, status_reason, asset, amount, fiat_currency, fiat_amount, funding_source, destination, destination_type, created_via
+ references
refund.queued It waits for money (status_reason); sent automatically when the money suffices. refund refund_id, status, status_reason, asset, amount, fiat_currency, fiat_amount, funding_source, destination, destination_type, created_via, needed, available
+ references
refund.processing Funded; its transfer is on its way. refund refund_id, status, status_reason, asset, amount, fiat_currency, fiat_amount, funding_source, destination, destination_type, created_via, settlement_id, network_fee
+ references
refund.completed Sent and confirmed. refund refund_id, status, status_reason, asset, amount, fiat_currency, fiat_amount, funding_source, destination, destination_type, created_via, tx_hash, explorer_url, network_fee
+ references
refund.failed Its transfer failed. refund refund_id, status, status_reason, asset, amount, fiat_currency, fiat_amount, funding_source, destination, destination_type, created_via, tx_hash, funds_returned
+ references
refund.cancelled Cancelled before anything was sent. refund refund_id, status, status_reason, asset, amount, fiat_currency, fiat_amount, funding_source, destination, destination_type, created_via
+ references
Payouts
payout.created A payout was created (funded, or waiting for approval). payout payout_id, status, status_reason, asset, amount, destination, destination_basis, customer_id, customer_email, order_id, matched_invoice_id, created_via, network_fee
payout.completed Sent and confirmed. payout payout_id, status, status_reason, asset, amount, destination, destination_basis, customer_id, customer_email, order_id, matched_invoice_id, created_via, tx_hash, explorer_url, network_fee
payout.failed Its transfer failed. payout payout_id, status, status_reason, asset, amount, destination, destination_basis, customer_id, customer_email, order_id, matched_invoice_id, created_via, tx_hash, funds_returned
payout.cancelled Cancelled before anything was sent. payout payout_id, status, status_reason, asset, amount, destination, destination_basis, customer_id, customer_email, order_id, matched_invoice_id, created_via
Settlements
settlement.requested A withdrawal was recorded (its amount set aside). settlement kind, status, asset, amount, destination, estimated_network_cost
+ references
settlement.planned A settlement was planned with its fees. settlement kind, gross, platform_fee, network_fee, execution_cost_reserve, distributable, asset
+ references
settlement.held A settlement waits (reason, e.g. below the minimum payout). settlement kind, reason, asset
+ references
settlement.awaiting_resources A settlement waits for gas; it continues by itself. settlement kind, reason, needed, available, fee_asset, budget_owner
+ references
settlement.settling Its transfers are broadcast. settlement kind, status, transfers_confirmed, transfers_total
+ references
settlement.pooled Daily schedule: moved to your collection address. settlement kind, status, transfers_confirmed, transfers_total
+ references
settlement.completed Every transfer confirmed. settlement kind, status, transfers_confirmed, transfers_total, settlement_batch_id
+ references
settlement.partially_completed Some transfers confirmed, others failed. settlement kind, status, transfers_confirmed, transfers_total
+ references
settlement.reconciliation_required It needs operator review (a failed transfer, or a payment reversed after it was credited: reason, reversed). settlement kind, status, transfers_confirmed, transfers_total, reason, reversed
+ references
settlement.credited On-request schedule: your share was credited to your balance. settlement kind, status, transfers_confirmed, transfers_total, credited
+ references
settlement.cancelled A withdrawal was cancelled or could not be planned (reason; its amount is available again), or a held invoice settlement was replaced by a new plan of the same payment (reason replanned: <id> with replaced_by, or replanned: nothing left), or recorded by the platform operator as paid to you outside the platform (replanned: settled outside). settlement kind, status, reason, replaced_by, asset, amount
+ references
settlement.transfer_confirmed One on-chain transfer of a settlement confirmed (not sent for the platform fee or the collection address). settlement_transfer settlement_id, settlement_kind, kind, asset, amount, destination, memo, tx_hash, explorer_url, status
+ references
settlement.transfer_failed One on-chain transfer of a settlement failed. settlement_transfer settlement_id, settlement_kind, kind, asset, amount, destination, memo, tx_hash, explorer_url, status, reason
+ references
fee.collected The platform fee transfer of a settlement confirmed. settlement_transfer asset, amount, tx_hash, settlement_id
+ references
Activity
deposit.observed A payment was seen on the network. deposit status, asset, amount, tx_hash, block_number, confirmations, classification, hold_reason, owner, address, payer_address
+ references
deposit.confirmed A payment confirmed. deposit status, asset, amount, tx_hash, block_number, confirmations, classification, hold_reason, owner, address, payer_address
+ references
deposit.reorged A confirmed payment was undone by a reorganisation. deposit status, asset, amount, tx_hash, block_number, confirmations, classification, hold_reason, owner, address, payer_address
+ references
deposit.held A payment was kept aside (reason). deposit reason, amount, asset
+ references
Splitters
splitter.created A splitter was created. splitter status, asset
splitter.paused A splitter was paused. splitter
splitter.resumed A splitter was resumed. splitter
Split sessions
split_session.created A split session was created. split_session status, asset, deposit_address
+ references
split_session.payment_detected A payment was seen. split_session deposit_id, asset, amount
+ references
split_session.payment_dropped The payment it detected left the chain before it confirmed. split_session deposit_id, tx_hash, reason, status
+ references
split_session.late_payment A payment arrived after it closed; it is held. split_session deposit_id, asset, amount
+ references
split_session.closed You closed it. split_session status
+ references
split_session.expired It expired without a payment. split_session expired_at
+ references
split_session.processing Its split is being sent. split_session status, asset, settled_gross
+ references
split_session.settled Every recipient was paid. split_session status, asset, settled_gross
+ references
split_session.partially_settled Some transfers confirmed, others not. split_session status, asset, settled_gross
+ references
split_session.held It is held: late money, or its target cannot pay out as configured. split_session status, asset, settled_gross, reason, hold_reason, expected_amount, platform_fee, network_fee, minimum_payout
+ references

Verify the signature #

HeaderMeaning
X-Webhook-IdEvent id (ULID): the same in every retry and replay. Dedupe on it.
X-Webhook-TimestampUnix seconds when this delivery was signed. Reject deliveries older than 300 seconds.
X-Webhook-Key-IdWhich endpoint secret signed it (k1, k2 after a rotation).
X-Webhook-Signaturev1=<hex HMAC-SHA256 of timestamp + "." + raw_body>. During a secret rotation two comma-separated signatures are sent (X-Webhook-Previous-Key-Id names the older key): accept the delivery when any of them matches.
X-Webhook-EventThe event type, e.g. invoice.paid.
<?php
// Verify a Cryptonera webhook. Use the RAW body — never re-encode JSON before hashing.
$secret   = getenv('CRYPTONERA_WEBHOOK_SECRET');           // whsec_...
$raw      = file_get_contents('php://input');
$sigHdr   = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$tsHdr    = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$eventId  = $_SERVER['HTTP_X_WEBHOOK_ID'] ?? '';

if (!ctype_digit($tsHdr) || abs(time() - (int) $tsHdr) > 300) {
    http_response_code(400); exit('stale timestamp');
}
$expected = 'v1=' . hash_hmac('sha256', $tsHdr . '.' . $raw, $secret);
$ok = false;
foreach (explode(',', $sigHdr) as $part) {   // two v1= values while a secret is rotated: compare each
    if (hash_equals($expected, trim($part))) { $ok = true; break; }
}
if (!$ok) { http_response_code(401); exit('bad signature'); }

// Dedupe on $eventId (retries and replays reuse it), then answer 2xx quickly.
$event = json_decode($raw, true, 64, JSON_THROW_ON_ERROR);
// $event['type'], $event['resource'], $event['data']
http_response_code(200);
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const app = express();
// Verify the raw body: never a re-serialised JSON object.
app.post('/webhooks/cryptonera', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.get('X-Webhook-Timestamp') ?? '';
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
  const expected = Buffer.from('v1=' + createHmac('sha256', process.env.CRYPTONERA_WEBHOOK_SECRET)
    .update(ts + '.').update(req.body).digest('hex'));
  const ok = (req.get('X-Webhook-Signature') ?? '').split(',').some((s) => {
    const b = Buffer.from(s.trim());
    return b.length === expected.length && timingSafeEqual(b, expected);
  });
  if (!ok) return res.sendStatus(401);
  const event = JSON.parse(req.body.toString('utf8'));
  // dedupe on req.get('X-Webhook-Id'), then handle event.type quickly
  res.sendStatus(200);
});
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ['CRYPTONERA_WEBHOOK_SECRET'].encode()


@app.post('/webhooks/cryptonera')
def cryptonera_webhook():
    raw = request.get_data()  # the raw body, before any JSON parsing
    ts = request.headers.get('X-Webhook-Timestamp', '')
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        abort(400)
    expected = 'v1=' + hmac.new(SECRET, ts.encode() + b'.' + raw, hashlib.sha256).hexdigest()
    signatures = request.headers.get('X-Webhook-Signature', '').split(',')
    if not any(hmac.compare_digest(expected, s.strip()) for s in signatures):
        abort(401)
    event = request.get_json()
    # dedupe on request.headers['X-Webhook-Id'], then handle event['type'] quickly
    return '', 200

Hash the raw body, never a re-serialised object; reject timestamps older than 300 s; compare in constant time; dedupe on X-Webhook-Id, then answer quickly. Check your receiver with this test vector:

Test vector (check your receiver with it):
  secret     whsec_test
  timestamp  1760000000
  raw body   {"id":"evt_1"}
  signed     1760000000.{"id":"evt_1"}
  header     v1=66e880d7175fffb43ce10c4e14db1cfb230c8804b5aafb116affbc9a836c7690

Delivery and retries #

Answer any 2xx within 8 seconds. A failed delivery is retried with growing waits (30 s · 2n, at most 6 hours) up to 12 attempts, about 17 hours after the first; then it is dead. An endpoint that keeps failing is marked failing and still receives. A disabled endpoint receives nothing: its pending deliveries are cancelled, and replays to it are refused until you enable it. A replay keeps the event id, so a receiver that already handled the event ignores it.

Product 2 of 2

Crypto Splitter API #

Split incoming crypto between several recipients by percentage. Two shapes: a splitter is a persistent receiving wallet whose recipients are versioned (edits apply from the next block on), and a split session is a one-off fresh deposit address for a single order or request. Both settle confirmed deposits to the recipients through the custody provider; the platform commission is snapshotted when funds are accepted.

Base URL, keys, idempotency, errors and webhooks are shared with Crypto Billing: start with the quick start.

Endpoints #

EndpointScopeIdempotency-KeyPurpose
Splitters
GET /splitters splitters:read — List splitters
POST /splitters splitters:write Optional Create a splitter
GET /splitters/{id} splitters:read — Read a splitter
PATCH /splitters/{id} splitters:write Optional Change the recipients
POST /splitters/{id}/pause splitters:write Optional Pause a splitter
POST /splitters/{id}/resume splitters:write Optional Resume a splitter
Split sessions
GET /split-sessions sessions:read — List split sessions
POST /split-sessions sessions:write Optional Create a split session
GET /split-sessions/{id} sessions:read — Read a split session
POST /split-sessions/{id}/close sessions:write Optional Close a split session

Assets, the fee policy, deposits, settlements and webhooks are in the shared endpoint table.

Example: create a fresh-address split session #

Two recipients on Tron receive USDT 60/40. Each confirmed deposit is settled as soon as it is confirmed. Replace the placeholders with addresses valid on the target network; addresses are validated before the session is created.

# split sessions have no order check: a new UUID per session protects a retry
curl -X POST https://cryptonera.com/api/v1/split-sessions \
  -H "Authorization: Bearer $CRYPTONERA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d4e1b2c-9f6a-4e38-a5d1-3b8c0e2f6a97" \
  -d '{
    "asset": "USDT_TRON",
    "recipients": [
      {
        "address": "<valid-tron-recipient-A>",
        "percentage": "60.00",
        "label": "Studio"
      },
      {
        "address": "<valid-tron-recipient-B>",
        "percentage": "40.00",
        "label": "Producer"
      }
    ],
    "settlement_trigger": "each_confirmed_deposit",
    "order_id": "order-10021",
    "expires_in_seconds": 3600
  }'

Response (201 Created, or 202 Accepted with status allocating when no fresh address is available yet: poll status_url):

{
  "id": "01J9Z2K7M3N4P5Q6R7S8T9V0WX",
  "object": "split_session",
  "transaction_id": "TST-4Q8M-2ZK7-VN3D",
  "status": "awaiting_payment",
  "environment": "sandbox",
  "asset": "USDT_TRON",
  "network": "tron-nile",
  "deposit_address": "<fresh-tron-deposit-address>",
  "memo": null,
  "payment_uri": null,
  "order_id": "order-10021",
  "settlement_trigger": "each_confirmed_deposit",
  "expected_amount": null,
  "minimum_deposit": "1.000000",
  "received_confirmed": "0.000000",
  "settled_gross": "0.000000",
  "held": "0.000000",
  "recipients": [
    {
      "address": "<valid-tron-recipient-A>",
      "memo": null,
      "percentage": "60.00",
      "label": "Studio"
    },
    {
      "address": "<valid-tron-recipient-B>",
      "memo": null,
      "percentage": "40.00",
      "label": "Producer"
    }
  ],
  "fee_policy": {
    "snapshot_id": "01J9Z2K7M3N4P5Q6R7S8T9V0WY",
    "platform_percentage": "1.00",
    "recipient_basis": "after_platform_fee_and_disclosed_same_asset_costs",
    "network_cost_mode": "separate_native_gas_budget",
    "operator_fee_wallet_configured": true
  },
  "confirmation_policy": {
    "required_confirmations": 19,
    "finality": "solidified"
  },
  "freshness": {
    "never_previously_allocated": true,
    "prior_activity_observed": false,
    "checked_at": "2026-10-12T10:00:00+00:00",
    "provider": "trongrid"
  },
  "late_payment_policy": "hold_for_review",
  "metadata": null,
  "expires_at": "2026-10-12T11:00:00+00:00",
  "closed_at": null,
  "first_payment_at": null,
  "settled_at": null,
  "created_at": "2026-10-12T10:00:00+00:00",
  "status_url": "https://cryptonera.com/api/v1/split-sessions/01J9Z2K7M3N4P5Q6R7S8T9V0WX",
  "checkout_url": "https://cryptonera.com/pay/s/<public-token>",
  "request_id": "req_01J9Z2K7M3N4P5Q6R7S8T9V0WZ"
}

Field values above are illustrative; the exact fee_policy and freshness objects depend on operator configuration. A persistent splitter is created the same way with POST /splitters (no expiry; recipients can be replaced later with PATCH, which opens a new version from the next block).

Lifecycle & events #

ValueMeaning
allocatingNo fresh address yet: poll status_url (or GET /split-sessions/{id}).
awaiting_paymentWaiting for the payment to deposit_address.
payment_detectedA payment is seen, waiting for confirmations.
processingConfirmed; the split is being sent.
settledEvery recipient was paid.
partially_settledSome transfers confirmed, others not (the operator reviews them).
expiredClosed at expires_at without a payment.
heldKept aside (late or not payable as configured): see the settlement or split_session.held.
cancelledClosed by you before a payment.

Late or excess money after a session closed is held for you, never sent to the recipients. Events: split_session.*, splitter.*, deposit.* and settlement.*: see events.

Sandbox (test mode) #

Sandbox is a complete, separate copy of the platform for building and testing your integration. It runs on public test networks (Bitcoin testnet, Ethereum Sepolia, Base Sepolia, Arbitrum Sepolia, Polygon Amoy, Tron Nile) with free test coins that have no value. Sandbox and live never mix: keys, addresses, balances, webhooks and events are separate, and a ck_sandbox_ key can only ever see sandbox data.

Never send real cryptocurrency to a sandbox address. Ethereum-style (0x…) and Tron (T…) test addresses look exactly like real ones; real coins sent there are not credited. Hosted checkout pages in sandbox show a TEST MODE banner.

Testing step by step #

  1. Switch your dashboard to Sandbox and create an API key under Developers → API keys. It starts with ck_sandbox_.
  2. Create a splitter, split session or invoice with that key. The response contains a payment address on a test network (or a hosted checkout URL).
  3. Get free test coins from a faucet (table below) into your own test wallet: MetaMask with "Show test networks" on (add Base Sepolia, Arbitrum Sepolia and Polygon Amoy from chainlist.org), TronLink set to the Nile node, or a Bitcoin testnet wallet.
  4. Pay the address with the exact asset on the exact network. Invoices expect an exact amount, so pay from your wallet; a splitter address may also be pasted straight into a faucet (any amount above the asset's minimum deposit). Paying with a token such as USDC also needs a little of the network's own test coin (ETH, or POL on Amoy) for the network fee.
  5. After the network's confirmations you receive the same webhooks as in live (deposit.confirmed, settlement.*, settlement.transfer_confirmed, invoice.paid…) with "environment": "sandbox", and the objects move through the same states.
  6. To go live, the platform enables live for your account; then create a ck_live_ key and replace the sandbox key. Nothing from sandbox carries over.

Free test coins, per asset #

Faucets are free public services run by other companies; their limits and sign-in rules change from time to time. Test networks are slower and less reliable than real ones.

AssetHow to get test coins
BTC
Bitcoin Testnet
coinfaucet.eu: Paste your tb1… address, solve the captcha, press "Get bitcoins". Arrives after one block (about 10–30 minutes).
bitcoinfaucet.uo1.net: Same steps; small amounts, limited per day.
Wallet: A Bitcoin testnet wallet (e.g. Electrum started in testnet mode). Addresses start with tb1.
BCH
Bitcoin Cash Testnet
tbch.googol.cash: Choose network "testnet4", paste your bchtest:… address, answer the sum and press "Get Coins!".
Wallet: Electron Cash started in testnet4 mode (electron-cash --testnet4). Addresses start with bchtest:.
ETH
Ethereum Sepolia
Google Cloud Web3 faucet: Sign in with a Google account, paste your 0x… address, press "Receive". Arrives within a minute.
Alchemy Sepolia faucet: Free Alchemy account; may ask for a small amount of real ETH on mainnet in the same address as an anti-abuse check.
Wallet: MetaMask (Settings → Advanced → "Show test networks", then pick Sepolia) or any EVM wallet.
TRX
Tron Nile Testnet
Nile faucet: Paste your T… address, solve the captcha, press "Obtain". Test TRX arrives in seconds; limited per day.
Wallet: TronLink (settings → Node → "Nile Testnet"). Addresses start with T.
USDT
Ethereum Sepolia
USDT has no official test token on Ethereum Sepolia (the issuer publishes none), so it can only be tested in live with a small amount.
USDT
Tron Nile Testnet
USDT has no official test token on Tron Nile Testnet (the issuer publishes none), so it can only be tested in live with a small amount.
USDC
Ethereum Sepolia
Circle USDC faucet: Choose USDC, pick the same network, paste your 0x… address, press "Send". Limited per hour. To pay with it you also need a little test ETH/POL on that network for the gas fee.
Wallet: MetaMask (Settings → Advanced → "Show test networks", then pick Sepolia) or any EVM wallet.
USDC
Base Sepolia
Circle USDC faucet: Choose USDC, pick the same network, paste your 0x… address, press "Send". Limited per hour. To pay with it you also need a little test ETH/POL on that network for the gas fee.
Wallet: MetaMask with the Base Sepolia network added (chainlist.org → search "Base Sepolia" → "Add to MetaMask").
USDC
Arbitrum Sepolia
Circle USDC faucet: Choose USDC, pick the same network, paste your 0x… address, press "Send". Limited per hour. To pay with it you also need a little test ETH/POL on that network for the gas fee.
Wallet: MetaMask with Arbitrum Sepolia added (chainlist.org → "Arbitrum Sepolia").
USDC
Polygon Amoy
Circle USDC faucet: Choose USDC, pick the same network, paste your 0x… address, press "Send". Limited per hour. To pay with it you also need a little test ETH/POL on that network for the gas fee.
Wallet: MetaMask with Polygon Amoy added (chainlist.org → "Amoy").

Reference files #

  • OpenAPI 3.1 document (both products; every operation, object and webhook event): https://cryptonera.com/developers/openapi.json
  • API revision 2026-10-12 (also the webhook schema_version).
  • docs/API.md in the source repository has the same reference as Markdown.
  • Every table on this page is generated from the platform's code and its OpenAPI document.