Use cases
Webhook HMAC-SHA256 verification (timing-safe)
Constant-time hash comparison to prevent timing attacks
All Scell.io webhooks are HMAC-SHA256-signed with the webhook secret (configured when the endpoint is created). The signature is computed on the concatenation `<timestamp>.<raw_body>` (timestamp in the `X-Scell-Timestamp` header) and sent in `X-Scell-Signature`. On the receiver side, NEVER compare hashes with `===` (vulnerable to timing attacks): use `timingSafeEqual` (Node) or `hash_equals` (PHP). Also verify the timestamp: any message > 5 minutes old is rejected to prevent replay attacks. Optional mTLS available on outbound webhooks for enterprise tenants.
Key facts
- Signature: `HMAC-SHA256(secret, timestamp + '.' + raw_body)`
- Header `X-Scell-Signature` (hex) + `X-Scell-Timestamp` (epoch)
- Timestamp tolerance: 5 minutes (anti-replay)
- Mandatory timing-safe comparison (`hash_equals` / `timingSafeEqual`)
- Rotatable secret via dashboard with no downtime
- Optional mTLS on outbound for enterprises
Code example
<?php
// Laravel controller webhook receiver
public function handle(\Illuminate\Http\Request $request)
{
$signature = $request->header('X-Scell-Signature');
$timestamp = (int) $request->header('X-Scell-Timestamp');
$rawBody = $request->getContent();
// 1. Anti-replay
if (abs(time() - $timestamp) > 300) {
abort(401, 'Timestamp too old');
}
// 2. Vérification timing-safe
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, config('services.scell.webhook_secret'));
if (!hash_equals($expected, $signature)) {
abort(401, 'Invalid signature');
}
// 3. Idempotence (table avec UNIQUE sur event_id)
$event = json_decode($rawBody, true);
\App\Models\WebhookEvent::firstOrCreate(['event_id' => $event['event_id']]);
return response()->noContent();
}