Aller au contenu principal
Notifications en temps réel

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.

1

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).

2

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.

3

Vérifier les signatures

Vérifiez toujours l'en-tête X-Scell-Signature avant de traiter les événements. Cela garantit l'authenticité.

Bonne pratique de sécurité : N'exposez jamais votre secret webhook dans du code côté client. Vérifiez toujours les signatures sur votre serveur backend.

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.

Important : Le format de signature est t=timestamp,v1=signature. Utilisez toujours des fonctions de comparaison résistantes aux attaques temporelles pour prévenir les 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,
};
}
}

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.

Événements factures
EventDescription
invoice.createdFacture créée
invoice.validatedFacture validée (Factur-X/UBL généré)
invoice.transmittedFacture transmise au PDP (SUPER PDP / réseau électronique)
invoice.acceptedFacture acceptée par le destinataire
invoice.rejectedFacture rejetée
invoice.errorErreur lors du traitement ou de la transmission de la facture
Événements factures entrantes
EventDescription
invoice.incoming.receivedUne facture entrante destinée à votre compte a été reçue via SUPER PDP
invoice.incoming.validatedLa facture entrante a passé la validation EN16931
invoice.incoming.acceptedLa facture entrante a été acceptée
invoice.incoming.rejectedLa facture entrante a été rejetée
invoice.incoming.disputedLa facture entrante a été contestée (litige)
invoice.incoming.paidLa facture entrante a été marquée comme payée
Événements signatures
EventDescription
signature.createdDemande de signature créée
signature.waitingEn attente de l'action d'un signataire
signature.signedDocument signé par un signataire
signature.completedTous les signataires ont signé
signature.refusedSignature refusée par un signataire
signature.expiredDemande de signature expirée
signature.errorErreur lors du processus de signature
Événements onboarding
EventDescription
onboarding.startedUn tunnel d'onboarding sub-tenant a démarré
onboarding.step_completedUne étape de l'onboarding a été franchie
onboarding.completedLe sub-tenant est créé et opérationnel (onboarding_status dans le payload)
onboarding.failedL'onboarding a échoué (KYB SUPER PDP refusé, popup fermé, etc.)
Événements compte
EventDescription
balance.lowSolde de crédits faible (seuil d'alerte)
balance.criticalSolde 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 :

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"
}
}
}

Champs communs

ChampTypeDescription
eventstringType d'événement (ex. invoice.validated)
webhook_iduuidIdentifiant unique de la configuration webhook
timestampISO 8601Horodatage de l'événement
company_iduuidIdentifiant de la société associée à l'événement
dataobjectDonné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.

TentativeDélaiTemps total
1re tentative1 minute1 min
2e tentative5 minutes6 min
3e tentative30 minutes36 min
Codes de succès : Toute réponse 2xx est considérée comme un succès. Le webhook est marqué comme livré.
Gestion des échecs : Après 3 échecs consécutifs, l'endpoint webhook est automatiquement désactivé. Réactivez-le depuis votre tableau de bord.
Timeout : Les requêtes expirent après 30 secondes. Répondez rapidement et traitez de manière asynchrone.

Prêt à intégrer les webhooks ?

Créez votre compte et configurez vos webhooks en quelques minutes.

Vos préférences cookies

Nous utilisons des cookies pour améliorer votre expérience. Les cookies essentiels sont toujours actifs. Politique cookies.