Skip to main content

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 body string for signing and for the request body. Do not re-stringify.
  • The signer is intentionally stateless — re-sign for every retry.
  • Adapt Error to your error class. Production code should attach request_id to errors when present.
  • For long-running workers, prefer webhooks over the submitAndWait polling loop.