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.

Authentication and scopes

Workspace automation sends a workspace API key:

Code example

Authorization: Bearer $SEAMWARD_API_KEY

Use contracts:read for listing and detail, 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/json

OpenAPI 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"  }}

Registration is safe to retry only while the existing version is still a draft and the retry is identical. 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/json

Code 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.

Errors

StatusBodyMeaning
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
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
503{"error":"service_unavailable"}Reanalysis is unavailable, so activation was not attempted

Next steps