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.
{
"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.
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}), 200Receiver flow
| Step | What to do |
|---|---|
| 1 | Verify X-EarningsAPI-Signature with your webhook secret. |
| 2 | Deduplicate by X-EarningsAPI-Delivery-Id. |
| 3 | Check template_id and event_type. |
| 4 | Store data or run your internal workflow. |
| 5 | Return 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.