Webhooks

When a sentinel matches a new SEC filing, edgar.tools delivers a JSON event to your endpoint with an HMAC-SHA256 signature so you can verify it came from us. Webhooks — configuration and delivery — are available on the Analyst plan and above.

Endpoints

You configure endpoints at Settings → Webhooks. Each endpoint has:

  • A POST URL (HTTPS, public host, no redirects)
  • A signing secret (edgar_whsec_…) shown once at creation and on every rotation
  • A status (active or disabled) and a per-sentinel health rollup

Sentinels reference an endpoint by id; one endpoint can serve many sentinels.

Webhooks settings page showing the Your webhook endpoints table with five active endpoints — webhook.site, discord.com, and three example.com test endpoints — each row listing the URL, masked signing secret, ACTIVE status, creation date, and action icons for test/rotate/copy/delete

Headers

Every delivery includes:

HeaderDescription
Content-TypeAlways application/json
X-Edgar-EventEvent type (e.g. filing.analyzed.v1)
X-Edgar-DeliveryUnique event id — use this as your idempotency key
X-Edgar-Signaturesha256=<hex> — HMAC-SHA256 of the raw body. During a rotation grace window, two signatures comma-joined: sha256=A,sha256=B
X-Edgar-TimestampUnix epoch seconds when we signed the request
X-Edgar-TestSet to true only for events sent from the "Send test event" button. Absent on real deliveries.

Verifying signatures

Compute HMAC-SHA256 of the raw request body with your signing secret and compare to the value(s) in X-Edgar-Signature. Use a constant-time comparison.

Node.js

import crypto from 'node:crypto';

export function verifyEdgarSignature(rawBody, header, secret) {
  if (!header) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  // header may be 'sha256=A' or 'sha256=A,sha256=B' during rotation.
  return header.split(',').some((sig) => {
    const [scheme, hex] = sig.split('=');
    if (scheme !== 'sha256' || !hex) return false;
    const a = Buffer.from(hex, 'hex');
    const b = Buffer.from(expected, 'hex');
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

Python

import hmac, hashlib

def verify_edgar_signature(raw_body: bytes, header: str, secret: str) -> bool:
    if not header:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    for sig in header.split(','):
        scheme, _, hex_sig = sig.partition('=')
        if scheme != 'sha256' or not hex_sig:
            continue
        if hmac.compare_digest(hex_sig, expected):
            return True
    return False

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strings"
)

func VerifyEdgarSignature(rawBody []byte, header, secret string) bool {
    if header == "" {
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := mac.Sum(nil)
    for _, sig := range strings.Split(header, ",") {
        parts := strings.SplitN(sig, "=", 2)
        if len(parts) != 2 || parts[0] != "sha256" {
            continue
        }
        got, err := hex.DecodeString(parts[1])
        if err != nil {
            continue
        }
        if hmac.Equal(got, expected) {
            return true
        }
    }
    return false
}

Payload — filing.analyzed.v1

The wire-shape we lock with a version tag. Future event types add new strings (e.g. webhook.test.v1); receivers branch on type first.

{
  "id": "evt_a1b2c3d4e5f6",
  "type": "filing.analyzed.v1",
  "workflow_type": "material_event_analysis",
  "timestamp": "2026-04-29T13:30:42.000Z",
  "filing": {
    "accession": "0000320193-26-000042",
    "ticker": "AAPL",
    "company": "APPLE INC",
    "form": "8-K",
    "filed_at": "2026-04-29T13:30:00-04:00"
  },
  "context_prompt": "...",
  "analysis": {
    "summary": "...",
    "signals": [],
    "confidence": 0.92,
    "model": "llama-3.3-70b",
    "latency_ms": 1500
  }
}

workflow_type carries the granular kind of analysis (material_event_analysis, earnings_event, insider_trade_signal, etc.) — branch on it for sub-classification.

Payload — webhook.test.v1

Sent only when you click "Send test event" on an endpoint. Body includes test: true and a title / message so a naive Slack template still reads as a test:

{
  "id": "evt_test_abc123def456",
  "type": "webhook.test.v1",
  "test": true,
  "timestamp": "2026-04-29T13:30:42.000Z",
  "title": "Test webhook from edgar.tools",
  "message": "This is a test event triggered from your webhook settings...",
  "filing": {
    "accession": "0000000000-00-000000",
    "ticker": "TEST",
    "company": "TEST COMPANY (webhook test event)",
    "form": "TEST",
    "filed_at": "2026-04-29T13:30:42.000Z"
  }
}

Retries

We retry on:

  • HTTP 5xx
  • HTTP 429 (rate-limited)
  • Network errors / timeouts

We do not retry on:

  • HTTP 2xx (delivered)
  • HTTP 4xx other than 429 (your service rejected it — fix the receiver, the next match will go through)
  • HTTP 3xx redirects (we don't follow; configure a final URL)

Up to 3 attempts with exponential backoff. After 3 failures the message goes to the dead-letter queue. Sustained failures across many events disable the endpoint automatically.

Idempotency

X-Edgar-Delivery is unique per event. Use it as your dedupe key — a retry of the same event has the same delivery id. We deliver each event at-least-once.

Secret rotation

Rotate from Settings → Webhooks. Rotation:

  1. Mints a new secret (shown once).
  2. Keeps the old secret valid for 24 hours.
  3. During the window, every delivery's X-Edgar-Signature carries both signatures, comma-joined: sha256=<new>,sha256=<old>.

Update your stored secret to the new value any time during the window. The verification snippets above already accept either match.

Verifying your endpoint

  1. Create an endpoint at Settings → Webhooks. Save the secret.
  2. Click the bolt icon (Send test event).
  3. Confirm the response panel shows HTTP 200, your latency, and any body excerpt your service returned.
  4. In your service logs, verify X-Edgar-Test: true and that signature verification succeeded.

If verification fails, the most common cause is reading the request body twice (e.g., as JSON before HMAC). HMAC must be over the raw bytes, exactly as received.

Recent deliveries

The Deliveries page lists every attempt for the past 30 days — HTTP status, latency, response excerpt, and which sentinel fired. Filter to failures to triage broken endpoints.

Deliveries page showing the per-attempt log table with All / Success / Failures filter tabs and one delivery row for a TEST event hitting webhook.site, returning HTTP 200 in 663 ms with SUCCESS status

Tier gating

Webhooks — both configuring an endpoint and receiving deliveries — require Analyst tier or above (see Compare Plans).