Skip to main content
Webhooks push a signed JSON payload to a URL you control every time a subscribed record changes in DualEntry: a new invoice is created, a bill is updated, a customer is deleted. Instead of polling the API on a schedule, your downstream system reacts to events within seconds of them happening in DualEntry. This enables real-time syncs into a data warehouse, alerting workflows, or companion apps. This page walks you through the full lifecycle: choose the events you care about, register the endpoint that will receive them, verify each delivery is genuinely from DualEntry, and monitor what got through. If you are building the receiver yourself, the Public API v2 reference for webhooks documents every endpoint field-by-field; this page is the operational companion.

Prerequisites

Confirm the following before registering a webhook:
  • An HTTPS endpoint you can deploy code to. It must be publicly reachable from DualEntry’s egress and terminate TLS. Loopback and private IPs are rejected on registration for SSRF protection. If your infrastructure allowlists inbound traffic, permit DualEntry’s source IPs.
  • A DualEntry organization-scoped Public API key with the INTEGRATION permission (create, view, edit, archive as appropriate). Create the key under Organization Management → your organization → API Keys. See Authentication.
  • A place to store the signing secret returned by the register call. Treat it like a password. It is shown once and can never be retrieved again.
  • Agreement with engineering on which topics you need. DualEntry maintains an explicit allowlist of topics for business records (customers, invoices, bills, journal entries, and similar). Not every audited change is exposed.

Source IPs to allowlist

DualEntry sends webhook events from the production IPs listed in the table below. DualEntry sends events from either IP, so allow both at your firewall, WAF, or reverse proxy. Do not pin traffic to just one address. Use this list in addition to (not as a replacement for) HMAC verification. IP allowlisting keeps casual traffic off the receiver; the signature check is what proves a request actually came from DualEntry.

How DualEntry webhooks work

DualEntry webhooks deliver events via HTTPS POST with these guarantees: signed payloads, at-least-once delivery, unordered arrival, and status-aware retries. Read this once; every subsequent decision in this doc depends on it. Your receiver must be idempotent because the system guarantees at-least-once delivery and does not order events.

Register a webhook

All calls authenticate with an organization API key via the X-API-KEY header.
1

List the topics you can subscribe to

The subscribable topic catalog is an allowlist of business records with created, updated, and deleted lifecycle actions.
Topic names follow the pattern <app>.<Model>/<action>, for example core.invoice/updated. Anything not returned by this endpoint cannot be subscribed to and will be rejected with 422 on registration.
2

Register your endpoint

Specify a public HTTPS URL and select the topics you want to subscribe to. The response includes a signing secret exactly once, so copy it into your secrets manager immediately.
The secret is only returned by this response. It is never included on GET calls. If it is lost, the only recovery is to delete the webhook and create a new one with a new URL registration. There is no rotate endpoint yet.

Verify and inspect deliveries

After registration, confirm the wiring is real end-to-end and learn where to look when something goes wrong.
  • Verify a live delivery. Trigger any subscribed record change (create a customer, edit an invoice) and confirm your endpoint received a POST whose signature verifies against the stored secret. See Verify the HMAC signature below for the algorithm.
  • Inspect the delivery log. Every attempt (successful or failed) is recorded. Poll the delivery log by webhook UUID:
    Each entry shows event_id, topic, status, attempts, response_status, last_error, delivered_at, and created_at. This is the single source of truth when a downstream system claims it “didn’t get” an event.

Payload envelope

Every request body follows the same shape, so your receiver can dispatch on topic without special-casing per resource.
  • event_id is a deterministic UUID. DualEntry generates the same event_id for the same business event across retries, replays, and process restarts, so use it as your idempotency key.
  • object is a stable reference ({"id": ...}), not the full record. Fetch the object from the corresponding /public/v2/<resource>/{id}/ endpoint if you need current field values. This keeps the payload compact and avoids shipping stale field snapshots.
  • api_version pins the envelope schema for that delivery. Rows in flight during version updates continue to ship with the version they were created under, so key off api_version if you support more than one.

Verify the HMAC signature

This section covers what to check on every incoming request so you can trust the payload. Read the headers, run three verification steps, and only then dispatch on the event.

Authentication headers

Every delivery carries three headers you use to authenticate the request:

Verification steps

Verification is three things, in this order, and none of them is optional:
  1. Recompute the HMAC over the exact string "<timestamp>.<raw_body>" using your stored secret. Use the raw bytes of the request body. Do not parse JSON and re-serialize, or whitespace and key order will change the hash.
  2. Constant-time compare the recomputed digest against the Dualentry-Webhook-Signature-V1 header value. A regular string comparison reveals timing information about prefix matches, enabling timing attacks.
  3. Reject stale timestamps. If abs(now - timestamp) > 300 seconds, reject the request. This defeats attackers who capture an old signed request and replay it later.
Only after all three checks pass do you treat the payload as authentic and dispatch on it.

Reference receiver implementation

This section provides ready-to-adapt code for the three verification steps above. The Python snippet is a full production-shaped receiver; the Node.js snippet is a signature-verification helper you can wire into any Express handler.

Python / Flask

The snippet below is a minimal, production-shaped receiver. It verifies the signature, rejects replays, dedupes on event_id, and returns 200 fast. Load WEBHOOK_SECRET from your secrets store; never hardcode it.
The four checks map directly to the delivery semantics:
  • Signature verification blocks anything that isn’t signed with your secret.
  • The 5-minute replay window blocks captured-and-replayed old requests.
  • The event_id dedupe handles at-least-once delivery.
  • A fast 2xx keeps you out of the retry loop; do the actual work asynchronously.

Node.js / Express

The algorithm is the same everywhere: HMAC-SHA256 over "<timestamp>.<raw_body>", hex-encoded, constant-time compared.

Manage webhooks over the API

Once a webhook is live, you rarely re-register it. Instead, you list what’s active, patch the URL when your receiver moves, toggle is_active off during maintenance windows, and pull the delivery log when downstream reports a missing event. The full lifecycle runs on the same /public/v2/webhooks/ router. All routes are org-scoped by the API key and gated by the INTEGRATION permission. Full request and response schemas are in the API reference.

Retry, disable, and reactivation policy

WebhookDelivery owns the retry state machine. Your endpoint’s response determines DualEntry’s next action: If an endpoint fails continuously for 5 days it is automatically disabled. Reactivate it after fixing the receiver:
Reactivation clears the disable state. Deliveries fired after you reactivate flow normally. Events that failed while the webhook was disabled are not automatically re-sent; if you need to catch up, backfill from the corresponding resource endpoints (for example GET /public/v2/invoices/).

Operate safely in production

The rules below cover most of the incidents this system creates. Treat them as a pre-launch checklist for the receiver.

Troubleshoot common failures

Common webhook issues and their solutions.

FAQ

Common questions about webhook implementation and behavior.

Are webhooks the same as the Stripe / Brex / Plaid webhooks I see in DualEntry?

No. Those are inbound webhooks that DualEntry receives from third parties (payment processors, banking) to update your books. This page describes outbound webhooks that DualEntry sends to systems you own when your records change. They share the word “webhook” and nothing else.

Do webhooks include the full record body?

No. The payload has object.id and enough envelope to route on (topic, object_type). Fetch the full record from the matching /public/v2/<resource>/{id}/ endpoint when you need current field values. This keeps payloads small, avoids shipping stale snapshots, and forces receivers to work against the API’s canonical representation.

Can I subscribe to every change in the system?

No. DualEntry maintains an explicit allowlist of business records as the topic catalog. Internal bookkeeping, session events, and non-lifecycle actions are never exposed. If you need a record that isn’t in the catalog, ask engineering to add it; it is a small edit on their side.

Can I use webhooks and polling together?

Yes, and for critical flows you probably should. Webhooks are the fast path; a periodic reconciliation job that lists the same resource on a longer schedule catches anything the webhook missed (an endpoint outage, an accidentally deleted webhook). Idempotency on event_id and record ID keeps them from stepping on each other.

Next steps

Last modified on July 7, 2026