Observation envelope
Every observation is one JSON envelope. The schema is strict: unknown keys are rejected, and there is no field anywhere in it for raw payload bodies or request headers, which is what makes the privacy posture provable rather than aspirational. Seamward collector SDKs build envelopes for you; this page is the contract for reading evidence or building envelopes yourself.
Envelope fields
The current envelope version is 0.3. Seamward continues to accept unchanged 0.1 and 0.2 envelopes. Version 0.3 adds an explicit indication that payload inspection was skipped. Node collectors emit 0.3; the current PHP and Laravel collectors continue emitting supported 0.2.
| Field | Type | Required | Constraints |
|---|---|---|---|
envelopeVersion | string | yes | Literal "0.3" for new envelopes; "0.1" and "0.2" remain readable |
eventId | string | yes | Starts with evt_; the idempotency key for ingestion |
tenantId | string | yes | Starts with ten_; send ten_authenticated, replaced from your credential |
environmentId | string | yes | Starts with env_; send env_authenticated, replaced from your credential |
integrationId | string | yes | Your public Integration key sw_int_... (resolved internally by Seamward) |
direction | enum | yes | inbound or outbound |
protocol | enum | yes | http-webhook, http-api, scheduled-feed, or queue |
occurredAt | string | yes | ISO 8601 datetime |
operationIdentityVersion | string | yes in v0.2+ | Literal "1" |
eventType | string | no | 1 to 128 chars matching [A-Za-z0-9._:-]; a low-cardinality event label such as candidate.completed, never an identifier |
deployment | object | no | Release context; see below |
correlation | object | yes | May be empty {}; see below |
contract | object | yes | See below |
transport | object | yes | See below |
payload | object | yes | Structural evidence; see below |
outcome | object | yes | See below |
transport (all fields required)
| Field | Type | Constraints |
|---|---|---|
method | enum | GET, POST, PUT, PATCH, DELETE |
routeTemplate | string | Non-empty. A stable template like /webhooks/candidates, never a concrete id or query string |
payloadLocation | enum | request, response, or message; required in v0.2+ |
statusCode | integer | Exactly 0, or 100 through 599; 0 means no protocol response was received |
durationMs | number | Zero or greater |
attempt | integer | 1 to 2,147,483,647 |
Protocol identity and compatibility
An operation identity combines direction, protocol, method, normalized routeTemplate, eventType, and payloadLocation. A status selector can participate in registered-contract matching, but the observed status does not split the operation-family key. Keep every identity field stable and low-cardinality.
HTTP observations use HTTP-native values. Queue and scheduled-feed collectors expose protocol-native metadata, then project it into the same transport object for envelope 0.2 compatibility:
| Observed operation | direction | protocol | method | routeTemplate | payloadLocation |
|---|---|---|---|---|---|
| Outbound API call | outbound | http-api | HTTP verb | stable absolute path | response or request |
| Inbound API call | inbound | http-api | HTTP verb | stable absolute path | request |
| Inbound webhook | inbound | http-webhook | HTTP verb | stable absolute path | message |
| Queue publish | outbound | queue | POST | queue or topic label | message |
| Queue consume | inbound | queue | POST | queue, topic, or subscription label | message |
| Scheduled import | inbound | scheduled-feed | POST | feed or job label | message |
| Scheduled export | outbound | scheduled-feed | POST | feed or job label | message |
For HTTP protocols, the route must be an absolute path without a query string. :id and {id} segments normalize to {}. For queues and feeds, the target is a label of at most 256 characters matching letters, numbers, ., _, :, /, or -, and it must start with a letter or number. Never use a concrete id, message id, run id, timestamp, or customer value.
The Node.js, PHP, and Laravel collectors use these compatibility status values for non-HTTP wrappers: queue acknowledgement 202, feed completion 200, explicit rejection 422, and thrown host error 500. These numbers describe the envelope projection. They do not command or replace broker acknowledgement, retry, dead-letter, or scheduler behavior.
payload (structural evidence, never values)
| Field | Type | Required | Constraints |
|---|---|---|---|
storage | enum | yes | none (the default posture: the body was dropped in your process) or encrypted-object |
schemaFingerprint | string | yes | sha256: plus 64 hex chars |
schemaShape | shape | yes | The recursive structure description; grammar below |
schemaInspected | boolean | no | Only in v0.3. false marks transport-only evidence whose placeholder shape must not be used for structural drift. Absence means the legacy inspection interpretation applies. |
redactionPolicyVersion | string | yes | The policy version active when the observation was built |
objectRef | string | only with encrypted-object | Present if and only if storage is encrypted-object |
contract
| Field | Type | Required | Constraints |
|---|---|---|---|
declaredVersion | string | no | The contract version you registered, when one exists |
observedFingerprint | string | yes | sha256: plus 64 hex chars |
outcome
| Field | Type | Required | Constraints |
|---|---|---|---|
accepted | boolean | yes | Whether the operation reached its intended result |
businessObjectType | string | no | Domain type such as candidate or payment |
businessObjectIdHash | string | no | Keyed hash of the business object id; never the raw id |
correlation (all fields optional, hashes only)
| Field | Constraints |
|---|---|
traceId | Non-empty string; safe to send as-is because trace ids are operational, not personal |
sourceEventIdHash | Keyed hash of the provider's event id |
idempotencyKeyHash | Keyed hash of the idempotency key |
hashNamespace | Non-secret key-generation identifier; required in v0.2+ when a keyed hash is present |
deployment (optional; at least one field when present)
| Field | Constraints |
|---|---|
service | 1 to 128 chars, starts alphanumeric, then [A-Za-z0-9._:/+~-] |
release | Same label rules as service |
commitSha | 7 to 64 hex chars (abbreviated through full SHA-256) |
An empty deployment: {} is invalid; omit the object instead.
The shape grammar
schemaShape describes structure with six node kinds and no values: {"kind":"null"}, {"kind":"boolean"}, {"kind":"number"}, {"kind":"string"}, {"kind":"array","items":[...]} and {"kind":"object","fields":{...}}. Object keys are sorted; array item shapes are deduplicated.
A payload of {"id":"x","amount":1,"tags":["a"]} produces:
Code example
{ "kind": "object", "fields": { "amount": { "kind": "number" }, "id": { "kind": "string" }, "tags": { "kind": "array", "items": [{ "kind": "string" }] } }}The fingerprint is the SHA-256 of the shape's canonical signature, here {amount:number,id:string,tags:[string]}:
Code example
sha256:ea4cc46608c9c12efd2466a49251ad85b3893dcf7a846a938a3d00aad5785488Two payloads with the same structure always share a fingerprint regardless of their values, key order, or array length. That is what lets Seamward detect contract drift by comparing fingerprints without ever seeing your data.
Validating an envelope
The contracts package that ships with the collector exports the schema, so you can validate your own envelopes before sending:
Code example
import { safeParseEnvelope } from "@seamward/contracts";
const result = safeParseEnvelope(envelope);if (!result.success) { console.error(result.error.issues);}Version compatibility
Strict readers must understand envelope 0.3 before consuming new Node collector
observations. Deploy the accepting API and worker first, then update any external
readers using safeParseEnvelope, and finally release the new Node collector.
The required @seamward/contracts build exports ENVELOPE_VERSION = "0.3" and
accepts all three versions. This change is pending the collector/contracts
Changesets release; do not assume the currently published alpha already supports it.
A schemaInspected field in a 0.1 or 0.2 envelope is rejected. Existing PHP
and Laravel 0.2 output needs no rewrite and remains accepted.
Next steps
- Ingest observations: sending envelopes to Seamward.
- Direct REST integration: building envelopes without the collector.
- Redaction: what leaves your process and what never does.
