> Markdown copy of https://makernice.com/docs/node/ for AI tools. The HTML page is the canonical version.

# Use the Makernice API with Node.js

Call Makernice from Node.js 18 or later with the built-in `fetch`, no packages: quote the exact price, create the generation, poll until it finishes, then download the file. Save the example as `quickstart.mjs` and run it with `node`.

[Makernice Team](https://makernice.com/about/) · Updated November 11, 2026

## Before you start

-   Create an API key in the [console](https://makernice.com/console/keys/) and export it as `MAKERNICE_API_KEY`.
-   Node.js 18 or later. Nothing to install.

## Complete example

quickstart.mjs

```
// Node.js 18+ (built-in fetch, no packages). Save as quickstart.mjs and run: node quickstart.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";

const API = "https://api.makernice.com/v1";
const ORIGIN = "https://api.makernice.com";
const HEADERS = { Authorization: `Bearer ${process.env.MAKERNICE_API_KEY}` };
const TIMEOUT_MS = 30_000;
const POLL_INTERVAL_MS = 5_000;
const MAX_WAIT_MS = 30 * 60 * 1000;
const FINAL = new Set(["succeeded", "failed", "canceled"]);

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// micro-USD integer string -> dollars, no float rounding
const usd = (micro) => `${BigInt(micro) / 1_000_000n}.${(BigInt(micro) % 1_000_000n).toString().padStart(6, "0")}`;

class Stop extends Error {}

async function call(method, path, { body, headers } = {}) {
  // Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; throws on any other error.
  for (let attempt = 0; attempt < 5; attempt++) {
    const r = await fetch(API + path, { signal: AbortSignal.timeout(TIMEOUT_MS),
      method,
      headers: { ...HEADERS, ...(body ? { "Content-Type": "application/json" } : {}), ...headers },
      body: body ? JSON.stringify(body) : undefined,
    });
    if (r.status === 429) {
      await sleep(Number(r.headers.get("Retry-After") ?? "5") * 1000);
      continue;
    }
    if (!r.ok) throw new Stop(`${method} ${path} -> ${r.status}: ${await r.text()}`);
    return r.json();
  }
  throw new Stop(`${method} ${path}: still rate limited`);
}

async function main() {
  const job = {
    "model": "seedance-2.5",
    "mode": "t2v",
    "prompt": "A slow dolly shot through a greenhouse",
    "duration": 5,
    "resolution": "720p",
  };

  // 1. Quote: the exact amount, locked for 12 minutes
  const quote = await call("POST", "/generations/quote", { body: job });
  console.log(`Quoted $${usd(quote.amount)} (price list v${quote.price_version}), valid until ${quote.expires_at}`);

  // 2. Create: same input + quote_id. A retry with the same Idempotency-Key returns the same generation
  const idempotencyKey = randomUUID();
  let gen = await call("POST", "/generations", { body: { quote_id: quote.id, input: job }, headers: { "Idempotency-Key": idempotencyKey } });

  // 3. Poll: queued -> generating -> moderating -> succeeded | failed | canceled (checking: keep polling)
  const deadline = Date.now() + MAX_WAIT_MS;
  while (!FINAL.has(gen.status)) {
    if (Date.now() > deadline) throw new Stop(`${gen.id} is still ${gen.status}; poll it again later`);
    await sleep(POLL_INTERVAL_MS);
    gen = await call("GET", `/generations/${gen.id}`);
  }

  if (gen.status !== "succeeded") {
    // billing is "released": the hold went back to your balance
    throw new Stop(`${gen.status}, billing ${gen.billing}: ${JSON.stringify(gen.error)}`);
  }

  // 4. Download: outputs[].url is relative to the API origin and answers 302 with a signed file URL.
  //    Follow it yourself so your API key is never sent to the file host.
  const [output] = gen.outputs;
  if (output.state === "blocked") throw new Stop("Output blocked by moderation (the run is charged); contact support for a manual review");
  const hop = await fetch(ORIGIN + output.url, { headers: HEADERS, redirect: "manual", signal: AbortSignal.timeout(TIMEOUT_MS) });
  const location = hop.headers.get("Location");
  if (hop.status < 300 || hop.status > 399 || !location) throw new Stop(`GET ${output.url} -> ${hop.status}`);
  const file = await fetch(new URL(location, ORIGIN + output.url), { signal: AbortSignal.timeout(120_000) });
  if (!file.ok) throw new Stop(`download -> ${file.status}`);
  await writeFile("output.mp4", Buffer.from(await file.arrayBuffer()));
  console.log(`Saved output.mp4, charged $${usd(gen.amount)}`);
}

main().catch((err) => {
  console.error(err instanceof Stop ? err.message : err);
  process.exit(1);
});
```

## What the code does

1.  **Quote**: `POST /v1/generations/quote` returns the exact amount as an integer string in micro-USD, locked for 12 minutes. Nothing is charged yet.
2.  **Create**: `POST /v1/generations` with the `quote_id`, the same input and an `Idempotency-Key` header. Retrying with the same key returns the same generation instead of a second one.
3.  **Wait**: poll `GET /v1/generations/{id}` until it is `succeeded`, `failed` or `canceled`, or pass a `callback_url` and we POST the result to your server.
4.  **Download**: each `outputs[].url` answers with a 302 to a signed file URL.

## Node.js notes

-   Amounts are integer strings in micro-USD. The example converts them with `BigInt`, so nothing is rounded.
-   Every request has a timeout through `AbortSignal.timeout`, and 429 responses are retried after `Retry-After`.
-   The download uses `redirect: "manual"` and follows the 302 itself, so your API key is never sent to the file host. Node versions differ on whether `fetch` strips `Authorization` on a cross-host redirect.

## FAQ

### Is there an official SDK?

No. The REST API is four calls and the code on this page is a complete client. If you want a generated client, the OpenAPI description is at [/openapi.json](https://makernice.com/openapi.json).

### Can I call the API from the browser?

Don't: the API key would be visible to every visitor. Call it from your server or a serverless function. The [Next.js guide](https://makernice.com/docs/nextjs/) shows a route that keeps the key on the server.

### Does this work with TypeScript?

Yes. The same calls with types are in the [Next.js guide](https://makernice.com/docs/nextjs/); both routes there compile under `strict`.

### Do I pay if a generation fails?

No. The amount is held when you create the generation and released in full if it fails or is canceled. An output blocked by content moderation after the run is still charged. Details: [No charge for failed generations](https://makernice.com/pricing/failed-generations/).

## Related

-   [API docs: Quickstart](https://makernice.com/docs/#quickstart)
-   [API docs: Webhooks](https://makernice.com/docs/#webhooks)
-   [API docs: Errors](https://makernice.com/docs/#errors)
-   [Use the Makernice API with Python](https://makernice.com/docs/python/)
-   [Use the Makernice API with Next.js](https://makernice.com/docs/nextjs/)
-   [Use the Makernice API with FastAPI](https://makernice.com/docs/fastapi/)
-   [Use it from Claude Code, Cursor and other AI tools (MCP)](https://makernice.com/mcp/)
