Back to Blog
Technology10 min read

Building Reliable Integrations With Invoice Webhooks

IN
Invoice Generator TeamAuthor
August 11, 2026Published
Also available in:NederlandsDeutsch

Webhooks look simple from the outside: something happens, you get a POST request, you do something in response. The complexity shows up once you start asking what happens when that POST request doesn't arrive, arrives twice, or arrives while your server is mid-deploy. Invoice Generator's webhook system is deliberately straightforward on the sending side, which means the burden of handling those failure modes sits with whoever is building the receiving end. This post covers what that actually means in practice, and how to build a receiver that stays correct even when individual deliveries don't.

How Webhook Delivery Actually Works Here

A workspace admin sets up a webhook subscription from the developer area of a workspace: a target URL, and a list of event types to subscribe to, or * to subscribe to everything. The confirmed event types are invoice.paid, estimate.approved, and estimate.rejected — in other words, the moments in an invoice or estimate's lifecycle where a client has taken an action worth reacting to. When one of those events fires, Invoice Generator dispatches a POST request to your target URL asynchronously, with an 8-second timeout on the attempt. Whatever happens — success, an error status, a timeout — gets written to a delivery log for that subscription, viewable through GET /webhooks/:id/logs.

That's the entire mechanism. There's no retry queue behind it. If your endpoint is down, mid-deploy, returns a 500, or simply doesn't respond within 8 seconds, the delivery is logged as failed and nothing tries again automatically. This is worth sitting with for a second, because it's easy to build a webhook receiver assuming the platform will paper over your downtime, and Invoice Generator won't. The correctness of your integration depends entirely on how you've built the receiving side.

None of this is a flaw to work around quietly — it's a design constraint to build against directly, the same way you'd design around any other single point of failure in a distributed system. The rest of this post is about doing that.

Rule One: Verify the Signature Before You Trust Anything

Every webhook request Invoice Generator sends is signed. The signature is an HMAC-SHA256 computed over the raw JSON payload using your webhook subscription's own secret, and it arrives in a header called X-Invoice-Webhook-Signature. Your receiver's first job, before touching the payload for anything else, is to recompute that signature locally and compare it to the header value.

The mechanics are the same in most languages: read the raw request body as bytes before any JSON parsing happens, compute HMAC-SHA256(secret, raw_body), hex-encode it, and compare against the signature header using a constant-time comparison function rather than a plain string equality check. A quick example in Node:

const crypto = require('crypto');

function isValidSignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const expectedBuf = Buffer.from(expected, 'utf8');
  const gotBuf = Buffer.from(signatureHeader || '', 'utf8');

  if (expectedBuf.length !== gotBuf.length) return false;
  return crypto.timingSafeEqual(expectedBuf, gotBuf);
}

A couple of details matter here. First, the signature is computed over the raw body — if your web framework parses JSON before you get access to the original bytes, and then you re-serialize it to check the signature, you can get a mismatch purely from key ordering or whitespace differences, even though the payload is legitimate. Make sure you're capturing the raw bytes on the way in, before any middleware touches them. Second, treat an invalid signature as a hard rejection, not a warning you log and continue past. A webhook endpoint is a URL you've made public specifically so an external service can POST to it — which also means it's a URL anyone else on the internet can POST to. Signature verification is what separates "a request that says an invoice was paid" from "a request that Invoice Generator actually sent."

Rule Two: Assume Every Event Might Arrive Twice

Because there's no retry queue, you might assume duplicate deliveries can't happen — no retries, no duplicates, right? In practice, duplicates can still occur, just from different sources: someone on your team using the manual "send test event" feature against a webhook subscription that's also wired into production, a race where your endpoint processed a request successfully but the connection dropped before the response confirmed it, or simply a mistake in how a subscription's events are configured. The practical answer to all of these is the same regardless of cause — your handler needs to be idempotent, meaning it produces the same end state whether it processes a given event once or five times.

Idempotency isn't something the API hands you here — there's no documented idempotency-key mechanism on the outbound side, so you're not going to get a token from Invoice Generator that lets you deduplicate for free. You have to build it yourself, and the good news is that the payload gives you what you need to do it. Each event carries a natural identifier: the invoice or estimate ID plus the event type is a stable, meaningful key. Before acting on an event, check whether you've already recorded that (id, event_type) pair.

A straightforward pattern:

  1. On receipt, verify the signature.
  2. Extract the invoice or estimate ID and event type from the payload.
  3. Look up whether you've already processed that exact (id, event_type) combination — a unique constraint on a small processed_events table works fine for this.
  4. If it's new, process it and record the key inside the same transaction as the side effect (updating your own database, sending a notification, whatever the event triggers).
  5. If it's already recorded, return a success response immediately without repeating the side effect.

The transaction detail in step 4 matters more than it looks. If you record the event as "processed" before actually completing the side effect, a crash between those two steps leaves you with a permanently skipped event. If you complete the side effect first and record the key afterward, a crash in between leaves you vulnerable to reprocessing on the next duplicate. Doing both atomically, in the same transaction, is what actually closes the gap.

It's also worth deciding upfront what "already processed" should mean for each event type. For invoice.paid, idempotency probably means "don't mark the invoice paid twice or double-send a receipt confirmation." For estimate.approved or estimate.rejected, it might mean "don't kick off a downstream workflow, like converting an estimate to a project, more than once." The mechanism is the same; the specific side effect you're guarding is different for each event, so it's worth being explicit about what duplicate processing would actually break before you write the guard.

Rule Three: Treat the Delivery Log as a Backstop, Not an Afterthought

Given that failed deliveries just stop — no retry, no backoff, no second attempt — you need a way to catch what fell through. That's what GET /webhooks/:id/logs is for. It's not just a debugging tool for when something looks wrong; it's a legitimate part of a reliability strategy, and it's worth using it that way from the start rather than only reaching for it after you've noticed a gap.

The practical use case is reconciliation. If your receiver was down for twenty minutes during a deploy, any webhook events that fired during that window are gone as far as automatic delivery is concerned — they were attempted once, they failed, and nothing is coming to fill the gap. Periodically pulling the delivery log and cross-referencing it against what your own system actually processed tells you exactly which events you missed, by ID, so you can go fetch the current state of those specific invoices or estimates through the regular API rather than guessing.

A workable reconciliation approach, roughly in order of effort:

  • Poll the delivery log on a schedule. Even a simple job that checks the log every 15 or 30 minutes, compares delivered event IDs against your own processed-events table, and flags gaps will catch most outage windows well before they become a business problem.
  • Poll the source data as a wider net. For invoices or estimates in a state you especially care about — paid, approved, rejected — a periodic sweep against the invoice or estimate API itself catches anything a webhook approach might miss entirely, including edge cases in log retention or subscription configuration.
  • Alert on your own downtime, not just on missing events. If you know precisely when your receiver was unavailable, you already know which time window needs a reconciliation pass, without waiting to notice a discrepancy days later.

None of this needs to be elaborate. The point is that "fire once, no retry" pushes the reliability problem onto you, and a scheduled reconciliation job is a cheap, mechanical way to solve it rather than trusting that your uptime happens to match Invoice Generator's dispatch attempts one-to-one.

Before wiring a webhook subscription into anything that matters, use the manual "send test event" option on the subscription to confirm your endpoint is actually reachable, returns a fast 2xx response, and handles the payload shape correctly. It's a small step, but it catches basic misconfiguration — wrong URL, firewall blocking inbound traffic, a bug in your signature check — before it costs you a real event during a real invoice lifecycle.

Why the SSRF Protection on Target URLs Matters to You

Webhook target URLs are validated to make sure they resolve to a public, non-internal address — not a loopback address, not an internal network range, not link-local infrastructure. This check runs both when you first register the subscription and again on every single dispatch. It's easy to read that as a protection for Invoice Generator's own infrastructure, which it is, but it's worth understanding why it also matters to you directly.

You're the one supplying the target URL, and that URL usually points at infrastructure you control — a server sitting inside your own network, maybe with other internal services reachable from the same host. Server-side request forgery protections exist because a URL field that accepts arbitrary addresses is a classic vector for tricking a server into making requests it shouldn't: reaching internal admin panels, cloud metadata endpoints, or other services never meant to be internet-facing. Revalidating on every dispatch, not just at setup, matters because DNS records can change after a subscription is created — a hostname that resolved to a public address on day one could later be repointed at an internal one, and checking only once wouldn't catch that.

The practical implication for your side is to keep your webhook receiver on a URL that's genuinely meant to be public, ideally with its own narrow scope rather than being an endpoint on a broader internal service. Treat it the way you'd treat any other public-facing API endpoint: rate-limit it if you're worried about abuse, log inbound requests, and don't assume that "only Invoice Generator knows this URL" is a security boundary — signature verification is what actually establishes trust, not obscurity of the endpoint address.

Putting the API and Webhooks Together

Webhooks and the Developer API work best as a pair rather than as substitutes for each other. Webhooks tell you when something changed; the API is how you go find out the full current state of that thing, and it's also your reconciliation fallback when a webhook simply never arrived. Authentication for API calls uses an X-Api-Key header or an Authorization: Bearer ak_... header, with keys scoped to a specific workspace and creatable or revocable by a workspace admin — which matters if a webhook handler needs to look up additional invoice or estimate details beyond what the event payload includes, or if you're building the reconciliation sweep described above. If you haven't set up a subscription yet, the Developer API & Webhooks documentation walks through generating a key and registering a target URL from the workspace settings.

This pairing is exactly the kind of thing that starts to matter once billing tooling moves from "a person checks a dashboard" to "a system reacts automatically," which is a broader shift covered in more depth in Running Team Billing: Workspaces, Reports, and the Developer API in Invoice Generator — webhooks are one input into that kind of internal tooling, not the whole of it. And if you're thinking about this at a larger scale, where billing events feed into provisioning logic, usage tracking, or customer lifecycle automation across a real SaaS product, The Architecture of Scale: Building a Robust SaaS Billing Infrastructure goes into the broader infrastructure patterns that this kind of event-driven integration eventually has to fit into.

None of the individual pieces here are exotic. Signature verification is a standard HMAC check. Idempotency is a unique constraint and a lookup before you act. Reconciliation is a scheduled job comparing two lists. What matters is treating all three as required parts of the integration rather than optional hardening you'll add later, because "later" is usually the exact moment a missed webhook during a deploy window turns into a client wondering why their payment confirmation never showed up.

Related Articles

Technology8 min read

How the Comment Notification Digest Batches Client Activity Into One Email

Why a burst of client comments produces exactly one email, not five — and how the rolling delay resets on every new comment.

IN
Invoice Generator TeamSeptember 11, 2026
Technology8 min read

How In-App Notifications Fan Out to Your Team

Why every workspace member gets their own independent notification row, and why you don't get notified about your own actions.

IN
Invoice Generator TeamSeptember 8, 2026
Technology9 min read

The Invoice Audit Trail: Every Event Logged Behind the Scenes

What actually gets recorded when an invoice is viewed, commented on, or changes status — and why the logging never blocks the action itself.

IN
Invoice Generator TeamSeptember 4, 2026
Technology6 min read

How Two-Factor Authentication Protects Your Account

2FA generates a six-digit code that changes every 30 seconds using the TOTP standard — no live connection between your phone and the server is ever required.

IN
Invoice Generator TeamAugust 27, 2026
Technology6 min read

How API Keys Are Stored (And What to Do If You Lose One)

The raw value of your API key is never stored anywhere after the moment you create it — only a one-way hash is kept, which is why a lost key can't be recovered.

IN
Invoice Generator TeamAugust 26, 2026
Technology11 min read

Is It Safe to Put a QR Code on Your Invoice?

If you've thought about adding a QR code to your invoices, you've probably also seen a headline or two about "quishing" — QR code phishing — and wondered whether you'd be handing your clients a security risk along with your bill. That's...

IN
Invoice Generator TeamAugust 13, 2026

Mastered Invoicing?

Put your knowledge into practice and create your first professional invoice today.

Create Your Invoice Now
Building Reliable Integrations With Invoice Webhooks | Invoice Generator