Skip to main content
Use this tutorial to receive signed webhook notifications when a saved filing monitor finds new matches. SEC API monitor webhooks are not a direct EDGAR firehose: monitor dispatch runs on a scheduled sweep, emits the public monitor.match Delivery event, and sends that event to org-level webhook endpoints subscribed to monitor.match. Typical delivery latency is about half of the 15-minute sweep interval. Worst-case latency is one full sweep plus dispatch and provider time. Use GET /v1/monitors/{monitor_id}/matches when you need an on-demand preview or audit check.

Prerequisites

  • A signed-in SEC API dashboard session to configure the monitor and webhook endpoint
  • An SEC API key in SECAPI_API_KEY for the read-only preview and audit calls below
  • A publicly reachable HTTPS URL for webhook delivery
  • Python 3.9+ if you want to run the sample handler

Step 1 - Register a signed webhook endpoint

Open the signed-in SEC API dashboard, add your public HTTPS destination, and subscribe it to monitor.match. Save the signing secret when it is shown. Endpoint creation and secret rotation are human-authenticated organization actions; an API key cannot perform them. SEC API sends webhook deliveries with x-secapi-signature: t=<unix_seconds>,v1=<hex_hmac> and x-secapi-signature-timestamp. Verify the signature against the exact raw request body before parsing JSON.

Step 2 - Create a filing monitor

Create a monitor in the dashboard for the query, forms, and tickers you care about. For webhook delivery, do not put a URL on the monitor: a matching filing is emitted as monitor.match and delivered through the organization endpoint from Step 1. For this tutorial, use the name Mega-cap 8-K monitor, the query material agreement OR results of operations OR departure of directors, forms 8-K and 8-K/A, and the tickers AAPL, MSFT, AMZN, GOOGL, and META.

Step 3 - Preview matches

Before waiting for the scheduled dispatch, preview what the monitor currently matches:
curl -H "x-api-key: $SECAPI_API_KEY" \
  "https://api.secapi.ai/v1/monitors/mon_9a1b2c3d/matches?limit=10"
This endpoint runs the saved search on demand. It is useful for setup checks, but it is not the same thing as webhook delivery. Webhook delivery happens during the monitor sweep.

Step 4 - Handle monitor.match

A monitor webhook delivery is a Delivery event envelope. The event type is monitor.match; details live under data.
{
  "object": "event",
  "id": "evt_6f1b7d99",
  "type": "monitor.match",
  "createdAt": "2026-07-10T18:30:00.000Z",
  "livemode": false,
  "orgId": "org_123",
  "requestId": "req_123",
  "data": {
    "monitor": {
      "id": "mon_9a1b2c3d",
      "name": "Mega-cap 8-K monitor",
      "query": "material agreement OR results of operations OR departure of directors",
      "searchMode": "keyword",
      "deliveryType": "webhook"
    },
    "matches": [
      {
        "id": "0000320193-26-000001",
        "form": "8-K",
        "ticker": "AAPL",
        "filingDate": "2026-07-10",
        "htmlUrl": "https://www.sec.gov/Archives/edgar/data/...",
        "companyName": "Apple Inc."
      }
    ],
    "matchCount": 1,
    "dispatchedAt": "2026-07-10T18:30:00.000Z",
    "deliveryResults": []
  }
}
Fields can vary by match source. Treat data.matchCount, data.monitor.id, and the normalized fields in data.matches[] as the stable routing surface. Create handler.py:
import hashlib
import hmac
import json
import os
import time

from flask import Flask, jsonify, request

app = Flask(__name__)
SIGNING_SECRET = os.environ["SECAPI_WEBHOOK_SIGNING_SECRET"]
MAX_SKEW_SECONDS = 300


def parse_signature(header: str) -> tuple[str, str]:
    parts = {}
    for item in header.split(","):
        if "=" in item:
            key, value = item.split("=", 1)
            parts[key] = value
    return parts.get("t", ""), parts.get("v1", "")


def verify_signature(raw_body: bytes, header: str) -> bool:
    timestamp, provided = parse_signature(header)
    if not timestamp or not provided:
        return False

    try:
        signed_at = int(timestamp)
    except ValueError:
        return False

    if abs(int(time.time()) - signed_at) > MAX_SKEW_SECONDS:
        return False

    expected = hmac.new(
        SIGNING_SECRET.encode("utf-8"),
        f"{timestamp}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(provided, expected)


@app.post("/webhooks/secapi")
def handle_secapi_webhook():
    raw_body = request.get_data()
    signature = request.headers.get("x-secapi-signature", "")
    if not verify_signature(raw_body, signature):
        return jsonify({"error": "invalid_signature"}), 401

    event = json.loads(raw_body)
    if event.get("type") != "monitor.match":
        return jsonify({"status": "ignored", "type": event.get("type")}), 200

    data = event.get("data", {})
    monitor = data.get("monitor", {})
    for match in data.get("matches", []):
        print(
            f"{monitor.get('name')} matched "
            f"{match.get('form', 'filing')} for {match.get('ticker', 'UNKNOWN')}: "
            f"{match.get('htmlUrl') or match.get('id')}"
        )

    return jsonify({"received": True}), 200


@app.get("/health")
def health():
    return jsonify({"status": "ok"})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
Run it locally:
pip install flask
SECAPI_WEBHOOK_SIGNING_SECRET="whsec_..." python handler.py
Use a tunnel such as ngrok or your hosting provider to expose https://.../webhooks/secapi, then update the endpoint destination in the dashboard if it changes.

Step 5 - Send a signed test event

Use the dashboard’s test-delivery action to validate connectivity and signature verification. Tests emit webhook.test, not monitor.match, so your handler should verify the signature and then ignore the event type.

Step 6 - Inspect delivery history

Use delivery history when debugging endpoint failures:
curl -H "x-api-key: $SECAPI_API_KEY" \
  "https://api.secapi.ai/v1/webhook_endpoints/wh_2ZK8Q1W9F4M6P7R3/deliveries?limit=10"
You can also list your org’s durable Delivery events:
curl -H "x-api-key: $SECAPI_API_KEY" \
  "https://api.secapi.ai/v1/delivery/events?type=monitor.match&limit=10"

Manage the monitor

# List monitors
curl -H "x-api-key: $SECAPI_API_KEY" \
  "https://api.secapi.ai/v1/monitors"

# Get one monitor
curl -H "x-api-key: $SECAPI_API_KEY" \
  "https://api.secapi.ai/v1/monitors/mon_9a1b2c3d"

# Pause or delete a monitor in the signed-in dashboard.

Delivery limitations

  • monitor.match is the public webhook event for monitor hits. Do not subscribe to custom event names such as filing.new or 8k.published unless they appear in GET /v1/event_types?status=public_emitting.
  • Monitor dispatch is scheduled. It is not guaranteed to fire the moment EDGAR publishes a filing.
  • The deprecated webhookUrl field on monitors may remain stored for backward compatibility, but it no longer delivers events. Use an org-level /v1/webhook_endpoints subscription to monitor.match.
  • Webhook retries and delivery audit records are tied to Delivery events. Use the webhook deliveries endpoint for troubleshooting and replay.