Observe webhooks
observeWebhook records what an inbound webhook actually did: whether the handler accepted it, which business object it produced, and what the payload looked like structurally. Getting three things right here (the boundary, the outcome, and the route template) determines how useful every later drift finding and incident is.
Choose the boundary
Wrap the handler that converts a provider event into an application result: one stable external workflow per Integration. Do not combine unrelated providers or business outcomes in one Integration, and do not wrap framework plumbing that runs before your handler.
Code example
const handleCandidateWebhook = seamward.observeWebhook( { routeTemplate: "/webhooks/candidates", }, existingCandidateHandler,);Preserve behavior
The wrapper invokes your existing handler and records its result after the operation. Nothing about your webhook contract changes:
- Your handler's return value is passed through unchanged, and a thrown error is recorded (
statusCode: 500,accepted: false) and rethrown unchanged. - Provider acknowledgement, retry, and idempotency logic stay yours. The collector never replaces a response.
- If Seamward is unreachable, evidence queues and eventually drops; your handler is unaffected.
Report the outcome
The value your handler returns is the contract that expected-outcome rules later key on:
Code example
async (payload) => { const candidate = await candidateService.accept(payload); return { statusCode: 202, eventType: "candidate.create", outcome: { accepted: true, businessObjectType: "candidate", businessObjectId: candidate.id, }, correlation: { sourceEventId: candidate.providerEventId }, };};outcome.acceptedstates whether the business operation succeeded, independent of transport status.businessObjectTypeplusbusinessObjectIdidentify what was produced; the id is keyed-hashed in your process before transmission, and reconciliation matches on the hash.correlation.sourceEventIdties this event to the downstream result that should follow it; it is hashed the same way.- On a malformed payload, return
{ statusCode: 400, outcome: { accepted: false } }rather than throwing, so the rejection is recorded as a decision, not a crash.
Route templates
routeTemplate and eventType become the operation's identity across every observation, so they must be stable and low-cardinality:
- Use
/webhooks/candidates, never/webhooks/candidates/123or anything containing an id, email, or query string. - Use
eventTypefor the provider's event name (candidate.create), never for identifiers. - Changing a template later splits the operation's baseline; pick templates you can keep.
Next steps
- Detect silent business failures: what the outcome contract enables.
- Redaction: how identifiers are hashed before transmission.
