Cryptain API
Create invoices, take payments on 13 blockchains and get a signed webhook the moment money arrives. JSON over HTTPS, amounts as strings, the same API our plugins use.
https://cryptain.net/api/v1
Quick start
- Create a store. Sign up, add your website under Stores and wait for approval (usually within a business day).
- Make an API key. Open the store, create a key and copy the secret key straight away. It is shown once.
- Set your webhook. Paste your server's webhook address into the store and copy its webhook secret.
- Create an invoice and send the customer to its
checkout_url. When the payment confirms we call your webhook.
All amounts are decimal strings such as "49.00", never floats, so nothing is lost to rounding.
BODY='{"amount":"49.00","currency":"USD","order_id":"1024"}'
TS=$(date +%s)
SIG=$(printf '%s\nPOST\n/api/v1/invoices\n%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$CRYPTAIN_SECRET" | cut -d' ' -f2)
curl https://cryptain.net/api/v1/invoices \
-H "Content-Type: application/json" \
-H "X-Api-Key: $CRYPTAIN_KEY" -H "X-Timestamp: $TS" \
-H "X-Signature-Version: 2" -H "X-Signature: $SIG" \
-d "$BODY"$body = json_encode(['amount' => '49.00', 'currency' => 'USD', 'order_id' => '1024']);
$ts = (string) time();
$sig = hash_hmac('sha256', "$ts\nPOST\n/api/v1/invoices\n$body", $secret);
$ch = curl_init('https://cryptain.net/api/v1/invoices');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-Api-Key: $key",
"X-Timestamp: $ts", 'X-Signature-Version: 2', "X-Signature: $sig"],
]);
$invoice = json_decode(curl_exec($ch), true)['data'];
header('Location: ' . $invoice['checkout_url']);import crypto from 'node:crypto';
const body = JSON.stringify({ amount: '49.00', currency: 'USD', order_id: '1024' });
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha256', process.env.CRYPTAIN_SECRET)
.update([ts, 'POST', '/api/v1/invoices', body].join('\n')).digest('hex');
const res = await fetch('https://cryptain.net/api/v1/invoices', { method: 'POST', body, headers: {
'Content-Type': 'application/json', 'X-Api-Key': process.env.CRYPTAIN_KEY,
'X-Timestamp': ts, 'X-Signature-Version': '2', 'X-Signature': sig } });
const { data: invoice } = await res.json();import hashlib, hmac, json, os, time, urllib.request
body = json.dumps({"amount": "49.00", "currency": "USD", "order_id": "1024"})
ts = str(int(time.time()))
msg = "\n".join([ts, "POST", "/api/v1/invoices", body])
sig = hmac.new(os.environ["CRYPTAIN_SECRET"].encode(), msg.encode(), hashlib.sha256).hexdigest()
req = urllib.request.Request("https://cryptain.net/api/v1/invoices", data=body.encode(), method="POST", headers={
"Content-Type": "application/json", "X-Api-Key": os.environ["CRYPTAIN_KEY"],
"X-Timestamp": ts, "X-Signature-Version": "2", "X-Signature": sig})
invoice = json.load(urllib.request.urlopen(req))["data"]Authentication
Every request carries four headers. Nothing else is needed: no OAuth, no sessions.
X-Api-Key | Your public key, pk_… |
X-Timestamp | Unix time in seconds, within 5 minutes of our clock |
X-Signature-Version | 2 |
X-Signature | Hex HMAC-SHA256 of the string on the right, keyed with your secret key |
The signed string is four parts joined by a single newline: the timestamp, the method in capitals, the path with its query string exactly as sent, and the raw body (an empty string for GET).
Keys can be read-only for reporting tools and can be limited to a list of server IP addresses. Keys made before version 2 signatures still work with HMAC(timestamp + body); tick v2 signatures only on a key once your integration is updated. All Cryptain plugins and SDKs already sign with version 2.
1767225600
GET
/api/v1/invoices?status=paid&limit=10
SDKs and plugins
Official libraries sign requests and verify webhooks for you. Each is a single file with no dependencies.
- PHP SDKPHP 7.4+, cURL
- Node.js SDKNode 18+, ES module
- Python SDKPython 3.8+, standard library
Running a store? Use a ready-made plugin for WooCommerce, WHMCS, Magento, PrestaShop, XenForo and more from the integrations page.
require 'Cryptain.php';
$cryptain = new Cryptain('https://cryptain.net/', getenv('CRYPTAIN_KEY'), getenv('CRYPTAIN_SECRET'));
$invoice = $cryptain->createInvoice(['amount' => '49.00', 'currency' => 'USD', 'order_id' => '1024']);
// in your webhook handler
$event = Cryptain::verifyWebhook(file_get_contents('php://input'), $_SERVER, getenv('CRYPTAIN_WEBHOOK_SECRET'));
if ($event && Cryptain::isPaid($event['data'])) { /* mark the order paid */ }import { Cryptain, verifyWebhook, isPaid } from './cryptain.mjs';
const cryptain = new Cryptain('https://cryptain.net/', process.env.CRYPTAIN_KEY, process.env.CRYPTAIN_SECRET);
const invoice = await cryptain.createInvoice({ amount: '49.00', currency: 'USD', order_id: '1024' });
// Express: app.post('/webhook', express.raw({ type: 'application/json' }), ...)
const event = verifyWebhook(req.body.toString('utf8'), req.headers, process.env.CRYPTAIN_WEBHOOK_SECRET);
if (event && isPaid(event.data)) markOrderPaid(event.data.order_id);from cryptain import Cryptain, verify_webhook, is_paid
cryptain = Cryptain("https://cryptain.net/", os.environ["CRYPTAIN_KEY"], os.environ["CRYPTAIN_SECRET"])
invoice = cryptain.create_invoice(amount="49.00", currency="USD", order_id="1024")
# Flask
event = verify_webhook(request.get_data(as_text=True), request.headers, os.environ["CRYPTAIN_WEBHOOK_SECRET"])
if event and is_paid(event["data"]):
mark_order_paid(event["data"]["order_id"])Create an invoice
POST/invoices
Returns 201 with the invoice. Send the customer to checkout_url; they pick a coin there unless you set pay_currency.
amountrequired | Price as a string, e.g. "49.00" |
currency | Fiat currency such as USD, EUR or INR. Defaults to the store's currency. |
order_id | Your order reference, up to 120 characters |
description | Shown to the customer on the checkout |
customer_email | We email a receipt when the invoice is paid |
pay_currency | Skip the coin picker, e.g. USDT_TRC20 (see codes) |
success_url, cancel_url | Where the customer's "Back to store" button goes |
callback_url | Webhook address for this invoice only |
metadata | Any JSON up to 4 KB, returned in every webhook |
Retries are safe. Send an Idempotency-Key header and repeating the request returns the same invoice instead of creating a second one.
HTTP/1.1 201 Created
{
"data": {
"id": "5b1f0e8a-…",
"order_id": "1024",
"status": "new",
"amount": "49.00",
"currency": "USD",
"checkout_url": "https://cryptain.net/pay/5b1f0e8a-…",
"expires_at": "2026-10-11T12:30:00Z",
"created_at": "2026-10-11T12:00:00Z"
}
}Get and list invoices
GET/invoices/{id}
GET/invoices
The list is newest first, for the store the key belongs to. Filter with status and order_id, page with page and limit (up to 100). The response includes meta.total.
Your webhook handler should read the invoice back with this call before shipping, so a forged request can never mark an order paid.
TS=$(date +%s)
SIG=$(printf '%s\nGET\n/api/v1/invoices?status=paid&limit=10\n%s' "$TS" "" \
| openssl dgst -sha256 -hmac "$CRYPTAIN_SECRET" | cut -d' ' -f2)
curl "https://cryptain.net/api/v1/invoices?status=paid&limit=10" \
-H "X-Api-Key: $CRYPTAIN_KEY" -H "X-Timestamp: $TS" \
-H "X-Signature-Version: 2" -H "X-Signature: $SIG"{
"data": [ { "id": "5b1f…", "status": "paid", … } ],
"meta": { "page": 1, "limit": 10, "total": 42 }
}Cancel an invoice
POST/invoices/{id}/cancel
Cancels an invoice that has not been paid; the body can be empty. Paid or partly paid invoices return 409 cannot_cancel. A payment that still arrives later is recorded and reported, never lost.
{ "data": { "id": "5b1f…", "status": "cancelled", … } }Invoice object
Once the customer picks a coin the invoice carries the locked rate, the exact crypto amount and a deposit address reserved for this invoice alone.
pay_amount | Crypto amount to send |
paid_amount | Crypto received so far |
fee_amount | Our fee, in the paid coin |
merchant_amount | What is credited to you |
memo | Destination tag or memo on networks that need one (XRP, TON) |
transactions | Each blockchain transfer with hash, amount and status |
{
"id": "5b1f…", "store_id": "…", "order_id": "1024", "status": "paid",
"amount": "49.00", "currency": "USD",
"pay_currency": "USDT_TRC20", "pay_symbol": "USDT", "network": "TRON",
"pay_amount": "49.00", "paid_amount": "49.00", "rate": "1.0000",
"address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE", "memo": null,
"fee_amount": "0.49", "merchant_amount": "48.51", "is_late": false,
"checkout_url": "https://cryptain.net/pay/5b1f…",
"transactions": [
{ "tx_hash": "9f2c…", "amount": "49.00", "status": "credited", "created_at": "…" }
],
"metadata": { "cart": 88 },
"expires_at": "…", "paid_at": "…", "created_at": "…"
}Statuses
| новый | Created; the customer has not picked a coin yet. |
| ожидание | Coin chosen, rate locked, waiting for the payment. |
| оплачен частично | Less than the amount arrived. The customer can send the rest before the invoice expires. |
| оплачен | Paid in full, within the store's underpayment tolerance. Deliver the order. |
| переплата | More than the amount arrived. Treat as paid. |
| оплачен с опозданием | Paid in full after expiry. Treat as paid; the rate may have moved. |
| истёк | Time ran out before full payment. paid_amount shows any partial payment. |
| отменён | Cancelled by you before payment. |
| refunded | You sent the money back to the customer. |
Webhooks
We POST JSON to your store's webhook address whenever an invoice changes. Reply with any 2xx. Failed deliveries retry for 24 hours (after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h and 24 h), and you can resend any delivery from the dashboard.
invoice.paid | Paid in full, including late payments |
invoice.partially_paid | Part of the amount arrived |
invoice.overpaid | More than the amount arrived |
invoice.expired | Time ran out |
invoice.cancelled | You cancelled it |
invoice.refunded | A refund you approved was sent |
test | Sent by the Send test webhook button |
Check X-Cryptain-Signature: hex HMAC-SHA256 of timestamp + "." + raw_body keyed with the webhook secret. Reject timestamps older than five minutes, and make your handler idempotent; the same event can arrive twice.
POST /your-webhook HTTP/1.1
Content-Type: application/json
X-Cryptain-Event: invoice.paid
X-Cryptain-Timestamp: 1767225600
X-Cryptain-Signature: 3f9a…
{
"id": "evt_…",
"event": "invoice.paid",
"created_at": "2026-10-11T12:04:10Z",
"data": { …invoice object… }
}$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_CRYPTAIN_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_CRYPTAIN_SIGNATURE'] ?? '';
$ok = hash_equals(hash_hmac('sha256', $ts . '.' . $raw, $webhookSecret), $sig)
&& abs(time() - (int) $ts) <= 300;
if (!$ok) { http_response_code(401); exit; }
$event = json_decode($raw, true);
if (in_array($event['data']['status'], ['paid', 'paid_over', 'paid_late'], true)) {
// mark order $event['data']['order_id'] as paid, once
}
http_response_code(200);app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
const ts = req.get('X-Cryptain-Timestamp') || '';
const expected = crypto.createHmac('sha256', process.env.CRYPTAIN_WEBHOOK_SECRET)
.update(ts + '.' + raw).digest('hex');
const sig = Buffer.from(req.get('X-Cryptain-Signature') || '');
if (sig.length !== expected.length || !crypto.timingSafeEqual(sig, Buffer.from(expected))
|| Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const event = JSON.parse(raw);
if (['paid', 'paid_over', 'paid_late'].includes(event.data.status)) markOrderPaid(event.data.order_id);
res.sendStatus(200);
});@app.post("/webhook")
def webhook():
raw = request.get_data(as_text=True)
ts = request.headers.get("X-Cryptain-Timestamp", "")
expected = hmac.new(SECRET.encode(), f"{ts}.{raw}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Cryptain-Signature", "")) \
or not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return "", 401
event = json.loads(raw)
if event["data"]["status"] in ("paid", "paid_over", "paid_late"):
mark_order_paid(event["data"]["order_id"])
return "", 200Payment links
POST/payment-links
A reusable page for a product, a service or donations. Leave amount empty to let the payer choose, with an optional min_amount. Other fields: title (required), currency, type (payment or donation), description, max_uses.
GET/payment-links
GET/payment-links/{id}
POST/payment-links/{id}
Update title, description, amount, max_uses or success_message, or switch it with "active": false. GET /payment-links/{id}/invoices lists the invoices it started.
{
"data": {
"id": "donate-7kq2",
"title": "Support the project",
"type": "donation",
"amount": null,
"currency": "USD",
"url": "https://cryptain.net/l/donate-7kq2",
"active": true
}
}Recurring invoices
POST/subscriptions
Crypto wallets cannot be charged automatically, so we email the customer a payment link every period. Fields: customer_email, description, amount, and optionally currency, period (weekly, monthly, quarterly, yearly), pay_days (1–30), max_cycles, start (YYYY-MM-DD), customer_name and locale.
POST/subscriptions/{id}/{action}
Actions: pause, resume, cancel and send (bill now). GET /subscriptions/{id} includes a customer_portal_url where the customer sees every invoice. Paid cycles send invoice.paid with metadata.subscription_id.
{
"customer_email": "ana@example.com",
"description": "Pro plan",
"amount": "29.00",
"currency": "USD",
"period": "monthly",
"pay_days": 7
}Balances and reporting
GET/balance
GET/withdrawals
GET/stats?from=2026-10-01&to=2026-10-31
Balances per coin, withdrawals, refunds and payouts with their transaction hashes, and a summary of invoices, volume and fees for any date range. Give accounting tools a read-only key.
{
"data": [
{ "currency": "USDT_TRC20", "symbol": "USDT", "available": "1227.60", "pending_withdrawal": "0" },
{ "currency": "BTC", "symbol": "BTC", "available": "0.01840000", "pending_withdrawal": "0" }
]
}Coins and rates
GET/currencies
GET/rates?currency=USD
GET/ping
The coins your store accepts, the current price of one coin in a fiat currency, and a quick check that your keys and signature are right.
{
"ok": true,
"store": { "id": "…", "name": "Northwind Supply", "status": "active" }
}Pay button and embed
Put a "Pay with crypto" button on any page with two lines of HTML. With Allow the checkout to open inside my website switched on in the store's branding, it opens as a pop-up; otherwise it goes to the payment page.
Use data-invoice="CHECKOUT-URL" for an invoice your server made, data-mode="redirect" to always leave the page, and data-success-url to continue after payment. The element fires cryptain:paid and cryptain:close.
To embed the checkout yourself, load CHECKOUT-URL?embed=1 in an iframe. It posts {source: "cryptain", type, invoice, status} messages with types paid, closed, status and resize.
<script src="https://cryptain.net/assets/js/button.js" async></script>
<div class="cryptain-button"
data-link="PAYMENT-LINK-ID"
data-label="Pay with crypto"
data-color="#d6b25e"></div>Testing
- Testnet. While the platform runs in testnet mode, checkouts use test networks and test coins, so you can pay invoices without real money.
- Test webhook. Press Send test webhook on your store to receive a signed
testevent and see the delivery log. - Postman. Import the collection, fill in
public_keyandsecret_key, and every request is signed for you. - OpenAPI. Generate a client in any language from the OpenAPI 3.1 file.
TS=$(date +%s)
SIG=$(printf '%s\nGET\n/api/v1/ping\n%s' "$TS" "" \
| openssl dgst -sha256 -hmac "$CRYPTAIN_SECRET" | cut -d' ' -f2)
curl https://cryptain.net/api/v1/ping \
-H "X-Api-Key: $CRYPTAIN_KEY" -H "X-Timestamp: $TS" \
-H "X-Signature-Version: 2" -H "X-Signature: $SIG"Errors
Errors come back with an HTTP status and a stable code; the message says what to fix.
| 401 | missing_auth, timestamp_expired, invalid_api_key, invalid_signature, signature_version_required |
| 403 | account_inactive, insufficient_scope (read-only key), ip_not_allowed |
| 404 | not_found |
| 409 | cannot_cancel |
| 422 | validation_error |
| 429 | rate_limited: more than 120 requests a minute per key |
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "validation_error",
"message": "amount must be a positive number like 25 or 19.99"
}
}Currency codes
Use these in pay_currency. Your store accepts the ones you switch on.
BTCBitcoin, BitcoinETHEthereum, Ethereum (ERC20)USDT_TRC20Tether (TRC20), Tron (TRC20)USDT_ERC20Tether (ERC20), Ethereum (ERC20)USDT_BEP20Tether (BEP20), BNB Smart Chain (BEP20)USDT_POLYGONTether (Polygon), PolygonUSDC_ERC20USD Coin (ERC20), Ethereum (ERC20)USDC_BEP20USD Coin (BEP20), BNB Smart Chain (BEP20)USDC_POLYGONUSD Coin (Polygon), PolygonBNBBNB, BNB Smart Chain (BEP20)POLPolygon, PolygonLTCLitecoin, LitecoinDOGEDogecoin, DogecoinSOLSolana, SolanaUSDT_SOLTether (Solana), SolanaUSDC_SOLUSD Coin (Solana), SolanaBCHBitcoin Cash, Bitcoin CashXRPXRP, XRP LedgerTONToncoin, TONUSDT_TONTether (TON), TONETH_ARBEthereum (Arbitrum), Arbitrum OneUSDC_ARBUSD Coin (Arbitrum), Arbitrum OneUSDT_ARBTether (Arbitrum), Arbitrum OneETH_BASEEthereum (Base), BaseUSDC_BASEUSD Coin (Base), BaseOpen an account in two minutes
Начать можно бесплатно. 1% с платежа. Переход на другой тариф — в любой момент.