Invoices
Base URL: https://txnod.com/api/v1. Every request must carry the three HMAC headers:
X-Project-Id— the API Key ID (sent in theX-Project-Idheader).X-Timestamp— current Unix timestamp (seconds). Requests outside the ±300-second window are rejected withtimestamp_out_of_window.X-Signature— HMAC-SHA256 hex digest of${method}\n${pathname}\n${timestamp}\n${rawBody}using the project’s API secret. Binding method + path makes a captured(X-Timestamp, X-Signature)pair non-replayable across endpoints.
The SDK (@txnod/sdk) handles signing transparently — prefer it over hand-rolled cURL except for smoke tests.
These endpoints serve production and testnet projects only — a sandbox project is rejected with production_project_required (403). Sandbox projects use the mirrored /api/v1/sandbox/invoices surface (client.sandbox.createInvoice and friends in the SDK).
Create an invoice
POST https://txnod.com/api/v1/invoices — creates a new invoice. Idempotent on (project_id, external_id): a replayed request returns the existing invoice with HTTP 200 instead of 201.
Request
Request body (application/json)
| Field | Type | Required | Constraints |
|---|---|---|---|
external_id | string | yes | min length: 1; max length: 128 |
amount_usd | number | no | min: 0.01; max: 1000000000 |
amount_crypto | string | no | pattern: ^\d+(\.\d+)?$ |
coin | "btc" | "eth" | "usdt_erc20" | "usdc_erc20" | "trx" | "usdt_trc20" | "ada" | "pol" | "usdt_polygon" | "usdc_polygon" | "bnb" | "usdt_bep20" | "usdc_bep20" | "ton" | "usdt_ton" | yes | — |
callback_url | string (URI) | no | — |
metadata | object | no | — |
matching_mode | "exact" | "at_least" | "any" | no | — |
amount_usd must be between 0.01 and 1,000,000,000 (inclusive); values outside that range are rejected at the schema edge with a 400 validation_error.
matching_mode (optional) overrides the project’s default amount-matching policy for this invoice. exact requires the deposit amount to equal the invoice amount; at_least accepts any amount ≥ the invoice amount (overpayment OK); any accepts any amount (donations / pay-what-you-want). Omit the field to inherit the project’s configured default. When the operator’s address pool is saturated such that the invoice would share a deposit address with another invoice, only exact can disambiguate by amount — at_least and any are then rejected with 422 overlay_matching_mode_required_exact. The resolved mode is returned on every invoice response as matching_mode.
Response
200 — Idempotent replay — existing invoice returned unchanged
| Field | Type | Required | Constraints |
|---|---|---|---|
id | string (ULID) | yes | — |
project_id | string (ULID) | yes | — |
external_id | string | yes | — |
coin | "btc" | "eth" | "usdt_erc20" | "usdc_erc20" | "trx" | "usdt_trc20" | "ada" | "pol" | "usdt_polygon" | "usdc_polygon" | "bnb" | "usdt_bep20" | "usdc_bep20" | "ton" | "usdt_ton" | yes | — |
address | string | yes | — |
amount_crypto | string | yes | — |
amount_crypto_units | string | yes | — |
amount_usd | number | null | yes | — |
rate_snapshot | object | null | yes | — |
payment_token | string | null | yes | pattern: ^[0-9a-f]{8}([0-9a-f]{8})?$ |
payment_uri | string | yes | — |
callback_url | string | null | yes | — |
metadata | object | null | yes | — |
matching_mode | "exact" | "at_least" | "any" | yes | — |
confirmation_threshold | integer | yes | min: 0 |
status | "pending" | "detected" | "paid" | "overpaid" | "partial" | "expired" | "expired_paid_late" | "reverted" | "cancelled" | "ambiguous" | yes | — |
expires_at | integer | yes | min: 0 |
expires_at_iso | string (ISO 8601) | yes | — |
created_at | integer | yes | min: 0 |
created_at_iso | string (ISO 8601) | yes | — |
derivation_path | string | no | pattern: ^m(\/\d+'?)+$ |
verification_standard | "bip84" | "bip44" | "cip1852" | "bip44_ed25519" | no | — |
transactions | array<object> | no | — |
confirmations | integer | no | min: 0 |
201 — Invoice created
| Field | Type | Required | Constraints |
|---|---|---|---|
id | string (ULID) | yes | — |
project_id | string (ULID) | yes | — |
external_id | string | yes | — |
coin | "btc" | "eth" | "usdt_erc20" | "usdc_erc20" | "trx" | "usdt_trc20" | "ada" | "pol" | "usdt_polygon" | "usdc_polygon" | "bnb" | "usdt_bep20" | "usdc_bep20" | "ton" | "usdt_ton" | yes | — |
address | string | yes | — |
amount_crypto | string | yes | — |
amount_crypto_units | string | yes | — |
amount_usd | number | null | yes | — |
rate_snapshot | object | null | yes | — |
payment_token | string | null | yes | pattern: ^[0-9a-f]{8}([0-9a-f]{8})?$ |
payment_uri | string | yes | — |
callback_url | string | null | yes | — |
metadata | object | null | yes | — |
matching_mode | "exact" | "at_least" | "any" | yes | — |
confirmation_threshold | integer | yes | min: 0 |
status | "pending" | "detected" | "paid" | "overpaid" | "partial" | "expired" | "expired_paid_late" | "reverted" | "cancelled" | "ambiguous" | yes | — |
expires_at | integer | yes | min: 0 |
expires_at_iso | string (ISO 8601) | yes | — |
created_at | integer | yes | min: 0 |
created_at_iso | string (ISO 8601) | yes | — |
derivation_path | string | no | pattern: ^m(\/\d+'?)+$ |
verification_standard | "bip84" | "bip44" | "cip1852" | "bip44_ed25519" | no | — |
transactions | array<object> | no | — |
confirmations | integer | no | min: 0 |
Errors
| Status | Error code(s) | Trigger |
|---|---|---|
| 400 | validation_error, invalid_coin, invalid_xpub_format, invalid_webhook_url | Request body or query failed validation |
| 401 | auth_invalid, signature_invalid, timestamp_out_of_window | HMAC authentication failed |
| 402 | subscription_expired | Operator’s TxNod subscription is not active; writes are blocked. Operator must renew via dashboard /billing |
| 403 | key_revoked, key_suspended, project_suspended, permission_denied | Auth key or project disabled by operator action |
| 409 | external_id_conflict, xpub_not_verified | Idempotent replay or ownership challenge incomplete |
| 422 | coin_not_enabled, amount_out_of_range, wallet_not_bound, overlay_matching_mode_required_exact, tron_no_activated_addresses_available | Coin not enabled, amount out of range, no verified wallet bound for the requested chain on the project, a non-exact matching_mode requested while the address pool is saturated, or — TRON only — operator’s address pool has zero activated rows |
| 429 | rate_limit_exceeded | Per-project invoice-create rate limit exceeded |
| 500 | internal_error | Internal error (including cold-start rate unavailability) |
| 503 | pool_exhausted, webhook_capacity_exhausted | Address pool at hard cap (pool_exhausted, retry after Retry-After) OR webhook fleet for this chain has no spare slots (webhook_capacity_exhausted, Retry-After: 0 — partner retry alone won’t free capacity, treat as a transient outage) |
Examples
curl -X POST https://txnod.com/api/v1/invoices \
-H "X-Project-Id: $TXNOD_API_KEY_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: <hex>" \
-H "Content-Type: application/json" \
-d '{"external_id":"order-123","amount_usd":10.0,"coin":"btc"}'import { TxnodClient } from '@txnod/sdk';
const client = new TxnodClient({
projectId: process.env.TXNOD_API_KEY_ID!,
apiSecret: process.env.TXNOD_API_SECRET!,
});
const invoice = await client.createInvoice({
external_id: 'order-123',
amount_usd: 10.0,
coin: 'btc',
});Search invoices
GET https://txnod.com/api/v1/invoices — cursor-paginated search over the caller project’s invoices.
Request
Query parameters
| Param | Type | Required | Constraints |
|---|---|---|---|
external_id | string | no | — |
address | string | no | — |
tx_hash | string | no | — |
amount | string | no | — |
status | "pending" | "detected" | "paid" | "overpaid" | "partial" | "expired" | "expired_paid_late" | "reverted" | "cancelled" | "ambiguous" | no | — |
date_from | string (ISO 8601) | no | — |
date_to | string (ISO 8601) | no | — |
cursor | string (ULID) | no | — |
limit | integer | no | min: 1; max: 200; default: 50 |
Response
200 — Cursor-paginated invoice list
| Field | Type | Required | Constraints |
|---|---|---|---|
items | array<object> | yes | — |
next_cursor | string (ULID) | no | — |
Errors
| Status | Error code(s) | Trigger |
|---|---|---|
| 400 | validation_error | Query validation error |
| 401 | auth_invalid, signature_invalid, timestamp_out_of_window | HMAC authentication failed |
Examples
curl "https://txnod.com/api/v1/invoices?status=paid&limit=20" \
-H "X-Project-Id: $TXNOD_API_KEY_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: <hex>"import { TxnodClient } from '@txnod/sdk';
const client = new TxnodClient({
projectId: process.env.TXNOD_API_KEY_ID!,
apiSecret: process.env.TXNOD_API_SECRET!,
});
const page = await client.searchInvoices({ status: 'paid', limit: 20 });
for (const invoice of page.items) console.log(invoice.id);Retrieve an invoice
GET https://txnod.com/api/v1/invoices/{id} — fetch a single invoice by ULID. Returns 404 on cross-project lookups (no enumeration).
Request
Path parameters
| Param | Type | Required | Constraints |
|---|---|---|---|
id | string (ULID) | yes | — |
Response
200 — Invoice detail including transactions + confirmations
| Field | Type | Required | Constraints |
|---|---|---|---|
id | string (ULID) | yes | — |
project_id | string (ULID) | yes | — |
external_id | string | yes | — |
coin | "btc" | "eth" | "usdt_erc20" | "usdc_erc20" | "trx" | "usdt_trc20" | "ada" | "pol" | "usdt_polygon" | "usdc_polygon" | "bnb" | "usdt_bep20" | "usdc_bep20" | "ton" | "usdt_ton" | yes | — |
address | string | yes | — |
amount_crypto | string | yes | — |
amount_crypto_units | string | yes | — |
amount_usd | number | null | yes | — |
rate_snapshot | object | null | yes | — |
payment_token | string | null | yes | pattern: ^[0-9a-f]{8}([0-9a-f]{8})?$ |
payment_uri | string | yes | — |
callback_url | string | null | yes | — |
metadata | object | null | yes | — |
matching_mode | "exact" | "at_least" | "any" | yes | — |
confirmation_threshold | integer | yes | min: 0 |
status | "pending" | "detected" | "paid" | "overpaid" | "partial" | "expired" | "expired_paid_late" | "reverted" | "cancelled" | "ambiguous" | yes | — |
expires_at | integer | yes | min: 0 |
expires_at_iso | string (ISO 8601) | yes | — |
created_at | integer | yes | min: 0 |
created_at_iso | string (ISO 8601) | yes | — |
derivation_path | string | no | pattern: ^m(\/\d+'?)+$ |
verification_standard | "bip84" | "bip44" | "cip1852" | "bip44_ed25519" | no | — |
transactions | array<object> | yes | — |
confirmations | integer | yes | min: 0 |
Errors
| Status | Error code(s) | Trigger |
|---|---|---|
| 401 | auth_invalid, signature_invalid, timestamp_out_of_window | HMAC authentication failed |
| 404 | invoice_not_found | Invoice not found or belongs to a different project |
Examples
curl https://txnod.com/api/v1/invoices/01HK8MAR2QEXAMPLE000000000 \
-H "X-Project-Id: $TXNOD_API_KEY_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: <hex>"import { TxnodClient } from '@txnod/sdk';
const client = new TxnodClient({
projectId: process.env.TXNOD_API_KEY_ID!,
apiSecret: process.env.TXNOD_API_SECRET!,
});
const invoice = await client.getInvoice('01HK8MAR2QEXAMPLE000000000');
console.log(invoice.status);Cancel an invoice
POST https://txnod.com/api/v1/invoices/{id}/cancel — cancels an invoice in pending or detected state and releases its pool address into cooldown. Terminal-status invoices return 409 invoice_not_cancellable.
Request
Path parameters
| Param | Type | Required | Constraints |
|---|---|---|---|
id | string (ULID) | yes | — |
Response
200 — Invoice cancelled
| Field | Type | Required | Constraints |
|---|---|---|---|
id | string (ULID) | yes | — |
project_id | string (ULID) | yes | — |
external_id | string | yes | — |
coin | "btc" | "eth" | "usdt_erc20" | "usdc_erc20" | "trx" | "usdt_trc20" | "ada" | "pol" | "usdt_polygon" | "usdc_polygon" | "bnb" | "usdt_bep20" | "usdc_bep20" | "ton" | "usdt_ton" | yes | — |
address | string | yes | — |
amount_crypto | string | yes | — |
amount_crypto_units | string | yes | — |
amount_usd | number | null | yes | — |
rate_snapshot | object | null | yes | — |
payment_token | string | null | yes | pattern: ^[0-9a-f]{8}([0-9a-f]{8})?$ |
payment_uri | string | yes | — |
callback_url | string | null | yes | — |
metadata | object | null | yes | — |
matching_mode | "exact" | "at_least" | "any" | yes | — |
confirmation_threshold | integer | yes | min: 0 |
status | "pending" | "detected" | "paid" | "overpaid" | "partial" | "expired" | "expired_paid_late" | "reverted" | "cancelled" | "ambiguous" | yes | — |
expires_at | integer | yes | min: 0 |
expires_at_iso | string (ISO 8601) | yes | — |
created_at | integer | yes | min: 0 |
created_at_iso | string (ISO 8601) | yes | — |
derivation_path | string | no | pattern: ^m(\/\d+'?)+$ |
verification_standard | "bip84" | "bip44" | "cip1852" | "bip44_ed25519" | no | — |
transactions | array<object> | yes | — |
confirmations | integer | yes | min: 0 |
Errors
| Status | Error code(s) | Trigger |
|---|---|---|
| 401 | auth_invalid, signature_invalid, timestamp_out_of_window | HMAC authentication failed |
| 404 | invoice_not_found | Invoice not found or belongs to a different project |
| 409 | invoice_not_cancellable, invalid_state_transition | Invoice is not in a cancellable state |
Examples
curl -X POST https://txnod.com/api/v1/invoices/01HK8MAR2QEXAMPLE000000000/cancel \
-H "X-Project-Id: $TXNOD_API_KEY_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: <hex>"import { TxnodClient } from '@txnod/sdk';
const client = new TxnodClient({
projectId: process.env.TXNOD_API_KEY_ID!,
apiSecret: process.env.TXNOD_API_SECRET!,
});
const cancelled = await client.cancelInvoice('01HK8MAR2QEXAMPLE000000000');
console.log(cancelled.status);