Example application

This is the reference instrumentation example: an ATS-style webhook consumer where the entire Seamward integration is the observeWebhook call. Copy it into your own project; everything it needs is the collector package and your two runtime connection values.

The complete example

Code example

import Fastify from "fastify";import { createSeamwardCollector } from "@seamward/collector";
const collector = createSeamwardCollector({  connectionKey: process.env.SEAMWARD_CONNECTION_KEY!,  ingestToken: process.env.SEAMWARD_INGEST_TOKEN!,  policy: {    version: "example-v1",    dropFields: [],    hashFields: ["candidate_email"],  },});
const candidates = new Map<string, Record<string, unknown>>();
const handleCandidateWebhook = collector.observeWebhook(  {    routeTemplate: "/webhooks/candidates",  },  async (payload) => {    const candidate = payload as { id?: string };    if (typeof candidate.id !== "string") {      return { statusCode: 400, outcome: { accepted: false } };    }    candidates.set(candidate.id, candidate);    return {      statusCode: 202,      eventType: "candidate.create",      outcome: {        accepted: true,        businessObjectType: "candidate",        businessObjectId: candidate.id,      },    };  },);
const app = Fastify();app.post("/webhooks/candidates", async (request, reply) => {  const result = await handleCandidateWebhook(    request.body as Record<string, unknown>,  );  return reply.code(result.statusCode ?? 200).send();});
await app.listen({ port: 4190 });
process.once("SIGTERM", async () => {  await collector.stop();  process.exit(0);});

Three things to notice:

  • The business logic is untouched. The handler stores the candidate exactly as it would without Seamward; observation wraps it.
  • The outcome is the contract. accepted, businessObjectType, and businessObjectId are what expected-outcome rules later match on; the malformed-payload branch records a rejection as a decision (400, accepted: false) rather than a crash.
  • PII is handled at the edge. The policy hashes candidate_email for any pipeline that uses the redaction helpers, and payload values never ship regardless.

Run it against your workspace

  1. Select the workspace's Development environment and connect an Integration there instead of using Production.
  2. Set SEAMWARD_CONNECTION_KEY and SEAMWARD_INGEST_TOKEN in the example's environment.
  3. Install the collector per the Node guide, start the app, and POST a candidate to /webhooks/candidates.
  4. Confirm the first observation from the integration page; the evidence you see contains fingerprints and hashes, and not one payload value.

Produce a known incident

With the example running, controlled changes produce each failure class on demand:

  • Send the same payload with id renamed to candidate_id: a structural finding.
  • Send id as a number instead of a string: a type-changed finding.
  • Create an expected-outcome rule for candidate.create, then send an event whose downstream result never arrives: a missing-outcome incident after the rule's delay window.

Keep these experiments in the test integration; baselines are per-integration, so production evidence stays clean.

Observe another protocol

The same SDK wraps every protocol without changing your existing clients. Create a separate Integration and Connection-key-bound collector for each direction/protocol boundary:

OperationCollector entry pointMetadata you provide
Outbound HTTP APIobserveFetchstable route template, optional event type, request or response side
Inbound webhookobserveWebhookstable route template, handler outcome
Queue publishobserveQueuePublishstable queue or topic label
Queue consumeobserveQueueConsumerstable queue, topic, or subscription label
Scheduled import or exportobserveScheduledFeedstable feed label and direction

The queue and scheduled-feed wrappers are broker-neutral; broker-specific adapters are not shipped. Follow the Node.js collector guide, PHP collector guide, or Laravel collector guide for runtime-specific examples.

Next steps