Skip to main content
Back to documentation

Lunidex Public API v1

Connect a server-side integration to the data already synced with a Lunidex account. Start with a key and one request, then explore cards, sealed products, pagination, and safe writes.

Base URLhttps://lunidex.app/api/v1
Manage API keysOpen the OpenAPI specification

Server-side keys only

Never put an API key in browser code, a mobile app, a public repository, a URL, a screenshot, or a shared log. Keep it in a server-side secret manager and send it only in the Authorization header.

Quick start

A working integration needs a key, a server-side secret, and an HTTPS request to the API base URL.

  1. 1

    Create a key

    Open the API keys card in your Lunidex dashboard. Give the key a name and choose read-only or read-and-write access. The full secret is shown once; copy it before closing the message.

  2. 2

    Store the secret on your server

    Save the key as LUNIDEX_API_KEY in your hosting provider’s secret manager. Do not commit it or send it to a browser.

  3. 3

    Make the first request

    Send GET /me from your backend. Private endpoints require Authorization: Bearer followed by the key.

cURL · run in a trusted server environment
curl --fail-with-body \
  -H "Authorization: Bearer $LUNIDEX_API_KEY" \
  https://lunidex.app/api/v1/me
Node.js · server-side fetch
const API_BASE_URL = "https://lunidex.app/api/v1";
const apiKey = process.env.LUNIDEX_API_KEY;

if (!apiKey) throw new Error("Missing LUNIDEX_API_KEY");

const response = await fetch(API_BASE_URL + "/me", {
  headers: { Authorization: "Bearer " + apiKey },
});
const result = await response.json();

if (!response.ok) {
  throw new Error(result.error?.code ?? "REQUEST_FAILED");
}

console.log(result.data);

Authentication and permissions

Send the key in the HTTP Authorization header for every private /api/v1 request. The API returns JSON in a data envelope; private responses use Cache-Control: private, no-store.

Read

Can call the GET endpoints. Use this permission when an integration only needs to display or export account data.

Read and write

Can call both GET endpoints and write endpoints. Use it only for a trusted server integration that must update the collection or sealed ledger.

The API sees only data synced to the Lunidex account in Neon. Data that exists only on a device is not available. Account identity omits the email address.

Pagination and filters

List endpoints return data plus meta.nextCursor. Pass that cursor unchanged to request the next page, and stop when it is null.

  • The default page size is 25 and the maximum is 100. Catalogue search is fixed at 24 results per page.
  • Cards accept language and set filters. Sealed positions accept language and productId; transactions accept language, productId, type, includeVoided, and voided. Catalogue search accepts q and cursor.
  • A cursor can become stale when the collection, journal, prices, or filters change. The API returns 409; start again without the cursor and use the latest state.
Fetch every card page from a server
const API_BASE_URL = "https://lunidex.app/api/v1";
const apiKey = process.env.LUNIDEX_API_KEY;
let cursor = null;

if (!apiKey) throw new Error("Missing LUNIDEX_API_KEY");

do {
  const query = new URLSearchParams({ limit: "100" });
  if (cursor) query.set("cursor", cursor);

  const response = await fetch(API_BASE_URL + "/cards?" + query, {
    headers: { Authorization: "Bearer " + apiKey },
  });
  const result = await response.json();
  if (!response.ok) throw new Error(result.error?.code ?? "REQUEST_FAILED");

  console.log(result.data);
  cursor = result.meta?.nextCursor ?? null;
} while (cursor);

Routes

Paths are relative to the API base URL. Each row shows required access and the parameters listed in the OpenAPI contract. Open the machine-readable specification for full schemas, request and response examples, and operation errors; the sections below provide the human-readable guide.

Account and contract

  • GETPublic · no key
    /openapi.json

    Public machine-readable contract; no API key required.

    Parameters:None
  • GETRead key
    /me

    Limited account identity; the response has no email address.

    Parameters:None
  • GETRead key
    /summary

    Main account statistics, physical and distinct card counts, and sealed totals in EUR cents.

    Parameters:None

Card collection

  • GETRead key
    /cards

    Paged card holdings, optionally filtered by language and set.

    Parameters:
    • cursor(query)
    • limit(query)
    • language(query)
    • set(query)
  • GETRead key
    /cards/{cardId}

    TCGdex metadata and this account’s holdings; no card valuations.

    Parameters:
    • cardId(path)· required
    • language(query)
  • PUTRead/write key
    /cards/{cardId}

    Set an absolute quantity for one card language and variant.

    Parameters:
    • cardId(path)· required

Sealed products and transactions

  • GETRead key
    /sealed/catalogue

    Search the existing Cardmarket sealed-product catalogue.

    Parameters:
    • q(query)
    • cursor(query)
  • GETRead key
    /sealed/positions

    Paged current positions with cost and stored market value.

    Parameters:
    • cursor(query)
    • limit(query)
    • language(query)
    • productId(query)
  • GETRead key
    /sealed/positions/{productId}

    Current language positions for one owned product.

    Parameters:
    • productId(path)· required
  • GETRead key
    /sealed/transactions

    Private, revisioned transaction journal with filters.

    Parameters:
    • cursor(query)
    • limit(query)
    • language(query)
    • productId(query)
    • type(query)
    • includeVoided(query)
    • voided(query)
  • POSTRead/write key
    /sealed/transactions

    Create a buy, sell, or exchange; idempotency key required.

    Parameters:
    • Idempotency-Key(header)· required
  • GETRead key
    /sealed/transactions/{id}

    Read one transaction belonging to this account.

    Parameters:
    • id(path)· required
  • PATCHRead/write key
    /sealed/transactions/{id}

    Revise a transaction with revision checks.

    Parameters:
    • id(path)· required
  • POSTRead/write key
    /sealed/transactions/{id}/void

    Void a transaction with revision checks.

    Parameters:
    • id(path)· required

Cards

GET /cards lists owned variants without loading the full TCGdex catalogue. Each holding includes cardId, setId, language, variant, and quantity; the optional list filters are language and set.

GET /cards/{cardId} returns useful TCGdex metadata and holdings for a card in this account. Add the language query parameter to narrow the holdings.

PUT /cards/{cardId} takes language, variant (unspecified, normal, reverse, or holo), and an absolute quantity from 0 to 10,000. A quantity of 0 removes that variant.

PUT /cards/{cardId} · application/json
{
  "language": "en",
  "variant": "reverse",
  "quantity": 2
}

Older holdings without a language remain visible; their language can be null and they may be marked legacy.

Card valuations are never returned by this API.

Sealed products

Sealed positions include quantities by language, cost, and current value from prices already stored in Lunidex. Amounts are integer EUR cents. A missing current price is null and is identified by missingPrice; the summary also reports missingPrices.

In GET /summary, the sealed value is null if any held product has no price; missingPrices gives the count.

GET /sealed/catalogue

Search products already in the Cardmarket catalogue with q (up to 150 characters) and cursor. Each page contains at most 24 products; use its product ID in later requests.

GET /sealed/positions

GET /sealed/positions supports language and productId filters. GET /sealed/positions/{productId} returns the current position for one product. These endpoints expose current totals, not a full price-history series.

GET /sealed/transactions

The private journal supports language, productId, type (buy, sell, exchange), includeVoided, and voided filters. It returns revisions so clients can make safe changes.

Writes, idempotency, and revisions

Set a card quantity

PUT /cards/{cardId} sets the quantity, rather than adding a delta. Repeating the same request leaves the same quantity; send 0 to remove the variant. The API validates the card, set, language, and available variant before writing.

Create a sealed transaction

POST /sealed/transactions accepts buy, sell, and exchange movements. It requires an Idempotency-Key header (8–200 characters) and expectedRevision. Reuse the same key for a retry of the same request so the operation does not create a second movement. The first request creates the transaction; a replay returns the original result.

Example buy body · replace the product ID and revision with current values
curl --request POST "https://lunidex.app/api/v1/sealed/transactions" \
  -H "Authorization: Bearer $LUNIDEX_API_KEY" \
  -H "Idempotency-Key: lunidex-import-example-001" \
  -H "Content-Type: application/json" \
  --data '{
    "expectedRevision": 0,
    "kind": "buy",
    "cardmarketProductId": 12345,
    "language": "en",
    "date": "2026-09-27",
    "quantity": 1,
    "unitPriceCents": 2500
  }'

Before a write, read the latest journal revision from GET /sealed/transactions metadata. Use it as expectedRevision. PATCH /sealed/transactions/{id} also requires that transaction’s revision. A stale revision returns 409; reload the latest transaction and journal before deciding whether to retry.

PATCH /sealed/transactions/{id}

POST /sealed/transactions/{id}/void
POST /sealed/transactions/{id}/void requires revision and expectedRevision. Creation, revision, and voiding use the same transaction validation, allocations, and audit trail as Lunidex.

application/json
{
  "revision": 1,
  "expectedRevision": 0
}

Errors and response shape

Success responses use { "data": ..., "meta": ... } (meta is optional). Errors use { "error": { "code": ..., "message": ..., "details": ... } } (details is optional).

Common responses: 400 invalid transaction data; 401 invalid key; 403 insufficient permission; 404 missing or unowned resource; 409 stale cursor or revision; 410 account being deleted; 422 invalid query or request body; 429 quota; 500 unexpected server error; 502 card data unavailable; and 503 API unavailable.

On 409, fetch the latest resource or restart the list without its cursor. Do not blindly turn a conflict into a new transaction request.

A 429 response includes Retry-After. Wait at least that many seconds before retrying.

All private API responses use Cache-Control: private, no-store. The public OpenAPI document is served separately at /api/v1/openapi.json.

Quotas

Limits are applied per Lunidex account and shared by its API keys.

Quotas
Reads per minute60
Writes per minute10
Reads per day1,000
Writes per day100
Card-detail reads per account per day100
Sealed-calculation reads per account per day100
Global daily ceiling per costly category5,000

Card details and sealed calculations also share separate global ceilings of 5,000 operations per day per category. A 429 response includes Retry-After.

Open the OpenAPI specification