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 (
activeordisabled) and a per-sentinel health rollup
Sentinels reference an endpoint by id; one endpoint can serve many sentinels.

Headers
Every delivery includes:
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Edgar-Event | Event type (e.g. filing.analyzed.v1) |
X-Edgar-Delivery | Unique event id — use this as your idempotency key |
X-Edgar-Signature | sha256=<hex> — HMAC-SHA256 of the raw body. During a rotation grace window, two signatures comma-joined: sha256=A,sha256=B |
X-Edgar-Timestamp | Unix epoch seconds when we signed the request |
X-Edgar-Test | Set 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 FalseGo
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:
- Mints a new secret (shown once).
- Keeps the old secret valid for 24 hours.
- During the window, every delivery's
X-Edgar-Signaturecarries 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
- Create an endpoint at Settings → Webhooks. Save the secret.
- Click the bolt icon (Send test event).
- Confirm the response panel shows HTTP 200, your latency, and any body excerpt your service returned.
- In your service logs, verify
X-Edgar-Test: trueand 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.

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