Skip to Content
WebhooksWebhook Event Types

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.

  1. Invoice enters paid; TxNod emits invoice.paid with event_id = A.
  2. A reorg orphans the confirming block. The invoice flips to terminal status reverted; TxNod emits invoice.reverted with a fresh event_id = B.
  3. The same transaction (or a functionally equivalent replacement) re-lands on the canonical chain at sufficient depth. The invoice enters paid again; TxNod emits invoice.paid with a stable, deterministic event_id derived 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’s created_at (Unix seconds) or created_at_iso if your consumer needs a stable ordering.
  • attempt starts at 1 and increments on each retry. A resend initiated via resendWebhookEvent carries a fresh event_id and attempt = 1, with original_event_id populated on the resend-response API (not on the delivered event).