Halyard delivers events to your endpoint with exponential backoff, signed payloads and a replay log you can actually query. One dependency, no queue to run.
You POST an event. We deliver it, retry it on failure with exponential backoff for up to 72 hours, sign every attempt, and keep a replayable log. If your endpoint was down for an afternoon, nothing is lost.
One package, no peer dependencies. Node 20+, Deno and Bun are all supported by the same build.
npm install @halyard/sdkCreate a client, register a destination, publish. The destination is created once and reused — publishing to an unknown destination is an error rather than an implicit create, so a typo cannot silently open a new stream.
import { Halyard } from '@halyard/sdk';
const halyard = new Halyard({ apiKey: process.env.HALYARD_KEY! });
// Destinations are created once, then referenced by id.
const destination = await halyard.destinations.create({
url: 'https://api.example.com/webhooks/halyard',
events: ['invoice.paid', 'invoice.voided'],
});
await halyard.publish({
destination: destination.id,
type: 'invoice.paid',
data: { invoiceId: 'in_4f2a', amountMinor: 24_900, currency: 'NOK' },
});$ node send-event.ts halyard destination created dst_8Kq2Rm halyard published evt_01HXQ… → dst_8Kq2Rm halyard delivered 204 in 118ms (attempt 1/14) Done in 0.94s
A delivery is one attempt at one destination. Any 2xx is success; anything else is retried on the schedule below until the window closes.
| Attempt | Delay | Elapsed |
|---|---|---|
| 1 | immediate | 0s |
| 2 | 5s | 5s |
| 3 | 30s | 35s |
| 4 | 2m | 2m 35s |
| 5–8 | ×4 each | ~2h |
| 9–14 | ×2, capped at 6h | 72h |
Every attempt carries a `Halyard-Signature` header. Verify it against the raw request body — not the parsed JSON, whose key order your framework is free to change.
import { verify } from '@halyard/sdk/webhook';
export async function POST(request: Request) {
const raw = await request.text(); // raw body, not request.json()
const signature = request.headers.get('Halyard-Signature');
if (!verify(raw, signature, process.env.HALYARD_SIGNING_SECRET!)) {
return new Response('bad signature', { status: 401 });
}
const event = JSON.parse(raw);
// ... handle it, then return any 2xx within 10 seconds.
return new Response(null, { status: 204 });
}Publishes one event to one destination. Idempotent on `idempotencyKey` for 24 hours.
| Parameter | Type | Default | Description |
|---|---|---|---|
destinationrequired | string | — | Destination id. Publishing to an unknown id is a 404, never an implicit create. |
typerequired | string | — | Event type. Must be one the destination subscribes to, or the call is a 422. |
datarequired | object | — | Your payload. Serialised verbatim; the signature covers exactly these bytes. |
idempotencyKey | string | auto | Replaying the same key within 24h returns the original event instead of a duplicate. |
notBefore | string (ISO 8601) | now | Hold the event until this instant. Useful for scheduled reminders. |
maxAttempts | number | 14 | Lower the ceiling for events that stop being useful. Cannot be raised above 14. |
Every error carries a stable `code`. Match on that, never on the message — messages are written for humans and we improve them.
Status, source and a real inbox. There is no chatbot.