IllyVoIPdevelopers

Webhooks

Signature verification & receiver

Verify the raw request body and persist each event before acknowledging delivery.

The example uses PHP 8 and PDO SQLite with an absolute Unix-style inbox path. It verifies and stores events; your separate worker must implement the business action.

Requires PHP 8 with PDO SQLite. Set ILLYVOIP_WEBHOOK_SECRET and an absolute ILLYVOIP_WEBHOOK_INBOX file path outside your public web directory, writable only by your application. Use one shared durable inbox if you run multiple receiver instances.

How to verify

Code example
signed_payload = timestamp + "." + raw_request_body
expected = HMAC_SHA256(webhook_secret, signed_payload)
compare expected with v1 from Illyvoip-Signature

Minimal PHP handler

This PHP example verifies each delivery and stores it in a durable inbox. Process stored events separately; the example does not perform your business action.

Code example
<?php
// PHP 8+, PDO SQLite. Keep the inbox outside your public web directory.
// Set ILLYVOIP_WEBHOOK_SECRET and ILLYVOIP_WEBHOOK_INBOX (an absolute file path).
function verifyIllyvoipEvent(string $raw, string $header, string $secret, int $now): array
{
    if ($secret === '' || !preg_match('/^t=(\d{1,12}),v1=([a-f0-9]{64})$/D', trim($header), $m)) {
        throw new RuntimeException('Invalid signature header');
    }
    $timestamp = (int) $m[1];
    // Recommended receiver tolerance. Signatures are refreshed on delivery retries.
    if ($timestamp < $now - 300 || $timestamp > $now + 60) {
        throw new RuntimeException('Signature timestamp outside tolerance');
    }
    $expected = hash_hmac('sha256', $m[1] . '.' . $raw, $secret);
    if (!hash_equals($expected, $m[2])) {
        throw new RuntimeException('Invalid signature');
    }
    $event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
    if (!is_array($event) || !is_string($event['id'] ?? null) || $event['id'] === '') {
        throw new RuntimeException('Missing event ID');
    }
    return $event;
}

$secret = (string) getenv('ILLYVOIP_WEBHOOK_SECRET');
$inbox = (string) getenv('ILLYVOIP_WEBHOOK_INBOX');
if ($secret === '' || $inbox === '' || $inbox[0] !== '/') {
    http_response_code(503); exit('Receiver is not configured');
}
try {
    $raw = file_get_contents('php://input');
    $event = verifyIllyvoipEvent($raw, $_SERVER['HTTP_ILLYVOIP_SIGNATURE'] ?? '', $secret, time());
} catch (Throwable $e) {
    http_response_code(401); exit('Invalid webhook');
}
try {
    $db = new PDO('sqlite:' . $inbox, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $db->exec('PRAGMA busy_timeout=3000');
    $db->exec('CREATE TABLE IF NOT EXISTS webhook_inbox (event_id TEXT PRIMARY KEY, payload TEXT NOT NULL, received_at INTEGER NOT NULL, processed_at INTEGER NULL)');
    $save = $db->prepare('INSERT OR IGNORE INTO webhook_inbox (event_id, payload, received_at) VALUES (?, ?, ?)');
    $save->execute([$event['id'], $raw, time()]);
    // Acknowledge only after durable storage. A duplicate event is already stored.
    // Process unprocessed rows separately and make your business effects idempotent.
    http_response_code(200); echo 'OK';
} catch (Throwable $e) {
    // A temporary storage failure must remain retryable; do not acknowledge it.
    http_response_code(503); echo 'Receiver temporarily unavailable';
}

Search API operations, parameters, SDK and webhooks.