API documentation

Base URL: https://cryptomo.net/api/v1. All requests and responses are JSON. Amounts are strings to avoid rounding errors.

  1. Create an account and a store, then wait for the store to be approved.
  2. Open the store in your dashboard and create an API key. Copy the secret key right away; it is shown only once.
  3. Set the store's Webhook URL and copy its webhook secret to verify notifications.

Authentication

Every request needs four headers:

X-Api-KeyYour public key (pk_…)
X-TimestampCurrent unix time in seconds. Must be within 5 minutes of our clock.
X-Signature-Version2
X-SignatureHex 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.

FieldRequiredDescription
amountyesPrice as a string, e.g. "49.00"
currencynoFiat currency, e.g. USD, EUR, INR. Defaults to the store currency.
order_idnoYour order reference (max 120 chars)
descriptionnoShown to the customer
customer_emailnoWe email the customer a receipt when paid
pay_currencynoSkip the coin picker, e.g. USDT_TRC20 (see codes below)
success_url / cancel_urlnoWhere the "Back to store" buttons go
callback_urlnoOverrides the store webhook URL for this invoice
metadatanoAny 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

newCreated; the customer has not picked a coin yet.
waitingCoin chosen, rate locked, waiting for the payment.
partially_paidLess than the amount was received. The customer can send the rest before expiry.
paidPaid in full (within the store's underpayment tolerance). Ship the order.
paid_overPaid more than required. Treat as paid.
paid_latePaid in full after the invoice expired. Treat as paid (the rate may have moved).
expiredTime ran out before full payment. paid_amount shows any partial payment.
cancelledCancelled 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.

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

CodeCoinNetwork
BTCBTCBitcoinBitcoin
ETHETHEthereumEthereum (ERC20)
USDT_ERC20USDTTether (ERC20)Ethereum (ERC20)
USDT_BEP20USDTTether (BEP20)BNB Smart Chain (BEP20)
USDT_POLYGONUSDTTether (Polygon)Polygon
USDC_ERC20USDCUSD Coin (ERC20)Ethereum (ERC20)
USDC_BEP20USDCUSD Coin (BEP20)BNB Smart Chain (BEP20)
USDC_POLYGONUSDCUSD Coin (Polygon)Polygon
BNBBNBBNBBNB Smart Chain (BEP20)
POLPOLPolygonPolygon
LTCLTCLitecoinLitecoin
DOGEDOGEDogecoinDogecoin
SOLSOLSolanaSolana
USDT_SOLUSDTTether (Solana)Solana
USDC_SOLUSDCUSD Coin (Solana)Solana
BCHBCHBitcoin CashBitcoin Cash
XRPXRPXRPXRP Ledger
TONTONToncoinTON
USDT_TONUSDTTether (TON)TON
ETH_ARBETHEthereum (Arbitrum)Arbitrum One
USDC_ARBUSDCUSD Coin (Arbitrum)Arbitrum One
USDT_ARBUSDTTether (Arbitrum)Arbitrum One
ETH_BASEETHEthereum (Base)Base
USDC_BASEUSDCUSD Coin (Base)Base