Skip to main content
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. The URL must use HTTPS and resolve to a publicly routable IP (RFC1918, loopback, link-local and CGNAT addresses are rejected at save time).

2

Get Your Secret

On creation (or regeneration), Scell.io shows your webhook secret (whsec_xxx...) ONCE. Store it immediately in a secret manager — subsequent GETs only return a fingerprint (last 4 characters) for identification.

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.transmittedInvoice transmitted to the PDP (SUPER PDP / electronic network)
invoice.acceptedInvoice accepted by recipient
invoice.rejectedInvoice rejected
invoice.errorError during invoice processing or transmission
Incoming Invoice Events
EventDescription
invoice.incoming.receivedAn incoming invoice addressed to your account was received via SUPER PDP
invoice.incoming.validatedThe incoming invoice passed EN16931 validation
invoice.incoming.acceptedThe incoming invoice was accepted
invoice.incoming.rejectedThe incoming invoice was rejected
invoice.incoming.disputedThe incoming invoice was disputed
invoice.incoming.paidThe incoming invoice was marked as paid
Signature Events
EventDescription
signature.createdSignature request created
signature.waitingWaiting for a signer action
signature.signedDocument signed by a signer
signature.completedAll signers have signed
signature.refusedSignature refused by a signer
signature.expiredSignature request expired
signature.errorError during the signature process
Onboarding Events
EventDescription
onboarding.startedA sub-tenant onboarding flow has started
onboarding.step_completedAn onboarding step was completed
onboarding.completedThe sub-tenant is created and operational (onboarding_status in the payload)
onboarding.failedOnboarding failed (SUPER PDP KYB rejected, popup closed, etc.)
Account Events
EventDescription
balance.lowCredit balance is low (alert threshold)
balance.criticalCredit balance is critically low

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": "2026-05-10T10:30:00+00:00",
"company_id": "123e4567-e89b-12d3-a456-426614174000",
"data": {
"invoice": {
"id": "inv_01975f80c4ee7800",
"invoice_number": "T0001-202605-00042",
"status": "validated",
"total_amount": 1200.00,
"currency": "EUR",
"customer": {
"name": "Client SARL",
"siret": "12345678900012"
},
"created_at": "2026-05-10T09:00:00+00:00",
"validated_at": "2026-05-10T10: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.

Store the Secret Immediately

Since the 2026-05-26 security audit, the webhook secret is exposed in clear text ONLY ONCE (at creation or regeneration). Copy it immediately to your secret manager — subsequent GET requests only return a fingerprint (secret_last4).

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.

Your cookie preferences

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