Skip to main content

FlareClient

The main entry point for the @zeridion/flare TypeScript/JavaScript SDK. A thin, dependency-free wrapper around the Zeridion Flare REST API using native fetch.

Package: @zeridion/flare · Runtime: Node 20+, Bun, Deno, Cloudflare Workers, Vercel Edge, Fastly Compute@Edge JS, or any environment with native fetch and crypto.subtle (or node:crypto in Node).

Installation​

npm install @zeridion/flare

Creating a client​

import { FlareClient } from "@zeridion/flare";

const flare = new FlareClient(opts: FlareClientOptions);

FlareClientOptions​

OptionTypeRequiredDefaultDescription
apiKeystringyes—Your Zeridion API key (zf_live_sk_... or zf_test_sk_...).
baseUrlstringnohttps://api.zeridion.comOverride the API base URL (e.g. for local dev).
maxRetriesnumberno3Maximum retry attempts on 429 / 5xx / network errors. Set 0 to disable.
retryBaseDelayMsnumberno500Base delay for exponential backoff schedule.
retryMaxDelayMsnumberno30000Upper bound on a single backoff wait; also caps server-supplied Retry-After hints.
maxResponseBytesnumberno10 485 760 (10 MiB)Hard cap on response body size. Protects against an unbounded body from a misbehaving server. Set 0 to disable.

RequestOptions​

All methods accept an optional RequestOptions argument:

interface RequestOptions {
idempotencyKey?: string; // sent as Idempotency-Key header — used by the server to dedupe a replayed write
requestId?: string; // sent as X-Request-Id header — used for distributed tracing and log correlation
signal?: AbortSignal; // for request cancellation
}
  • idempotencyKey — semantic deduplication on the server. A second createJob with the same idempotencyKey returns the original response (status code and body), not a fresh write. See idempotency guide.
  • requestId — pass through your application's trace / correlation ID so it shows up in Zeridion's server logs alongside your own. Omit it and the server generates a ULID and returns it in the response X-Request-Id header.

Job methods​

createJob(req, opts?)​

Enqueue a new background job. Returns the created job's summary.

createJob(req: CreateJobRequest, opts?: RequestOptions): Promise<CreateJobResponse>
const job = await flare.createJob({
job_type: "SendWelcomeEmail",
payload: { email: "alice@example.com", name: "Alice" },
queue: "default",
max_attempts: 3,
});

console.log(job.id, job.state); // "job_abc123", "pending"

See CreateJobRequest and CreateJobResponse for the full type shapes.


getJob(id, opts?)​

Retrieve a job by ID. Returns null if the job does not exist (404 — does not throw).

getJob(id: string, opts?: RequestOptions): Promise<JobDetail | null>
const detail = await flare.getJob("job_abc123");

if (detail) {
console.log(detail.state, detail.progress);
}

listJobs(opts?)​

List jobs with optional filters. Supports cursor-based pagination.

listJobs(opts?: ListJobsOptions & RequestOptions): Promise<ListJobsResponse>
const { data, next_cursor, has_more } = await flare.listJobs({
state: "failed",
queue: "critical",
limit: 25,
});

See ListJobsOptions for all filter parameters.


cancelJob(id, opts?)​

Cancel a pending or scheduled job. Returns null when the job is in a non-cancellable state (409 — does not throw). Throws NotFoundError when the job does not exist (404).

cancelJob(id: string, opts?: RequestOptions): Promise<CancelJobResponse | null>
const result = await flare.cancelJob("job_abc123");

if (result === null) {
console.log("Job is already processing or completed — cannot cancel");
}

retryJob(id, opts?)​

Retry a failed or dead-lettered job, resetting it to pending. Returns null when the job is not retryable (409 — does not throw). Throws NotFoundError when the job does not exist (404).

retryJob(id: string, opts?: RequestOptions): Promise<RetryJobResponse | null>
const result = await flare.retryJob("job_abc123");

if (result) {
console.log("Job reset to pending, attempt:", result.attempt);
}

Worker methods​

Use these to build a custom worker process that polls for jobs and acknowledges results.

Prefer the built-in worker runtime. For most workers you don't need the low-level poll/ack calls below — @zeridion/flare/worker gives you a complete worker host (register, poll, dispatch, heartbeat, progress, cancellation, graceful drain) with a small job-definition API. See the worker runtime reference. The methods below remain for bespoke loops.

pollWorker(req, opts?)​

Poll for available jobs on one or more queues. The server returns up to capacity jobs that are ready for processing.

pollWorker(req: PollRequest, opts?: RequestOptions): Promise<PollResponse>
const { jobs } = await flare.pollWorker({
worker_id: "worker-1",
queues: ["default", "critical"],
capacity: 5,
});

ackWorker(req, opts?)​

Acknowledge the result of a job execution. Call this after every job — success, failure, or retry request.

ackWorker(req: AckRequest, opts?: RequestOptions): Promise<AckResponse>
// Success
await flare.ackWorker({
job_id: job.id,
worker_id: "worker-1",
status: "succeeded",
duration_ms: 240,
});

// Failure
await flare.ackWorker({
job_id: job.id,
worker_id: "worker-1",
status: "failed",
error: { type: "Error", message: "Downstream timeout" },
});

registerWorker(req, opts?)​

Announce this worker — its queues, job types, and any recurring schedules — to the server. Registration is best-effort: a rejection is safe to ignore and your worker can keep polling.

registerWorker(req: RegisterWorkerRequest, opts?: RequestOptions): Promise<RegisterWorkerResponse>
await flare.registerWorker({
worker_id: "worker-1",
queues: ["default", "critical"],
job_types: ["SendWelcomeEmail", "GenerateReport"],
hostname: "ip-10-0-1-42",
recurring_schedules: [
{ job_type: "NightlyDigest", cron_expression: "0 2 * * *", timezone: "America/New_York" },
],
});

See RegisterWorkerRequest and RegisterWorkerResponse for the full type shapes.


heartbeat(req, opts?)​

Report liveness and optional progress (0–1) for a single in-flight job. The response status tells you whether to keep going ("ok") or stop: a "cancel" status means you should abort the job and acknowledge it.

heartbeat(req: HeartbeatRequest, opts?: RequestOptions): Promise<HeartbeatResponse>
const beat = await flare.heartbeat({
job_id: job.id,
worker_id: "worker-1",
progress: 0.5,
});

if (beat.status === "cancel") {
// Stop work, then ack with status "cancelled".
}

See HeartbeatRequest and HeartbeatResponse for the full type shapes.


Full worker loop example​

import { FlareClient, FlareError, RateLimitError } from "@zeridion/flare";

const flare = new FlareClient({ apiKey: process.env.FLARE_API_KEY! });

async function runWorker() {
while (true) {
const { jobs } = await flare.pollWorker({
worker_id: "worker-1",
queues: ["default"],
capacity: 5,
});

for (const job of jobs) {
const start = Date.now();
try {
await dispatch(job.job_type, job.payload);
await flare.ackWorker({
job_id: job.id,
worker_id: "worker-1",
status: "succeeded",
duration_ms: Date.now() - start,
});
} catch (err) {
await flare.ackWorker({
job_id: job.id,
worker_id: "worker-1",
status: "failed",
duration_ms: Date.now() - start,
error: { type: err instanceof Error ? err.name : "Error", message: String(err) },
});
}
}

if (jobs.length === 0) {
await new Promise(r => setTimeout(r, 1000));
}
}
}

async function dispatch(jobType: string, payload: unknown) {
// Map job_type strings to handler functions
const handlers: Record<string, (p: unknown) => Promise<void>> = {
SendWelcomeEmail: async (p) => { /* ... */ },
};
const handler = handlers[jobType];
if (!handler) throw new Error(`Unknown job type: ${jobType}`);
await handler(payload);
}

Outbound webhooks​

Verifying an incoming webhook (SDK helper)​

The SDK exports verifyWebhook, a Web Crypto-based verifier that works in every supported runtime (Node 20+, Bun, Deno, Cloudflare Workers, Vercel Edge, Fastly Compute@Edge).

The X-Zeridion-Signature header has the form t=<unix_timestamp>,v1=<lowercase_hex_digest> — the signature is HMAC-SHA256 over <unix_timestamp>.<raw_body>. verifyWebhook parses the header, recomputes the digest, and compares in constant time.

import { verifyWebhook } from "@zeridion/flare";

// Express example — note express.raw() so we receive the exact bytes the server signed.
app.post("/hooks/zeridion", express.raw({ type: "*/*" }), async (req, res) => {
const header = req.header("X-Zeridion-Signature") ?? "";
const ok = await verifyWebhook(req.body, header, process.env.WEBHOOK_SECRET!, {
toleranceSeconds: 300, // recommended: reject signatures older than 5 minutes
});
if (!ok) return res.status(400).send("invalid signature");

const event = JSON.parse(req.body.toString());
console.log("Received event:", event.event_type, event.data.job_id);
res.sendStatus(200);
});

Subscription CRUD (REST fallback)​

The TypeScript SDK does not yet expose webhook subscription management (create / list / update / delete). Use fetch directly against the REST API while SDK support is pending.

const API_KEY = process.env.FLARE_API_KEY!;
const BASE = "https://api.zeridion.com/flare/v1";

// Create
const created = await fetch(`${BASE}/webhooks`, {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://your-app.example.com/hooks/zeridion",
events: ["job.succeeded", "job.failed"],
}),
}).then(r => r.json());
// { id: "whk_...", url, events, secret: "whsec_...", created_at }
// Store created.secret — that's the value you pass to verifyWebhook().

// List
const list = await fetch(`${BASE}/webhooks`, {
headers: { Authorization: `Bearer ${API_KEY}` },
}).then(r => r.json());

// Update
await fetch(`${BASE}/webhooks/${created.id}`, {
method: "PATCH",
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ events: ["job.succeeded"] }),
});

// Delete
await fetch(`${BASE}/webhooks/${created.id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${API_KEY}` },
});

Audit log export (REST fallback)​

The TypeScript SDK does not yet expose an exportAuditLog method. Use fetch with streaming while SDK support is pending.

async function exportAuditLog(opts: {
projectId: string;
from: string; // ISO 8601, e.g. "2026-01-01T00:00:00Z"
until: string; // ISO 8601, e.g. "2026-04-01T00:00:00Z"
format?: "jsonl" | "csv";
outputPath: string;
jwt: string; // Audit-log export requires admin/owner JWT, not an API key
}) {
const { projectId, from, until, format = "jsonl", outputPath, jwt } = opts;
const params = new URLSearchParams({ from, until, format });

const response = await fetch(
`https://api.zeridion.com/flare/v1/projects/${projectId}/audit-log/export?${params}`,
{ headers: { Authorization: `Bearer ${jwt}` } },
);

if (!response.ok) {
const err = await response.json();
throw new Error(`Export failed: ${err.code} — ${err.message}`);
}

// Stream to disk (Node 18+ ReadableStream → fs.WriteStream)
const { createWriteStream } = await import("node:fs");
const { Readable } = await import("node:stream");
const out = createWriteStream(outputPath);
await new Promise<void>((resolve, reject) => {
Readable.fromWeb(response.body as any).pipe(out)
.on("finish", resolve)
.on("error", reject);
});

console.log(`Audit log saved to ${outputPath}`);
}

// Usage
await exportAuditLog({
from: "2026-01-01T00:00:00Z",
until: "2026-04-01T00:00:00Z",
format: "jsonl",
outputPath: "./audit-2026-q1.jsonl",
});

See also​