Documentația se poate citi și de asistenții AI. Server MCP

Ilustromania.
Intră în aplicație

Webhook-uri

Platforma anunță serverul tău când se emite o factură, un aviz, o ofertă sau vine o comandă de catering: cereri POST semnate după Standard Webhooks, cu reîncercări și jurnal al livrărilor.

Cum funcționează

Adaugi o adresă https a serverului tău și alegi evenimentele care te interesează. Când se întâmplă unul dintre ele, platforma trimite la adresa ta o cerere POST cu un corp JSON scurt, semnată cu un secret pe care îl primești la adăugare. Serverul tău verifică semnătura, răspunde 2xx și, dacă are nevoie de detalii, citește documentul prin API.

Formatul urmează specificația Standard Webhooks: antetele webhook-id, webhook-timestamp, webhook-signature și semnătură HMAC-SHA256. Bibliotecile oficiale Standard Webhooks pot verifica semnătura direct.

Evenimentul se scrie în aceeași tranzacție cu documentul: dacă documentul nu se salvează, nu pleacă nici evenimentul. Trimiterea o face o sarcină din fundal, la câteva secunde după emitere, niciodată în cererea omului care a emis documentul.

Evenimentele

EvenimentCând se trimitedata
factura.emisas-a emis o factură (din documente sau dintr-un bon)id, serie, numar
aviz.emiss-a emis un aviz de însoțireid, serie, numar
oferta.creatas-a creat o ofertăid, an, numar
comanda_catering.primitaa venit o comandă de cateringid
test.pingdoar la „Trimite test”mesaj

data conține doar identificatori, nu date personale sau sume. Ca să afli restul, citești documentul prin API.

Corpul unei livrări (JSON compact, pe un singur rând)json
{"type":"factura.emisa","timestamp":"2026-11-12T09:30:00.000Z","data":{"id":"3f0c2a8e-6b1d-4c3a-9e2f-1a2b3c4d5e6f","serie":"A","numar":1042}}
Antetele unei livrărihttp
POST /webhook HTTP/1.1
Content-Type: application/json
webhook-id: msg_2f1c0d3e4a5b4c2b9b1a8f14e45fea8e
webhook-timestamp: 1794475800
webhook-signature: v1,xvrLd4URs9sqyiPEOJPIQbrLYrXbWMUwTlIJXh4lqBU=

Adaugă o destinație

Destinațiile se gestionează doar de un Administrator al firmei, din sesiune. Pe o firmă unde livrarea webhook-urilor nu e pornită, rutele răspund 409 cu {"eroare": "webhookuri_oprite_pe_firma"}.

  1. Trimite adresa și evenimentele

    Adăugarebash
    curl -X POST https://app.ilustromania.com/nucleu/v1/webhookuri \
      -H "Cookie: __Host-sesiune=$JETON_SESIUNE" \
      -H "Content-Type: application/json" \
      -d '{"url": "https://exemplu.ro/webhook", "evenimente": ["factura.emisa", "aviz.emis"], "descriere": "Contabilitate"}'
    CâmpTipObligatoriuDetalii
    urlstringdacel mult 2048 de caractere; doar https, pe portul 443, fără utilizator și parolă în adresă, fără adrese locale sau private
    evenimentestring[]daunul sau mai multe din factura.emisa, aviz.emis, oferta.creata, comanda_catering.primita
    descrierestringnucel mult 200 de caractere

    O adresă refuzată dă 400 cu {"eroare": "url_interzis"}; un corp greșit, 400 cu {"eroare": "corp_invalid"}.

  2. Salvează secretul

    Răspunsul 201 are forma {"destinatie": {…}, "secret": "whsec_…"}. Secretul se vede doar acum; platforma îl ține criptat în Seif și nu îl mai arată. Pune-l într-o variabilă de mediu a serverului care primește.

  3. Trimite un test

    Testbash
    curl -X POST https://app.ilustromania.com/nucleu/v1/webhookuri/$ID_DESTINATIE/test \
      -H "Cookie: __Host-sesiune=$JETON_SESIUNE"

    Răspunsul este 202; în câteva secunde serverul tău primește un test.ping. Rezultatul apare în jurnalul livrărilor.

Verifică semnătura

Semnătura este HMAC-SHA256 peste textul <webhook-id>.<webhook-timestamp>.<corp>, cu cheia dată de partea base64 de după whsec_, codată în base64 și prefixată cu v1,. Ca să o verifici:

  1. Citește corpul exact cum a venit, ca text sau octeți, înainte de orice parsare JSON. Un corp parsat și scris din nou nu mai are aceeași semnătură.
  2. Refuză cererea dacă webhook-timestamp e la mai mult de 5 minute de ora ta: așa nu poate fi retrimisă mai târziu de altcineva.
  3. Calculează semnătura și compar-o, în timp constant, cu fiecare valoare v1,… din webhook-signature. Valorile sunt despărțite prin spațiu; ajunge să se potrivească una.
Node.js (Express)javascript
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const SECRET = process.env.WEBHOOK_SECRET; // whsec_…
const TOLERANTA_S = 5 * 60;

function verifica(secret, antete, corp) {
  const id = antete["webhook-id"];
  const ts = Number(antete["webhook-timestamp"]);
  const semnaturi = String(antete["webhook-signature"] ?? "");
  if (!id || !Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > TOLERANTA_S) return false;
  const cheie = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const asteptata = createHmac("sha256", cheie).update(`${id}.${ts}.${corp}`).digest();
  return semnaturi.split(" ").some((parte) => {
    const [versiune, valoare] = parte.split(",", 2);
    if (versiune !== "v1" || !valoare) return false;
    const primita = Buffer.from(valoare, "base64");
    return primita.length === asteptata.length && timingSafeEqual(primita, asteptata);
  });
}

const app = express();
// corpul brut: semnătura se calculează pe textul exact primit
app.post("/webhook", express.raw({ type: "application/json" }), (cerere, raspuns) => {
  const corp = cerere.body.toString("utf8");
  if (!verifica(SECRET, cerere.headers, corp)) return raspuns.sendStatus(400);
  const eveniment = JSON.parse(corp);
  // ... pune evenimentul într-o coadă și răspunde repede
  raspuns.sendStatus(204);
});
app.listen(3000);
Python (Flask)python
import base64
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

SECRET = os.environ["WEBHOOK_SECRET"]  # whsec_…
TOLERANTA_S = 5 * 60


def verifica(secret: str, antete, corp: bytes) -> bool:
    id_ = antete.get("webhook-id", "")
    ts = antete.get("webhook-timestamp", "")
    semnaturi = antete.get("webhook-signature", "")
    if not id_ or not ts.isdigit() or abs(time.time() - int(ts)) > TOLERANTA_S:
        return False
    cheie = base64.b64decode(secret.removeprefix("whsec_"))
    asteptata = hmac.new(cheie, f"{id_}.{ts}.".encode() + corp, hashlib.sha256).digest()
    for parte in semnaturi.split(" "):
        versiune, _, valoare = parte.partition(",")
        if versiune == "v1" and valoare:
            try:
                primita = base64.b64decode(valoare)
            except ValueError:
                continue
            if hmac.compare_digest(primita, asteptata):
                return True
    return False


app = Flask(__name__)


@app.post("/webhook")
def webhook():
    corp = request.get_data()  # octeții exacți, înainte de JSON
    if not verifica(SECRET, request.headers, corp):
        abort(400)
    eveniment = request.get_json()
    # ... pune evenimentul într-o coadă și răspunde repede
    return "", 204
Un exemplu calculat, ca să-ți probezi codul

Cu secretul de probă format din whsec_ urmat de Y2hlaWUtZGUtcHJvYmEtcGVudHJ1LWRvY3VtZW50YXRpZSEh (nu e al nimănui; partea de după prefix este base64 pentru textul cheie-de-proba-pentru-documentatie!!), webhook-id = msg_2f1c0d3e4a5b4c2b9b1a8f14e45fea8e, webhook-timestamp = 1794475800 și corpul de mai sus, exact pe un rând, semnătura corectă este:

texttext
v1,xvrLd4URs9sqyiPEOJPIQbrLYrXbWMUwTlIJXh4lqBU=

Momentul 1794475800 este fix (12.11.2026, 09:30 UTC), deci funcțiile de mai sus îl refuză din cauza ferestrei de 5 minute; pentru probă, scoate temporar verificarea timpului. Un singur spațiu în plus în corp schimbă semnătura.

Răspunde repede, cu 2xx, și fă munca grea după aceea. Platforma așteaptă cel mult 15 secunde; peste acest timp livrarea se consideră eșuată și se reîncearcă.

Livrări, reîncercări și dubluri

Răspunsul serverului tăuCe face platforma
2xxlivrat
410 Goneoprește destinația; nu mai trimite nimic
3xx (redirecționările nu se urmează), alt cod, eroare de rețea, peste 15 seșec: reîncearcă după pauza următoare

Pauzele dintre încercări sunt 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h, 24 h, fiecare cu o abatere mică (până la +10%), deci cel mult 10 încercări în aproximativ trei zile și jumătate. Un antet Retry-After (secunde sau dată HTTP) poate lungi pauza, până la 24 de ore, dar nu o scurtează. După a zecea încercare eșuată, destinația se oprește, iar Administratorii firmei primesc o notificare.

  • Aceeași livrare are mereu același webhook-id și același corp, la fiecare încercare și la „Retrimite”. Doar webhook-timestamp și semnătura se refac.
  • Pot apărea dubluri: dacă procesul se oprește în timpul unei trimiteri, livrarea se reia. Păstrează webhook-id-urile primite și ignoră-le pe cele deja tratate.
  • Ordinea nu e garantată. Folosește timestamp din corp sau citește documentul prin API, nu ordinea sosirii.
  • Corpul răspunsului tău nu se citește: contează doar codul și Retry-After.

Rotirea secretului

POST /v1/webhookuri/{id}/roteste-secret întoarce {"destinatie": {…}, "secret": "whsec_…"} cu un secret nou. Timp de 24 de ore după rotire, fiecare livrare poartă două semnături în webhook-signature, despărțite prin spațiu: cu secretul nou și cu cel vechi. Ai deci o zi să pui secretul nou pe server; codul de verificare de mai sus le acceptă pe amândouă fără schimbări.

Toate rutele

RutăCe face
GET /v1/webhookuridestinațiile firmei, fără secrete
POST /v1/webhookuriadaugă o destinație; 201 cu secretul, o singură dată
POST /v1/webhookuri/{id}/opresteoprește destinația; livrările în așteptare se anulează
POST /v1/webhookuri/{id}/roteste-secretsecret nou; cel vechi mai semnează 24 de ore
POST /v1/webhookuri/{id}/test202: un test.ping doar spre această destinație
GET /v1/webhookuri/{id}/livrarijurnalul livrărilor destinației (cele mai noi 100)
POST /v1/webhookuri/livrari/{id}/retrimite202: aceeași livrare, de la încercarea 1

Coduri comune: 401 fără sesiune, 403 fără rolul admin, 404 pentru o destinație a altei firme sau inexistentă, 409 cu {"eroare": "destinatie_oprita"} pe o destinație oprită, 503 cu {"eroare": "seif_neconfigurat"} când Seiful nu poate păstra secretul. O destinație oprită nu se mai pornește: adaugi una nouă.

Câmpurile unei destinații și ale unei livrări

Destinația: id, url, descriere, evenimente, activ, dezactivat_la și motiv (de exemplu oprita_de_admin, destinatia_a_raspuns_410, livrari_esuate), rotire_pana (până când mai semnează secretul vechi), creata_la.

Livrarea: id, destinatie_id, eveniment_id (valoarea din webhook-id), tip, incercare (1–10), stare (de_trimis, livrat, esuat, anulat), manual (test sau retrimitere), planificata_la, trimisa_la, cod (codul HTTP primit), durata_ms, eroare (de exemplu termen_depasit, dns_esuat, adresa_interna, conexiune_esuata), urmatoarea_la. Momentele sunt ISO 8601, în UTC.

Securitatea trimiterii

Platforma trimite doar la adrese https pe portul 443. Numele serverului se rezolvă înainte de trimitere, iar cererea pleacă doar dacă toate adresele găsite sunt publice; conexiunea merge apoi exact la adresa verificată, fără o a doua căutare DNS. Nu se urmează redirecționări. Secretul de semnare stă criptat în Seif și se folosește doar la semnare.

A fost utilă pagina?
Documentație Ilustromania · ediția 12.11.2026Generată din codul platformei