Card Orb API
Version 1.0.0 · openapi.yaml
Card Orb keeps one person's Pokémon card collection in Postgres, matches it against three card catalogues, prices it from Cardmarket, and serves it here.
**Two clients read this API and both are ours**: the web tool at cardorb.com and the iOS app. Nothing here is designed for a third party yet.
**Every failure is `{ "error": "<sentence>" }`** at a status a client can branch on. The sentence is for a person to read; the status is for the code.
**The version is in the path.** `/v1` changes only by addition: a new field, a new route, a new optional query. Anything that removes or renames is `/v2`.
Servers
- https://api.cardorb.com — The API on its own host. `/v1/…` here is `/api/v1/…` on cardorb.com.
- https://cardorb.com/api — The same API under the web tool's own origin, which is what the browser uses.
- http://localhost:3000/api — Local development.
Collection
What the signed-in person owns, and the writes that change it.
GET /v1/collection
The whole collection, grouped by set · bearer token or session cookie
Every set the caller owns a card from, with every card and every copy. Prices included. An empty collection is `{ "sets": [] }`, not an error.
- 200 The collection.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 429 Over this route's rate limit for this address.
- 503 This deployment has no database, or the database is not answering.
POST /v1/cards
Add a card · bearer token or session cookie
Writes one collection row. `BODY_LIMIT.card`, 8 kB.
- 200 Added.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 415 A write without `Content-Type application/json`, which is what forces a browser's preflight.
- 429 Over this route's rate limit for this address.
- 502 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
OPTIONS /v1/cards
CORS preflight for a cross-origin write · no key
- 204 Allowed. Carries the `Access-Control-Allow-*` headers.
- 403 That origin is not in `ALLOWED_ORIGINS`. Empty body.
GET /v1/cards/{tcgId}
One card, its printings and its price · bearer token or session cookie
- tcgId path, required — TCGdex's card id, e.g. `swsh3-136`.
- 200 The card.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 404 Nothing by that name.
- 429 Over this route's rate limit for this address.
- 503 This deployment has no database, or the database is not answering.
PATCH /v1/collection/items/{id}
Change one copy's inventory fields · bearer token or session cookie
- id path, required — The collection row, from `Variant.id` or `BrowseCard.itemIds`.
- 200 Changed. The row as it now is.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 415 A write without `Content-Type application/json`, which is what forces a browser's preflight.
- 429 Over this route's rate limit for this address.
- 502 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
DELETE /v1/collection/items/{id}
Remove one copy · bearer token or session cookie
Needs `Content-Type application/json` even without a body; that is what forces the preflight.
- id path, required — The collection row, from `Variant.id` or `BrowseCard.itemIds`.
- 200 Removed.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 415 A write without `Content-Type application/json`, which is what forces a browser's preflight.
- 429 Over this route's rate limit for this address.
- 502 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
OPTIONS /v1/collection/items/{id}
CORS preflight for a cross-origin write · no key
- id path, required — The collection row, from `Variant.id` or `BrowseCard.itemIds`.
- 204 Allowed.
- 403 That origin is not in `ALLOWED_ORIGINS`. Empty body.
GET /v1/fields
The database's select options · bearer token or session cookie
The cheapest call there is, so it is also how a client learns whether its credential still works.
- 200 The options.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 429 Over this route's rate limit for this address.
- 502 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
Catalogue
Every set and every card, whether owned or not. Read from pokemontcg.io, cached five minutes.
GET /v1/catalog/sets
Every set, with how much of each the caller holds · bearer token or session cookie
- 200 The sets.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 429 Over this route's rate limit for this address.
- 502 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
GET /v1/catalog/sets/{setId}
One whole set, a page at a time · bearer token or session cookie
- setId path, required — pokemontcg.io's set id, e.g. `swsh3`.
- page query
- pageSize query — Clamped to 250.
- 200 The page.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 404 Nothing by that name.
- 429 Over this route's rate limit for this address.
- 502 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
GET /v1/catalog/search
Find a card by name, number, set or type · bearer token or session cookie
Two modes. With any of `name`, `number`, `set`, `type` it filters; otherwise `query` (two characters or more) is a free search.
- query query
- name query
- number query
- set query
- type query
- page query
- 200 The matches.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 429 Over this route's rate limit for this address.
- 502 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
Value
What the collection is worth, over time.
GET /v1/value-history
The caller's collection value over time, oldest first · bearer token or session cookie
Needs a bearer token or a session. The deprecated passcode names no session and gets an empty series.
- 200 The series.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 429 Over this route's rate limit for this address.
- 503 This deployment has no database, or the database is not answering.
Profile
The signed-in person's name, avatar and public flag.
GET /v1/profile
The caller's own profile · bearer token or session cookie
- 200 The profile.
- 401 No viewer. Sign in, or send a bearer token.
- 404 Nothing by that name.
- 503 This deployment has no database, or the database is not answering.
PATCH /v1/profile
Change the display name, the public flag, or mark onboarding done · bearer token or session cookie
Same-origin only. At least one key; `onboarded` accepts only `true`.
- 200 Changed. Echoes the keys that were applied.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 500 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
POST /v1/profile/avatar
Set the avatar · bearer token or session cookie
Same-origin only. A PNG, JPEG or WebP as a data URL, at most 2 MB decoded. Thirty an hour.
- 200 Set.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 429 Over this route's rate limit for this address.
- 500 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
DELETE /v1/profile/avatar
Remove the avatar · bearer token or session cookie
- 200 Removed.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 500 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
POST /v1/username
Claim a username · bearer token or session cookie
Same-origin only. Lower-cased and trimmed first; claiming your own name is a no-op that answers 200.
- 200 Claimed.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 409 Taken, or reserved.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 500 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
GET /v1/usernames/{name}
Is this name free? · no key
Same-origin only, no viewer needed, sixty a minute. Answers 200 either way; a "no" carries the reason.
- name path, required
- 200 The answer.
- 403 The request came from an origin this deployment does not allow.
- 429 Over this route's rate limit for this address.
Account
Signing up, signing in, passwords and deletion. Browser-only, same-origin.
POST /v1/session
Sign in · no key
Same-origin only; the browser's form. Sets the session cookie. Twenty attempts per address and five per address-and-email each fifteen minutes.
- 200 Signed in.
- 400 The request was understood and refused. The sentence says why.
- 401 That email or password is not right.
- 403 Wrong origin, or the address is not confirmed yet (then `unconfirmed` is `true`).
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 429 Over this route's rate limit for this address.
- 503 This deployment has no database, or the database is not answering.
DELETE /v1/session
Sign out · no key
Clears the session cookie. Always 200, even with no session.
- 200 Signed out.
POST /v1/signup
Create an account · no key
Same-origin only. Five per address each fifteen minutes. Sends a confirmation email; the account is `pending` until it is followed.
- 200 Created, pending confirmation.
- 400 The request was understood and refused. The sentence says why.
- 403 Wrong origin, or new accounts are closed.
- 409 That address is already in use.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 429 Over this route's rate limit for this address.
- 503 This deployment has no database, or the database is not answering.
POST /v1/confirmation
Send the confirmation email again · no key
Same-origin only. Always 200 so an address cannot be probed; over three per fifteen minutes it silently does nothing.
- 200 Accepted, whether or not a mail went out.
- 400 The request was understood and refused. The sentence says why.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 503 This deployment has no database, or the database is not answering.
POST /v1/password
Change the password · session cookie
Same-origin, session only. `currentPassword` is required unless the caller arrived through recovery.
- 200 Changed.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 429 Over this route's rate limit for this address.
- 503 This deployment has no database, or the database is not answering.
POST /v1/password/reset
Send a password reset email · no key
Same-origin only. Always 200; over three per fifteen minutes it silently does nothing.
- 200 Accepted.
- 400 The request was understood and refused. The sentence says why.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 503 This deployment has no database, or the database is not answering.
POST /v1/email
Change the email address · session cookie
Same-origin, session only. Ten per address each fifteen minutes. Supabase mails both addresses.
- 200 Requested.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 429 Over this route's rate limit for this address.
- 503 This deployment has no database, or the database is not answering.
DELETE /v1/account
Delete the account and everything in it · bearer token or session cookie
Needs the password in the body, as confirmation. Irreversible.
- 200 Deleted.
- 400 The request was understood and refused. The sentence says why.
- 401 No viewer. Sign in, or send a bearer token.
- 403 Wrong origin, or that password is not right.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 429 Over this route's rate limit for this address.
- 500 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
Public
The three unkeyed routes behind /user/<name>. No prices, no inventory, own rate limiter.
GET /v1/public/{username}/collection
A public collection, without prices · no key
No key. Of each copy, only `rarity` and `owned` are published (R-API-002); `imageHigh` is dropped too. Sixty a minute per address; cached five minutes at the CDN.
- username path, required
- 200 The collection.
- 404 Nothing by that name.
- 429 Over this route's rate limit for this address.
GET /v1/public/{username}/cards/{tcgId}
One card from a public collection, without its price · no key
- username path, required
- tcgId path, required
- 200 The card, with `price` and `market` both `null`.
- 404 Nothing by that name.
- 429 Over this route's rate limit for this address.
GET /v1/public/{username}/latest-pull
The most recent addition · no key
The one route meant to be read from another site, so the one that sends `Access-Control-Allow-Origin: *`. The newest owned, dated, non-excluded printing. 404 means there is nothing to show.
- username path, required
- 200 The card.
- 404 Nothing by that name.
- 429 Over this route's rate limit for this address.
OPTIONS /v1/public/{username}/latest-pull
CORS preflight · no key
- 204 Allowed for any origin.
Import
Loading a collection from a CSV file.
POST /v1/import/csv
Preview or commit a CSV import · session cookie
Same-origin, session only. Without `commit` it is a dry run that answers with the header, the guessed column map and a sample. With `commit: true` it writes, ten times per person each fifteen minutes. 2 MB, 5,000 rows.
- 200 The outcome. A preview also carries `header`, `guessed` and `skippedRows`.
- 400 Refused. When the name and set columns could not be found, `header` and `guessed` come along so the client can ask.
- 401 No viewer. Sign in, or send a bearer token.
- 403 The request came from an origin this deployment does not allow.
- 413 The body is bigger than this route's `BODY_LIMIT`.
- 429 Over this route's rate limit for this address.
- 500 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.
GET /v1/imports
Recent imports · session cookie
- 200 The most recent imports, newest first.
- 401 No viewer. Sign in, or send a bearer token.
- 503 This deployment has no database, or the database is not answering.
Platform
The health check and the nightly price snapshot. Not for clients.
GET /v1/health
Is the database reachable? · no key
No key. Also the daily keepalive, so a free Supabase project does not pause.
- 200 Fine.
- 503 The database is configured and not answering.
GET /v1/cron/snapshot
Record today's collection value for every account · cron secret
Vercel's cron, with `CRON_SECRET` as the bearer. Sixty seconds at most.
- 200 Every account written.
- 207 Some accounts written; `failed` names the rest.
- 401 No viewer. Sign in, or send a bearer token.
- 502 A catalogue, the price guide or the database did not answer.
- 503 This deployment has no database, or the database is not answering.