SellubSDKssellub-webhooks

@duabalabs/sellub-webhooks

HMAC-SHA256 helpers for signing outbound webhooks (the Sellub server uses this) and verifying inbound ones (your integration uses this). Tiny, no dependencies beyond Node’s built-in crypto.

npm

Install

pnpm add @duabalabs/sellub-webhooks

Verify an inbound Sellub webhook

Sellub posts events with header X-Sellub-Signature: <hex hmac>. Verify before doing any work:

import { verifySignature } from "@duabalabs/sellub-webhooks";
 
export async function POST(req: Request) {
  const raw = await req.text();
  const signature = req.headers.get("x-sellub-signature") ?? "";
 
  const ok = verifySignature({
    secret: process.env.SELLUB_WEBHOOK_SECRET!,
    payload: raw,
    signature,
  });
 
  if (!ok) return new Response("invalid signature", { status: 401 });
 
  const event = JSON.parse(raw);
  // … handle event.type
  return Response.json({ ok: true });
}

Always read the body as a raw string before parsing it. JSON serialisation is not byte-stable across runtimes; verifying against a re-stringified body breaks intermittently.

Send a signed webhook

If you need to fire your own signed webhooks (e.g. between two of your own services using the same primitives Sellub uses):

import { dispatchWebhook } from "@duabalabs/sellub-webhooks";
 
await dispatchWebhook(
  {
    url: "https://my-service.example/sellub-events",
    secret: process.env.MY_WEBHOOK_SECRET!,
  },
  { type: "order.placed", orderId: "abc" }
);

API

signPayload({ secret, payload }): string
 
verifySignature({ secret, payload, signature }): boolean
 
// New in 0.2 — verify + parse in one step, get a typed event back.
verifyAndParse({ secret, payload, signature }): ApiResult<WebhookEvent>
 
dispatchWebhook(
  { url, secret, headers?, fetch? },
  body: unknown
): Promise<{ ok, status, text }>
 
// Re-exported from @duabalabs/sellub-types
isWebhookEvent(event, type): event is Extract<WebhookEvent, { type: T }>

verifyAndParse example

import { verifyAndParse } from "@duabalabs/sellub-webhooks";
 
export async function POST(req: Request) {
  const raw = await req.text();
  const r = verifyAndParse({
    secret: process.env.SELLUB_WEBHOOK_SECRET!,
    payload: raw,
    signature: req.headers.get("x-sellub-signature") ?? "",
  });
  if (!r.success) return new Response(r.error?.message ?? "invalid", { status: 401 });
 
  switch (r.data!.type) {
    case "payment.succeeded":
      await markOrderPaid(r.data.data.payment.reference);
      break;
    case "order.refunded":
      await issueRefund(r.data.data.order.id, r.data.data.refundedAmount);
      break;
    case "fulfillment.shipped":
      await notifyCustomer(r.data.data.order.id, r.data.data.fulfillment.trackingUrl);
      break;
  }
  return Response.json({ ok: true });
}
  • verifySignature uses timingSafeEqual so it’s safe against timing attacks.
  • dispatchWebhook defaults to globalThis.fetch. In environments without one (older Node), pass { fetch: undici.fetch }.

Wiring on the Sellub side

When you add a webhook endpoint in the Sellub dashboard you’ll get a secret shown once. Store it in your environment as SELLUB_WEBHOOK_SECRET and use it in the snippet above. Rotate via rotateWebhookSecret (Admin GraphQL) if it leaks.