Download OpenAPI specification:
Public API for the RTK card platform: catalog, listings, sold comps, pricing, inventory, outcomes, and PSA vault. The /api/v1 surface is the stable, token-authenticated contract consumed via the published SDK.
IDs are opaque, prefixed strings: <prefix>_<26-char lowercase ULID> (e.g. lst_01j9zq3vp4m8x2k7r5t1w9e6ha). Do not parse them; treat them as stable handles. Prefixes are self-identifying (card_, lst_, comp_, bid_, job_, ...).
Money is always an integer amount in minor units (cents) plus an ISO 4217 currency code: {"amount": 12999, "currency": "USD"}. No floats, ever.
Timestamps are RFC 3339 UTC with a Z suffix; field names end in _at.
Collections return the envelope {"items": [...], "pagination": {"next_cursor", "has_more", "limit"}, "meta": {...}} with opaque cursor pagination. meta carries opt-in aggregates requested via ?include=total_count,total_amount (comma list); each endpoint documents which aggregates it supports, and unbounded feeds (listings, comps) support none. Changing filters invalidates a cursor (400, code invalid_cursor). Single resources are returned bare.
Errors are RFC 9457 application/problem+json on every non-2xx response. Switch on the stable machine code; quote trace_id when contacting support.
Every response carries an X-Request-Id header (the request's trace id) — success and error alike.
Every authenticated response also carries the rate-limit trio X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset — on errors (401/403/404/429/500) as well as success, so a client can read its remaining budget no matter the outcome. The unauthenticated /api/v1/health endpoint is the sole exception (it is not rate-limited).
Mutating requests (POST/PUT/PATCH/DELETE) require an Idempotency-Key header (UUIDv4). Semantics are per-operation: some endpoints use a 24h per-account replay store (replay → original response; same key, different body → 409, code idempotency_conflict); others rely on natural idempotency (unique constraints / content hashes) and accept the header for convention only — see each operation's description.
Request bodies are capped at 1 MiB. A larger body is rejected with 413, code request_too_large, before the handler reads it; for bulk data use the documented batch or cursor-paginated endpoints rather than one oversized payload.
Async jobs: operations that cannot complete synchronously return 202 Accepted with a Job resource (job_<ulid>); poll GET /api/v1/jobs/{job_id} until status is succeeded or failed, then follow result_url.
Enums are open: clients must tolerate unknown enum values; new values are additive, not breaking.
Returns the service health status and the running build version (git SHA or release tag). Unauthenticated and not rate-limited; used by SDK smoke tests and synthetic canaries to assert what build is live. No dependency probing — deep health checks are internal only.
{- "status": "ok",
- "version": "e5b7cee9d2c1f4a8b3e6d7c0a1f2b3c4d5e6f7a8"
}Fetches the status of an asynchronous job. Async operations return 202 Accepted with a Job resource; poll this endpoint until the job reaches succeeded or failed, then follow result_url.
Not yet implemented. This endpoint reserves the platform's async-job convention; no operations create jobs yet, so every request currently returns 404 (code not_found). It becomes live with the first async operation.
| job_id required | string^job_[0-9a-hjkmnp-tv-z]{26}$ Example: job_01j9zq3vp4m8x2k7r5t1w9e6ha Job identifier ( |
{- "id": "job_01j9zq3vp4m8x2k7r5t1w9e6ha",
- "status": "running",
- "created_at": "2026-06-12T17:32:11Z",
- "updated_at": "2026-06-12T17:32:42Z"
}Authenticated self-account identity for the calling API key (no caller-supplied account selector).
Returns the platform account bound to the calling API key: stable account_id, lifecycle status, trading_enabled, granted scopes, source_entitlement, and a monotonic state_revision with state_updated_at.
Account identity is derived solely from the bearer key. There is no path or query account selector; cross-account lookup is impossible.
{- "account_id": "acct_01j9zq3vp4m8x2k7r5t1w9e6ha",
- "status": "active",
- "trading_enabled": true,
- "scopes": [
- "*",
- "candidates:read"
], - "source_entitlement": "all",
- "state_revision": 1722988800123,
- "state_updated_at": "2026-08-06T18:00:00.123Z"
}Watch subscriptions and matched-listing candidate feed (M18 P15a). Watch-only — no buying authority.
Returns the account's watch subscription entries. Requires scope candidates:read (or *) and source_entitlement=all. Gated by release flag subscriptions.watch_enabled (off → 404).
Watch-only: entries carry no price ceiling, quantity, or buying authority. Account is derived solely from the bearer key.
| status | string Enum: "open" "expired" "cancelled" Optional status filter. |
| cursor | string Opaque pagination cursor from a prior page's |
| limit | integer [ 1 .. 200 ] Default: 50 |
{- "items": [
- {
- "id": "sub_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "kind": "watch",
- "selector": { },
- "book_id": "string",
- "requested_exit_intent": "model_horizon",
- "status": "open",
- "expires_at": "2019-08-24T14:15:22Z",
- "source": "ui",
- "priority": 0,
- "version": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Resumable pull feed of marketplace listings that matched ≥1 of this account's open watch subscriptions (matched listings). One delivery per listing version with the full ranked match set.
Requires scope candidates:read (or *) and source_entitlement=all. Flag subscriptions.watch_enabled off → 404.
Resume with cursor = prior page's pagination.next_cursor (the last seen matched-listing seq, decimal string). limit max 100; larger limits are capped and may set shed_reason.
| cursor | string Watermark cursor: decimal string of the last consumed |
| limit | integer [ 1 .. 100 ] Default: 100 |
{- "items": [
- {
- "id": "ml_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "seq": 0,
- "listing_id": "string",
- "listing_version": "string",
- "card_id": "string",
- "payload": { },
- "matches": [
- {
- "subscription_id": "string",
- "version": 0,
- "kind": "watch",
- "book_id": "string",
- "requested_exit_intent": "model_horizon",
- "priority": 0,
- "match_rank": 0
}
], - "match_rank_version": "v1",
- "matches_overflow": true,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}, - "tip_seq": 0,
- "shed_reason": "string"
}Card archetype lookup. Merged (alias) ids resolve server-side to the canonical body; merged_into_id and identity_key are never exposed.
Returns one catalog card archetype (deliberately-public fields only). Merged (alias) ids resolve server-side to the canonical body with HTTP 200 — never a redirect, and never expose merged_into_id or identity_key. Unknown ids return 404 (not_found).
Catalog identity is global (not source-entitled). The route still requires an API key: auth gates the public surface; market feeds remain the entitlement boundary.
| card_id required | string^card_[0-9A-HJKMNP-TV-Z]{26}$ Example: card_01J9ZQ3VP4M8X2K7R5T1W9E6HA Card identifier ( |
{- "id": "card_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "category": "SPORT",
- "set": {
- "id": "set_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Prizm",
- "year": 2023,
- "manufacturer": "Panini",
- "brand": "Prizm",
- "sport": "basketball"
}, - "subset": {
- "id": "sset_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Kaboom!"
}, - "parallel": {
- "id": "par_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Silver"
}, - "card_number": "23",
- "year": 2023,
- "print_run": 99,
- "flags": [
- "rookie"
], - "subjects": [
- {
- "id": "subj_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "LeBron James",
- "kind": "player",
- "sport": "basketball",
- "team_id": "string",
- "team_name": "Los Angeles Lakers",
- "role": "primary"
}
], - "facets": [
- {
- "type": "autograph",
- "value": "on-card"
}
]
}Cursor-paginated subject (player/character) browse. Optional q is a prefix on normalized name (NormalizeKey). Canonical rows only — merged aliases are omitted. Use a returned id with GET /cards?subject_id=.
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| q | string <= 128 characters Example: q=lebron Name prefix (normalized). |
| sport | string Example: sport=basketball |
| kind | string Enum: "player" "character" "other" |
{- "items": [
- {
- "id": "string",
- "name": "LeBron James",
- "kind": "player",
- "sport": "basketball",
- "disambiguator": "string"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Cursor-paginated team browse. Optional q is a case-insensitive name prefix. Pair with GET /cards?team_id= (and optional subject_id).
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| q | string <= 128 characters Example: q=Lakers |
| sport | string Example: sport=basketball |
{- "items": [
- {
- "id": "string",
- "name": "Los Angeles Lakers",
- "league": "string",
- "sport": "string"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Cursor-paginated set browse. Optional q is a case-insensitive name prefix; optional year / sport narrow further.
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| q | string <= 128 characters Example: q=Prizm |
| year | integer <int32> Example: year=2023 |
| sport | string Example: sport=basketball |
{- "items": [
- {
- "id": "set_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Prizm",
- "year": 2023,
- "manufacturer": "Panini",
- "brand": "Prizm",
- "sport": "basketball"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Cursor-paginated parallel browse. Optional q is a prefix on normalized name. Canonical rows only.
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| q | string <= 128 characters Example: q=silver |
{- "items": [
- {
- "id": "par_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Silver"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Cursor-paginated subset browse. Prefer filtering with set_id (subsets are set-scoped). Optional q is a normalized-name prefix.
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| q | string <= 128 characters Example: q=kaboom |
| set_id | string^set_[0-9A-HJKMNP-TV-Z]{26}$ |
{- "items": [
- {
- "id": "string",
- "set_id": "string",
- "name": "Kaboom!",
- "kind": "insert"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Cursor-paginated card browse for the Trading Floor / UI. Filter by subject_id and/or team_id (via card_subjects), and/or set / parallel / subset. Optional q is a card_number prefix. Canonical cards only; response shape matches GET /cards/{id} (facets may be empty on the list path). Merged subject ids are resolved one hop.
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| q | string <= 64 characters Example: q=23 Card number prefix. |
| subject_id | string^subj_[0-9A-HJKMNP-TV-Z]{26}$ |
| team_id | string^team_[0-9A-HJKMNP-TV-Z]{26}$ |
| set_id | string^set_[0-9A-HJKMNP-TV-Z]{26}$ |
| parallel_id | string^par_[0-9A-HJKMNP-TV-Z]{26}$ |
| subset_id | string^sset_[0-9A-HJKMNP-TV-Z]{26}$ |
{- "items": [
- {
- "id": "card_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "category": "SPORT",
- "set": {
- "id": "set_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Prizm",
- "year": 2023,
- "manufacturer": "Panini",
- "brand": "Prizm",
- "sport": "basketball"
}, - "subset": {
- "id": "sset_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Kaboom!"
}, - "parallel": {
- "id": "par_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Silver"
}, - "card_number": "23",
- "year": 2023,
- "print_run": 99,
- "flags": [
- "rookie"
], - "subjects": [
- {
- "id": "subj_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "LeBron James",
- "kind": "player",
- "sport": "basketball",
- "team_id": "string",
- "team_name": "Los Angeles Lakers",
- "role": "primary"
}
], - "facets": [
- {
- "type": "autograph",
- "value": "on-card"
}
]
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Account-scoped owned inventory (API-key identity). Status transitions are write-path only; this tag covers reads. acquisition_cost is never exposed on /api.
Cursor-paginated inventory for the authenticated account (API-key identity). Opaque keyset cursor on inventory id; changing any filter invalidates the cursor (400, code invalid_cursor).
Never exposed: acquisition_cost / any *_cents field — cost basis stays off the public /api surface. Tenant isolation is account-scoped in SQL (inventory has no RLS; the binder uses the key's account).
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| status | string Enum: "acquired" "grading" "listed" "sold" "shipped" "returned" "withdrawn" Example: status=listed Inventory state-machine status filter. |
| instance_id | string^inst_[0-9A-HJKMNP-TV-Z]{26}$ Example: instance_id=inst_01J9ZQ3VP4M8X2K7R5T1W9E6HA Filter to the slab (graded instance) held by this row. |
| sealed_product_id | string^sprod_[0-9A-HJKMNP-TV-Z]{26}$ Example: sealed_product_id=sprod_01J9ZQ3VP4M8X2K7R5T1W9E6HA Filter to sealed-product (wax) rows. |
{- "items": [
- {
- "id": "inv_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "instance_id": "string",
- "sealed_product_id": "string",
- "status": "listed",
- "venue": "ebay",
- "vault_source": "string",
- "external_vault_id": "string",
- "listed_price": {
- "amount": 12999,
- "currency": "USD"
}, - "currency": "USD",
- "listed_at": "2019-08-24T14:15:22Z",
- "list_expiry_at": "2019-08-24T14:15:22Z",
- "delisted_at": "2019-08-24T14:15:22Z",
- "sold_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Returns one inventory row for the authenticated account. Missing or other-account ids return 404 (not_found) — never a cross-tenant existence leak. acquisition_cost is never present.
| inventory_id required | string^inv_[0-9A-HJKMNP-TV-Z]{26}$ Example: inv_01J9ZQ3VP4M8X2K7R5T1W9E6HA Inventory identifier ( |
{- "id": "inv_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "instance_id": "string",
- "sealed_product_id": "string",
- "status": "listed",
- "venue": "ebay",
- "vault_source": "string",
- "external_vault_id": "string",
- "listed_price": {
- "amount": 12999,
- "currency": "USD"
}, - "currency": "USD",
- "listed_at": "2019-08-24T14:15:22Z",
- "list_expiry_at": "2019-08-24T14:15:22Z",
- "delisted_at": "2019-08-24T14:15:22Z",
- "sold_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Account-scoped strategy/mandate books (P16). Opaque allowed-archetype codes and user-defined strategies; engine owns policy/binding. Creating a book never auto-binds a mandate. Gated by books.registry_enabled (off → 404). Distinct from M7 custody inventory_book_events.
Cursor-paginated strategy/mandate books for the authenticated account (API-key identity). Opaque keyset cursor on book id DESC. Tenant isolation is account-scoped in SQL (books have no RLS; the binder uses the key's account).
Gated by release flag books.registry_enabled (off → 404 not_found). Creating or listing a book never auto-binds an engine mandate.
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
{- "items": [
- {
- "id": "book_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "account_id": "acct_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Core graded singles",
- "enabled": true,
- "state_revision": 3,
- "allowed_archetype_codes": [
- "graded_single",
- "raw_modern"
], - "strategies": [
- {
- "id": "bstrat_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "version": 1,
- "name": "Aggressive bid",
- "anchor_code": "last_sold",
- "window_days": 30,
- "overrides": { }
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Creates an account-scoped strategy/mandate book. Requires API-key scope books:write (not granted by sources:all alone; * grants it once listed as a known discrete scope).
Empty allowed_archetype_codes forces enabled=false. Creating a book never auto-binds an engine mandate. Gated by books.registry_enabled (off → 404 not_found).
Idempotency. Natural create (fresh ULID each call). Idempotency-Key satisfies the public-API convention and is accepted; this operation does not emit 409 idempotency_conflict.
| Idempotency-Key required | string <uuid> Example: 5f2b6c1e-9a4d-4f6e-8c3a-2d1b0e9f8a7c UUIDv4 idempotency key, required on every mutating request. The Go SDK generates this automatically. Semantics are per-operation: when an endpoint documents a 24h per-account replay store, a replay returns the original response and the same key with a different body returns |
| name required | string [ 1 .. 200 ] characters |
| enabled | boolean Optional; defaults true unless archetypes are empty (forced false). |
| allowed_archetype_codes required | Array of strings[ items [ 1 .. 64 ] characters ] |
Array of objects (CreateBookStrategy) Optional strategies to create with the book. |
{- "name": "Core graded singles",
- "enabled": true,
- "allowed_archetype_codes": [
- "string"
], - "strategies": [
- {
- "name": "string",
- "anchor_code": "last_sold",
- "window_days": 1,
- "overrides": { }
}
]
}{- "id": "book_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "account_id": "acct_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Core graded singles",
- "enabled": true,
- "state_revision": 3,
- "allowed_archetype_codes": [
- "graded_single",
- "raw_modern"
], - "strategies": [
- {
- "id": "bstrat_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "version": 1,
- "name": "Aggressive bid",
- "anchor_code": "last_sold",
- "window_days": 30,
- "overrides": { }
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns one strategy/mandate book for the authenticated account. Missing or other-account ids return 404 with code book_unavailable — never a cross-tenant existence leak. Flag off → 404 not_found.
| book_id required | string^book_[0-9A-HJKMNP-TV-Z]{26}$ Example: book_01J9ZQ3VP4M8X2K7R5T1W9E6HA Strategy/mandate book identifier ( |
{- "id": "book_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "account_id": "acct_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "name": "Core graded singles",
- "enabled": true,
- "state_revision": 3,
- "allowed_archetype_codes": [
- "graded_single",
- "raw_modern"
], - "strategies": [
- {
- "id": "bstrat_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "version": 1,
- "name": "Aggressive bid",
- "anchor_code": "last_sold",
- "window_days": 30,
- "overrides": { }
}
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Cursor-paginated feed of normalized marketplace listings (deliberately public fields only). Default filter is status=active. Opaque keyset cursor on listing id (time-ordered); changing any filter invalidates the cursor (400, code invalid_cursor).
Source entitlement. Visibility is gated per API key (api_keys.source_entitlement): default keys see only data_sources.redistributable=true sources; a key granted all (the valuation-engine key) sees every enabled source. Every item carries its source code. Unbounded aggregates (include=) are not supported on this feed.
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| status | string Default: "active" Enum: "active" "ended" "sold" "cancelled" Example: status=active Listing lifecycle status. Default |
| card_id | string^card_[0-9A-HJKMNP-TV-Z]{26}$ Example: card_id=card_01J9ZQ3VP4M8X2K7R5T1W9E6HA Filter to listings bound to this card. Merged (alias) ids are expanded server-side — pass either the canonical or a historical id; both return the same rows. |
| cert_number | string <= 64 characters Example: cert_number=12345678 Filter to listings whose resolved cert number matches. |
| listing_type | string Enum: "auction" "fixed_price" "auction_with_bin" Example: listing_type=auction Sale format filter (open enum). |
{- "items": [
- {
- "id": "list_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "source": "ebay",
- "vendor_item_id": "123456789012",
- "card_id": "card_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "sealed_product_id": "string",
- "instance_id": "string",
- "cert_number": "12345678",
- "grade": 10,
- "listing_type": "auction",
- "status": "active",
- "price": {
- "amount": 12999,
- "currency": "USD"
}, - "current_bid": {
- "amount": 12999,
- "currency": "USD"
}, - "currency": "USD",
- "quantity": 1,
- "identity_confidence": 0.95,
- "scheduled_end_at": "2019-08-24T14:15:22Z",
- "first_seen_at": "2026-06-12T17:30:00Z",
- "last_seen_at": "2026-06-12T17:32:11Z"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Server-sent events feed of listing lifecycle events (listing.created, listing.updated). Each SSE data: payload is a JSON ListingEvent; the SSE id: field carries an event id usable as Last-Event-ID on reconnect for gapless resumption.
Source entitlement matches GET /listings. Last-Event-ID is the outbox row id for gapless resume. Durable listing.image_url prefers CDN when a card_images row exists. Polls event_outbox (listing entity) — Redis pub/sub fan-out remains a follow-up.
| Last-Event-ID | string Example: 42 Standard SSE reconnection header: the last event id the client processed. The stream resumes after this event when it is still retained. |
{- "title": "Unauthorized",
- "status": 401,
- "detail": "The API key is missing or invalid.",
- "instance": "/api/v1/jobs/job_01j9zq3vp4m8x2k7r5t1w9e6ha",
- "code": "unauthorized",
- "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}Sold-comp intake and related machine-API surfaces. Scrapers POST raw vendor payloads; the platform owns normalization and identity.
Authenticated machine-API intake for external scraper services (CardLadder, Alt, future sources). Scrapers POST raw vendor JSON only (M1.6 D13): the platform normalizers own origin_marketplace / origin_sale_id, identity, and money/date parsing. Scraper-computed identity or normalized price/date fields inside a record are ignored (typed unmarshal drops unknown keys); the canonical sale structure is always the normalizer's output.
Not for eBay-direct. source=ebay is rejected — eBay sold comps arrive via the platform poller.
Auth posture. Bearer API key (Authorization: Bearer rtk_<env>_…) PLUS scope or allowlist (dual-run, M6 G4): prefer comps:ingest on api_keys.scopes; legacy keys pass when release flag comps.scraper_intake_enabled is on and its ops value allowed_account_ids includes the key's account (empty scopes AND empty allowlist → 403). Source entitlement (sources:all vs redistributable) gates READ surfaces, not this write.
Granularity. Accept the vendor's natural document shape per docs/data-sources/<source>.md: one CardLadder hit → one capture; one Alt asset document (with marketTransactions[]) fans out server-side to N captures. CardLadder sales resolve by cert first, then by the profileId previously attested through CL1 external-card intake; marketplace-ingestion must complete that CL1 step before posting CL2 hits.
Limits. Request body ≤ 1 MiB (global MaxBytesHandler). At most 500 records per request (reject, do not silent-chunk). After fan-out, at most 500 captures reach the ingest write path (same cap as the worker).
Idempotency. Natural end-to-end: sold_comps uniques, quarantine content-hash uniques, deterministic three-band grouping. Replays converge (unchanged / no new rows) without a separate Idempotency-Key store — the required header satisfies the public-API convention and is accepted, but is not used as a second 24h replay mechanism. This operation does not emit 409 idempotency_conflict.
Outcomes. Per-record results — never batch-abort on a bad record. Batch-level 4xx only for malformed envelope / auth / size / unknown source / unregistered normalizer. Gated by release flag comps.scraper_intake_enabled (off by default → 404).
| Idempotency-Key required | string <uuid> Example: 5f2b6c1e-9a4d-4f6e-8c3a-2d1b0e9f8a7c UUIDv4 idempotency key, required on every mutating request. The Go SDK generates this automatically. Semantics are per-operation: when an endpoint documents a 24h per-account replay store, a replay returns the original response and the same key with a different body returns |
| source required | string
|
| fetched_at | string <date-time> Optional scraper fetch timestamp (metadata only). |
| scraper_version | string <= 64 characters Optional scraper build/version string (metadata only). |
required | Array of objects [ 1 .. 500 ] items Vendor-native raw JSON documents. Max 500. CardLadder: one hit per element. Alt: one asset document per element (fans out server-side). |
{- "source": "cardladder",
- "fetched_at": "2026-07-09T14:00:00Z",
- "scraper_version": "cardladder-scraper@1.2.0",
- "records": [
- { }
]
}{- "source": "cardladder",
- "results": [
- {
- "index": 0,
- "outcome": "ingested",
- "reason": "missing_origin",
- "captures": 1
}
], - "summary": {
- "ingested": 3,
- "unchanged": 1,
- "quarantined": 0,
- "failed": 0,
- "unresolved": 0
}
}Cursor-paginated sold-comp history (deliberately-public fields only). Requires a sold_at window for partition pruning: omit sold_from / sold_to to use the default rolling 24 months ending now. The window span must not exceed 36 months (400, code validation_failed). Opaque keyset cursor on (sold_at, id); changing any filter invalidates the cursor (400, code invalid_cursor).
Source entitlement. Same per-API-key gate as listings: default keys see only redistributable sources; all sees every enabled source. Penny-bid / private activity rows are never returned on this market feed. Unbounded aggregates (include=) are not supported.
| cursor | string Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ Opaque pagination cursor from a previous response's |
| limit | integer <int32> [ 1 .. 200 ] Default: 50 Example: limit=50 Page size. Default 50, maximum 200 (server-enforced; values above the maximum return |
| sold_from | string <date-time> Example: sold_from=2024-07-14T00:00:00Z Inclusive lower bound of |
| sold_to | string <date-time> Example: sold_to=2026-07-14T00:00:00Z Exclusive upper bound of |
| card_id | string^card_[0-9A-HJKMNP-TV-Z]{26}$ Example: card_id=card_01J9ZQ3VP4M8X2K7R5T1W9E6HA Filter to comps bound to this card. Merged (alias) ids are expanded server-side. |
| grade | number <double> Example: grade=10 Exact numeric grade filter (e.g. 10 or 9.5). Without |
| grading_company | string Enum: "PSA" "BGS" "SGC" "CGC" "RAW" "OTHER" Example: grading_company=PSA Exact grading company filter (PSA / BGS / SGC / CGC / RAW / OTHER). Pair with |
| grade_qualifier | string Enum: "OC" "ST" "MK" "" Example: grade_qualifier=OC Exact grade qualifier filter (OC / ST / MK). Omit to match any qualifier including unqualified (NULL) rows when only grade / company are set. Pass an empty string to match unqualified rows only (NULL qualifier). |
{- "items": [
- {
- "source": "ebay",
- "source_sale_id": "string",
- "sold_at": "2026-06-12T17:32:11Z",
- "sale_price": {
- "amount": 12999,
- "currency": "USD"
}, - "landed_price": {
- "amount": 12999,
- "currency": "USD"
}, - "currency": "USD",
- "card_id": "string",
- "sealed_product_id": "string",
- "instance_id": "string",
- "grade": 10,
- "grading_company": "PSA",
- "grade_qualifier": "OC",
- "sale_type": "auction"
}
], - "pagination": {
- "next_cursor": "lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ",
- "has_more": true,
- "limit": 50
}
}Card-level external data intake (valuations, population, vendor crosswalks) from marketplace-ingestion services.
Authenticated machine-API intake for marketplace-ingestion services (CardLadder, Alt). Scrapers POST raw vendor JSON only (M6 D13) bound to a platform card_id. Platform normalizers extract card_external_ids, valuations, and population_reports. Append semantics for valuations/pop; upsert for external ids.
CardLadder identity ordering. A CL1 cert-lookup document is an identity attestation, not a valuation capture. Platform-api reconciles its cert facts against the existing card/listing claim, then atomically binds the cert and CardLadder profileId to card_id, recording CardLadder as attestation provenance. Conflict or sparse identity fails closed. Marketplace-ingestion must receive an accepted CL1 result before fetching or posting CL2 sales; CL3 valuation is posted separately.
Not for eBay-direct. source=ebay is rejected.
Auth posture. Bearer API key PLUS scope or allowlist (dual-run, M6 G4): prefer external-data:ingest on api_keys.scopes; legacy keys pass when release flag external_data.intake_enabled is on and its ops value allowed_account_ids includes the key's account (empty scopes AND empty allowlist → 403). Every source=cardladder request additionally requires both cardladder:ingest and an account listed in allowed_account_ids; neither grant alone confers CardLadder source or attestation authority.
Limits. Request body ≤ 1 MiB. At most 500 records per request.
Idempotency. Natural end-to-end via unique keys on valuations, population_reports, and card_external_ids. Replays converge (unchanged). Idempotency-Key required by API convention; not a separate 24h replay store.
Outcomes. Per-record ingested / unchanged / failed (no quarantine band in v1). Gated by external_data.intake_enabled (off → 404).
| Idempotency-Key required | string <uuid> Example: 5f2b6c1e-9a4d-4f6e-8c3a-2d1b0e9f8a7c UUIDv4 idempotency key, required on every mutating request. The Go SDK generates this automatically. Semantics are per-operation: when an endpoint documents a 24h per-account replay store, a replay returns the original response and the same key with a different body returns |
| source required | string
|
| card_id required | string^card_[0-9A-HJKMNP-TV-Z]{26}$ Platform card id the observations apply to ( |
| fetched_at | string <date-time> Optional scraper fetch timestamp (drives |
| scraper_version | string Optional scraper build id (metadata only). |
required | Array of objects [ 1 .. 500 ] items |
{- "source": "alt",
- "card_id": "card_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "fetched_at": "2019-08-24T14:15:22Z",
- "scraper_version": "string",
- "records": [
- { }
]
}{- "source": "string",
- "card_id": "string",
- "results": [
- {
- "index": 0,
- "outcome": "ingested",
- "reason": "string"
}
], - "summary": {
- "ingested": 0,
- "unchanged": 0,
- "failed": 0
}
}Source-first identity observation intake (M1.7). Machine surface may return cluster/assurance; does not replace card-first /external-card-data.
Authenticated machine-API intake for marketplace-ingestion services (CardLadder, Alt) when the platform card may be unknown (M1.7 D15). Scrapers POST raw vendor JSON only (D13). Platform normalizers derive anchors, create or attach an identity cluster, and may return cluster_id, optional card_id, scoped assurance, and per-record outcomes (D10 machine write surface).
Additive to card-first intake. This endpoint does not change POST /api/v1/external-card-data. Card-first callers keep requiring a platform card_id. Here request-envelope card_id and cluster_id are optional and ignored (accepted for forward-compat / shape validation only) — they never force a catalog mapping or cluster attach. The platform creates the cluster from normalized anchors; response card_id / cluster_id are platform-resolved.
Not for eBay-direct. source=ebay is rejected (same posture as comps and external-card-data intakes; eBay stays on the platform poller).
Auth posture. Bearer API key PLUS scope or allowlist (dual-run, same house style as external-data): prefer identity:ingest on api_keys.scopes; legacy keys pass when release flag identity.observations_intake_enabled is on and its ops value allowed_account_ids includes the key's account (empty scopes AND empty allowlist → 403). Every source=cardladder request additionally requires both cardladder:ingest and an account listed in allowed_account_ids; neither grant alone confers CardLadder source authority.
Limits. Request body ≤ 1 MiB (global MaxBytesHandler). At most 500 records per request (reject, do not silent-chunk).
Idempotency. Natural end-to-end via immutable assertion keys / observation keys and cluster projection CAS. Replays converge (unchanged). Idempotency-Key required by API convention; not a separate 24h replay store. This operation does not emit 409 idempotency_conflict.
Outcomes. Per-record results — never batch-abort on a bad record. Batch-level 4xx only for malformed envelope / auth / size / unknown source / unregistered normalizer. Gated by release flag identity.observations_intake_enabled (off by default → 404).
| Idempotency-Key required | string <uuid> Example: 5f2b6c1e-9a4d-4f6e-8c3a-2d1b0e9f8a7c UUIDv4 idempotency key, required on every mutating request. The Go SDK generates this automatically. Semantics are per-operation: when an endpoint documents a 24h per-account replay store, a replay returns the original response and the same key with a different body returns |
| source required | string
|
| card_id | string^card_[0-9A-HJKMNP-TV-Z]{26}$ Optional. Accepted for forward-compat and shape-validated when present ( |
| cluster_id | string^icl_[0-9A-HJKMNP-TV-Z]{26}$ Optional. Accepted for forward-compat and shape-validated when present ( |
| fetched_at | string <date-time> Optional scraper fetch timestamp (drives |
| scraper_version | string Optional scraper build id (metadata only). |
required | Array of objects [ 1 .. 500 ] items |
{- "source": "cardladder",
- "card_id": "card_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "cluster_id": "icl_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
- "fetched_at": "2019-08-24T14:15:22Z",
- "scraper_version": "string",
- "records": [
- { }
]
}{- "source": "string",
- "results": [
- {
- "index": 0,
- "outcome": "ingested",
- "reason": "string",
- "cluster_id": "string",
- "card_id": "string",
- "assurance": {
- "cert_claim_assurance": "captured",
- "card_mapping_assurance": "unmapped",
- "instance_authority": "none",
- "resolution_state": "unresolved"
}
}
], - "summary": {
- "ingested": 0,
- "unchanged": 0,
- "failed": 0
}
}