Read
Can call the GET endpoints. Use this permission when an integration only needs to display or export account data.
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.
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.
A working integration needs a key, a server-side secret, and an HTTPS request to the API base URL.
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.
Save the key as LUNIDEX_API_KEY in your hosting provider’s secret manager. Do not commit it or send it to a browser.
Send GET /me from your backend. Private endpoints require Authorization: Bearer followed by the key.
curl --fail-with-body \
-H "Authorization: Bearer $LUNIDEX_API_KEY" \
https://lunidex.app/api/v1/meconst 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);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.
Can call the GET endpoints. Use this permission when an integration only needs to display or export account data.
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.
List endpoints return data plus meta.nextCursor. Pass that cursor unchanged to request the next page, and stop when it is null.
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);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.
/openapi.jsonPublic machine-readable contract; no API key required.
/meLimited account identity; the response has no email address.
/summaryMain account statistics, physical and distinct card counts, and sealed totals in EUR cents.
/cardsPaged card holdings, optionally filtered by language and set.
cursor(query)limit(query)language(query)set(query)/cards/{cardId}TCGdex metadata and this account’s holdings; no card valuations.
cardId(path)· requiredlanguage(query)/cards/{cardId}Set an absolute quantity for one card language and variant.
cardId(path)· required/sealed/catalogueSearch the existing Cardmarket sealed-product catalogue.
q(query)cursor(query)/sealed/positionsPaged current positions with cost and stored market value.
cursor(query)limit(query)language(query)productId(query)/sealed/positions/{productId}Current language positions for one owned product.
productId(path)· required/sealed/transactionsPrivate, revisioned transaction journal with filters.
cursor(query)limit(query)language(query)productId(query)type(query)includeVoided(query)voided(query)/sealed/transactionsCreate a buy, sell, or exchange; idempotency key required.
Idempotency-Key(header)· required/sealed/transactions/{id}Read one transaction belonging to this account.
id(path)· required/sealed/transactions/{id}Revise a transaction with revision checks.
id(path)· required/sealed/transactions/{id}/voidVoid a transaction with revision checks.
id(path)· requiredGET /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.
{
"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 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.
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 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.
The private journal supports language, productId, type (buy, sell, exchange), includeVoided, and voided filters. It returns revisions so clients can make safe changes.
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.
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.
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.
{
"revision": 1,
"expectedRevision": 0
}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.
Limits are applied per Lunidex account and shared by its API keys.
| Reads per minute | 60 |
|---|---|
| Writes per minute | 10 |
| Reads per day | 1,000 |
| Writes per day | 100 |
| Card-detail reads per account per day | 100 |
| Sealed-calculation reads per account per day | 100 |
| Global daily ceiling per costly category | 5,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