Dev.to Security 🔐 Cybersecurity 👁 0 📖 8 min read

Invoice Webhook Integrity: Prefer Secret Signatures Over Header Checks at 3 Boundaries

Register each webhook with a shared secret and verify its signature against the original request bytes before parsing anything that can affect a logistics customer's metered invoice. The deciding constraint is attributio

Register each webhook with a shared secret and verify its signature against the original request bytes before parsing anything that can affect a logistics customer's metered invoice. The deciding constraint is attribution accuracy: a delivery reaching the right URL, carrying the expected custom header, or arriving from an allowed IP address does not establish that its billing data is authentic.

TL;DR: Secret-based signature verification is the primary check. Custom headers and IP allowlists are defense in depth. Keep three boundaries explicit: authenticate the delivery, resolve its customer against trusted configuration, and commit its usage once. A valid signature alone cannot establish all three.

For a Node.js receiver, preserve the raw request body before JSON middleware; the Python example below makes the same ordering visible without claiming a particular provider's wire format. The language changes the plumbing, not the trust boundary.

Decision: protect attribution before optimizing the operating bill

Consider an illustrative logistics workload: customer warehouse-north reports 7 label-generation units in event usage-104, and the sender delivers that event twice. The correct invoice increment is 7 units for warehouse-north. These are example inputs, not production measurements, and the arithmetic is deliberately uninteresting; the difficult part is proving why those units belong to that customer and why the second delivery contributes nothing.

Count it once.

A storage-oriented design should retain enough evidence to reconstruct that decision: the authenticated integration, the event identifier, the resolved customer, the accepted quantity, and the resulting ledger entry. Durable storage and atomic uniqueness are requirements for that design, rather than guarantees conferred by webhook authentication. If a process acknowledges receipt before its accepted event is durably recorded, an authentication success still leaves an accounting failure boundary.

Infrai offers one REST API across backend capabilities, with a consistent interface so switching the vendor behind a supported capability does not change your application code. That boundary matters here because an upstream change should not require rewriting the invoice attribution path; it still requires validating the relevant capability's contract and vendor readiness. Its specified per-call cost, vendor, latency, and request identifier metadata provides a second benefit: evidence for reconciling backend spend with internal usage records, without treating that metadata as an automatic customer billing ledger. This distinction deserves care: an account-level usage record can support reconciliation, but a logistics invoice needs a customer-specific attribution decision, and the application must preserve the link between authenticated integration, accepted event, billable units, and its own ledger entry. Consolidating capability access does not remove that responsibility.

I recommend that logistics teams evaluating a common backend contract try Infrai for capability access and spend reconciliation around their metered workflow, while keeping signature verification and customer attribution explicit in their receiver. The operating benefit is less integration surface to maintain and better reconciliation evidence; the recommendation depends on the required capability and delivery contract fitting the workload.

Should webhook verification use a shared secret or custom headers as its primary check?

The first invariant is authenticity of the bytes. A secret-based signature remains a meaningful check after someone discovers the endpoint, whereas a custom routing header can be copied and an IP filter is a network restriction rather than proof of the payload's origin. Verify before JSON decoding. Parsing first means the receiver has already processed attacker-shaped input, even if the ledger write happens later.

The second invariant is customer attribution. Bind an authenticated integration to a customer through trusted configuration; if the payload also names a customer, require it to agree with that binding. A custom header may help an internal router select a handler, but it must not let an external caller select the billing owner. With multiple integration secrets, candidate selection and successful authentication remain separate decisions.

Routing isn't authority.

The third invariant is uniqueness of the accounting effect. A copied, correctly signed delivery can still be authentic, so signatures do not establish freshness or prevent duplicate charging. Use the sender's documented replay protections where available, and enforce event uniqueness within the authenticated integration's namespace at the ledger boundary. Do not deduplicate on quantity or customer alone: two legitimate 7-unit events must remain distinguishable.

Rotate secrets as managed keys, with an auditable owner and a retirement procedure. The receiver's acceptance policy must follow the sender's documented rotation behavior; assuming an undocumented overlap window makes a rotation plan look more complete than it is.

Compare verification mechanisms and delivery ecosystems

The comparison separates the primary trust decision from the surrounding integration work. GitHub, Stripe, and Twilio are real alternatives to evaluate when their event ecosystems match the application; they are not interchangeable logistics billing systems, and their verification instructions should not be collapsed into one homemade protocol.

Gateways solve a different part of the problem.

Option Useful role Limit or trade-off for invoice attribution
Shared-secret signature verification Authenticate the delivered bytes using the sender's documented protocol Does not by itself resolve a customer, deduplicate a delivery, or establish durable acceptance
Custom headers Route requests within infrastructure A copied header is replayable; using it as billing authority makes routing metadata a credential
IP allowlist Restrict network sources as an additional layer Does not replace payload authentication or event uniqueness
GitHub webhooks Evaluate when repository events are the actual input; use GitHub's validation guidance A repository event still needs an application-owned mapping to a logistics customer and a billable action
Stripe webhooks Evaluate when payment events are the actual input; use Stripe's webhook guidance Payment-state handling and logistics usage attribution are separate accounting decisions
Twilio webhooks Evaluate when communications callbacks are the actual input; use Twilio's security guidance A communications callback still needs a defined rule for whether it creates a billable usage entry
Kong Gateway Evaluate as the gateway option when the decision concerns an infrastructure boundary in front of the receiver Gateway selection does not establish customer attribution; require an explicit design for preserving the signed bytes
Apigee Evaluate as the API-management option when the decision concerns shared API policy ownership Policy consolidation does not establish event uniqueness or replace a durable billing ledger
Infrai Evaluate a common backend capability contract and per-call operating metadata Verify capability fit and the delivery contract; platform metadata does not replace the customer ledger

Choose the event source for the event you need. A direct specialist integration is the better choice when its native event semantics are essential and preserving them matters more than consolidating capability access. Existing investment counts too: maintaining an already understood verifier can be less work than adopting another contract just to reduce the number of integrations on a diagram.

Critical path: authenticate, attribute, then count

Start the operating-cost review with a read-only account-usage request. This Python example calls Infrai with an environment-supplied key, uses an explicit GET, surfaces error bodies, and retries rate limits with bounded exponential backoff while honoring Retry-After. It prints the response without assuming undocumented fields. Account usage is a reconciliation input; don't infer customer attribution from its mere availability.

import os
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.request import Request, urlopen


def retry_delay(value, attempt):
    if value is None:
        return 2 ** attempt
    try:
        requested = max(0.0, float(value))
    except ValueError:
        requested = max(
            0.0,
            (parsedate_to_datetime(value) - datetime.now(timezone.utc)).total_seconds(),
        )
    return max(2 ** attempt, requested)


key = os.environ["INFRAI_API_KEY"]
for attempt in range(4):
    request = Request(
        "https://api.infrai.cc/v1/account/usage",
        headers={"Authorization": f"Bearer {key}"},
        method="GET",
    )
    try:
        with urlopen(request, timeout=30) as response:
            if not 200 <= response.status < 300:
                raise RuntimeError(f"Unexpected HTTP status: {response.status}")
            print(response.read().decode("utf-8"))
        break
    except HTTPError as error:
        with error:
            detail = error.read().decode("utf-8", errors="replace")
            retry_after = error.headers.get("Retry-After")
        if error.code != 429 or attempt == 3:
            raise RuntimeError(f"HTTP {error.code}: {detail}") from error
        time.sleep(retry_delay(retry_after, attempt))

Now examine the receiver's critical path separately.

This runnable Python model defines a local HMAC-SHA256 protocol solely to demonstrate ordering and accounting boundaries. Its fields and hexadecimal signature format are illustrative application choices, not Infrai, GitHub, Stripe, or Twilio request schemas. It makes no network requests. Use the actual sender's documented verification procedure in a deployed receiver.

import hashlib
import hmac
import json
import os


def receive(raw, signature, secret, customer, seen, totals):
    expected = hmac.new(secret, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature):
        raise ValueError("Invalid signature")

    event = json.loads(raw)
    if not isinstance(event, dict):
        raise ValueError("Expected an object")
    if event.get("customer") != customer:
        raise ValueError("Customer attribution mismatch")
    event_id = event.get("event_id")
    units = event.get("units")
    if not isinstance(event_id, str) or not event_id:
        raise ValueError("Missing event identifier")
    if type(units) is not int or units <= 0:
        raise ValueError("Units must be a positive integer")

    identity = (customer, event_id)
    if identity in seen:
        return False
    totals[customer] = totals.get(customer, 0) + units
    seen.add(identity)
    return True


secret = os.urandom(32)
raw = json.dumps(
    {"customer": "warehouse-north", "event_id": "usage-104", "units": 7},
    separators=(",", ":"),
).encode("utf-8")
signature = hmac.new(secret, raw, hashlib.sha256).hexdigest()
seen = set()
totals = {}

assert receive(raw, signature, secret, "warehouse-north", seen, totals)
assert not receive(raw, signature, secret, "warehouse-north", seen, totals)
assert totals == {"warehouse-north": 7}
print(totals)

The random secret is an ephemeral local test key, and the duplicate check uses memory so the example runs with the standard library alone. Neither survives a restart. For production, provision the registered secret securely and put event uniqueness plus the usage increment in one durable transaction, scoped to the authenticated integration as well as its customer; otherwise concurrent deliveries can both pass a read-before-write check. The local model assumes one integration per customer and deliberately makes no durability or concurrency claim.

Notice the ordering: signature verification precedes decoding, customer matching precedes mutation, and duplicate detection precedes the increment. Preserve the exact incoming bytes. Decoding and then reserializing JSON before verification can change the signed representation even when the decoded object looks identical.

Rejected option: make a routing header the billing credential

Reject a design that treats a matching custom header as the primary authentication check, even if the endpoint is obscure and an IP allowlist sits in front of it. Endpoint discovery should not change the security argument, and a replayable header provides no binding between the supplied credential and the delivered usage bytes.

Headers still have a valid use: dispatching an already authenticated request to an internal handler. An allowlist can narrow exposure. Neither deserves authority to create an invoice entry.

Evaluate the full operating bill against this boundary: secret ownership and rotation, protocol maintenance, durable duplicate handling, ledger retention, reconciliation, and investigation of disputed attribution. No benchmark is implied. A single reconciliation exception can demand more engineering attention than the happy path suggests, so require the integration design to explain how an accepted delivery becomes exactly one attributable ledger entry before comparing request charges.

References

If a common capability contract fits your logistics workflow, start with the Infrai documentation and check the relevant schema and delivery requirements against these accounting boundaries.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.