> ## Documentation Index
> Fetch the complete documentation index at: https://docs.secapi.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Filing Review Intake Queue

> Verify signed monitor events and create source-linked items for a human review.

This guide receives a `monitor.match` event, verifies the signature, and creates a source-linked intake record. It does not decide whether a filing is material, resolve a counterparty, or clear a compliance issue. The filing remains the review source.

## Prerequisites

You need Python 3.9+, Flask, a public HTTPS receiver, and the signing secret for
an already configured delivery endpoint. Configure the endpoint and a narrow
monitor in the signed-in [SEC API dashboard](https://secapi.ai/app/webhooks),
then store the secret as `SECAPI_WEBHOOK_SIGNING_SECRET`. An API key is only
needed to read a configured monitor's matches.

## 1. Verify the raw delivery

Create `receiver.py`:

```python theme={null}
import hashlib
import hmac
import json
import os
import time
from flask import Flask, jsonify, request

app = Flask(__name__)
secret = os.environ["SECAPI_WEBHOOK_SIGNING_SECRET"].encode()

def valid(raw: bytes, header: str) -> bool:
    fields = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
    try:
        timestamp = int(fields["t"])
    except (KeyError, ValueError):
        return False
    signature = fields.get("v1", "")
    if not signature or abs(time.time() - timestamp) > 300:
        return False
    expected = hmac.new(secret, f"{timestamp}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.post("/webhooks/secapi/review")
def receive():
    raw = request.get_data()
    if not valid(raw, request.headers.get("x-secapi-signature", "")):
        return jsonify({"error": "invalid_signature"}), 401
    event = json.loads(raw)
    if event.get("type") != "monitor.match":
        return jsonify({"status": "ignored"}), 200
    for match in event.get("data", {}).get("matches", []):
        print({"ticker": match.get("ticker"), "form": match.get("form"),
               "filingDate": match.get("filingDate"), "sourceUrl": match.get("htmlUrl")})
    return jsonify({"received": True}), 200
```

Run `pip install flask`, then expose this receiver at the configured HTTPS destination. A valid production event prints one source-linked record for each delivered match. A valid `webhook.test` is acknowledged but ignored; it proves the receiver can verify delivery, not that the monitor query will produce a filing.

## 2. Make the review record durable

Use the event id as an idempotency key before enqueueing work. Retain event `requestId`, monitor name, issuer identity, form, filing date, and `htmlUrl`. The event is a notification envelope, not the filing itself, so give the reviewer the source URL and keep an LLM summary separate from the decision.

For a configured monitor, an API key can run its saved query and retrieve matches:

```bash theme={null}
curl --fail-with-body -H "x-api-key: $SECAPI_API_KEY" \
  "https://api.secapi.ai/v1/monitors/MONITOR_ID/matches?limit=25"
```

## Limits and failure posture

`401 invalid_signature` usually means the handler signed parsed JSON instead of raw bytes, used the wrong secret, or received a stale timestamp. Treat duplicate delivery as normal: persist the event id before scheduling the review. A text match or model classification is a lead, not a legal conclusion; require the appropriate reviewer to read the linked source.

## Next links

Replace `print` with one transactional write that stores the event ID, source
reference, and pending-review status. A dashboard test proves receiver handling,
not that a monitor query found a filing. See [webhook delivery audit](/webhook-delivery-audit).
