TypeScript reference
Drop-in TypeScript module built on node:crypto and global fetch. No dependencies. Tested against Node 20.
import crypto from 'node:crypto';
const BASE = 'https://api.flowbeacon.ai/api/public/v1';
const API_PATH_PREFIX = '/api/public/v1';
const API_KEY = process.env.FLOWBEACON_API_KEY!;
const ORG_ID = process.env.FLOWBEACON_ORG_ID!;
function signRequest(method: string, path: string, rawBody: string): string {
const timestamp = Math.floor(Date.now() / 1000);
const message = `${timestamp}.${method.toUpperCase()}.${path}.${rawBody}`;
const signature = crypto
.createHmac('sha256', API_KEY)
.update(message)
.digest('hex');
return `t=${timestamp},v1=${signature}`;
}
async function callFB<T = unknown>(
path: string,
init: RequestInit & {body?: string} = {},
): Promise<T> {
const method = (init.method ?? 'GET').toUpperCase();
const rawBody = init.body ?? '';
const res = await fetch(`${BASE}${path}`, {
...init,
method,
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
'X-FB-Signature': signRequest(method, `${API_PATH_PREFIX}${path}`, rawBody),
...(init.headers ?? {}),
},
});
const json = (await res.json()) as {ok: boolean; data?: T; error?: string; detail?: string};
if (!res.ok || json.ok === false) {
throw new Error(json.error ?? json.detail ?? `HTTP ${res.status}`);
}
return (json.data ?? (json as unknown)) as T;
}
export async function submitAndWait(scenarioIds: string[]) {
type Submit = {evaluation_id: string};
type Status = {status: 'pending' | 'processing' | 'complete' | 'error'};
const maxAttempts = 120;
const submit = await callFB<Submit>('/evaluate', {
method: 'POST',
body: JSON.stringify({scenario_ids: scenarioIds, org_id: ORG_ID}),
});
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const data = await callFB<Status>(`/evaluations/${submit.evaluation_id}`);
if (data.status === 'complete' || data.status === 'error') return data;
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error(`Timed out waiting for evaluation ${submit.evaluation_id}`);
}
Usage
const result = await submitAndWait(['4729318']);
console.log(result);
Notes
- Reuse the same
bodystring for signing and for the request body. Do not re-stringify. - The signer is intentionally stateless — re-sign for every retry.
- Adapt
Errorto your error class. Production code should attachrequest_idto errors when present. - For long-running workers, prefer webhooks over the
submitAndWaitpolling loop.