Webhook Event Types
TxNod emits seven event types. Every event carries the same envelope — the event_type literal discriminates which invoice state transition occurred. The data payload follows the strict WebhookEventData shape (universal top-level fields + a chain-namespaced chain_specific object); new fields are added as additive optional properties so existing consumers stay binary-compatible across SDK versions.
Every event is signed with HMAC-SHA256 over ${timestamp}.${rawBody} and delivered with the single combined X-Txnod-Signature: t=<unix>,v1=<hex> header. See the verifyWebhookSignature reference for the canonical handler.
invoice.detected
Emitted when the watcher observes a matching on-chain transaction at any confirmation count below the finalization threshold. Fires at most once per invoice per confirmation delta; duplicate observations from reorganizations are deduplicated by event_id.
import type { WebhookEventData } from '@txnod/sdk';
type InvoiceDetectedEvent = {
event_id: string; // ULID — stable across retries, per-event unique
event_type: 'invoice.detected';
created_at: number; // Unix seconds
created_at_iso: string; // ISO 8601
project_id: string; // ULID
data: WebhookEventData;
attempt: number; // 1-based delivery attempt
// 'production' for mainnet projects, 'testnet' for testnet projects, 'sandbox' for sandbox-project events
mode: 'production' | 'testnet' | 'sandbox';
};Common data fields: invoice_id, tx_hash, confirmations, amount_crypto.
invoice.paid
Emitted when the invoice crosses the finalization threshold (coin-specific — e.g. 2 confirmations on BTC, 19 on TRON; see the supported chains matrix for the full per-chain table) and transitions detected → paid. Consumers should treat this as the authoritative “fulfill the order” signal.
import type { WebhookEventData } from '@txnod/sdk';
type InvoicePaidEvent = {
event_id: string; // ULID — stable across retries, per-event unique
event_type: 'invoice.paid';
created_at: number; // Unix seconds
created_at_iso: string; // ISO 8601
project_id: string; // ULID
data: WebhookEventData;
attempt: number; // 1-based delivery attempt
// 'production' for mainnet projects, 'testnet' for testnet projects, 'sandbox' for sandbox-project events
mode: 'production' | 'testnet' | 'sandbox';
};Common data fields: invoice_id, tx_hash, amount_crypto, amount_usd, confirmations.
invoice.overpaid
Emitted when the total received amount exceeds the invoice amount by more than the project-configured tolerance. The invoice status is overpaid (terminal); the excess remains in the derived address and can be handled manually via the dashboard or the Orphan Payments API.
import type { WebhookEventData } from '@txnod/sdk';
type InvoiceOverpaidEvent = {
event_id: string; // ULID — stable across retries, per-event unique
event_type: 'invoice.overpaid';
created_at: number; // Unix seconds
created_at_iso: string; // ISO 8601
project_id: string; // ULID
data: WebhookEventData;
attempt: number; // 1-based delivery attempt
// 'production' for mainnet projects, 'testnet' for testnet projects, 'sandbox' for sandbox-project events
mode: 'production' | 'testnet' | 'sandbox';
};Common data fields: invoice_id, amount_crypto_expected, amount_crypto_received, overpayment_crypto.
invoice.partial
Emitted when the received amount is below the invoice amount after the expiry window plus grace period. Non-terminal — the invoice can still transition to paid or overpaid if a follow-up payment lands before the configured partial-timeout.
import type { WebhookEventData } from '@txnod/sdk';
type InvoicePartialEvent = {
event_id: string; // ULID — stable across retries, per-event unique
event_type: 'invoice.partial';
created_at: number; // Unix seconds
created_at_iso: string; // ISO 8601
project_id: string; // ULID
data: WebhookEventData;
attempt: number; // 1-based delivery attempt
// 'production' for mainnet projects, 'testnet' for testnet projects, 'sandbox' for sandbox-project events
mode: 'production' | 'testnet' | 'sandbox';
};Common data fields: invoice_id, amount_crypto_expected, amount_crypto_received, shortfall_crypto.
invoice.expired
Emitted when the invoice expiry window elapses without any matching on-chain receipt. Terminal; the pool address is returned to cooldown.
import type { WebhookEventData } from '@txnod/sdk';
type InvoiceExpiredEvent = {
event_id: string; // ULID — stable across retries, per-event unique
event_type: 'invoice.expired';
created_at: number; // Unix seconds
created_at_iso: string; // ISO 8601
project_id: string; // ULID
data: WebhookEventData;
attempt: number; // 1-based delivery attempt
// 'production' for mainnet projects, 'testnet' for testnet projects, 'sandbox' for sandbox-project events
mode: 'production' | 'testnet' | 'sandbox';
};Common data fields: invoice_id, expired_at_iso.
invoice.expired_paid_late
Emitted when a matching payment arrives after an invoice has already emitted invoice.expired. Idempotent on (invoice_id, terminal_status): the original invoice.expired event is never retracted. Consumers should treat this as a delayed fulfillment signal and decide whether to honor the order.
import type { WebhookEventData } from '@txnod/sdk';
type InvoiceExpiredPaidLateEvent = {
event_id: string; // ULID — stable across retries, per-event unique
event_type: 'invoice.expired_paid_late';
created_at: number; // Unix seconds
created_at_iso: string; // ISO 8601
project_id: string; // ULID
data: WebhookEventData;
attempt: number; // 1-based delivery attempt
// 'production' for mainnet projects, 'testnet' for testnet projects, 'sandbox' for sandbox-project events
mode: 'production' | 'testnet' | 'sandbox';
};Common data fields: invoice_id, tx_hash, amount_crypto, received_at_iso.
invoice.reverted
Emitted when a previously-paid/overpaid invoice loses its on-chain backing due to a reorganization. The invoice status flips to reverted (a terminal flag in its own right) and — if the same transaction re-lands on the canonical chain at sufficient depth — a new invoice.paid event fires with a stable, deterministic event_id derived from (invoice_id, terminal_status). See Reorg semantics below for how to dedupe.
import type { WebhookEventData } from '@txnod/sdk';
type InvoiceRevertedEvent = {
event_id: string; // ULID — stable across retries, per-event unique
event_type: 'invoice.reverted';
created_at: number; // Unix seconds
created_at_iso: string; // ISO 8601
project_id: string; // ULID
data: WebhookEventData;
attempt: number; // 1-based delivery attempt
// 'production' for mainnet projects, 'testnet' for testnet projects, 'sandbox' for sandbox-project events
mode: 'production' | 'testnet' | 'sandbox';
};Common data fields: invoice_id, tx_hash, reverted_at_iso, reason.
Reorg semantics
TxNod’s finalization threshold is coin-specific — BTC waits 2 confirmations, TRON 19, and so on (see supported chains for the full table) — but reorganizations can still roll back a “paid” invoice on any chain. The invoice.reverted → re-emitted invoice.paid pair is the contract that lets consumers recover without losing idempotency.
- Invoice enters
paid; TxNod emitsinvoice.paidwithevent_id = A. - A reorg orphans the confirming block. The invoice flips to terminal status
reverted; TxNod emitsinvoice.revertedwith a freshevent_id = B. - The same transaction (or a functionally equivalent replacement) re-lands on the canonical chain at sufficient depth. The invoice enters
paidagain; TxNod emitsinvoice.paidwith a stable, deterministicevent_idderived from(invoice_id, terminal_status)— not a fresh ULID.
Point 3 is the load-bearing idempotency invariant. If your consumer already processed event_id = A as “fulfill order”, the re-emitted invoice.paid will arrive with the same event_id, so your UNIQUE(event_id) dedupe naturally skips it. Your system sees “this order was already fulfilled” and does nothing — which is exactly the right behavior, since you already reverted the fulfillment on invoice.reverted.
Do not rely on tx_hash for dedupe. The re-emitted invoice.paid may carry a different tx_hash if the reorg replaced the transaction with a functionally equivalent one (e.g. a replacement of an RBF-enabled Bitcoin transaction). event_id is the only stable key.
Do not treat invoice.paid as irreversible inside its own handler. The correct guarantee is: event_id is unique per terminal transition. If you reverse a fulfillment on invoice.reverted, the re-emitted invoice.paid will naturally replay through your normal handler — your event_id dedupe becomes a no-op only if you have already re-processed the same event_id.
Delivery guarantees
- At-least-once: events are retried up to 9 times with exponential backoff before landing in the DLQ (see Configuring partner webhooks for resend procedures).
- In-order-per-invoice is not guaranteed across different
event_types. Use the envelope’screated_at(Unix seconds) orcreated_at_isoif your consumer needs a stable ordering. attemptstarts at 1 and increments on each retry. A resend initiated viaresendWebhookEventcarries a freshevent_idandattempt = 1, withoriginal_event_idpopulated on the resend-response API (not on the delivered event).