> ## Documentation Index
> Fetch the complete documentation index at: https://docs.daya.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Verification

> Verify Daya Stocks webhook authenticity using HMAC signatures

## Overview

Every Daya Stocks webhook includes an `X-Webhook-Signature` header carrying an HMAC-SHA256 signature of the raw request body with a `sha256=` prefix. Always verify this signature to confirm the request came from Daya.

<Warning>
  Never process an unverified webhook. Without verification, an attacker could post fake events to your endpoint.
</Warning>

## Signature Header

```
X-Webhook-Signature: sha256=a8f5f167f44f4964e6c998dee827110c447be52d40d67b6a60b78c1e3e01b7e8
```

The signing key is the 64-character hex secret returned when you create the webhook or rotate its secret. Store it securely; it is shown only at those two moments.

## Additional Headers

Daya sends these headers with every delivery:

| Header                | Description                      |
| --------------------- | -------------------------------- |
| `X-Webhook-Event`     | Event type (e.g. `order.filled`) |
| `X-Webhook-ID`        | Unique event identifier (UUID)   |
| `X-Webhook-Timestamp` | Event timestamp (RFC3339)        |
| `User-Agent`          | `Daya-Webhook/1.0`               |

## Verification Algorithm

<Steps>
  <Step title="Read the raw body">
    Capture the request body as raw bytes, before any JSON parsing.
  </Step>

  <Step title="Compute the HMAC">
    Compute HMAC-SHA256 over the raw body using your webhook secret, and hex-encode the result.
  </Step>

  <Step title="Strip the prefix">
    Remove the `sha256=` prefix from the `X-Webhook-Signature` header.
  </Step>

  <Step title="Compare in constant time">
    Compare your computed signature with the header value using a timing-safe comparison.
  </Step>
</Steps>

## Implementation Examples

<CodeGroup>
  ```javascript Node.js theme={"dark"}
  const crypto = require('crypto');

  function verifyWebhookSignature(rawBody, signatureHeader, secret) {
    const signature = (signatureHeader || '').replace('sha256=', '');
    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody, 'utf8')
      .digest('hex');

    try {
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expected)
      );
    } catch {
      return false;
    }
  }

  // Express.js: use the raw body, not the parsed JSON
  app.post('/webhooks/daya-stocks', express.raw({ type: 'application/json' }), (req, res) => {
    const rawBody = req.body.toString('utf8');
    if (!verifyWebhookSignature(rawBody, req.headers['x-webhook-signature'], process.env.DAYA_STOCKS_WEBHOOK_SECRET)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(rawBody);
    res.status(200).send('OK');
  });
  ```

  ```python Python theme={"dark"}
  import hmac
  import hashlib

  def verify_webhook_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
      signature = (signature_header or '').replace('sha256=', '')
      expected = hmac.new(
          secret.encode('utf-8'),
          raw_body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(signature, expected)

  # Flask: read the raw body, not request.json
  from flask import Flask, request, jsonify

  @app.route('/webhooks/daya-stocks', methods=['POST'])
  def handle_webhook():
      raw_body = request.get_data()
      if not verify_webhook_signature(raw_body, request.headers.get('X-Webhook-Signature', ''), DAYA_STOCKS_WEBHOOK_SECRET):
          return jsonify({'error': 'Invalid signature'}), 401

      event = request.get_json()
      return jsonify({'status': 'ok'}), 200
  ```

  ```go Go theme={"dark"}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "io"
      "net/http"
      "strings"
  )

  func verifyWebhookSignature(rawBody []byte, signatureHeader, secret string) bool {
      signature := strings.TrimPrefix(signatureHeader, "sha256=")
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(rawBody)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(signature), []byte(expected))
  }

  func handleWebhook(w http.ResponseWriter, r *http.Request) {
      rawBody, _ := io.ReadAll(r.Body)
      if !verifyWebhookSignature(rawBody, r.Header.Get("X-Webhook-Signature"), dayaStocksWebhookSecret) {
          http.Error(w, "Invalid signature", http.StatusUnauthorized)
          return
      }

      // Parse rawBody and handle the event...
      w.WriteHeader(http.StatusOK)
  }
  ```
</CodeGroup>

## Important Notes

<AccordionGroup>
  <Accordion title="Use the raw request body">
    Compute the HMAC over the exact bytes Daya sent. Re-serializing parsed JSON changes whitespace and key order, which breaks the signature.
  </Accordion>

  <Accordion title="Use timing-safe comparison">
    Plain string comparison leaks timing information. Use a constant-time comparison: `crypto.timingSafeEqual()` (Node.js), `hmac.compare_digest()` (Python), or `hmac.Equal()` (Go).
  </Accordion>

  <Accordion title="Rotate secrets safely">
    Rotation takes effect immediately and the new secret is shown only after rotation. Pause the webhook in the Dashboard, rotate and copy the secret, update your secret store and receiver, then re-enable delivery. Reconcile API state for the paused interval.
  </Accordion>

  <Accordion title="Keep secrets out of source control">
    Store the secret in an environment variable or secret manager. Use a different secret per environment.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhooks Overview" icon="webhook" href="/stocks/webhooks/overview">
    Events, delivery, and retries
  </Card>

  <Card title="Rotate a Secret" icon="rotate" href="https://dashboard.daya.co">
    Rotate signing secrets from your workspace's Webhooks page.
  </Card>
</CardGroup>
