# Verifying Signatures (https://yapl.app/en/docs/user-guides/webhooks/verifying-signatures)

# Verifying Webhook Signatures

Every webhook delivery from YAPL includes a cryptographic signature so your receiving service can verify the request is authentic and hasn't been tampered with. This prevents attackers from sending fake webhook payloads to your endpoint.

## How It Works

1. When you create a webhook, YAPL generates a unique **signing secret**
2. For each delivery, YAPL computes an HMAC-SHA256 signature using your secret and the payload
3. The signature is included in the `X-YAPL-Signature-256` request header
4. Your service recomputes the signature using the same secret and compares them

If the signatures match, the delivery is authentic.

## Delivery Headers

Every webhook delivery includes these headers:

| Header | Description | Example |
|--------|-------------|---------|
| `X-YAPL-Signature-256` | HMAC-SHA256 signature | `sha256=a1b2c3d4...` |
| `X-YAPL-Event` | The event type | `project.created.v1` |
| `X-YAPL-Delivery-ID` | Unique delivery identifier | `del_abc123` |
| `X-YAPL-Timestamp` | When the signature was created (ISO 8601) | `2026-03-25T10:30:00.000Z` |

## Signature Computation

The signature is computed over the timestamp concatenated with the raw JSON body:

```
signable_payload = timestamp + "." + raw_json_body
signature = "sha256=" + HMAC-SHA256(secret, signable_payload)
```

The timestamp is included to prevent replay attacks — even if an attacker captures a valid delivery, they can't reuse it because the timestamp will be different.

## Verification Examples

### Node.js

```javascript
const crypto = require('crypto');

function verifyWebhook(secret, signature, timestamp, body) {
  const signablePayload = `${timestamp}.${body}`;
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(signablePayload, 'utf8');
  const expected = `sha256=${hmac.digest('hex')}`;

  // Use timing-safe comparison to prevent timing attacks
  if (signature.length !== expected.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your endpoint handler:
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-yapl-signature-256'];
  const timestamp = req.headers['x-yapl-timestamp'];
  const body = req.rawBody; // Raw request body as string

  if (!verifyWebhook(YOUR_SECRET, signature, timestamp, body)) {
    return res.status(401).send('Invalid signature');
  }

  // Process the webhook
  const event = JSON.parse(body);
  console.log(`Received ${event.type}:`, event.data);
  res.status(200).send('OK');
});
```

### Python

```python
import hmac
import hashlib

def verify_webhook(secret, signature, timestamp, body):
    signable_payload = f"{timestamp}.{body}"
    expected = "sha256=" + hmac.new(
        secret.encode('utf-8'),
        signable_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

# In your endpoint handler (Flask example):
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-YAPL-Signature-256')
    timestamp = request.headers.get('X-YAPL-Timestamp')
    body = request.get_data(as_text=True)

    if not verify_webhook(YOUR_SECRET, signature, timestamp, body):
        return 'Invalid signature', 401

    event = request.get_json()
    print(f"Received {event['type']}: {event['data']}")
    return 'OK', 200
```

### Go

```go
package main

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

func verifyWebhook(secret, signature, timestamp, body string) bool {
    signablePayload := fmt.Sprintf("%s.%s", timestamp, body)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(signablePayload))
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(signature), []byte(expected))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-YAPL-Signature-256")
    timestamp := r.Header.Get("X-YAPL-Timestamp")
    body, _ := io.ReadAll(r.Body)

    if !verifyWebhook(yourSecret, signature, timestamp, string(body)) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    // Process the webhook
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}
```

## Security Recommendations

### Always verify signatures

Never skip signature verification, even for testing. Unverified endpoints are vulnerable to spoofed requests.

### Use timing-safe comparison

Always use constant-time comparison functions (like `crypto.timingSafeEqual` in Node.js or `hmac.compare_digest` in Python) to prevent timing attacks.

### Check the timestamp

To prevent replay attacks, verify that the `X-YAPL-Timestamp` is recent (e.g., within the last 5 minutes):

```javascript
const deliveryTime = new Date(timestamp).getTime();
const now = Date.now();
const fiveMinutes = 5 * 60 * 1000;

if (Math.abs(now - deliveryTime) > fiveMinutes) {
  return res.status(401).send('Delivery too old');
}
```

### Store secrets securely

- Never hardcode secrets in source code
- Use environment variables or a secrets manager
- Rotate secrets by creating a new webhook if you suspect compromise

### Use HTTPS

Your receiving endpoint should always use HTTPS to protect the webhook payload in transit. YAPL requires HTTPS for production webhook URLs.

## Related Topics

- [Creating Webhooks](/docs/user-guides/webhooks/creating-webhooks) — Set up endpoints and get your signing secret
- [Event Types](/docs/user-guides/webhooks/event-types) — Events that trigger deliveries
- [Delivery Logs](/docs/user-guides/webhooks/delivery-logs) — Monitor delivery status