API documentation & developer guide
v1 · liveQuickstart
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 (
KeyIdandSecret) 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.
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.
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.
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.
| Scope | What it grants |
|---|---|
| invoices:read | Read invoices, balances, rates and credit entries. |
| invoices:write | Create and cancel invoices, attach deposits, change credit settings. |
| webhooks:read | Read webhook registrations and delivery history. |
| webhooks:write | Register and deactivate webhooks, requeue a delivery. |
| addresses:write | Manage static and sweep addresses. Listing requires this scope too. |
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.
Idempotency-Key: 7f3c1e9a4b8d42f0a1c6e5b3d9074a2eResending 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.
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,cancelledpending→confirming,partially_paid,expired,cancelledconfirming→paid,overpaid,failedpaid⇄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.
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.
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)
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.pendinginvoice.confirminginvoice.partially_paidinvoice.paidinvoice.overpaidinvoice.expiredinvoice.cancelledpayment.reverteddeposit.unmatchedcredit.lowcredit.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": { "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.
| Code | HTTP | Meaning |
|---|---|---|
| validation_error | 422 | Something's off in what you entered. Check the fields. |
| unauthenticated | 401 | Email or password is incorrect. |
| forbidden | 403 | You don't have permission for this. |
| not_found | 404 | We couldn't find that record. |
| conflict | 409 | That record already exists. |
| insufficient_credit | 402 | Your fee credit has run out. Top up from the dashboard. |
| rate_limited | 429 | Too many requests. Wait a moment and try again. |
| unsupported_asset | 400 | That network and asset pair isn't supported. |
| upstream_error | 502 | The blockchain service is unreachable right now. Try again shortly. |
| stale_rate | 503 | That rate quote has expired. Request a new one. |
| amount_space_exhausted | 503 | No distinguishing amount is left for this address. Try again shortly. |
{"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_bpsfield of theGET /v1/creditresponse (basis points: 10 = 0.1%, 100 = 1%). - The fee is charged only on invoices that complete successfully (
paidandoverpaid). - 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.
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/autoor 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.
| Network | Asset | Decimals | Collection model | Contract address |
|---|---|---|---|---|
| TRON | TRX | 6 | Per-invoice dynamic address | Native coin |
| TRON | USDT | 6 | Static address + unique amount | TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t |
| BSC | BNB | 18 | Per-invoice dynamic address | Native coin |
| BSC | USDT | 18 | Static address + unique amount | 0x55d398326f99059fF775485246999027B3197955 |
| BSC | USDC | 18 | Static address + unique amount | 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d |
| TON | GRAM | 9 | Per-invoice dynamic address | Native coin |
| TON | USDT | 6 | Static address + unique amount | EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs |
| Ethereum | USDT | 6 | Static address + unique amount | 0xdAC17F958D2ee523a2206206994597C13D831ec7 |
| Ethereum | USDC | 6 | Static address + unique amount | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| Solana | SOL | 9 | Per-invoice dynamic address | Native coin |
| Solana | USDT | 6 | Static address + unique amount | Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB |
| Solana | USDC | 6 | Static address + unique amount | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
API endpoints
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /healthz | Service health check | public |
| GET | /readyz | Dependency and readiness status | public |
| GET | /v1/networks | Supported blockchain networks and assets | public |
| POST | /v1/invoices | Create a payment invoice | invoices:write |
| GET | /v1/invoices | List invoices (paginated) | invoices:read |
| GET | /v1/invoices/{invoice_id} | Fetch invoice details | invoices:read |
| POST | /v1/invoices/{invoice_id}/cancel | Cancel an invoice | invoices:write |
| GET | /v1/rates | Get a lockable rate quote | invoices:read |
| GET | /v1/balances | Wallet balances by network and asset | invoices:read |
| GET | /v1/deposits/unmatched | List unmatched incoming transfers | invoices:read |
| POST | /v1/deposits/{deposit_id}/attach | Attach an incoming transfer to an invoice manually | invoices:write |
| POST | /v1/static-addresses | Register a static settlement wallet | addresses:write |
| GET | /v1/static-addresses | List registered static addresses | addresses: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-addresses | Register a native sweep wallet | addresses:write |
| GET | /v1/sweep-addresses | Lists your configured sweep addresses. | addresses:write |
| DELETE | /v1/sweep-addresses/{address_id} | Deactivates a sweep address. | addresses:write |
| GET | /v1/credit | Current fee credit balance | invoices:read |
| GET | /v1/credit/entries | Credit ledger entries and history | invoices:read |
| GET | /v1/credit/deposit-addresses | Lists your credit top-up addresses. | invoices:read |
| POST | /v1/credit/deposit-addresses | Get a credit top-up wallet address | invoices:write |
| PUT | /v1/credit/auto | Update the automatic top-up setting | invoices:write |
| POST | /v1/webhooks | Register a webhook endpoint | webhooks:write |
| GET | /v1/webhooks | Lists 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/deliveries | Webhook notification and delivery history | webhooks:read |
| POST | /v1/webhooks/deliveries/{delivery_id}/replay | Replay a failed notification | webhooks:write |
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.
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| network | string | yes | tron | bsc | ethereum | ton | solana | Chain identifier. |
| asset | string | yes | 2–16 | Asset symbol. Upper-cased by the server. |
| address | string | yes | 20–128 | Your own wallet address. Validated against the chain's format. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| network | string | yes | tron | bsc | ethereum | ton | solana | Chain identifier. |
| address | string | yes | 20–128 | Your own wallet address. Validated against the chain's format. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| url | string | yes | — | HTTPS URL that notifications are sent to. |
| events | string[] | no | — | Events to subscribe to. Leave empty to receive all of them. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| network | string | no | tron | bsc | ethereum | ton | solana | Chain identifier. |
| asset | string | no | 2–16 | Asset symbol. Upper-cased by the server. |
| allowed_assets | string[] | no | ≤ 32 | Assets offered to the payer: USDT (any network) or tron:USDT (one network). |
| amount | decimal | no | > 0 | Crypto amount, set directly. Cannot be combined with fiat_amount. |
| fiat_amount | decimal | no | > 0 | Fiat amount; the rate is locked when the invoice opens. |
| fiat_currency | string | no | 3–8 | Fiat currency code. Required when fiat_amount is given. |
| order_id | string | no | ≤ 128 | Your own order reference. Must be unique within your merchant account. |
| expires_in | integer | no | 60–86400 | Invoice lifetime, in seconds. |
| callback_url | string | no | — | Notification URL for this invoice only; overrides your registered endpoint. |
| return_url | string | no | — | Where the payer is sent once payment finishes. |
| locale | string | no | — | Language of the payment page. Falls back to the payer's browser language. |
| balance_payment | boolean | no | — | 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. |
| metadata | object | no | — | Free-form object. Returned verbatim in responses and webhooks. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| enabled | boolean | yes | — | Whether automatic credit top-up is on. |
| max_amount | decimal | no | > 0 | Ceiling for a single automatic top-up. |
Query parameters
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| from | string | yes | 2–16 | Source asset symbol, e.g. USDT. |
| to | string | yes | 3–8 | Target fiat currency, e.g. TRY. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| status | string | no | — | Invoice status — the constants from the lifecycle section above. |
| network | string | no | tron | bsc | ethereum | ton | solana | Chain identifier. |
| created_after | datetime | no | — | Created after this timestamp. |
| created_before | datetime | no | — | Created before this timestamp. |
| limit | integer | no | 1–200 | Maximum number of records to return. |
| cursor | datetime | no | — | Pagination cursor: records older than this. Pass back the next_cursor from the response. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| invoice_id | uuid | no | — | Filters down to a single invoice. |
| status | string | no | — | Delivery status: pending, succeeded, dead_letter. |
| limit | integer | no | 1–200 | Maximum number of records to return. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| kind | string | no | — | Entry kind: topup, fee, reversal, adjustment. |
| status | string | no | — | Entry status: pending, applied, void. |
| invoice_id | uuid | no | — | Filters down to a single invoice. |
| limit | integer | no | 1–200 | Maximum number of records to return. |
| cursor | datetime | no | — | Pagination cursor: records older than this. Pass back the next_cursor from the response. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| include_resolved | boolean | no | — | Include deposits that have already been matched. |
| limit | integer | no | 1–500 | Maximum number of records to return. |
| Field | Type | Required | Constraint | Description |
|---|---|---|---|---|
| include_inactive | boolean | no | — | Include addresses that have been released. |