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.

EventTrigger
invoice.signedInvoice is normalized (UBL 2.1) and signed (XAdES).
invoice.transmittedInvoice is submitted for clearance.
invoice.acceptedClearance granted: DGI reference and QR code available.
invoice.rejectedClearance refused: errorCode and errorMessage populated.
invoice.failedUnrecoverable technical error during processing.
invoice.cancelledInvoice was cancelled prior to transmission.
test.pingSent by the dashboard 'Test' button (data is null).
Request Body
{
  "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=.

Headers
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
  1. Read the raw body bytes before any JSON parsing: re-serializing alters whitespace and invalidates the signature.
  2. Reject any efact-webhook-timestamp differing by more than 5 minutes from your server clock (replay protection).
  3. Compute the expected HMAC and compare it in constant time against each v1= signature in the header. A single match is sufficient.
  4. On verification failure, return 400 and 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.

MethodEndpointDescription
POST/v1/webhooksCreate an endpoint (201, secret returned once)
GET/v1/webhooksList endpoints (paginated: page, size)
GET/v1/webhooks/:idRetrieve an endpoint
PATCH/v1/webhooks/:idUpdate url, description, events, or active (provided fields only)
DELETE/v1/webhooks/:idDelete an endpoint (pending deliveries stopped)
POST/v1/webhooks/:id/testSend a signed test.ping and return received HTTP status code
POST/v1/webhooks/:id/rotate-secretGenerate a new secret (old secret remains valid 24h)
GET/v1/webhooks/:id/deliveriesDelivery log (status, attempts, last error)
POST/v1/webhooks/deliveries/:deliveryId/retryRetry 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).

Create Endpoint
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"]
  }'
Response 201
{
  "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, Disable, Rotate
# 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 to id in 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 status and createdAt rather 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.ping and 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.