E-Invoicing Webhooks: Signature Verification & Events
Instead of polling the API continuously, receive invoice status updates directly on your own HTTPS endpoint. Every delivery is signed: verify it before trusting the payload.
In short
EFact sends JSON POST requests to your HTTPS endpoint on every invoice status transition. Each request is signed with HMAC-SHA256 using an endpoint-specific whsec_... secret displayed once upon creation. Verify the signature and timestamp, return 2xx within 10 seconds, and deduplicate using the delivery ID.
How do I create an endpoint?
In the dashboard, navigate to Webhooks, enter a public HTTPS URL, and select the event types. Upon creation, EFact generates the signing secret and displays it once only: copy it to your server configuration (e.g. EFACT_WEBHOOK_SECRET). You do not need to generate one yourself: server-generated secrets ensure sufficient entropy. Private and local IP addresses are rejected. Up to 5 active endpoints per organization.
Which events are sent?
An endpoint created without an event filter receives all events.
| Event | Trigger |
|---|---|
invoice.signed | Invoice is normalized (UBL 2.1) and signed (XAdES). |
invoice.transmitted | Invoice is submitted for clearance. |
invoice.accepted | Clearance granted: DGI reference and QR code available. |
invoice.rejected | Clearance refused: errorCode and errorMessage populated. |
invoice.failed | Unrecoverable technical error during processing. |
invoice.cancelled | Invoice was cancelled prior to transmission. |
test.ping | Sent by the dashboard 'Test' button (data is null). |
{
"id": "3f6c2a9e-8d1b-4c7a-9e2f-1a2b3c4d5e6f",
"type": "invoice.accepted",
"apiVersion": "2026-07-01",
"createdAt": "2026-09-30T14:05:12Z",
"data": {
"invoice": {
"id": "b1e2c3d4-0000-4000-8000-000000000001",
"invoiceNumber": "FAC-2026-0142",
"status": "ACCEPTED",
"previousStatus": "TRANSMITTED",
"currency": "MAD",
"amountTtc": 1200.00,
"dgiReference": "DGI-REF-...",
"dgiQrCode": "...",
"errorCode": null,
"errorMessage": null
}
}
}Payloads never include the signed XML: retrieve tax artifacts via the Invoices API. New fields may be added over time; ignore unrecognized properties.
How do I verify the signature?
Each delivery includes three HTTP headers. The signature is computed as HMAC-SHA256(secret, id + "." + timestamp + "." + rawBody) encoded as hexadecimal, prefixed with v1=.
POST /webhooks/efact HTTP/1.1
Content-Type: application/json
efact-webhook-id: 3f6c2a9e-8d1b-4c7a-9e2f-1a2b3c4d5e6f
efact-webhook-timestamp: 1790777112
efact-webhook-signature: v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd- Read the raw body bytes before any JSON parsing: re-serializing alters whitespace and invalidates the signature.
- Reject any
efact-webhook-timestampdiffering by more than 5 minutes from your server clock (replay protection). - Compute the expected HMAC and compare it in constant time against each
v1=signature in the header. A single match is sufficient. - On verification failure, return
400and drop the event.
The following examples use only standard libraries with no external dependencies. Adapt them to your web framework following the rules above.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.EFACT_WEBHOOK_SECRET!;
function verifyEfactSignature(rawBody: Buffer, headers: express.Request["headers"]): boolean {
const id = headers["efact-webhook-id"];
const timestamp = headers["efact-webhook-timestamp"];
const header = headers["efact-webhook-signature"];
if (typeof id !== "string" || typeof timestamp !== "string" || typeof header !== "string") {
return false;
}
// Anti-replay: 5-minute tolerance.
const ts = Number(timestamp);
if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest();
// One "v1=" entry per valid secret (two during rotation).
return header.split(",").some((entry) => {
const [scheme, hex] = entry.trim().split("=");
if (scheme !== "v1" || !hex) return false;
const candidate = Buffer.from(hex, "hex");
return candidate.length === expected.length && crypto.timingSafeEqual(candidate, expected);
});
}
const app = express();
// RAW body required: signature covers exact received bytes.
app.post("/webhooks/efact", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyEfactSignature(req.body, req.headers)) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
// Respond 2xx immediately, process asynchronously (job queue).
res.sendStatus(200);
queue.add("efact-event", event, { jobId: event.id }); // jobId = deduplication
});How do I rotate secrets without dropping events?
The Rotate secret button in the dashboard generates a new secret while keeping the previous secret active for 24 hours. During this window, deliveries carry two signatures: v1=<new>,v1=<old>. Deploy the new secret at your convenience; your endpoint accepts deliveries matching either secret. Rotate immediately if a leak is suspected.
How do I manage endpoints via the API?
All dashboard capabilities are exposed via the REST API using your secret key (Authorization: Bearer efact_sk_...). Endpoints belong to the key's organization; endpoints outside your tenant return 404. Responses follow the standard envelope described in the API overview.
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/webhooks | Create an endpoint (201, secret returned once) |
GET | /v1/webhooks | List endpoints (paginated: page, size) |
GET | /v1/webhooks/:id | Retrieve an endpoint |
PATCH | /v1/webhooks/:id | Update url, description, events, or active (provided fields only) |
DELETE | /v1/webhooks/:id | Delete an endpoint (pending deliveries stopped) |
POST | /v1/webhooks/:id/test | Send a signed test.ping and return received HTTP status code |
POST | /v1/webhooks/:id/rotate-secret | Generate a new secret (old secret remains valid 24h) |
GET | /v1/webhooks/:id/deliveries | Delivery log (status, attempts, last error) |
POST | /v1/webhooks/deliveries/:deliveryId/retry | Retry a delivery immediately |
Empty or omitted events subscribes to all events. Possible creation errors: 422 WEBHOOK_URL_INVALID / WEBHOOK_URL_FORBIDDEN (non-HTTPS, private, or local URL) and 409 WEBHOOK_LIMIT_REACHED (maximum of 5 active endpoints).
curl -X POST https://api.efact.ma/v1/webhooks \
-H "Authorization: Bearer $EFACT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.example.ma/webhooks/efact",
"description": "ERP production",
"events": ["invoice.accepted", "invoice.rejected", "invoice.failed"]
}'{
"code": "OK",
"message": null,
"data": {
"id": "7c1d9b2e-4f3a-4e8b-9a6d-2b5c8e1f0a3d",
"organizationId": "5e0b...",
"url": "https://erp.example.ma/webhooks/efact",
"description": "ERP production",
"events": ["invoice.accepted", "invoice.rejected", "invoice.failed"],
"active": true,
"secret": "whsec_...",
"createdAt": "2026-09-30T14:00:00Z",
"previousSecretExpiresAt": null
},
"errors": null
}# Test receiver (response: { success, responseStatus, message })
curl -X POST https://api.efact.ma/v1/webhooks/$ENDPOINT_ID/test \
-H "Authorization: Bearer $EFACT_SECRET_KEY"
# Temporarily disable endpoint
curl -X PATCH https://api.efact.ma/v1/webhooks/$ENDPOINT_ID \
-H "Authorization: Bearer $EFACT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "active": false }'
# Rotate secret (data.secret, displayed once)
curl -X POST https://api.efact.ma/v1/webhooks/$ENDPOINT_ID/rotate-secret \
-H "Authorization: Bearer $EFACT_SECRET_KEY"
# Query recent deliveries, then retry one
curl "https://api.efact.ma/v1/webhooks/$ENDPOINT_ID/deliveries?page=0&size=20" \
-H "Authorization: Bearer $EFACT_SECRET_KEY"
curl -X POST https://api.efact.ma/v1/webhooks/deliveries/$DELIVERY_ID/retry \
-H "Authorization: Bearer $EFACT_SECRET_KEY"What happens if my server does not respond?
Non-2xx responses, timeouts (5s connect, 10s read), and redirects are treated as failures. EFact retries up to 8 times across approximately 26 hours (30s, 2m, 10m, 30m, 1h, 4h, 12h, with ±20% jitter). After 20 consecutive delivery failures, the endpoint is automatically paused; re-enable it in the dashboard. Deliveries can also be retried manually from the dashboard delivery log.
What are server-side best practices?
- • Deduplicate deliveries: delivery is "at least once". Store
efact-webhook-id(equal toidin the JSON payload) and ignore already-processed events. - • Acknowledge fast: return 2xx immediately after verifying the signature, then process the payload in a background queue.
- • Do not assume ordering: rely on
statusandcreatedAtrather than delivery arrival sequence; if uncertain, re-fetch invoice status via the API. - • Keep secrets out of source code: store in environment variables or secrets managers, never in code or logs.
- • Test your endpoint: use the dashboard "Test" button to send a signed
test.pingand inspect the HTTP response.
Next steps
Frequently asked questions
Which invoice events does EFact send via webhook?
EFact sends invoice.signed, invoice.transmitted, invoice.accepted, invoice.rejected, invoice.failed, and invoice.cancelled, plus test.ping from the dashboard Test button. An endpoint created without an event filter receives all events. invoice.accepted includes the DGI reference and QR code; invoice.rejected includes errorCode and errorMessage.
How many webhook endpoints can I create?
Up to 5 active endpoints per organization. Beyond that, creation returns 409 WEBHOOK_LIMIT_REACHED. Each URL must be public and use HTTPS: private, local, or non-HTTPS URLs are rejected with 422 WEBHOOK_URL_INVALID or WEBHOOK_URL_FORBIDDEN.
Do I still need to poll the API if I use webhooks?
Not for normal tracking: every status transition is delivered to your URL. However, keep API polling as a fallback, as webhook delivery is 'at least once' and ordering is not guaranteed. If a status is uncertain, retrieve the invoice via the API rather than relying solely on delivery order.