Webhooks - Real-time Financial Data Updates

Get real-time notifications the moment financial data updates. Webhooks deliver clean fundamentals to your app within minutes of disclosure.

What are Webhooks?

Webhooks allow you to receive real-time notifications when financial data is updated for companies in your coverage. Instead of polling our API repeatedly, you can configure webhook endpoints that we'll call whenever new financial statements, ratios, or other data becomes available.

Setup and Management

Adding a Webhook

  1. Access Organization Dashboard: Navigate to your organization dashboard through the "Manage API Keys" link
  2. Configure Webhook URL: Add your webhook endpoint URL that can receive HTTP POST requests
  3. Get Your Signing Secret: Copy your signing secret from the dashboard to verify webhook authenticity
  4. Enable/Disable: Toggle webhooks on or off as needed through the dashboard

Webhook URL Requirements:

Your webhook endpoint must:

  • Accept HTTP POST requests
  • Return HTTP status codes 200-299 for successful processing
  • Be publicly accessible (no localhost or private network URLs)
  • Respond within 20 seconds to avoid timeouts

Managing Webhooks

All webhook management is done through your organization dashboard:

  • View Active Webhooks: See all configured webhook endpoints
  • Enable/Disable: Turn webhooks on or off without deleting the configuration
  • Update URLs: Modify webhook endpoints as needed
  • Monitor Delivery: Track webhook delivery status and failures

Webhook Payload

When financial data or company information is updated, we'll send a POST request to your configured webhook URL with the following JSON payload:

{
  "event_type": "financial_data_updated",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "company_key": "NASDAQ_AAPL",
  "statement_type": "income_statement",
  "file_type": "financial_data_as_reported",
  "metadata": {
    "source": "fiscal",
    "version": "1.0"
  }
}

Payload Fields

FieldTypeDescription
event_typestringType of event: company_updated, company_deleted, financial_data_updated, or financial_data_deleted
timestampstringISO 8601 timestamp when the data was updated
company_keystringCompany identifier in EXCHANGE_TICKER format (MIC_TICKER outside of US/CA)
statement_typestringType of financial statement updated (if applicable, otherwise null)
file_typestringSpecific data type that was updated
metadataobjectAdditional metadata about the update

Event Types

The event_type field indicates what type of change occurred:

  • financial_data_updated - Financial data was created or updated (most common)
  • financial_data_deleted - Financial data was deleted or removed
  • company_updated - Company profile or metadata was created or updated
  • company_deleted - Company was removed from the system

File Types

The file_type field indicates what type of data was updated:

  • financial_data_as_reported - any updates to as-reported or standardized financial data
  • template_metric_mapping - any additions, removals or renamings of standardized metrics
  • eps_and_waso - any updates to earnings per share and weighted average shares outstanding
  • shares_outstanding - any updates to shares outstanding data
  • adjusted_numbers - any updates to adjusted metrics data
  • filings - any updates to filing information data
  • segments_and_kpis - any updates to company segments and key performance indicators data

Statement Types

When applicable, the statement_type field will contain:

  • income_statement - Income statement data
  • balance_sheet - Balance sheet data
  • cash_flow_statement - Cash flow statement data

Reliability and Retries

Automatic Retries

Our webhook system includes automatic retry logic:

  • Maximum Retries: 2 additional attempts after the initial request
  • Retry Delay: 5 seconds between retry attempts
  • Timeout: 20 seconds per request
  • Total Window: Up to 30 seconds for all attempts

Error Handling

We consider a webhook delivery successful when your endpoint returns HTTP status codes 200-299. Other status codes or timeouts will trigger retries.

Best Practices

  1. Idempotency: Handle duplicate webhook deliveries gracefully using the timestamp and company_key fields
  2. Fast Response: Acknowledge webhooks quickly and process data asynchronously if needed
  3. Error Logging: Log webhook failures for debugging
  4. Validation: Verify webhook payloads match the expected format

Verifying Webhook Signatures

To ensure webhook requests are genuinely from Fiscal and haven't been tampered with, we sign all webhooks using HMAC-SHA256. Your signing secret is available in your organization dashboard.

How Signature Verification Works

Each webhook request includes two headers, (the header names referring to our update engine that we've named Atlas):

  • X-Atlas-Signature: The HMAC-SHA256 signature in the format sha256={hex_signature}
  • X-Atlas-Timestamp: Unix timestamp (seconds) when the webhook was sent

The signature is computed using:

  1. Your signing secret (hex-encoded)
  2. A message string in the format: {timestamp}.{json_payload}
  3. The JSON payload is serialized with sorted keys and no spaces: {"company_key":"NASDAQ_AAPL","event_type":"financial_data_updated","file_type":"financial_data_as_reported",...}

Implementation Examples

import hmac
import hashlib
import json
import time

def verify_webhook_signature(
    payload_body: bytes,
    signature_header: str,
    timestamp_header: str,
    signing_secret: str,
    tolerance_seconds: int = 300
) -> bool:
    """
    Verify the webhook signature from Fiscal.

    Args:
        payload_body: Raw request body as bytes
        signature_header: Value of X-Atlas-Signature header
        timestamp_header: Value of X-Atlas-Timestamp header
        signing_secret: Your signing secret from the dashboard (hex string)
        tolerance_seconds: Maximum age of webhook (default 5 minutes)

    Returns:
        True if signature is valid, False otherwise
    """
    # Check timestamp to prevent replay attacks
    try:
        webhook_timestamp = int(timestamp_header)
        current_timestamp = int(time.time())

        if abs(current_timestamp - webhook_timestamp) > tolerance_seconds:
            return False  # Webhook is too old or from the future
    except (ValueError, TypeError):
        return False

    # Parse the signature header (format: "sha256=<hex>")
    if not signature_header.startswith("sha256="):
        return False

    expected_signature = signature_header[7:]  # Remove "sha256=" prefix

    # Reconstruct the signed message
    # Important: Use the raw body bytes to preserve exact JSON formatting
    signed_message = f"{timestamp_header}.{payload_body.decode('utf-8')}"

    # Compute HMAC-SHA256 signature
    secret_bytes = bytes.fromhex(signing_secret)
    computed_signature = hmac.new(
        key=secret_bytes,
        msg=signed_message.encode('utf-8'),
        digestmod=hashlib.sha256
    ).hexdigest()

    # Constant-time comparison to prevent timing attacks
    return hmac.compare_digest(computed_signature, expected_signature)


# Example usage in a Flask endpoint
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/fiscal', methods=['POST'])
def handle_fiscal_webhook():
    signature = request.headers.get('X-Atlas-Signature')
    timestamp = request.headers.get('X-Atlas-Timestamp')

    # Your signing secret from the Fiscal dashboard
    signing_secret = 'your_signing_secret_hex_string_here'

    if not verify_webhook_signature(
        payload_body=request.get_data(),
        signature_header=signature,
        timestamp_header=timestamp,
        signing_secret=signing_secret
    ):
        return jsonify({'error': 'Invalid signature'}), 401

    # Process the webhook payload
    payload = request.get_json()
    event_type = payload.get('event_type')
    company_key = payload.get('company_key')
    file_type = payload.get('file_type')

    # Your business logic here
    if event_type == 'financial_data_updated':
        print(f"Financial data updated for {company_key}: {file_type}")
    elif event_type == 'financial_data_deleted':
        print(f"Financial data deleted for {company_key}: {file_type}")
    elif event_type == 'company_updated':
        print(f"Company profile updated for {company_key}")
    elif event_type == 'company_deleted':
        print(f"Company removed: {company_key}")

    return jsonify({'status': 'success'}), 200

Important Security Notes

Signature Verification Best Practices:

  • Always verify signatures: Check the signature on every webhook request before processing

  • Use raw request body: Verify the signature against the raw request body before parsing JSON

  • Check timestamp: Reject webhooks older than 5 minutes to prevent replay attacks

  • Use constant-time comparison: Prevent timing attacks when comparing signatures

  • Keep secrets secure: Store signing secrets in environment variables or secret managers

  • Rotate secrets regularly: Update your signing secret periodically from the dashboard

Testing Signature Verification

You can test your signature verification implementation by:

  1. Creating a test webhook endpoint with logging enabled
  2. Waiting for webhook events to be delivered to your endpoint
  3. Logging the signature components to verify your implementation matches

If signature verification fails, check:

  • The raw request body is used (not parsed JSON)
  • The signing secret is correct and properly hex-decoded
  • The timestamp header is included in the signed message
  • The JSON payload format matches exactly (sorted keys, no spaces)

Security Considerations

Security Best Practices:

  • HTTPS Only: Always use HTTPS endpoints for webhook URLs

  • Signature Verification: Always verify webhook signatures before processing

  • Validation: Validate incoming payloads match expected format

  • Rate Limiting: Implement rate limiting on your webhook endpoints

  • Monitoring: Monitor for unusual webhook activity