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
POST /v1/webhook_endpoints
Authorization: Bearer sk_live_xxx
Content-Type: application/json{
"url": "https://monapp.ma/webhooks/efact",
"events": ["invoice.accepted", "invoice.rejected"],
"description": "Production webhook"
}Response 201 Created
{
"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
| Event | Déclenché quand |
|---|---|
invoice.accepted | La DGI a accepté la facture. dgi_id disponible. |
invoice.rejected | La DGI a rejeté la facture. rejection_reason disponible. |
invoice.signing | La signature XAdES a commencé. |
invoice.transmitted | La facture signée a été envoyée à la DGI. |
Structure d'un event
{
"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
t: timestamp UNIX de l'envoiv1: signature HMAC-SHA256
Algorithme de vérification
- 1. Extraire
tetv1du header - 2. Construire la chaîne signée :
{t}.{raw_body} - 3. Calculer HMAC-SHA256 avec le
webhook_secret - 4. Comparer le digest calculé à
v1 - 5. Vérifier que
test dans les 5 minutes de l'heure courante (protection contre les replays)
Via SDK Node (recommandé)
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)
@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.
| Tentative | Délai avant retry |
|---|---|
| 1ère tentative | Immédiat |
| 2ème tentative | 5 minutes |
| 3ème tentative | 30 minutes |
| 4ème tentative | 2 heures |
| 5ème tentative | 6 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é :
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
GET /v1/webhook_endpoints
Authorization: Bearer sk_live_xxxDésactiver un endpoint
PATCH /v1/webhook_endpoints/we_01H...
Authorization: Bearer sk_live_xxx
Content-Type: application/json
{ "active": false }Supprimer un endpoint
DELETE /v1/webhook_endpoints/we_01H...
Authorization: Bearer sk_live_xxxTester les webhooks en local
Utiliser un tunnel HTTP (ngrok, Cloudflare Tunnel) pour exposer votre localhost :
ngrok http 3000
# Copier l'URL https://xxxx.ngrok.io/webhooks/efact dans le dashboardEn environnement test (sk_test_), les events peuvent être déclenchés manuellement depuis le dashboard via Webhooks > Envoyer un event test .