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
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, andoutcome.businessObjectIdHashcarry 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.integrationIdcarries your publicsw_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:
| Operation | direction | protocol | method | routeTemplate | payloadLocation |
|---|---|---|---|---|---|
| Outbound HTTP API | outbound | http-api | verb | stable path such as /v1/candidates | response by default, or request |
| Inbound webhook | inbound | http-webhook | verb | stable path such as /webhooks/events | message |
| Queue publish | outbound | queue | POST | stable queue or topic label | message |
| Queue consume | inbound | queue | POST | stable queue, topic, or subscription | message |
| Scheduled import | inbound | scheduled-feed | POST | stable feed or job label | message |
| Scheduled export | outbound | scheduled-feed | POST | stable feed or job label | message |
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
400or401unchanged, and do not resend207-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
- Ingest observations reference: the complete contract, error table, and a signed curl example.
- Observation envelope: every field and the shape grammar.
