Agent skill

Metronome Webhooks Skill

Receive and verify Metronome webhooks. Use when setting up Metronome webhook handlers, debugging Metronome-Webhook-Signature verification, or handling usage-based billing events like alerts.spend_threshold_reached, contract.create, or invoice.finalized.

Install this skill

npx skills add hookdeck/webhook-skills --skill metronome-webhooks


When to Use This Skill

  • How do I receive Metronome webhooks?
  • How do I verify the Metronome-Webhook-Signature header?
  • Why is my Metronome webhook signature verification failing?
  • How do I handle alerts.spend_threshold_reached, contract.create or invoice.finalized events?
  • How do I deduplicate Metronome webhook retries?

Verification (core)

Metronome sends the signature as bare lowercase hex in Metronome-Webhook-Signature. It is HMAC-SHA256, keyed with the webhook's secret key (UTF-8 bytes, used as-is), over X-Metronome-Date + a newline (LF, 0x0A) + the raw request body bytes. Use X-Metronome-Date, not Date (intermediaries may rewrite Date). No official SDK helper exists, so verify manually.

const crypto = require('crypto');

function verifyMetronomeSignature(rawBody /* Buffer */, signature, dateHeader, secret) {
  if (!signature || !dateHeader || !secret) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before decoding
  const signed = Buffer.concat([Buffer.from(dateHeader, 'latin1'), Buffer.from('\n'), rawBody]);
  const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(signed).digest();
  const provided = Buffer.from(signature.toLowerCase(), 'hex');
  return provided.length === expected.length && crypto.timingSafeEqual(provided, expected);
}

Python:

import hashlib, hmac, re

def verify(raw_body: bytes, signature: str, date_header: str, secret: str) -> bool:
    if not signature or not date_header or not secret or not re.fullmatch(rb"[0-9a-fA-F]{64}", signature.encode("latin-1")):
        return False
    signed = b"".join([date_header.encode("latin-1"), b"\n", raw_body])
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest().encode()
    return hmac.compare_digest(signature.encode("latin-1").lower(), expected)

Return 401 for a missing or malformed signature or header, or a mismatch. Read the body as bytes (Express: express.raw({ type: '*/*' }); Next.js: await request.arrayBuffer(); FastAPI: await request.body()) and parse JSON only after the check.

Timestamp window is off by default. Hookdeck retries and replays resend the original X-Metronome-Date, and Hookdeck itself applies no tolerance, so a fixed window would reject legitimate retries. The examples offer an opt-in METRONOME_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off); X-Metronome-Date is an HTTP date such as Mon, 02 Jan 2006 22:04:05 GMT.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Common Event Types

Dispatch on the body's type. Deduplicate on the body's id (duplicates are possible, and a status above 299 is retried for up to two days).

EventDescription
alerts.spend_threshold_reachedA spend threshold alert fired
alerts.low_remaining_credit_balance_reachedRemaining credit balance fell below the threshold
alerts.low_remaining_commit_balance_reachedRemaining commit balance fell below the threshold
alerts.usage_threshold_reachedA usage threshold alert fired
alerts.invoice_total_reachedAn invoice total threshold was reached
contract.create / contract.start / contract.edit / contract.end / contract.archiveContract lifecycle
commit.createA commit was created
credit.createA credit was created
invoice.finalized / invoice.voidInvoice finalized or voided
invoice.billing_provider_errorPushing an invoice to the billing provider failed
subscription.start / subscription.endSubscription lifecycle

Metronome documents 34 types (more commit, credit, marketplace and integration events); the examples log any type they don't handle.

For the full list, see Metronome webhook documentation.

Environment Variables

METRONOME_WEBHOOK_SECRET=your_webhook_secret_key   # Per-webhook secret key
METRONOME_WEBHOOK_TOLERANCE_SECONDS=300            # Optional; unset or 0 = no timestamp window

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 metronome --path /webhooks/metronome

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →