Verifying Signatures
Validate that webhook deliveries genuinely came from YAPL using HMAC-SHA256 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
- When you create a webhook, YAPL generates a unique signing secret
- For each delivery, YAPL computes an HMAC-SHA256 signature using your secret and the payload
- The signature is included in the
X-YAPL-Signature-256request header - 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
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
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', 200Go
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):
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 — Set up endpoints and get your signing secret
- Event Types — Events that trigger deliveries
- Delivery Logs — Monitor delivery status
Was this page helpful?