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 typeUpload 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 fieldThe 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).