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 three key security guarantees:
- Authenticity - Confirms the webhook was sent by Yolfi, not an imposter
- Integrity - Ensures the payload wasn't modified in transit
- Replay Protection - Prevents attackers from reusing old valid webhooks
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 your API key as the 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 | Unique event identifier |
Verification Process
To verify a webhook signature:
Extract the X-Yolfi-Signature header from the request.
Retrieve your API key from your organization settings.
Calculate HMAC-SHA256 of the raw request body using your API key.
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, apiKey) {
if (!signature || !apiKey) {
return false;
}
const expected = crypto
.createHmac('sha256', apiKey)
.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 apiKey = process.env.YOLFI_API_KEY;
const isValid = verifyWebhookSignature(payload, signature, apiKey);
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, api_key: str) -> bool:
if not signature or not api_key:
return False
expected = hmac.new(
api_key.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)
api_key = os.environ.get('YOLFI_API_KEY')
if not verify_signature(payload, signature, api_key):
return 'Invalid signature', 400
event = request.get_json()
print(f"Received webhook: {event['type']}")
return 'OK', 200import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"os"
)
func verifySignature(payload string, signature string, apiKey string) bool {
if signature == "" || apiKey == "" {
return false
}
expected := hmac.New(sha256.New, []byte(apiKey))
expected.Write([]byte(payload))
expectedSig, _ := base64.StdEncoding.DecodeString(expected.Sum(nil))
sig, _ := base64.StdEncoding.DecodeString(signature)
return subtle.ConstantTimeCompare(expectedSig, sig) == 1
}
// 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)
apiKey := os.Getenv("YOLFI_API_KEY")
if !verifySignature(string(payload), signature, apiKey) {
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 your API keys in environment variables or a secure secrets manager. Never commit them to version control.
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 - the same event should not cause duplicate actions.