Webhook Signatures
Learn how to verify webhook authenticity and protect against spoofing attacks
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.
Always verify webhook signatures before processing any webhook event. Failing to do so could allow attackers to send fake payment notifications to your system.
Why Verify Signatures?
Signature verification provides two key security guarantees:
- Authenticity - Confirms the webhook was sent by Yolfi, not an imposter
- Integrity - Ensures the payload wasn't modified in transit
Replay and deduplication are application concerns. Use the signed payload
idwith your idempotency pattern to avoid duplicate side effects.X-Yolfi-Event-IDmirrors the same value for routing convenience, but the header is not part of the HMAC.
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.
How It Works
Signature Generation
When we send a webhook, we:
- Create a JSON payload with the event data
- Generate an HMAC-SHA256 signature using that endpoint's signing secret
- Encode the signature in Base64
- Send it in the request 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
To verify a webhook signature:
Extract the X-Yolfi-Signature header from the request.
Retrieve the signing secret returned when this webhook endpoint was created or last rotated. Store each endpoint's secret separately.
Calculate HMAC-SHA256 of the raw request body using the endpoint signing secret.
Use constant-time comparison to compare your computed signature with the one in the header.
Code Examples
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');
});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', 200import (
"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")
})Security Best Practices
Always use constant-time comparison functions (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks.
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.
Rotation affects new deliveries. A delivery queued before rotation can still be retried with the previous secret, so accept both secrets until pre-rotation deliveries have drained.
Compute the signature over the raw request body, not a parsed/re-serialized version.
Verify the signature and return 200 OK immediately. Process the event asynchronously afterward.
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.