Contracts
The Contract API imports operation-scoped schemas, preserves every version as immutable evidence, and changes analysis only after explicit activation. For an HTTP API Integration, OpenAPI imports supported JSON request and response operations from paths. For an HTTP webhook Integration, it imports supported message operations from webhooks. A standalone JSON Schema must be bound to one explicit operation selector.
The legacy browser-session POST /contracts writer is retired. Every otherwise valid, owned request returns 409 canonical_lifecycle_required without changing contracts, activation history or audit. It retains deprecation headers until removal after 30 November 2026. Use the public reviewed draft-registration and activation routes documented below. Existing collector-authenticated baseline reads remain available for historical contracts.
For a canonical Sandbox or Live connection, the legacy route returns 409 with
canonical_lifecycle_required and writes nothing. Register a draft through the
public API, review it, and activate it explicitly. The legacy route cannot
activate a contract in either canonical mode.
Account management can also use CLI OAuth with X-Seamward-Workspace. Its current account membership and explicit permission scopes are checked separately from SDK ingestion and project setup. Credential issuance, rotation and recovery are unavailable to CLI OAuth. Observation reads require an exact integrationId; contract paths select their exact connection.
Authentication and scopes
Workspace automation sends a workspace API key:
Code example
Authorization: Bearer $SEAMWARD_API_KEYUse contracts:read for listing, detail and drafting from traffic, contracts:write for draft registration, and contracts:activate for activation or rollback. One key may carry multiple scopes. An ingest token cannot call these routes.
The seamward CLI may also list contracts with its browser-approved setup
OAuth token carrying seamward:setup:contracts:read. That token is bound to
one environment and an approved Integration. It cannot register or activate a
contract through these public routes. Requesting another Integration returns
integration_scope_mismatch.
Copy {integrationId} from the integration's Overview tab under Connection status, using the Integration ID code block. It is a non-secret identifier. Sandbox tooling can derive the same ID from the public Connection key, so it does not need a second configuration value.
List contracts
Code example
GET /v1/integrations/{integrationId}/contracts?limit=25&cursor={opaqueCursor}The response identifies the active version and returns immutable versions in descending createdAt, then descending id, order. limit defaults to 25 and accepts 1 through 100. When nextCursor is not null, pass it unchanged as cursor to read the next page. A cursor is scoped to one integration.
Code example
{ "integration": { "id": "int_orders", "name": "Orders" }, "activeContractVersionId": "contract_v1", "contracts": [ { "id": "contract_v1", "declaredVersion": "2026-08", "fingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "importedFrom": "json-schema", "lifecycleStatus": "active", "activatedAt": "2026-08-20T12:00:00.000Z", "supersededAt": null, "createdAt": "2026-08-20T11:55:00.000Z", "operations": [ { "id": "operation_create_order_201", "operationKey": "createOrder:response:201", "direction": "outbound", "protocol": "http-api", "method": "POST", "routeTemplate": "/orders", "eventType": null, "payloadLocation": "response", "statusSelector": "201", "fingerprint": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "schema": { "type": "object", "required": ["id"] } } ] } ], "nextCursor": null}Register a draft
A reviewed automation may include expectedConnectionRevision, the opaque
string from connection metadata.
The API checks this epoch while holding the connection row lock and returns
connectionRevision from the committed draft transaction. Any intervening
configuration change returns connection_configuration_changed (409) without
writing a draft. Keep the returned epoch for the next reviewed mutation.
If a response is lost or the epoch no longer matches, inspect the current
state and review again. Do not replace the expected epoch with an arbitrary
fresh read to revive an old approval.
Code example
POST /v1/integrations/{integrationId}/contractsContent-Type: application/jsonOpenAPI example:
Code example
{ "declaredVersion": "2026-08", "expectedScope": { "direction": "outbound", "protocol": "http-api" }, "format": "openapi", "document": { "openapi": "3.1.0", "info": { "title": "Orders", "version": "2026-08" }, "paths": { "/orders": { "post": { "operationId": "createOrder", "responses": { "201": { "content": { "application/json": { "schema": { "type": "object", "required": ["id"] } } } } } } } } }}JSON Schema example:
Code example
{ "declaredVersion": "2026-08", "expectedScope": { "direction": "outbound", "protocol": "http-api" }, "format": "json-schema", "document": { "type": "object", "required": ["id"] }, "operation": { "method": "POST", "routeTemplate": "/orders", "payloadLocation": "response", "statusSelector": "201" }}Registration returns 201 with lifecycleStatus: "draft". It does not enqueue analysis or replace the active version.
expectedScope is an optimistic safety guard. Seamward returns 409 integration_scope_mismatch before writing a version or audit event when the target Integration's direction or protocol differs. The current CLI and console always send it. The field remains optional only so existing Contract API clients can migrate without a breaking change; new automation should always include it.
Code example
{ "contract": { "id": "contract_v2", "declaredVersion": "2026-09", "fingerprint": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "importedFrom": "json-schema", "lifecycleStatus": "draft", "operationCount": 1, "createdAt": "2026-08-21T09:30:00.000Z" }}Registering on one connection also gives the Integration's other mode connection the same version as a draft, in the same transaction. Activation stays separate per connection. A version label means one thing across both modes: if the other connection already holds the declaredVersion with different content, registration returns 409 contract_version_already_exists and writes nothing.
Registration is safe to retry only while the existing version is still a draft and the retry is identical. Registering the same document from the other mode's connection is such a retry: it returns the draft that connection already holds. If the same integration and declaredVersion already have a draft with the same document fingerprint, import format, operation count, and every operation selector and schema fingerprint, the API returns 201 with that existing draft. It does not create another version or audit event. A changed document, format, selector, or schema returns 409 contract_version_already_exists. An already active or superseded version is not treated as an identical draft retry.
Supported OpenAPI methods are GET, POST, PUT, PATCH, and DELETE. OpenAPI can be registered only against an HTTP API or HTTP webhook Integration. For an HTTP API Integration, Seamward imports paths operations. For an HTTP webhook Integration, it imports webhooks operations. Queue and scheduled-feed Integrations use an operation-bound JSON Schema. Imports read JSON and application/*+json schemas, local references, and the supported JSON Schema subset: type, properties, required, items, enum, and additionalProperties. Remote references, composition keywords such as allOf, oneOf, and anyOf, and non-JSON media types are not imported.
Get one version
Code example
GET /v1/integrations/{integrationId}/contracts/{contractVersionId}The response contains contract with the same immutable version shape returned by the list endpoint. A version id from another integration or workspace returns contract_not_found or integration_not_found without revealing foreign records.
Code example
{ "contract": { "id": "contract_v1", "declaredVersion": "2026-08", "fingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "importedFrom": "json-schema", "lifecycleStatus": "active", "activatedAt": "2026-08-20T12:00:00.000Z", "supersededAt": null, "createdAt": "2026-08-20T11:55:00.000Z", "operations": [ { "id": "operation_create_order_201", "operationKey": "createOrder:response:201", "direction": "outbound", "protocol": "http-api", "method": "POST", "routeTemplate": "/orders", "eventType": null, "payloadLocation": "response", "statusSelector": "201", "fingerprint": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "schema": { "type": "object", "required": ["id"] } } ] }}Activate a version
Code example
POST /v1/integrations/{integrationId}/contracts/{contractVersionId}/activateContent-Type: application/jsonCode example
{ "expectedActiveContractVersionId": "contract_v1", "reason": "promote"}Reviewed automation may also include expectedConnectionRevision. It checks
all connection configuration alongside expectedActiveContractVersionId and
returns the committed connectionRevision. Stale configuration returns
connection_configuration_changed (409), with no activation, audit or enqueue.
This opaque connection epoch is separate from the integer analysis revision.
Use null when the integration has no active version. Use reason: "rollback" when reactivating a prior immutable version. Activation supersedes the previous version, records a new revision and audit event, and enqueues reanalysis in one database transaction.
Code example
{ "activationId": "activation_contract_v2_revision_8", "activeContractVersionId": "contract_v2", "previousActiveContractVersionId": "contract_v1", "revision": 8}If another deployment changed the active version first, the request returns 409:
Code example
{ "error": "active_contract_changed", "activeContractVersionId": "contract_v3"}Read the current version, review it, and retry with a new explicit precondition. Do not retry blindly.
Draft from traffic
Code example
POST /v1/integrations/{integrationId}/contracts/draft-from-trafficContent-Type: application/jsonCode example
{ "windowDays": 7 }Requires contracts:read. The route drafts a contract for one connection from the payload shapes already observed on it and compares the draft with the active version. It saves nothing: to keep a draft, send one of the returned documents to Register a draft.
windowDays is optional, from 1 to 90, and defaults to 7. Shapes last seen inside the window are used. An empty body is valid.
Code example
{ "connection": { "id": "int_orders", "name": "Paystack webhook", "mode": "sandbox" }, "windowStart": "2026-09-30T12:00:00.000Z", "windowDays": 7, "payloads": 2104, "operations": [ { "method": "POST", "routeTemplate": "/webhooks/paystack", "eventType": "charge.success", "payloadLocation": "message", "statusSelector": null, "schema": { "type": "object", "properties": { "email": { "type": "string" }, "reference": { "type": "string" } }, "required": ["reference"] }, "evidence": { "payloads": 2104, "distinctShapes": 2, "firstObservedAt": "2026-10-01T08:00:00.000Z", "lastObservedAt": "2026-10-07T11:45:00.000Z", "requiredDecided": true, "fields": [ { "path": "$.email", "types": ["string"], "presentIn": 1980, "outOf": 2104, "required": false, "mixedTypes": false, "insideArray": false }, { "path": "$.reference", "types": ["string"], "presentIn": 2104, "outOf": 2104, "required": true, "mixedTypes": false, "insideArray": false } ] }, "comparison": { "changes": [ { "path": "$.email", "kind": "field-added", "class": "added" } ], "highest": "added", "newOperation": false } } ], "carriedOperations": [], "documents": [ { "format": "openapi", "document": { "openapi": "3.1.0", "info": { "title": "Paystack webhook", "version": "1.1.0" }, "webhooks": { "charge.success": { "post": { "x-seamward-event-type": "charge.success", "x-seamward-route-template": "/webhooks/paystack", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "email": { "type": "string" }, "reference": { "type": "string" } }, "required": ["reference"] } } } } } } } } } ], "activeVersion": { "id": "contract_v1", "declaredVersion": "1.0.0" }, "highest": "added", "proposedVersion": "1.1.0"}| Field | Meaning |
|---|---|
payloads | Payloads the draft rests on, across all operations. |
operations | One entry per operation that had traffic in the window: the drafted schema, the evidence for it, and the comparison with the active version. |
evidence.fields | Each field with its types and how many payloads had it. requiredDecided is false below 50 payloads, when nothing is newly marked required. |
comparison | null when no version is active. Each change has a class: added, loosened, tightened or retyped. newOperation is true when the active version does not describe the operation. |
openFinding | Present on a change that matches a change Seamward detected inside the window. Registering and activating that change makes the contract agree with the traffic that raised it. |
carriedOperations | Operations of the active version that had no traffic in the window. A single OpenAPI document keeps them exactly as they are, so saving it never drops a quiet operation. JSON Schema documents hold one operation each and cannot keep them: activating one replaces the version that checks these. |
documents | The drafted and carried operations in a form the registration route accepts. HTTP API operations, and webhook events that each have an event type, fit one OpenAPI document. Anything else is one JSON Schema document each. |
highest | The most significant change class across all operations, or null when the draft matches the active version. |
proposedVersion | The next label when the active version is a version number: a minor step for added and loosened, a major step for tightened and retyped. null when nothing changed, no version is active, or the label is not numeric. |
A draft describes field names and types only. Seamward reads the shape of a payload, never its values, so a draft has no enumerations and no formats.
Errors
| Status | Body | Meaning |
|---|---|---|
400 | {"error":"invalid_contract"} | The document, selector, or supported schema subset is invalid |
400 | {"error":"invalid_contract_query"} | A list limit or query parameter is invalid |
400 | {"error":"invalid_contract_cursor"} | The cursor is malformed or belongs to another integration |
400 | {"error":"invalid_contract_activation"} | The activation body or reason is invalid |
400 | {"error":"invalid_contract_draft_request"} | The draft body has an unknown field or a window outside 1 to 90 |
401 | {"error":"unauthenticated"} | The key is missing, malformed, expired, or revoked |
403 | {"error":"insufficient_scope"} | The key lacks the endpoint scope |
403 | {"error":"integration_scope_mismatch"} | Setup OAuth is missing the requested Integration |
404 | {"error":"integration_not_found"} | The integration is absent from this workspace |
404 | {"error":"contract_not_found"} | The version is absent from this integration |
409 | {"error":"contract_version_already_exists"} | The declared version exists with different content or selectors |
409 | {"error":"integration_scope_mismatch"} | The expected direction or protocol differs from the Integration |
409 | {"error":"active_contract_changed"} | The optimistic concurrency precondition is stale |
409 | {"error":"contract_not_activatable"} | The lifecycle transition is invalid |
409 | {"error":"contract_draft_traffic_too_large"} | More than 1,000 distinct payload shapes are in the draft window |
503 | {"error":"service_unavailable"} | Reanalysis is unavailable, so activation was not attempted |
Next steps
- Authentication: create and scope a workspace API key.
- Workspace API: available and planned workspace automation.
