Webhooks

Les webhooks permettent à votre application de recevoir des notifications asynchrones quand une facture change de statut. EFact pousse un POST HTTPS signé vers votre endpoint dès qu'un event survient.

Configurer un endpoint

Via l'API

Request
POST /v1/webhook_endpoints
Authorization: Bearer sk_live_xxx
Content-Type: application/json
Body
{
  "url": "https://monapp.ma/webhooks/efact",
  "events": ["invoice.accepted", "invoice.rejected"],
  "description": "Production webhook"
}

Response 201 Created

Response
{
  "id": "we_01H...",
  "url": "https://monapp.ma/webhooks/efact",
  "events": ["invoice.accepted", "invoice.rejected"],
  "secret": "whsec_abc123...",
  "active": true,
  "created_at": "2026-04-07T10:00:00Z"
}

Important : Le champ secret n'est affiché qu'à la création. Sauvegarder immédiatement dans une variable d'environnement.

Via le dashboard

Dashboard > Webhooks > Ajouter un endpoint

Events disponibles

EventDéclenché quand
invoice.acceptedLa DGI a accepté la facture. dgi_id disponible.
invoice.rejectedLa DGI a rejeté la facture. rejection_reason disponible.
invoice.signingLa signature XAdES a commencé.
invoice.transmittedLa facture signée a été envoyée à la DGI.

Structure d'un event

Event Structure
{
  "id": "evt_01H...",
  "object": "event",
  "type": "invoice.accepted",
  "created": 1712487600,
  "data": {
    "id": "inv_01H8XYZABC",
    "object": "invoice",
    "status": "accepted",
    "amount": 14400,
    "currency": "MAD",
    "customer": {
      "ice": "002345678901234",
      "name": "Société Exemple SARL"
    },
    "dgi_id": "DGI-2026-0042-XYZABC",
    "xml_url": "https://storage.googleapis.com/...",
    "pdf_url": "https://storage.googleapis.com/...",
    "metadata": {
      "internal_id": "INV-2026-042"
    },
    "rejection_reason": null,
    "created_at": "2026-04-07T10:00:00Z",
    "updated_at": "2026-04-07T10:04:23Z"
  }
}

Pour invoice.rejected, dgi_id est null et rejection_reason contient le motif de rejet.

Vérifier la signature HMAC

Chaque requête webhook inclut un header Itzenata-Signature. Toujours vérifier cette signature avant de traiter l'event.

Format du header

Itzenata-Signature: t=1712487600,v1=abc123def456...
  • t: timestamp UNIX de l'envoi
  • v1 : signature HMAC-SHA256

Algorithme de vérification

  1. 1. Extraire t et v1 du header
  2. 2. Construire la chaîne signée : {t}.{raw_body}
  3. 3. Calculer HMAC-SHA256 avec le webhook_secret
  4. 4. Comparer le digest calculé à v1
  5. 5. Vérifier que test dans les 5 minutes de l'heure courante (protection contre les replays)

Via SDK Node (recommandé)

Node.js SDK
import { Itzenata } from '@itzenata/efact-node'

const itz = new Itzenata(process.env.ITZENATA_SECRET_KEY)

export async function POST(req: Request) {
  const payload = await req.text()
  const signature = req.headers.get('itzenata-signature') ?? ''

  let event
  try {
    event = itz.webhooks.constructEvent(
      payload,
      signature,
      process.env.ITZENATA_WEBHOOK_SECRET
    )
  } catch (err) {
    // Signature invalide - rejeter la requête
    return new Response('Webhook signature invalid', { status: 400 })
  }

  switch (event.type) {
    case 'invoice.accepted':
      await handleAccepted(event.data)
      break
    case 'invoice.rejected':
      await handleRejected(event.data)
      break
    default:
      console.log(`Event non géré : ${event.type}`)
  }

  return new Response('ok', { status: 200 })
}

async function handleAccepted(invoice) {
  await db.invoice.update({
    where: { id: invoice.metadata.internal_id },
    data: {
      status: 'dgi_accepted',
      dgi_id: invoice.dgi_id,
      signed_xml_url: invoice.xml_url,
      signed_pdf_url: invoice.pdf_url,
    }
  })
}

async function handleRejected(invoice) {
  await db.invoice.update({
    where: { id: invoice.metadata.internal_id },
    data: { status: 'dgi_rejected', rejection_reason: invoice.rejection_reason }
  })
  await notifyAccountant(invoice.rejection_reason)
}

Via SDK Java (Spring Boot)

Java SDK
@PostMapping("/webhooks/efact")
public ResponseEntity<String> handleWebhook(
    @RequestBody String payload,
    @RequestHeader("Itzenata-Signature") String signature) {

  Event event;
  try {
    event = itz.webhooks().constructEvent(
      payload,
      signature,
      System.getenv("ITZENATA_WEBHOOK_SECRET")
    );
  } catch (SignatureVerificationException e) {
    return ResponseEntity.badRequest().body("Invalid signature");
  }

  switch (event.getType()) {
    case "invoice.accepted" -> {
      String internalId = (String) event.getData().getMetadata().get("internal_id");
      invoiceRepository.markAsAccepted(internalId, event.getData().getDgiId());
    }
    case "invoice.rejected" -> {
      notificationService.notifyAccountant(event.getData().getRejectionReason());
    }
  }

  return ResponseEntity.ok("ok");
}

Politique de retry

EFact retentera la livraison d'un event si votre endpoint retourne un code HTTP autre que 2xx.

TentativeDélai avant retry
1ère tentativeImmédiat
2ème tentative5 minutes
3ème tentative30 minutes
4ème tentative2 heures
5ème tentative6 heures

Après 5 échecs, l'event est marqué failed et visible dans le dashboard sous Webhooks > Logs . Il peut être renvoyé manuellement.

Idempotence côté récepteur

EFact peut délivrer un même event plusieurs fois en cas de retry. Toujours implémenter l'idempotence côté récepteur en utilisant event.id comme clé :

Idempotence Example
const alreadyProcessed = await db.processedEvent.findUnique({
  where: { event_id: event.id }
})

if (alreadyProcessed) {
  return new Response('ok', { status: 200 }) // ne pas retraiter
}

await processEvent(event)

await db.processedEvent.create({ data: { event_id: event.id } })

Gestion des endpoints

Lister les endpoints

List Endpoints
GET /v1/webhook_endpoints
Authorization: Bearer sk_live_xxx

Désactiver un endpoint

Disable Endpoint
PATCH /v1/webhook_endpoints/we_01H...
Authorization: Bearer sk_live_xxx
Content-Type: application/json

{ "active": false }

Supprimer un endpoint

Delete Endpoint
DELETE /v1/webhook_endpoints/we_01H...
Authorization: Bearer sk_live_xxx

Tester les webhooks en local

Utiliser un tunnel HTTP (ngrok, Cloudflare Tunnel) pour exposer votre localhost :

Ngrok
ngrok http 3000
# Copier l'URL https://xxxx.ngrok.io/webhooks/efact dans le dashboard

En environnement test (sk_test_), les events peuvent être déclenchés manuellement depuis le dashboard via Webhooks > Envoyer un event test .