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.
On this page
Install
Add the package to your project:
npm install @halyard/nodenpm install @halyard/node# orpnpm add @halyard/node# oryarn add @halyard/nodeRequirements:
- 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:
HALYARD_KEY=hy_test_51Hq8Zx2rTmV9cKpL3dHALYARD_WEBHOOK_SECRET=whsec_7mYb2Qe9LkT4Configure 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.
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"timeoutnumberdefault: 30000maxRetriesnumberdefault: 2baseUrlstringfetchtypeof fetchCreate 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.
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.
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.
const event = await halyard.events.publish({ type: "order.paid", data: { order_id: "ord_77Hc1", amount: 4200, currency: "eur" },}); console.log(event.id); // evt_9Vb2Lm4Qa1Verify 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.
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.
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;}| Error | Raised when |
|---|---|
| HalyardError | Base class. Every error below extends it and carries type, code, status and requestId. |
| InvalidRequestError | 400 or 422. A parameter is missing or invalid; param names it. |
| AuthenticationError | 401. The key is missing, revoked or for the wrong environment. |
| PermissionDeniedError | 403. The key's scopes don't allow this call. |
| NotFoundError | 404. The ID doesn't exist or belongs to another project. |
| ConflictError | 409. Includes idempotency_mismatch when a key is reused with a different body. |
| RateLimitError | 429. retryAfter holds the seconds to wait. |
| APIConnectionError | The 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.
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.
"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 }, });}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.
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.
Other SDKs