Skip to main content
Polling for state changes works, but it’s slow and wasteful. Webhooks invert the relationship: when something interesting happens (a subscription is confirmed, a contract activates, a customer cancels), Nomos sends a signed POST request to a URL you control, with the event payload. This guide goes from “I have a URL” to “I’m reliably reacting to subscription lifecycle events.”

What you’ll need

  • An HTTPS endpoint on your backend that can accept POST requests with a JSON body. Plain HTTP is rejected.
  • A webhook registered in your Nomos dashboard pointing at that URL. Registration returns a signing secret; keep it.

1. Register your endpoint

In the Nomos dashboard, add a new webhook with your endpoint URL. The dashboard issues a webhook secret. Store it the same way you’d store any other production secret. Send a test event from the dashboard’s ”…” action to confirm the endpoint is reachable. The payload looks like:
{
  "topic": "test.event"
}
A 2xx response acks the event. Anything else is treated as a failure (see retries).

2. Verify the signature

Every webhook arrives with a signature header so you can prove it came from Nomos and hasn’t been replayed. Reject anything that doesn’t verify, no exceptions.
import crypto from "node:crypto";

function verifyNomosSignature(rawBody: string, header: string, secret: string) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(header, "hex"),
  );
}
The exact header name and signing scheme are documented under Verify requests. Use the raw request body (not a JSON-parsed-and-restringified one) when computing the HMAC, or signatures will silently mismatch.
Don’t JSON.parse and re-serialize the body before verifying. Frameworks often reorder keys, and that breaks the signature. Capture the raw bytes before anything else touches them.

3. Handle the event

Once verified, dispatch on topic:
app.post("/webhooks/nomos", async (req, res) => {
  const raw = await readRawBody(req);
  if (!verifyNomosSignature(raw, req.header("X-Nomos-Signature"), SECRET)) {
    return res.status(401).end();
  }

  const event = JSON.parse(raw);

  switch (event.topic) {
    case "subscription.created":
      await onSubscriptionCreated(event.context.subscription_id);
      break;
    case "subscription.confirmed":
      await onSubscriptionConfirmed(event.context.subscription_id);
      break;
    case "subscription.activated":
      await onSubscriptionActivated(event.context.subscription_id);
      break;
    case "subscription.terminated":
      await onSubscriptionTerminated(event.context.subscription_id);
      break;
    case "subscription.ended":
      await onSubscriptionEnded(event.context.subscription_id);
      break;
  }

  res.status(200).end();
});
The context only carries IDs. To act on the latest state, fetch the resource:
async function onSubscriptionConfirmed(subscriptionId: string) {
  const subscription = await fetch(
    `https://api.nomos.energy/subscriptions/${subscriptionId}`,
    { headers: { Authorization: `Bearer ${access_token}` } },
  ).then((r) => r.json());

  // do something with subscription
}
This pattern keeps payloads small and avoids the “stale event” problem: by the time you read the resource, you have the truth, not a snapshot from when the event fired.

4. Be idempotent

Nomos retries on non-2xx responses, and at-least-once delivery means you may see the same event.id twice. Dedupe on id before doing any side effect:
const inserted = await db
  .insertInto("processed_webhooks")
  .values({ event_id: event.id })
  .onConflict("do nothing")
  .returning("event_id")
  .executeTakeFirst();

if (!inserted) return res.status(200).end(); // already processed

5. Respond fast

Return 2xx within 30 seconds. If you have heavy work to do, hand the event off to a background queue and ack immediately. See Retries and replays for the full retry schedule.

What’s next

Available events

The list of every event topic and the shape of its envelope.

Verify requests

The exact signing header and verification recipe.

Retries and replays

How long we keep retrying, and how to replay a delivery.

Subscription events

Reference pages for each subscription lifecycle event.