AlgoMaster Logo

Webhooks

High Priority15 min readUpdated September 13, 2026
Listen to this chapter
Unlock Audio

Once a partner has requested a catalog export, it needs a way to learn when the file is ready. Repeated status requests work, but most responses may say that nothing has changed. A webhook lets the bookstore contact the partner when the export succeeds.

That reverses the direction of the HTTP request and introduces a new contract. The partner must know what the notification means, whether it is authentic, and when it can safely acknowledge receipt.

This chapter covers webhook subscriptions, event payloads, authenticated delivery, and receiver behavior using a fictional bookstore API over HTTPS.

1. The Webhook Interaction

A webhook is an HTTP request that a provider sends to a registered receiver when a relevant event occurs. The provider becomes the HTTP client for that exchange. The receiver operates an endpoint that accepts incoming requests and responds according to the delivery contract.

The original business request and the notification are separate exchanges. The bookstore can finish an export even if the partner's webhook endpoint is temporarily unavailable. A response to the webhook acknowledges that delivery; it cannot retroactively change the response to the original export submission.

The basic interaction is:

Request catalog exportAccepted with operation IDPublish file and record completion eventPOST completion notificationVerify and durably accept event204 No ContentRetrieve operation with partner credentialsSuccessful operation and result linkPartner applicationBookstore APIPartner webhook endpointPartner applicationBookstore APIPartner webhook endpoint
8 / 8
algomaster.io

The notification gives the partner a reason to act without waiting for its next scheduled status request. It does not guarantee immediate delivery, and the acknowledgment does not prove that the partner has downloaded or imported the file.

Webhooks fit services that can operate an available receiver. A browser tab or a command-line tool without an inbound endpoint usually needs a backend to receive notifications on its behalf. Ordinary status retrieval remains useful when that backend is unavailable or a notification never arrives.

HTTP supplies the transport. Event names, payloads, signature headers, retry behavior, and acknowledgment rules still need an explicit contract. The bookstore conventions below are illustrative API choices, not universal webhook standards.

2. Subscription Design

A subscription connects a destination to the events it may receive. Treat it as an authorized resource with an owner, rather than a URL that a caller includes in an arbitrary business request.

The bookstore lets a partner register one destination for selected export outcomes. This example subscription applies to future matching events from that partner's exports after activation; it does not replay historical events. Subscription management requires a credential with the relevant account permission.

All HTTP JSON bodies in these examples use compact UTF-8 without a trailing newline. API tokens are nonfunctional placeholders.

The service validates the destination and event selection, binds the subscription to the authenticated account, and creates it in a pending state:

Creating the subscription does not yet enable business event delivery. The partner configures its receiver with a separate signing secret that the service supplies through its authenticated secret-management flow. The API does not return the secret in ordinary subscription reads or include it in event payloads.

Before activation, this API verifies destination control using a short-lived, unpredictable challenge that the API binds to this subscription and URL. The receiver authenticates the verification request with its configured secret and echoes the challenge through the documented verification response. Challenge messages are setup messages and must not trigger export processing.

The setup boundary is:

Destination verification establishes that the configured receiver cooperates with the setup flow. It does not grant the account permission to subscribe to another tenant's events. Account authorization and destination verification solve different problems.

A registration containing an unsupported event type receives a validation error before the service creates a subscription:

An authenticated caller without subscription-management permission receives 403 with subscription_not_allowed. The service determines the account from the authenticated context; a client-supplied ownership field cannot change it.

3. Destination Safety

Accepting webhook destinations lets users influence where the provider sends network requests. Without restrictions, this can create server-side request forgery, where a user causes the service to contact a destination it should not access.

For this public partner integration, permit HTTPS destinations on port 443, require valid server certificates, and reject URLs containing embedded credentials or fragments. Block loopback, private, link-local, and other non-public destinations for both IPv4 and IPv6. Enforce outbound network restrictions as well as URL validation.

Check the actual resolved destination when connecting, including on retries. Validating a hostname once during registration is insufficient because its DNS results can change. The connection must use an approved address while still verifying the intended hostname's certificate. Do not automatically follow delivery redirects into a different destination.

These rules apply to verification requests too. A challenge request is already an outbound network action; sending it before destination checks would leave the same vulnerability in the setup flow. Private-network integrations need a separately controlled connectivity model.

Limit event subscriptions to the data the partner may receive. Disabling a subscription should prevent new deliveries under the documented policy, but the provider cannot recall a request it has already sent. The provider should therefore minimize sensitive information before sending it.

4. Event Payloads

An event should describe a meaningful fact. catalog_export.succeeded means that the service published a complete export file. It does not mean that a worker started or that the service queued a completion notification.

The provider records the completion fact and its delivery intent durably with the business outcome, or through a recovery design that preserves the same relationship. Announcing success before publication can cause the receiver to fetch a result that does not exist. Forgetting the notification after publication can leave the partner unaware of completed work.

The bookstore sends a small envelope containing identity, type, time, schema version, and relevant data:

event_id identifies the business event and remains stable when the provider delivers that event again. subscription_id identifies the destination subscription. The timestamp the provider includes in the authenticated delivery is separate from occurred_at: the provider may legitimately deliver an old event now.

The payload version defines how to interpret this envelope and its event-specific data. The subscription fixes this version, so an unrelated change to the partner's normal API requests does not silently change incoming event shapes. Additive fields should not break a receiver that validates required fields and ignores safe, unknown additions.

Choose how much business data to include deliberately:

Scroll
Payload approachReceiver obtainsMain benefitMain trade-off
SnapshotSelected resource values at event timeCan process without another readMore retained data; values may now be stale
ReferenceIdentity and a lookup locationSmall payload; retrieval uses current permissionsDepends on another API request and resource retention
HybridA few event-time facts plus a referenceUseful context with limited payload sizeMust distinguish historical facts from current state

This export event uses a reference. Its relative status_url resolves against the bookstore API origin, not the receiver's hostname. The receiver restricts lookups to that known origin and expected operation route; it does not attach its API token to arbitrary URLs in incoming JSON.

Fetching a resource later usually obtains its current representation, not a historical snapshot. For this terminal export outcome, that works well. An inventory event that must preserve the exact stock change would need the change itself or a retained historical record. A reference to current inventory alone cannot reconstruct every past transition.

5. Delivery Authentication

An endpoint that accepts requests from the internet will receive requests from callers other than the expected provider. A familiar event name, account ID, or HTTP user agent is not authentication.

HTTPS protects the connection and authenticates the destination server. The receiver also needs evidence that a holder of the expected signing key generated the payload. The bookstore uses HMAC-SHA256, a message authentication code that uses a secret key, to authenticate each delivery. A shared-key MAC provides integrity and authenticity between the key holders; it is not encryption or proof that only one party could have produced the message.

For this example, the custom Bookstore-Signature header has exactly one t value and one v1 value. t is a decimal Unix timestamp in seconds for this delivery attempt. v1 is the lowercase hexadecimal HMAC-SHA256 of the ASCII timestamp, one literal period, and the exact request body bytes, using the subscription's secret bytes as the key.

The following request is a reproducible example. Its public demonstration key is the 32 bytes you get by decoding the hexadecimal string 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f. It is not a deployment secret. Real subscriptions require secrets that the service generates independently and stores securely.

The timestamp corresponds to 12:04:05 UTC, five seconds after the event occurred. The signature covers the signed body, including its account and subscription identifiers. It does not cover the URL or other headers, so the receiver must also enforce its configured route, method, and content type.

Verify against the raw body before parsing JSON. Parsing and serializing again can change whitespace, escaping, or property order while preserving the JSON meaning, which changes the signed bytes. This contract sends uncompressed request bodies; middleware must not transform them before verification.

The core verification calculation in Python is:

This is the signature check, not a complete request handler. The HTTP layer rejects duplicate signature headers, bounds the body to 64 KiB, rejects unsupported content encoding, and obtains the secret from trusted endpoint configuration. After verification, the receiver parses and validates the payload and checks that its account and subscription match that configuration.

The five-minute tolerance is this contract's replay window, not a universal rule. The receiver uses its current clock and rejects timestamps too far in the future as well as too old. Keeping clocks synchronized matters. The provider generates a fresh timestamp and signature for each retry while preserving the event identity.

Timestamp verification limits how long an attacker can replay a captured request; it does not prevent a duplicate inside that window. The receiver must still deduplicate events. It rejects an invalid or stale signature before accepting any work:

Keep the error generic. Do not return the expected signature, key material, or details that expose secret selection. Signature formats vary by provider; a receiver must implement the documented scheme rather than assume that every webhook uses these headers or these signed bytes.

6. Receipt and Business Processing

A receiver should acknowledge only after it has taken the responsibility its response promises. For this integration, any 2xx delivery response means the receiver has durably accepted the event, or has established that it already accepted that event. The receiver normally returns 204 No Content with no response body.

The work before acknowledgment is deliberately small:

The inbox is durable storage for accepted notifications awaiting processing. Coordinate the inbox write with any record needed to dispatch the event, so the receiver cannot acknowledge an event that no worker will process. A worker can poll this inbox directly, or dispatch can use another durable mechanism.

Once the receiver safely records the event, it responds:

The receiver does not need to download the file before responding. If the receiver starts a background task only in memory and immediately acknowledges, a process crash can lose the task after the provider has stopped trying to deliver it.

Authentication does not establish payload validity. A correctly signed body missing event_id receives 400 with invalid_event and creates no inbox entry. A temporary inbox storage failure receives 503; the receiver cannot honestly acknowledge durable receipt. Neither status tells the provider that partner business processing succeeded.

Document how the sender handles non-success responses, timeouts, and exhausted retries separately from what the acknowledgment means. HTTP alone does not prescribe a webhook retry schedule. In particular, a timeout can occur after the inbox commit, so another attempt must remain safe.

7. Duplicates, Ordering, and Missing Notifications

Suppose the inbox commit succeeds but the provider never receives the 204 response. The provider delivers evt_903 again. The receiver verifies the new attempt, recognizes the accepted event, and returns success without creating a second inbox item.

For this receiver, a unique constraint on (provider, subscription_id, event_id) establishes that boundary atomically. Checking for a row and then inserting without a uniqueness guarantee allows concurrent deliveries to create duplicates. The receiver also acknowledges a duplicate when its original inbox entry is still pending. Processing continues from that original durable entry.

If several subscriptions feed the same business workflow, deduplication may also need to span those subscriptions. Likewise, distinct events can refer to the same business outcome. Download-and-import processing should guard its own operation identity so that overlapping notification paths do not import the same export twice.

Inbox deduplication alone does not make side effects exactly once. A worker can commit an import and crash before marking the inbox entry processed. Coordinate the business write with its processing record when possible, or give the downstream action an idempotent identity. Retain duplicate-protection records for the supported retry and redelivery horizon; forgetting an ID makes an old delivery look new.

Do not assume arrival order. The provider may deliver separate events late, and workers may process them concurrently. The occurred_at timestamp is not a global sequence number. This terminal export event can trigger a current operation read. A stream of mutable resource updates may instead need per-resource versions or reconciliation so an old notification cannot overwrite newer local state.

A successful signature also does not prove that the underlying resource is still accessible. If result access has expired or the partner lost permission, a later lookup can fail legitimately. The receiver must handle that outcome without bypassing authorization or treating the notification as a download credential.

Finally, notifications are not a complete historical database. The partner should retain operation IDs from submissions and reconcile unfinished exports through normal API reads when needed. That provides a path to recover from missing notifications without requesting new work. Detailed delivery history, redelivery controls, and monitoring belong to the delivery system's operational contract.

Summary

A webhook is a separate HTTP exchange that reports an event to an authorized destination. Define subscription scope, event meaning, payload version, and the acknowledgment boundary clearly.

Verify the exact signed bytes, check delivery freshness and account binding, and persist accepted events before responding. Handle duplicates and delayed notifications without repeating business effects, and preserve an API lookup path when notification delivery is insufficient.