ffacturifirma

Documentation

Webhooks

Receive signed HTTP notifications in your systems every time a document changes in facturifirma.ro.

Registering a webhook

From the app: the Webhooks page (admin menu) — add the target URL and a signing secret. The secret is write-only: it cannot be read back after saving.

Via the API, with an admin API key:

curl -X POST https://app.facturifirma.ro/api/v1/webhooks \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhook", "secret": "<SIGNING_SECRET>"}'

Events

A single event type is currently delivered:

document.changedany change to a document — create, edit, issue, payment, reversal, cancel or delete, for every document type (invoice, proforma, receipt, delivery note, order, credit note).

New event types will be added over time; treat the "event" field as extensible and ignore unknown types.

Payload

Every delivery is a POST with a JSON body:

POST https://example.com/webhook

{
  "event": "document.changed",
  "documentId": "8b1f7c02-4c33-4a6e-9f0d-2f6f6f0a9d11",
  "at": "2026-07-15T09:30:00Z"
}

Headers

Every request carries the event type and the body signature:

Content-Type: application/json
X-Facturifirma-Event: document.changed
X-Facturifirma-Signature: sha256=<hex(HMAC-SHA256(secret, body))>

Verifying the signature

The signature is HMAC-SHA256 over the raw request body with your signing secret, formatted "sha256=<hex>". Compare it in constant time:

import crypto from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader), Buffer.from(expected));
}

Delivery and retries

Deliveries are dispatched from a job queue with automatic retries (at-least-once delivery, no ordering guarantee). Your endpoint must answer with a 2xx status; anything else counts as a failure and is retried.

Security

Only absolute http(s) URLs resolving to public addresses are accepted (SSRF guard). The signing secret is encrypted at rest and never returned by the API.