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.

Consign report channel

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

Purchases & ownership

You act on a suggestion from outside the wall.

POST /api/v1/purchases

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

funding_mode is wallet (default; deducted from your spendable balance), card_shortfall (calculate the card charge from the current wallet shortfall), or card_full (charge the full listing price and preserve existing wallet credit). The legacy payment_method: "credit" field is accepted as a compatibility alias for funding_mode: "wallet". offer_id, expect_token, and renew_of_grant_id select a composed/priced license offer or a bounded-term renewal, and work on every funding mode — wallet and both card modes. Omit them to buy the standard tier with live-price legacy semantics.

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 (omit/null for standard) and token into this REST request as offer_id and expect_token. 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": null,
      "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, price, and token.

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 not yet GA (pending counsel review). Electing a priced/composed offer or a renewal on a card mode ships only when the deployment has LICENSE_TERMS_ENABLED; APP_ENV=prod refuses to boot with the flag on until the terms are counsel-ratified. 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_full",
  "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 Presigned download URL for an owned listing. Omit format to download any listing's canonical owned artifact. Explicit parquet and xlsx select formats only for manifested slice children.

Slice-on-demand deployments also expose the buyer routes below and the workflow documented in Buying slices. 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 5-minute URL. When format is omitted, the service returns the ownership row's canonical data_ref for every listing. When present, format is a closed enum (parquet or xlsx) and both values are available only for an exact manifested slice child; object keys are resolved from the listing and manifest, never caller input. The MCP ownership_download tool remains the canonical-format surface and does not accept a format argument.

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. 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 submitting a candidate as a seller are also MCP tools (create_request_draft, list_my_requests, list_open_requests, submit_request_candidate — 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}/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)

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.

Listing responses always include public catalog metadata plus the AMN-827 standard quote token:

{"license":{"standard":{"token":"dv1:..."}}}

That token binds the listing's current price_cents, the pre-ratification instrument version, and the platform's current default composition. That composition is a provisional quote-integrity input, not a settled interpretation of model training or any other operative right under ToS §5.3; the interpretation remains pending counsel review. The token is present even when the priced-license surface is off so standard card checkout can assert the quote it displayed.

When LICENSE_TERMS_ENABLED is on, the same license object is additively enriched:

{
  "license": {
    "schema_version": "lts/1",
    "standard": {"terms": {"...": "..."}, "price_cents": 5000, "token": "dv1:..."},
    "offers": [
      {"offer_id": "...", "offer_key": "training", "terms": {"...": "..."},
       "price_cents": 5000000, "currency": "usd", "token": "..."}
    ]
  }
}

offers is always an array when enabled ([] when no composed tier is live). With the flag off, the added schema, terms, price, and offers fields are absent and the catalog does not read offer rows. The displayed standard composition does not itself grant or settle training rights. This surface remains pending counsel review and is not GA; production refuses to enable it until that gate clears.

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 standard or an active composed offer_key from the listing's license block. It returns the exact stored text of record together with the resolved terms, versions, hashes, price, currency, and settlement token:

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

Composed tiers also include offer_id. Responses are Cache-Control: no-store; bind what you reviewed with token, 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 50 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 requires a data dictionary, while CSV/XLSX can be converted to canonical parquet with an auto-derived dictionary). Non-production environments with LARGE_LISTING_ENABLED also expose the REST-only resumable upload flow POST /api/v1/seller/uploadsPOST /api/v1/seller/uploads/{upload_id}/partsPOST /api/v1/seller/uploads/{upload_id}/complete plus DELETE abort; these routes are absent when the flag is off, and APP_ENV=prod refuses to boot with the flag enabled until resumable buyer download is deployed.

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

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 license offers (pending counsel review — not yet GA)

License offers let a seller publish additional priced licensing tiers on one of their own listings, alongside the listing's standard (default) terms. Each offer names a short offer_key, a sparse set of elected licensing dimensions (overlaid on the frozen defaults), and a price. Before composing any offers the seller's account must accept the current licensing Terms of Service — a versioned legal instrument the server stamps at acceptance time (the client never supplies a version). These routes exist only when the deployment has LICENSE_TERMS_ENABLED; APP_ENV=prod refuses to boot with the flag on until the licensing terms are counsel-ratified. Identity and listing ownership are always derived from the authenticated token — never from the request body.

Method + path Purpose
PUT /api/v1/seller/documents/{id}/license-offers Replace a listing's full live offer set (compose)
GET /api/v1/seller/documents/{id}/license-offers Read a listing's offers (?include_retired=1 to include retired rows)

PUT …/license-offers takes the seller's desired live offer set — it is a full replace, not a patch. The server validates every entry, runs an id-stable diff (unchanged offers keep their id and version; repriced or recomposed offers get a new versioned row; dropped offers are retired), and returns 200 with the resulting live offers plus any non-fatal coherence warnings:

{
  "offers": [
    {
      "offer_key": "internal-training",
      "terms": { "...": "..." },
      "terms_schema_version": "lts/1",
      "dls_version": "DLS-1",
      "price_cents": 250000,
      "currency": "usd",
      "token": "…",
      "status": "active"
    }
  ],
  "warnings": []
}

An empty offers array retires all offers (and needs no acceptance).

The request body:

{ "offers": [ { "offer_key": "internal-training", "terms": { … }, "price_cents": 250000 } ],
  "accept_licensing_terms": true }

There is no tos_version field (the server stamps the licensing version) and no price_matrix field (reserved). Set accept_licensing_terms: true to record your account's acceptance of the current licensing terms as part of the same call — it is idempotent (a no-op once your account has already accepted).

If your account has not accepted the current licensing terms and you did not set accept_licensing_terms, the compose is refused with 409 and a non-opaque body naming the version to accept and linking the instrument:

{ "error": "licensing terms acceptance required",
  "code": "licensing_terms_acceptance_required",
  "required_version": "tos/5.3-draft",
  "instrument_url": "https://amnetic.ai/terms" }

Invalid offers (an unknown dimension, an incoherent composition, a price outside the allowed range, a reserved or malformed offer_key, or two offers with the same composition) return 400 with an actionable message. A listing you do not own — or one that does not exist — returns an opaque 404 listing not found (a seller cannot probe another seller's inventory).

A platform-created slice child is immutable. Replacing its offer set returns 409 with code: "platform_slice_frozen"; editing its listing fields or purchase ACL uses the same error contract. These guards freeze the child after its atomic materialization transaction while leaving the sanctioned unsold deactivation lifecycle available to the platform.

Compose at create time. Rather than creating a listing and then composing offers in a second PUT, the three seller create endpoints — POST /api/v1/seller/documents, POST /api/v1/seller/files, and POST /api/v1/seller/datasets — accept an optional inline license_offers block on the request (for the multipart file/dataset paths it rides the metadata JSON):

{ "license_offers": {
    "offers": [ { "offer_key": "internal-training", "terms": { … }, "price_cents": 250000 } ],
    "accept_licensing_terms": true } }

The block carries the same entries and accept_licensing_terms flag as the PUT sub-resource and runs through the same compose chokepoint (same validation, same server-stamped consent gate), so a listing is born with its ladder in one call. These endpoints exist only when LICENSE_TERMS_ENABLED is on; supplying the block while licensing is off is rejected (400 license offers are not enabled) — the priced/legal block is never silently dropped. A structurally malformed block is rejected before the listing is created. The inline block is also exposed on the create_listing MCP tool (see the MCP tools reference).

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": -800,
    "net_earnings_cents": 7200,
    "balance_cents": 7200,
    "withdrawable_balance_cents": 7200
  },
  "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 available only where LICENSE_TERMS_ENABLED is on. 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.

Buyer license read (pending counsel review — not yet GA)

Every purchase records an append-only license grant — the evidence of exactly what license you bought and under which versioned legal instrument. When a deployment has LICENSE_TERMS_ENABLED, buyers can read that evidence back. These surfaces are read-only, mint no purchase handle, and exist only when the flag is on; APP_ENV=prod keeps them dark until the licensing terms are counsel-ratified. 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).