Skip to content

SDKs

Node.js SDK

The Node.js SDK wraps every Halyard endpoint with promise-based methods, runs workers in the same process as your app or on their own, and ships full TypeScript types.

@halyard/nodev3.4.2 Source Changelog
On this page

Install

Add the package to your project:

npm install @halyard/node
Other package managers
npm install @halyard/node# orpnpm add @halyard/node# oryarn add @halyard/node

Requirements:

  • Node.js 20 or newer (Bun 1.2+ and Deno 2 also work)
  • TypeScript 5.4+ if you use the typed task registry

Keep your API key and webhook secret out of source control. The examples on this page read them from the environment:

.env
HALYARD_KEY=hy_test_51Hq8Zx2rTmV9cKpL3dHALYARD_WEBHOOK_SECRET=whsec_7mYb2Qe9LkT4

Configure the client

Create one client and share it across your app. It holds a connection pool, so making a new client per request wastes connections.

lib/halyard.js
import { Halyard } from "@halyard/node"; export const halyard = new Halyard(process.env.HALYARD_KEY, {  region: "eu",        // keep data in Frankfurt  timeout: 10_000,     // per request, in milliseconds  maxRetries: 3,       // network errors, 429 and 5xx});

Client options

region"us" | "eu" | "ap"default: "us"
Region your account lives in. Requests go to that region's API host.
timeoutnumberdefault: 30000
Milliseconds before a single request is aborted. Retries get a fresh timeout.
maxRetriesnumberdefault: 2
Automatic retries for connection errors, 429 and 5xx responses, with exponential backoff.
baseUrlstring
Override the API host, for example to point at a local mock server in tests.
fetchtypeof fetch
Custom fetch implementation, useful for proxies or instrumentation.

Create a job

A job needs a queue, a task name and a JSON payload. It runs as soon as a worker is free, or at run_at if you set one.

JavaScript
import { halyard } from "./lib/halyard.js"; const job = await halyard.jobs.create({  queue: "emails",  task: "send-welcome-email",  payload: { user_id: "usr_8KfQ2" },  runAt: new Date(Date.now() + 10 * 60_000), // optional: in 10 minutes  maxAttempts: 5,}); console.log(job.id, job.status); // job_3Rk9Tq7Xw2 "scheduled"

Request fields keep their API names in the payload, but options use camelCase: runAt, maxAttempts, idempotencyKey. Every create call sends an idempotency key automatically, so a retried request never makes a duplicate job.

Run a worker

Workers long-poll Halyard for jobs on the queues you name and call the handler registered for each task. Run as many worker processes as you like; Halyard hands each job to exactly one of them at a time.

worker.js
import { Worker } from "@halyard/node";import { db, mailer } from "./services.js"; const worker = new Worker(process.env.HALYARD_KEY, { queues: ["emails"], concurrency: 10 }); worker.task("send-welcome-email", async (job) => {  const user = await db.users.find(job.payload.user_id);  await mailer.send({ to: user.email, template: "welcome" });  job.log(`Sent welcome email to ${user.email}`);}); process.on("SIGTERM", () => worker.stop()); // finish running jobs, then exitawait worker.start();

Return normally and the attempt succeeds. Throw and Halyard records the error and schedules a retry with backoff. Throw a NonRetryableError to skip the remaining attempts and send the job straight to the dead-letter queue.

Publish an event

Events describe something that happened. Halyard fans each one out to every subscription that matches its type, whether that subscription starts a job or calls a webhook. Publishing is fire-and-forget: you get an evt_ ID back in a few milliseconds.

JavaScript
const event = await halyard.events.publish({  type: "order.paid",  data: { order_id: "ord_77Hc1", amount: 4200, currency: "eur" },}); console.log(event.id); // evt_9Vb2Lm4Qa1

Verify webhooks

Halyard signs every webhook delivery with your endpoint's secret. Verify the signature before you trust the body; the SDK does the HMAC check, the timestamp check and the JSON parsing in one call.

routes/webhooks.js
import express from "express";import { halyard } from "../lib/halyard.js"; export const router = express.Router(); router.post("/webhooks/halyard", express.raw({ type: "application/json" }), (req, res) => {  let event;  try {    event = halyard.webhooks.verify(      req.body,                              // raw Buffer, not parsed JSON      req.get("Halyard-Signature"),      process.env.HALYARD_WEBHOOK_SECRET,    );  } catch {    return res.status(400).send("Invalid signature");  }   if (event.type === "job.failed") {    console.warn("Job failed", event.data.job.id);  }  res.sendStatus(204);});

verify() checks the HMAC SHA-256 signature and rejects timestamps older than five minutes. Pass { tolerance: 600 } as a fourth argument to change the window.

Handle errors

Failed API calls raise a typed error with the API's type, code and the request_id to quote to support. Retryable failures have already been retried by the time you see them.

JavaScript
import { HalyardError, InvalidRequestError, RateLimitError } from "@halyard/node"; try {  await halyard.jobs.create({ queue: "emails", task: "send-welcome-email", payload });} catch (err) {  if (err instanceof RateLimitError) {    console.warn(`Slow down, retry in ${err.retryAfter}s`);  } else if (err instanceof InvalidRequestError) {    console.error(`Bad parameter ${err.param}: ${err.message}`);  } else if (err instanceof HalyardError) {    console.error(err.type, err.code, err.requestId); // req_4Tn8Wc2Ke7  }  throw err;}
Node.js SDK error classes
ErrorRaised when
HalyardErrorBase class. Every error below extends it and carries type, code, status and requestId.
InvalidRequestError400 or 422. A parameter is missing or invalid; param names it.
AuthenticationError401. The key is missing, revoked or for the wrong environment.
PermissionDeniedError403. The key's scopes don't allow this call.
NotFoundError404. The ID doesn't exist or belongs to another project.
ConflictError409. Includes idempotency_mismatch when a key is reused with a different body.
RateLimitError429. retryAfter holds the seconds to wait.
APIConnectionErrorThe request never got a response: DNS, TLS or timeout.

The full list of types and codes is on the Errors page.

Typed payloads

Payloads are JSON on the wire, but they don't have to be untyped in your code. Describe each task's payload once and the SDK checks both ends: the code that enqueues and the handler that runs.

halyard.d.ts
import "@halyard/node"; declare module "@halyard/node" {  interface Tasks {    "send-welcome-email": { user_id: string; locale?: "en" | "de" };    "resize-image": { url: string; widths: number[] };  }} // Elsewhere: payloads are now checked against the task name.await halyard.jobs.create({  queue: "emails",  task: "send-welcome-email",  payload: { user_id: "usr_8KfQ2", locale: "fr" }, // Type error: "fr"}); worker.task("resize-image", async (job) => {  job.payload.widths; // number[]});

Declare your tasks once by augmenting the Tasks interface. jobs.create and worker.handle pick the payload type from the task name, so a typo in either is a compile error. Run npx halyard types pull to generate the file from the schemas stored on your queues.

Framework integrations

The SDK works anywhere your language runs. These are the patterns we recommend for the frameworks we see most.

Next.js

Enqueue from Server Actions or Route Handlers, and verify webhooks in a Route Handler. Read the body with request.text() so the signature is checked against the exact bytes Halyard sent.

app/actions.ts
"use server"; import { halyard } from "@/lib/halyard"; export async function signUp(formData: FormData) {  const user = await createUser(formData);  await halyard.jobs.create({    queue: "emails",    task: "send-welcome-email",    payload: { user_id: user.id },  });}
app/api/webhooks/halyard/route.ts
import { halyard } from "@/lib/halyard"; export async function POST(request: Request) {  const body = await request.text();  const event = halyard.webhooks.verify(    body,    request.headers.get("Halyard-Signature") ?? "",    process.env.HALYARD_WEBHOOK_SECRET!,  );  await handle(event);  return new Response(null, { status: 204 });}

Express

Mount express.raw() on the webhook route only, so the rest of your app keeps parsing JSON as usual. The full route is in the webhook section above.

server.js
import express from "express";import { router as halyardWebhooks } from "./routes/webhooks.js"; const app = express(); app.use(halyardWebhooks);   // raw body for /webhooks/halyardapp.use(express.json());    // JSON for everything else app.listen(3000);

Reference

Every method on the client maps to an endpoint in the API reference. Source, issues and release notes for the Node.js SDK live at code.halyard.example/sdks/halyard-node.

Suggested