Agent skill

Axiom Webhooks Skill

Receive and authenticate Axiom monitor webhooks. Use when setting up an Axiom Custom webhook notifier endpoint, checking a secret header (Axiom does not sign requests), debugging 401s, or handling monitor notifications with action Open and Closed.

Install this skill

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


Axiom (observability) sends a JSON POST to your URL when a monitor fires or resolves, through a Custom webhook notifier. Requests are not signed. You authenticate them with a secret header you add to the notifier yourself, and your endpoint compares that header with the value you stored.

Use the Custom webhook notifier with a secret header. The plain Webhook notifier cannot send one.

The body is a Go template you write. The default template puts the state in action (Open or Closed) and the monitor data under event. Axiom adds no event-type field of its own.

When to Use This Skill

  • How do I receive Axiom monitor webhooks?
  • How do I authenticate Axiom Custom webhook notifier requests?
  • Does Axiom sign webhook payloads? (No: use a secret header)
  • How do I handle Open and Closed monitor notifications?
  • Why does my Axiom webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyAxiomRequest(headers, {
  name = process.env.AXIOM_WEBHOOK_HEADER || 'Authorization',
  secret = process.env.AXIOM_WEBHOOK_SECRET,
} = {}) {
  if (!secret) return false;                      // never accept when unconfigured
  const received = headers[name.toLowerCase()];   // header name is user-chosen
  return Boolean(received) && safeEqual(received, secret); // compare the WHOLE value
}
  • The header name is whatever you typed in the notifier, so read it from AXIOM_WEBHOOK_HEADER (default Authorization) and the expected value from AXIOM_WEBHOOK_SECRET.
  • AXIOM_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed. Compare it whole: do not strip or parse a Bearer prefix.
  • Python: hmac.compare_digest on bytes (a.encode(), b.encode()); passing str raises on non-ASCII.
  • Missing header or mismatch: 401. Body that isn't a JSON object: 400. Handled: 200.
  • Read the body raw (express.raw({ type: '*/*' })) and authenticate before parsing it.

A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the credential if it leaks.

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

Notification Types

Axiom sends no event field. With the default template, dispatch on action:

actionSent When
OpenA match monitor matches, or a threshold monitor triggers
ClosedA threshold monitor resolves

If you edit the template, dispatch on whatever field you put in it. The examples and tests use the default template as an illustrative payload.

Environment Variables

AXIOM_WEBHOOK_HEADER=Authorization       # header name you set on the notifier
AXIOM_WEBHOOK_SECRET=Bearer your-token   # exact, whole header value

Local Development

Axiom can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 axiom --path /webhooks/axiom

Use the printed URL as the Webhook URL in the Custom webhook notifier.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →