JavaScript SDKSoon
The JavaScript package is in development and cannot be installed yet. Until it ships, call the REST API with fetch, or copy the small client on this page.
An official JavaScript package is in development. It is not published: @infrared/sdk cannot be installed today, and nothing should depend on that name yet.
Until it ships, call the REST API directly. Every request in these docs is shown in Node.js with fetch, next to cURL and Python, and the API is regular enough that a short file of your own covers it.
A client you can copy
The file below is example code, not a package. Copy it into your project, read it and change it: from then on it is yours to maintain. It is an ES module for Node.js 18 or later and has no dependencies.
It does four things:
requestsends one request with your key and returns the parsed body.- A refused request throws an
InfraredErrorthat carries the error'scode, itsstatusand itsrequest_id. listreads a list page after page.constructEventverifies a webhook signature.
import crypto from "node:crypto";
export class InfraredError extends Error {
constructor(response, body) {
const error = body?.error ?? {};
super(error.message ?? `Infrared answered ${response.status}`);
this.name = "InfraredError";
this.status = response.status; // 401, 404, 422, 429, 500
this.code = error.code ?? null; // "rate_limited", "validation_error"
this.details = error.details ?? []; // [{ field, message }]
this.request_id = error.request_id ?? response.headers.get("x-request-id");
this.retry_after = Number(response.headers.get("retry-after")) || null; // seconds, on a 429
}
}
export function createClient(key = process.env.INFRARED_KEY, { baseUrl = "https://api.useinfrared.com" } = {}) {
// One request. Returns the parsed body, or throws an InfraredError.
async function request(method, path, { query, body, headers } = {}) {
const url = new URL(path, baseUrl);
for (const [name, value] of Object.entries(query ?? {})) {
if (value !== undefined && value !== null) url.searchParams.set(name, value);
}
const response = await fetch(url, {
method,
headers: { Authorization: `Bearer ${key}`, ...(body === undefined ? {} : { "Content-Type": "application/json" }), ...headers },
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
// Not JSON: something between you and the API answered.
}
if (!response.ok) throw new InfraredError(response, data);
return data;
}
// Every item of a list, page after page.
async function* list(path, query = {}) {
let cursor;
do {
const page = await request("GET", path, { query: { ...query, cursor } });
yield* page.data;
cursor = page.has_more ? page.next_cursor : undefined;
} while (cursor);
}
return { request, list };
}
export class SignatureError extends Error {}
// rawBody: the request body as received, a Buffer or a string, before any JSON parsing
// header: the value of the Infrared-Signature header
// secret: the endpoint's signing secret, or an array of two while you replace an endpoint
export function constructEvent(rawBody, header, secret, toleranceSeconds = 300) {
const parts = {};
for (const item of String(header ?? "").split(",")) {
const at = item.indexOf("=");
if (at > 0) parts[item.slice(0, at).trim()] = item.slice(at + 1).trim();
}
if (!/^[0-9]{1,15}$/.test(parts.t ?? "") || !parts.v1) throw new SignatureError("malformed signature header");
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
if (age > toleranceSeconds) throw new SignatureError("timestamp outside the tolerance");
const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody), "utf8");
const received = Buffer.from(parts.v1, "utf8");
let valid = false;
for (const candidate of [secret].flat()) {
const expected = Buffer.from(crypto.createHmac("sha256", candidate).update(`${parts.t}.`).update(body).digest("hex"), "utf8");
// timingSafeEqual throws when the lengths differ, so compare them first.
if (received.length === expected.length && crypto.timingSafeEqual(received, expected)) valid = true;
}
if (!valid) throw new SignatureError("signature mismatch");
return JSON.parse(body.toString("utf8"));
}Send a request
request takes the method, the path and, when the endpoint has them, a query, a body and extra headers. This call screens a message and passes an Idempotency-Key, so it is safe to run again after a timeout.
import { createClient } from "./infrared.js";
const infrared = createClient(); // reads INFRARED_KEY from the environment
const moderation = await infrared.request("POST", "/v1/moderations", {
body: { type: "text", input: "Is this still available? I can collect on Saturday.", content_id: "message_7702" },
headers: { "Idempotency-Key": "message-7702-v2" },
});
console.log(moderation.id, moderation.recommendation);mod_4iiStmy545F7ONwOybqtsD allowHandle an error
A refused request throws. Branch on code, and keep request_id in your logs: it is what support asks for.
import { createClient, InfraredError } from "./infrared.js";
const infrared = createClient();
try {
await infrared.request("GET", "/v1/moderations/mod_1B7lY4kzM9n2Qx8sRt3wVu");
} catch (error) {
if (!(error instanceof InfraredError)) throw error; // no answer at all: the network failed
console.error(error.status, error.code, error.request_id);
if (error.code === "rate_limited") console.error(`retry in ${error.retry_after} seconds`);
}404 not_found req_8cb281489b904ef89f9564f04661e5e6An error that is not an InfraredError means no answer arrived. For a moderation, send the request again with the same Idempotency-Key.
Read a whole list
list follows the cursor for you and yields one item at a time.
import { createClient } from "./infrared.js";
const infrared = createClient();
for await (const event of infrared.list("/v1/webhooks/events", { type: "moderation.completed", limit: 100 })) {
console.log(event.id, event.data.moderation.recommendation);
}Verify a webhook
constructEvent is the function from Verify signatures, unchanged. The receiver on the Webhooks page works with this file as it is: import constructEvent and SignatureError from ./infrared.js.
Other languages
Nothing here is specific to JavaScript. In Python the same client is a few lines around requests, and every request in these docs has a Python tab to start from.
Next
- QuickstartCreate an API key, send one request and read the verdict. It works on the Free plan, in a project where nothing is configured yet.
- API referenceThe Infrared REST API in one page. The base URL, how it is versioned, what a request and a response look like, how ids and request ids work.
- Verify signaturesEvery delivery is signed with the endpoint's secret. Check the signature and its timestamp before you trust the body, with code you can copy.
Something missing or unclear on this page? Write to support@useinfrared.com.