← Export Contract
API
Tokens

Run Export Contract from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page ranks the camp and the cadence, diffs this batch's schema against the last one, and checks the layout, the credentials, the cost split and residency, then hands it all to the run as facts. The run writes the export design or the data contract; the page then holds it to its own facts (ecreply.js).

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"export-contract"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, export, schema, facts) is missing. The page always sends all five of task, export, schema, carry, facts; carry is an empty string for design.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - the design the data-platform team builds, then the contract consumers read:

A missing or unknown task is answered as design. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the design's decisions into carry for the contract lane.

The input

FieldRequiredWhat
taskyesdesign or contract. If it is missing or neither, do design and set "task": "design".
exportyesthe product, the data and its consumers, the delivery today, the layout, the access model, the cost split, the regions as region | customers there | posture, and the interview's answers; "not given" means not given.
schemayesthe last batch's columns, this batch's columns (one per line, name type, NOT NULL where it applies), the page's diff as change | column | detail, and the issues in fix order.
carrysend it (empty for design)contract task only: the design's decisions, when the design task ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, and the ranked ladders. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @samber/bulk-data-sharing-design - the only standard the run applies:

House rules

The replies

design

Write the export design the data-platform team builds, in the skill's order.

  1. summary - one sentence, at most 40 words: the camp, the cadence, and the first thing to fix.
  2. camp - rung (share, filedrop or streaming, exactly as the facts rank it), second (the second surface to ship, or "none"), why (one sentence).
  3. layout - partitions (the partition path, two or three low-cardinality keys), files (one sentence: target size and compaction), catalog (one sentence).
  4. cadence - rung (full, incremental, cdc or streaming, exactly as the facts rank it), posture (fixed, besteffort or sla, as the must line gives it), promotion (one sentence: what would move it up the ladder, or how often to run it).
  5. security - credentials, isolation, encryption (one sentence each).
  6. cost - exactly three objects in this order: layer (storage, compute, egress) and payer (customer or provider), plus note (one sentence).
  7. residency - one object per region in the must line, in its order: region, posture (pinned, mechanism, notoffered or none, exactly as the must line gives it), note (one sentence).
  8. pricing - one sentence: the paid tier or add-on the export sits behind.
  9. rollout - at least three objects in order: stage (a short name), what (one sentence).
  10. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "design",
  "summary": "one sentence",
  "camp": { "rung": "filedrop", "second": "none", "why": "one sentence" },
  "layout": { "partitions": "event_date / tenant", "files": "one sentence", "catalog": "one sentence" },
  "cadence": { "rung": "full", "posture": "fixed", "promotion": "one sentence" },
  "security": { "credentials": "one sentence", "isolation": "one sentence", "encryption": "one sentence" },
  "cost": [ { "layer": "storage", "payer": "customer", "note": "one sentence" } ],
  "residency": [ { "region": "EU", "posture": "pinned", "note": "one sentence" } ],
  "pricing": "one sentence",
  "rollout": [ { "stage": "Stage 1", "what": "one sentence" } ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in ecreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; a summary of at most 40 words; no figure the input never gives; the ranked camp (with the file-drop second when both ship) and cadence; a named posture; a partition path with no high-cardinality key and at most three levels; per-recipient credentials; cost rows storage, compute, egress, each with a payer; every region with the posture the facts give; a paid gate; at least three rollout stages.

contract

Write the data contract the export publishes to its consumers - plain and exact.

  1. summary - one sentence, at most 40 words: what the contract commits to.
  2. schema - one object per column of this batch, in order: column, type, nullable (yes or no), description (one short phrase drawn from the column name and the input; "to be described" when nothing supports one).
  3. changes - one object per change in the page's diff, in its order: change (the diff's change id), column, handling (one or two sentences: for a breaking change, the old shape kept in parallel through a dated deprecation window and the new column added; for an addition, that it is appended and nullable).
  4. freshness - one sentence naming the posture and the delivery interval exactly as the must line gives them.
  5. quality - one sentence: the completeness commitment, "to be set" where the input gives no figure.
  6. availability - one sentence, "to be set" where the input gives no figure.
  7. evolution - two sentences: the add-only rule, and how a breaking change is announced.
  8. caveats - at most 4 short strings for the producing team; [] when there is nothing to say.
{
  "task": "contract",
  "summary": "one sentence",
  "schema": [ { "column": "event_id", "type": "string", "nullable": "no", "description": "a short phrase" } ],
  "changes": [ { "change": "add", "column": "session_ref", "handling": "one or two sentences" } ],
  "freshness": "one sentence",
  "quality": "one sentence",
  "availability": "one sentence",
  "evolution": "two sentences",
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; exactly this batch's columns in order with their types and nullability; exactly the page's diff in order, each breaking change handled with the old shape in parallel through a dated deprecation window; freshness naming the posture and the interval; quality and availability stated; the add-only evolution rule.

Worked examples

Three runs from the page's free samples (invented exports): the design for a nightly CSV dump that broke its schema, the contract for the same export carrying that design, and the design for a CDC feed with phantom deletes. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

design - A nightly CSV dump that broke its schema

The page read it as: this batch breaks customer pipelines; camp filedrop; cadence full; diff rename status, retype weight_kg, insert service_level; 1 of 5 gates; failing checks: posture, drift, validate, partitions, bytes, credentials, cost, residency, contractGate, schemaGate, costGate, residencyGate.

Input (schema is 2044 characters; the first 1393 are shown; facts is 3823 characters; the first 1399 are shown):

{
 "task": "design",
 "export": "Product: Tarnwhistle\nWhat the export carries and for whom: A logistics platform; enterprise shippers want their shipment and tracking records in their own analytics.\nData shape: mutable records with updates and deletes; target accounts on one shareable warehouse: 12%; bucket-only consumers: yes\nFreshness customers asked for: 24 hours; delivery today: Scheduled full dump every 24 hours; freshness posture: Not stated; CDC delete reconciliation: no; team has run CDC before: no\nLayout: csv, partitions user_id / day, average file 6 MB, compaction no, catalog no, batches validated against the contract no\nAccess: credentials shared\nbytes proxied through our servers yes; customer-managed keys no\nCost: storage paid by customer, compute by customer, egress by unstated; cross-region delivery yes; pricing: An enterprise-tier gate\nRegions (region | customers there | posture):\n- United States | yes | Region-pinned\n- EU | yes | Nothing documented\nDeadline: no; a compounding platform surface; effort ceiling: medium",
 "schema": "Last batch (6 columns):\nshipment_id string NOT NULL\naccount_id string NOT NULL\nstatus string\ncarrier string\nweight_kg float32\ncreated_at timestamp NOT NULL\nThis batch (7 columns):\nshipment_id string NOT NULL\naccount_id string NOT NULL\nstatus_code string\ncarrier string\nservice_level string\nweight_kg decimal(10,2)\ncreated_at timestamp NOT NULL\nDiff (change | column | detail):\n- rename | status | status is now status_code\n- retype | weight_kg | weight_kg float32 to decimal(10,2)\n- insert | service_level | service_level (string) added at position 5, before existing columns\nIssues in fix order: drift - status is now status_code; weight_kg float32 to decimal(10,2) - these break consumers that read by name and type; ship the new shape as a new column and keep the old one in parallel through a dated deprecation window; additive - service_level (string) added at position 5, before existing columns - append new nullable columns at the end and keep the order; validate - nothing checks a written batch against the documented schema - drift reaches customers' dashboards before your logs; posture - say which: a fixed cadence, a best-effort target or a contractual SLA - silence reads as best-effort to the customer and no promise to you; files - files average 6 MB - the contract targets 256-512 MB; no compaction - small files measured a 4x query penalty; partitions - high-cardinality key\n\n[... 651 more characters cut from this display ...]",
 "carry": "",
 "facts": "The page's checks (12 fail, 3 warn, 2 pass, 3 n/a); verdict: this batch breaks customer pipelines:\n- The camp and the cadence | camp | na | Parquet file-drop on object storage > Native warehouse or lake share > Real-time streaming push - 12% on one warehouse platform - below the 30-40% where a native share leads (a self-set threshold); the file-drop leads\n- The camp and the cadence | cadence | na | Scheduled full dump > Micro-batch incremental > Log-based CDC > Streaming push - the full dump's interval meets the freshness customers asked for - stay on the simplest rung\n- The camp and the cadence | posture | fail | say which: a fixed cadence, a best-effort target or a contractual SLA - silence reads as best-effort to the customer and no promise to you\n- The camp and the cadence | deletes | na | not on CDC\n- The schema contract | drift | fail | status is now status_code; weight_kg float32 to decimal(10,2) - these break consumers that read by name and type; ship the new shape as a new column and keep the old one in parallel through a dated deprecation window\n- The schema contract | additive | warn | service_level (string) added at position 5, before existing columns - append new nullable columns at the end and keep the order\n- The schema contract | validate | fail | nothing checks a written batch against the documented schema - drift reaches customers' dashboards before your logs\n\n[... 2424 more characters cut from this display ...]"
}

Reply:

{
 "task": "design",
 "summary": "Ship Tarnwhistle's export as a Parquet file-drop on a scheduled full dump every 24 hours, and first restore status and weight_kg in parallel with their new shapes so this batch stops breaking shipper pipelines.",
 "camp": {
  "rung": "filedrop",
  "second": "none",
  "why": "Only 12% of target accounts sit on one shareable warehouse and bucket-only consumers exist, so partitioned Parquet on object storage leads and a native share waits until that concentration grows."
 },
 "layout": {
  "partitions": "day / account",
  "files": "Switch from CSV to Parquet and compact each partition from today's 6 MB average up to files of roughly 256-512 MB.",
  "catalog": "Register every schema version in a catalog so shippers read declared columns and types instead of inferring them from files."
 },
 "cadence": {
  "rung": "full",
  "posture": "fixed",
  "promotion": "Keep the full dump every 24 hours, which already meets the 24 hours shippers asked for, and consider incremental only if the dump's cost or a tighter customer freshness need forces it."
 },
 "security": {
  "credentials": "Replace the one shared key with per-recipient scoped temporary credentials or signed URLs, with access review pinned to each credential's lifetime.",
  "isolation": "Stop proxying downloads through the API servers and let each shipper read only its own account_id prefix straight from object storage.",
  "encryption": "Offer customer-managed keys, regionalized so each region's data is encrypted under keys held in that region."
 },
 "cost": [
  {
   "layer": "storage",
   "payer": "customer",
   "note": "Shippers already pay for storage of their export, and the contract keeps that in writing."
  },
  {
   "layer": "compute",
   "payer": "customer",
   "note": "Shippers already pay for compute, including the Parquet writes and compaction behind their export."
  },
  {
   "layer": "egress",
   "payer": "customer",
   "note": "Egress, including the cross-region delivery case, is named as a customer cost in the enterprise-tier terms, with the rate to be set."
  }
 ],
 "residency": [
  {
   "region": "United States",
   "posture": "pinned",
   "note": "Keep United States data written, stored and keyed in its own region as it is today."
  },
  {
   "region": "EU",
   "posture": "pinned",
   "note": "Pin EU shippers' exports to an EU bucket with EU-held keys before go-live, and document it, since nothing is documented today."
  }
 ],
 "pricing": "The export stays behind the enterprise-tier gate, with no free-tier access.",
 "rollout": [
  {
   "stage": "Fix the schema",
   "what": "Restore status and weight_kg as float32 in their old positions, add status_code and the decimal(10,2) weight as new nullable columns at the end, move service_level to the end, and announce a dated removal for the old columns."
  },
  {
   "stage": "Validate batches",
   "what": "Check every written batch against the documented contract before it counts as delivered, and publish the fixed 24-hour cadence as the freshness posture."
  },
  {
   "stage": "Re-lay files",
   "what": "Write Parquet under day / account, keyed by each shipper's account_id, compact to roughly 256-512 MB, and register the schema in a catalog."
  },
  {
   "stage": "Lock down access",
   "what": "Issue per-recipient credentials, vend signed URLs instead of proxying bytes, and offer customer-managed keys."
  },
  {
   "stage": "Pin the EU",
   "what": "Stand up the EU region-pinned path and put the egress terms, including cross-region delivery, in writing before EU shippers are switched on."
  }
 ],
 "caveats": [
  "The records are mutable with deletes, so a full dump stays correct only while each batch is a complete snapshot.",
  "The deprecation window length for status and the float32 weight_kg is to be set."
 ]
}

What step 5 prints for it: filedrop | full | fixed

The page's check: kept - nothing to flag

contract - A nightly CSV dump that broke its schema

The input carries the saved design (R.laneForm fills carry from the design reply), as the page's handoff does. The issues in fix order: drift, additive, validate, posture, files, partitions, format, catalog, proxied, credentials, cost, residency.

Input (schema is 2044 characters; the first 1393 are shown; facts is 3959 characters; the first 1399 are shown):

{
 "task": "contract",
 "export": "Product: Tarnwhistle\nWhat the export carries and for whom: A logistics platform; enterprise shippers want their shipment and tracking records in their own analytics.\nData shape: mutable records with updates and deletes; target accounts on one shareable warehouse: 12%; bucket-only consumers: yes\nFreshness customers asked for: 24 hours; delivery today: Scheduled full dump every 24 hours; freshness posture: Not stated; CDC delete reconciliation: no; team has run CDC before: no\nLayout: csv, partitions user_id / day, average file 6 MB, compaction no, catalog no, batches validated against the contract no\nAccess: credentials shared\nbytes proxied through our servers yes; customer-managed keys no\nCost: storage paid by customer, compute by customer, egress by unstated; cross-region delivery yes; pricing: An enterprise-tier gate\nRegions (region | customers there | posture):\n- United States | yes | Region-pinned\n- EU | yes | Nothing documented\nDeadline: no; a compounding platform surface; effort ceiling: medium",
 "schema": "Last batch (6 columns):\nshipment_id string NOT NULL\naccount_id string NOT NULL\nstatus string\ncarrier string\nweight_kg float32\ncreated_at timestamp NOT NULL\nThis batch (7 columns):\nshipment_id string NOT NULL\naccount_id string NOT NULL\nstatus_code string\ncarrier string\nservice_level string\nweight_kg decimal(10,2)\ncreated_at timestamp NOT NULL\nDiff (change | column | detail):\n- rename | status | status is now status_code\n- retype | weight_kg | weight_kg float32 to decimal(10,2)\n- insert | service_level | service_level (string) added at position 5, before existing columns\nIssues in fix order: drift - status is now status_code; weight_kg float32 to decimal(10,2) - these break consumers that read by name and type; ship the new shape as a new column and keep the old one in parallel through a dated deprecation window; additive - service_level (string) added at position 5, before existing columns - append new nullable columns at the end and keep the order; validate - nothing checks a written batch against the documented schema - drift reaches customers' dashboards before your logs; posture - say which: a fixed cadence, a best-effort target or a contractual SLA - silence reads as best-effort to the customer and no promise to you; files - files average 6 MB - the contract targets 256-512 MB; no compaction - small files measured a 4x query penalty; partitions - high-cardinality key\n\n[... 651 more characters cut from this display ...]",
 "carry": "Camp: filedrop - Only 12% of target accounts sit on one shareable warehouse and bucket-only consumers exist, so partitioned Parquet on object storage leads and a native share waits until that concentration grows.\nLayout: day / account; Switch from CSV to Parquet and compact each partition from today's 6 MB average up to files of roughly 256-512 MB.\nCadence: full, posture fixed - Keep the full dump every 24 hours, which already meets the 24 hours shippers asked for, and consider incremental only if the dump's cost or a tighter customer freshness need forces it.\nCost: storage customer, compute customer, egress customer\nResidency: United States pinned, EU pinned\nPricing: The export stays behind the enterprise-tier gate, with no free-tier access.",
 "facts": "The page's checks (12 fail, 3 warn, 2 pass, 3 n/a); verdict: this batch breaks customer pipelines:\n- The camp and the cadence | camp | na | Parquet file-drop on object storage > Native warehouse or lake share > Real-time streaming push - 12% on one warehouse platform - below the 30-40% where a native share leads (a self-set threshold); the file-drop leads\n- The camp and the cadence | cadence | na | Scheduled full dump > Micro-batch incremental > Log-based CDC > Streaming push - the full dump's interval meets the freshness customers asked for - stay on the simplest rung\n- The camp and the cadence | posture | fail | say which: a fixed cadence, a best-effort target or a contractual SLA - silence reads as best-effort to the customer and no promise to you\n- The camp and the cadence | deletes | na | not on CDC\n- The schema contract | drift | fail | status is now status_code; weight_kg float32 to decimal(10,2) - these break consumers that read by name and type; ship the new shape as a new column and keep the old one in parallel through a dated deprecation window\n- The schema contract | additive | warn | service_level (string) added at position 5, before existing columns - append new nullable columns at the end and keep the order\n- The schema contract | validate | fail | nothing checks a written batch against the documented schema - drift reaches customers' dashboards before your logs\n\n[... 2560 more characters cut from this display ...]"
}

Reply (long strings shortened for display - see the cuts below):

{
 "task": "contract",
 "summary": "Tarnwhistle delivers each shipper's shipment records as a full dump on a fixed 24-hour cadence, in the seven columns below, with status and the float32 weight_kg kept in parallel through a dated deprecation window.",
 "schema": [
  {
   "column": "shipment_id",
   "type": "string",
   "nullable": "no",
   "description": "identifier of the shipment"
  },
  {
   "column": "account_id",
   "type": "string",
   "nullable": "no",
   "description": "shipper account that owns the shipment"
  },
  {
   "column": "status_code",
   "type": "string",
   "nullable": "yes",
   "description": "shipment status, replacing status"
  },
  {
   "column": "carrier",
   "type": "string",
   "nullable": "yes",
   "description": "carrier handling the shipment"
  },
  {
   "column": "service_level",
   "type": "string",
   "nullable": "yes",
   "description": "service level of the shipment"
  },
  {
   "column": "weight_kg",
   "type": "decimal(10,2)",
   "nullable": "yes",
   "description": "shipment weight in kilograms"
  },
  {
   "column": "created_at",
   "type": "timestamp",
   "nullable": "no",
   "description": "when the shipment record was created"
  }
 ],
 "changes": [
  {
   "change": "rename",
   "column": "status",
   "handling": "status stays in every batch in its old position alongside the new status_code column through a dated deprecation window. status is removed only on the announced date, which is to be set."
  },
  {
   "change": "retype",
   "column": "weight_kg",
   "handling": "weight_kg stays as float32 in parallel through a dated deprecation window, while the decimal(10,2) weight ships as a new nullable column appended at the end. The float32 column is removed only on the announced date, which is to be set."
  },
  {
   "change": "insert",
   "column": "service_level",
   "handling": "service_level is appended as a nullable column at the end of the batch rather than at position 5, so existing columns keep their order."
  }
 ],
 "freshness": "Freshness is a fixed cadence: a complete full dump is delivered every 24 hours.",
 "quality": "Every batch is checked against this contract before it counts as delivered and holds every shipment record for the shipper's account, with a completeness figure to be set.",
 "availability": "Availability of delivered files is to be set.",
 "evolution": "Columns are only ever added, as new nullable columns appended at the end; no column is renamed, removed or retyped in place, and column order and types stay the same across batches. A breaking change is announced in advance with a dated [... 105 more characters cut from this display]",
 "caveats": [
  "This batch moved status to status_code, retyped weight_kg and inserted service_level mid-row; republish it in the contracted shape.",
  "Nothing validates batches against the contract yet; add that check before publishing this contract.",
  "The deprecation dates and the quality and availability figures are to be set."
 ]
}

Cut from this display only (the saved reply and the check use the full text): evolution (341 characters, 236 shown).

What step 5 prints for it: rename status | retype weight_kg | insert service_level

The page's check: kept - nothing to flag

design - CDC with phantom deletes, given away free

The page read it as: this batch breaks customer pipelines; camp filedrop; cadence cdc; diff tighten status; 2 of 5 gates; failing checks: cadence, deletes, drift, residency, pricing, schemaGate, residencyGate, premiumGate.

Input (facts is 3172 characters; the first 1395 are shown):

{
 "task": "design",
 "export": "Product: Fennoledger\nWhat the export carries and for whom: A subscription-billing platform; finance teams reconcile invoices and payments in their own lake.\nData shape: mutable records with updates and deletes; target accounts on one shareable warehouse: 0%; bucket-only consumers: yes\nFreshness customers asked for: 1 hour; delivery today: Log-based CDC every 6 hours; freshness posture: Fixed cadence; CDC delete reconciliation: no; team has run CDC before: yes\nLayout: parquet, partitions invoice_month, average file 300 MB, compaction yes, catalog yes, batches validated against the contract yes\nAccess: credentials broker-minted, short-term\nbytes proxied through our servers no; customer-managed keys no\nCost: storage paid by customer, compute by customer, egress by customer; cross-region delivery no; pricing: Free on every plan\nRegions (region | customers there | posture):\n- United States | yes | Region-pinned\n- EU | yes | Legal mechanism documented (SCC/TIA)\n- India | yes | Legal mechanism documented (SCC/TIA) (localization mandate)\nDeadline: no; a compounding platform surface; effort ceiling: medium",
 "schema": "Last batch (6 columns):\ninvoice_id string NOT NULL\ncustomer_id string NOT NULL\namount_minor int64 NOT NULL\ncurrency string NOT NULL\nstatus string\nissued_at timestamp NOT NULL\nThis batch (6 columns):\ninvoice_id string NOT NULL\ncustomer_id string NOT NULL\namount_minor int64 NOT NULL\ncurrency string NOT NULL\nstatus string NOT NULL\nissued_at timestamp NOT NULL\nDiff (change | column | detail):\n- tighten | status | status no longer accepts nulls\nIssues in fix order: drift - status no longer accepts nulls - this breaks consumers that read by name and type; ship the new shape as a new column and keep the old one in parallel through a dated deprecation window; cadence - log-based cdc every 6 hours cannot deliver data 1 hour old - run it at least every 1 hour; deletes - CDC with no delete tombstones and no periodic full-scan reconciliation - deletes that never emit an event become phantom rows in every customer's copy; residency - India (a localization mandate - pin it or do not offer it); free - every vendor studied gates bulk export behind a paid tier or add-on - margin a later repricing cannot easily claw back",
 "carry": "",
 "facts": "The page's checks (8 fail, 0 warn, 11 pass, 1 n/a); verdict: this batch breaks customer pipelines:\n- The camp and the cadence | camp | na | Parquet file-drop on object storage > Real-time streaming push - no target account runs a shareable warehouse - the native share is deleted, not demoted; it comes back when accounts concentrate on one platform\n- The camp and the cadence | cadence | fail | Log-based CDC > Scheduled full dump > Micro-batch incremental > Streaming push - customers asked for data 1 hour old - keep log-based cdc but run it at least every 1 hour instead of every 6 hours; a team that has run log-based CDC before buys that rung far cheaper than the ladder assumes\n- The camp and the cadence | posture | pass | Fixed cadence, every 6 hours\n- The camp and the cadence | deletes | fail | CDC with no delete tombstones and no periodic full-scan reconciliation - deletes that never emit an event become phantom rows in every customer's copy\n- The schema contract | drift | fail | status no longer accepts nulls - this breaks consumers that read by name and type; ship the new shape as a new column and keep the old one in parallel through a dated deprecation window\n- The schema contract | additive | pass | no new columns\n- The schema contract | validate | pass | a written batch counts only once it matches the documented schema\n- The file layout | files | pass | 300 MB files,\n\n[... 1777 more characters cut from this display ...]"
}

Reply:

{
 "task": "design",
 "summary": "Ship a partitioned Parquet file-drop fed by log-based CDC on a fixed cadence, and first restore the status column's old nullable shape beside a new non-null column before this batch reaches any customer.",
 "camp": {
  "rung": "filedrop",
  "second": "none",
  "why": "No target account runs a shareable warehouse and finance teams read invoices into their own lake, so the native share is dropped and Parquet on object storage is the only surface."
 },
 "layout": {
  "partitions": "tenant / invoice_month",
  "files": "Keep the current 300 MB Parquet files, inside the 256-512 MB target, and keep compacting small CDC writes into them.",
  "catalog": "Keep every batch registered in the catalog and validated against the published contract before it counts as delivered."
 },
 "cadence": {
  "rung": "cdc",
  "posture": "fixed",
  "promotion": "Keep log-based CDC but run it at least every hour instead of every 6 hours, since customers asked for data 1 hour old and the team has run CDC before."
 },
 "security": {
  "credentials": "Keep broker-minted short-term credentials, minted per recipient and scoped to that recipient's tenant prefix, with access review tied to their lifetime.",
  "isolation": "Recipients read storage directly with their own scoped credentials, and bytes never pass through Fennoledger servers.",
  "encryption": "Add customer-managed keys, held per region, so each finance team can revoke its own copy."
 },
 "cost": [
  {
   "layer": "storage",
   "payer": "customer",
   "note": "The customer pays to store the delivered Parquet files, as agreed today."
  },
  {
   "layer": "compute",
   "payer": "customer",
   "note": "The customer pays the compute to run the CDC jobs and compaction, as agreed today."
  },
  {
   "layer": "egress",
   "payer": "customer",
   "note": "The customer pays egress, and because cross-region delivery is not offered there is no cross-region charge to split."
  }
 ],
 "residency": [
  {
   "region": "United States",
   "posture": "pinned",
   "note": "Keep United States data and its keys in a United States region."
  },
  {
   "region": "EU",
   "posture": "mechanism",
   "note": "Keep the documented SCC/TIA mechanism on file and move EU data into a pinned region as soon as one is available."
  },
  {
   "region": "India",
   "posture": "notoffered",
   "note": "India carries a localization mandate, so the export is not offered there until data and keys can be pinned inside India."
  }
 ],
 "pricing": "Put bulk export behind a paid tier or add-on instead of every plan, with the price to be set, before more accounts come to depend on it.",
 "rollout": [
  {
   "stage": "Fix the schema",
   "what": "Hold this batch, keep status nullable in its old shape, and add the non-null version as a new column appended at the end with a dated deprecation window for the old one."
  },
  {
   "stage": "Fix deletes",
   "what": "Emit explicit delete tombstones from the CDC log, dedup by invoice_id on upsert, and run a periodic full-scan reconciliation so no deleted invoice survives as a phantom row."
  },
  {
   "stage": "Raise cadence",
   "what": "Run the CDC job at least every hour and publish the fixed cadence as the freshness posture."
  },
  {
   "stage": "Close residency and pricing",
   "what": "Withdraw the export in India, add customer-managed regional keys, and gate the export behind a paid tier or add-on."
  }
 ],
 "caveats": [
  "Changing the published cadence from every 6 hours to every hour must be announced to current recipients.",
  "The reconciliation interval and the deprecation window length are to be set."
 ]
}

What step 5 prints for it: filedrop | cdc | fixed

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./eckit.js"); require("./ecreply.js")), then:

const K = ExportKit, R = ExportReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", credentials: "shared",
  prev: "id string not null\nstatus string", next: "id string not null\nstate string" }));
// f.campR, f.cadenceR (the ranked ladders), f.diff (the schema changes), f.partitionKeys, f.regionRows, f.issues (in fix order), f.gate
// K.schemaMd(f), K.diffCsv(f) - the schema section and the diff
const b = K.buildInput(f, "design");                         // or "contract" (R.laneForm(form, "contract", design) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "design");
// R.check(reply, b)                                         // {status: kept or flagged, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://export-contract.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"export-contract"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "design", "export": "Product: Tarnwhistle
What the export carries and for whom: ...", "schema": "Last batch (6 columns):
shipment_id string NOT NULL ...", "carry": "", "facts": "The page's checks (...); verdict: this batch breaks customer pipelines:
- The camp and the cadence | camp | na | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses export-contract:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A design reply carries summary, camp, layout, cadence, security, cost, residency, pricing, rollout and caveats; a contract reply carries summary, schema, changes, freshness, quality, availability, evolution and caveats. The code below prints the gist of either: each cost layer and its payer, or each schema change.

KEY="export-contract:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "contract" then (.changes[] | "\(.change): \(.column)") 
       else (.cost[] | "\(.layer): \(.payer)") end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: export-contract:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json