Skip to main content

Governed Read Contract

Fetch or publish the versioned read contract that lets Apps and cross-pipeline rules consume a data product through a frozen field shape and record identity.

A data product does not become a machine-consumable source merely because it exists. Cross-pipeline consumers — Business Rules and Apps — read a product only through its governed read contract: a versioned snapshot that freezes the served field shape, the declared record identity, the permitted match-key sets, and the serve representation. The contract is what makes a read reproducible and auditable: consumers pin a contract version, and a later shape change creates a new version rather than silently retargeting them.

Two endpoints manage the contract. GET /v1/data-products/{id}/read-contract returns the currently active version, and POST /v1/data-products/{id}/read-contract/publish publishes one from the product's currently served shape. Publishing is idempotent on shape: if the active contract already matches the served fields, the same version is returned unchanged; if the shape drifted, the old version is superseded (its supersededAt is stamped) and the next contractVersion is minted.

Every contract declares exactly one record identity. By default it is derived from the product schema's field tags (record_key, business_key, or unique). When the schema carries no such tag — common for spec-built products that cannot be re-tagged through the API — declare the identity in the publish body: either record_key_fields (1-8 served, non-restricted field names) or record_key_anchored: true, which identifies rows by the record anchor itself, the only option for a per-document product with no unique served field. Declaring both is rejected with 400: a contract has one record identity.

Uniqueness is never caller-asserted. The declared key is validated against the product's actual rows, and a key that is missing values or not unique within the publication fails the publish with 422. The same 422 with a machine-readable code covers every other reason the product cannot be frozen — an incomplete publication, an empty served schema, or a product that is not an active pipeline-backed source.

GET/v1/data-products/{id}/read-contract

Path parameters

id*uuidData product UUID.

Contract Response

Response fields

dataProductIdstringUUID of the data product the contract governs.
workspaceIdstringOwning workspace (organization) UUID.
contractVersionstringMonotonically increasing version number, as a string.
schemaHashstringHash of the frozen served field shape. A different hash on republish means the shape drifted.
servePathIdstringIdentifier of the read path consumers resolve through.
serveRepresentationVersionstringVersion of the serve representation the contract pins.
fieldsarrayFrozen served fields: `fieldId`, `name`, `type`, `nullable`, `sensitivity`.
recordKeyFieldIdsstring[]Field IDs forming the record identity. Empty when the contract is anchor-keyed.
recordKeyUniqueWithinPublicationbooleanWhether the record key was verified unique within the pinned publication.
recordKeyAnchoredbooleanTrue when identity is the record anchor rather than served fields. An anchored key is never a permitted match key.
permittedMatchKeySetsstring[][]Field-ID sets that relation rules may join on.
publicationRetentionobject`minimumDays`, `historicalPublicationsQueryable`, `retentionHoldSupported`.
publishedAtstringISO 8601 timestamp the contract version was published.
supersededAtstring | nullWhen a newer version replaced this one; null for the active version.

curl

curl -s https://api.talonic.com/v1/data-products/6ecb46fa-24ba-4b5d-93ee-b8b6c6b97655/read-contract \
  -H "Authorization: Bearer tlnc_your_api_key"

Response

{
  "dataProductId": "6ecb46fa-24ba-4b5d-93ee-b8b6c6b97655",
  "workspaceId": "0b54c3e1-9a4d-4f0e-8b2a-7c6d5e4f3a21",
  "contractVersion": "2",
  "schemaHash": "sha256:9f8e7d6c5b4a",
  "servePathId": "pipeline-output-reader",
  "serveRepresentationVersion": "1",
  "fields": [
    { "fieldId": "load_number", "name": "load_number", "type": "string", "nullable": false, "sensitivity": "internal" },
    { "fieldId": "carrier_name", "name": "carrier_name", "type": "string", "nullable": true, "sensitivity": "internal" }
  ],
  "recordKeyFieldIds": ["load_number"],
  "recordKeyUniqueWithinPublication": true,
  "recordKeyAnchored": false,
  "permittedMatchKeySets": [["load_number"]],
  "publicationRetention": {
    "minimumDays": 0,
    "historicalPublicationsQueryable": false,
    "retentionHoldSupported": true
  },
  "publishedAt": "2026-08-10T09:00:00.000Z",
  "supersededAt": null
}
Read-contract responses use camelCase field names — an exception to the snake_case convention of the rest of the public API. The contract payload is shared verbatim with the internal rule engine, which pins these exact keys in stored source snapshots.

Publish a Contract Version

Publish (or refresh) the contract from the product's currently served shape. The endpoint is write-scoped: the same authority that can create products can freeze the contract consumers read through, which is what makes a freshly built product consumable by Apps without a manual relay. The body is optional — send it only to declare the record identity when the schema carries no key tag.

POST/v1/data-products/{id}/read-contract/publish

Body parameters

record_key_fieldsstring[]Served, non-restricted field names that identify a record (1-8 entries). Mutually exclusive with record_key_anchored.
record_key_anchoredbooleanIdentify records by the record anchor instead of served fields — the only option for a per-document product. Mutually exclusive with record_key_fields.

curl

curl -s -X POST https://api.talonic.com/v1/data-products/6ecb46fa-24ba-4b5d-93ee-b8b6c6b97655/read-contract/publish \
  -H "Authorization: Bearer tlnc_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "record_key_fields": ["load_number"] }'

Errors

Error responses

400bad_requestBoth record_key_fields and record_key_anchored declared — a contract has one record identity.
401unauthorizedMissing or invalid API key.
404not_foundProduct not found, or (on GET) no active contract — the message points you at the publish endpoint.
422unprocessableThe product cannot be frozen. The body carries a machine-readable `code` such as `publication_incomplete`, `source_schema_empty`, `source_record_key_unconfigured`, or `source_record_key_not_unique`.
429rate_limitedToo many requests. Retry after the period indicated in the Retry-After header.
503unavailableRead contracts are not available in this deployment.

Frequently asked questions

When do I need to publish a read contract?+
Whenever another surface should consume the product programmatically — an App reading it as input, or a cross-pipeline Business Rule joining against it. Without an active contract, those consumers refuse the product and `GET /read-contract` returns 404 with guidance.
What happens when the product's schema changes after publishing?+
Nothing, until you publish again. The active contract keeps serving the frozen shape it pinned. Republishing with a drifted shape supersedes the old version and mints the next `contractVersion`; consumers referencing the old version see it as stale rather than being silently retargeted.
When should I use record_key_anchored instead of record_key_fields?+
Use `record_key_anchored: true` for a per-document product, where no served field is unique per row. The record anchor is unique by construction but local to the publication, so an anchored contract exposes no permitted match-key sets — relation rules cannot join on it.
Why does publish return 422 with source_record_key_not_unique?+
The declared (or tag-derived) record key was checked against the product's actual rows and at least two rows share a key value. Uniqueness is never caller-asserted — fix the data or declare a different identity, then publish again.