Documentation
Webhooks
Register a URL once; matching events on your watchlist arrive as a signed POST within seconds of being persisted — no polling loop, no missed events between requests. Available on the Business plan.
1Create an endpoint
POST /api/user/webhooks — an empty event_types list subscribes to every event type. The secret is returned once on create/update — subsequent GET calls redact it to null, so store it when you create the endpoint.
curl -X POST https://api.openfilings.org/api/user/webhooks \
-H "X-API-Key: of_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.example.com/openfilings-webhook",
"event_types": ["filing.discovered"],
"secret": "whsec_your_own_random_string",
"active": true
}'Response:
{
"id": "b6b8e0b0-....-....-....-............",
"url": "https://yourapp.example.com/openfilings-webhook",
"event_types": ["filing.discovered"],
"secret": "whsec_your_own_random_string",
"active": true,
"created_at": "2026-08-08T09:00:00+00:00",
"updated_at": "2026-08-08T09:00:00+00:00"
}PATCH /api/user/webhooks/{id} updates any subset of url, event_types, secret, active — omitted fields are left unchanged. POST /api/user/webhooks/{id}/test fires one signed webhook.test ping synchronously so you can verify your handler without waiting for a real filing.
2Envelope
Every delivery is one JSON object — event, version, timestamp, and an event-specific data object:
{
"event": "filing.discovered",
"version": "1",
"timestamp": "2026-08-08T09:14:02.331Z",
"data": {
"canonical_key": "sec:0000320193",
"ticker": "AAPL",
"market_id": "us",
"form_type": "10-Q",
"filing_id": "3f1b7e2a-....-....-....-............",
"filing_date": "2026-08-08"
}
}3Verify the signature
Requests carry X-Webhook-Signature and X-Webhook-Timestamp (the same value embedded in the body's timestamp):
POST /openfilings-webhook HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: sha256=6b0f...e2a1
X-Webhook-Timestamp: 2026-08-08T09:14:02.331Z
{"event":"filing.discovered","version":"1", ...}Signature = HMAC-SHA256(secret, f"{timestamp}." + raw_body_bytes), hex-encoded with a sha256= prefix. Verify against the raw request body bytes — re-serializing parsed JSON before verifying will break the signature if key order or whitespace differs. Use a constant-time comparison (hmac.compare_digest / crypto.timingSafeEqual).
Python — FastAPI (recommended)
import os
from fastapi import Depends, FastAPI
from openfilings.integrations.fastapi import openfilings_webhook_dependency
from openfilings.webhooks import WebhookEvent
app = FastAPI()
verify = openfilings_webhook_dependency(os.environ["OPENFILINGS_WEBHOOK_SECRET"])
@app.post("/openfilings-webhook")
async def on_event(event: WebhookEvent = Depends(verify)) -> dict:
if event.event == "filing.discovered":
print(event.data["ticker"], event.data["filing_id"])
return {"ok": True}pip install openfilings[fastapi] — openfilings_webhook_dependency verifies the raw body and yields a typed WebhookEvent. The core verify_webhook() helper only imports the standard library and works in a Lambda without the rest of the SDK.
Python — Flask
from flask import Flask, request
from openfilings import verify_webhook
from openfilings.webhooks import InvalidSignatureError
app = Flask(__name__)
SECRET = "whsec_your_own_random_string"
@app.route("/openfilings-webhook", methods=["POST"])
def on_event():
try:
event = verify_webhook(request.get_data(), request.headers, SECRET)
except InvalidSignatureError:
return "invalid signature", 400
if event.event == "filing.discovered":
print(event.data["ticker"], event.data["filing_id"])
return "ok", 200Or pip install openfilings[flask] and verify_flask_webhook(request, SECRET).
Python — standard library only
import hashlib
import hmac
def verify(body: bytes, signature_header: str, timestamp_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), f"{timestamp_header}.".encode() + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)Node.js
const crypto = require("crypto");
function verify(rawBody, signatureHeader, timestampHeader, secret) {
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(`${timestampHeader}.`)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}4Event catalog
| event | Fires when |
|---|---|
| filing.discovered | A new filing was persisted for a ticker on your watchlist. |
| press_release.discovered | A wire headline matched a watched ticker (Pro+). |
| insider_transaction.discovered | A Form 4 / insider transaction was parsed for a watched ticker. |
| earnings_release.upcoming | A tracked issuer has an earnings release coming up. |
| earnings_release.reported | A tracked issuer's earnings release was reported. |
| earnings_guidance.available | Forward guidance became available on a tracked call. |
| event_transcript.available | An earnings call transcript finished processing. |
Pass the exact strings above in event_types — leave the list empty to receive all of them. A synthetic webhook.test event is only sent by the manual test-ping endpoint, never as a real notification.
5Retry policy
- A non-2xx response or timeout is retried up to 5 times with a fixed 60-second delay between attempts.
- Any 4xx response is treated as permanent — your endpoint rejected the payload, so it is not retried. Fix the handler, then use the test ping to confirm before the next real event.
- Deliveries are best-effort, at-least-once — a handler should be idempotent on the envelope's identifiers (e.g.
filing_id) in case of a retried delivery after a slow 2xx.
url at a public HTTPS endpoint you control.6Python SDK
pip install openfilings wraps the same REST API used above with typed models and automatic 429 backoff, plus the verify_webhook() helper shown in step 3:
client.webhooks.create(
"https://yourapp.example.com/openfilings-webhook",
event_types=["filing.discovered"],
secret="whsec_...",
)
client.webhooks.test(webhook_id)