Webhooks
Recevez des notifications en temps réel pour tous les événements de votre compte. Factures, signatures, alertes de solde — restez informé instantanément.
Démarrage rapide
Configurez vos webhooks depuis le tableau de bord ou via l'API pour commencer à recevoir des notifications en temps réel.
Configurer l'URL webhook
Ajoutez votre URL de callback dans le tableau de bord sous Paramètres > Webhooks. L'URL doit obligatoirement utiliser HTTPS et pointer vers une IP publique routable (les adresses RFC1918, loopback, link-local et CGNAT sont rejetées au moment de la sauvegarde).
Récupérer votre secret
À la création (ou à la régénération), Scell.io affiche votre secret webhook (whsec_xxx...) UNE SEULE FOIS. Conservez-le immédiatement dans un secret manager — les GET ultérieurs ne retournent qu'un fingerprint (4 derniers caractères) pour identification.
Vérifier les signatures
Vérifiez toujours l'en-tête X-Scell-Signature avant de traiter les événements. Cela garantit l'authenticité.
Vérification de signature
Vérifiez toujours les signatures des webhooks pour vous assurer que les requêtes proviennent de Scell.io. La signature est incluse dans l'en-tête X-Scell-Signature.
t=timestamp,v1=signature. Utilisez toujours des fonctions de comparaison résistantes aux attaques temporelles pour prévenir les 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, }; }}Référence des événements
Abonnez-vous aux événements dont vous avez besoin. Chaque événement inclut les données pertinentes dans son payload.
| Event | Description |
|---|---|
invoice.created | Facture créée |
invoice.validated | Facture validée (Factur-X/UBL généré) |
invoice.transmitted | Facture transmise au PDP (SUPER PDP / réseau électronique) |
invoice.accepted | Facture acceptée par le destinataire |
invoice.rejected | Facture rejetée |
invoice.error | Erreur lors du traitement ou de la transmission de la facture |
| Event | Description |
|---|---|
invoice.incoming.received | Une facture entrante destinée à votre compte a été reçue via SUPER PDP |
invoice.incoming.validated | La facture entrante a passé la validation EN16931 |
invoice.incoming.accepted | La facture entrante a été acceptée |
invoice.incoming.rejected | La facture entrante a été rejetée |
invoice.incoming.disputed | La facture entrante a été contestée (litige) |
invoice.incoming.paid | La facture entrante a été marquée comme payée |
| Event | Description |
|---|---|
signature.created | Demande de signature créée |
signature.waiting | En attente de l'action d'un signataire |
signature.signed | Document signé par un signataire |
signature.completed | Tous les signataires ont signé |
signature.refused | Signature refusée par un signataire |
signature.expired | Demande de signature expirée |
signature.error | Erreur lors du processus de signature |
| Event | Description |
|---|---|
onboarding.started | Un tunnel d'onboarding sub-tenant a démarré |
onboarding.step_completed | Une étape de l'onboarding a été franchie |
onboarding.completed | Le sub-tenant est créé et opérationnel (onboarding_status dans le payload) |
onboarding.failed | L'onboarding a échoué (KYB SUPER PDP refusé, popup fermé, etc.) |
| Event | Description |
|---|---|
balance.low | Solde de crédits faible (seuil d'alerte) |
balance.critical | Solde de crédits critique |
Structure du payload
Tous les webhooks sont envoyés en requête POST avec un corps JSON. Voici un exemple de payload :
{ "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" } }}Champs communs
| Champ | Type | Description |
|---|---|---|
event | string | Type d'événement (ex. invoice.validated) |
webhook_id | uuid | Identifiant unique de la configuration webhook |
timestamp | ISO 8601 | Horodatage de l'événement |
company_id | uuid | Identifiant de la société associée à l'événement |
data | object | Données spécifiques à l'événement |
Bonnes pratiques
Suivez ces recommandations pour garantir un traitement fiable des webhooks.
Stocker le secret immédiatement
Depuis l'audit sécurité 2026-05-26, le secret webhook n'est exposé en clair QU'UNE SEULE FOIS (création ou régénération). Copiez-le immédiatement dans votre secret manager — les requêtes GET ultérieures ne retournent plus qu'un fingerprint (secret_last4).
Toujours vérifier les signatures
Ne traitez jamais un webhook sans vérifier sa signature au préalable. Cela évite les requêtes forgées.
Répondre rapidement
Retournez un code 200 en moins de 5 secondes. Traitez la logique lourde de manière asynchrone via une file d'attente.
Implémenter l'idempotence
Utilisez webhook_id et timestamp pour détecter les doublons. Les webhooks peuvent être retentés en cas d'échec.
Gérer les nouvelles tentatives
Scell.io retente les webhooks échoués avec un backoff exponentiel. Concevez votre gestionnaire pour être résilient.
Politique de retry
Scell.io retente automatiquement les livraisons de webhooks échouées avec un backoff exponentiel.
| Tentative | Délai | Temps total |
|---|---|---|
| 1re tentative | 1 minute | 1 min |
| 2e tentative | 5 minutes | 6 min |
| 3e tentative | 30 minutes | 36 min |
Prêt à intégrer les webhooks ?
Créez votre compte et configurez vos webhooks en quelques minutes.