# Webhook Signatures (/en/webhooks/webhook-signatures)



import { Shield, Lock, AlertTriangle } from 'lucide-react';

Overview [#overview]

Every webhook request includes a signature in the header that you can verify to ensure the request genuinely came from Yolfi and wasn't tampered with during transit.

<Callout type="warning">
  Always verify webhook signatures before processing any webhook event. Failing to do so could allow attackers to send fake payment notifications to your system.
</Callout>

***

Why Verify Signatures? [#why-verify-signatures]

Signature verification provides two key security guarantees:

1. **Authenticity** - Confirms the webhook was sent by Yolfi, not an imposter
2. **Integrity** - Ensures the payload wasn't modified in transit
   Replay and deduplication are application concerns. Use the signed payload `id` with your idempotency pattern to avoid duplicate side effects. `X-Yolfi-Event-ID` mirrors the same value for routing convenience, but the header is not part of the HMAC.

<Callout type="info">
  `YOLFI_API_KEY` authenticates requests to the Yolfi management API. It must not be used as a webhook signing secret. Each webhook endpoint has its own `signingSecret` for `X-Yolfi-Signature` verification.
</Callout>

***

How It Works [#how-it-works]

Signature Generation [#signature-generation]

When we send a webhook, we:

1. Create a JSON payload with the event data
2. Generate an HMAC-SHA256 signature using that endpoint's signing secret
3. Encode the signature in Base64
4. Send it in the request headers

Headers [#headers]

Each webhook request includes these headers:

| Header              | Description                                 |
| :------------------ | :------------------------------------------ |
| `Content-Type`      | Always `application/json`                   |
| `X-Yolfi-Signature` | Base64-encoded HMAC-SHA256 signature        |
| `X-Yolfi-Event-ID`  | Convenience copy of the signed payload `id` |

Verification Process [#verification-process]

To verify a webhook signature:

<Steps>
  <Step title="Get the signature">
    Extract the `X-Yolfi-Signature` header from the request.
  </Step>

  <Step title="Get the endpoint secret">
    Retrieve the signing secret returned when this webhook endpoint was created or last rotated. Store each endpoint's secret separately.
  </Step>

  <Step title="Compute expected signature">
    Calculate HMAC-SHA256 of the raw request body using the endpoint signing secret.
  </Step>

  <Step title="Compare signatures">
    Use constant-time comparison to compare your computed signature with the one in the header.
  </Step>
</Steps>

***

Code Examples [#code-examples]

<Tabs items={["Node.js", "Python", "Go"]}>
  <Tab value="Node.js">
    ```javascript
    const crypto = require('crypto');

    function verifyWebhookSignature(payload, signature, webhookSecret) {
      if (!signature || !webhookSecret) {
        return false;
      }

      const expected = crypto
        .createHmac('sha256', webhookSecret)
        .update(payload, 'utf8')
        .digest('base64');

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

    // Express.js example
    app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = req.headers['x-yolfi-signature'];
      const payload = req.body.toString(); // Keep raw body as string
      const webhookSecret = process.env.YOLFI_WEBHOOK_SECRET;

      const isValid = verifyWebhookSignature(payload, signature, webhookSecret);

      if (!isValid) {
        return res.status(400).send('Invalid signature');
      }

      const event = JSON.parse(payload);
      console.log('Received webhook:', event.type);

      // Process the event
      res.status(200).send('OK');
    });
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import hmac
    import hashlib
    import base64
    import os

    def verify_signature(payload: str, signature: str, webhook_secret: str) -> bool:
        if not signature or not webhook_secret:
            return False

        expected = hmac.new(
            webhook_secret.encode('utf-8'),
            payload.encode('utf-8'),
            hashlib.sha256
        ).digest()

        return hmac.compare_digest(
            base64.b64decode(signature),
            expected
        )

    # Flask example
    @app.route('/webhook', methods=['POST'])
    def webhook():
        signature = request.headers.get('X-Yolfi-Signature')
        payload = request.get_data(as_text=True)
        webhook_secret = os.environ.get('YOLFI_WEBHOOK_SECRET')

        if not verify_signature(payload, signature, webhook_secret):
            return 'Invalid signature', 400

        event = request.get_json()
        print(f"Received webhook: {event['type']}")
        return 'OK', 200
    ```
  </Tab>

  <Tab value="Go">
    ```go
    import (
        "crypto/hmac"
        "crypto/sha256"
        "encoding/base64"
        "os"
    )

    func verifySignature(payload string, signature string, webhookSecret string) bool {
        if signature == "" || webhookSecret == "" {
            return false
        }

        mac := hmac.New(sha256.New, []byte(webhookSecret))
        mac.Write([]byte(payload))
        expected := mac.Sum(nil)

        sig, err := base64.StdEncoding.DecodeString(signature)
        if err != nil {
            return false
        }

        // hmac.Equal is constant-time.
        return hmac.Equal(expected, sig)
    }

    // Echo framework example
    e.POST("/webhook", func(c echo.Context) error {
        signature := c.Request().Header.Get("X-Yolfi-Signature")
        payload, _ := io.ReadAll(c.Request().Body)
        webhookSecret := os.Getenv("YOLFI_WEBHOOK_SECRET")

        if !verifySignature(string(payload), signature, webhookSecret) {
            return c.String(400, "Invalid signature")
        }

        var event map[string]interface{}
        json.Unmarshal(payload, &event)
        fmt.Printf("Received webhook: %v\n", event["type"])

        return c.String(200, "OK")
    })
    ```
  </Tab>
</Tabs>

***

Security Best Practices [#security-best-practices]

<Steps>
  <Step title="Use timing-safe comparison">
    Always use constant-time comparison functions (`crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python) to prevent timing attacks.
  </Step>

  <Step title="Store signing secrets securely">
    Keep endpoint signing secrets in environment variables or a secure secrets manager. Never commit them to version control, and never substitute the organization API key.
  </Step>

  <Step title="Coordinate rotation with your receiver">
    Save the secret returned by rotation and update your receiver immediately. Rotation applies to new deliveries and queued retries. Requests already in flight can still carry the previous signature; allow both secrets briefly while those requests finish, then remove the old secret.
  </Step>

  <Step title="Verify the entire payload">
    Compute the signature over the raw request body, not a parsed/re-serialized version.
  </Step>

  <Step title="Respond quickly">
    Verify the signature and return 200 OK immediately. Process the event asynchronously afterward.
  </Step>

  <Step title="Handle duplicates">
    Webhooks may be retried on failure. Design your handler to be idempotent and deduplicate using the signed payload `id`. If you use `X-Yolfi-Event-ID`, first require it to match the payload value.
  </Step>
</Steps>
