Webhook icon
Schedule icon
Request icon
Query icon
If icon
Exit icon
Loop icon
Sequential icon
SlackIncomingWebhook icon
Switch icon
Log icon

Track Bitcoin Invoice Payments to Final Confirmation with walletnotify

Track Bitcoin invoice payments from mempool to final confirmation, flag under- and overpayments, catch reorgs, and notify your app and Slack.

Categories
BusinessData

Accepting Bitcoin means answering the same questions for every invoice: has the customer paid, did they pay the right amount, is it confirmed enough to ship, and did a reorg just undo a payment you already counted? This blueprint answers them automatically. Bitcoin Core's walletnotify hook calls a Kestra webhook whenever the wallet sees a transaction, and the flow re-checks every open invoice against the node, updates its status in Postgres, and tells your application and Slack about every change.

The node recomputes received amounts from the current chain on every check, so the flow never trusts stale data: a reorg shows up as a lower confirmed amount and is reported as such.

How it works

  1. wallet_notify (Webhook) starts a run whenever Bitcoin Core sees a wallet transaction. confirmation_sweep (Schedule) catches later confirmations and reorgs, which walletnotify does not report.
  2. node_check confirms the node is reachable, the credentials work, and the wallet is loaded, before any invoice is touched.
  3. ensure_table creates the invoices table on first run. Your application inserts one row per invoice with a fresh address from getnewaddress, the amount due, and an expiry.
  4. load_open_invoices reads invoices that can still change, paid invoices inside the reorg window, and any invoice with a change not yet delivered.
  5. check_invoices (Loop) asks the node what each address has received, with and without required_confirmations, using getreceivedbyaddress.
  6. update_status classifies the invoice and claims the notification in a single SQL statement, returning the last delivered status and the new one:
    • overpaid: confirmed amount above the amount due plus the tolerance
    • paid: confirmed amount covers the amount due
    • seen: full amount in the mempool or below the confirmation threshold
    • underpaid: something received, but less than the amount due
    • expired: nothing received before expires_at
  7. When the status changed, deliver (Sequential) calls your application first (post_callback, with an Idempotency-Key header), then alerts Slack on reorgs (reorg_check) and posts paid, overpaid (with the refund amount), and underpaid (with the shortfall) messages (notify_by_status, a Switch).
  8. summary counts invoices by status for the logs and the invoices_by_status output.

Error handling

Payment notifications must arrive exactly when something changed, never twice, and never get lost. The flow is built around that:

  • No duplicates under bursts. One transaction can fire walletnotify several times, and runs can overlap. update_status records the delivered status in the same UPDATE that classifies the invoice. Postgres row locking then guarantees that only one run sees a given change; tested with ten simultaneous webhook calls.
  • No lost notifications. HTTP and Slack calls retry with exponential backoff. If delivery still fails, the local errors handler of deliver (release_claim) puts the previous status back. The next walletnotify call or sweep sends the change again, and alert_delivery_failed tells Slack which invoice is pending and which endpoint to check. Your application should use the Idempotency-Key header to ignore a change it already processed.
  • One bad endpoint does not stop the rest. deliver has allowFailure: true, so the other invoices in the run are still checked and notified.
  • Fail fast on infrastructure problems. node_check stops the run before touching invoices when the node is down or misconfigured. The flow-level errors alert names the failing task and the cause, for example failed at `node_check`: Connect to http://127.0.0.1:18443 failed: Connection refused.
  • The chain is the source of truth. Amounts are recomputed by the node on every check, so a reorg lowers the confirmed amount and is reported as REORG: invoice #2 was paid and is now seen.

What this blueprint teaches about Kestra

  • Pairing an event trigger (Webhook) with a Schedule sweep: events give speed, the sweep catches what the event source never reports.
  • Local error handling: errors on a flowable task (Sequential) runs compensation for just that block, while allowFailure keeps the run going.
  • Loop iterations run as sub-executions of the same flow. In testing, the flow-level concurrency limit did not keep overlapping runs apart once iterations were involved. The flow keeps the limit to smooth bursts, but relies on the database claim, not on Kestra, for correctness.
  • errorLogs() in an errors block to send actionable alerts.

Prerequisites

  • A Bitcoin Core node (v24 or newer) with RPC enabled and a wallet that generates the invoice addresses. Start on signet or testnet4.
  • walletnotify configured to call the webhook, for example in bitcoin.conf: walletnotify=curl -s -X POST https://kestra.example.com/api/v1/main/executions/webhook/company.team/bitcoin-deposit-confirmation-tracker/btc-deposits-change-me?txid=%s Change the webhook key before exposing it.
  • A Postgres database for the invoices.
  • A Slack incoming webhook, and optionally an HTTP endpoint in your application for status callbacks.

Secrets

  • BITCOIN_RPC_USER, BITCOIN_RPC_PASSWORD: Bitcoin Core RPC credentials.
  • POSTGRES_URL, POSTGRES_USER, POSTGRES_PASSWORD: JDBC URL (for example jdbc:postgresql://db:5432/shop) and credentials.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL.

Inputs

  • rpc_url, wallet (STRING): node endpoint and invoice wallet.
  • required_confirmations (INT, default 3): confirmations before an invoice counts as paid.
  • overpay_tolerance_btc (FLOAT, default 0.00001): how far above the amount due a payment can be before it is flagged as overpaid.
  • reorg_window_hours (INT, default 24): how long paid invoices stay under watch.
  • max_invoices (INT, default 200): invoices checked per run.
  • callback_url (STRING, optional): your application's endpoint for status changes.
  • explorer_address_url (STRING): explorer prefix for links.

Outputs

  • invoices_by_status (JSON): invoice count and confirmed BTC per status after the run.

Callback payload

{"invoice_id": 2, "address": "tb1q...", "old_status": "seen", "status": "paid", "amount_due_btc": 0.02, "received_btc": 0.02, "confirmed_btc": 0.02}

Quick start

  1. Add the secrets (in the open-source edition, as base64-encoded SECRET_* environment variables) and change the webhook key.
  2. Run the flow once to create the invoices table.
  3. Create an invoice: INSERT INTO invoices (address, amount_due_btc) VALUES ('<getnewaddress output>', 0.001);
  4. Pay it from a signet wallet and watch the invoice move from seen to paid as it confirms.
  5. Add walletnotify to bitcoin.conf and enable confirmation_sweep.

How to extend

  • Pair it with the bitcoin-fee-aware-payout-batching blueprint to refund overpayments automatically: insert a payout row for the refund amount when an invoice becomes overpaid.
  • Give large invoices more confirmations by storing a per-invoice threshold and comparing against it in update_status.
  • Expire seen invoices that never confirm by adding a rule for transactions dropped from the mempool.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.