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.

inbound_shipment is recorded going forward only. Every card stored before it existed reads null, and so does a card the bench stored as not coming from a tracked package — null means "no box recorded", not "not received". To reconcile one delivery, sweep GET /cards?type=all&inbound_shipment_id=<id> once the box is RECEIVED and its cards have had their check-in hour, and compare against the card_count you declared.

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.

New cards show up about an hour after a delivery is checked in, not the moment it is scanned — that hour is when we confirm every card is filed to the right account and sub-account. When they appear they are placed after everything already in your sweep, so keep your cursor and sweep again: they arrive from where you left off, never behind it.

?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 string—Exact-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 string—Narrow 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
inbound_shipment_idquery int—Only the cards that came out of one package: the id of an inbound shipment you announced (POST /inbound-shipments returns it). Applies to raw and graded rows alike, combines with every other filter, and works in both pagination modes. An id that is not one of your shipments simply matches nothing. Cards stored before boxes were recorded have no box and never match
sincequery int—Keyset cursor: pass the previous page’s pagination.cursor. Returns the cards after that cursor, in the order they became visible to you — drift-free under concurrent inserts and not bounded by the offset cap. A card that appears later (still being checked in, or still being identified) is placed after everything you have already swept, so the cursor you saved picks it up and nothing is skipped. Omit to use offset mode
limitquery int—1..100, default 50
offsetquery int—0..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[].inbound_shipmentThe package the card arrived in: { id, tracking_number, carrier, reference } — the inbound shipment you announced with POST /inbound-shipments (id is the one GET /inbound-shipments/:id takes; tracking_number, carrier and reference are what you sent). The receiving bench picks the box as it stores each card. null when no box was recorded: cards stored before this field existed, cards that came in some other way than a package you shipped, and cards the bench stored as not from a tracked box. Appended as the last key of the card object
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. Cards still in their check-in hour (see below) are in neither yet. Present only while the count is available
(still being checked in)Newly received cards appear about an hour after the warehouse finishes checking a delivery in. Until then they are not in cards[], not counted in pending_identity, and not reachable by unique_id anywhere in this API — that hour is when we confirm every card is filed to the right account (and sub-account) before you see it. Reconcile a delivery an hour or more after it is received
(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
400Invalid inbound_shipment_idinbound_shipment_id is not a single positive integer
403Tenant API key requiredkey is not tenant-scoped

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