Real-time Notifications

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.

1

Configure Webhook URL

Add your callback URL in the dashboard under Settings > Webhooks. This URL must be HTTPS and publicly accessible.

2

Get Your Secret

Copy your webhook secret (whsec_xxx...). Store it securely — you'll use it to verify incoming webhooks.

3

Verify Signatures

Always verify the X-Scell-Signature header before processing events. This ensures authenticity.

Security Best Practice: Never expose your webhook secret in client-side code. Always verify signatures on your backend server.

Signature Verification

Always verify webhook signatures to ensure requests originate from Scell.io. The signature is included in the X-Scell-Signature header.

Important: The signature format is t=timestamp,v1=signature. Always use timing-safe comparison functions to prevent timing attacks.
WebhookController.php
php
<?php
namespace 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.

Invoice Events
EventDescription
invoice.createdInvoice has been created
invoice.validatedInvoice validated (Factur-X/UBL generated)
invoice.submittedInvoice submitted to PDP
invoice.acceptedInvoice accepted by recipient
invoice.rejectedInvoice rejected
invoice.paidInvoice marked as paid
Signature Events
EventDescription
signature.createdSignature request created
signature.signedDocument signed by a signer
signature.completedAll signers have signed
signature.declinedSignature declined
signature.expiredSignature request expired
Account Events
EventDescription
balance.lowCredit balance is low (alert threshold)
balance.depletedCredit balance is depleted
kyb.approvedKYB verification approved
kyb.rejectedKYB verification rejected

Event Payload

All webhooks are sent as POST requests with a JSON body. Here is an example payload:

invoice.validated payload
json
{
"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

FieldTypeDescription
eventstringEvent type (e.g., invoice.validated)
webhook_iduuidUnique webhook configuration ID
timestampISO 8601Event timestamp
company_iduuidCompany ID associated with the event
dataobjectEvent-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.

AttemptDelayTotal Time
1st retry1 minute1 min
2nd retry5 minutes6 min
3rd retry30 minutes36 min
Success Codes: Any 2xx response is considered successful. The webhook is marked as delivered.
Failure Handling: After 3 consecutive failures, the webhook endpoint is automatically disabled. Re-enable it from your dashboard.
Timeout: Requests timeout after 30 seconds. Respond quickly and process asynchronously.

Ready to integrate webhooks?

Create your account and configure webhooks in minutes.