Skip to content
pokemontcgapi.com

no-code

Use the API from Sheets and no-code tools

Pull card data and prices into a spreadsheet or an automation platform without writing a service — and without leaking your key.

Who this is for

You keep an inventory in a spreadsheet, run a shop on a no-code stack, or want to prototype before committing to a codebase.

Plan you need

The free tier is designed for exactly this. A 500-row sheet refreshed daily is well inside the monthly allowance.

A very large amount of real trading card business runs in spreadsheets. Shop inventory, personal collections, consignment lists, buylists — all of it lives in a grid that someone maintains by hand, and the manual part is almost always looking up what a card is and what it is worth. That is a lookup problem, and a REST API with a free tier solves it without anyone having to become a developer first.

Two things make the difference between a sheet that works and one that gets you rate-limited. Batching, because a naive custom function fires once per row and a 500-row sheet is 500 requests every time it recalculates. And caching, because spreadsheets recalculate far more often than their authors expect — on open, on edit, on sort, sometimes on a timer you did not set.

The code

An Apps Script custom function for Google Sheets: it caches for six hours, batches up to 100 ids per call, and keeps the key in script properties rather than in a cell.

ptcg.gs
// Store the key once: Project Settings → Script Properties → PTCG_API_KEY.
// Never put it in a cell — anyone with view access on the sheet can read it.
const BASE = "https://api.pokemontcgapi.com";

function key_() {
  return PropertiesService.getScriptProperties().getProperty("PTCG_API_KEY");
}

function fetch_(path) {
  const cache = CacheService.getScriptCache();
  const hit = cache.get(path);
  if (hit) return JSON.parse(hit);

  const res = UrlFetchApp.fetch(BASE + path, {
    headers: { "X-Api-Key": key_() },
    muteHttpExceptions: true,
  });
  const body = JSON.parse(res.getContentText());
  if (res.getResponseCode() !== 200) {
    throw new Error(body.error.code + ": " + body.error.message);
  }

  // Six hours: card data changes when a set releases, prices once a day.
  cache.put(path, JSON.stringify(body), 21600);
  return body;
}

/**
 * Card name for one or many ids.
 * @param {string|Array} ids A card id, or a range of them.
 * @customfunction
 */
function PTCG_NAME(ids) {
  return lookup_(ids, "name");
}

/**
 * Current index price in EUR.
 * @param {string|Array} ids A card id, or a range of them.
 * @customfunction
 */
function PTCG_PRICE(ids) {
  const flat = flatten_(ids);
  return flat.map(function (id) {
    if (!id) return "";
    const body = fetch_("/v1/cards/" + id + "/prices?currency=EUR");
    const point = body.data.filter(function (p) { return p.variant === "INDEX"; })[0];
    return point ? point.price : "";
  }).map(function (v) { return [v]; });
}

function flatten_(ids) {
  return (Array.isArray(ids) ? ids : [[ids]])
    .reduce(function (acc, row) { return acc.concat(row); }, [])
    .map(String);
}

// One request per 100 ids instead of one per row. This is the whole trick.
function lookup_(ids, field) {
  const flat = flatten_(ids);
  const found = {};

  for (var i = 0; i < flat.length; i += 100) {
    const slice = flat.slice(i, i + 100).filter(String);
    if (slice.length === 0) continue;
    const body = fetch_(
      "/v1/cards/batch?select=id,name,set_name,rarity&ids=" + slice.join(","),
    );
    body.data.forEach(function (card) { found[card.id] = card; });
  }

  return flat.map(function (id) {
    return [found[id] ? found[id][field] : ""];
  });
}

Batch, or the sheet will rate-limit itself

The default shape of a spreadsheet custom function is one call per cell, and that is the shape that gets people into trouble. A column of 500 lookups is 500 requests, and because spreadsheets recalculate on open, on edit and on sort, that column can fire several times an hour without anybody asking it to.

Write the function to accept a range instead of a single value and return a column. Inside, group the ids into batches of 100 and issue one request per batch. Five hundred rows becomes five requests. That single change is the difference between comfortably inside a free tier and repeatedly hitting a burst limit.

Cache, because recalculation is not under your control

Apps Script gives every script a cache with a six-hour ceiling, and six hours is a good fit here: card data changes when a set releases, prices change once a day. Keying the cache by request path means two different formulas asking for the same card share one entry, which is usually most of the traffic in a real sheet.

For platforms without a cache primitive, add a hidden sheet that stores the last fetched value and a timestamp per id, and have the function read from it unless the row is older than your threshold. It is less elegant and works the same way. Either approach also makes the sheet usable offline, which matters more than it sounds like at a convention with bad wifi.

Keep the key out of the document

A key in a cell is visible to everyone with view access, survives in version history, and travels with the file when someone duplicates it. Use script properties in Apps Script, the connection secret store in your automation platform, or an environment variable — anywhere that is part of the project rather than part of the document.

If a key does get exposed, rotate it. Old and new keys overlap for an hour so nothing breaks mid-refresh. And if what you are building is browser-facing rather than a spreadsheet, use a public key instead: catalogue-only scope with a referrer allow-list, which is safe to ship in a bundle in a way a secret key never is.

What usually goes wrong

Not a disclaimer list: these are the failure modes that cost people time on this specific integration.

One formula per cell
A per-cell function feels natural and scales terribly. Accept a range, return a column, batch inside. It is the same amount of code and two orders of magnitude fewer requests.
Volatile functions in the same sheet
NOW, RAND and their relatives force a full recalculation, which re-runs every custom function in the document. Keep them out of a sheet that does API lookups, or the cache is the only thing standing between you and your monthly limit.
Storing prices without their date
Put the as_of date in the next column. A spreadsheet is exactly the kind of artefact that gets emailed around six months later, and a price with no date attached will be read as current.

Endpoints used

  • GET/v1/cards/batchUp to 100 ids per request: the reason a 500-row sheet is five calls.
  • GET/v1/cardsResolve a typed card name to an id, newest printing first.
  • GET/v1/cards/{id}/pricesCurrent price with its provenance and capture date.
  • GET/v1/setsA set list to build a validated dropdown in the sheet.

Related