YAPL Documentation
User GuidesWebhooks

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

  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:

HeaderDescriptionExample
X-YAPL-Signature-256HMAC-SHA256 signaturesha256=a1b2c3d4...
X-YAPL-EventThe event typeproject.created.v1
X-YAPL-Delivery-IDUnique delivery identifierdel_abc123
X-YAPL-TimestampWhen 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', 200

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):

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.

Was this page helpful?

On this page