REST · JSON · v1
API documentation
Accept crypto payments on your site or in your app. Create an invoice, send the customer to a hosted payment page and get a signed webhook when they pay.
https://zypher.exchange/apiHow it works
All requests and responses are JSON over HTTPS. Amounts are decimal strings and timestamps are ISO 8601 in UTC.
- 1
Create a store
Open Merchant, create a store and copy its API key and secret.
- 2
Create an invoice
Call POST /v1/invoice and send the customer to the returned paymentUrl.
- 3
Get paid
We call your webhook with invoice.paid. Check the signature and fulfil the order.
Authentication
Every request carries your store's API key and an HMAC-SHA256 signature made with its secret.
- X-API-Key
Your store's API key.
- X-API-Sign
Hex HMAC-SHA256 of the request body, keyed with your API secret. For GET requests, sign an empty string.
- X-API-Timestamp
Recommended. Unix time in seconds. With it, sign `timestamp.METHOD.path.body` instead of the body: the request is valid for 5 minutes and only once, so a captured request cannot be replayed.
Sign the compact JSON exactly as JSON.stringify produces it: no extra spaces, keys in the order you send them.
sign = hex(HMAC_SHA256(api_secret, body)) // body = '' for GETts = unix_seconds()
sign = hex(HMAC_SHA256(api_secret, ts + "." + METHOD + "." + "/api/v1/invoice?..." + "." + body))
// headers: X-API-Key, X-API-Sign: sign, X-API-Timestamp: tsTry it with your key
Add your store's API key and secret to send real requests from this page.
Create invoice
Price an invoice in a coin, to get a deposit address at once, or in fiat and let the customer pick the coin on the payment page.
/v1/invoiceBody parameters
- currencystringrequired
Coin, for example USDT. Required unless fiatAmount is set.
- networkstringrequired
Network, for example tron. Required unless fiatAmount is set.
- amountstringrequired
Amount in that coin, as a decimal string. Required unless fiatAmount is set.
- fiatAmountstring
Amount in fiat, up to 1,000,000. The customer chooses the coin on the payment page.
- fiatCurrencystring
Fiat currency code. Defaults to USD.
- orderIdstring
Your own order reference. It comes back in responses and webhooks.
- metadataobject
Any JSON object. Stored with the invoice and returned when you read it.
- acceptedCurrenciesstring[]
Fiat invoices only: the coins the buyer may pay with, e.g. ["USDT", "BTC"]. Default: every supported coin.
- expiresIninteger
How long the invoice stays open, in seconds (300 to 604800). Default: 3600 for fiat, 1800 for crypto invoices.
Example request
BODY='{"currency":"USDT","network":"tron","amount":"100.00","orderId":"order-123"}'
SIGN=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $NF}')
curl -X POST https://zypher.exchange/api/v1/invoice \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Sign: $SIGN" \
-H "Content-Type: application/json" \
-d "$BODY"{
"id": "5f0c3a52-7b1e-4f6a-9d2e-0c1b7a9e4d11",
"storeId": "b2d8e0f4-33a1-4c57-8e6b-1f9a2c7d5e30",
"orderId": "order-123",
"currency": "USDT",
"network": "tron",
"amount": "100.00",
"fiatAmount": null,
"fiatCurrency": null,
"paidAmount": null,
"depositAddress": "TXk3...Qp9",
"status": "PENDING",
"expiresAt": "2026-01-01T00:30:00.000Z",
"paidAt": null,
"txHash": null,
"metadata": { "customer": "[email protected]" },
"createdAt": "2026-01-01T00:00:00.000Z",
"paymentUrl": "/pay/5f0c3a52-7b1e-4f6a-9d2e-0c1b7a9e4d11"
}Returns the invoice. Send the customer to paymentUrl on our site, for example https://zypher.exchange/pay/{id}.
Get invoice
Returns the invoice with its current status. Rely on webhooks for changes and use this to double-check before you ship.
/v1/invoice/{id}BODY=''
SIGN=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $NF}')
curl https://zypher.exchange/api/v1/invoice/5f0c3a52-7b1e-4f6a-9d2e-0c1b7a9e4d11 \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Sign: $SIGN"Invoice object
Fields that do not apply yet are null.
- idstring
Invoice id (UUID).
- storeIdstring
The store that owns the invoice.
- orderIdstring | null
Your order reference.
- currencystring | null
Coin. null on a fiat invoice until the customer picks one.
- networkstring | null
Network. null on a fiat invoice until the customer picks one.
- amountstring | null
Amount due, in the coin.
- fiatAmountstring | null
Amount due in fiat, on fiat invoices.
- fiatCurrencystring | null
Fiat currency code.
- paidAmountstring | null
Amount received so far, in the coin.
- depositAddressstring | null
Address the customer pays to.
- statusstring
Current status, see Statuses.
- expiresAtstring
When the invoice stops accepting payment.
- paidAtstring | null
When the payment was confirmed.
- txHashstring | null
Transaction hash of the payment.
- metadataobject | null
The object you sent, if any.
- createdAtstring
When the invoice was created.
- paymentUrlstring
Path of the hosted payment page for this invoice.
Statuses
A payment within 2% below the amount due counts as paid, and more than 2% above is overpaid.
- PENDING
Waiting for payment.
- CONFIRMING
Payment seen on the network, waiting for confirmations.
- PAID
Paid and confirmed.
- OVERPAID
Confirmed. The customer sent more than required.
- UNDERPAID
Part of the amount arrived. The invoice waits for the rest until it expires.
- EXPIRED
Expired without full payment.
Webhooks
Set a webhook URL in your store settings. We send a POST request to it when an invoice is paid.
Headers
- X-Webhook-Signature
Hex HMAC-SHA256 of the raw request body, keyed with your API secret.
- X-Webhook-Event
Event name.
- Content-Type
application/json
Events
- invoice.paid
The invoice was paid. data.status is PAID or OVERPAID.
- test
Sent by the test button in your store settings.
Payload
{
"event": "invoice.paid",
"data": {
"invoiceId": "5f0c3a52-7b1e-4f6a-9d2e-0c1b7a9e4d11",
"orderId": "order-123",
"status": "PAID",
"currency": "USDT",
"network": "tron",
"amount": "100.00",
"paidAmount": "100.00",
"txHash": "0x...",
"paidAt": "2026-01-01T00:15:00.000Z"
},
"timestamp": 1767226500000
}Verify the signature
Compute the HMAC over the raw body before you parse it, and compare in constant time. Reject the request if it does not match.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request body as received (Buffer or string), before JSON.parse
export function verifyWebhook(rawBody, signature, apiSecret) {
const expected = createHmac('sha256', apiSecret).update(rawBody).digest('hex');
const a = Buffer.from(signature ?? '');
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
// Express: app.post('/zypher', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verifyWebhook(req.body, req.get('X-Webhook-Signature'), process.env.API_SECRET)) return res.sendStatus(401);
// const { event, data } = JSON.parse(req.body);
// res.sendStatus(200);
// });Delivery
- Reply with any 2xx status within 10 seconds.
- Failed deliveries are retried up to 5 times with growing delays, starting at 1 minute.
- The same event can arrive more than once. Use data.invoiceId to deduplicate.
- The URL must be public: localhost and private network addresses are not called.
Test mode (sandbox)
Build and check your integration without real money. Create test keys in the merchant dashboard: the key starts with test_. The same endpoints and the same HMAC signature work with it, made with the test secret.
- Invoices made with a test key live apart from real ones: no real address is issued, nothing is watched on chain, nothing is credited to a balance and no commission is charged.
- The deposit address is a placeholder that starts with TEST-. Do not send coins to it.
- Mark a test invoice paid, overpaid, underpaid or expired with the simulate endpoint, or with the buttons on its payment page.
- Your webhook URL receives the same invoice.paid event as for a real payment, with "test": true in data, signed with the test secret.
- Test invoices are deleted after 30 days. A store can create up to 1000 a day.
- Other endpoints refuse test keys with 403, so a test key can never move funds.
Simulate a payment
/v1/invoice/{id}/simulateTest keys only. A live key gets 403.
Body parameters
- statusstringrequired
paid, overpaid, underpaid or expired. underpaid sends half of the amount and leaves the invoice open; a later paid sends the rest.
- currencystring
For a fiat-priced invoice with no coin picked yet. USDT on Tron when left out.
- networkstring
Network of that coin.
BODY='{"status":"paid"}'
SIGN=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $NF}')
curl -X POST https://zypher.exchange/api/v1/invoice/5f0c3a52-7b1e-4f6a-9d2e-0c1b7a9e4d11/simulate \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Sign: $SIGN" \
-H "Content-Type: application/json" \
-d "$BODY"Payload
{
"event": "invoice.paid",
"data": {
"invoiceId": "5f0c3a52-7b1e-4f6a-9d2e-0c1b7a9e4d11",
"orderId": "order-123",
"status": "PAID",
"currency": "USDT",
"network": "tron",
"amount": "100.00",
"paidAmount": "100",
"txHash": "test_4f1c...",
"paidAt": "2026-01-01T00:15:00.000Z",
"test": true
},
"timestamp": 1767226500000
}AML address check
Check a crypto address against our risk database before you pay out to it or accept funds from it. Same authentication as invoices.
/v1/aml/checkBody parameters
- addressstringrequired
The address to check, up to 128 characters.
- networkstring
Network, for example tron. If you leave it out, we work it out from the address format.
BODY='{"address":"TXk3...Qp9","network":"tron"}'
SIGN=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $NF}')
curl -X POST https://zypher.exchange/api/v1/aml/check \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Sign: $SIGN" \
-H "Content-Type: application/json" \
-d "$BODY"{
"address": "TXk3...Qp9",
"network": "tron",
"riskScore": 95,
"riskLevel": "SEVERE",
"signals": [
{ "category": "hack", "share": 95 },
{ "category": "mixer", "share": 60 }
],
"sources": ["manual"],
"checkedAt": "2026-01-01T00:00:00.000Z",
"quota": { "used": 12, "limit": 100, "tier": "start" }
}- riskScorenumber
Risk from 0 (clean) to 100 (illicit).
- riskLevelstring
LOW under 25, MEDIUM 25 to 49, HIGH 50 to 79, SEVERE 80 and up.
- signalsarray
Risk categories found with a score from 0 to 100, highest first. Categories: sanctions, ransomware, hack, darknet, scam, mixer, gambling, other, blocklist.
- sourcesstring[]
Where the matches come from.
- checkedAtstring
When the check ran.
- quotaobject
Checks used today, your daily limit and your merchant tier.
Currencies and networks
Pass the currency and network values exactly as listed.
- ETHethereumbasearbitrum
- USDTethereumtronbsctonsolana
- USDCethereumbasearbitrumbscsolana
- DAIethereumbasearbitrumbsc
- TRXtron
- BTCbitcoin
- BNBbsc
- GRAMton
- XMRmonero
- LTClitecoin
- SOLsolana
- DOGEdogecoin
Limits and errors
Limits apply per IP address. Past a limit you get 429, so pause and retry.
- 3 requests per second
- 20 requests per 10 seconds
- 100 requests per minute
Errors
Errors return JSON with statusCode, message and error.
400Invalid parameters, or an unsupported currency and network pair.
401Missing key or signature, unknown key or wrong signature.
403The request IP is not on the store's allowlist.
404Invoice not found in your store.
429Too many requests.
{
"statusCode": 401,
"message": "Invalid signature",
"error": "Unauthorized"
}