HTTP-first Node integration
Node 18+ ships with global fetch, so no package install is required. Keep APPSCREENSHOTAPI_KEY in the environment, send an idempotency key per release, and let the response drive downstream preview or upload steps.
SDKs · Node.js
Use built-in fetch to call the API directly from release scripts, coding agents, or CI jobs. The request and response bodies match the OpenAPI contract exactly.
Node 18+ ships with global fetch, so no package install is required. Keep APPSCREENSHOTAPI_KEY in the environment, send an idempotency key per release, and let the response drive downstream preview or upload steps.
One call renders the whole set. Authenticate with a live key (asa_live_…) from api.appscreenshotapi.com/register, post the set to POST /v1/renders, and read images[].url off the JSON response. Keep the key in an environment variable, never in source.
// Works today against the live API - no SDK required.
// Node 18+ has global fetch built in.
const APPSCREENSHOTAPI_KEY = process.env.APPSCREENSHOTAPI_KEY; // asa_live_...
const res = await fetch("https://api.appscreenshotapi.com/v1/renders", {
method: "POST",
headers: {
"Authorization": `Bearer ${APPSCREENSHOTAPI_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "release-42-en-us",
},
body: JSON.stringify({
preset: "aurora-midnight",
canvas: { preset: "appstore.iphone_6_9" },
theme: {
accentColor: "#5EEAD4",
device: { frame: { style: "clay" }, shadow: { elevation: "medium" } },
},
slides: [
{
role: "hook",
headline: { lines: ["Track every habit", "in one tap"], accent: "one tap" },
screenshot: { url: "https://acme.app/shots/home.png" },
},
{
role: "feature",
headline: { lines: ["See your streaks build"] },
subtitle: "Daily, weekly, and monthly views.",
screenshot: { url: "https://acme.app/shots/streaks.png" },
tilt: "left",
},
{
role: "proof",
headline: { lines: ["Loved by 40,000 builders"] },
screenshot: { url: "https://acme.app/shots/stats.png" },
overlays: [{ kind: "stars", rating: 4.9, count: "12,400 reviews" }],
},
],
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`${err.error?.code}: ${err.error?.message}`);
}
const render = await res.json();
// images[] are CDN URLs (never base64), retained for 30 days.
for (const image of render.images) {
console.log(image.url, `${image.width}x${image.height}`);
}
// The lint report ships in every response - read it and self-correct.
const failures = render.lint.filter((finding) => finding.status === "fail");
if (failures.length) console.warn("Store lint failures:", failures);The response also carries the machine-readable lint report and credits_used. Send an Idempotency-Key so retries replay the original render instead of billing twice.
Small sets render synchronously in one round trip. For large locale or store matrices, send async: true. The API responds with 202 and a render id, and you poll GET /v1/renders/{id} until the status is succeeded or failed.
// Large locale or store matrices: send async and poll the render id.
const apiKey = process.env.APPSCREENSHOTAPI_KEY;
const base = "https://api.appscreenshotapi.com";
// 1. Kick off an async render - returns 202 with a polling_url.
const accepted = await fetch(`${base}/v1/renders`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
async: true,
preset: "aurora-midnight",
canvas: { preset: "appstore.iphone_6_9" },
output: { locales: ["en-US", "de-DE", "ja-JP"], stores: ["appstore", "play"] },
slides: [
{ role: "hook", headline: { lines: ["Track every habit"] },
screenshot: { url: "https://acme.app/shots/home.png" } },
],
}),
}).then((r) => r.json());
// 2. Poll GET /v1/renders/{id} until the status is terminal.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitForRender(id) {
for (;;) {
const render = await fetch(`${base}/v1/renders/${id}`, {
headers: { "Authorization": `Bearer ${apiKey}` },
}).then((r) => r.json());
if (render.status === "succeeded") return render;
if (render.status === "failed") throw new Error(render.error?.message);
await sleep(2000);
}
}
const render = await waitForRender(accepted.id);
console.log(render.images.map((image) => image.url));For inline build steps, poll the render id as shown above. If your workflow uses callbacks, pass webhook_url and verify the signed event before acting on it.
Set output.format to fastlane_zip and the response adds an archive.url: a deliver-ready ZIP laid out in locale folders. This is live today and pairs well with the CI/CD workflow.
// Ask for a Fastlane-ready archive alongside the images.
// Set output.format to fastlane_zip and read archive.url from the response.
const render = await fetch("https://api.appscreenshotapi.com/v1/renders", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.APPSCREENSHOTAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
preset: "aurora-midnight",
canvas: { preset: "appstore.iphone_6_9" },
output: { locales: ["en-US", "de-DE"], format: "fastlane_zip" },
slides: [
{ role: "hook", headline: { lines: ["Track every habit"] },
screenshot: { url: "https://acme.app/shots/home.png" } },
],
}),
}).then((r) => r.json());
// archive.url is a deliver-ready ZIP arranged in locale folders.
console.log(render.archive.url, `${render.archive.bytes} bytes`);The full request cascade (preset, theme, slides), every style enum, canvas presets, and the lint rules live in the API reference. For the flat list of every accepted value, see the options reference. The same endpoints power the Python quickstart and the CI/CD pipeline examples.