List cards

GET /api/v1/cards tenant key

Lists your cards — a thin shape suited for inventory reconciliation. Newest first by default; pass ?since for a drift-free keyset sweep. Every row now carries a `type` of "RAW" or "GRADED", and `?type=` picks which inventory you sweep. Leaving ?type off returns exactly the rows this endpoint has always returned, so an existing sweep needs no edit — what changes is that the graded ones among them are finally labelled as such. `?type=graded` lists your slabs, which had no endpoint at all before, and `?type=all` sweeps both as one continuous pass. Inventory is mixed: a row is either a TCG card or a sports card, and category tells you which vocabulary it speaks before you read anything else. Every row also carries a sub_account label — null while the card sits in your own inventory — and ?sub_account= narrows the list to one of them; omit it and you get everything, exactly as you did before sub-accounts existed. Cards are returned only while your account is active — a suspended account receives an empty list rather than an error, so treat an unexpectedly empty sweep as a signal to check with your account manager rather than as zero inventory.

?sub_account= is additive, not a behavior change. Omitted, empty, or `all` returns exactly the same ROWS this endpoint returned before sub-accounts existed — every card, across your own inventory and every sub-account. The one change on the wire is a sub_account key appended to each card object; it is appended last precisely so key order is untouched for anything that hashes or golden-diffs the body. An existing sweep needs no edit.

Sweep inventory with ?since, not ?offset: keyset pagination never re-serves or skips rows while cards are being inserted, and is not bounded by the 10,000-row offset cap. Start with no since, then pass the returned cursor until has_more is false.

?type=all is one sweep, not two. It pages through your raw cards first and then continues into your slabs, and a single page can hold the tail of one and the head of the other — so keep following pagination.cursor until has_more is false rather than stopping when the first GRADED row appears, and do not assume a page is all one type. Reconciling a whole shipment is: cards[] from a ?type=all sweep, plus pending_identity.

Adopt `type` before you adopt `?type=`. Reading the field costs nothing and immediately tells you which of the rows you already receive are slabs; a row you have been pricing as a raw card and that comes back "GRADED" was always a slab, and its declared_value is the figure to trust.

Read sports.record_status before you trust a sports row. "provisional" means the row was machine-extracted from the warehouse scan at intake and has never been checked against a catalog — the player name, parallel, print run and card number are a model reading a photograph, and any of them can change. "canonical" is catalog-verified; "merged" means the row has been folded into a canonical one. A provisional that later merges is repointed at the canonical row, so sports.id can change for a card you have already seen: key your records on unique_id, never on sports.id.

Try it GET /api/v1/cards?limit=10

These inputs are shared across all docs pages, so an id entered here carries over.

response

Not run yet. Press Run to make a live call against https://service.rip.fun (through this demo's server-side proxy; the API key never reaches the browser).

curl (tracks the inputs above)
curl -X GET 'https://service.rip.fun/api/v1/cards?limit=10' \
  -H 'X-API-Key: rip_…'

Request fields

FieldTypeRequiredDescription
unique_idquery stringExact-match filter (applies to both pagination modes)
typequery string`raw` | `graded` | `all`. OMITTING it is not the same as `raw`: omitted, you get every row this endpoint has always returned — which includes a small number of older slabs that live alongside the raw cards — so nothing disappears from a sweep you already run. `raw` is the clean raw-only stream, `graded` is your slabs only, `all` is both as one sweep (raw first, then graded). Branch on the `type` field on each row, not on which query you sent
sub_accountquery stringNarrow to one sub-account by slug, matched case-insensitively. Omitted, empty, or `all` returns every card across your own inventory and all sub-accounts; `main` (or `none`) returns only cards in your own inventory. An unrecognized slug returns 400 listing every slug your account accepts — retired ones included, since their cards stay filterable — never an empty 200. Supply it once: repeating it (?sub_account=a&sub_account=b) is a 400. Read the slugs from GET /sub-accounts
sincequery intKeyset cursor: pass the previous page’s pagination.cursor. Returns cards with id > since, ascending — drift-free under concurrent inserts and not bounded by the offset cap. Omit to use offset mode
limitquery int1..100, default 50
offsetquery int0..10_000, default 0 (ignored when since is supplied)

Response fields (data)

FieldDescription
cards[].unique_id / card_idThe canonical CARD-RIP… identifier + catalog card id. card_id is null on sports cards — they are not in the TCG catalog
cards[].type"RAW" | "GRADED", on every row — including the raw ones you already receive, so you never have to infer it from the absence of another key. It is a SECOND discriminator, orthogonal to category: a row is (tcg | sports) × (RAW | GRADED). A GRADED row is a card in a grading company’s sealed holder; its grade and cert live in the `graded` block
cards[].gradedGrading identity. Present only when type is "GRADED" — the key is absent, not null, on a raw row: company (PSA | CGC | BGS | TAG | …), grade (as written, e.g. "10" or "9.5", null if we could not resolve it), grade_numeric (tenths, so 100 is a 10 — compare on this, never parse the string), cert_number, cert_url (the grader’s own verification page), verification_status ("verified" means the grading company’s record confirmed this cert; anything else means we could not reach or match it and the grade is our reading of the label), graded_at
cards[].category / gameThe discriminator, on every row. category is "tcg" | "sports"; game is pokemon | onepiece | basketball | baseball | football | soccer | hockey | other. Both are null when the row resolves to neither catalog — treat that as unknown, not as TCG. Branch on category; new game values are added over time
cards[].sub_accountWhich of your sub-accounts the card is filed under: { slug, name }, or null when it is in your own inventory. A label, not a boundary — a filed card is handled, billed, insured and shelved exactly like the rest of your inventory and sits in the same bin. GET /sub-accounts enumerates the slugs; POST /cards/sub-account changes them
cards[].name / card_number / set_nameCatalog identity of the card, filled from whichever catalog owns the row. On a sports card, name is the player name and card_number / set_name come from the sports catalog row
cards[].release_date / yearSet release date (ISO 8601, always null for sports cards — a sports row carries a season, not a release date) + release year (sports cards use the season start year)
cards[].image_url / scan_image_urlCatalog image + your scanned image. image_url is null on sports cards: we hold no licensed product art for them, so scan_image_url — our own warehouse photograph — is the only image you get. On a GRADED row scan_image_url is a photograph of the SLAB, not of the bare card: the grading company’s own cert scan, replaced by our warehouse photograph if we take one
cards[].insured_priceThe catalog MARKET price for the card (decimal string) — not a declared value and not an insured amount. Null when our catalog has no price for the card. On a GRADED row read declared_value instead: that figure is snapshotted from the price for the actual (card, company, grade), whereas insured_price can fall back to the RAW card’s price, which understates a high-grade slab by a wide margin
cards[].declared_valueThe value this copy is carried and insured at: { amount_cents, currency, source, effective_at }. source is "tenant" once you set one via PUT /declared-values, or "RIP" while it is still our snapshot of the catalog price at scan time. amount_cents is null when the card has neither
cards[].sportsSports-catalog identity. Present only when category is "sports" — the key is absent, not null, on every other row: id, sport, player_name, team, manufacturer, set_name, year (the raw season string, e.g. "2023-24"), card_number, parallel_name, print_run, is_autograph, is_relic, is_rookie, record_status. Every field inside is nullable
pending_identityCards we have received and are still identifying. Counts the streams you asked for, so with ?type=graded it is slabs only and with ?type=all it is both. They are NOT in cards[] and will appear there once identified — a card is listed only when we can state what it is and what it is worth, rather than a placeholder we would have to correct later. Reconcile a shipment against cards[] + pending_identity; the same card is never in both. Present only while the count is available
(removed duplicates)A card we added by mistake — the same physical card scanned in twice — is taken back out of your inventory and drops out of cards[] for good. If you had already fetched it, a card.removed event in GET /inventory/events names its unique_id (and the copy we kept); a card you have already fetched is only ever removed within three hours of its intake
cards[].created_at / updated_atIntake + last-touch timestamps
paginationOffset mode: { limit, offset, has_more }. Keyset mode (?since): { cursor, has_more, limit } — pass cursor back until has_more is false. With ?type= the cursor is an opaque STRING ("r:1234", "g:57") because raw and graded cards are numbered independently; without ?type= it stays the plain integer it has always been. Either way, treat it as a token to hand back rather than a number to store or compare

Errors

StatusCodeWhen
400invalid cursorsince is not a cursor this endpoint issued (fails loudly instead of re-delivering your whole inventory). Also when a cursor from one stream is replayed against another — a "g:" cursor with ?type=raw — or when a bare integer is sent with ?type=all, where it cannot say which stream it belongs to
400invalid typetype is not raw, graded or all (the message lists the valid values)
400invalid sub_accountsub_account is a slug your account does not have (the message lists every accepted slug, retired included), or was supplied more than once
403Tenant API key requiredkey is not tenant-scoped

See Errors for the response envelope and the full code list.