RTK Card Platform API (0.2.0)

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.

Conventions

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.

system

Service health, async jobs, and platform metadata.

Health check

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.

Responses

Response samples

Content type
application/json
{
  • "status": "ok",
  • "version": "e5b7cee9d2c1f4a8b3e6d7c0a1f2b3c4d5e6f7a8"
}

Get an async job

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.

Authorizations:
apiKey
path Parameters
job_id
required
string^job_[0-9a-hjkmnp-tv-z]{26}$
Example: job_01j9zq3vp4m8x2k7r5t1w9e6ha

Job identifier (job_ prefix + 26-char lowercase ULID).

Responses

Response samples

Content type
application/json
{
  • "id": "job_01j9zq3vp4m8x2k7r5t1w9e6ha",
  • "status": "running",
  • "created_at": "2026-06-12T17:32:11Z",
  • "updated_at": "2026-06-12T17:32:42Z"
}

account

Authenticated self-account identity for the calling API key (no caller-supplied account selector).

Get the authenticated account

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.

Authorizations:
apiKey

Responses

Response samples

Content type
application/json
{
  • "account_id": "acct_01j9zq3vp4m8x2k7r5t1w9e6ha",
  • "status": "active",
  • "trading_enabled": true,
  • "scopes": [
    ],
  • "source_entitlement": "all",
  • "state_revision": 1722988800123,
  • "state_updated_at": "2026-08-06T18:00:00.123Z"
}

subscriptions

Watch subscriptions and matched-listing candidate feed (M18 P15a). Watch-only — no buying authority.

List watch subscriptions for the authenticated account

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.

Authorizations:
apiKey
query Parameters
status
string
Enum: "open" "expired" "cancelled"

Optional status filter.

cursor
string

Opaque pagination cursor from a prior page's pagination.next_cursor. Omit for the first page.

limit
integer [ 1 .. 200 ]
Default: 50

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Pull matched listings for the authenticated account

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.

Authorizations:
apiKey
query Parameters
cursor
string

Watermark cursor: decimal string of the last consumed seq (from pagination.next_cursor). Omit or empty to start from the beginning of the account's matched-listing feed.

limit
integer [ 1 .. 100 ]
Default: 100

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    },
  • "tip_seq": 0,
  • "shed_reason": "string"
}

catalog

Card archetype lookup. Merged (alias) ids resolve server-side to the canonical body; merged_into_id and identity_key are never exposed.

Get a card by id

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.

Authorizations:
apiKey
path Parameters
card_id
required
string^card_[0-9A-HJKMNP-TV-Z]{26}$
Example: card_01J9ZQ3VP4M8X2K7R5T1W9E6HA

Card identifier (card_ prefix + 26-char uppercase Crockford ULID). Alias (merged) ids are accepted and resolve to the canonical body.

Responses

Response samples

Content type
application/json
{
  • "id": "card_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "category": "SPORT",
  • "set": {
    },
  • "subset": {
    },
  • "parallel": {
    },
  • "card_number": "23",
  • "year": 2023,
  • "print_run": 99,
  • "flags": [
    ],
  • "subjects": [
    ],
  • "facets": [
    ]
}

Search/list subjects

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

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

q
string <= 128 characters
Example: q=lebron

Name prefix (normalized).

sport
string
Example: sport=basketball
kind
string
Enum: "player" "character" "other"

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Search/list teams

Cursor-paginated team browse. Optional q is a case-insensitive name prefix. Pair with GET /cards?team_id= (and optional subject_id).

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

q
string <= 128 characters
Example: q=Lakers
sport
string
Example: sport=basketball

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Search/list sets

Cursor-paginated set browse. Optional q is a case-insensitive name prefix; optional year / sport narrow further.

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

q
string <= 128 characters
Example: q=Prizm
year
integer <int32>
Example: year=2023
sport
string
Example: sport=basketball

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Search/list parallels

Cursor-paginated parallel browse. Optional q is a prefix on normalized name. Canonical rows only.

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

q
string <= 128 characters
Example: q=silver

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Search/list subsets

Cursor-paginated subset browse. Prefer filtering with set_id (subsets are set-scoped). Optional q is a normalized-name prefix.

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

q
string <= 128 characters
Example: q=kaboom
set_id
string^set_[0-9A-HJKMNP-TV-Z]{26}$

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Search/list cards

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.

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

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}$

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

inventory

Account-scoped owned inventory (API-key identity). Status transitions are write-path only; this tag covers reads. acquisition_cost is never exposed on /api.

List account inventory

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

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

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.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Get one inventory item

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.

Authorizations:
apiKey
path Parameters
inventory_id
required
string^inv_[0-9A-HJKMNP-TV-Z]{26}$
Example: inv_01J9ZQ3VP4M8X2K7R5T1W9E6HA

Inventory identifier (inv_ prefix + 26-char uppercase Crockford ULID).

Responses

Response samples

Content type
application/json
{
  • "id": "inv_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "instance_id": "string",
  • "sealed_product_id": "string",
  • "status": "listed",
  • "venue": "ebay",
  • "vault_source": "string",
  • "external_vault_id": "string",
  • "listed_price": {
    },
  • "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"
}

books

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.

List account strategy books

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.

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Create a strategy book

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.

Authorizations:
apiKey
header Parameters
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 409, code idempotency_conflict. Endpoints that document natural idempotency (e.g. POST /api/v1/comp-sales/ingest) accept the header for convention only and do not use it as a replay store.

Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
{
  • "name": "Core graded singles",
  • "enabled": true,
  • "allowed_archetype_codes": [
    ],
  • "strategies": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "book_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "account_id": "acct_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "name": "Core graded singles",
  • "enabled": true,
  • "state_revision": 3,
  • "allowed_archetype_codes": [
    ],
  • "strategies": [
    ],
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Get one strategy book

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.

Authorizations:
apiKey
path Parameters
book_id
required
string^book_[0-9A-HJKMNP-TV-Z]{26}$
Example: book_01J9ZQ3VP4M8X2K7R5T1W9E6HA

Strategy/mandate book identifier (book_ prefix + 26-char uppercase Crockford ULID). Distinct from M7 custody inventory book events.

Responses

Response samples

Content type
application/json
{
  • "id": "book_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "account_id": "acct_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "name": "Core graded singles",
  • "enabled": true,
  • "state_revision": 3,
  • "allowed_archetype_codes": [
    ],
  • "strategies": [
    ],
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

listings

Live marketplace listings normalized into the platform catalog.

List live marketplace listings

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.

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

status
string
Default: "active"
Enum: "active" "ended" "sold" "cancelled"
Example: status=active

Listing lifecycle status. Default active. Open enum — tolerate unknown values.

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

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

Stream listing events (SSE)

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.

Authorizations:
apiKey
header Parameters
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.

Responses

Response samples

Content type
application/problem+json
{
  • "title": "Unauthorized",
  • "status": 401,
  • "detail": "The API key is missing or invalid.",
  • "instance": "/api/v1/jobs/job_01j9zq3vp4m8x2k7r5t1w9e6ha",
  • "code": "unauthorized",
  • "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}

comps

Sold-comp intake and related machine-API surfaces. Scrapers POST raw vendor payloads; the platform owns normalization and identity.

Ingest raw sold-comp scraper payloads

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

Authorizations:
apiKey
header Parameters
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 409, code idempotency_conflict. Endpoints that document natural idempotency (e.g. POST /api/v1/comp-sales/ingest) accept the header for convention only and do not use it as a replay store.

Request Body schema: application/json
required
source
required
string

data_sources.code of the reporter (e.g. cardladder, alt). Must exist; ebay is explicitly rejected.

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

Responses

Request samples

Content type
application/json
{
  • "source": "cardladder",
  • "fetched_at": "2026-07-09T14:00:00Z",
  • "scraper_version": "cardladder-scraper@1.2.0",
  • "records": [
    ]
}

Response samples

Content type
application/json
{
  • "source": "cardladder",
  • "results": [
    ],
  • "summary": {
    }
}

List sold comps

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.

Authorizations:
apiKey
query Parameters
cursor
string
Example: cursor=lst_v1:eyJvYnNlcnZlZF9hdCI6IjIwMjYtMDYtMTEifQ

Opaque pagination cursor from a previous response's pagination.next_cursor. Omit for the first page. Do not parse or store long-term; cursors may expire, and changing any filter invalidates the cursor (400, code invalid_cursor).

limit
integer <int32> [ 1 .. 200 ]
Default: 50
Example: limit=50

Page size. Default 50, maximum 200 (server-enforced; values above the maximum return 400).

sold_from
string <date-time>
Example: sold_from=2024-07-14T00:00:00Z

Inclusive lower bound of sold_at (RFC 3339 UTC). Default = now − 24 months when both bounds are omitted.

sold_to
string <date-time>
Example: sold_to=2026-07-14T00:00:00Z

Exclusive upper bound of sold_at (RFC 3339 UTC). Default = now when both bounds are omitted. Must be after sold_from; span ≤ 36 months.

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 / grade_qualifier this is explicitly broad across companies and qualifiers (M1.7 D6). Exact pricing cohorts require the full key.

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 (+ optional grade_qualifier) for an exact pricing cohort.

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

Responses

Response samples

Content type
application/json
{
  • "items": [
    ],
  • "pagination": {
    }
}

external-data

Card-level external data intake (valuations, population, vendor crosswalks) from marketplace-ingestion services.

Ingest raw external card data (valuations, pop, vendor ids)

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

Authorizations:
apiKey
header Parameters
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 409, code idempotency_conflict. Endpoints that document natural idempotency (e.g. POST /api/v1/comp-sales/ingest) accept the header for convention only and do not use it as a replay store.

Request Body schema: application/json
required
source
required
string

data_sources.code of the reporter (e.g. cardladder, alt). Must exist; ebay is explicitly rejected.

card_id
required
string^card_[0-9A-HJKMNP-TV-Z]{26}$

Platform card id the observations apply to (card_ + 26-char uppercase Crockford ULID).

fetched_at
string <date-time>

Optional scraper fetch timestamp (drives observed_at).

scraper_version
string

Optional scraper build id (metadata only).

required
Array of objects [ 1 .. 500 ] items

Responses

Request samples

Content type
application/json
{
  • "source": "alt",
  • "card_id": "card_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "fetched_at": "2019-08-24T14:15:22Z",
  • "scraper_version": "string",
  • "records": [
    ]
}

Response samples

Content type
application/json
{
  • "source": "string",
  • "card_id": "string",
  • "results": [
    ],
  • "summary": {
    }
}

identity

Source-first identity observation intake (M1.7). Machine surface may return cluster/assurance; does not replace card-first /external-card-data.

Ingest source-first identity observations (raw vendor docs)

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

Authorizations:
apiKey
header Parameters
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 409, code idempotency_conflict. Endpoints that document natural idempotency (e.g. POST /api/v1/comp-sales/ingest) accept the header for convention only and do not use it as a replay store.

Request Body schema: application/json
required
source
required
string

data_sources.code of the reporter (e.g. cardladder, alt). Must exist; ebay is explicitly rejected.

card_id
string^card_[0-9A-HJKMNP-TV-Z]{26}$

Optional. Accepted for forward-compat and shape-validated when present (card_ + 26-char uppercase Crockford ULID), but ignored by the server. Never treated as proof of a unique fingerprint match; catalog mapping requires platform FindUniqueFingerprintMatch. Prefer omitting.

cluster_id
string^icl_[0-9A-HJKMNP-TV-Z]{26}$

Optional. Accepted for forward-compat and shape-validated when present (icl_ + 26-char uppercase Crockford ULID), but ignored until a later design wires safe cluster attach. Never trusted as a client-chosen merge target (avoids IDOR). Prefer omitting; the platform creates/attaches clusters from normalized anchors.

fetched_at
string <date-time>

Optional scraper fetch timestamp (drives observed_at).

scraper_version
string

Optional scraper build id (metadata only).

required
Array of objects [ 1 .. 500 ] items

Responses

Request samples

Content type
application/json
{
  • "source": "cardladder",
  • "card_id": "card_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "cluster_id": "icl_01J9ZQ3VP4M8X2K7R5T1W9E6HA",
  • "fetched_at": "2019-08-24T14:15:22Z",
  • "scraper_version": "string",
  • "records": [
    ]
}

Response samples

Content type
application/json
{
  • "source": "string",
  • "results": [
    ],
  • "summary": {
    }
}