Webhooks
Receive real-time notifications for all events on your account. Invoices, signatures, balance alerts — stay informed instantly.
Quick Start
Configure webhooks in your dashboard or via the API to start receiving real-time notifications.
Configure Webhook URL
Add your callback URL in the dashboard under Settings > Webhooks. This URL must be HTTPS and publicly accessible.
Get Your Secret
Copy your webhook secret (whsec_xxx...). Store it securely — you'll use it to verify incoming webhooks.
Verify Signatures
Always verify the X-Scell-Signature header before processing events. This ensures authenticity.
Signature Verification
Always verify webhook signatures to ensure requests originate from Scell.io. The signature is included in the X-Scell-Signature header.
t=timestamp,v1=signature. Always use timing-safe comparison functions to prevent timing attacks.<?phpnamespace App\Http\Controllers;use Illuminate\Http\Request;use Illuminate\Http\Response;class WebhookController extends Controller{ private string $webhookSecret; public function __construct() { $this->webhookSecret = config('services.scell.webhook_secret'); } public function handle(Request $request): Response { // 1. Get signature header $signatureHeader = $request->header('X-Scell-Signature'); if (!$signatureHeader) { return response('Missing signature header', 400); } // 2. Parse timestamp and signature $elements = $this->parseSignatureHeader($signatureHeader); $timestamp = $elements['timestamp']; $signature = $elements['signature']; // 3. Verify timestamp (replay attack protection) if (abs(time() - $timestamp) > 300) { return response('Timestamp expired', 403); } // 4. Verify signature $payload = $request->getContent(); $expectedSignature = $this->computeSignature($timestamp, $payload); if (!hash_equals($expectedSignature, $signature)) { return response('Invalid signature', 403); } // 5. Process event $event = $request->input('event'); $data = $request->input('data'); $this->processEvent($event, $data); return response('OK', 200); } private function parseSignatureHeader(string $header): array { $parts = explode(',', $header); $elements = []; foreach ($parts as $part) { [$key, $value] = explode('=', $part, 2); $elements[$key] = $value; } return [ 'timestamp' => (int) $elements['t'], 'signature' => $elements['v1'], ]; } private function computeSignature(int $timestamp, string $payload): string { $signedPayload = "{$timestamp}.{$payload}"; return hash_hmac('sha256', $signedPayload, $this->webhookSecret); } private function processEvent(string $event, array $data): void { match ($event) { 'invoice.validated' => $this->handleInvoiceValidated($data), 'signature.completed' => $this->handleSignatureCompleted($data), 'balance.low' => $this->handleBalanceLow($data), default => null, }; }}Events Reference
Subscribe to the events you need. Each event includes relevant data in the payload.
| Event | Description |
|---|---|
invoice.created | Invoice has been created |
invoice.validated | Invoice validated (Factur-X/UBL generated) |
invoice.submitted | Invoice submitted to PDP |
invoice.accepted | Invoice accepted by recipient |
invoice.rejected | Invoice rejected |
invoice.paid | Invoice marked as paid |
| Event | Description |
|---|---|
signature.created | Signature request created |
signature.signed | Document signed by a signer |
signature.completed | All signers have signed |
signature.declined | Signature declined |
signature.expired | Signature request expired |
| Event | Description |
|---|---|
balance.low | Credit balance is low (alert threshold) |
balance.depleted | Credit balance is depleted |
kyb.approved | KYB verification approved |
kyb.rejected | KYB verification rejected |
Event Payload
All webhooks are sent as POST requests with a JSON body. Here is an example payload:
{ "event": "invoice.validated", "webhook_id": "550e8400-e29b-41d4-a716-446655440000", "timestamp": "2024-01-15T10:30:00+00:00", "company_id": "123e4567-e89b-12d3-a456-426614174000", "data": { "invoice": { "id": "inv_abc123", "invoice_number": "FAC-2024-001", "status": "validated", "total_amount": 1200.00, "currency": "EUR", "customer": { "name": "Client SARL", "siren": "123456789" }, "created_at": "2024-01-15T09:00:00+00:00", "validated_at": "2024-01-15T10:30:00+00:00" } }}Common Fields
| Field | Type | Description |
|---|---|---|
event | string | Event type (e.g., invoice.validated) |
webhook_id | uuid | Unique webhook configuration ID |
timestamp | ISO 8601 | Event timestamp |
company_id | uuid | Company ID associated with the event |
data | object | Event-specific data |
Best Practices
Follow these guidelines to ensure reliable webhook processing.
Always Verify Signatures
Never process a webhook without verifying its signature first. This prevents spoofed requests.
Respond Quickly
Return a 200 status code within 5 seconds. Process heavy logic asynchronously using a queue.
Implement Idempotence
Use webhook_id and timestamp to detect duplicates. Webhooks may be retried on failure.
Handle Retries
Scell.io retries failed webhooks with exponential backoff. Design your handler to be resilient.
Retry Policy
Scell.io automatically retries failed webhook deliveries with exponential backoff.
| Attempt | Delay | Total Time |
|---|---|---|
| 1st retry | 1 minute | 1 min |
| 2nd retry | 5 minutes | 6 min |
| 3rd retry | 30 minutes | 36 min |
Ready to integrate webhooks?
Create your account and configure webhooks in minutes.