| API Documentation

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 generated
  • v1 = HMAC-SHA256 signature of timestamp.payload using 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.