Skip to main content
When you receive a webhook from Exa, you should verify that it came from us to ensure the integrity and authenticity of the data. Exa signs all webhook payloads with a secret key that’s unique to your webhook endpoint.

How Webhook Signatures Work

Exa uses HMAC SHA256 to sign webhook payloads. The signature is included in the Exa-Signature header, which contains:
  • A timestamp (t=) indicating when the webhook was sent
  • One or more signatures (v1=) computed using the timestamp and payload
The signature format looks like this:
Exa-Signature: t=1234567890,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Verification Process

To verify a webhook signature:
  1. Extract the timestamp and signatures from the Exa-Signature header
  2. Create the signed payload by concatenating the timestamp, a period, and the raw request body
  3. Compute the expected signature using HMAC SHA256 with your webhook secret
  4. Compare your computed signature with the provided signatures
  • Python
  • JavaScript/Node.js
  • Java
python
import hmac
import hashlib
import time

def verify_webhook_signature(payload, signature_header, webhook_secret):
    """
    Verify the signature of a webhook payload.

    Args:
        payload (str): The raw request body as a string
        signature_header (str): The Exa-Signature header value
        webhook_secret (str): Your webhook secret

    Returns:
        bool: True if signature is valid, False otherwise
    """
    try:
        # Parse the signature header
        pairs = [pair.split('=', 1) for pair in signature_header.split(',')]
        timestamp = None
        signatures = []

        for key, value in pairs:
            if key == 't':
                timestamp = value
            elif key == 'v1':
                signatures.append(value)

        if not timestamp or not signatures:
            return False

        # Optional: Check if timestamp is recent (within 5 minutes)
        current_time = int(time.time())
        if abs(current_time - int(timestamp)) > 300:
            print("Warning: Webhook timestamp is more than 5 minutes old")

        # Create the signed payload
        signed_payload = f"{timestamp}.{payload}"

        # Compute the expected signature
        expected_signature = hmac.new(
            webhook_secret.encode('utf-8'),
            signed_payload.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()

        # Compare with provided signatures
        return any(hmac.compare_digest(expected_signature, sig) for sig in signatures)

    except Exception as e:
        print(f"Error verifying signature: {e}")
        return False

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

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    # Get the raw payload and signature
    payload = request.get_data(as_text=True)
    signature_header = request.headers.get('Exa-Signature', '')
    webhook_secret = os.environ.get('WEBHOOK_SECRET')

    # Verify the signature
    if not verify_webhook_signature(payload, signature_header, webhook_secret):
        return jsonify({'error': 'Invalid signature'}), 400

    # Process the webhook
    webhook_data = request.get_json()
    print(f"Received {webhook_data['type']} event")

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


Security Best Practices

Following these practices will help ensure your webhook implementation is secure and robust:
  • Always Verify Signatures - Never process webhook data without first verifying the signature. This prevents attackers from sending fake webhooks to your endpoint.
  • Use Timing-Safe Comparison - When comparing signatures, use functions like hmac.compare_digest() in Python or crypto.timingSafeEqual() in Node.js to prevent timing attacks.
  • Check Timestamp Freshness - Consider rejecting webhooks with timestamps that are too old (e.g., older than 5 minutes) to prevent replay attacks.
  • Store Secrets Securely - Store your webhook secrets in environment variables or a secure secret management system. Never hardcode them in your application. Important: The webhook secret is only returned when you create a webhook - make sure to save it securely as it cannot be retrieved later.
  • Use HTTPS - Always use HTTPS endpoints for your webhooks to ensure the data is encrypted in transit.


Troubleshooting

Invalid Signature Errors

If you’re getting signature verification failures:
  1. Check the raw payload: Make sure you’re using the raw request body, not a parsed JSON object
  2. Verify the secret: Ensure you’re using the correct webhook secret from when the webhook was created
  3. Check header parsing: Make sure you’re correctly extracting the timestamp and signatures from the header
  4. Encoding issues: Ensure consistent UTF-8 encoding throughout the verification process

Testing Signatures Locally

You can test your signature verification logic using the webhook secret and a sample payload:
python
# Test with a known payload and signature
test_payload = '{"type":"webset.created","data":{"id":"ws_test"}}'
test_timestamp = "1234567890"
test_secret = "your_webhook_secret"

# Create test signature
import hmac
import hashlib

signed_payload = f"{test_timestamp}.{test_payload}"
test_signature = hmac.new(
    test_secret.encode('utf-8'),
    signed_payload.encode('utf-8'),
    hashlib.sha256
).hexdigest()

test_header = f"t={test_timestamp},v1={test_signature}"

# Verify it works
is_valid = verify_webhook_signature(test_payload, test_header, test_secret)
print(f"Test signature valid: {is_valid}")  # Should print True


What’s Next?

I