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.accepted states whether the business operation succeeded, independent of transport status.
  • businessObjectType plus businessObjectId identify what was produced; the id is keyed-hashed in your process before transmission, and reconciliation matches on the hash.
  • correlation.sourceEventId ties 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/123 or anything containing an id, email, or query string.
  • Use eventType for 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