Observations

The Observation API reads the same tenant-scoped redacted evidence shown in the workspace console. It never returns raw request bodies, response bodies, headers, secrets, or unhashed business identifiers.

Authentication and scope

Use either a workspace API key carrying observations:read or the setup OAuth access token managed by the Seamward CLI:

Code example

Authorization: Bearer $SEAMWARD_API_KEY

The CLI setup token requires seamward:setup:observations:read, is bound to one workspace environment, and may read only an Integration approved in that setup grant. Supply integrationId on list requests and as a query parameter on detail requests when using setup OAuth. A missing or foreign binding returns integration_scope_mismatch.

An ingest token can write observations but cannot read them. A workspace API key can read only its own workspace. A foreign observation id returns observation_not_found.

List observations

Code example

GET /v1/observations

Supported query parameters:

ParameterMeaning
integrationIdOne integration
environmentIdOne workspace environment
eventTypeExact event type
operationKeyExact persisted operation identity key
statusCodeExact status from 0 through 599; v0.2 uses 0 or 100 through 599
retryOnlytrue for attempts greater than one, false for first attempts
hasFindingstrue or false based on relational drift-finding evidence
hasIncidentstrue or false based on relational incident evidence
from / toISO-8601 received-time range. from is inclusive and to is exclusive
qCase-insensitive search across id, event type, route, release, and commit
limitPage size from 1 to 100; default 25
cursorOpaque nextCursor from the prior response
pageNumbered page from 1 to 100000; cannot be combined with cursor
sortByreceivedAt or durationMs; defaults to receivedAt
sortOrderasc or desc; defaults to desc

Use cursor for stable sequential API processing. Use page for interactive tables that need page numbers and a total row count. Cursor pagination supports the default receivedAt desc order. Numbered pagination supports both documented sort fields.

Code example

{  "observations": [    {      "id": "evt_01J8ZQ4X2KD9",      "envelopeVersion": "0.2",      "integration": {        "id": "int_orders",        "name": "Order sync",        "provider": "Acme ERP"      },      "environmentId": "env_production",      "occurredAt": "2026-08-21T11:59:59.000Z",      "receivedAt": "2026-08-21T12:00:00.000Z",      "direction": "outbound",      "protocol": "http-api",      "eventType": "order.created",      "method": "POST",      "routeTemplate": "/orders",      "payloadLocation": "response",      "statusCode": 202,      "durationMs": 48,      "attempt": 1,      "operationIdentityKey": "op_v1_orders_create_response",      "outcome": {        "accepted": true,        "businessObjectType": "order"      },      "deployment": {        "service": "orders-api",        "release": "release-42",        "commitSha": "edfe985ab2486420645673ef50e12491d498f379"      },      "observedFingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",      "findingCount": 0,      "incidentCount": 0,      "state": "recorded"    }  ],  "nextCursor": null,  "pagination": null}

When page is supplied, nextCursor is null and the response includes:

Code example

{  "pagination": {    "page": 2,    "pageSize": 25,    "totalCount": 61,    "totalPages": 3  }}

The state is a review aid, not a billing rule. incident has highest priority, then finding. Status 0 is classified as transport_error before retry state; for responses, retry takes priority over transport_error. A non-retry rejected outcome is outcome_rejected; otherwise the state is recorded.

For v0.2, status 0 is the transport-failure sentinel: no protocol response was received. It is returned with state transport_error; a v0.2 envelope cannot combine status 0 with outcome.accepted: true.

Legacy v0.1 observations remain readable with their original 0 through 599 status range and legacy state semantics. New v0.2 collectors emit only 0 or an HTTP status from 100 through 599.

Get one observation

Code example

GET /v1/observations/{observationId}

The response adds the complete redacted envelope plus linked finding and incident summaries. The envelope contains shape metadata, fingerprints, and hashes only. Links are relational, so evidence remains queryable when an incident is updated or a finding projection changes.

Get usage

Code example

GET /v1/observations/usage

Code example

{  "usage": {    "period": "utc_calendar_month",    "periodStart": "2026-08-01T00:00:00.000Z",    "periodEnd": "2026-09-01T00:00:00.000Z",    "planCode": "developer",    "monthlyLimit": 50000,    "recordedCount": 125,    "remainingCount": 49875,    "usagePercent": 0.25,    "evidenceRetentionDays": 7,    "daily": [{ "date": "2026-08-21", "recordedCount": 125 }]  }}

One usage unit is one unique, successfully persisted redacted envelope. Healthy and unhealthy operations both count. An invalid rejected envelope does not count. Re-delivering the same eventId is idempotent and does not count again. A genuine retry with a new event id is a separate observation. Usage buckets remain after evidence rows expire under the workspace retention policy.

This route reports usage for the authenticated workspace. Its remainingCount is the difference between that workspace's count and its configured plan limit; it is not the remaining shared account allowance. Account administrators can read the pooled monthly total through GET /billing-accounts/{accountId}/usage with a browser session.

Read shadow usage by mode

The compatibility bridge adds a separate read endpoint:

Code example

GET /v1/observations/usage-by-mode

Use a workspace API key with observations:read. Setup OAuth and ingest tokens cannot read these workspace aggregates.

Code example

curl --fail-with-body https://api.seamward.com/v1/observations/usage-by-mode \  -H "Authorization: Bearer $SEAMWARD_API_KEY"

Code example

{  "usage": {    "period": "utc_calendar_month",    "periodStart": "2026-09-01T00:00:00.000Z",    "periodEnd": "2026-10-01T00:00:00.000Z",    "accounting": "shadow",    "enforcement": "disabled",    "enforcementScope": "sandbox",    "startedAt": "2026-09-20T00:00:00.000Z",    "coverage": "partial",    "recordedCount": 10,    "unclassifiedCount": 5,    "modes": {      "sandbox": { "recordedCount": 3 },      "live": { "recordedCount": 2 }    }  }}

The compatibility counters measure accepted observations within this workspace. accounting: "shadow" retains the bridge response format. enforcement reports disabled, grace, or active from the account policy, and enforcementScope is always sandbox. This schedule cannot introduce Live quota rejection. Querying this endpoint does not change the policy. During the bridge, stored Development and Staging environments classify as Sandbox; stored Production classifies as Live. The authenticated connection determines the stored environment. A caller's mode flag, workspace name, or application hostname does not change classification.

recordedCount equals the two mode counts plus unclassifiedCount. The historical aggregate is frozen when accounting begins and remains unclassified. New mode counters are reconciled against that independent baseline, so a missing increment cannot be mistaken for old usage. startedAt is the accounting epoch, not the time of your first historical observation. coverage is not_started when no epoch exists, partial when the month is not fully classified, and complete when accounting covers the full month with no unclassified usage.

The counters survive evidence retention and connection deletion. They report the authenticated workspace only. Account-level remaining allowances require a separate account usage credential. This endpoint exposes no other workspace and reports no pooled remaining allowance. The console's Test mode switch selects existing testing or Live environments and shows a testing banner. Existing-workspace conversion still requires reviewed migration. Sandbox enforcement stays off until the published grace period ends. Live plan usage and remaining allowance are informational.

Errors

StatusBodyMeaning
400{"error":"invalid_observation_filter"}A filter is malformed or outside its allowed range
400{"error":"invalid_observation_cursor"}The opaque pagination cursor is invalid
401{"error":"unauthenticated"}The key is missing, malformed, expired, or revoked
403{"error":"insufficient_scope"}The key lacks observations:read
403{"error":"integration_scope_mismatch"}Setup OAuth is missing the requested Integration
404{"error":"observation_not_found"}The observation is absent from this workspace
404{"error":"workspace_not_found"}The authenticated workspace no longer exists
503{"error":"mode_usage_inconsistent"}Shadow counters or their epoch do not reconcile
503{"error":"mode_accounting_paused"}Capture is paused pending reviewed reconciliation

If mode_usage_inconsistent occurs, retain the response and contact support with the workspace and request time. Do not use an inconsistent response to change allowances. Retry transient failures; persistent disagreement requires an accounting investigation.

If mode_accounting_paused occurs, contact support. Mode counts remain unavailable until capture and accounting have been reviewed. The existing GET /v1/observations/usage route continues reporting the original workspace ledger.

Retention and billing

Evidence retention and usage accounting are separate. Expiring an old observation removes its detail row and evidence links, but does not reduce the historical usage total for that billing period. Seamward does not automatically charge an overage when the displayed plan limit is reached.

Next steps