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=prodrefuses 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_ENABLEDis 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 requiresAWS_REGIONfor the real Bedrock extraction agent andTRUST_RECORDING_ENABLED=truefor transactional parent-to-child provenance.SLICE_MATCH_MODELandSLICE_AGENT_MODELmust 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 buyerrequest_sliceorslice_statusMCP 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/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, 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). ThePUTbody is{ "policy": { … }, "warnings": [ … ] }, wherewarningsare 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 (onGET) has no policy configured. The three are deliberately indistinguishable.400— a validation error:min_price_centsbelow the $1.00 floor, a missing or non-positiveper_row_centson an enabled policy, an unknown key column, amax_price_centsbelow 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}addsoffer_keyandterms_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/ownershipadds a per-rowlicenseblock:{ "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).