Integration guides
Node.js and TypeScript
A small moderation module for Node.js 18 or later, with timeouts and a decision that fails closed.
This guide uses the fetch that ships with Node.js 18 and later. It has no dependency.
Set the key
Put the key in the environment of your server, never in the code:
OPENMOD_API_KEY=om_live_...
The module
moderation.ts
const base = "https://api.openshield.dev"
export type Action = "accept" | "review" | "reject"
export interface Reason {
id: string
text: string
message: string
score: number
threshold: number
source: string
}
export interface Decision {
action: Action
rejectReasons: Reason[]
reviewReasons: Reason[]
requestId: string
}
export class ModerationError extends Error {
constructor(
readonly status: number,
readonly type: string,
message: string,
readonly retryAfter?: number,
) {
super(message)
}
}
async function send(path: string, init: RequestInit): Promise<Decision> {
const key = process.env.OPENMOD_API_KEY
if (!key) throw new Error("OPENMOD_API_KEY is not set")
const response = await fetch(`${base}${path}`, {
...init,
headers: { ...init.headers, Authorization: `Bearer ${key}` },
method: "POST",
signal: AbortSignal.timeout(10_000),
})
const body = await response.json()
if (!response.ok) {
const retryAfter = Number(response.headers.get("retry-after")) || undefined
throw new ModerationError(response.status, body?.error?.type ?? "unknown", body?.error?.message ?? response.statusText, retryAfter)
}
return {
action: body.summary.action,
rejectReasons: body.summary.reject_reason,
reviewReasons: body.summary.review_reason,
requestId: body.request_id,
}
}
// `workflow` names a workflow of yours: `default` when it is left out.
export function checkText(text: string, ref?: string, workflow?: string): Promise<Decision> {
return send("/v1/text", {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, ref, workflow }),
})
}
export function checkImage(bytes: Uint8Array, ref?: string, workflow?: string): Promise<Decision> {
const query = new URLSearchParams()
if (ref) query.set("ref", ref)
if (workflow) query.set("workflow", workflow)
const suffix = query.toString()
return send(`/v1/image${suffix ? `?${suffix}` : ""}`, { body: bytes })
}
Use it, and fail closed
Decide what happens to the content for each outcome, and what happens when the check itself fails.
import { checkText, ModerationError } from "./moderation"
export async function moderateComment(text: string, commentId: string): Promise<"publish" | "hold" | "refuse"> {
try {
const decision = await checkText(text, commentId)
if (decision.action === "accept") return "publish"
if (decision.action === "review") return "hold"
return "refuse"
} catch (error) {
// The content cannot be accepted as it is: refuse it.
if (error instanceof ModerationError && [400, 413, 415].includes(error.status)) return "refuse"
// Anything else, a timeout included: keep it hidden until someone looks at it.
return "hold"
}
}
For rejected content, show your user a message chosen from decision.rejectReasons[0].id, in your own words and your own languages. Best practices says more.