API reference

The market-platform exposes a REST API (default base http://localhost:8080, prefix /api/v1). Authenticated endpoints take a Cognito ID token as Authorization: Bearer <jwt>; identity is derived from the token, never from the request body. See Authentication.

MCP is the supported surface. The buyer/seller flow runs over the 13 core MCP tools (enter_market, purchase, create_listing, …) — plus the three buyer-posting request tools where that surface is enabled — see the MCP tools reference. This REST + SSE surface is the advanced fallback for integrators who can't use MCP, and the only documented home for the flows that have no MCP tool: raw session creation with explicit controls, custom buyer-image push, buyer request posting and request board reads, the signed audit record, arbitrary file upload, and CSV/XLSX/parquet dataset intake.

This reference covers that advanced surface. Shapes are illustrative — fields may be added over time.

Error responses

Every error from this API is JSON:

{ "error": "safe message" }

Some endpoints add a stable machine-readable "code" (the slice-request and workbook-intake tables below list theirs), and a few add bounded numeric context such as a rate limit or size cap. Nothing else is added: error bodies never carry the underlying cause, a stack trace, or an internal identifier.

Two details worth knowing before you write a client.

A browser navigation gets HTML — on 404, and only on 404. A request that is unambiguously a top-level browser navigation receives a small HTML "Page not found" page in place of the JSON body when the response is a 404. The status code is still 404; there is no redirect and no soft-200. A request qualifies only when all of the following hold:

  • the method is GET or HEAD;
  • Sec-Fetch-Dest is absent, or is exactly document;
  • the Accept header explicitly ranks text/html above application/json. Wildcards never count — */* and text/* do not qualify — an absent or empty Accept never qualifies, and a tie (Accept: text/html, application/json) resolves to JSON.

So curl (Accept: */*), a browser fetch() (Accept: */*), the Amnetic CLI and SDKs (Accept: application/json), and any client that sends no Accept header at all keep the JSON body byte for byte. Send Accept: application/json if you want to be explicit about it. Every 404 response — JSON or HTML — carries Vary: Accept.

No other status is affected. 400, 401, 402, 403, 409, 413, 422, 429 and every 5xx are JSON for every caller, browsers included. A wrong method on a valid path is a plain 405 with an Allow header.

The HTML page is byte-identical for every 404 cause and says nothing about why the request failed: no path, no identifier, no error message, no cause-specific headers. That is deliberate. The opaque 404 described throughout this page — where a restricted, inactive, pre-offer, withdrawn, or simply nonexistent resource all answer identically — is preserved on both representations, and the HTML page is if anything more opaque than the JSON, since it does not distinguish an unknown route from an unknown resource.

Consign buyer forensic-marker disclosure

GET /api/v1/consign/disclosure

Fetch the exact current buyer disclosure before accepting. The authenticated response contains instrument, the content-derived version, the full lowercase sha256, and the exact document_markdown. Present the markdown to the buyer and retain the full hash for the POST compare-and-set. This buyer legal act accepts only a Cognito ID token; an amn_… account API key cannot reach it. The route is absent while CONSIGN_CANARY_ENABLED is off.

GET /api/v1/consign/disclosure-acceptance

Status for one owned listing (listing_id query). Returns the current instrument plus accepted for the buyer's newest license grant on that listing. Missing, foreign, and grant-less subjects are the same 404 disclosure_grant_not_found. Identity is taken from the token, never the query.

POST /api/v1/consign/disclosure-acceptance

Record the buyer's acceptance that delivered data may carry forensic markers including synthetic records (they know that, not which). The body is:

{
  "listing_id": "…",
  "accept_disclosure": true,
  "disclosure_sha256": "<full lowercase SHA-256 from GET>"
}

The server stamps source=portal, pins the current draft version, and writes an append-only consign_disclosure_acceptances row for the newest buyer-owned grant. A stale or missing hash is 409 disclosure_changed. Repeat acceptance of the same version returns 200 with the original accepted_at. Canonical source: docs/legal/consign-disclosure.md. The draft is pending counsel (OD-CE1 / OD-CE3), is not yet effective, and is not legal advice. This surface does not refuse download; the canary materializer (CE-2c) is the structural interlock.

Consign attestation-gated refresh

Refresh of a new rotation cycle is gated on a short, versioned use-compliance attestation. Completing it releases the new cycle's artifact to that license. Refusing it keeps the gate closed and raises a Category-8 Review-ceiling candidate. The buyer response is an append-only Consign record; it is not a trust_grounding_event. These routes accept only a Cognito ID token; an amn_… account API key cannot reach them. They are absent while CONSIGN_ENFORCER_ENABLED is off.

GET /api/v1/consign/attestations

Fetch the exact current questions before submitting. The authenticated response contains instrument, the content-derived version, the full lowercase sha256, document_markdown, and question_ids. Present the markdown to the buyer and retain the full hash for the POST compare-and-set. Canonical source: docs/legal/consign-refresh-attestation.md. The draft is not yet effective and is not legal advice.

GET /api/v1/consign/deliveries/{id}

Refresh-status for one buyer-owned delivery. Returns whether a newer rotation cycle is pending, whether attestation is required, and whether the cycle is released. Missing and foreign delivery ids are the same 404 attestation_delivery_not_found. This is a status read, not a download.

POST /api/v1/consign/attestations

Record a response (every question answered yes) or a refusal. The body is:

{
  "delivery_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "questions_sha256": "<full lowercase SHA-256 from GET>",
  "outcome": "response",
  "answers": {
    "permitted_use": "yes",
    "no_redistribution": "yes",
    "no_forbidden_training": "yes",
    "will_apply_refresh": "yes"
  }
}

A refusal omits answers. The server Ed25519-signs the record with the Consign signing key and writes an append-only consign_attestations row for the pending cycle. gated_refresh_released is true only for response. A stale hash is 409 attestation_changed. Repeat of the identical submit returns 200 with the original row. A later response after a refusal is a new row and releases the gate. Download of a pending cycle without a response is 409 attestation_required.

Consign seller canary registration

GET /api/v1/seller/consign/canary-config

Fetch the exact current authorization before registering. The authenticated response contains instrument, the content-derived version, the full lowercase sha256, and the exact document_markdown. Present the markdown to the seller and retain the full hash for the POST compare-and-set.

POST /api/v1/seller/consign/canary-config

Register one seller-reviewed canary set and accept the current server-pinned canary authorization. This seller-account legal act accepts only a Cognito ID token; an amn_… account API key cannot reach it. Cognito authentication does not by itself prove that a person was physically present. The route is absent unless both CONSIGN_ENFORCER_ENABLED and CONSIGN_CANARY_ENABLED are on. The latter also requires a dedicated canary KMS key; it never reuses the encrypt-only Consign Report key.

{
  "listing_id": "9dad1234-…",
  "license_grant_id": "716cc51e-…",
  "dataset_classification": "regulated_ttb_cola",
  "accept_authorization": true,
  "authorization_sha256": "<full sha256 returned by GET>",
  "canaries": [
    {
      "record_key": "synthetic-record-001",
      "value": { "producer": "Example Cellars", "cola_number": "synthetic-outside-real-namespace" },
      "plausibility_status": "approved"
    }
  ]
}

dataset_classification is exactly general or regulated_ttb_cola. form may be omitted: general defaults to seeded, while regulated defaults to and requires sidecar. General may explicitly request sidecar; regulated may never request seeded. canaries contains 1–100 items. Each record_key is unique and at most 4,096 bytes; each value is a JSON object whose canonical encoding is at most 64 KiB; plausibility_status is pending, approved, or rejected. Unknown fields and multiple JSON values are rejected. The whole request body is capped at 8 MiB.

accept_authorization must be true, and authorization_sha256 must exactly match the full hash returned by the GET. A missing or stale hash returns 409 canary_authorization_changed; fetch and present the current instrument before retrying. The server still owns and stamps the version and hash.

Seller identity, the current ready content generation, the grant/listing relationship, authorization version/SHA, cycle index, and acceptance time are all resolved or stamped server-side. Registration fails closed if the listing or grant is foreign, the listing is a frozen derived child, or its ready head changes while values are being sealed. The append-only authorization, rotation cycle, every encrypted canary, and listings.enforcement_enabled=true commit in one transaction. Plaintext, ciphertext, record keys, and key ARNs are never in the response.

New registrations return 201 Created; an exact content replay returns 200 OK with the original identifiers and accepted_at plus replayed: true:

{
  "listing_id": "9dad1234-…",
  "content_generation_id": "b2698d7c-…",
  "license_grant_id": "716cc51e-…",
  "form": "sidecar",
  "canary_set_ref": "consign-canary-set-v1:…",
  "rotation_cycle_id": "25de776a-…",
  "cycle_index": 0,
  "authorization": {
    "id": "60346429-…",
    "instrument_version": "consign-canary-authorization-v0-draft-…",
    "instrument_sha256": "…",
    "source": "seller_api",
    "accepted_at": "2026-08-11T18:00:00Z"
  },
  "canaries": [
    { "id": "30f9368d-…", "plausibility_status": "approved" }
  ],
  "replayed": false
}

Stable error codes are canary_authorization_required and canary_config_invalid (400), canary_registration_not_found (404), canary_generation_not_ready, canary_generation_changed, canary_registration_conflict, canary_authorization_changed, and derived_listing_frozen (409), plus a temporary-unavailability 503. Oversized bodies return 413 with canary_config_invalid.

The operative authorization is currently a non-effective counsel draft (OD-CE1), not legal advice. Its exact content determines the server-stamped version and SHA-256; changing the text creates a new version rather than rewriting prior acceptance evidence.

Consign seller verdict feed

GET /api/v1/seller/consign/cases

Read the authenticated seller's own Consign cases. Cognito identity is taken from the bearer token and the seller-owned datastore policy scopes every row; there is no seller or buyer id in the query. Account API keys cannot reach this seller-human route. The feed is paged with optional limit (1–100, default 50) and offset (default 0):

{
  "cases": [
    {
      "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
      "kind": "license_enforcement",
      "confidence": "critical",
      "verdict": "serious",
      "case_ceiling": "probable",
      "created_at": "2026-08-18T08:00:00Z",
      "reviewed_at": "2026-08-18T10:00:00Z",
      "evidence_refs": {
        "review_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
        "evidence_package_id": "01ARZ3NDEKTSV4RRFFQ69G5FAX",
        "evidence_sha256": "…"
      },
      "evidence": {
        "fact": "the detected use conflicts with the governing license",
        "fingerprint_match": "decoded_fingerprint",
        "statistical_inference": "category_4:coverage_profile",
        "contextual_indicator": "category_7:public_activity",
        "alternative_explanation": "independent publication was checked",
        "unverified_assumption": "the source artifact is hash-pinned"
      }
    }
  ],
  "limit": 50,
  "offset": 0
}

The current verdict is the newest valid row in the append-only review ledger; the intake-time consign_cases.verdict snapshot is not used. A case without a review has verdict: null and no evidence projection. The evidence reference identifies the sealed package without exposing its object-store bucket/key, reviewer signature, candidate membership, or private account identifiers. The six evidence dimensions remain separate: a fact is not a match, inference, contextual indicator, alternative explanation, or unverified assumption.

Consign Verify

POST /api/v1/consign/verify

Look up one record from a licensed Consign delivery. The route exists only where CONSIGN_ENFORCER_ENABLED is on and accepts the same amn_… account API key or Cognito ID-token credential shapes as Report. The body is a closed object:

{
  "record_ref": "AMN1-…",
  "record_key": "canonical-unpadded-base64url-token"
}

record_ref is printed in the delivered license notice. record_key is deterministically derived from the delivered data.parquet row; it is not a second server-minted secret. Use the listing data dictionary's ordered primary_key names and the delivered Parquet physical schema/value for each named column. The v1 byte grammar is:

  1. UTF-8 consign-record-key/v1, then one zero byte.
  2. For each of the one to three primary-key columns in declared order: unsigned LEB128 byte length plus UTF-8 column name; length plus canonical type; one value type-tag; length plus canonical value bytes.
  3. Unpadded URL-safe base64 of the complete byte string. The decoded key may not exceed 3 KiB (4096 token characters).

Lengths count bytes, not characters. Unsigned LEB128 is the ordinary little- endian base-128 varint with seven payload bits per byte and the high bit set on every non-final byte. The tag is one literal ASCII byte and is not itself length-prefixed. Canonical types and values are exactly:

Parquet/Arrow value Canonical type string Tag Canonical value bytes
Boolean bool b ASCII 0 or 1
Signed integer int8, int16, int32, or int64 i Minimal base-10 ASCII, optional leading -, no leading zeroes
Unsigned integer uint8, uint16, uint32, or uint64 u Minimal base-10 ASCII, no leading zeroes
IEEE float float32 or float64 f Go strconv.FormatFloat(value, 'g', -1, bits) ASCII; finite values only and negative zero becomes 0
UTF-8 string string s Original UTF-8 bytes
Binary binary x Original bytes
Fixed binary fixed_binary(<byte_width>) x Original bytes
Date date32 or date64 d YYYY-MM-DD in UTC; years 0001–9999, and date64 must be midnight UTC
Timestamp timestamp_s(<timezone>), timestamp_ms(<timezone>), timestamp_us(<timezone>), or timestamp_ns(<timezone>) t UTC RFC3339Nano; <timezone> is the original empty or valid IANA Arrow timezone string, and the parentheses remain when it is empty, for example timestamp_ns()
Time of day time32_s, time32_ms, time64_us, or time64_ns T HH:MM:SS with a fractional part only when non-zero, up to nine digits and without trailing zeroes
Decimal decimal32(<precision>,<scale>), decimal64(...), decimal128(...), or decimal256(...) D Signed unscaled base-10 integer with the scale applied: for non-negative scale, insert/pad a decimal point with exactly scale fractional digits; for negative scale, append -scale zeroes

Null primary keys, non-finite floats, invalid UTF-8, and temporal values outside the stated ranges are invalid. Full-mode marking never changes key columns; preserve-mode payload bytes are unchanged. These executable golden vectors pin independent clients (each row is one complete record_key):

Primary key and canonical type record_key
id="alpha" (string), revision=7 (int64) Y29uc2lnbi1yZWNvcmQta2V5L3YxAAJpZAZzdHJpbmdzBWFscGhhCHJldmlzaW9uBWludDY0aQE3
ts=2026-08-11T19:20:30.123456789Z (timestamp_ns()) Y29uc2lnbi1yZWNvcmQta2V5L3YxAAJ0cw50aW1lc3RhbXBfbnMoKXQeMjAyNi0wOC0xMVQxOToyMDozMC4xMjM0NTY3ODla
amount=12345678901234.567890 (decimal128(20,6)) Y29uc2lnbi1yZWNvcmQta2V5L3YxAAZhbW91bnQQZGVjaW1hbDEyOCgyMCw2KUQVMTIzNDU2Nzg5MDEyMzQuNTY3ODkw
blob=00 ff 10 20 (fixed_binary(4)) Y29uc2lnbi1yZWNvcmQta2V5L3YxAARibG9iD2ZpeGVkX2JpbmFyeSg0KXgEAP8QIA
ratio=1.25 (float64) Y29uc2lnbi1yZWNvcmQta2V5L3YxAAVyYXRpbwdmbG9hdDY0ZgQxLjI1

On success the response is exactly the opaque key and the record fields; it contains no source, canary, confidence, delivery, account, license, listing, or generation field:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{"record_key":"canonical-unpadded-base64url-token","fields":{"COLA":"Palyt"}}

The platform resolves the verified account to its delivery, unexpired license grant, effective rotation cycle, and ready authoritative generation. Invalid, absent, expired, and foreign selectors are deliberately indistinguishable:

HTTP/1.1 404 Not Found
Cache-Control: no-store

{"error":"record_not_found"}

An unwarmed cycle, dependency failure, authoritative/canary key collision, or failed compliance-ledger append returns 503 with {"error":"verify_unavailable"}. Every resolved attempt is durably logged before release. One configured absolute deadline is a ceiling, not response padding: completed warm work returns immediately and unfinished work fails at the boundary. Canary envelopes are decrypted only as their cycles enter a bounded process-local warm overlay, never on the request path; the deployment gate statistically compares authoritative and canary latency. Wrong methods remain 405, missing authentication is 401, and while the feature is dark this path is an ordinary absent-route 404.

Every newly published authoritative generation carries a durable consign-verify-envelope/v1 internal attestation after all of its records pass the 3 KiB key and 64 KiB compact-fields caps. Startup checks only that marker for enforcement-enabled heads and unexpired delivery-bound cycles; it never rescans a million-row generation during a fleet boot.

Before the first production enablement, run the time-bounded operator command named by any readiness error for each legacy generation:

amnetic-internal --database-url "$DATABASE_URL" consign verify-attest GENERATION_ID --timeout 30m

The command streams the legacy derived index once and transactionally adds only the attestation metadata after a complete pass. It never rewrites records, delivery facts, grants, or immutable rotation cycles. If it reports an incompatible ordinal, keep CONSIGN_ENFORCER_ENABLED off: a replacement head does not supersede what an earlier buyer actually received. That historical entitlement remains a cutover blocker until it is no longer reachable or a separately reviewed serving-envelope version admits it.

POST /api/v1/consign/report

Report a questionable record from a licensed Consign delivery. This endpoint is available only where CONSIGN_ENFORCER_ENABLED is on; while dark, the route is absent. It accepts either an amn_… account API key or the normal Cognito ID token. Both credential paths resolve marketplace identity from the verified account reference, never from the request body.

Expected body:

{
  "record_ref": "AMN1-…",
  "record_key": "opaque-record-key",
  "where_encountered": "optional description of where this appeared"
}

record_ref is the exact support handle carried by the delivered artifact and is the only caller field used to locate a delivery. The platform derives the account, delivery, license, and listing server-side. Do not send any of those identity fields; they are ignored. A non-empty where_encountered confession is envelope-encrypted under the dedicated Consign Report key before durable storage. RecordRefs are high-entropy, unguessable support handles and the lookup is also scoped to the authenticated owner; clients must not treat the response as a way to discover or validate them.

After authentication, every request-body byte sequence—including an empty or malformed JSON body, wrong field types, unknown fields, an oversized body, a missing or foreign RecordRef, or an infrastructure failure—receives the same literal response:

HTTP/1.1 202 Accepted
Content-Type: application/json
Cache-Control: no-store
Content-Length: 58

{"status":"Logged. Under review with the data provider."}

The acknowledgment provides no record verdict, ownership signal, correction, or admission status and is not proof that a report row committed. This invariant covers the response status, headers above, and body bytes; it does not promise equal processing-time distributions or timing secrecy. A usable owner-scoped submission may synchronously perform KMS encryption and a durable database commit, so its latency can differ from a rejected submission. Missing or invalid authentication remains an opaque 401 before this invariant begins; a wrong method on this exact path receives 405, and other paths remain ordinary 404 responses. Triage and any later case promotion are asynchronous, but a durably admitted report is encrypted and committed before the acknowledgment—there is no post-response plaintext queue.

Market-entry sessions

POST /api/v1/market/sessions

Create a session (the caller-session handoff). Body (abridged):

{
  "caller": {
    "agent": { "name": "claude-code", "version": "1.4.2" },
    "llm":   { "provider": "anthropic", "model": "claude-opus-4-6" },
    "source_format": "amnetic-canonical-v1"
  },
  "session": {
    "system_prompt": "optional outer-agent system prompt",
    "messages": [ { "role": "user", "content": [ … ] } ],
    "workspace_summary": "optional"
  },
  "handoff": {
    "instruction": "Find a dataset of US retail foot-traffic under $50",
    "expected_outcome": "optional success criteria"
  },
  "controls": {
    "examination_ceiling": 8,
    "suggestion_budget": 1,
    "session_timeout_s": 600
  },
  "image_digest": "sha256:…"
}

The request body is capped at 8 MiB. Required: the caller.* identity fields, session.messages, handoff.instruction, and image_digest (must reference a ready image).

201 Created:

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "tier": "standard",
  "examination_ceiling_N": 8,
  "suggestion_budget_B": 1,
  "session_timeout_seconds": 600,
  "image_digest": "sha256:…",
  "stream_url": "…/api/v1/market/sessions/{id}/stream",
  "caller_session_hash": "<sha256 hex of the canonicalized envelope>"
}

GET /api/v1/market/sessions/{id}/stream

Server-Sent Events for a session. Event types:

Event Meaning
suggest_purchase A recommendation: { listing_id, reason, ts }. reason is a short platform-owned compatibility value, not inner-agent prose. Up to B per session.
submit_no_match The agent finished with no recommendation: { unmet?: [{ class }], ts } (terminal). class is closed vocabulary only: coverage, freshness, granularity, format, price, trust, rights.
refuse The agent declined or the session failed: { reason, ts } (terminal). reason is a closed enum, not prose.
session_timeout The wall-clock deadline was reached (terminal).
cancelled The session was cancelled by the buyer or an operator. A final audit_id follows when available.
watchdog_kill The platform stopped the session to enforce spend or runtime limits. A final audit_id follows when available.
audit_id Final close event: { audit_id, audit_url, signature_alg, ts }.
keepalive Heartbeat (no payload of interest).
gap Some events were dropped; the audit record is authoritative.

Other session endpoints

Method + path Purpose
GET /api/v1/market/sessions List your sessions (filtered by token identity)
GET /api/v1/market/sessions/{id} Session status
POST /api/v1/market/sessions/{id}/cancel Cancel a running session ({ "reason": "..." })
GET /api/v1/market/sessions/{id}/audit Fetch the signed audit record. Buyer verification is fixed to Ed25519; unsupported, missing, or malformed signed tuples fail closed.

The CLI also requires the signed session_id to match the requested route. Current records bind audit_id, raw_blob_ref, and raw_blob_hash; initial Ed25519 records that predate those additions remain verifiable but clearly mark the displayed audit identifier as unbound and cannot trigger an automatic blob download. Blob URLs must resolve to the exact signed S3 bucket/key, are fetched without your bearer token, and are never followed through redirects.

Purchases & ownership

You act on a suggestion from outside the wall.

POST /api/v1/purchases

{
  "listing_id": "9dad1234-…",
  "funding_mode": "wallet",
  "offer_id": "immutable offer UUID",
  "renew_of_grant_id": "optional grant UUID to renew"
}

funding_mode is wallet (default; deducted from your spendable balance) or card (charge the full listing price via hosted Stripe Checkout and preserve existing wallet credit). The legacy payment_method: "credit" field is accepted as a compatibility alias for funding_mode: "wallet". offer_id selects the immutable license-offer row and renew_of_grant_id selects a bounded-term renewal; both work on every funding mode — wallet and card. A missing or unavailable offer is reported only after listing visibility is established.

When the counsel-gated MCP licensing preview is enabled, enter_market accepts an enum-only required_rights preference and returns a mechanical rights_fit.best_offer for each fit. After reviewing the operative exact text, copy that summary's offer_id into this REST request as offer_id. The recommendation and its conditional total are advisory snapshots—not legal advice, permission, a price reservation, acceptance, or a grant. This purchase endpoint re-resolves current state, and only its completed grant is license evidence. Production's live MCP schema omits required_rights while the capability remains dark.

For card modes, the elected offer's price (not the listing base price) is resolved at checkout creation, so the Stripe charge and funded_cents reflect the offer. The card checkout row persists only the three selectors; the resolved price, terms, and binding are re-derived server-side at webhook settlement from the immutable offer row (never a client-supplied snapshot), so a client cannot force a cheaper or different grant across the async gap.

Wallet purchases complete immediately:

{
  "transaction_id": "txn_01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "status": "completed",
  "payment_method": "credit",
  "funding_mode": "wallet",
  "already_owned": false,
  "purchases": [
    {
      "listing_id": "9dad1234-…",
      "outcome": "purchased",
      "offer_id": "offer-...",
      "offer_key": "standard",
      "terms_hash": "sha256:…",
      "grant_id": "grant UUID",
      "price_cents": 2500
    }
  ]
}

Offer-level conflicts return 409 with error_code, listing_id, and detail. Codes are offer_unavailable, price_changed, and renewal_invalid. Listing visibility failures remain opaque 404. offer_unavailable.detail.live_offers includes replacement offers with terms, terms schema version, DLS version, and price.

The request carries no eligibility declaration. Your annual revenue band is an account setting: declare it once with POST /api/v1/party/representations ({"operand": "annual_revenue_band", "value": "under_1m" | "1m_10m" | "over_10m"}), or in the portal under Account → Eligibility, and read it back with GET /api/v1/party. Every purchase uses that standing declaration and the signed Agreement records it. A band-restricted offer returns 409 representation_required until you have declared one and 409 ineligible (with detail.declared and detail.allowed) when your band isn't allowed; an offer for legal entities returns 409 party_required until POST /api/v1/party records your entity. A body that still sends representations is refused with 400 representations_not_accepted.

Card modes return a hosted Stripe Checkout URL. The webhook funds your wallet and attempts fulfillment after payment; if the listing can no longer settle, the paid amount remains as non-withdrawable spendable credit.

Offer-aware card checkout is live. There is no LICENSE_TERMS_ENABLED boot gate. Counsel review still governs how composed terms are interpreted; it does not hide the routes. The drift outcome below is provisional and subject to change: whether a paid-but-drifted card offer should instead be refunded (rather than recorded as non-withdrawable credit) is an open question before counsel and is not yet a settled buyer entitlement.

For an elected offer that is retired, re-versioned, or repriced between checkout and payment (or a renewal already fulfilled on another rail), settlement resolves the offer live and, on any drift, currently records the payment as non-withdrawable wallet credit and mints no license grant (funded_only). This is intended to be analogous to the wallet rail's synchronous refuse-not-charge behaviour — the money is preserved as recoverable credit and no wrong grant is minted — but note the card rail converts fresh card money into non-withdrawable credit where the wallet rail moves no money at all, and whether that difference requires a refund is the open counsel question above.

{
  "status": "pending",
  "payment_method": "card",
  "funding_mode": "card",
  "checkout_url": "https://checkout.stripe.com/c/...",
  "session_id": "cs_...",
  "amount_cents": 2500,
  "funded_cents": 2500,
  "wallet_applied_cents": 0
}
Method + path Purpose
GET /api/v1/purchases/{id} Transaction status
GET /api/v1/ownership List listings you own
GET /api/v1/ownership/{listingId}/download?format=parquet|xlsx|csv Presigned download URL for an owned listing. Omit format to download any listing's canonical owned artifact. Explicit parquet, xlsx and csv resolve against the listing's recorded derivation artifacts, so they cover every derived child — platform-created slice children and agent-derived listings alike; an agent-derived listing's canonical artifact is the file its worker staged, in its own format (csv/xlsx name it explicitly), and parquet serves only the platform's own parquet copy of it (never a worker-supplied extra). A listing with no such recorded artifact is unavailable in that format. ?prefer=source (not combinable with format) is the portal's person-default: it serves the staged spreadsheet of an earlier derived listing whose canonical artifact is a platform-converted parquet, and the canonical artifact for every other listing.

Each GET /api/v1/ownership row embeds a listing object whose listing.large_listing boolean reports whether the listing is on the resumable large-upload rail (its advertised size exceeds the server's ordinary-upload cap). It is computed server-side by the single IsLargeListing predicate and is the field the portal keys its download affordance off — large listings have no working in-browser download and must be re-downloaded with amnetic buyer download <listing-id>; small listings download in-browser. Clients should read this field rather than comparing data_size_bytes to a client-mirrored threshold, which would drift. The download handle response carries the same large_listing verdict.

Slice-on-demand deployments also expose the buyer routes below and the workflow documented in Buying derived listings. Slice children use this same purchase endpoint unchanged; clients send the offered child listing_id, never a price assertion or account identity.

Listings with non-clean file malware scan status are treated as unavailable for purchase. Owned listings can also be temporarily refused at download time while a file scan is pending or failed closed. Downloads use the same ownership and scan gates and issue a short-lived URL. When format is omitted, the service returns the canonical data_ref for every listing. When present, format is a closed enum (parquet or xlsx); the xlsx alternate is resolved from the listing's recorded derived artifacts, never caller input. The MCP ownership_download tool remains the canonical-format surface and does not accept a format argument.

The download response is a re-mintable handle, built so a multi-gigabyte object survives an expiring URL: when the URL dies mid-transfer, call the endpoint again and resume with an HTTP Range request.

{
  "download_url": "https://…?X-Amz-…",
  "expires_at": "2026-08-04T18:31:07Z",
  "total_size_bytes": 8589934592,
  "sha256": "9f2c…",
  "recommended_part_size_bytes": 67108864,
  "resumable": true,
  "large_listing": true
}
Field Meaning
expires_at When download_url stops working. Read this rather than hardcoding a lifetime; operators set it with OWNERSHIP_DOWNLOAD_URL_TTL (default 20 minutes).
total_size_bytes Size of the exact object being served, measured when the URL was signed — this, not the listing's advertised data_size_bytes, is the length your transfer plan and Range arithmetic should use.
sha256 Raw-byte checksum of the served object, returned only when the platform can prove it describes those exact bytes. Omitted otherwise (for example for text-body listings, alternate-format exports, and enforced-delivery bundles).
recommended_part_size_bytes Suggested chunk size for a parallel or resumed fetch. Omitted when the object was not uploaded in parts.
resumable true when re-requesting the handle is cheap and returns the same bytes. false on a delivery-enforced listing, where each call materializes a fresh signed bundle behind a rate limiter — take the URL you were issued and do not poll for a refreshed one.
large_listing true when the listing's advertised size exceeds the server's ordinary (non-large) upload cap — i.e. it was served through the resumable large-upload rail and has no working in-browser download path. Computed by the same server predicate as the ownership row's listing.large_listing, so the two can never disagree.
matches_evaluated_pin Present only if the caller has a released evaluation of this listing. true only when the platform confirmed the exact object being served and its checksum equals the evaluation's pinned_artifact_sha256. false otherwise.
evaluated_pin {evaluation_id, pinned_artifact_sha256, status}. status is match (confirmed, same bytes), changed (the listing changed after the evaluation) or unverified (the platform could not confirm which object it is serving, for example after a pinned-version lookup failed, or for an enforced-delivery bundle). unverified is never reported as a match.

The CLI does all of this for you: amnetic buyer download <listing-id> [--out <path>] transfers the object as a sequence of Range requests, re-mints the handle when a chunk fails or the URL is about to expire, resumes from the last byte written, verifies sha256 when the handle carries one, and renames the file into place only once the whole object is on disk — so an interrupted transfer never leaves a truncated file looking complete. --max-retries (default 5) bounds the consecutive chunk failures it tolerates — a chunk that succeeds clears the count, so a long transfer is not capped at five failures overall — and --url-only prints the URL instead of downloading. On a resumable: false listing it makes a single request and does not retry, for the reason in the table above. Transfers of 32 MiB or more print a progress line to stdout every few seconds, so parse --url-only rather than the transfer output if you are scripting against it.

Download authorization is grant-OR-owner. A caller may download a listing they purchased (a grant) or a listing whose seller is their own account (the owner — so a Seller's Agent can fetch the parent it derives from). The owner serve is scan-gated exactly like a buyer's and, on an enforced listing, is served verbatim (no delivery marking — the owner already possesses the content). When the caller neither owns a grant nor is the seller — and equally when the listing does not exist — the response is a single opaque 404 {"error":"not found"} (previously 403): missing, not-purchased, and not-owned are byte-identical so a caller cannot probe another account's inventory. The 409 cases (scan-blocked, data-missing, unavailable-format) are reachable only after authorization succeeds.

Enforced delivery (dark; pending counsel)

Off by default. Consign delivery enforcement is gated by CONSIGN_ENFORCER_ENABLED (default off) plus a per-listing enforcement_enabled opt-in, and stays dark pending counsel. When both are off the download path is byte-identical to the behavior above.

When a parquet-backed dataset listing has enforcement enabled, each download is served as a signed per-delivery bundle (the authoritative data.parquet unchanged, plus a license-notice sheet carrying a per-delivery record reference) rather than the bare object. Every download mints a fresh bundle and records one append-only delivery-ledger row. Under enforcement the explicit format=parquet|xlsx|csv selectors are unavailable (the deliverable is the canonical signed bundle), and the inline body is never returned. If the marked bundle cannot be produced the request fails closed with 503 materialization_unavailable (retriable — a later download mints a new delivery) and the plain object is never presigned.

Enforced buyer downloads are admitted through fleet-wide concurrency and fixed-window attempt limits before any source object is downloaded or delivery identity is minted. Quota exhaustion returns 429 materialization_rate_limited with an integer Retry-After header. Capacity exhaustion, datastore failure, and operation timeout remain the opaque 503 materialization_unavailable. Failed downstream attempts still count toward the quota, and every admitted retry creates a new delivery bundle; final artifacts are never reused.

The buyer half of the canary interlock is a separate, versioned disclosure: GET/POST /api/v1/consign/disclosure-acceptance records that delivered data may carry forensic markers including synthetic records. Wording is a draft pending counsel. Missing that row does not block this download path; canary-bearing delivery (when enabled) refuses in code without it.

Cutover: enabling enforcement on a listing with outstanding ownership rows does not retroactively re-mark anything — each buyer's next download is the first materialized-and-ledgered one; earlier downloads leave no delivery row.

Buyer slice requests

Opt-in beta; pending counsel approval. These REST routes exist only when SLICE_ON_DEMAND_ENABLED is on. They are absent—not stubbed—when it is off, and production refuses to start with the flag enabled until the Slice Authorization rider is counsel-ratified and the production path is deployed. An enabled non-production deployment also requires AWS_REGION for the real Bedrock extraction agent and TRUST_RECORDING_ENABLED=true for transactional parent-to-child provenance. SLICE_MATCH_MODEL and SLICE_AGENT_MODEL must be exact reviewed Claude-on-Bedrock profiles from the platform registry, and startup resolves AWS credentials and invokes each distinct configured model with a fixed synthetic preflight. It fails rather than installing a fallback. There are no buyer request_slice or slice_status MCP tools in this demo.

All three routes require authentication, derive buyer identity from the verified principal, and return Cache-Control: no-store on success and error. A missing, foreign, ACL-hidden, or otherwise ineligible parent/job is always the same opaque 404. The shapes in this section are the closed normative buyer projection for the beta; the reference's general illustrative-shape note does not permit internal slice fields to appear here.

POST /api/v1/listings/{id}/slice-requests

Submit multipart/form-data with at most one of each named part:

Part Contract
text Optional UTF-8 text, at most 1 MiB. Without a file, each non-empty line is one query; each query must be printable NFC text no longer than 256 UTF-8 bytes.
file Optional non-empty CSV, XLSX, or TXT file, at most 1 MiB. When both parts are present, the file is authoritative and text is ignored.

At least one non-empty part is required. Duplicate and unknown parts are invalid. The total request-body limit is exactly 2 MiB + 64 KiB (2,162,688 bytes): one 1 MiB budget per legal part plus the bounded multipart envelope. The server also enforces each part's limit independently.

Parent ACL and all other slice eligibility checks run before the body is interpreted, so malformed input cannot probe an inaccessible listing. A successful request is free and returns 202 Accepted only after the canonical query list and job are durable:

{ "job_id": "7d1df2b8-7dbc-4a23-b739-6aef25ac33a4" }

The platform processes the submitted text or file into a canonical query list and does not deliberately store the raw uploaded file after interpretation. For every multi-column file, interpretation sends its column headers and up to five complete sampled data rows across all columns to the configured Claude model on AWS Bedrock. The bounded canonical query list and job evidence are retained for processing and audit. When the flagged D9 seller-only activity/report surface is enabled, the canonical query list and requesting account identity are shared with the seller. The beta publishes no query-retention or deletion control. See Where your data goes for the public model-layer disclosure. Treat submission as a disclosure-bound named order: do not submit query material you are not allowed to share with that seller.

The seller policy's max_jobs_per_buyer_per_day applies to the verified buyer and this parent listing over the preceding rolling 24 hours. The final count and insert use the database clock and are atomic. Every accepted job counts regardless of its eventual outcome; malformed, query-limit, and other pre-commit refusals do not. The demo does not yet add cluster-keyed, global cross-listing, or cumulative-row metering.

GET /api/v1/slice-requests

Returns the caller's jobs as a bare newest-first array:

[
  {
    "job_id": "7d1df2b8-7dbc-4a23-b739-6aef25ac33a4",
    "listing": {
      "id": "f17a858c-3981-4afb-aea9-0b80345bc475",
      "title": "Catalog"
    },
    "kind": "row_match",
    "projected_state": "offered",
    "price_cents": 1234,
    "created_at": "2026-07-15T18:30:00Z"
  }
]

projected_state is exactly processing, offered, not_available, purchased, or expired. price_cents is omitted unless the projected state is offered or purchased; an internal checkpoint price is never exposed while the job is still processing.

GET /api/v1/slice-requests/{job_id}

Every detail contains the closed projected_state and buyer-safe progress. Optional members are omitted rather than returned as null:

{
  "projected_state": "offered",
  "progress": [
    { "at": "2026-07-15T18:30:00Z", "line": "Request received." },
    { "at": "2026-07-15T18:30:02Z", "line": "Matching your request." }
  ],
  "offer": {
    "totals": {
      "queries": 500,
      "matched": 412,
      "near_miss": 31,
      "unmatched": 57
    },
    "per_query": [
      { "q": "makers mark 46 750ml", "status": "matched" }
    ],
    "price_cents": 1234,
    "expires_at": "2026-07-29T18:30:05Z",
    "slice_listing_id": "68053322-d174-4848-bd49-8e7c4dfcefc7"
  }
}

offer is present for offered and retained for purchased so the frozen child remains the purchase/download target. Its totals contains exactly queries, matched, near_miss, and unmatched; it does not contain rows_selected. per_query is present only when the seller's disclosure mode was snapshotted as per_query, preserves submitted-query order, and contains only q plus matched, near_miss, or unmatched. Aggregate disclosure omits it. Neither mode exposes seller row keys, per-query row counts, candidate values, match class/confidence, row contents, internal state, or mutable policy.

Progress line is selected from fixed platform copy; stored templates, arguments, counts, prices, review state, and worker/model prose are never returned:

Meaning Buyer-visible line
Request accepted Request received.
Matching Matching your request.
Matching complete Matching complete.
Review/materialization Preparing your result.
Offer ready Your offer is ready.
Purchased Purchase complete.
Expired This offer expired.
No offer No offer is available.

Unknown stored templates are omitted, and consecutive entries that map to the same line are coalesced. Valid progress timestamps remain in stored order.

Only terminal selection_too_large and matcher_budget_exhausted may appear as error_class. selection_too_large carries no selected-row count or cap; all other failures collapse to not_available without a class.

After the same buyer owns the child and the detail projects purchased, it may also contain this receipt:

{
  "receipt": {
    "queries": [
      {
        "q": "sku 1",
        "matches": [
          {
            "ordinal": 3,
            "key_column_values": {
              "sku": "SKU-1",
              "upc": "012345678905"
            }
          }
        ]
      }
    ]
  }
}

The receipt contains matched queries only. ordinal is the one-based position inside the delivered slice, not the parent dataset's ordinal. Key-column values are JSON scalars. The server reconstructs this view only after authorization from hash-verified query, match-report, manifest row-key, and immutable profile evidence. It does not expose row keys/object references/digests, consult a mutable current policy/profile, or persist a separate receipt artifact.

Slice-request errors

Errors use { "error": "safe message", "code": "stable_code" } plus only the typed integer fields shown below:

Status code Additional contract
404 not_found Exact opaque body { "error": "not found", "code": "not_found" }.
413 slice_request_too_large Outer request exceeded 2,162,688 bytes.
413 slice_text_too_large limit_bytes: 1048576.
413 slice_file_too_large limit_bytes: 1048576.
422 input_invalid Wrong content type or malformed/duplicate/unknown/missing/empty/invalid text parts.
422 buyer_file_invalid Unsupported, empty, or invalid CSV/XLSX/TXT, including bounded workbook/formula refusals.
422 query_limit_exceeded limit is the current caller-applicable parent-policy query cap; no job is created.
429 daily_job_limit_exceeded limit is the caller-applicable parent-policy job cap and window_seconds is 86400.
503 slice_request_unavailable Temporary interpreter/model/source unavailability before job creation.
500 internal_error Generic dependency, persistence, or integrity failure.

Purchase remains POST /api/v1/purchases with the offered slice_listing_id. The request has no expected_price_cents, buyer/account identity, or object key; the frozen child is the quote-integrity boundary.

Buyer requests

Buyer requests are human-signed-off asks backed by escrowed credit. Draft creation does not move money or make anything visible. Sign-off records the three buyer consents, reserves the bounty, and runs platform-side intake moderation before the request can leave the buyer's private workflow.

Where buyer posting is enabled, draft creation, listing your own requests, browsing the board, and responding as a seller — by submitting a candidate or an above-bounty quote — are also MCP tools (create_request_draft, list_my_requests, list_open_requests, submit_request_candidate, post_request_quote — see the MCP tools reference). Sign-off and withdraw stay REST/portal only: sign-off is the portal-only, escrow-reserving human approval and has no MCP tool.

Method + path Purpose
POST /api/v1/requests Create a draft request
GET /api/v1/requests List your requests
GET /api/v1/requests/{id} Get one request
POST /api/v1/requests/{id}/signoff Sign off and reserve escrow
POST /api/v1/requests/{id}/withdraw Withdraw a draft, open, or held-for-review request
POST /api/v1/requests/{id}/candidates Submit one of your listings as a candidate (seller)
POST /api/v1/requests/{id}/quotes Submit one of your above-bounty quotes on a request (seller) — the amount must be a whole number of cents and strictly above the request bounty
POST /api/v1/requests/{id}/candidates/{cid}/confirm Confirm a passed candidate — settles a normal pass at its captured price or a qualifying committed-quote pass at the quoted amount (buyer)
POST /api/v1/requests/{id}/candidates/{cid}/decline Decline a normal passed candidate and keep the request open; after its deadline the server records it as lapsed while the response remains the request object (buyer)
POST /api/v1/requests/{id}/quotes/{qid}/commit Commit to a seller's above-bounty quote — earmark the top-up and reserve the fill window (buyer)

The buyer confirm/decline/commit routes are the REST twins of the respond_request MCP tool — the same state machine. For a normal pass, confirm releases the escrow bounty and buys the candidate's listing at the price captured when it was submitted (never a later, higher price); a price drift / withdrawn / already-owned listing is a 409 with the candidate voided, and an above-bounty price short of the buyer's spendable is a 402. Confirmation authorization lasts 72 hours from the platform-recorded pass. The deadline is exclusive: at or after it, confirm cannot charge and decline records a lapse rather than extending the window.

commit accepts a seller's quote (a bespoke price above your bounty): it earmarks the top-up above the bounty in escrow and reserves the seller's exclusive fill window. It settles nothing — the purchase happens automatically at the quoted amount when the seller delivers. The request stays open. A quote at or under the bounty is a 400; a spendable balance short of the top-up is a 409. The unattended fill (the automatic purchase on delivery) and the lapse of an expired commitment are worker transitions with no REST route. An in-window seller pass remains eligible for unattended settlement after the commitment deadline if recovery was delayed: the persisted pass time is checked against the committed interval inclusively, never against restart time. A pass from that committed seller inside either exact boundary has earned settlement at the quoted amount: it cannot be declined or lapsed, and an explicit candidate confirm routes through the same idempotent quote settlement. With no earned pass, lapse releases only the quote top-up. The base bounty is released separately when an idle closing request resolves to expired.

When expires_at arrives with evaluation or quote work still in flight, the request becomes closing rather than expiring immediately. It accepts no new work, but existing work may still fill it. Once the last in-flight item ends, it becomes filled or expired; the latter transition releases the base bounty exactly once.

POST /api/v1/requests body:

{
  "title": "UPC-level bev-alc depletions",
  "body": "Monthly UPC-level control-state depletions, 2024-2026.",
  "category": "retail",
  "tags": ["beverage", "sales"],
  "hints": { "freshness": "monthly" },
  "bounty_micro_usd": 250000000,
  "expires_at": "2026-08-01T00:00:00Z",
  "attribution": "named",
  "acl_mode": "open"
}

POST /api/v1/requests/{id}/signoff body:

{
  "consents": {
    "content": true,
    "attribution": true,
    "commitment": true
  }
}

Sign-off can return state: "open" or state: "held_for_review". A held request has the bounty escrowed but is not eligible for seller-facing surfaces until an operator releases it. Buyers may withdraw a held request, which releases the escrow and removes it from the review queue. If an operator rejects it, the state becomes rejected and the escrow is released. Buyer sign-off is not a substitute for platform moderation.

GET /api/v1/request-board

Search open, unexpired buyer requests visible to the authenticated seller. Held requests do not appear until an operator releases them. The seller board is authenticated and ACL-filtered for the caller; there is no public request-board route.

Query params:

Param Meaning
q Search text
category Exact category filter
min_bounty_micro_usd Minimum bounty, in micro-USD
mode text, vector, or hybrid (hybrid by default; blank q uses text browse)
limit Max results, capped at 100

Response:

{
  "mode": "hybrid",
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "title": "Need California wildfire permit data",
      "body": "Looking for county-level permit and incident data...",
      "category": "dataset",
      "tags": ["wildfire", "permits"],
      "hints": { "freshness": "2023-2026" },
      "bounty_micro_usd": 25000000,
      "expires_at": "2026-08-01T00:00:00Z",
      "attribution": "named",
      "created_at": "2026-07-07T18:00:00Z"
    }
  ]
}

Candidate submission and passive matching are not part of this REST surface yet; request-board rows are discovery only until the seller action endpoints ship.

Catalog

Public metadata

Method + path Purpose
GET /api/v1/listings List listings
GET /api/v1/listings/{id} Get one listing
GET /api/v1/listings/{id}/license-text/{offerKey} Read the operative text for a public offer before purchase (licensing flag only)
GET /api/v1/sellers/{id} Read public seller metadata

These routes are anonymous JSON APIs, not viewable pages. GET /api/v1/listings/{id} serves a listing only when it is active, its purchase ACL is open, and — for a listing that belongs to a sealed aggregate (a derived child, or a workbook root/worksheet) — that aggregate is published. Restricted, non-active, awaiting-sign-off, and nonexistent listings all collapse to the same opaque 404 {"error":"listing not found"}, so the route cannot be used as an existence or disposition oracle. (With licensing enabled, a listing whose offers are not publicly readable takes the same 404.) Sellers do not have to guess against that opacity for their own inventory — every seller-facing response for a listing you own carries a public_visibility object; see Is my listing in the public catalog?.

public_visibility is {"visible": bool, "reason": "visible" | "not_active" | "restricted_access" | "pending_publication"}, and it is a purely additive field — no existing field was removed or renamed. It appears on:

Method + path Where
GET /api/v1/seller/documents each row
POST /api/v1/seller/documents the created listing
POST /api/v1/seller/datasets the created listing (single-sheet/CSV/parquet; see the multi-sheet note below)
POST /api/v1/seller/files the created listing
POST /api/v1/seller/large-uploads/{id}/complete the created listing
PATCH /api/v1/seller/documents/{id} the updated listing
POST /api/v1/seller/documents/{id}/publish the published listing

On the write responses in particular, status reports the state you asked for and public_visibility.visible reports whether the public catalog serves the listing right now — the two can legitimately disagree while a create or a content edit is being processed. The field is omitted when the check was unavailable, which means unknown, never hidden; it never fails the request.

Seller-private derived pricing

Seller listing rows and seller listing write responses (such as PATCH /api/v1/seller/documents/{id}) carry the read-only, seller-private derived_pricing object. It contains enabled, independent row and column axes, min_price_cents, optional max_price_cents, counter_floor_cents, and optional buyer_type_adjustments entries for business and enterprise, plus server-computed row_facts / column_facts. An adjustment is either {"kind":"multiplier","multiplier_bp":11000} (basis points, where 10000 is 1.0x) or {"kind":"override","price_cents":9000}. All money fields are integer US cents; axis rates are integer micro-USD. The listing price is the listing's price_cents; reconstruction facts are measured against it.

The object is a projection of the listing policy. Price and derivation pricing are written only with PUT /api/v1/seller/documents/{id}/policy: the base rung's price_cents is the listing price, and the policy envelope and derivation_pricing carry the bounds, per-row rate, and buyer adjustments. PATCH /api/v1/seller/documents/{id} accepts only title, description, category, tags, status (active | inactive), sample_data_enabled, and allow_evaluations; a body carrying price_cents or derived_pricing is refused with 400 naming the policy endpoint.

A derived purchase is priced at ceil(rows × per-row rate), clamped to the envelope. The listing price applies only when there is no per-row rate; the two are never added. Enabling derivation pricing is atomic: the listing price must be positive and within the configured listing cap, the minimum and counter floor must meet the transaction floor, an optional maximum must be at least the minimum, and a per-row rate is required. The policy endpoint refuses derivation_pricing.enabled: true without a rows cell (derivation_row_rate_required) and any derivation_pricing.columns cell (column_pricing_unsupported; column pricing is not yet supported). It also refuses a price that would leave an active, open-access, non-sample listing at $0 (zero_price_open_listing). Public GET /api/v1/listings and GET /api/v1/listings/{id} responses omit the entire object, including clamps and buyer adjustments.

GET /api/v1/sellers/{id} is also anonymous. Its response is an explicit public profile projection and contains exactly id, name, and created_at:

{
  "id": "seller-id",
  "name": "Acme Data",
  "created_at": "2026-08-19T12:00:00Z"
}

The internal account record also has an email address, but email is private account data and is never returned by this public route. An unknown seller returns the standard 404 error response.

One exception: a multi-sheet workbook upload to POST /api/v1/seller/datasets returns the workbook aggregate response (the root listing plus its workbook block), which does not carry public_visibility. Read the root's publication state from GET /api/v1/seller/documents (or MCP get_my_listing) instead.

Listing lineage (additive; enabled in the reviewed production deployment)

The public listing projection (GET /api/v1/listings/{id} row shape) and the seller listing projection (GET /api/v1/seller/documents rows plus the create / publish write acknowledgements) carry two additive lineage fields when the server runs with DERIVED_LISTING_LINEAGE_ENABLED=true: derived_from (the real parent listing when this listing is a derived child) and derivations (the catalog-visible, published children of this listing). Each node is an explicit public-safe allow-list of exactly id, title, description, price_cents — no data_ref, dictionary, seller, or ACL data is copied (the same posture as the public projection itself).

{
  "derived_from": {
    "id": "parent-id",
    "title": "Parent listing",
    "description": "…",
    "price_cents": 2500
  },
  "derivations": [
    {
      "id": "child-id",
      "title": "Derived child",
      "description": "…",
      "price_cents": 990
    }
  ]
}

Semantics (ADR-216):

  • Derived children are real listings. Lineage identity is sourced from the listing-bound derived_from edge between each listing's canonical trust attestations; derivation_manifests.parent_listing_id remains a checked operational pin, not a public identity authority. The projection never uses overlay ids or client-invented slugs. A #listing/{id} link always opens that child's own listing page.
  • derivations lists only catalog-visible, published children. Pending (unsigned), rejected, retired, and sample-class children are excluded by the same public-visibility authority the anonymous catalog applies — they never appear. A parent with no published children has an empty derivations array.
  • derived_from is omitted when there is no parent (an ordinary listing) or when the parent is not itself publicly readable.
  • Off by default elsewhere. While DERIVED_LISTING_LINEAGE_ENABLED is off, both keys are omitted entirely, so the catalog and seller JSON stay byte-identical to the pre-feature shape.

GET /api/v1/listings pages with limit and offset and returns {"data":[…],"total":…,"limit":…,"offset":…}. A limit that is absent or not a positive integer falls back to 20 (the echoed limit reports what you sent, so use your own requested page size when advancing, not the echoed one).

Each page is assembled per request against a live catalog, so a page can contain fewer rows than you asked for even when it is not the last page — and in the limit, none at all. A listing that stops being publicly visible while that page is being built is omitted from it, and total is a count over the whole filter rather than of the rows returned.

Page by advancing offset by your requested page size and continuing while offset < total. Do not treat a short — or empty — data array as the end of the catalog, and do not derive the page count from len(data); both are snapshots of a catalog that changes underneath you.

POST /api/v1/search is not a public/anonymous catalog API. It is a temporary in-wall transport adapter for market-read-proxy, requires a valid propagated buyer-session OBO token, and is scheduled to move to the mesh-only listings datastore. API keys and Cognito bearer tokens do not authorize it. Buyer-agent applications should use the documented buyer-session/MCP workflow, not call this transport route directly. Its valid per_page range is 1 through 25 and the omitted-field default is 20. As defense in depth, the server maps non-positive integers to 20 and silently clamps integers above 25, but callers should not rely on those out-of-contract values. The agent-facing filters.max_results field remains valid through 50 for deployment compatibility; values above 25 still produce at most 25 results.

Workbook catalog projection is additive. A workbook is one root listing with "workbook":{"root_listing_id":"…","sheet_count":2,"is_root":true,"artifact_scope":"data_bearing_worksheets"}. Worksheets are not separate catalog or purchase targets; workbook.sheets is empty under the root-only publication contract. sheet_count and the values- only buyer artifact include data-bearing worksheets only; cover, notes-only, and chart-only tabs are omitted rather than represented as buyer data.

Listing responses include public catalog metadata plus the always-on materialized menu:

{"menu":[{"offer_id":"offer-...","offer_key":"standard","price_cents":5000,"currency":"usd"}]}

That offer id binds the listing to its immutable materialized base rung. The offer row carries the canonical policy bytes and derived display fields; it is the sole purchase pricing and terms authority. A missing or malformed real row fails closed; the API never synthesizes a virtual standard tier.

When SLICE_ON_DEMAND_ENABLED is on, visible rows from both public listing endpoints also carry sliceable. A value of false has no slice_summary; true includes the live buyer-safe row-match policy projection:

{
  "sliceable": true,
  "slice_summary": {
    "kinds": ["row_match"],
    "min_price_cents": 100,
    "per_row_cents": 1
  }
}

Slice metadata is computed only after the parent listing's current visibility check. When slice-on-demand is off, both new keys are absent, preserving the previous response shape. Seller-authored standing segments are ordinary frozen child listings in the catalog; the parent summary does not enumerate them.

While that same flag is on, the unauthenticated license-text route accepts an active offer_key from the listing's menu (standard for the current base rung). It returns the exact stored text of record together with the resolved terms, versions, hashes, price, currency, and offer id:

{
  "offer_key": "standard",
  "terms": {"...": "..."},
  "terms_schema_version": "lts/1",
  "dls_version": "tos/5.3-draft",
  "terms_hash": "sha256:...",
  "price_cents": 5000,
  "currency": "usd",
  "offer_id": "offer-...",
  "rendered_text": "...the operative text...",
  "rendered_sha256": "..."
}

Composed tiers also include offer_id. Responses are Cache-Control: no-store; bind what you reviewed with offer_id, not the human-facing URL. Hidden listings and unknown, retired, malformed, or wrong-listing keys return opaque 404s. Missing or mismatched stored evidence fails closed with 500 and no text. The route is absent when licensing is off. It covers public/open listings only; whether restricted-listing buyers need a separate authenticated pre-purchase path, and whether assembled availability is legally sufficient for purchase-as-acceptance, remain pending counsel review.

In-wall search derives the buyer account and access groups from the signed session token; the request body cannot override self-seller or listing-ACL filters. Each hit is rechecked against current PostgreSQL state, so a stale search-index document cannot expose an ineligible listing.

Buyer images

Method + path Purpose
POST /api/v1/buyer/images Push a Dockerfile + context (multipart)
GET /api/v1/buyer/images List your images
GET /api/v1/buyer/images/{digest}/status Build state + egress-test results
GET /api/v1/buyer/images/{digest}/build-logs Build log tail
DELETE /api/v1/buyer/images/{digest} Soft-delete an image

Account

Method + path Purpose
POST /api/v1/accounts/api-keys Mint an API key ({ "name": "..." }; plaintext returned once)
GET /api/v1/accounts/api-keys List key prefixes + metadata
DELETE /api/v1/accounts/api-keys/{id} Revoke a key
GET /api/v1/accounts/credits Credit balance

Seller endpoints are covered in Selling data. The two advanced multipart upload paths are POST /api/v1/seller/files (any file format up to 256 MiB with required metadata.content_type, original bytes stored; text-like files are capped at 200 KB and DOCX packages at 8 MiB; PDF extraction is best-effort over a bounded prefix, while malformed accepted DOCX packages remain queued for durable search retry instead of silently publishing metadata-only content; opaque formats are metadata-only; arbitrary files expose file_scan_status and only clean files are purchasable/downloadable; a private raw-byte digest blocks byte-identical re-upload of purchased files) and POST /api/v1/seller/datasets (CSV, XLSX, or parquet dataset upload; native parquet is streamed to disk and accepted up to 256 MiB and requires a data dictionary, while CSV/XLSX are converted to canonical parquet projections with an auto-derived dictionary). New original-file intake preserves the uploaded file as the deliverable, with optional bounded query projections. See Selling data for staged MCP create/replace and multipart uploads up to 100 GiB. Malware inspection covers up to the first 2,000,000,000 bytes; scan_coverage.partial discloses an unscanned remainder. The digest still covers the whole file. A partial scan is not a full-file safety guarantee.

For a multi-sheet XLSX submitted without the legacy sheet selector, the dataset metadata JSON must include the current versioned acknowledgement:

{
  "title": "FY 2026 operations",
  "price_cents": 10000,
  "workbook_publication_acknowledged": true,
  "workbook_publication_ack_version": "workbook-original-publication-v1"
}

price_cents is the price of one whole-workbook listing. Worksheets are not sold separately, and the retired worksheet_price_cents field is rejected. The success response preserves the ordinary root listing fields and additively includes the server-authoritative original-file summary:

{
  "id": "workbook-listing-id",
  "status": "active",
  "original_file": {
    "version": 1,
    "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "size_bytes": 4096,
    "source_sheet_count": 2,
    "executable_content": true,
    "projection_gaps": [{"component": "Forecast", "reason": "workbook_projection_unavailable"}],
    "scan_coverage": {"scanned_bytes": 4096, "total_bytes": 4096, "partial": false}
  }
}

The deliverable is the original uploaded file, including its formulas, formatting, links, comments, charts, macros, and embedded features. Query projections are optional and do not modify this file. Conversion limits or missing formula caches produce disclosed projection gaps; invalid or encrypted packages remain refused. Executable content is disclosed, never executed by platform services. Binary Excel originals may omit an unknown worksheet count.

CSV, native Parquet, single-sheet XLSX, and XLSX with explicit sheet retain the singular listing response shape. New intake delivers the original rather than substituting canonical Parquet.

GET /api/v1/seller/documents likewise returns a workbook as one root row with the reference-only workbook summary. MCP list_my_listings uses the same root projection. Original-file byte replacement uses the staged replacement flow, which appends a new version and preserves prior owners' bytes. Legacy workbook roots retain their existing mutation restrictions.

Workbook intake and mutation errors use stable machine-readable codes:

HTTP code Meaning
400 workbook_acknowledgement_required The multi-sheet publication act was not confirmed.
400 workbook_acknowledgement_version_invalid The acknowledgement version is absent or stale.
400 workbook_metadata_inapplicable Workbook-only metadata was sent for a non-XLSX upload.
422 worksheet_pricing_retired The retired per-worksheet price was supplied.
409 workbook_payout_setup_required The seller cannot publish paid workbook products yet.
409 workbook_frozen A published root or worksheet mutation is not supported.
413 workbook_request_too_large The upload exceeds the dataset request cap.
422 workbook_invalid OOXML structure or data is invalid or unsafe to convert.
422 workbook_no_data_sheets No worksheet contains publishable tabular data.
422 workbook_limit_exceeded A bounded workbook conversion limit was exceeded.
503 workbook_scanner_unavailable Raw-source malware scanning could not establish a clean verdict.
503 workbook_capacity_unavailable The bounded workbook intake lane is busy; retry later.

The portal shows and sends the v2 acknowledgement for an XLSX because only the server can safely inspect its worksheets. The server enforces it when multiple data-bearing sheets are found; a single data-bearing sheet remains an ordinary dataset listing and still returns any workbook-format warnings.

Production enables LARGE_LISTING_ENABLED. The REST-only resumable upload flow is POST /api/v1/seller/uploads → POST /api/v1/seller/uploads/{upload_id}/parts → POST /api/v1/seller/uploads/{upload_id}/complete plus DELETE abort. These routes are absent when the flag is off (the recovery kill-switch). complete answers 400 when a declared-parquet upload fails footer validation (same bounds as POST /api/v1/seller/datasets), and 409 once any buyer owns the listing: sold bytes are frozen, and replacement content ships as a new listing rather than overwriting the object those buyers paid for.

Replace a pending SAT deliverable (seller-human REST)

The authenticated portal can stage a corrected deliverable against a pending SAT-backed Change Request. These four Cognito-only routes are owner-scoped and are not available to seller agents, scoped API keys, MCP, or the public router:

Method + path Purpose
POST /api/v1/seller/change-requests/{id}/replacements Start or replay an idempotent replacement candidate and receive a short-lived quarantine upload_url. Body is the closed {idempotency_key,name,content_type,size_bytes,content_sha256} object.
POST /api/v1/seller/change-requests/{id}/replacements/{replacement_id}/complete Verify the exact versioned upload and enqueue safety checks.
GET /api/v1/seller/change-requests/{id}/replacements/{replacement_id} Read the closed owner projection and, after a successful gate, the successor_cr_id.
POST /api/v1/seller/change-requests/{id}/replacements/{replacement_id}/cancel Cancel a pre-swap candidate; replay is idempotent.

Candidate states are awaiting_upload, queued, scanning, gating, ready_for_review, refused, timed_out, cancelled, and abandoned. Until ready_for_review, the predecessor review is unchanged and no listing, Change Request, offer, payment, ownership, or delivery is created. A successful gate atomically supersedes the predecessor and exposes exactly one successor Change Request with fresh pins. The seller must explicitly approve that successor through the existing approve route; upload and polling never approve or send an offer. See Selling derived listings for the portal handoff and failure states.

The routes fail closed with replacement_invalid_json or replacement_unknown_field (400), replacement_metadata_invalid or replacement_digest_mismatch (422), replacement_too_large (413), an opaque replacement_not_found (404), and state/idempotency conflicts or replacement_already_swapped (409). A transient presign or object check is replacement_unavailable (503) and does not advance the candidate.

Seller slice authorization (pending counsel review — not yet GA)

Slice-on-demand lets a seller offer buyers derivative sub-slices of a large dataset listing. The connected portal workflow is documented in Selling derived listings. Before any slice policy can be enabled, the seller's account must accept the current Slice Authorization rider (a versioned legal instrument). Acceptance is account-level — once per account per rider version — and recorded append-only as compliance evidence. These routes exist only when the deployment has SLICE_ON_DEMAND_ENABLED; APP_ENV=prod refuses to boot with the flag on until the rider is counsel-ratified. Enabled non-production deployments also require AWS_REGION and TRUST_RECORDING_ENABLED=true; no model or provenance stub is substituted. Slice model settings are restricted to reviewed Claude-on-Bedrock profiles, and startup verifies usable AWS credentials and access to each configured model. The account is always derived from the authenticated token — never from the request body.

Method + path Purpose
POST /api/v1/seller/slice-rider/accept Accept the current rider version for your account
GET /api/v1/seller/slice-rider/status Whether your account has accepted the current version

POST /api/v1/seller/slice-rider/accept takes an empty body — the rider version is the server's pinned current version, and the source IP and user-agent are stamped server-side from the request (never read from the body). It returns 201 with { "rider_version": "slice-rider-v0-draft-e4e4e8ce7beb", "accepted_at": "..." }. Re-accepting the same version is idempotent: it returns 201 with the original acceptance timestamp preserved and writes no new row.

GET /api/v1/seller/slice-rider/status returns 200 with { "rider_version": "slice-rider-v0-draft-e4e4e8ce7beb", "accepted": true, "accepted_at": "..." }. accepted is false when the account has never accepted, or has accepted only a now-superseded rider version (accepted_at is then omitted).

Once the rider is accepted, the per-listing slice policy is set and read through these routes (same flag gate; the account is from the token, the listing id is the path resource — never the body):

Method + path Purpose
PUT /api/v1/seller/documents/{id}/slice-policy Create/replace the slice policy for one of your parent listings
GET /api/v1/seller/documents/{id}/slice-policy Read the slice policy for one of your parent listings

PUT body carries the policy: enabled, kinds ([] or ["row_match"]), key_columns, per_row_cents (US cents/row; fractional/sub-cent allowed, e.g. 0.5; stored at 1/10,000-cent resolution — nearest micro-USD — and quoted from the rounded rate), min_price_cents (≥ 100 — a mandatory $1.00 floor), optional max_price_cents, the bounded-job fields (max_rows_per_slice, max_queries_per_job, max_jobs_per_buyer_per_day; positive when supplied, with omitted max_queries_per_job / max_jobs_per_buyer_per_day defaulting to 500 / 3), and the disclosure_mode / review_mode / watermark_mode enums. kinds is the buyer-request allow-list, not the seller segment job kind. An enabled policy with kinds: [] still anchors profile-backed seller authoring but does not advertise buyer row-match requests. Every enabled policy requires a positive per_row_cents because it is the fallback when seller-authored slices omit an explicit price. max_cumulative_rows_per_buyer is accepted and stored as a reserved policy value but is not enforced in this beta; cluster-wide, cross-listing, and cumulative-row metering remain deferred. Any listing_id in the body is ignored — the path id wins.

Status matrix:

  • 200 — policy saved (PUT) or read (GET). The PUT body is { "policy": { … }, "warnings": [ … ] }, where warnings are non-blocking cannibalization notices (such as per-row pricing that reconstructs the dataset below the parent price) — surfaced, never fatal.
  • 409 — { "rider_acceptance_required": true, "rider_version": "…", "rider_url": "/api/v1/seller/slice-rider/accept" }. The account has not accepted the current rider; nothing is written. Accept the rider (portal/REST) and retry — acceptance is not part of this route.
  • 409 — { "error": "platform-created slice listings are immutable", "code": "platform_slice_frozen" }. The target is a materialized slice child, not a seller-editable parent; nothing is written.
  • 404 — opaque not-found: the listing is not yours, does not exist, or (on GET) has no policy configured. The three are deliberately indistinguishable.
  • 400 — a validation error: min_price_cents below the $1.00 floor, a missing or non-positive per_row_cents on an enabled policy, an unknown key column, a max_price_cents below the floor, or an invalid enum or non-positive supplied usage cap. The message names the offending field; no write happens.

Seller segment builder routes

The flagged seller portal's Slicing → Segments tab consumes the following owner-scoped routes. They are absent when slice-on-demand is off and return the same opaque 404 for missing, ineligible, or other-seller parents. See Selling derived listings for the guided profile, preview, and creation flow.

Method + path Purpose
GET /api/v1/seller/documents/{id}/slice-profile Read profile metadata and typed column facets
POST /api/v1/seller/documents/{id}/slice-profile/refresh Queue re-profiling (202; acknowledgement only)
POST /api/v1/seller/documents/{id}/slice-preview Preview a SelectionSpec and receive authoritative counts/sample/edge cases
POST /api/v1/seller/documents/{id}/slice-agent/compile Compile { "instruction": "…" } into the closed filter grammar
POST /api/v1/seller/documents/{id}/slice-segments Queue eager materialization of a reviewed segment

Both profile routes are authenticated. The account comes only from the verified principal and {id} only from the path; neither route accepts seller, account, or listing identity in a request body. Missing listings, other sellers' listings, unsupported parents, parents without a slice policy, and profiles that have not completed all return the same opaque 404 { "error": "not found" }.

GET …/slice-profile returns the profile object directly:

{
  "profiled_at": "2026-07-15T20:00:00Z",
  "profiler_algo_version": "slice-profile-v2",
  "row_count": 12,
  "columns": [
    {
      "name": "category",
      "type": "text",
      "distinct_count": 2,
      "values": [
        { "value": "Whiskey", "count": 7 },
        { "value": "Gin", "count": 5 }
      ]
    },
    {
      "name": "active",
      "type": "bool",
      "distinct_count": 2,
      "values": [
        { "value": true, "count": 9 },
        { "value": false, "count": 3 }
      ]
    },
    {
      "name": "unit_price",
      "type": "float",
      "distinct_count": 12,
      "min": 10,
      "max": 100
    }
  ]
}

Column type is the closed set text | bool | int | float | date. Profile scalars preserve their JSON type: text and date values are strings, booleans are JSON booleans, and integer/float bounds are JSON numbers. distinct_count is present for every column; values is present only for low-cardinality text/bool columns (at most 200 distinct values); and min / max are present only for numeric/date columns. The response never contains row keys, row ordinals, an object/profile reference, a profile hash, or a separate segment enumeration.

POST …/slice-profile/refresh takes an empty body. A successful response is an exact empty 202 Accepted, written only after the refresh has been durably marked pending. It does not mean profiling has completed. GET may return the opaque 404 while that generation is pending; reload or poll GET later and compare profiled_at. Repeating refresh is safe and does not grant a caller any additional visibility.

If deterministic parent-data limits or invalid Parquet pages prevent profiling, the policy remains saved and its response includes a safe warning; the profile stays at the same opaque 404. Automatic retries stop for that generation. Replace or repair the parent dataset, then request refresh again.

Preview JSON is { "selection_spec": {…}, "sample_seed"?: "…" }. For an optional CSV/XLSX/TXT key list of at most 1 MiB, send multipart spec (the same JSON envelope) plus file; the response may return an opaque queries_ref for reuse in selection_spec.key_match. Clients must treat that reference as opaque and invalidate it when the parent, spec, key columns, or file changes. Invalidation means the client stops reusing the reference; it does not promise immediate deletion of the corresponding temporary server object.

Multipart preview requires selection_spec.key_match. The server parses and stores the query list under the authenticated actor, parent listing, and parent seller, runs the bounded row matcher against the pinned profile generation, and passes that evidence through the same waterfall evaluator used for materialization. A queries_ref from another actor or parent is rejected; it is never a download or presign target. The entire upload part, not merely its parsed rows, is capped at 1 MiB.

SelectionSpec version 1 is closed: filters contains only typed value_in (text/bool) or range (numeric/date) entries; optional agent contains the instruction, compiled filters, and adjudications; optional key_match contains queries_ref and key_columns; pins.include / pins.exclude are applied last. Preview responses expose row_count, waterfall, a sample of at most 20 rows, optional edge cases, and queries_ref. A portal must not display row_key; it is only an internal reference for pins/adjudications.

Creating sends exactly { "display_name": "…", "selection_spec": {…}, "price_cents"?: 100 } and returns { "job_id": "…" }. The job id acknowledges queued materialization with 202 Accepted; it does not mean a child listing is already live. When selection_spec.key_match is present, its opaque queries_ref must still be live and bound to the same parent, parent seller, and authenticated acting account that created it during preview. The worker runs the same bounded matcher and immutable evidence checks used for buyer row-match, but the resulting seller segment remains a shared standing listing whose purchase ACL is copied from the parent at child creation. In this beta, later parent ACL changes are not propagated to an already-materialized segment or re-checked against the parent during child purchase. Custom children never appear in the parent's slice summary; they remain discoverable through ordinary catalog/detail access and purchasable under the stale copied ACL. Disable the parent's Slice-on-Demand policy before tightening access and do not rely on the parent ACL change alone to restrict the existing child. The buyer-only max_rows_per_slice rejection cap does not apply to this seller-authored segment; a separate platform evidence row/byte ceiling still fails oversized jobs safely. Once creation returns a job id, the validated queries are snapshotted into that job's immutable private artifacts, so later preview-reference expiry cannot invalidate queued work. The optional direct price must be at least 100 cents and no greater than the lower of the slice policy maximum and the deployment's listing price ceiling. There is no price_cents field when the seller leaves price blank; server policy pricing applies. A successful response without a nonempty job id leaves status unknown and must not be retried unchanged. There is no segment-offer toggle, profile-row exclusion mutation, custom-child list, or child deactivation operation in this builder contract.

All three builder routes return an opaque 404 { "error": "not found" } for a missing, unsupported, inactive, unprofiled, or other-seller parent. Preview and compile do not require rider acceptance or payout setup. Segment creation checks both against the parent listing's seller even when an operator acts on behalf of that seller; the operator remains the recorded requester. Missing current rider acceptance returns 409 with rider_acceptance_required, rider_version, and rider_url. Missing payout setup returns 409 with code: "payout_setup_required". Invalid selection grammar, price, instruction, or upload returns 400 and creates no job. Temporary extraction-model or profile-source failures return 503; callers may retry those without changing the request.

Seller slice activity and review contract

These owner-scoped routes back the flagged portal Activity and Review tabs. They are registered only when SLICE_ON_DEMAND_ENABLED is on and remain outside the production/GA API while the Slice Authorization rider is counsel-gated. Every resource lookup starts from the account in the verified principal; foreign and nonexistent resources share the same opaque response. The seller journey and buyer-visible boundary are explained in Selling derived listings.

Method + path Purpose
GET /api/v1/seller/documents/{id}/slice-jobs?kind=&state= Activity for one owned parent; state=pending_review selects actionable review holds
GET /api/v1/seller/slice-jobs/summary Owner-wide { "pending_review_count": … } for the Listings navigation badge
GET /api/v1/seller/slice-jobs/{job_id}/match-report Full seller-only report for one owned job
POST /api/v1/seller/slice-jobs/{job_id}/review Atomically approve or decline a pending review
POST /api/v1/seller/slice-children/{listing_id}/deactivate Retire one unowned, frozen seller-segment child so a later materialization can use a new price

The activity response is this closed seller projection:

[{ "job_id": "…",
   "kind": "seller_segment | row_match",
   "state": "processing | pending_review | active | offered | purchased | not_available | expired | deactivated",
   "requester": { "account_ref": "…", "display": "…" },
   "spec_summary": "…",
   "outcome": "pending | matched | near_miss | unmatched | declined | expired | failed",
   "price_cents": 1250,
   "created_at": "…",
   "has_report": true,
   "sales_30d": { "units": 2, "seller_net_cents": 2500 },
   "review": { "state": "not_required | pending | approved | declined",
               "expires_at": "…" } }]

review.expires_at is optional. seller_segment jobs always use review.state=not_required; pending, approved, and declined review states are valid only for row_match jobs. Unknown internal job states and incompatible kind/review combinations are never serialized. sales_30d is an authoritative rolling 30×24-hour projection: standing segments can sell more than once, so clients must not infer units from job count or net revenue from price_cents. The Activity KPIs sum the supplied units and seller net by kind. They count requests only from valid created_at values inside the same inclusive UTC window and exclude future/invalid dates or unsafe/negative metrics. The pending-review KPI and navigation badge use the selected parent's closed job list and the owner-wide summary endpoint, respectively. The account-wide badge is never derived by fanning out across a possibly truncated listing page.

The match-report response is the seller's full, owner-scoped view:

{ "queries": [
    { "q": "makers mark 46 750ml",
      "status": "matched | near_miss | unmatched",
      "row_keys": ["…"],
      "match_class": "exact | normalized | fuzzy | reasoned",
      "confidence": 0.97 }
  ],
  "totals": { "queries": 500, "matched": 412,
              "near_miss": 31, "unmatched": 57 },
  "rows_selected": 431,
  "buyer_disclosure_mode": "aggregate_only | per_query" }

The disclosure mode is snapshotted with the job, so historical UI must not read the parent's current policy to explain what the buyer saw. The portal's CSV is generated locally from this explicit seller projection, neutralizes spreadsheet formula prefixes, and never uploads the report. This DTO must not be reused by a buyer endpoint: it contains the seller's full query list and internal row keys.

Review mode stores the match report, selected-row checkpoint, row count, and frozen quote first, then places the job on a seller hold before any child is materialized or an offer becomes visible to the buyer. Review sends exactly { "action": "approve" } or { "action": "decline" }; no account, seller, requester, parent, or job identity belongs in the body. A 200 returns the canonical seller job projection. The server performs one atomic transition from review.state=pending; stale or duplicate decisions return 409 and cannot create two offers or children. Missing, foreign, wrong-seller, and ineligible-parent resources all return the same opaque 404. After any response whose outcome might be stale or unknown, clients reload the authoritative list.

Child deactivation accepts no identity or price body. It is available only for an owned-parent, manifest-backed seller_segment child with no ownership row; ordinary listings, row-match children, missing ids, and foreign children are the same opaque 404. Purchase-first or an existing owner returns 409 and retains the active listing and paid artifacts. Deactivation-first makes the listing inactive before deleting only the canonical Parquet and XLSX objects. If object cleanup fails, the inactive state remains safe and repeating the request retries cleanup. Success is an empty 200; a later seller create produces a fresh job and listing id rather than mutating the frozen child.

Seller policy authoring transition

Seller-authored licensing policy is moving to the replacement terms workflow. The retired offer-authoring routes and inline create-time offer block are not part of this API. Every listing receives one materialized platform-default base rung at its listing price, with an immutable offer_id used by the buyer purchase contract. Existing historical offer rows remain immutable; see Licensing for the buyer-facing menu and offer binding.

Derived listing sign-off

Opt-in seller capability. These REST routes exist only when DERIVED_LISTINGS_ENABLED is on. They are enabled in the reviewed production seller-derivation deployment and absent—not stubbed—when the flag is off. Creation is available only through the create_derived_listing MCP tool to a verified seller-worker; this applies to both standard and sample children. Creation atomically commits the child, its Change Request, and its referring Inbox delivery. Review is a human act on the Cognito-only Change Request routes, so an amn_ agent API key cannot approve, return, or decline. There is deliberately no sign_off MCP tool.

Method + path Purpose
GET /api/v1/seller/change-requests/{id} Sole owner-facing review resource. Returns the typed Change Request, complete review pins, lifecycle fields, and a review packet containing the derived listing, parent, artifacts, frozen terms, history, consent, and available containment metadata. Sealed creator/approver identities, live-slot/idempotency coordination, suppression-review internals, and artifact bytes are omitted.
POST /api/v1/seller/change-requests/{id}/approve Cognito-human approve act. Echo the complete CR pin set; stale pins return 409 cr_pins_stale. There is no MCP equivalent.
POST /api/v1/seller/change-requests/{id}/return Cognito-human request-change act. Echo the complete CR pin set and a non-empty note (maximum 4,000 characters). There is no MCP equivalent.
POST /api/v1/seller/change-requests/{id}/decline Cognito-human decline act. Echo the complete CR pin set; an optional note is retained with the terminal disposition. There is no MCP equivalent.
POST /api/v1/seller/derivation-authorization/accept One-time acceptance of the Derivation Authorization instrument (body ignored; version from the pinned loader)
GET /api/v1/seller/derivation-authorization/status {instrument, version, accepted, document_markdown}

Change Request act bodies fail closed with stable 400 codes: unknown fields return cr_unknown_field, malformed or trailing JSON returns cr_invalid_json, an absent pin object returns cr_pins_required, and incomplete or conflicting pin forms return cr_pins_invalid.

The User Inbox refers to the owning Change Request rather than acting as a second decision queue. Its act_href resolves to POST /api/v1/seller/change-requests/{id}/approve, .../return, or .../decline; every act echoes the CR's fresh owner projection, read immediately before the write. The portal never submits the Inbox delivery's cached copy. The act body carries payload_sha256, primary_artifact_sha256, listing_revision, and pins_sha256. A SAT-attached request must use the nested pins form and echo only sat_binding.spec_sha256, coverage_summary_sha256, and transcript_sha256; SAT bundle coordinates and price are server-owned and are re-read at apply. Non-SAT requests use the four ordinary pins. Approve may explicitly carry accept_derivation_authorization:true to fold the current instrument acceptance into the same transaction. Return additionally requires a non-empty note of at most 4,000 characters. The portal never navigates the browser to an API action URL.

The portal exposes the human portion of these routes under Sell → Inbox. Selecting a pending child opens its review packet as a Change Request inside My Listings. The seller can download the owned child with the normal GET /api/v1/ownership/{id}/download path, approve with the packet's primary_sha256, request a change, or decline through a two-step confirmation. The former agent sessions, agent images, and agent marketplace views are not part of this seller review flow.

The seller-worker MCP request never accepts an account or seller identity. create_idempotency_key is required, canonical opaque ASCII (maximum 200 bytes), and account-scoped. Repeating it returns the already-created child and Change Request, including after a concurrent retry loses the unique create race; it never mints a second child or Inbox delivery. See the MCP tools reference for the complete creation schema.

derivation_class is standard by default or sample for Sample Data. A sample create requires price_cents: 0, open ACL, an eligible original columnar parent, and a row-capped columnar artifact. Seller listing reads expose sample_data_enabled, owner-only sample_listing_id, and the default-deny sample_parent_eligible answer used by the portal (false for restricted ACL, enforcement/Consign, workbook membership, derived ancestry, or an unavailable eligibility check); PATCH accepts only sample_data_enabled. The public parent projection includes sample_listing_id only while the complete live-sample predicate holds, and a public sample child includes sample_of_listing_id. Samples are omitted from catalog/search lists. Buyers acquire the public child through the ordinary POST /api/v1/purchases endpoint at zero price and download through GET /api/v1/ownership/{id}/download; settlement emits sample_acquired without sale, debit, fee, or payout entries.

Each artifact sets exactly one of text, standard base64, or stage_id; inline artifacts require name and content_type. The authenticated MCP envelope is capped before decoding, while the intake service separately enforces its decoded artifact count and byte limits.

For create_derived_child, the owner Change Request's review packet is {listing_id, title, price_cents, currency, status, published, parent_listing_id, parent_title?, method, actor_kind, pending_reason?, derivation_note?, primary_sha256?, artifacts[], frozen_terms?, history[], consent} — artifact metadata only ({role, content_type, size_bytes?, sha256}), never content. See the MCP tools reference for the field-by-field notes.

A derived listing's content and commercial state are frozen after creation. For a sidecar-less agent-derived child, the sole lifecycle exception is a seller-directed, one-way retirement through PATCH /api/v1/seller/documents/{id} with {"status":"inactive"} as the only effective recognized update. As on other partial updates, unknown properties and recognized properties set to null are ignored; any other recognized non-null update is refused. Retirement removes the child from the pending queue and buyer surfaces and prevents future purchases. It does not alter or delete content, price, ACL, license terms, sign-offs, ownership, or prior buyers' download rights. Reactivation, a mixed retirement-plus-edit request, every other listing edit, set_listing_acl, the slice policy remain refused with 409 platform_slice_frozen. Platform-created slice children use their dedicated unsold-child lifecycle instead. To change a derived listing, create a new one. Editable derived listings — where an unsigned edit pauses the listing until you re-approve it — are planned and not yet available.

An approve must name the complete state it adopts. The act echoes payload_sha256, primary_artifact_sha256, listing_revision, and pins_sha256 from a fresh owner Change Request read. The server refuses stale pins with 409 cr_pins_stale and returns the fresh pin set, so the seller can re-read the review before deciding. A decline atomically records the audit act and the child's terminal rejected disposition. The rejected child leaves every buyer surface; rework creates a new child and Change Request. A decline can never un-publish an approved child.

Amnetic records derivation consent against the general Terms of Service you accepted at signup, so no separate acceptance is required, the accept_derivation_authorization flag below is inert, and the derivation_authorization_acceptance_required refusal does not fire. Both apply only where a deployment is configured to record the separate per-account Derivation Authorization instrument instead; the accept and status routes remain available in either configuration.

Set accept_derivation_authorization: true on the approve to record acceptance of the current instrument in the same transaction when a version roll has flipped your consent currency. The remaining refusals are:

Status code Meaning
409 derivation_authorization_acceptance_required You have no current-version Derivation Authorization acceptance and did not set the accept flag. The body names the instrument, required_version, and accept_url.
409 listing_inactive The child is not active, and a non-active derivation is not publishable. This includes a sidecar-less agent-derived child the seller retired through the inactive-only PATCH, as well as a platform-suppressed child (quarantine or resale adjudication). Reactivation is unavailable in this release.
409 platform_managed_slice The child is a platform-managed slice listing — those are flow-approved under your standing policy, never human-signed.
409 already_approved You tried to decline a child that is currently approved. Approve is monotone: a decline can never un-publish a signed-off listing.
404 — Opaque: missing, not yours, and not-a-derivation are byte-identical, so this surface is not an inventory oracle.

Seller review expiry

Task expiry is a worker transition, not a route. A pending approval bundle carries a review deadline (the review window). When it passes while the bundle is still pending, the single lifecycle sweeper — the one worker; the approval-bundle component owns no worker and no clock — expires the live bundle (its live_slot cleared, so the revision is never a live approvable object again) and closes the transaction with close_reason: review_expired. There is no REST route that triggers expiry: it is scheduled with SAT (enabled in production; 404/absent only if the SELLER_TXN_ENABLED kill-switch is off), and is the point at which an approvable object becomes permanently un-approvable.

Seller sales stats

GET /api/v1/seller/stats returns seller-level totals and one row for every listing owned by the authenticated account (including unsold listings):

{
  "totals": {
    "total_sales": 3,
    "gross_revenue_cents": 8000,
    "marketplace_fees_cents": -1600,
    "net_earnings_cents": 6400,
    "balance_cents": 6400,
    "withdrawable_balance_cents": 6400
  },
  "listings": [{
    "listing_id": "9dad1234-…",
    "sales_count": 3,
    "gross_revenue_cents": 8000,
    "last_sale_at": "2026-07-14T12:00:00Z",
    "offer_breakdown": [{
      "offer_key": "internal-training",
      "sales_count": 2,
      "gross_revenue_cents": 6000
    }]
  }]
}

offer_breakdown is always present on seller stats. It is nested per listing, ordered by stable offer_key, and contains only completed transactions that have a license grant. Amounts are immutable transaction amounts charged at sale time, not current listing/offer prices. Renewals and paid upgrades count once; retries, already-licensed, and funded-only outcomes do not. Retired or re-versioned offer ids with the same key roll up together. A listing with no grant-backed sales returns []. Pre-evidence transactions remain in the parent lifetime totals without being assigned a fabricated offer bucket. When licensing is off, offer_breakdown is absent and the grant query does not run.

Seller agent transactions (SAT) — enabled in production

Enabled in production. These seller-agent-transaction endpoints are live. They 404 by absence only if the recovery kill-switch (SELLER_TXN_ENABLED) is off; rollback is .github/workflows/flag-rollout.yml.

Seller pricing rules

The platform's deterministic pricing engine prices transactions from an immutable compiled pricing-rules/v2 document. The document is listing-scoped: it contains one complete entry per priced listing, including the source-pricing fingerprint, and has no seller-wide default or fallback. Listing pricing edits are the only authoring path. The platform normalizes the listing and compiles the seller's complete active document in the same transaction, reusing the live version when canonical bytes are unchanged or appending exactly one new immutable version when they change.

Route Meaning
GET /api/v1/seller/pricing-rules Read-only history: the active compiled rule-set version and document, plus the immutable version list (version, content_sha256, created_at).

There is no full-document write or old-version activation route. To restore historical pricing intent, apply the desired values through the owner-scoped listing pricing update; that reviewed listing change compiles a new immutable version without overwriting unrelated listings.

An active quote must find the exact listing entry. A missing entry returns ErrNoPricingRulesForListing and no price. The engine also compares the entry's source_pricing_sha256 with the current owner-scoped listing pricing source; ErrPricingRulesOutOfSync refuses a quote when they differ, so a missed listing writer cannot silently price from stale evidence. Pinned historical quotes use the exact immutable version selected by the Spec and intentionally do not compare against current listing state.

Seller standing rules

Append-only ledger of what the system has learned "always allow". v1 is write + list only — nothing evaluates it.

Route Meaning
GET /api/v1/seller/standing-rules Your rules, newest first, bounded page (?limit=/?offset=).

Seller task queue

The seller task queue is the seller's home surface: every pending approval bundle waiting on you, oldest-first by review deadline, with whole-set counts so the header is never a guess. Cognito-only (the queue is the human seller's; neither transaction agent has it — both are event-woken, never poll).

Route Meaning
GET /api/v1/seller/tasks?kind=&overdue_only=&limit=&after= Your bundle-native negotiation and inquiry tasks, oldest-first by review deadline. Derived approvals are intentionally absent and appear in the Change Request queue. items[] rows: {bundle_id, transaction_id, kind, revision, created_at, expires_at, overdue, due_in_hours, requester_label, buyer_type, is_redraft}. requester_label is the buyer's declared legal name, or else a neutral "Buyer " placeholder — never their account name or email. Keyset-paged via an opaque ?after= cursor; kind (negotiation_escalation/inquiry_escalation) and overdue_only=true narrow the page.

Approval bundles (seller review)

The approval-bundle surface is the human seller's review boundary. It is Cognito-only, owner-scoped, and 404-by-absence only if the SELLER_TXN_ENABLED recovery kill-switch is off:

Route Meaning
GET /api/v1/seller/bundles/pending Pending approval-bundle revisions, oldest review deadline first.
GET /api/v1/seller/bundles/{id} One bundle-native negotiation or inquiry packet. Derivation-approval bundles return the same opaque 404 as a missing or foreign bundle; their sole owner review resource is the Change Request. Attachment object refs remain server-only and ready attachments receive short-lived URLs.
POST /api/v1/seller/bundles/{id}/act Bundle-native negotiation and inquiry exits only. A derivation_approval bundle is read support for its owner Change Request and cannot be acted on here.

Derived approval is a human act through the owner Change Request; neither seller agent role can perform it. Approval creates the offer from the platform pricing engine. It does not claim that a buyer accepted or paid until the later buyer steps succeed.

Buyer transaction status

The buyer transaction-status surface is the buyer's read over their own seller agent transactions: where each stands, whose turn it is, the one live deadline, and (once an offer exists) their current offer. It is the read counterpart to the write/act edges and the thread surface.

Route Meaning
POST /api/v1/sellers/{seller_id}/transactions Open or replay a transaction with a public seller. Cognito ID token + Content-Type: application/json; exact body {"idempotency_key":"…"}. The key is 1–200 ASCII letters/digits or ._:-. Buyer identity is sealed from the principal and seller identity from the path; identity fields anywhere else are rejected. Same buyer/key/seller returns the original row; a different seller for that key is opaque 409 idempotency_conflict; unavailable/self seller is opaque 404; the open cap is 429. The aggregate, seller_txn_opened event, and replay binding commit atomically.
GET /api/v1/transactions/{id} Your own transaction's status detail.
GET /api/v1/transactions Your own transactions, newest first.

Both are authenticated with your Cognito ID token and are owner-scoped: a transaction that is not yours — or an id that does not exist — returns the same opaque 404/not_found, so you can never probe whether another buyer has an open negotiation.

The buyer thread and payment routes are part of the same production-on SAT surface (404 only if the SELLER_TXN_ENABLED kill-switch is off):

Route Meaning
POST /api/v1/transactions/{id}/thread Post a buyer-authored message (body and attachment caps are refused, never truncated).
GET /api/v1/transactions/{id}/thread Read the buyer's own fenced, keyset-paged thread.
GET /api/v1/seller/transactions/{id}/thread Seller-side read of the transaction thread; identity is the authenticated seller.
GET /api/v1/transactions/{id}/payment Read the current offer, obligation, and settlement attempts.
POST /api/v1/transactions/{id}/offers/{offer_id}/accept Accept the current offer and create the payment obligation.
POST /api/v1/transactions/{id}/offers/{offer_id}/decline Decline the current offer.
POST /api/v1/transactions/{id}/payment/abandon Abandon an accepted obligation.
POST /api/v1/transactions/{id}/payment/wallet Pay an open obligation from wallet credit.
POST /api/v1/transactions/{id}/payment/card Start card payment for an open obligation.

All buyer reads/acts derive identity from the verified Cognito principal and return an opaque 404 for another buyer's transaction. Payment and delivery are not implied by an approval or an offer; the state and settlement records are the source of truth.

System-authored entries carry author_kind: "system", plus the closed system_template_id and its system_params object. The replacement notice template status.deliverable_updated_pending_review is emitted only after a successful seller deliverable swap; it tells you that the earlier draft was never approved or offered and that the updated deliverable awaits seller approval. It contains no filename, digest, scan result, or private coverage.

Status detail fields:

Field Meaning
state The storage state: converging, drafting, seller_review, offered, awaiting_payment, delivered/closed, or closed_revivable.
state_label Server-rendered label for the state (e.g. Awaiting payment).
whose_move Whose turn it is: requester (you), owner (the seller), platform (system working), or none (terminal).
deadline The single live clock of the current state (its kind and expires_at), absent when no clock is running.
offer Your current offer: id, price, currency, expiry, delivery_format, immutable terms_ref, the bounded coverage summary, frozen gap options, and counter round. Absent before an offer exists (nothing is disclosed pre-offer).
thread The keyset thread cursor: latest_entry_id (ULID of the newest entry, or empty) and unread_hint (0 until read-tracking ships).
revive_available True when the state is closed_revivable and can be revived.

Everything is server-rendered from transaction state — state_label, whose-move, and the deadline come from one platform vocabulary, so the buyer portal, seller portal, and email never disagree about what a state is called.

Buyer counter + revive (SAT OF9 / T-9)

The buyer counter and revive acts on a seller-agent transaction. Both are Cognito-only buyer edges — identity comes from your verified token, never the body — and both are absent (404) while the feature flag is off. The offer/tx ids come from the path.

Method + path Purpose
POST /api/v1/transactions/{id}/offers/{offer_id}/counter Offer a strictly-lower price for the current offer (a counter at or above the offer price is refused; the transaction-wide counter cap is enforced).
POST /api/v1/transactions/{id}/revive Re-open an expired (closed_revivable) transaction: re-issue the offer under the standing seller approval (0 pings) when every gate passes, else re-enter drafting for a fresh offer.

POST …/counter takes

{ "amount_cents": 800, "message": "I'd pay 800 for this." }

An in-rules counter re-issues the offer at the countered price under the standing approval (the transaction stays offered); an out-of-rules counter withdraws the live offer, opens a negotiation_escalation bundle, and moves the transaction to seller_review. The offer_id path segment must equal the transaction's live open offer: a stale or wrong offer_id counters nothing and is refused (409, the same pinning posture as accept), so a client can never silently target a different offer than the one it named. Both acts return the post-act transaction row:

{
  "transaction_id": "…",
  "state": "offered",
  "offer_round": 2,
  "out_of_rules_counter_count": 0,
  "current_offer_id": "…"
}

Refusals: a counter at/above the offer price, an invalid amount/currency, or an over-length message → 422 invalid_counter; a non-owner or missing transaction → opaque 404 not_found; a capped transaction → 429 counter_cap_reached; a transaction not in offered → 409 transaction_state_conflict. POST …/revive takes an empty body and returns the same post-act projection (state offered on a 0-ping re-issue, drafting on the redraft path). A revive re-issues the offer at the old (expired) offer's price — never a fresh re-quote — and only when every hard gate passes (bundle approved, child present and unretired with its artifact unchanged, ACL account-mode to you, terms still valid under the current vocabulary); any gate failure instead takes the drafting redraft path.

Buyer license read

Every purchase records an append-only license grant — the evidence of exactly what license you bought and under which versioned legal instrument. Buyers can read that evidence back. These surfaces are read-only and mint no purchase handle. Counsel review still governs how composed terms are interpreted; it does not hide the routes. Identity is always the authenticated buyer — never the request.

Method + path Purpose
GET /api/v1/ownership/{listingId}/license The rendered license text-of-record for a listing you own

GET /api/v1/ownership/{listingId}/license returns the read-time hash-verified operative license text for the newest grant you hold on the listing. Before serving, the platform re-hashes the stored text and confirms it matches the hash pinned into your grant at purchase time; if the stored evidence is missing or has drifted, the read fails closed with a 500 and serves no text (it never substitutes or re-renders a fallback). It returns 200 with:

{
  "grant": {
    "grant_id": "…",
    "offer_key": "standard",
    "terms": { "...": "..." },
    "terms_schema_version": "lts/1",
    "dls_version": "tos/5.3-draft",
    "granted_at": "…",
    "instrument_status": "pre_ratification"
  },
  "rendered_text": "…the full operative license text of record…",
  "rendered_sha256": "…",
  "dls_sha256": "…",
  "terms_hash": "…"
}

instrument_status is pre_ratification while purchases are governed by the ToS draft (before the Data License Schedule is counsel-ratified) and is omitted once a ratified instrument governs the grant. The grant view is deliberately token free — it is proof of what you already hold, never a handle to buy again. A listing you do not own — or one that does not exist — returns an opaque 404 (the endpoint is not an existence oracle).

When the flag is on, two existing surfaces also carry license fields:

  • GET /api/v1/purchases/{id} adds offer_key and terms_hash (the licensing tier purchased and the hash of its composed terms) for transactions that minted a grant. Both are omitted when license terms are off, so the wire is unchanged on the dark path.
  • GET /api/v1/ownership adds a per-row license block: { "newest": {…grant view…}, "grants": [ …newest-first history… ] } for listings you hold a grant on, or { "implied": true } for a listing you own with no recorded grant (the implied standard-license display convention — no grant row is synthesized). The operative scope of that implied "standard license" is the ToS default and is pending counsel confirmation (which is why this whole surface is not yet GA).

The CLI wraps the render endpoint as amnetic buyer license <listing-id> (add --json for the raw payload with all hashes).

Seller Inbox and Change Requests

When INBOX_ENABLED and CHANGE_REQUEST_ENABLED are enabled, Cognito sellers can read referring review deliveries at GET /api/v1/inbox and GET /api/v1/inbox/{delivery_id}, then mark or archive them with the matching POST .../read and POST .../archive routes. A derived-listing delivery carries an authorized subject projection only:

{"subject":{"change_request_id":"…","target_listing_id":"…","state":"pending_approval","pins":{"payload_sha256":"…","primary_artifact_sha256":"…","listing_revision":0,"pins_sha256":"…"}},"act_href":"/api/v1/seller/change-requests/…"}

Unauthorized or terminal subjects are tombstoned (subject omitted). The Change Request resource is GET /api/v1/seller/change-requests/{id}; actions are separate POST .../approve, .../return, and .../decline routes. Each body must echo the complete pins object. Non-SAT requests may instead use the equivalent flat pin fields; a SAT request must echo nested sat_binding with only spec_sha256, coverage_summary_sha256, and transcript_sha256. Legacy {action, artifact_sha256} bodies are rejected. return requires a trimmed, non-empty note no longer than 4,000 characters. Approve is retry-safe for an identical applied pin echo; stale pins return 409 cr_pins_stale with fresh_pins. Return and decline atomically archive the Inbox delivery and mark the unreleased child rejected; a revision is always a new child and Change Request. A direct archive of a delivery whose Change Request is still pending is refused with 409 inbox_action_pending while the CR is either draft or pending_approval; act on the Change Request so its review state and Inbox visibility cannot diverge.

Inbox pages are stable keyset pages: items are ascending by delivery ULID and after returns items strictly after that cursor. limit is clamped to the server page maximum, and total_unread covers the whole live mailbox rather than only the returned page. Native messages project a bounded body preview; referring deliveries project their preview and action through the owning subsystem's authorization, with unreadable subjects rendered unavailable.