Skip to main content

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();
}

See also

Your cookie preferences

We use cookies to improve your experience. Essential cookies are always active. Cookie policy.