Scripting the admin API
The admin API is what the admin UI uses, behind a session and a CSRF token. A script drives it the same way. Keep the credentials in the environment; this signs in as a real administrator and can do everything one can.
BELUGA_URL=https://shop.example.com \BELUGA_EMAIL=you@example.com BELUGA_PASSWORD='...' \node publish-all.mjspublish-all.mjs
Section titled “publish-all.mjs”const BASE = process.env.BELUGA_URL;const EMAIL = process.env.BELUGA_EMAIL;const PASSWORD = process.env.BELUGA_PASSWORD;
let cookie = "";let csrf = "";
function remember(response) { const set = response.headers.getSetCookie?.() ?? []; if (set.length) cookie = set.map((c) => c.split(";")[0]).join("; ");}
async function api(method, path, body) { const response = await fetch(`${BASE}/api${path}`, { method, headers: { accept: "application/json", ...(body ? { "content-type": "application/json" } : {}), ...(cookie ? { cookie } : {}), ...(csrf ? { "x-csrf-token": csrf } : {}), }, body: body ? JSON.stringify(body) : undefined, }); remember(response); if (!response.ok) { const text = await response.text(); throw new Error(`${method} ${path} → ${response.status}: ${text}`); } return response.json();}
// 1. A session, and with it the CSRF token every write needs.const session = await api("GET", "/session");csrf = session.csrfToken;
// 2. Sign in. The response is identical for a wrong email and a wrong password.await api("POST", "/session", { email: EMAIL, password: PASSWORD });csrf = (await api("GET", "/session")).csrfToken;
// 3. Everything, drafts included.const { products } = await api("GET", "/products/full");const pending = products.filter( (p) => p.isLive && p.variants.some((v) => !v.stripePriceId),);
console.log(`${pending.length} live product(s) not fully published.`);
// 4. Publish each one. This is the only call in this script that writes to Stripe.for (const product of pending) { await api("POST", `/products/${product.id}/publish`); console.log(`published ${product.slug}`);}
await api("DELETE", "/session");What this is careful about
Section titled “What this is careful about”- Publish is deliberate. This script’s whole purpose is to write to Stripe, so it does it through the one route that may, and prints each product it touches. A script that saves products should never expect them to reach Stripe.
- The CSRF token is re-read after login, because the session it belongs to changed.
- The password is a live credential. The session is destroyed at the end, and nothing is written to disk.
- The login route is rate-limited. A script that retries in a tight loop locks itself out for a while, which is the feature working.
Other useful one-offs
Section titled “Other useful one-offs”GET /admin/orders?status=paidandPUT /admin/orders/:idwith{ status, carrier, trackingNumber }to bulk-mark a batch as shipped from a carrier’s manifest.GET /admin/orders.csv?from=&to=nightly into your accounting.POST /admin/products/import/validatewith a CSV body to lint a catalogue file in CI before anyone commits it.