Start with the raw request, not the claimed payment

The first trust boundary is the HTTP request as received by the merchant. A public endpoint can receive arbitrary JSON, copied payloads, malformed headers and requests that happen to resemble real payment events. Give that endpoint its own route, enforce a sensible byte-size limit, accept only the documented method and content type, and avoid printing the body or signature in general application logs. None of those checks authenticates a sender. They simply keep the later cryptographic check operating on a defined input rather than on a deserialized object of unknown origin.

The crucial implementation detail is to retain the raw body bytes before parsing. If the provider signs the exact body, then serializing the parsed JSON again may reorder fields, normalize escapes or change whitespace. Even an apparently harmless body transformation can make a genuine signature fail. Conversely, signing an application-created version rather than the documented input can leave the verifier checking the wrong message. Consult the provider's API integration page and its actual callback specification for the canonical signed string; a product page is context, not a substitute for a signature specification.

Model the arrival record with separate fields: local receipt time, raw-body hash for investigation, verified provider event ID if supplied, merchant invoice reference, claimed event time, verification outcome, and processing outcome. Never treat a client-supplied payment_id as a database authorization merely because it resembles a legitimate ID. A webhook may refer to an invoice, but the invoice and merchant account still need to be looked up in trusted records after verification. This separation matters when the same invoice progresses through several states: a plausible notification can be authentic yet irrelevant to the current fulfilment decision.

Make the signature check byte-exact and explicit

For an HMAC-based scheme, obtain the signing secret from a protected server-side secret store, not from the request or a browser client. Extract exactly the provider-defined signature and signed timestamp fields. Reject missing fields, multiple conflicting values, unsupported algorithms and malformed encodings before doing business work. If the contract defines a signed string as a timestamp followed by a separator and raw body, follow it byte for byte; if it signs only the body or includes a request path, follow that instead. The generic shape is expected = HMAC(secret, documented_signed_bytes). The example is a shape, not a claim about any provider's header format.

Decode the supplied signature using the documented encoding and compare fixed-length byte arrays with a constant-time comparison routine. Do not use ordinary string equality, silently normalize hexadecimal case unless the specification allows it, or accept a signature that matches after dropping unrecognized characters. Reject algorithm identifiers supplied by the sender unless your local configuration explicitly allows that algorithm. Logging should store a bounded verification reason such as missing_signature, bad_encoding or mismatch, not the signing secret or an entire sensitive payment payload.

A valid MAC only proves that someone with the configured secret constructed the signed message. A leaked secret lets an attacker construct valid messages until it is revoked. Scope secrets by environment and endpoint where supported; staging and production should not share one secret. During a planned rotation, verify against a small, explicitly configured set of active and retiring secrets, attribute the successful key version internally, and remove the old key after a bounded overlap period. Do not fetch candidate keys using an unverified merchant identifier from the JSON body. If multiple tenants share an endpoint, resolve the candidate account by a provider-defined route or key identifier whose meaning is constrained locally, then check the verified event's account binding before processing. For the broader secret lifecycle, see the API credential rotation guide; webhook signing keys require their own documented handling.

Put time into the signed boundary

A timestamp is useful only if it is covered by the signature. If an attacker can edit the timestamp without breaking the MAC, they can make an old signed body appear fresh. After checking the MAC over the provider-defined signed material, parse the timestamp using the exact documented unit and format. Reject invalid values, far-future values and requests outside a locally chosen freshness window. Use a trusted server clock and monitor clock drift; seconds-versus-milliseconds confusion can make every legitimate notification fail or widen an intended short window dramatically.

Choose the window from the provider's retry and delivery behavior, your clock tolerance and your threat model, rather than copying an arbitrary universal number. Record both the signed event time and your receipt time: one is a claim authenticated by the signed material, the other is your own observation. A delayed legitimate callback can fail the freshness test even while the provider still regards the event as deliverable. Make that a visible exception with a documented recovery route—usually a read-only lookup against the provider's authenticated API or a controlled backfill—not a silent policy relaxation across the whole endpoint.

If the provider's signing contract does not authenticate the timestamp, changing a mutable header cannot establish freshness. Do not treat that header as replay evidence or silently widen the acceptance rule. Block automatic fulfilment until the integration has a documented authenticated event identifier and a durable replay boundary, or reconcile the payment through a separately authenticated provider lookup and a controlled human-approved recovery process. The integrator must verify the actual signed fields in the provider's specification; this is a design requirement, not a claim that every provider signs timestamps.

Time checking alone is not replay protection. An attacker who captures a valid request can resend it inside the allowed window, while a legitimate provider may retry the same request after a timeout. Those two arrivals look similar at the HTTP layer; neither should trigger a second entitlement. Read the pre-release payment-flow testing guide alongside the callback test plan, but test the time boundary as a separate control: valid-now, too old, too far in the future, malformed time and server-clock drift. Do not confuse the timestamp printed inside the event JSON with a signed header unless the specification explicitly covers it.

Treat delivery and fulfilment as different ledgers

Replay defence is not the same as dropping duplicate HTTP requests. Maintain a durable event receipt ledger keyed by the provider's authenticated event ID when available, with a merchant/provider namespace. If the provider does not guarantee a stable event ID, define a conservative deduplication key from documented immutable signed fields and record its limitations. Put a uniqueness constraint on the key. Record verification separately from processing so a process crash between acceptance and fulfilment is recoverable; an in-memory seen set disappears on restart and cannot protect workers in different regions.

The fulfilment ledger needs its own unique business key, such as merchant plus invoice plus entitlement type. Two different legitimate events may describe the same paid invoice; deduplicating only by event ID will not prevent granting access twice. Conversely, discarding an event because an earlier notification was seen can hide a subsequent state transition. A useful design stores each verified event, then decides whether its state transition is permissible against the merchant's current invoice record. For a merchant selling software access, insert or update the paid record and enqueue one grant under a transactionally enforced uniqueness rule. An acknowledgement should mean the event is durably queued or processed, not that the handler has promised success after a fragile background step.

Fail closed if the signing key configuration is absent, the trusted clock cannot be checked, the durable receipt/replay store is unavailable or the entitlement uniqueness constraint cannot be enforced. In those states do not mark the invoice paid, release goods or return a success acknowledgement for an event that has not been durably accepted. Return a retriable failure under the provider's documented retry contract; when retries cannot safely recover the case, quarantine the event without fulfilment and reconcile it later through a separately authenticated provider channel with a recorded operator decision. An in-memory queue or a successful HTTP response alone is not durable evidence.

Hypothetical case — digital subscription: an accepted invoice generates a provider notification; the merchant writes it to the receipt ledger, but the access-grant worker times out. The provider retries. The second delivery must not issue a second licence, yet the first delivery must remain eligible for a worker retry. A unique event receipt and a separately unique entitlement grant achieve both goals. The duplicate-fulfilment analysis discusses why the paid-state transition deserves its own guard. This is a payment-control issue, not merely a way to suppress noisy HTTP traffic.

Use the authenticated event as a prompt to decide, not as the decision

Once a callback passes authenticity, freshness and deduplication gates, validate its business binding: known merchant account, expected invoice, asset and network where relevant, accepted amount and state transition. A signature does not certify that your internal invoice has the same value or that an earlier payment event did not change the record. For a high-impact release, query the provider's authenticated API for the current payment state when the callback contract recommends or permits it. Do not use a browser redirect, a copied transaction hash, or a customer screenshot as the sole release signal. Network confirmation policies and merchant acceptance rules remain distinct from callback authenticity; see the confirmation-policy discussion.

Hypothetical case — marketplace milestone: a buyer pays the initial deposit on an invoice that will later have a balance due. A signed payment_received event arrives twice and a subsequent payment_confirmed event arrives once. The merchant should map each provider state to the correct milestone, not release the full seller service on the first deposit or issue two releases because the confirmed event has a new event ID. The invoice record, fulfilment ledger and provider record must agree on the business object. This is why a marketplace payment flow needs explicit allocation rules even when transport security is sound.

What teams often underestimate is the cost of ambiguous acceptance. A handler that responds successfully before persisting the event may lose a payment silently; a handler that performs delivery synchronously before acknowledging may produce retry storms and staff investigations when the downstream worker is slow. Measure verified-but-unprocessed events, receipts with no matched invoice, and paid invoices with no completed entitlement. Put these in a review queue with owners and evidence rather than treating every unexpected field as a reason to mark an invoice paid. When a transfer is visible elsewhere but absent from the merchant record, the missing-payment checklist helps locate the broken handoff without granting access from weak evidence.

Prove the boundary with hostile and boring tests

A compact test matrix is more valuable than a happy-path demo. Capture a known-good test event using approved sandbox tooling, then vary one input at a time. Alter a raw body byte, alter only the timestamp, change the signature encoding, omit a header, duplicate a valid delivery, deliver an older but otherwise valid event, rotate the secret, and present an event for the wrong merchant. Assert both the HTTP response and the downstream record: rejected events must create no paid entitlement; repeated valid events may create delivery records but not a duplicate grant. Keep synthetic keys and payloads out of production logs.

Probe Expected boundary behavior Evidence to retain
Valid signed body, new event Accept and durably record before acknowledging Key version, event ID and queue outcome
One changed body byte Reject before parsing business meaning Bounded mismatch reason
Valid but stale signed request Reject or quarantine per written policy Signed and local receipt times
Missing key, untrusted clock or unavailable replay/entitlement store Fail closed; no paid state or success response before durable acceptance; retriable failure or authenticated recovery quarantine Failed dependency and incident owner
Mutable unsigned timestamp with otherwise valid body signature Never accept the mutable time as freshness proof; block automatic release pending authenticated replay design Provider signing contract and exception decision
Same event delivered again Acknowledge safely without a second grant Receipt and entitlement keys
New event for already-paid invoice Evaluate transition; never equate a new event ID with new revenue Invoice state and decision reason

A verifier also has limits: a compromised signing secret defeats its authenticity check; a valid event may describe a reversed or superseded payment state; a delayed legitimate event may fall outside the clock window; and a provider outage can block confirmatory lookups. For each, specify an owner and a controlled recovery step. During an incident, retain enough evidence to replay your processing decision without replaying an untrusted public request as though it were fresh. The payment incident-response guide is relevant to escalation, while the site FAQ can orient nontechnical staff; neither replaces the callback specification for your integration.

The practical finish line is not “the signature matched.” It is a traceable path from byte-exact verification to a single justified business action, with failures that can be recovered without weakening the boundary for everybody. Test that path with retry and crash behavior before allowing a callback to drive access, shipment or payout.