Webhook Verification
Always verify webhook signatures to ensure requests are from Pixlpay.
Why Verify?
Without verification, attackers could send fake webhook payloads to your endpoint. Signature verification ensures the request actually came from Pixlpay.
How Verification Works
- Pixlpay signs each webhook with your secret using HMAC-SHA256
- The signature is sent in the
X-Pixlpay-Signatureheader - You compute the signature using the raw request body
- Compare your computed signature with the header value
Verification Code Examples
PHP
php
function verifyWebhook(string $payload, string $signature, string $secret): bool
{
$computed = hash_hmac('sha256', $payload, $secret);
return hash_equals($computed, $signature);
}
// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PIXLPAY_SIGNATURE'] ?? '';
$secret = getenv('PIXLPAY_WEBHOOK_SECRET');
if (!verifyWebhook($payload, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($payload, true);
// Process the event...Node.js
javascript
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(signature)
);
}
// Express.js example
app.post('/webhooks/pixlpay', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-pixlpay-signature'];
const secret = process.env.PIXLPAY_WEBHOOK_SECRET;
if (!verifyWebhook(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process the event...
res.sendStatus(200);
});Python
python
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/pixlpay', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Pixlpay-Signature', '')
secret = os.environ['PIXLPAY_WEBHOOK_SECRET']
if not verify_webhook(request.data, signature, secret):
abort(401, 'Invalid signature')
event = request.json
# Process the event...
return '', 200Go
go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func verifyWebhook(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
computed := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computed), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-Pixlpay-Signature")
secret := os.Getenv("PIXLPAY_WEBHOOK_SECRET")
if !verifyWebhook(payload, signature, secret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the event...
w.WriteHeader(http.StatusOK)
}Best Practices
- Always verify - Never skip signature verification
- Use timing-safe comparison - Prevents timing attacks
- Use raw body - Parse JSON after verification
- Secure your secret - Use environment variables
- Log failures - Track invalid signature attempts
Troubleshooting
Signature Mismatch
- Ensure you're using the raw request body (not parsed JSON)
- Check your secret is correct
- Verify no middleware modified the body
- Check for encoding issues
Missing Header
- Ensure your framework passes through all headers
- Header name is
X-Pixlpay-Signature(case-insensitive)
Testing
Use the webhook test feature to verify your implementation:
bash
curl -X POST "https://yourstore.pixlpay.net/api/external/v1/webhooks/1/test" \
-H "Authorization: Bearer YOUR_TOKEN"