saascode

Webhooks that carry product events in both directions

Signed outbound webhooks, guarded inbound endpoints, replay protection, delivery logs, event routing, and direct bridges to automation platforms.

fabric · aug 21, 2026 · 5 min read

Inbound and outbound webhooks solve opposite problems. An outbound webhook tells another system that something happened inside the product. An inbound endpoint lets another system create an event inside it. The base includes both, and connects both sides to the same event layer.

That shared center is the useful part. A signup, payment, subscription change, inbound payload, or other dispatched event can feed notifications, automations, the admin bell, analytics, and outgoing webhooks. The product does not need a separate integration path for every destination.

Outbound: from a product event to a signed delivery

An owner can create an outgoing webhook with a URL and a list of subscribed events. The product generates a signing secret, returns the plaintext once, and stores only its encrypted form. The row belongs to the owner's organization and records whether it is active, its last delivery time, and its consecutive failure count.

When an event is dispatched, the webhook engine selects active rows subscribed to that event. Organization events only reach that organization's endpoints. It serializes the event name, data, and timestamp, signs the body with HMAC-SHA256, and sends the signature and event name in headers.

Delivery is observable. Each attempt records its source, event, payload, response status, a bounded response body, and success state. A successful response resets the failure count. A failed response increments it. Ten consecutive failures disable the endpoint and emit a webhook-failed event, which can itself reach the admin bell or an alert channel.

One bad secret does not stop the rest of the fan-out. Secret decoding happens inside the per-webhook delivery boundary. If one row cannot be decoded, that delivery fails and is logged while later endpoints still run.

The network request has its own guard. User-configured URLs pass through a fetch path that validates the URL, resolves and pins its public IP, rechecks every redirect, applies one timeout across the chain, and caps the response body. That makes the webhook feature less useful as a route into private network addresses.

The admin's test action uses the same guarded request path and the same stored secret. A test result is also written to the delivery log.

Integration cards ride the same engine

Zapier, Make, n8n, and Pabbly appear as integration cards, but they do not have a second delivery system. Saving one of those URLs creates a managed outgoing-webhook row with six initial subscriptions: signup, user creation, subscription creation, subscription cancellation, completed payment, and failed payment.

The owner can refine that event selection in the webhooks area. Saving the integration again updates the URL and reactivates the managed row without overwriting those later choices. Manually created webhooks remain separate from these managed rows.

This is a small but important connection. The integration card, event selector, signature, delivery log, test action, and failure policy all describe one path. There is no decorative automation card beside an unrelated webhook feature.

Inbound: from an external POST to a product event

An owner can also create a public inbound endpoint. The product generates a random endpoint ID and, by default, a signing secret. The plaintext secret is shown once and stored encrypted. Creating an unsigned endpoint requires an explicit waiver; turning off secret generation alone is rejected.

A signed sender includes a Unix timestamp, a unique event ID, and an HMAC-SHA256 signature over the timestamp, event ID, and raw request body. The receiver accepts timestamps within a five-minute window and compares signatures with a timing-safe operation.

Replay protection is durable. Before the event is dispatched, the receiver inserts the endpoint ID and external event ID into a table with a unique constraint. A repeated or concurrent copy returns 409. If that claim cannot be recorded, the receiver returns 503 and does not dispatch. The replay guard fails closed.

The endpoint also caps the request at 512 KB, using both the declared content length and the actual body. JSON becomes structured event data; a non-JSON body is preserved as a raw string. Accepted input is dispatched under a webhook.* event name, updates receive statistics, and enters the shared webhook log as an inbound record.

Once inside, the event can trigger the same automation and alert machinery used by native product events. The endpoint is not merely an inbox. It is an entrance to the product's event fabric.

The honest limit

The signed inbound contract is generic, not a prebuilt adapter for every vendor signature. An external sender must be able to provide the required timestamp, event ID, and signature headers. Services with a fixed webhook-signing scheme need a product-specific adapter unless their format matches this contract.

Unsigned endpoints are possible only by explicit choice, and that choice removes HMAC authentication and replay protection. They should be treated as a narrower compatibility path, not the default.

Outbound delivery is one attempt per dispatched event in the reviewed function. Failures are logged and eventually disable an endpoint; this layer does not enqueue an automatic retry. The destination should be idempotent because networks can still produce ambiguous outcomes.

The four automation cards save their URL before the managed-webhook bridge runs. A bridge error is logged but does not fail the settings save. The owner should confirm that the managed row appears in the webhooks area; a successful test POST to the URL proves the destination answered, not that event subscriptions were persisted.

Disconnecting a Zapier, Make, n8n, or Pabbly card clears its integration settings, but the current disconnect route does not remove the managed webhook row created for that card. The owner should disable or delete that row in the webhooks area as well. The synchronization function has a deletion branch for an explicitly cleared URL; the card's disconnect action does not call it.

The dispatcher is fire-and-forget. An inbound 200 means the payload passed the receiver and was handed to the event layer. It does not prove that every downstream notification, automation, or outgoing delivery completed.

Why both directions belong together

The useful unit is not an inbound page and an outbound page. It is a loop with clear boundaries. External systems can introduce signed events. Native and inbound events enter one dispatcher. That dispatcher can notify people, run product actions, or send signed deliveries elsewhere. Logs preserve which direction each webhook traveled.

That is what makes the webhooks layer part of a fabric: it does not only connect the product to outside services. It connects outside events to the same internal reactions as the product's own events.

end