Webhooks
Real-time payment notifications with HMAC-SHA256 security
Webhooks
Moosyl webhooks deliver real-time notifications for payment events. Every webhook is secured with HMAC-SHA256 signatures to ensure authenticity and integrity.
Events & Headers
Supported Events
| Event | Description |
|---|---|
payment-request-created | New payment request created |
payment-request-updated | Payment request status changed |
payment-created | Payment successfully processed |
payment-updated | Payment status updated |
Request Headers
| Header | Description |
|---|---|
x-webhook-signature | HMAC-SHA256 signature (sha256=<hex>) |
x-webhook-event | Event type that triggered the webhook |
content-type | Always application/json |
user-agent | Moosyl-Webhook/1.0 |
Payload Structure
{
"event": "payment-created",
"data": {
"id": "uuid",
"amount": 1000,
"currency": "MRU",
"status": "completed",
// ... event-specific fields
}
}Signature Verification
How It Works
Webhooks are signed using HMAC-SHA256 with your webhook secret as the key and the raw request body as the message.
Verification Steps
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const receivedHex = signature.slice(7); // Remove 'sha256=' prefix
return crypto.timingSafeEqual(
Buffer.from(receivedHex, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}Security Requirements
- Always verify signatures before processing
- Use timing-safe comparison to prevent timing attacks
- Validate event types against allowed list
- Store secrets securely (never in client-side code)
Implementation
Node.js/Express
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const event = req.headers['x-webhook-event'];
// Verify signature
if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process event
const payload = JSON.parse(req.body);
handleWebhookEvent(event, payload.data);
res.json({ received: true });
});Python/Flask
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('x-webhook-signature')
event = request.headers.get('x-webhook-event')
# Verify signature
if not verify_webhook_signature(request.data, signature, os.environ['WEBHOOK_SECRET']):
return jsonify({'error': 'Invalid signature'}), 401
# Process event
payload = json.loads(request.data)
handle_webhook_event(event, payload['data'])
return jsonify({'received': True}), 200PHP
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$event = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';
$payload = file_get_contents('php://input');
// Verify signature
if (!verifyWebhookSignature($payload, $signature, $_ENV['WEBHOOK_SECRET'])) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process event
$data = json_decode($payload, true);
handleWebhookEvent($event, $data['data']);
http_response_code(200);
echo json_encode(['received' => true]);Configuration
Setting Up Webhooks
- Go to dashboard → Webhooks section
- Add webhook URL → Your endpoint URL
- Select events → Choose which events to receive
- Set webhook secret → Generate secure secret for verification
Retry Policy
- Initial retry: 1 minute
- Backoff: Exponential (2, 4, 8, 16, 32 minutes)
- Max attempts: 6 retries
- Total window: ~1 hour
Testing
Local Development
Use ngrok to expose local server to the internet. Use ngrok URL in dashboard webhook settings.
ngrok http 3000Testing Tools
- Webhook.site: Free temporary endpoints
- RequestBin: Capture and inspect requests
- Postman: Manual endpoint testing
Common Issues
Signature Verification Fails
Causes:
- Wrong webhook secret
- Using parsed body instead of raw payload
- Incorrect HMAC-SHA256 implementation
Fix:
// Use raw body, not parsed JSON
app.use(express.raw({ type: 'application/json' }));Webhook Not Received
Check:
- Endpoint is publicly accessible
- Returns 200 status code
- HTTPS enabled
- Firewall allows POST requests
Duplicate Events
Webhooks may be delivered multiple times due to timeouts, retries, or other network issues. You need to handle duplicates on your own.
Timeout Errors
Fix:
- Respond within 30 seconds
- Process heavy operations asynchronously
- Use background jobs for complex logic
Best Practices
- Always verify signatures before processing
- Implement idempotency for duplicate handling
- Respond quickly (200 status within 30s)
- Use HTTPS for all webhook endpoints
- Log all webhook attempts for debugging
- Monitor delivery success in dashboard
- Test thoroughly before production deployment
Support
Need help with webhooks?
- Dashboard logs: Check delivery history and errors
- Email support: support@moosyl.com
- FAQ: Common webhook questions
- API docs: Full webhook reference