Direct REST integration

Deliver observations to POST /ingest from any language by reproducing the collector contract: build a redaction-safe envelope, sign the request body, and send batches with retry. This page is the guide; the API reference and observation envelope hold the complete tables.

Advanced integration

Prefer a Node.js, PHP, or Laravel collector when it supports your runtime; each is tested against the shared envelope fixtures. Build direct delivery only for runtimes the collectors do not cover.

When to use

  • Your backend does not have a supported collector SDK (Go, Python, JVM, and others).
  • You already have an event pipeline and want to ship envelopes from it.

Your client owns three responsibilities the collector normally covers: redaction before anything leaves your process, request signing, and bounded batching with retry.

Build the envelope

Construct one envelope per observed operation. The rules that matter most:

  • Never include payload values or headers. The envelope has no field for them. You send the payload's structural shape (field names and types) and its sha256: fingerprint, computed per the shape grammar.
  • Hash identifiers before sending. correlation.sourceEventIdHash, correlation.idempotencyKeyHash, and outcome.businessObjectIdHash carry hashes, never raw ids. Use a keyed hash with a key private to your backend.
  • Use placeholders for scope. Send "tenantId": "ten_authenticated" and "environmentId": "env_authenticated"; Seamward replaces them with the scope bound to your credential. integrationId carries your public sw_int_... key.
  • Generate unique evt_ event ids. They are the idempotency keys that make retries safe.

A complete minimal envelope for an accepted inbound webhook:

Code example

{  "envelopeVersion": "0.2",  "eventId": "evt_01J8ZQ4X2KD9",  "tenantId": "ten_authenticated",  "environmentId": "env_authenticated",  "integrationId": "sw_int_c9K2mQ7xLp4wR8tZ",  "direction": "inbound",  "protocol": "http-webhook",  "occurredAt": "2026-08-15T09:24:11.000Z",  "operationIdentityVersion": "1",  "eventType": "candidate.create",  "correlation": {},  "contract": {    "observedFingerprint": "sha256:9c4f2a7d8e1b6a3c5d9e0f2a4b6c8d1e3f5a7b9c1d3e5f7a9b1c3d5e7f9a0b2c"  },  "transport": {    "method": "POST",    "routeTemplate": "/webhooks/candidates",    "payloadLocation": "message",    "statusCode": 202,    "durationMs": 48,    "attempt": 1  },  "payload": {    "storage": "none",    "schemaFingerprint": "sha256:9c4f2a7d8e1b6a3c5d9e0f2a4b6c8d1e3f5a7b9c1d3e5f7a9b1c3d5e7f9a0b2c",    "schemaShape": {      "kind": "object",      "fields": {        "candidateId": { "kind": "string" },        "status": { "kind": "string" }      }    },    "redactionPolicyVersion": "acme-go-v1"  },  "outcome": { "accepted": true }}

Represent each protocol

Envelope 0.2 uses one transport object for all four shipped protocols. For queue and scheduled-feed observations, this is a compatibility projection rather than an HTTP request:

OperationdirectionprotocolmethodrouteTemplatepayloadLocation
Outbound HTTP APIoutboundhttp-apiverbstable path such as /v1/candidatesresponse by default, or request
Inbound webhookinboundhttp-webhookverbstable path such as /webhooks/eventsmessage
Queue publishoutboundqueuePOSTstable queue or topic labelmessage
Queue consumeinboundqueuePOSTstable queue, topic, or subscriptionmessage
Scheduled importinboundscheduled-feedPOSTstable feed or job labelmessage
Scheduled exportoutboundscheduled-feedPOSTstable feed or job labelmessage

For HTTP traffic, statusCode is the response code, or 0 when the attempt received no response. For a direct queue or feed implementation, choose status semantics that stay stable across versions and document them for your team. Seamward's Node.js, PHP, and Laravel collectors use 202 for an acknowledged queue operation, 200 for a completed feed, 422 for an explicit rejection, and 500 when the wrapped host operation throws.

routeTemplate is restricted to a bounded, low-cardinality value. HTTP protocols require an absolute path with no query string; queue and scheduled-feed protocols accept a queue, topic, subscription, feed, or job label. Never put message ids, run ids, timestamps, customer ids, or other per-event values in it.

Sign and send

Every request carries three headers: X-Seamward-Connection (your public Connection key), Authorization: Bearer (that Integration's ingest token), and X-Seamward-Signature. The signature is HMAC-SHA256 over "<unix seconds>.<exact body string>", keyed with the ingest token, formatted t=<seconds>,v1=<64 lowercase hex>:

Code example

import hashlib, hmac, json, timeimport urllib.request
def deliver(envelopes: list, connection_key: str, ingest_token: str, endpoint: str):    body = json.dumps({"envelopes": envelopes}, separators=(",", ":"))    ts = int(time.time())    digest = hmac.new(        ingest_token.encode(), f"{ts}.{body}".encode(), hashlib.sha256    ).hexdigest()    request = urllib.request.Request(        endpoint,        data=body.encode(),        headers={            "Content-Type": "application/json",            "X-Seamward-Connection": connection_key,            "Authorization": f"Bearer {ingest_token}",            "X-Seamward-Signature": f"t={ts},v1={digest}",        },        method="POST",    )    return urllib.request.urlopen(request)

Serialize once and sign that exact string; re-serializing after signing changes the bytes and fails with reason: "mismatch". Signatures older than 300 seconds fail with reason: "stale", so sign immediately before sending.

Handle the response

202 means every envelope was accepted; 207 means the batch was mixed, with per-item results aligned by index:

Code example

{  "accepted": 1,  "persisted": 1,  "results": [    { "index": 0, "accepted": true, "eventId": "evt_01J8ZQ4X2KD9" },    {      "index": 1,      "accepted": false,      "issues": ["integration key does not match the connection"]    }  ]}

Batching and retry rules, matching the collector's behavior:

  • Send at most 50 envelopes per request and keep requests under 1 MiB.
  • Retry 5xx, 409, and network failures with bounded backoff; event-id idempotency makes redelivery safe.
  • Do not retry 400 or 401 unchanged, and do not resend 207-rejected items until fixed.
  • Bound your queue and drop oldest under sustained failure; telemetry must never take down your service.

The direct REST contract is available to any server runtime. Seamward does not currently ship Go, Python, JVM, browser, edge-runtime, broker-specific, or scheduler-specific SDKs. PHP teams can instead install the alpha framework-neutral PHP collector or Laravel collector from Packagist. A direct client owns redaction, shape calculation, hashing, signing, retry, and fail-open behavior.

Envelope versions and skipped inspection

Existing 0.1 and 0.2 senders remain supported. Use 0.3 when sending payload.schemaInspected: false to indicate that a body was not inspected. Keep the placeholder shape out of your own structural comparisons too. A new field inside a strict older version is rejected. Update external strict readers before sending 0.3; see the wire contract and rollout order.

Next steps