Base URL https://cryptoinvoice.dev/api. JSON in, JSON out. Authenticate with your workspace secret key: Authorization: Bearer sk_live_…. Public endpoints (checkout status, pay pages, rates) need no key and allow CORS.
amount_units) are wei / drops / lovelace / token base units. Timestamps are Unix seconds.1. Create a workspace (once) — or do it in the dashboard:
curl -X POST https://cryptoinvoice.dev/api/workspaces \
-H "Content-Type: application/json" \
-d '{"name":"Acme","handle":"acme","xrp_address":"rXXXX…","ada_address":"addr1…","eth_address":"0x…"}'
# → { "workspace": {…}, "secret_key": "sk_live_…" } (shown once)
2. Create an invoice and send the customer to invoice.url:
curl -X POST https://cryptoinvoice.dev/api/invoices \
-H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
-d '{"amount":"25.00","currency":"USD","asset":"XRP","memo":"Order #1042","redirect_url":"https://shop.example/thanks","metadata":{"order_id":1042}}'
{
"invoice": {
"id": "7kq2m9x4p1zt",
"status": "pending",
"chain": "xrpl", "asset": "XRP",
"address": "rXXXX…", "dest_tag": 2716349901,
"amount": "16.583212", "amount_units": "16583212", "decimals": 6,
"fiat_currency": "USD", "fiat_amount": "25.00", "rate": "1.50748",
"memo": "Order #1042", "metadata": {"order_id": 1042},
"required_confirmations": 1, "confirmations": 0,
"created_at": 1787440000, "expires_at": 1787443600,
"url": "https://cryptoinvoice.dev/i/7kq2m9x4p1zt",
"payment_uri": "ripple:rXXXX…?amount=16.583212&dt=2716349901",
"explorer_address_url": "https://livenet.xrpl.org/accounts/rXXXX…"
}
}
3. Receive a webhook (or poll GET /api/invoices/:id) when status becomes paid then confirmed.
| Status | Meaning |
|---|---|
pending | Awaiting payment. |
paid | A matching transaction was seen on-chain. Safe for low-value goods; wait for confirmed for anything valuable. |
confirmed | Reached the required confirmations (XRP: validated ledger = 1; Ethereum: 6 blocks; Cardano: 6 blocks). Final. |
expired | Not paid before expires_at (or cancelled). Late payments are still detected for 24 hours and flip the invoice to paid with late: true in the webhook. |
dest_tag. A payment to your address with that tag and ≥ 99% of the amount matches. If your XRP address is an exchange deposit address, set xrp_dest_tag on the workspace: every invoice then carries that fixed tag and is told apart by a unique amount (last 1–999 drops), exactly like ADA./reconcile to attach a specific transaction manually (e.g. the customer forgot the destination tag).| asset | chain | decimals | minimum | notes |
|---|---|---|---|---|
| XRP | xrpl | 6 | 0.01 | destination tags |
| ADA | cardano | 6 | 1.5 | mainnet addr1… |
| ETH | ethereum | 18 | 0.0001 | mainnet; amounts shown with 8 decimals |
| USDC | ethereum | 6 | 0.5 | ERC-20 on mainnet |
| USDT | ethereum | 6 | 0.5 | ERC-20 on mainnet |
Fiat currencies for pricing: USD, EUR, GBP. Rates come from Coinbase (fallback CoinGecko), refreshed every minute.
POST /api/workspaces — create. Body: name (required), handle, xrp_address, xrp_dest_tag (only for exchange-hosted XRP accounts), ada_address, eth_address (at least one address), webhook_url (https). Returns secret_key once. Rate limit: 5 per hour per IP.
GET /api/me — your workspace, plan and monthly usage.
PATCH /api/me — update any of: name, handle, xrp_address, xrp_dest_tag, ada_address, eth_address, webhook_url, brand_name, brand_color, pay_page_enabled. Send null or "" to clear an address.
DELETE /api/me — permanently delete the workspace and all its invoices.
POST /api/me/rotate-key · POST /api/me/rotate-webhook-secret · GET /api/me/webhooks (recent deliveries) · POST /api/me/upgrade ({"asset":"XRP","months":1|12} → a Pro payment invoice).
POST /api/invoices
| field | type | description |
|---|---|---|
amount | string | Required. Decimal amount in currency. |
currency | string | USD (default), EUR, GBP, or an asset symbol to price directly in crypto. |
asset | string | Required when currency is fiat: which asset to receive. Must have an address configured for its chain. |
memo | string ≤200 | Shown to the payer. |
redirect_url | url | Payer is sent here after payment is detected. |
metadata | object/string ≤2000 chars | Returned in webhooks — your order id, user id, etc. |
expires_in | seconds | 600 – 604800. Default 3600. |
Rate limit: 120 per minute per workspace. Free plan: 50 invoices per calendar month (HTTP 402 when exceeded).
GET /api/invoices?limit=50&before=<created_at>&status=paid — list newest first; paginate with next_before.
GET /api/invoices/:id — one invoice (full fields incl. tx_id, paid_amount, explorer_tx_url).
POST /api/invoices/:id/check — force an on-chain check now (normally every minute automatically, plus whenever the checkout page is open).
POST /api/invoices/:id/reconcile — body {"tx_id":"…"}. Attaches a specific transaction to this invoice (must be a successful payment to the invoice address from the last ~24h, not already used). Amount and destination tag are not enforced.
POST /api/invoices/:id/cancel — expire a pending invoice immediately.
GET /api/public/invoices/:id — payer-facing status. Polling this endpoint also triggers an on-chain check when the last one is older than 20 seconds. Add ?check=0 to skip that.
POST /api/public/invoices/:id/check — “I've paid” button. Throttled to once per 10 s per invoice.
GET /api/public/pay/:handle — pay-page info. POST /api/public/pay/:handle — create an invoice from a pay page: {"amount":"10","currency":"USD","asset":"ADA","payer_note":"…","memo":"…","redirect_url":"…"}. 12 per hour per IP.
GET /api/public/rates — USD prices · GET /api/public/config — supported assets, plan prices.
Drop a styled "Pay with crypto" button on any site with one line — no key needed: <script async src="https://cryptoinvoice.dev/embed.js" data-handle="yourname" data-amount="25" data-currency="USD" data-memo="Order 1"></script>. Or call CryptoInvoice.button({handle,amount,currency,asset,memo,text,theme,target}). Generate one at /tools/pay-button.
Pre-fill a pay page: /pay/acme?amount=10¤cy=USD&asset=XRP&memo=Consulting&redirect=https://…&ref=abc. The payer sees a one-click confirm, then the checkout. ref lands in metadata.ref.
When set, your webhook_url receives a POST for invoice.paid, invoice.confirmed and invoice.expired:
POST https://yourserver.com/cryptoinvoice
Content-Type: application/json
X-CryptoInvoice-Event: invoice.paid
X-CryptoInvoice-Signature: t=1787440123,v1=4f1c…e9
X-CryptoInvoice-Delivery: 812
{"id":"evt_7kq2m9x4p1zt_paid_1787440123","event":"invoice.paid","created_at":1787440123,
"data":{ "id":"7kq2m9x4p1zt","status":"paid","asset":"XRP","amount":"16.583212","paid_amount":"16.583212",
"tx_id":"A6F5…","metadata":{"order_id":1042},"confirmations":1,"required_confirmations":1, … },
"late": false}
Respond with any 2xx within 10 seconds. Failures are retried after 1 min, 5 min, 30 min, 2 h and 12 h. Deliveries may arrive out of order or more than once — key your logic on data.id + event.
// Node.js
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 600;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
# Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=") for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return abs(time.time() - int(parts["t"])) < 600 and hmac.compare_digest(expected, parts["v1"])
<?php // PHP
function verify(string $raw, string $header, string $secret): bool {
parse_str(str_replace(",", "&", $header), $p);
$expected = hash_hmac("sha256", $p["t"] . "." . $raw, $secret);
return abs(time() - (int)$p["t"]) < 600 && hash_equals($expected, $p["v1"]);
}
Non-2xx responses carry {"error": "message"}. 400 validation, 401 bad key, 402 plan limit, 404, 409 conflict (handle taken), 429 rate limited, 503 rate provider unavailable.
paid as provisional on Ethereum and Cardano until confirmed.