API documentation
Base URL: https://cryptomo.net/api/v1. All requests and responses are JSON. Amounts are strings to avoid rounding errors.
- Create an account and a store, then wait for the store to be approved.
- Open the store in your dashboard and create an API key. Copy the secret key right away; it is shown only once.
- Set the store's Webhook URL and copy its webhook secret to verify notifications.
Authentication
Every request needs four headers:
X-Api-Key | Your public key (pk_…) |
X-Timestamp | Current unix time in seconds. Must be within 5 minutes of our clock. |
X-Signature-Version | 2 |
X-Signature | Hex HMAC-SHA256(timestamp + "\n" + METHOD + "\n" + path + "\n" + raw_body, secret_key). path is everything after the host exactly as you send it, including the query string (e.g. /api/v1/invoices?status=paid). For requests without a body, raw_body is an empty string. The four parts are joined with a single newline. |
<?php
$secret = 'sk_...';
$url = 'https://cryptomo.net/api/v1/invoices';
$body = json_encode(['amount' => '49.00', 'currency' => 'USD', 'order_id' => '1024']);
$ts = (string) time();
$sig = hash_hmac('sha256', $ts . "\n" . 'POST' . "\n" . parse_url($url, PHP_URL_PATH) . "\n" . $body, $secret);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Api-Key: pk_...', "X-Timestamp: $ts", 'X-Signature-Version: 2', "X-Signature: $sig"],
]);
$invoice = json_decode(curl_exec($ch), true)['data'];
header('Location: ' . $invoice['checkout_url']);
// Node.js 18+
import crypto from 'node:crypto';
const url = new URL('https://cryptomo.net/api/v1/invoices');
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.CRYPTOMO_SECRET)
.update([ts, 'POST', url.pathname + url.search, body].join('\n')).digest('hex');
const res = await fetch(url, { method: 'POST', body,
headers: { 'Content-Type': 'application/json', 'X-Api-Key': process.env.CRYPTOMO_KEY, 'X-Timestamp': ts, 'X-Signature-Version': '2', 'X-Signature': sig } });
Older integrations that send no X-Signature-Version are still accepted with the previous signature, HMAC-SHA256(timestamp + raw_body, secret_key). It does not cover the method or the path, so once your integration uses version 2, tick v2 signatures only on the key (Dashboard → Stores → API keys). The key list shows which version each key last used. Our plugins from version 1.1 and the PHP SDK use version 2.
Create an invoice
POST /invoices → 201 with {"data": Invoice}. Send the customer to checkout_url.
| Field | Required | Description |
|---|---|---|
amount | yes | Price as a string, e.g. "49.00" |
currency | no | Fiat currency, e.g. USD, EUR, INR. Defaults to the store currency. |
order_id | no | Your order reference (max 120 chars) |
description | no | Shown to the customer |
customer_email | no | We email the customer a receipt when paid |
pay_currency | no | Skip the coin picker, e.g. USDT_TRC20 (see codes below) |
success_url / cancel_url | no | Where the "Back to store" buttons go |
callback_url | no | Overrides the store webhook URL for this invoice |
metadata | no | Any JSON (max 4 KB), returned in webhooks |
Send an Idempotency-Key header to safely retry: the same key returns the same invoice instead of creating a new one.
Get / list invoices
GET /invoices/{id} returns one invoice. GET /invoices?status=paid&order_id=1024&page=1&limit=25 lists invoices of the store, newest first.
Cancel an invoice
POST /invoices/{id}/cancel cancels an unpaid invoice (body can be empty).
Invoice object
{
"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.52", "paid_amount": "49.52", "rate": "0.9895",
"address": "T…", "memo": null,
"fee_amount": "0.4952", "merchant_amount": "49.0248", "is_late": false,
"checkout_url": "https://cryptomo.net/pay/5b1f…",
"transactions": [{ "tx_hash": "…", "amount": "49.52", "status": "credited", "created_at": "…" }],
"metadata": null, "expires_at": "…", "paid_at": "…", "created_at": "…"
}
Statuses
new | Created; the customer has not picked a coin yet. |
waiting | Coin chosen, rate locked, waiting for the payment. |
partially_paid | Less than the amount was received. The customer can send the rest before expiry. |
paid | Paid in full (within the store's underpayment tolerance). Ship the order. |
paid_over | Paid more than required. Treat as paid. |
paid_late | Paid in full after the invoice expired. Treat as paid (the rate may have moved). |
expired | Time ran out before full payment. paid_amount shows any partial payment. |
cancelled | Cancelled by you before payment. |
Webhooks
We POST JSON to your store's webhook URL on these events: invoice.paid, invoice.partially_paid, invoice.overpaid, invoice.expired, invoice.cancelled, invoice.refunded (a refund you requested was sent to the customer). Reply with any 2xx status. Failed deliveries are retried for 24 hours (1m, 5m, 15m, 1h, 3h, 6h, 12h, 24h).
POST /your-webhook
X-Cryptomo-Event: invoice.paid
X-Cryptomo-Timestamp: 1767225600
X-Cryptomo-Signature: hex(HMAC-SHA256(timestamp + "." + raw_body, webhook_secret))
{ "id": "…", "event": "invoice.paid", "created_at": "…", "data": { …Invoice… } }
<?php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_CRYPTOMO_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_CRYPTOMO_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, 'YOUR_WEBHOOK_SECRET');
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
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 (do this idempotently)
}
http_response_code(200);
Always check the invoice status from the payload (or call GET /invoices/{id}) and make your handler idempotent: the same event can arrive more than once.
Payment links
POST /payment-links with title, optional amount (empty = payer chooses), min_amount, currency, type (payment or donation), description, max_uses. Returns {"data": {"id", "url", …}}. GET /payment-links lists them.
GET /payment-links/{id} — one link with its counters. POST /payment-links/{id} — update title, description, amount, max_uses, success_message or turn it on/off with "active": true|false. GET /payment-links/{id}/invoices — invoices started from the link (?status=paid to filter).
Recurring invoices
POST /subscriptions with customer_email, description, amount, optional currency, period (weekly, monthly, quarterly, yearly), pay_days (1–30), max_cycles, start (YYYY-MM-DD), customer_name and locale (the customer's language: en, hi, es, ar, tr, ru, pt). The customer gets an email with a payment link every period.
GET /subscriptions, GET /subscriptions/{id} (includes customer_portal_url — one link where the customer sees all invoices and pays open ones). POST /subscriptions/{id}/pause, /resume, /cancel, /send (bill now). Paid recurring invoices send the usual invoice.paid webhook with metadata.subscription_id.
Balances & reporting
GET /balance — available and held balance per coin. GET /withdrawals — withdrawals, refunds and payouts with status and transaction hash. GET /stats?from=YYYY-MM-DD&to=YYYY-MM-DD — invoices created and paid, volume per currency, and amount received and fees per coin for this store.
Pay button (JavaScript)
Put a "Pay with crypto" button on any website. With Allow the checkout to open inside my website turned on in your store's branding settings, the payment opens as a pop-up; otherwise it opens the payment page.
<script src="https://cryptomo.net/assets/js/button.js" async></script>
<div class="cryptomo-button" data-link="PAYMENT-LINK-ID" data-label="Pay with crypto" data-color="#4f46e5"></div>
Use data-invoice="CHECKOUT-URL" instead of data-link for an invoice your server created. Other options: data-mode="redirect", data-success-url. The element fires cryptomo:paid and cryptomo:close events. To embed the checkout yourself, load CHECKOUT-URL?embed=1 in an iframe; it posts {source: "cryptomo", type: "paid" | "closed" | "status" | "resize", invoice, status} messages to the parent page.
Currencies, rates, balance
GET /currencies — coins your store accepts. GET /rates?currency=USD — current price of 1 coin. GET /balance — your balances. GET /ping — checks your keys.
Errors
HTTP 422
{ "error": { "code": "validation_error", "message": "amount must be a positive number like 25 or 19.99" } }
Codes: missing_auth, timestamp_expired, invalid_api_key, invalid_signature, signature_version_required (the key only accepts v2 signatures) (401), account_inactive, insufficient_scope (read-only key used for a change), ip_not_allowed (request from an IP not on the key's allowlist) (403), not_found (404), cannot_cancel (409), validation_error (422), rate_limited (429, max 120 requests/minute).
Currency codes
| Code | Coin | Network |
|---|---|---|
BTC | Bitcoin | |
ETH | Ethereum (ERC20) | |
USDT_ERC20 | Ethereum (ERC20) | |
USDT_BEP20 | BNB Smart Chain (BEP20) | |
USDT_POLYGON | Polygon | |
USDC_ERC20 | Ethereum (ERC20) | |
USDC_BEP20 | BNB Smart Chain (BEP20) | |
USDC_POLYGON | Polygon | |
BNB | BNB Smart Chain (BEP20) | |
POL | Polygon | |
LTC | Litecoin | |
DOGE | Dogecoin | |
SOL | Solana | |
USDT_SOL | Solana | |
USDC_SOL | Solana | |
BCH | Bitcoin Cash | |
XRP | XRP Ledger | |
TON | TON | |
USDT_TON | TON | |
ETH_ARB | Arbitrum One | |
USDC_ARB | Arbitrum One | |
USDT_ARB | Arbitrum One | |
ETH_BASE | Base | |
USDC_BASE | Base |