Gareth Wilson Gareth Wilson

Guide to OpenMeter Webhooks: Features and Best Practices

Published


OpenMeter webhooks tell your systems when usage crosses a line that matters: a customer has used 80% of their credits, an entitlement has reset for a new period, or an invoice has been created or changed state. If you meter usage and enforce limits with OpenMeter, webhooks are how your product, billing and customer messaging react without polling OpenMeter's API.

This guide covers how OpenMeter notifications are configured with channels and rules, how deliveries are signed, the four event types and their payloads, and the best practices for running an OpenMeter receiver in production.

What are OpenMeter webhooks?

OpenMeter calls them notifications. Two objects set them up:

  • A channel is the destination. The only channel type is WEBHOOK, which sends a POST to a URL, with any custom headers you add and a signing secret.
  • A rule picks an event type, its conditions (such as thresholds or a feature filter), and the channels to send to. One channel can serve several rules.

When a rule matches, OpenMeter creates an event and delivers it through Svix, which signs each request with the headers svix-id, svix-timestamp and svix-signature.

Every payload has the same envelope: id, timestamp, type and data. The data is contextual, so the receiver "doesn't have to make additional calls to the OpenMeter APIs to gather it".

OpenMeter webhook features

FeatureDetails
ConfigurationNotification channels and rules, through /api/v1/notification/channels and /api/v1/notification/rules
Channel typeWEBHOOK only. A POST to your URL
Custom headersOptional customHeaders on the channel
Signature headerssvix-id, svix-timestamp, svix-signature
SchemeSvix: HMAC-SHA256, base64, over {svix-id}.{svix-timestamp}.{raw body}
SecretThe channel's signingSecret: base64 random bytes, optionally prefixed with whsec_
Event nameTop-level type field
Event IDTop-level id (a ULID)
Event logGET /api/v1/notification/events, with delivery status per channel
ResendPOST /api/v1/notification/events/{eventId}/resend
TestingPOST /api/v1/notification/rules/{ruleId}/test sends a test event with random data
AlternativesHTTP Basic auth or IP blocking, where you can't verify signatures

OpenMeter does not document its retry schedule or response timeout. The Cloud UI's Webhooks page shows the status of deliveries and any errors.

Common events

Event typeSent when
entitlements.balance.thresholdA metered entitlement's usage crosses a threshold in the rule
entitlements.resetA metered entitlement is reset, manually or at the end of its period
invoice.createdA new invoice is generated
invoice.updatedAn existing invoice's state changes

Balance threshold events

Threshold rules work only for metered entitlements, because only those have balances. A threshold can be a percentage of the total grants (such as 80% or 100%) or an exact usage number, and one rule can hold several thresholds. An optional feature filter limits the rule to certain features.

OpenMeter guarantees "only one event is triggered per Entitlement per threshold". Thresholds are re-evaluated when grants are added or voided, so a new grant can produce a notification for a lower threshold, and a reset starts the cycle again.

Here is OpenMeter's example for a numeric threshold of 2000, shortened:

{
  "id": "01J5WQFP993XTYM1HAZH3SHP2G",
  "timestamp": "2024-08-22T09:43:03.209883Z",
  "type": "entitlements.balance.threshold",
  "data": {
    "entitlement": {
      "id": "01J5WM25BAXSCBKG2D9NDYJB6S",
      "featureKey": "llm_tokens",
      "subjectKey": "notification-test-2",
      "type": "metered",
      "isSoftLimit": false,
      "currentUsagePeriod": { "from": "2024-08-22T08:43:00Z", "to": "2024-08-29T08:43:00Z" }
    },
    "feature": { "id": "01J5WKAM9FJ4621ARR641JS17Q", "key": "llm_tokens" },
    "subject": { "id": "01J5WM256DNGXKG2N17AHVXKTD", "key": "notification-test-2" },
    "threshold": { "type": "NUMBER", "value": 2000 },
    "value": { "balance": 254, "hasAccess": true, "overage": 0, "usage": 2369 }
  }
}

Reset and invoice events

entitlements.reset has the same shape as threshold events and can be filtered by feature. Invoice events wrap the invoice as the Get Invoices endpoint returns it. Notifications for gathering invoices are not supported.

Setting up OpenMeter webhooks

  1. Generate a signing secret. OpenMeter expects base64 random bytes, optionally prefixed with whsec_, and its example is whsec_S6g2HLnTwd9AhHwUIMFggVS9OfoPafN8.
  2. Create a WEBHOOK channel with your URL and the secret in signingSecret, using POST /api/v1/notification/channels. Add customHeaders if your endpoint needs them.
  3. Create a rule for each event type you want, with its thresholds or feature filter, and attach the channel.
  4. Send a test event with POST /api/v1/notification/rules/{ruleId}/test and check that your endpoint verifies it.

Store the secret server-side, for example as OPENMETER_WEBHOOK_SECRET.

Securing OpenMeter webhooks

OpenMeter's own example verifies with the svix library: new Webhook(secret).verify(payload, headers). To do it by hand, build the signed content as {svix-id}.{svix-timestamp}.{raw body}, compute an HMAC-SHA256 keyed with the base64-decoded secret (without whsec_), base64-encode it, and compare it in constant time with each v1, entry in svix-signature.

const crypto = require('crypto');
const express = require('express');

function verifyOpenMeterSignature(rawBody, headers, secret) {
  const id = headers['svix-id'];
  const timestamp = headers['svix-timestamp'];
  const signatureHeader = headers['svix-signature'];
  if (!id || !timestamp || !signatureHeader || typeof secret !== 'string') return false;

  // OpenMeter secrets are base64, with or without the "whsec_" prefix.
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  if (key.length === 0) return false;

  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest();

  // The header can hold several space-separated "v1,<base64>" entries.
  return signatureHeader.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'base64');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/openmeter', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.OPENMETER_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyOpenMeterSignature(req.body, req.headers, secret)) {
    return res.sendStatus(401);
  }

  // Optional: if your own policy needs it, reject svix-timestamp values that are too old.
  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.id, route on event.type, and acknowledge unknown types with 200.
  res.sendStatus(200);
});

The same check in Python:

import base64
import hashlib
import hmac


def verify_openmeter_signature(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers.get("svix-id")
    timestamp = headers.get("svix-timestamp")
    signature_header = headers.get("svix-signature")
    if not (msg_id and timestamp and signature_header and secret):
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed_content = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode(), expected):
            return True
    return False

One detail in OpenMeter's example is worth changing. It returns 400 for any event type it doesn't handle, which turns a new event type into a delivery failure. Acknowledge unknown types with a 2xx instead.

To check your verifier, use the secret whsec_aG9va2RlY2stZ3VpZGUtdGVzdC1rZXktMzJieXRlcyE=, the headers svix-id: msg_2kOpenMeterTest0001 and svix-timestamp: 1761040800, and this exact body:

{"id":"01J5WQFP993XTYM1HAZH3SHP2G","type":"entitlements.balance.threshold","data":{}}

The svix-signature header should be:

v1,mw/6fHxn4K9F+U19zpnQtq8lyGVDDbVdIFlqzfdizfc=

When you set up an OpenMeter source in Hookdeck, enter the secret with its whsec_ prefix. If you chose a custom secret without the prefix, add whsec_ in front of it in Hookdeck.

Make OpenMeter webhooks production-ready. Hookdeck Event Gateway verifies the Svix signature on every OpenMeter delivery, deduplicates on the event id, and queues threshold and invoice events before your service sees them.

OpenMeter webhook limitations and pain points

Delivery behaviour is not documented

The Problem: You can't tell from OpenMeter's docs how long your endpoint has to respond, how often a failed event is retried, or when retries stop.

Why It Happens: OpenMeter hands delivery to Svix but does not publish its retry schedule or timeout.

Workarounds:

  • Respond with a 2xx quickly and process the event afterwards.
  • Poll GET /api/v1/notification/events for events in the FAILED state, and resend them with POST /api/v1/notification/events/{eventId}/resend.
  • Check the Webhooks page in OpenMeter Cloud after an outage.

How Hookdeck Can Help: Hookdeck accepts each event as soon as it arrives and stores it, so delivery to Hookdeck succeeds even while your service is down. You then set your own retry policy and replay what failed, and issues alert you when delivery starts failing.

Threshold events fire once

The Problem: A customer's access is never blocked because your service missed the single 100% threshold event.

Why It Happens: OpenMeter sends one event per entitlement per threshold. If you drop it, nothing reminds you until the next threshold or a reset.

Workarounds:

  • Treat the event as a trigger to read the entitlement's current value from the OpenMeter API, and also check access on your own schedule.
  • Monitor the Events API for failed deliveries of threshold events.

How Hookdeck Can Help: Hookdeck keeps every event it receives, so a threshold event your service failed to process can be found and replayed.

Custom secrets need the prefix in some tools

The Problem: Signature checks fail even though you copied the secret correctly.

Why It Happens: OpenMeter accepts a secret with or without whsec_. Svix-style verifiers, including Hookdeck's, treat a whsec_ secret as base64 to decode. Hookdeck uses a secret without the prefix as plain text, so the key it computes with doesn't match the one OpenMeter signs with.

Workarounds:

  • Use the whsec_ form everywhere.
  • If you set a custom secret without the prefix, add whsec_ in front of it in your verifier's configuration.

How Hookdeck Can Help: Hookdeck's OpenMeter source verifies svix-signature for you once the secret is entered with its whsec_ prefix.

Best practices

Deduplicate on the event id

Every event has a unique id, and a resend through the API delivers the same event again. Store each id you process and skip repeats.

Acknowledge unknown event types

OpenMeter's event type list has four values today. Route on type and return 200 for anything you don't handle, so a new type never counts as a failed delivery.

Use thresholds that leave room to act

A 100% threshold tells you when to block. An 80% threshold gives you time to warn the customer first. One rule can hold both.

Verify against the raw body

Svix signs the raw bytes. Use express.raw() or your framework's raw body before parsing JSON.

Acknowledge fast and process asynchronously

Return 200 after verification and queue the work. See why to process webhooks asynchronously.

Make handlers idempotent

Processing the same event twice should have the same result as processing it once. See our guide to webhook idempotency.

Conclusion

OpenMeter sends entitlement threshold, entitlement reset and invoice events through webhook channels, signed by Svix over svix-id, svix-timestamp and the raw body. Verify with the channel's signing secret, deduplicate on the event id, acknowledge event types you don't handle, and use the Events API to find and resend anything that failed.

Hookdeck Event Gateway checks every OpenMeter signature, stores each event before your service sees it, and retries on a schedule you control.

Get started with Hookdeck for free and handle OpenMeter webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.