API documentation & developer guide

v1 · live

Quickstart

Accepting crypto on your site takes three steps: create an API key, register your settlement wallet addresses, and open your first invoice. Because nothing is custodial, settlements move straight to a wallet you control.

  • Dashboard → API keys — Sign in and generate your key pair (KeyId and Secret) there. The secret is shown once, at creation.
  • Panel → Settings or POST /v1/static-addresses — registers the static wallet address for token settlements such as USDT (one per network and asset).
  • Panel → Settings or POST /v1/sweep-addresses — registers the main wallet (sweep address) that native coin settlements such as TRX, BNB and GRAM are collected into.
  • POST /v1/webhooks — registers the URL we notify the moment a payment completes.
Your API secret is encrypted at rest and, for security, shown exactly once. If you lose it, issue a new key from the dashboard and revoke the old one in a single click.

Request signing (HMAC)

Every HTTP request is signed with HMAC-SHA256. The signature is computed over the raw request body, so it stays valid even if the JSON key order changes.

Canonical signing template
TEMPLATE
METHOD \n PATH \n TIMESTAMP \n NONCE \n SHA256(raw_body)
X-Api-Key
API key id (pk_…)
X-Timestamp
Unix timestamp in seconds (server clock tolerant)
X-Nonce
Unique random string (max 128 characters, single use)
X-Signature
hex(hmac_sha256(secret, canonical))

Two layers protect the request: the timestamp must fall inside the tolerance window, and each nonce may be used only once. Replaying the same request is rejected automatically (HTTP 401).

X-Timestamp is Unix epoch seconds and may deviate from server time by at most 300 seconds. X-Nonce is unique per request; a used nonce is rejected for 600 seconds.

Example signed request client in Python
PYTHON
import hashlib, hmac, os, time, json, urllib.request
KEY_ID = os.environ["COINZEN_KEY_ID"]
SECRET = os.environ["COINZEN_SECRET"]
BASE = "https://coinzen.cerceyn.com"
def request(method: str, path: str, payload: dict | None = None) -> bytes:
body = json.dumps(payload).encode() if payload is not None else b""
ts = str(int(time.time()))
nonce = os.urandom(16).hex()
canonical = "\n".join(
[method.upper(), path, ts, nonce, hashlib.sha256(body).hexdigest()]
)
signature = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
req = urllib.request.Request(
BASE + path,
data=body or None,
method=method.upper(),
headers={
"X-Api-Key": KEY_ID,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": signature,
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req) as resp:
return resp.read()

Scopes

A key may only call endpoints covered by the scopes granted to it; a missing scope returns 403. You pick the scopes when you generate the key in the panel.

ScopeWhat it grants
invoices:readRead invoices, balances, rates and credit entries.
invoices:writeCreate and cancel invoices, attach deposits, change credit settings.
webhooks:readRead webhook registrations and delivery history.
webhooks:writeRegister and deactivate webhooks, requeue a delivery.
addresses:writeManage static and sweep addresses. Listing requires this scope too.
There is no addresses:read scope. The address routes enforce the scope at router level, so listing also requires addresses:write.

Rate limits

Limits apply per merchant over a one-minute sliding window. When exceeded you get 429 along with a Retry-After header.

  • General endpoints — 120 requests / minute
  • Invoice creation — 30 requests / minute

Idempotency

The Idempotency-Key header is required on POST /v1/invoices. Without it the request is rejected with 422. Generate a fresh, unique value for each request.

Required header
Idempotency-Key: 7f3c1e9a4b8d42f0a1c6e5b3d9074a2e

Resending the same body with the same key does not open a second invoice: the first response is replayed verbatim, with its original status code. You can safely retry after a dropped connection.

Using the same key with a different body is rejected with 409 idempotency_key_reused. The fingerprint covers the request method, path and raw body — reordering JSON keys counts as a different body.

Invoice lifecycle

Creating an invoice starts a state machine. If no network and asset were given, the invoice opens as selecting. When the customer picks a network and coin on the payment page it moves to pending and a payment address is allocated.

  • selecting → pending, expired, cancelled
  • pending → confirming, partially_paid, expired, cancelled
  • confirming → paid, overpaid, failed
  • paid ⇄ overpaid — if the customer sends another transfer against a paid invoice, the amount updates automatically.

paid, overpaid, expired, cancelled and failed are terminal. On a chain reorg or a late payment the system updates the state itself and tells your system over a webhook.

When amount_exact is true, the customer has to send exactly the stated amount so we can match the transfer instantly and without ambiguity.

Balance Payment

If you sell balance or credit, a fixed amount is the wrong model: the payer rounds 5.188 down to 5 and the invoice stays short. Open the invoice with balance_payment: true and whatever arrives closes it — the displayed amount is only a suggestion, and the payment page says so to the payer.

Credit from the webhook's received_amount and the received_fiat_amount computed at the locked rate, not from fiat_amount. If the invoice asked for 250 TRY and 5 USDT arrived, what you credit is the value of those 5 USDT. The fee accrues on the amount that arrived too.

The smallest amount that still closes the invoice is a store setting, written onto the invoice at creation. If the store sets no value of its own, the general setting applies (by default 25% of the expected amount); both are editable in the panel. A payment below the floor stays partially_paid. Changing the setting later does not touch open invoices.

For token collection the address is shared, and what binds a payment to an invoice is the amount. If several balance invoices are open at once on the same network/asset, a rounded payment can't be attributed; we don't guess — it lands in unmatched deposits and a deposit.unmatched webhook fires. You attach it to the right invoice from the panel. Can't be combined with a credit-funding invoice.

Webhook notifications

Every payment status change is pushed to your registered webhook endpoint. Each payload is signed with HMAC-SHA256, so you can verify that the data is intact and really came from Coinzen.

The webhook secret used for verification (whsec_…) is issued once, when you register the endpoint: on the dashboard's Webhooks page, or in the secret field of the POST /v1/webhooks response. It is not your API key's Secret; that one only signs the requests you send. Registering the same URL again issues a new webhook secret and disables the old entry.

X-Coinzen-Signature
t=<unix>,v1=<hex hmac_sha256(secret, "<t>.<raw_body>")>
X-Coinzen-Delivery
Unique delivery id (UUID)
X-Coinzen-Event
Event name that fired (e.g. invoice.paid)
Webhook verification example in Python
PYTHON
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
"""X-Coinzen-Signature: t=<unix>,v1=<hex>"""
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, v1)

Supported webhook events

  • invoice.pending
  • invoice.confirming
  • invoice.partially_paid
  • invoice.paid
  • invoice.overpaid
  • invoice.expired
  • invoice.cancelled
  • payment.reverted
  • deposit.unmatched
  • credit.low
  • credit.exhausted

Retry schedule

The first attempt fires immediately after the payment. If your server doesn't answer 200 OK, we retry across seven steps at 30 · 120 · 600 · 3600 · 21600 · 86400 second intervals. If every attempt fails the record moves to dead_letter and can be re-queued from the dashboard in one click.

Error contract

Every error uses the same envelope. The code field is stable and meant for programmatic matching; message is for humans and may change.

Error envelope
{
"error": {
"code": "conflict",
"subcode": "order_id_exists",
"message": "bu order_id zaten kullanılmış",
"details": { "order_id": "A-1042" }
}
}

The subcode field narrows the class code — order_id_exists instead of plain conflict. Check subcode first and fall back to code; the contract only widens, never narrows.

When a request fails schema validation (missing field, wrong type, out-of-range value), code is validation_error and details maps each field path to an error type: {"body.url": "url_scheme"}. The type is meant for programmatic matching (missing, greater_than, …); the explanation is in message.

CodeHTTPMeaning
validation_error422Something's off in what you entered. Check the fields.
unauthenticated401Email or password is incorrect.
forbidden403You don't have permission for this.
not_found404We couldn't find that record.
conflict409That record already exists.
insufficient_credit402Your fee credit has run out. Top up from the dashboard.
rate_limited429Too many requests. Wait a moment and try again.
unsupported_asset400That network and asset pair isn't supported.
upstream_error502The blockchain service is unreachable right now. Try again shortly.
stale_rate503That rate quote has expired. Request a new one.
amount_space_exhausted503No distinguishing amount is left for this address. Try again shortly.
Three responses fall outside this envelope: {"detail":"Not Found"}, returned when no route matches at all — it means the path is wrong; {"detail":"Method Not Allowed"}, returned when the path does not support the HTTP method; and an unexpected server error, which arrives as a 500 with a non-JSON body.

Fees and credit

Coinzen never takes its fee out of the customer's payment. USDT and every other crypto payment goes straight to your address, and only you hold that wallet's keys. The platform fee comes out of credit you topped up beforehand, so what you collect stays whole.

  • The standard rate is 1%. New accounts get 0.1% for their first 45 days. Your current rate is in the fee_bps field of the GET /v1/credit response (basis points: 10 = 0.1%, 100 = 1%).
  • The fee is charged only on invoices that complete successfully (paid and overpaid).
  • Cancelled, expired and underpaid invoices are never charged.
  • When your credit runs out, open invoices keep settling normally; only new invoice requests get a balance warning (HTTP 402).

Two-step charging

When an invoice is paid, the fee entry opens immediately as pending so chain tracking isn't slowed down, and it settles without waiting on any external service. The rate conversion happens in the background and the net amount comes off your balance.

If a chain reorg pulls the payment back, the fee is voided at once; if it had already been deducted, a reversal entry refunds it.

Topping up credit

POST /v1/credit/deposit-addresses returns a permanent USDT top-up address of your own. Anything you send there is added to your credit the moment it confirms on chain — no manual approval, no waiting.

The top-up address is your fee funding address; do not hand it to customers as a settlement address.

Automatic top-up

With this on, your sales keep running even if your credit hits zero. The proceeds of your next small invoice are routed into your credit balance instead, and service continues uninterrupted.

  • No separate fee is charged on an invoice routed into credit.
  • A cap applies for your protection (25 USD by default). Invoices above the cap are never routed, so large orders aren't put at risk.
  • Toggle it and set your own cap at any time through PUT /v1/credit/auto or the dashboard.

Balance alerts

When your credit drops below the threshold a credit.low event fires, and credit.exhausted when it runs out — so you hear about it before it bites.

Supported networks and assets

The table below summarises the live mainnet registry. For the runtime list, call GET /v1/networks.

NetworkAssetDecimalsCollection modelContract address
TRONTRX6Per-invoice dynamic addressNative coin
TRONUSDT6Static address + unique amountTR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
BSCBNB18Per-invoice dynamic addressNative coin
BSCUSDT18Static address + unique amount0x55d398326f99059fF775485246999027B3197955
BSCUSDC18Static address + unique amount0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d
TONGRAM9Per-invoice dynamic addressNative coin
TONUSDT6Static address + unique amountEQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs
EthereumUSDT6Static address + unique amount0xdAC17F958D2ee523a2206206994597C13D831ec7
EthereumUSDC6Static address + unique amount0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
SolanaSOL9Per-invoice dynamic addressNative coin
SolanaUSDT6Static address + unique amountEs9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
SolanaUSDC6Static address + unique amountEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

API endpoints

MethodPathWhat it doesScope
GET/healthzService health checkpublic
GET/readyzDependency and readiness statuspublic
GET/v1/networksSupported blockchain networks and assetspublic
POST/v1/invoicesCreate a payment invoiceinvoices:write
GET/v1/invoicesList invoices (paginated)invoices:read
GET/v1/invoices/{invoice_id}Fetch invoice detailsinvoices:read
POST/v1/invoices/{invoice_id}/cancelCancel an invoiceinvoices:write
GET/v1/ratesGet a lockable rate quoteinvoices:read
GET/v1/balancesWallet balances by network and assetinvoices:read
GET/v1/deposits/unmatchedList unmatched incoming transfersinvoices:read
POST/v1/deposits/{deposit_id}/attachAttach an incoming transfer to an invoice manuallyinvoices:write
POST/v1/static-addressesRegister a static settlement walletaddresses:write
GET/v1/static-addressesList registered static addressesaddresses:write
DELETE/v1/static-addresses/{address_id}Releases a static address. Refused while an amount reservation is still pending on it.addresses:write
POST/v1/sweep-addressesRegister a native sweep walletaddresses:write
GET/v1/sweep-addressesLists your configured sweep addresses.addresses:write
DELETE/v1/sweep-addresses/{address_id}Deactivates a sweep address.addresses:write
GET/v1/creditCurrent fee credit balanceinvoices:read
GET/v1/credit/entriesCredit ledger entries and historyinvoices:read
GET/v1/credit/deposit-addressesLists your credit top-up addresses.invoices:read
POST/v1/credit/deposit-addressesGet a credit top-up wallet addressinvoices:write
PUT/v1/credit/autoUpdate the automatic top-up settinginvoices:write
POST/v1/webhooksRegister a webhook endpointwebhooks:write
GET/v1/webhooksLists your registered webhook endpoints.webhooks:read
DELETE/v1/webhooks/{webhook_id}Deactivates a webhook endpoint. Past deliveries are kept, new ones stop.webhooks:write
GET/v1/webhooks/deliveriesWebhook notification and delivery historywebhooks:read
POST/v1/webhooks/deliveries/{delivery_id}/replayReplay a failed notificationwebhooks:write
The machine-readable version of this table lives at GET /openapi.json. There is no interactive try-it console: the signature is computed per request, so browser-issued calls would only ever return 401.

Request schemas

Below are the bodies for the endpoints you call on day one. The complete schema for every endpoint is published in machine-readable form at GET /openapi.json — import it into Postman or generate a client from it.

POST /v1/static-addresses
FieldTypeRequiredConstraintDescription
networkstringyestron | bsc | ethereum | ton | solanaChain identifier.
assetstringyes2–16Asset symbol. Upper-cased by the server.
addressstringyes20–128Your own wallet address. Validated against the chain's format.
POST /v1/sweep-addresses
FieldTypeRequiredConstraintDescription
networkstringyestron | bsc | ethereum | ton | solanaChain identifier.
addressstringyes20–128Your own wallet address. Validated against the chain's format.
POST /v1/webhooks
FieldTypeRequiredConstraintDescription
urlstringyes—HTTPS URL that notifications are sent to.
eventsstring[]no—Events to subscribe to. Leave empty to receive all of them.
POST /v1/invoices
FieldTypeRequiredConstraintDescription
networkstringnotron | bsc | ethereum | ton | solanaChain identifier.
assetstringno2–16Asset symbol. Upper-cased by the server.
allowed_assetsstring[]no≤ 32Assets offered to the payer: USDT (any network) or tron:USDT (one network).
amountdecimalno> 0Crypto amount, set directly. Cannot be combined with fiat_amount.
fiat_amountdecimalno> 0Fiat amount; the rate is locked when the invoice opens.
fiat_currencystringno3–8Fiat currency code. Required when fiat_amount is given.
order_idstringno≤ 128Your own order reference. Must be unique within your merchant account.
expires_inintegerno60–86400Invoice lifetime, in seconds.
callback_urlstringno—Notification URL for this invoice only; overrides your registered endpoint.
return_urlstringno—Where the payer is sent once payment finishes.
localestringno—Language of the payment page. Falls back to the payer's browser language.
balance_paymentbooleanno—Balance payment. Defaults to false. When enabled the invoice completes even if the payer sends less or more than shown; the webhook reports the amount that arrived. The floor comes from your store settings. Can't be combined with a credit-funding invoice.
metadataobjectno—Free-form object. Returned verbatim in responses and webhooks.
PUT /v1/credit/auto
FieldTypeRequiredConstraintDescription
enabledbooleanyes—Whether automatic credit top-up is on.
max_amountdecimalno> 0Ceiling for a single automatic top-up.

Query parameters

GET /v1/rates
FieldTypeRequiredConstraintDescription
fromstringyes2–16Source asset symbol, e.g. USDT.
tostringyes3–8Target fiat currency, e.g. TRY.
GET /v1/invoices
FieldTypeRequiredConstraintDescription
statusstringno—Invoice status — the constants from the lifecycle section above.
networkstringnotron | bsc | ethereum | ton | solanaChain identifier.
created_afterdatetimeno—Created after this timestamp.
created_beforedatetimeno—Created before this timestamp.
limitintegerno1–200Maximum number of records to return.
cursordatetimeno—Pagination cursor: records older than this. Pass back the next_cursor from the response.
GET /v1/webhooks/deliveries
FieldTypeRequiredConstraintDescription
invoice_iduuidno—Filters down to a single invoice.
statusstringno—Delivery status: pending, succeeded, dead_letter.
limitintegerno1–200Maximum number of records to return.
GET /v1/credit/entries
FieldTypeRequiredConstraintDescription
kindstringno—Entry kind: topup, fee, reversal, adjustment.
statusstringno—Entry status: pending, applied, void.
invoice_iduuidno—Filters down to a single invoice.
limitintegerno1–200Maximum number of records to return.
cursordatetimeno—Pagination cursor: records older than this. Pass back the next_cursor from the response.
GET /v1/deposits/unmatched
FieldTypeRequiredConstraintDescription
include_resolvedbooleanno—Include deposits that have already been matched.
limitintegerno1–500Maximum number of records to return.
GET /v1/static-addresses
FieldTypeRequiredConstraintDescription
include_inactivebooleanno—Include addresses that have been released.
API documentation & developer guide · Coinzen