Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.financialdatasets.ai/llms.txt

Use this file to discover all available pages before exploring further.

This guide walks you through receiving your first webhook delivery end-to-end: create a destination in the dashboard, spin up a receiver, verify the signature, and replay a delivery. By the end you’ll have a production-ready handler.
Already integrated? Jump to the Webhooks reference for the full event catalog, payload envelope, retry schedule, and production checklist.

Prerequisites

  • Pro or Enterprise plan. Upgrade if needed.
  • Python 3.10+ for the code samples (Node alternative shown in tabs).
  • An HTTPS endpoint you control. For local testing, use a tunnel like ngrok or localtunnel to expose localhost.

What we’ll build

Throughout this guide we’ll build a handler for a fictional integrator, Acme Capital. Acme wants a Slack ping the moment we publish new earnings for any ticker on their watchlist (AAPL, MSFT, NVDA).
Value
Acme’s endpoint URLhttps://acme.example.com/webhooks/financial-datasets
Signing secret (placeholder)whsec_PLACEHOLDER_FROM_DASHBOARD
Event typeearnings.created
WatchlistAAPL, MSFT, NVDA
Substitute your own values as you follow along.

Step 1: Add a destination in the dashboard

  1. Go to the Webhooks page in your dashboard.
  2. Click Add destination.
  3. Paste your HTTPS endpoint URL — for Acme that’s https://acme.example.com/webhooks/financial-datasets.
  4. Expand the Earnings group, check earnings.created, click Add destination.
  5. The new row appears with a truncated signing secret (whsec_...). Click the row to expand it, then click Reveal next to the signing secret and copy the full value. You’ll plug it into the receiver in the next step.
Treat your signing secret like a credential. Store it in a secret manager (not git, not your .env checked into source control), and rotate it from the dashboard if it ever leaks.

Step 2: Spin up a receiver

The receiver does three things: read the raw request body, verify the FD-Signature header, and return 2xx.
Install Flask:
pip install flask
Save as receiver.py:
import hmac
import hashlib
import time
from flask import Flask, request, abort

app = Flask(__name__)

# From the dashboard — Acme's destination signing secret.
SIGNING_SECRET = "whsec_PLACEHOLDER_FROM_DASHBOARD"
SIGNATURE_MAX_SKEW_SECONDS = 300

# Acme's earnings watchlist. Replace with your own.
WATCHLIST = {"AAPL", "MSFT", "NVDA"}


def verify(raw_body: bytes, header_value: str, secret: str) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in header_value.split(","))
        ts = int(parts["t"])
        candidate = parts["v1"]
    except (KeyError, ValueError):
        return False

    if abs(int(time.time()) - ts) > SIGNATURE_MAX_SKEW_SECONDS:
        return False

    signing_input = f"{ts}.".encode("utf-8") + raw_body
    expected = hmac.new(
        secret.encode("utf-8"), signing_input, hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(candidate, expected)


def handle_earnings_created(event: dict) -> None:
    """Acme's business logic — ping Slack for watchlisted tickers."""
    obj = event["data"]["object"]
    ticker = obj.get("ticker")
    if ticker in WATCHLIST:
        # Replace this with your Slack / pager / queue integration.
        print(f"🔔 watchlist hit: {ticker} reported earnings for "
              f"{obj.get('fiscal_period')}")
    else:
        print(f"   skip {ticker} (not on watchlist)")


@app.post("/webhooks/financial-datasets")
def receive():
    # IMPORTANT: read raw bytes BEFORE Flask parses to JSON.
    # The signature is computed over the exact bytes we sent.
    raw = request.get_data()
    sig = request.headers.get("FD-Signature", "")

    if not verify(raw, sig, SIGNING_SECRET):
        abort(400, "invalid signature")

    event = request.get_json()
    print(f"received {event['type']} id={event['id']} livemode={event['livemode']}")

    # TODO: in production, dedupe on event["id"] before doing side effects
    # and enqueue heavy work to a background job so this handler returns fast.
    if event["type"] == "earnings.created":
        handle_earnings_created(event)

    return "", 200


if __name__ == "__main__":
    app.run(port=5001)
Run it:
python receiver.py
Then expose it via a tunnel for the dashboard to reach:
ngrok http 5001
# → copy the https://...ngrok-free.app URL into the destination's URL field

Step 3: Fire a test event

Back in the dashboard:
  1. Find your destination row and click it to expand.
  2. Click Send test event.
  3. Within ~5 seconds your receiver should log two lines and the event should appear in Recent events with a green checkmark and HTTP 200.
Acme’s receiver should log something like:
received earnings.created id=b5fd9c10-e3b2-4c5d-a2b3-98fda43365cf livemode=False
   skip BLDR (not on watchlist)
The canned test event uses ticker BLDR, which isn’t on Acme’s watchlist — so it goes through the verify-and-skip branch. Once real earnings.created events for AAPL, MSFT, or NVDA arrive, the handler will hit the 🔔 watchlist hit branch instead. Test events have livemode: false and use a canned earnings.created payload — useful for confirming the wiring before real data flows.
Test events are throttled to 1 per minute per destination. A second click within 60 seconds will return 429 Too Many Requests.

Step 4: Inspect a delivery

Click any row in Recent events to slide up the detail drawer. You’ll see:
  • Request: event type, event id, endpoint URL, attempt number, timestamps.
  • Response: HTTP status, duration, body returned by your endpoint.
  • Signed payload: the exact JSON we POSTed, ready to copy or download as .json for replaying in your local tests.
If verification fails or your endpoint 5xx’s, this drawer is where you’ll diagnose it. The response body field will show whatever your server returned.

Step 5: Replay a delivery

From the detail drawer, click Replay delivery. We’ll re-send the same event payload through the full delivery pipeline (signed with your current secret) and create a new row in Recent events with attempt number 1. The original row stays as a historical record. This is the fastest way to debug a handler fix — patch your code, deploy, hit Replay.

Production checklist

Before flipping real customers onto your handler:
  • HTTPS endpoint. Required in production; we reject http:// URLs.
  • Return 2xx within 10s. Anything else counts as a failure. If real processing takes longer, enqueue to a background job and return 200 immediately.
  • Dedupe on event.id. Retries and replays mean the same id can arrive multiple times. A unique constraint on a processed_events table is the simplest reliable pattern.
  • Watch the auto-disable threshold. After 50 consecutive failed deliveries we automatically disable the destination to protect your endpoint. Catch issues earlier by monitoring the Recent failures field on the destination row.
  • Rotate the signing secret if it leaks. Click Regenerate secret from the destination’s expanded row. The old secret stops working immediately, so deploy your config change in lockstep.

Common pitfalls

Signature mismatch on every request. You’re hashing the JSON-parsed body instead of the raw bytes. Read the raw request body before any framework middleware parses it (see the request.get_data() / express.raw() calls in the samples above). The 200 comes from your auth middleware, not your handler. If a reverse proxy or auth layer swallows the request and replies with its own 200, our worker sees success but your handler never runs. Check your access logs. Timeouts on big handlers. Don’t process the entire downstream pipeline inside the webhook handler. Enqueue and acknowledge. Clock drift. Our verification rejects timestamps more than 5 minutes off. If you keep failing right after a deploy, check NTP sync on your server.

Next steps

  • Webhooks reference — full event catalog, payload envelope, retry semantics, and security model.
  • Join the Discord if you hit something not covered here.