Resources

Webhooks

Configure an endpoint that receives Earnings API event deliveries, verifies signatures, and responds with a 2xx status after processing.

Delivery request

Earnings API sends each delivery as an HTTP POST to your saved endpoint URL.

Webhook endpoint must use HTTPS.

Hosted webhook delivery requires a public https:// URL.

localhost and private network URLs are not supported. Use an HTTPS tunnel for local testing.

POST https://your-domain.com/webhooks/earnings

Content-Type: application/json

User-Agent: EarningsAPI-Webhooks/1.0

X-EarningsAPI-Delivery-Id: {deliveryId}

X-EarningsAPI-Timestamp: {unixTimestamp}

X-EarningsAPI-Signature: sha256={hmac}

Webhook payload examples

Use template_id and event_type to route each delivery, then read the fields in data for the subscribed symbol.

Earnings schedule
{
  "id": "del_7a871a19a44926c2496db1ed39a997dc",
  "created_at": "2026-07-09T12:03:10Z",
  "template_id": "earnings_schedule",
  "event_type": "earnings_schedule.changed",
  "context": {
    "symbol": "ASML"
  },
  "data": {
    "date": "2026-07-15",
    "time": "time-pre-market",
    "epsEstimate": 7.98,
    "revenueEstimate": 10331810000,
    "eps": null,
    "revenue": null
  },
  "changed_fields": ["revenueEstimate"]
}

Signature verification

Use the webhook secret from your dashboard to verify the HMAC signature before trusting the body. Verify against the raw request body, then use the delivery id for idempotency.

expected = "sha256=" + HMAC_SHA256(secret, timestamp + rawBody)

Use the exact raw request body bytes. Do not parse JSON and stringify it again before verification.

Do not insert a separator between timestamp and rawBody; timestamp + "." + rawBody will not match.

Compare with the X-EarningsAPI-Signature value after handling the sha256= prefix consistently.

Receiver examplePython
import hashlib
import hmac
import os
from flask import Flask, jsonify, request

app = Flask(__name__)

def verify_earnings_api_webhook(raw_body, timestamp, signature, secret):
    signed_payload = timestamp.encode("utf-8") + raw_body
    expected = hmac.new(
        secret.encode("utf-8"),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()
    normalized_signature = (signature or "").removeprefix("sha256=")
    return hmac.compare_digest(normalized_signature, expected)

@app.post("/webhooks/earnings")
def earnings_api_webhook():
    raw_body = request.get_data()
    timestamp = request.headers.get("X-EarningsAPI-Timestamp", "")
    signature = request.headers.get("X-EarningsAPI-Signature", "")

    if not verify_earnings_api_webhook(
        raw_body,
        timestamp,
        signature,
        os.environ["EARNINGS_API_WEBHOOK_SECRET"],
    ):
        return jsonify({"error": "Invalid signature"}), 401

    delivery_id = request.headers.get("X-EarningsAPI-Delivery-Id")
    payload = request.get_json()
    # Use delivery_id for idempotency before running your workflow.

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

Receiver flow

StepWhat to do
1Verify X-EarningsAPI-Signature with your webhook secret.
2Deduplicate by X-EarningsAPI-Delivery-Id.
3Check template_id and event_type.
4Store data or run your internal workflow.
5Return any 2xx response after successful processing.

Delivery status

2xx response

Only a 2xx response is marked delivered with deliveredAt and the response status.

Redirect, non-2xx, or timeout

3xx redirects are not followed. 4xx, 5xx, and timeouts are treated as failed or retryable attempts.

How it fits together

webhook_deliveries decides who receives which template and symbol.

webhook_template_states stores the source data used to build payloads.

The delivery runner builds the payload right before POST, and your receiver verifies the signature before returning 2xx.

Quota and limits

Free tier includes 60 requests per minute, 100 requests per day, and 1,000 requests per month. Paid plans are built for production use with higher limits and a 300 requests per minute rate limit. Daily and monthly reset windows use New York time.

View usage