For AI agents: this page is also available as Markdown at https://docs.refabric.com/api-reference/client-libraries/javascript-over-http.md, and the index of every page is https://docs.refabric.com/llms.txt.

Client libraries

JavaScript over HTTP

Call Refabric from Node.js with the built-in fetch: submit a task, wait for it, read its files, upload and page through lists.

Node.js 18 or later has fetch, FormData and Blob built in, so this client needs no package. Run it on your server; keep the key out of the browser (Proxy setup).

The client

const API = "https://api.refabric.com/v1";
const KEY = process.env.REFABRIC_API_KEY;

export class RefabricError extends Error {
  constructor(status, error) {
    super(`${error.code}: ${error.message}`);
    Object.assign(this, { status, ...error }); // code, type, field, retryable, request_id, …
  }
}

export async function call(method, path, { body, headers = {} } = {}) {
  const isForm = body instanceof FormData;
  const r = await fetch(path.startsWith("https://") ? path : `${API}${path}`, {
    method,
    headers: {
      Authorization: `Key ${KEY}`,
      ...(body && !isForm ? { "Content-Type": "application/json" } : {}),
      ...headers,
    },
    body: body === undefined ? undefined : isForm ? body : JSON.stringify(body),
  });
  const data = await r.json().catch(() => ({}));
  // An answer without our error object (a proxy's page, say): retry it like a network error.
  if (!r.ok) throw new RefabricError(r.status, data.error ?? { code: `http_${r.status}`, message: r.statusText, retryable: [429, 500, 502, 503, 504].includes(r.status) });
  return { status: r.status, data, headers: r.headers };
}

const sleep = (ms) => new Promise((done) => setTimeout(done, ms));

/** Submit a task and return its result once the job has ended. */
export async function run(task, input, { idempotencyKey = crypto.randomUUID(), pollMs = 5000 } = {}) {
  const { data: handle } = await call("POST", `/tasks/${task}`, { body: input, headers: { "Idempotency-Key": idempotencyKey } });
  for (;;) {
    const { data: job } = await call("GET", handle.status_url);
    if (job.lifecycle === "terminal") {
      if (job.outcome !== "succeeded") throw new RefabricError(200, job.error);
      return (await call("GET", handle.result_url)).data;
    }
    await sleep(pollMs);
  }
}

/** Every item of a list, following `next_cursor` while `has_more` is true. */
export async function* all(path) {
  let cursor;
  do {
    const sep = path.includes("?") ? "&" : "?";
    const { data } = await call("GET", cursor ? `${path}${sep}cursor=${encodeURIComponent(cursor)}` : path);
    yield* data.items;
    cursor = data.has_more ? data.next_cursor : undefined;
  } while (cursor);
}

Run a task

import { run } from "./refabric.mjs";

const result = await run("image.change_background", {
  image: "https://example.com/look.jpg",
  background: { type: "prompt", prompt: "a sunlit stone terrace" },
});
for (const file of result.files) console.log(file.file, file.url);

result.files is the first page of the job's files; when result.has_more is true, read the rest with ?cursor= (Asynchronous jobs).

Estimate first

import { call } from "./refabric.mjs";

const { data: estimate } = await call("POST", "/tasks/image.generate/estimate", {
  body: { prompt: "a navy linen shirt dress", image_count: 4 },
});
console.log(estimate); // the most this request can cost, and its credit type

Upload a file

import { readFile } from "node:fs/promises";
import { call } from "./refabric.mjs";

const form = new FormData();
form.append("file", new Blob([await readFile("shirt.jpg")]), "shirt.jpg");
form.append("name", "Linen shirt");
const { data: upload } = await call("POST", "/files", { body: form });
console.log(upload.file); // file:… — pass it to any media field

The accepted file types and the size cap are limits.upload of GET /v1/meta (Upload).

Page through a list

import { all } from "./refabric.mjs";

for await (const job of all("/jobs?lifecycle=terminal&limit=50")) console.log(job.job_id, job.outcome);

Handle errors

import { run, RefabricError } from "./refabric.mjs";

try {
  await run("image.glam", { image: "art:x1", prompt: "make the lipstick red" });
} catch (e) {
  if (!(e instanceof RefabricError)) throw e;
  if (e.retryable) {
    // retry later with backoff — and, for a submit, the same Idempotency-Key
  } else {
    console.error(e.code, e.field, e.request_id); // fix the input named by `field`
  }
}

Branch on code, never on message (Task errors). On 429, wait Retry-After seconds before the next call (Limits).