API reference

CryptoInvoice API

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.

Amounts are strings. Integer units (amount_units) are wei / drops / lovelace / token base units. Timestamps are Unix seconds.

Quickstart

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.

Concepts

Statuses

StatusMeaning
pendingAwaiting payment.
paidA matching transaction was seen on-chain. Safe for low-value goods; wait for confirmed for anything valuable.
confirmedReached the required confirmations (XRP: validated ledger = 1; Ethereum: 6 blocks; Cardano: 6 blocks). Final.
expiredNot 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.

How payments are matched

Assets

assetchaindecimalsminimumnotes
XRPxrpl60.01destination tags
ADAcardano61.5mainnet addr1…
ETHethereum180.0001mainnet; amounts shown with 8 decimals
USDCethereum60.5ERC-20 on mainnet
USDTethereum60.5ERC-20 on mainnet

Fiat currencies for pricing: USD, EUR, GBP. Rates come from Coinbase (fallback CoinGecko), refreshed every minute.

Workspaces

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).

Invoices

POST /api/invoices

fieldtypedescription
amountstringRequired. Decimal amount in currency.
currencystringUSD (default), EUR, GBP, or an asset symbol to price directly in crypto.
assetstringRequired when currency is fiat: which asset to receive. Must have an address configured for its chain.
memostring ≤200Shown to the payer.
redirect_urlurlPayer is sent here after payment is detected.
metadataobject/string ≤2000 charsReturned in webhooks — your order id, user id, etc.
expires_inseconds600 – 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.

Public endpoints (no key, CORS enabled)

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.

Embeddable button

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.

Pay page links

Pre-fill a pay page: /pay/acme?amount=10&currency=USD&asset=XRP&memo=Consulting&redirect=https://…&ref=abc. The payer sees a one-click confirm, then the checkout. ref lands in metadata.ref.

Webhooks

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.

Verify the signature

// 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"]);
}

Errors

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.

Security notes