API documentation
Register the addresses you care about on any of 23 networks, then receive an alert for every incoming transaction — as a signed webhook, an API event, a Telegram message, an email, or a Discord or Slack post.
Authentication
Every request carries your API key in the Authorization header. You get two keys: ck_test_… (sandbox — simulated transactions, free) and ck_live_… (production).
$ curl https://api.cryptanio.com/v1/watches \
-H "Authorization: Bearer ck_live_4t9G…R1SC"
Quickstart
One request starts the watching. The next transaction on that address triggers your first alert — typically within seconds of the transaction entering a block.
$ curl -X POST https://api.cryptanio.com/v1/watches \
-H "Authorization: Bearer ck_live_…" \
-H "Content-Type: application/json" \
-d '{
"project": "1002",
"chain": "ethereum",
"address": "0x7c3A1d4E8b2f6C9a0D5e8F1b4C7a2E5d8B1f9eF2",
"asset": "USDC",
"label": "Main treasury",
"note": "Operations wallet"
}'
{
"id": "1008",
"project_id": "1002",
"service": "monitoring",
"book_address_id": "1007",
"status": "active",
"chain": "ethereum",
"address": "0x7c3A…9eF2",
"asset": "USDC",
"watch_outgoing": false,
"origin_kinds": ["manual"]
}
Configure Webhook, Web Push, Email, Slack, Discord and Telegram destinations for the account or project in the dashboard. Delivery policy belongs to the project, not to an individual address. Test everything against the sandbox first: addresses attached with a ck_test_ key can be triggered manually with POST /v1/simulate.
Watches API
A watch is one address attached to one Monitoring project. The address itself is stored once in the account address book; each project keeps its own direction, asset and event rules. All endpoints live under https://api.cryptanio.com/v1.
| Endpoint | What it does |
|---|---|
| POST/v1/watches | Create a watch. Returns 201 with the watch object. |
| GET/v1/watches?project=:id | List one Monitoring project's watches. project is required; chain is optional. |
| GET/v1/watches/:id | One watch with its recent payments. |
| DELETE/v1/watches/:id | Stop watching. In-flight transactions are still reported for a short grace period. |
| GET/v1/payments | Query detected payments — by watch, address, tx id or time range. |
| GET/v1/chains | The networks this deployment watches, with each one's confirmation rule and token support. No key required. |
| GET/v1/events | Re-fetch delivered events (up to 30 days) — your safety net if an endpoint was down. |
Request fields
| Field | Type | Notes |
|---|---|---|
| project | string | Required Monitoring project id. An address cannot exist outside a project route. |
| chain | string | One of bitcoin, ethereum, solana, tron, ton, bsc, base, xrp, dash. Required. Call GET /v1/chains for the live list. |
| address | string | The receiving address. On Solana pass the wallet — associated token accounts are derived and watched automatically. |
| label | string | Optional address-book name, up to 60 characters. |
| note | string | Optional account note, up to 280 characters. |
| asset | string | Shortcut for one exact asset: native by default, a supported symbol, or a network-specific identifier. |
| asset_policy | string | exact (default), allow, or all. |
| asset_ids | string[] | Assets included by an allow rule. Each explicit asset becomes its own physical scan target. |
| excluded_asset_ids | string[] | Assets excluded from an all rule. |
| watch_outgoing | boolean | Also alert when money leaves this address. Departures arrive as their own event types — debit.*, never payment.* — so nothing you already handle changes. Not every network can serve it; one that cannot refuses the watch and says so, rather than watching half of what you asked for. |
| min_confirmations | number | Optional project rule stricter than the network default. |
| min_amount | string | Optional positive amount in minimal units. Smaller movements remain auditable but do not notify. |
| watch_contracts | boolean | Enable supported allowance, owner, threshold and permission events. |
| watch_unexpected_assets | boolean | Report assets outside the accepted rule without crediting them. |
Webhooks
Each state change is one HTTP POST to your endpoint. Delivery is at-least-once: duplicates are possible and expected — deduplicate on (chain, tx_id, index, address, asset), or simply on event_id.
Every customer webhook uses the same versioned shape, regardless of chain. In the dashboard each project can choose After confirmation (confirmed states, rollbacks and contract changes), Full lifecycle (every current and future event, including pending states), or a custom event list. Live/sandbox scope, valuation currency and pause stay independent. A project may inherit the complete account route or own a complete custom route.
| Header | Meaning |
|---|---|
| X-Event-Id | Unique event identifier (ULID). Also in the body as event_id. |
| X-Event-Type | One of the twelve business types below, or the webhook.test control event. Route on this and never on a field inside the body. |
| X-Timestamp | Unix seconds at signing time — part of the signed material. |
| X-Signature | Hex HMAC-SHA256 over <timestamp>.<body>. |
| X-Attempt | Delivery attempt counter, starting at 1. |
Event types
Three families, and they are separate on purpose. You route on the type, and no amount of forgetting to read a field should let a withdrawal or a multisig change be read as money arriving.
| Type | Means |
|---|---|
| payment.pending | Money arrived and the chain has seen it. Nothing is final yet. |
| payment.confirmed | Arrived and settled. This is the one to ship goods on. |
| payment.rolled_back | A reorganisation took an arrival back. It did not happen. |
| debit.pending | Money is leaving the address. Only on watches created with watch_outgoing. |
| debit.confirmed | It left, and that is settled. |
| debit.rolled_back | A reorganisation took a departure back. |
| contract.approval | An allowance was granted against your address. |
| contract.owner_added | A multisig you watch has a new owner. |
| contract.owner_removed | An owner was removed from it. |
| contract.threshold_changed | The number of signatures it needs changed. |
| contract.permissions_changed | A chain-native account permission set was replaced, including its signing threshold. |
| asset.unexpected | An asset arrived where none was expected. Money really moved, and nothing was credited. |
Two body shapes, not twelve. The payment.* and debit.* families carry a payment object with an amount. The contract.* family and asset.unexpected carry a contract object instead — no amount, no confirmation depth, because nothing about them settles.
Payload
{
"schema_version": "v1",
"event_id": "1101",
"event_type": "payment.confirmed",
"occurred_at": "2026-09-10T20:00:00Z",
"project_id": "1002",
"mode": "live",
"sandbox": false,
"payment": {
"chain": "ethereum",
"direction": "in",
"asset": {
"symbol": "USDT",
"standard": "erc20",
"contract": "0xdAC17F…1ec7"
},
"address": "0x7c3A…9eF2",
"tx_id": "0x9b41f2…c77d",
"index": 2,
"height": 20731442,
"amount": "10.000000",
"amount_raw": "10000000",
"decimals": 6,
"confirmations": 12,
"valuation": {
"currency": "USD",
"amount": "10.00",
"rate": "1.00",
"rate_at": "2026-09-10T20:00:00Z"
}
},
"invoice": {
"id": "1009",
"reference": "order-4471",
"status": "paid",
"currency": "USD",
"amount": "10.00"
},
"customer": { "id": "customer-1842" },
"product": {
"id": "vps-month", "name": "VPS rental", "quantity": 1
},
"payer": { "comment": "Thank you!" },
"metadata": { "campaign": "summer" }
}
{
"schema_version": "v1",
"event_id": "1102",
"event_type": "debit.confirmed",
"occurred_at": "2026-09-10T20:07:44Z",
"project_id": "1002",
"mode": "live",
"sandbox": false,
"payment": {
"chain": "bitcoin",
"direction": "out",
"asset": { "symbol": "BTC" },
"tx_id": "c1d89fd9…fc8f",
"index": 0,
"address": "bc1qltw…uxjh",
"amount": "0.00435000",
"amount_raw": "435000",
"decimals": 8,
"confirmations": 3
}
}
{
"schema_version": "v1",
"event_id": "1103",
"event_type": "contract.owner_added",
"occurred_at": "2026-09-10T20:19:02Z",
"project_id": "1002",
"mode": "live",
"sandbox": false,
"contract": {
"chain": "ethereum",
"tx_id": "0x4ae1…b902",
"index": 4,
"height": 20731509,
"address": "0x7c3A…9eF2",
"kind": "owner_added",
"detail": { "owner": "0x91Bd…4c7A" }
}
}
- direction is "in" or "out" and is always present on a payment. The event type says it too; read whichever you prefer, but never infer it from the sign of an amount — amounts are never negative.
- Every value in detail is a string. An allowance is a uint256 and no JSON number can hold one.
- match_key is what the chain itself carried to say what a payment was for — a TON comment, a Solana Pay reference. Absent on chains and payments that carried nothing, which is most of them.
- mode is "live" or "test": which installation sent this. It is inside the signed body, not only a header, so a rehearsal cannot be handed to your production endpoint with the marker stripped off.
- amount_raw and amount are strings. The first is exact minimal units for arithmetic; the second is the exact decimal representation for display. Neither passes through a JSON float.
- invoice, customer, product, payer and metadata are immutable snapshots. They are present only when supplied for the matched invoice. invoice.id is Cryptanio's id; invoice.reference is yours.
- index separates several credits inside one transaction: a Bitcoin output index, an Ethereum log index, zero on Solana.
- valuation is what the payment was worth when it arrived. Its amount and rate are decimal strings, and rate_at fixes the price moment. The invoice currency wins; otherwise the project's selected currency is used. The object is absent when no trustworthy rate was available.
- Optional fields are omitted, not filled with empty strings. The schema is stable across every supported chain; chain-specific source documents are never the customer webhook contract.
Retries
- Timeout per attempt: 10 s. Network failures, 408, 425, 429 and 5xx are retried. Redirects are never followed; other 3xx/4xx responses end that delivery.
- Exponential retry targets grow from 2 s to 15 min with jitter. A valid Retry-After delta or HTTP date on any retryable response is respected as the minimum delay. Retries continue for 24 hours.
- Within one Cryptanio process, only one request at a time is sent to the same hostname. A transient failure starts a shared host cooldown, while healthy hosts have no artificial requests-per-second limit.
- 410 Gone retires the destination and its queued backlog. For a primary project route it pauses webhook delivery; deleting an API-created endpoint removes that subscription and closes its backlog too.
- Missed something anyway? GET /v1/events re-serves the last 30 days.
Queue test is a control event. The dashboard sends webhook.test with the same v1 envelope, signature and project routing but no payment or contract. It bypasses the project's pause and event filter so you can test before enabling delivery.
Verifying signatures
The signature is a hex HMAC-SHA256 over the string <timestamp>.<raw body>, keyed with your webhook secret (dashboard → Webhooks). Verify in constant time and reject stale timestamps to close replays.
const crypto = require("node:crypto");
function verify(secret, timestamp, rawBody, signature) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(signature));
}
// reject if |now − timestamp| > 300 s, then verify
func verify(secret, ts string, body []byte, sig string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts))
mac.Write([]byte("."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}
Live stream
GET /v1/stream is a server-sent events feed for live dashboards and local development. It keeps the query/event representation used by GET /v1/events; customer webhooks use the stricter versioned v1 contract documented above.
$ curl -N https://api.cryptanio.com/v1/stream \
-H "Authorization: Bearer ck_live_…"
: connected
retry: 5000
event: payment
data: {"event_id":"01J5WQ…","event_type":"payment.confirmed", … }
: ping
- Every event is event: payment, and data: contains the stored event representation. Do not use the stream parser as a webhook v1 parser.
- Lines beginning with a colon are comments — SSE's own keep-alive. One arrives on connect and one every twenty seconds, so a quiet account is distinguishable from a dead connection.
- It is not a replay. Only events that happen while you are connected arrive. For anything missed, GET /v1/events serves the last 30 days.
- Reconnect freely — the retry: 5000 hint tells a standard EventSource to come back after five seconds, and every browser implementation does it for you.
Endpoints API
Subscribe URLs to one project's events without opening the dashboard. A live key creates live endpoints; a test key creates sandbox endpoints, and each key can list or delete only its own mode.
| Endpoint | What it does |
|---|---|
| POST /v1/endpoints | Subscribe an https:// URL. Returns the endpoint with its secret — the only time that secret is shown. |
| GET /v1/endpoints | List the subscriptions on this account. Secrets are not repeated. |
| DELETE /v1/endpoints/:id | Stop delivering to it. |
| GET /v1/health | Liveness and the running version. No key required. |
{
"url": "https://example.com/hooks/cryptanio",
"label": "production",
"project": "1002"
}
→ 201
{
"id": "1010",
"project_id": "1002",
"sandbox": false,
"url": "https://example.com/hooks/cryptanio",
"label": "production",
"secret": "0460765e9292…544ce",
"source": "api",
"created_at": "2026-08-16T13:52:01Z"
}
- Each endpoint signs with its own secret, returned once at creation. Verify exactly as above; a rotated endpoint is a new one plus a delete.
- The project chooses delivery. Extra webhook endpoints follow that project's event, pause and live/sandbox policy; an individual address does not select channels.
- Public HTTPS on port 443 only. Credentials in URLs, fragments, redirects, loopback/private/link-local addresses and DNS answers containing a non-public address are refused. Environment proxy settings are not inherited.
- The same URL may be registered separately for different projects and for live and sandbox mode. Each registration has its own id and secret.
- Twenty per account. Not a plan limit — one payment becoming a hundred deliveries spends our egress and somebody else's endpoint, and no honest integration needs more than a handful.
Invoices API
Ask to be paid a specific amount, and be told when it arrives. An invoice takes an address out of your address book, holds it for the life of the order, and matches the exact number that lands on it — so you never have to guess which payment belongs to which cart.
This is the payments module. Every account gets ten invoices a month; a paid tier raises it. An invoice uses an address already attached to that Payments project, including addresses supplied by a connected address-book list or an automatic address source.
| Endpoint | What it does |
|---|---|
| POST/v1/invoices | Issue one. Returns 201 with the address and the exact amount to display. |
| GET/v1/invoices | List them, filterable by project, chain, status. |
| GET/v1/invoices/:id | One invoice as it stands now. |
| POST/v1/invoices/:id/cancel | Void it and release the address. The record stays readable — you will want to find the order you voided. |
Request fields
| Field | Type | Notes |
|---|---|---|
| chain | string | Required. Same list as GET /v1/chains. |
| amount | string | Required, as a decimal string in the asset — "12.50", not cents and not a float. A JSON number would lose the low digits of an 18-decimal token. |
| asset | string | The symbol. Defaults to the chain's own coin. |
| reference | string | Your order id. Echoed on every event, so you never keep a second mapping. |
| customer | object | Optional immutable customer snapshot: {"id":"customer-1842"}. The id is yours, not a Cryptanio user id. |
| product | object | Optional immutable product snapshot with id, name and integer quantity. Supply only the fields that apply. |
| payer | object | Optional payer snapshot: {"comment":"Thank you!"}. |
| metadata | object | Up to 20 string key-value pairs. Keys use letters, digits, dot, dash or underscore. Stored with the invoice and echoed in its payment webhooks. |
| invoice_currency | string | Your three-letter order currency, such as USD. Supply it together with invoice_amount; it also takes priority for payment valuation. |
| invoice_amount | string | Your positive decimal order total in invoice_currency. This is separate from crypto amount and is never parsed through a JSON float. |
| project | string | Which of your businesses this is for. Defaults to your first project; the pool never reaches across them. |
| address | string | Pin it to one of your own addresses instead of taking one from the pool. Several open orders can then share it, told apart by amount. |
| tolerance | string | How far under the asked amount still counts as paid — network fees deducted at the sender. Empty means exact. |
| expires_in | number | Minutes. The invoice holds an address the whole time, so this has a ceiling. |
{
"chain": "bitcoin",
"amount": "0.0125",
"reference": "order-4471",
"customer": { "id": "customer-1842" },
"product": { "id": "vps-month", "name": "VPS rental", "quantity": 1 },
"payer": { "comment": "Thank you!" },
"metadata": { "campaign": "summer" },
"invoice_currency": "USD",
"invoice_amount": "10.00",
"expires_in": 30
}
→ 201
{
"id": "1009",
"status": "pending",
"reference": "order-4471",
"customer": { "id": "customer-1842" },
"product": { "id": "vps-month", "name": "VPS rental", "quantity": 1 },
"payer": { "comment": "Thank you!" },
"metadata": { "campaign": "summer" },
"invoice_currency": "USD",
"invoice_amount": "10.00",
"chain": "bitcoin",
"asset": "BTC",
"address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"asked": "0.0125",
"amount": "0.01250002",
"amount_raw": "1250002",
"decimals": 8,
"sandbox": false,
"expires_at": "2026-08-16T14:22:01Z",
"created_at": "2026-08-16T13:52:01Z"
}
- Show amount, reconcile on amount_raw. When another open order on the same address already wanted your round number, we add a few minimal units so the two can be told apart. asked is what you agreed with the buyer; amount is what must actually arrive.
- An invoice settles from the same events as everything else. Your webhook fires on the payment; the invoice moves to paid in the same breath. There is no second stream to subscribe to.
- Two states ask a human. underpaid and late set needs_decision and wait: we will not decide on your behalf whether short money or money after the window counts. Resolve them in the dashboard.
- A test key issues sandbox invoices only, against sandbox addresses. Your integration suite can run all day without reserving the addresses your real orders are paid at — a released address cools off for 48 hours before it goes back in the pool, and a busy test run would otherwise empty it.
Payment lifecycle
Every payment moves through an explicit state machine. Each transition is one alert; configured channels receive their own deliveries.
| State | Meaning | When you receive it |
|---|---|---|
| pending | The transaction is in a block that is not yet irreversible. | Within seconds of inclusion — show progress in your checkout. |
| confirmed | Deep or final enough that the network's own rules say it will not be undone. | Each network's own rule: protocol finality on Ethereum, BNB Chain and Base; a finalized slot on Solana; a solidified block on TRON; a ChainLock on Dash; a validated ledger on XRP; depth on Bitcoin (3). Ship goods on this event. |
| rolled_back | The block left the chain in a reorganisation, or the transaction failed. | Only with proof — never inferred from silence. Rare, but this alert is why you can trust the other two. |
A payment reversed by a reorganisation and later re-mined produces a new pending → confirmed sequence with a new revision — your event log stays complete and ordered.
Limits & billing
Monitoring plans differ only in volume — every network, every channel and the whole API are in all of them, including the free allowance. Only the support level differs. Full pricing on the monitoring page.
| Plan | Watched addresses | Alerts / month | Price |
|---|---|---|---|
| Free | 5 | 1,000 | $0 |
| Start | 50 | 10,000 | $9 / mo |
| Pro | 500 | 100,000 | $29 / mo |
| Scale | 5,000 | 1,000,000 | $99 / mo |
- A watch = one address attached to one Monitoring project. Several asset targets for that same project address do not multiply the plan unit.
- An alert = one state change. Sending it to several channels and retrying the same event do not multiply usage.
- Over the limit? With metered overage off (the default), alerts keep flowing for 48 hours while we email you — no surprise charges, nothing silently dropped. If you explicitly enable overage, usage is charged only up to your monthly ceiling.
- API rate limit: 10 requests/s (Free), 50 requests/s (paid plans). 429 with Retry-After beyond that.
Questions?
Write to hello@cryptanio.com — an engineer answers, usually within a few hours.