Agent skill

Exa Webhooks Skill

Receive and verify Exa Websets webhooks. Use when setting up Exa webhook handlers, debugging Exa-Signature verification failures, or handling Websets events like webset.created, webset.idle, webset.search.completed, webset.item.created, or webset.item.enriched.

Install this skill

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


When to Use This Skill

  • How do I receive Exa Websets webhooks?
  • How do I verify the Exa-Signature header?
  • How do I handle webset.item.created, webset.item.enriched, or webset.idle events?
  • Why is my Exa webhook signature verification failing?
  • Where do I find my Exa webhook secret?

Essential Code (USE THIS)

Exa signs every delivery with HMAC-SHA256. The Exa-Signature header looks like t=<unix seconds>,v1=<hex> and may carry several v1 values; the request is valid if any one matches. The signed content is "<t>.<raw body>", and the key is the webhook secret as a UTF-8 string (returned only in the response that creates the webhook, so store it then).

Verification (Node.js)

const crypto = require('crypto');

function safeEqual(a, b) {
  const x = Buffer.from(a), y = Buffer.from(b);
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyExa(rawBody, header, secret) {
  const pairs = header.split(',').map((kv) => kv.trim().split(/=(.*)/s).slice(0, 2));
  const t = pairs.find(([k]) => k === 't')?.[1];
  const sigs = pairs.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || sigs.length === 0) return false;
  // Optional replay window, off by default: retries and replays keep the original timestamp
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.`).update(rawBody).digest('hex');
  return sigs.some((s) => safeEqual(s, expected));
}

Verification (Python)

import hashlib, hmac

def verify_exa(raw_body: bytes, header: str, secret: str) -> bool:
    t, sigs = None, []
    for kv in header.split(","):
        k, _, v = kv.strip().partition("=")
        if k == "t":
            t = v
        elif k == "v1":
            sigs.append(v)
    if not t or not sigs:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s, expected) for s in sigs)

Exa documents no SDK helper for this, so verify manually. Always use the raw body; re-serialised JSON will not match.

The timestamp check is opt-in: Hookdeck retries and replays keep the original timestamp, so a freshness window would reject legitimate redeliveries. The examples skip it unless EXA_TIMESTAMP_TOLERANCE_SECONDS is set to a positive number of seconds (default 0 = off).

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

Common Event Types

The event arrives as JSON: { "id", "object": "event", "type", "data", "createdAt" }, where data is the full resource.

EventDescription
webset.createdA Webset was created
webset.idleA Webset has no running operations
webset.search.completedA search finished finding all items
webset.item.createdAn item was added to the Webset
webset.item.enrichedAn item's enrichment completed

Others: webset.deleted, webset.paused, webset.search.created, webset.search.updated, webset.search.canceled, import.created, import.completed, webset.export.created, webset.export.completed, monitor.created, monitor.updated, monitor.deleted, monitor.run.created, monitor.run.completed. See references/overview.md.

For the full event reference, see Exa event types.

Important Headers

HeaderDescription
Exa-Signaturet=<unix seconds>,v1=<hex>[,v1=<hex>...]

Environment Variables

EXA_WEBHOOK_SECRET=your_webhook_secret   # "secret" from the create-webhook response
EXA_TIMESTAMP_TOLERANCE_SECONDS=0        # optional; a positive value opts in to the replay window

Local Development

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

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →