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

request format
$ curl https://api.cryptanio.com/v1/watches \
  -H "Authorization: Bearer ck_live_4t9G…R1SC"
Keep keys server-side. A key grants full account access — never ship it in a mobile app or browser code. To roll a key, create a second one, move your traffic across, then revoke the first: revocation takes effect immediately, so cutting over before you revoke is what keeps you from locking yourself out.

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.

create a watch
$ 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"
  }'
response · 201 created
{
  "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.

EndpointWhat it does
POST/v1/watchesCreate a watch. Returns 201 with the watch object.
GET/v1/watches?project=:idList one Monitoring project's watches. project is required; chain is optional.
GET/v1/watches/:idOne watch with its recent payments.
DELETE/v1/watches/:idStop watching. In-flight transactions are still reported for a short grace period.
GET/v1/paymentsQuery detected payments — by watch, address, tx id or time range.
GET/v1/chainsThe networks this deployment watches, with each one's confirmation rule and token support. No key required.
GET/v1/eventsRe-fetch delivered events (up to 30 days) — your safety net if an endpoint was down.

Request fields

FieldTypeNotes
projectstringRequired Monitoring project id. An address cannot exist outside a project route.
chainstringOne of bitcoin, ethereum, solana, tron, ton, bsc, base, xrp, dash. Required. Call GET /v1/chains for the live list.
addressstringThe receiving address. On Solana pass the wallet — associated token accounts are derived and watched automatically.
labelstringOptional address-book name, up to 60 characters.
notestringOptional account note, up to 280 characters.
assetstringShortcut for one exact asset: native by default, a supported symbol, or a network-specific identifier.
asset_policystringexact (default), allow, or all.
asset_idsstring[]Assets included by an allow rule. Each explicit asset becomes its own physical scan target.
excluded_asset_idsstring[]Assets excluded from an all rule.
watch_outgoingbooleanAlso 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_confirmationsnumberOptional project rule stricter than the network default.
min_amountstringOptional positive amount in minimal units. Smaller movements remain auditable but do not notify.
watch_contractsbooleanEnable supported allowance, owner, threshold and permission events.
watch_unexpected_assetsbooleanReport 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.

HeaderMeaning
X-Event-IdUnique event identifier (ULID). Also in the body as event_id.
X-Event-TypeOne of the twelve business types below, or the webhook.test control event. Route on this and never on a field inside the body.
X-TimestampUnix seconds at signing time — part of the signed material.
X-SignatureHex HMAC-SHA256 over <timestamp>.<body>.
X-AttemptDelivery 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.

TypeMeans
payment.pendingMoney arrived and the chain has seen it. Nothing is final yet.
payment.confirmedArrived and settled. This is the one to ship goods on.
payment.rolled_backA reorganisation took an arrival back. It did not happen.
debit.pendingMoney is leaving the address. Only on watches created with watch_outgoing.
debit.confirmedIt left, and that is settled.
debit.rolled_backA reorganisation took a departure back.
contract.approvalAn allowance was granted against your address.
contract.owner_addedA multisig you watch has a new owner.
contract.owner_removedAn owner was removed from it.
contract.threshold_changedThe number of signatures it needs changed.
contract.permissions_changedA chain-native account permission set was replaced, including its signing threshold.
asset.unexpectedAn 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

payment.confirmed · application/json
{
  "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" }
}
debit.confirmed · money leaving
{
  "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
  }
}
contract.owner_added · no amount
{
  "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.

node.js
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
go
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))
}
Why the timestamp is inside the MAC. Signing the body alone would let an observer replay a captured request forever. With the timestamp bound in, rejecting old timestamps closes the replay window — and the timestamp can't be moved without breaking the signature.

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.

terminal
$ 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.

EndpointWhat it does
POST /v1/endpointsSubscribe an https:// URL. Returns the endpoint with its secret — the only time that secret is shown.
GET /v1/endpointsList the subscriptions on this account. Secrets are not repeated.
DELETE /v1/endpoints/:idStop delivering to it.
GET /v1/healthLiveness and the running version. No key required.
$ curl -X POST … /v1/endpoints
{
  "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.

EndpointWhat it does
POST/v1/invoicesIssue one. Returns 201 with the address and the exact amount to display.
GET/v1/invoicesList them, filterable by project, chain, status.
GET/v1/invoices/:idOne invoice as it stands now.
POST/v1/invoices/:id/cancelVoid it and release the address. The record stays readable — you will want to find the order you voided.

Request fields

FieldTypeNotes
chainstringRequired. Same list as GET /v1/chains.
amountstringRequired, 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.
assetstringThe symbol. Defaults to the chain's own coin.
referencestringYour order id. Echoed on every event, so you never keep a second mapping.
customerobjectOptional immutable customer snapshot: {"id":"customer-1842"}. The id is yours, not a Cryptanio user id.
productobjectOptional immutable product snapshot with id, name and integer quantity. Supply only the fields that apply.
payerobjectOptional payer snapshot: {"comment":"Thank you!"}.
metadataobjectUp 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_currencystringYour three-letter order currency, such as USD. Supply it together with invoice_amount; it also takes priority for payment valuation.
invoice_amountstringYour positive decimal order total in invoice_currency. This is separate from crypto amount and is never parsed through a JSON float.
projectstringWhich of your businesses this is for. Defaults to your first project; the pool never reaches across them.
addressstringPin 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.
tolerancestringHow far under the asked amount still counts as paid — network fees deducted at the sender. Empty means exact.
expires_innumberMinutes. The invoice holds an address the whole time, so this has a ceiling.
$ curl -X POST … /v1/invoices
{
  "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.

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

PlanWatched addressesAlerts / monthPrice
Free51,000$0
Start5010,000$9 / mo
Pro500100,000$29 / mo
Scale5,0001,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.