Webhooks
Webhooks allow you to receive real-time notifications when events occur on Raffaly, such as when a raffle draw is completed or an audit record is published.
MVP Status: Webhook infrastructure is ready. Contact support@raffaly.com to register webhook subscriptions during the MVP phase.
Available Events
draw.completed
Triggered when a raffle draw is completed and a winner is selected.
Payload Example:
{
"event": "draw.completed",
"raffle_id": "550e8400-e29b-41d4-a716-446655440000",
"draw_id": "660e8400-e29b-41d4-a716-446655440001",
"winner_entry_id": "12345",
"audit_url": "https://raffaly.com/api/v1/raffles/550e8400.../audit",
"timestamp": "2025-11-06T20:32:01+00:00"
}
audit.published
Triggered when a draw audit record is published and available via the API.
Payload Example:
{
"event": "audit.published",
"raffle_id": "550e8400-e29b-41d4-a716-446655440000",
"audit_url": "https://raffaly.com/api/v1/raffles/550e8400.../audit",
"timestamp": "2025-11-06T20:32:01+00:00"
}
Security
All webhook requests are signed using HMAC-SHA256 to ensure they originate from Raffaly. You must verify the signature before processing webhook events.
Signature Header Format
X-Raffaly-Signature: t=1699305120, v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Where:
t= Unix timestamp when the signature was generatedv1= HMAC-SHA256 signature oftimestamp.payloadusing your webhook secret
Verification Code (PHP)
function verifyWebhook($payload, $signatureHeader, $secret) {
// Parse signature header
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = explode('=', trim($part), 2);
$parts[$key] = $value;
}
if (!isset($parts['t']) || !isset($parts['v1'])) {
return false;
}
$timestamp = $parts['t'];
$receivedSignature = $parts['v1'];
// Check timestamp tolerance (5 minutes)
if (abs(time() - $timestamp) > 300) {
return false;
}
// Compute expected signature
$expectedSignature = hash_hmac('sha256', "$timestamp.$payload", $secret);
// Constant-time comparison
return hash_equals($expectedSignature, $receivedSignature);
}
// Usage
$payload = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_RAFFALY_SIGNATURE'];
$secret = 'your_webhook_secret_here';
if (verifyWebhook($payload, $signatureHeader, $secret)) {
$event = json_decode($payload, true);
// Process event
} else {
http_response_code(401);
exit('Invalid signature');
}
Verification Code (Node.js)
const crypto = require('crypto');
function verifyWebhook(payload, signatureHeader, secret) {
const parts = {};
signatureHeader.split(',').forEach(part => {
const [key, value] = part.trim().split('=');
parts[key] = value;
});
if (!parts.t || !parts.v1) {
return false;
}
const timestamp = parts.t;
const receivedSignature = parts.v1;
// Check timestamp tolerance (5 minutes)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false;
}
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}
// Express.js example
app.post('/webhooks/raffaly', express.raw({type: 'application/json'}), (req, res) => {
const payload = req.body.toString();
const signatureHeader = req.headers['x-raffaly-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (verifyWebhook(payload, signatureHeader, secret)) {
const event = JSON.parse(payload);
// Process event
res.sendStatus(200);
} else {
res.sendStatus(401);
}
});
Best Practices
- Always verify signatures before processing webhook events
- Check timestamp tolerance to prevent replay attacks (5 minute window recommended)
- Respond quickly - Return 200 OK within 10 seconds to avoid timeouts
- Handle idempotency - Same event may be delivered multiple times, use event IDs to deduplicate
- Process asynchronously - Queue events for processing to avoid blocking the webhook endpoint
- Use HTTPS - Your webhook endpoint must use HTTPS in production
Delivery & Retries
MVP behavior: Webhooks are delivered once (fire-and-forget) with a 10-second timeout. After 10 consecutive failures, subscriptions are automatically disabled.
Future versions will support automatic retries with exponential backoff for failed deliveries.
Testing
Use tools like webhook.site or ngrok to test webhook delivery during development.
Need Help? Contact support@raffaly.com to register webhooks or for integration assistance.