When Services Call Back

Corsair’s incoming-event design addresses the reverse of an API call: the provider starts the interaction, but the application still needs to identify the source, verify it, attach it to the right tenant, and update useful local state. The focus here is the full webhook and public-delivery pipeline, including out-of-order events and the difference between direct provider webhooks and Hub-routed messages. Understanding those transitions lets an operator choose a transport without confusing delivery convenience with delivery guarantees.

From an opaque request to a tenant-owned event

A webhook arrives as an HTTP request, not as a trusted application object. Its headers and body may differ by provider, and the same application endpoint may receive events from Slack, GitHub, or another configured service. Corsair’s processing path begins by normalizing the request into a common shape. It can then use a plugin-level matcher to ask whether the request looks like it belongs to a provider and an event-level matcher to find the specific handler in that plugin’s webhook tree.

Matching is identification, not verification. A GitHub event header or a Slack signature header can help select the right plugin, but the request still needs signature verification using the provider’s stored webhook secret. The plugin’s key builder chooses the verification key for a webhook source, which is deliberately separate from the key used for an outbound API call. If Hub has already verified a delivered message, the request can carry a verified state and the application-side path can avoid repeating the provider signature check; the signed Hub delivery itself still has to be accepted with the application’s Hub signing secret.

Tenant resolution is the next boundary. In a simple one-tenant application, the event can use the default scope. In a multi-tenant application, the provider event must be associated with the account that authorized it. A plugin can extract an external identifier such as a Slack team or GitHub installation and use the link established during connection. An application can also supply routing information, but a query parameter is not automatically proof that the sender belongs to the named tenant. The provider signature, the external account link, and the application’s own identity rules each answer a different question.

Once the plugin and tenant are known, Corsair invokes the matched webhook handler with a typed request. The handler can normalize provider data into the plugin’s entity schema and update the shared local projection. A before hook can validate or transform the request, or silently skip an event that is irrelevant to the application. An after hook runs after successful processing and can notify another service, update an application table, or enqueue work. The ordering protects a useful invariant: a post-processing side effect should not claim success when the provider event could not be reconciled.

Each gate answers a different failure question. If no plugin matcher accepts the request, the application should not guess which provider’s parser to use. If a plugin matches but the signature fails, the event should not reach entity persistence. If verification succeeds but the external identifier cannot be linked to an account, the application should not silently write to a default tenant. If the entity update succeeds but a notification fails, the local event and the downstream side effect may need different retry or idempotency treatment. Separating these conditions makes the system’s response more intelligible than a single generic webhook error.