Webhook-uri către clienții 3PL
notso WMS poate anunța sistemele unui client 3PL despre propriile lui mărfuri: o comandă expediată (order.shipped), anulată (order.cancelled), livrată (order.delivered), pusă în așteptare (order.held) sau scoasă din așteptare (order.released) ori o recepție finalizată (receiving.completed). Endpointurile se configurează la Clienți → Webhook-uri, per client. Fiecare livrare este o cerere POST cu corp JSON, semnată cu un secret cunoscut doar de client și de WMS.
Această pagină descrie ce trimite WMS, cum verificați semnătura și ce reguli se aplică adresei destinației.
Corpul livrării
Corpul este un obiect JSON cu cinci câmpuri, în această ordine: event, event_id, occurred_at, sent_at și data. Câmpul data conține doar datele clientului căruia îi aparține endpointul.
{
"event": "order.shipped",
"event_id": "3f2b8c1e-6a4d-4e0b-9c57-1d2e3f4a5b6c",
"occurred_at": "2026-10-12T09:15:00+03:00",
"sent_at": "2026-10-12T09:15:02+03:00",
"data": { /* depinde de eveniment */ }
}event_id(UUID) identifică evenimentul din afaceri și este același la fiecare încercare, la fiecare reîncercare și la fiecare retrimitere, pentru toate endpointurile clientului. Același identificator este trimis și în antetulX-Wms-Event-Id.occurred_ateste momentul în care s-a petrecut faptul: expedierea (order.shipped), finalizarea recepției (receiving.completed), decizia asupra cererii de retur (return.authorized,return.rejected), momentul în care returul a fost asociat cererii (return.received), momentul în care unitățile au fost propuse clientului (disposition.awaiting_decision), momentul în care a fost ridicată alerta (stock.low,stock.expiring), momentul anulării și momentul livrării comenzii (order.cancelled,order.delivered), momentul în care așteptarea a devenit activă (order.held) și momentul în care s-a încheiat (order.released). Nu se schimbă între încercări.sent_ateste momentul acestei încercări și se schimbă la fiecare încercare.
La apăsarea butonului Testează, WMS trimite o livrare cu evenimentul webhook.test, semnată la fel ca una reală.
Evenimentul receiving.completed
Se trimite când depozitul finalizează o recepție. Câmpul data conține, în această ordine: session_id, session_number, reference (documentul furnizorului, sau null), completed_at, asn_id, asn_reference și lines.
asn_idșiasn_referencenumesc avizul de expediție (ASN) al clientului pe care s-a făcut recepția; suntnullcând recepția nu a pornit de la un aviz.- Fiecare linie conține
sku,received_quantity,batch_number,accepted_quantityșidamaged_quantity.accepted_quantityeste cantitatea bună, primită în stoc (inclusiv marfa reținută pe care depozitul a eliberat-o după aprobare);damaged_quantityeste marfa deteriorată sau expirată încă reținută. Marfa respinsă și marfa bună încă în așteptarea aprobării fac parte dinreceived_quantity, dar nu și din celelalte două cifre.
{
"event": "receiving.completed",
"event_id": "9a1d7e52-0c3b-4f68-8d21-5b4e6f7a8c90",
"occurred_at": "2026-10-12T09:15:00+03:00",
"sent_at": "2026-10-12T09:15:02+03:00",
"data": {
"session_id": 31,
"session_number": "RCV-2026-0031",
"reference": "FACT-ASN-77",
"completed_at": "2026-10-12T09:15:00+03:00",
"asn_id": 42,
"asn_reference": "ASN-2026-0042",
"lines": [
{ "sku": "A-1", "received_quantity": 5, "batch_number": "B-100", "accepted_quantity": 5, "damaged_quantity": 0 },
{ "sku": "A-2", "received_quantity": 2, "batch_number": null, "accepted_quantity": 0, "damaged_quantity": 2 }
]
}
}Câmpurile noi sunt adăugate la sfârșit sau în locul indicat, fără a le schimba pe cele existente; un receptor care ignoră câmpurile necunoscute continuă să funcționeze.
Evenimentul stock.low
Se trimite când stocul disponibil al unui produs al clientului ajunge la pragul pe care clientul l-a ales pentru el (în portal, la Stoc → Alerte de stoc), sau sub el. Se trimite o singură dată pentru fiecare episod: un nou eveniment apare doar după ce stocul a revenit peste prag și a scăzut din nou. Revenirea peste prag nu trimite nimic. Câmpul data conține, în această ordine: alert_id, sku, warehouse, available, on_hand, threshold și status.
alert_ididentifică alerta; o retrimitere a aceleiași livrări repetă aceleași cifre.warehouseeste codul depozitului pentru care s-a stabilit pragul, saunullcând pragul este valabil pentru toate depozitele.availableeste cantitatea disponibilă pentru comenzi în momentul ridicării alertei;on_handeste stocul fizic din același moment (include marfa blocată, de exemplu în carantină);thresholdeste pragul în vigoare atunci.statusesteopen.
{
"event": "stock.low",
"event_id": "5c0e8a13-7b2f-4d94-a6e1-3f8b9c0d1e2a",
"occurred_at": "2026-10-18T10:00:00+03:00",
"sent_at": "2026-10-18T10:00:01+03:00",
"data": {
"alert_id": 17,
"sku": "A-1",
"warehouse": null,
"available": 5,
"on_hand": 8,
"threshold": 5,
"status": "open"
}
}Evenimentul stock.expiring
Se trimite când un lot al clientului expiră în fereastra aleasă de client (implicit 30 de zile; 0 oprește alerta). Se trimite o singură dată pentru fiecare lot, depozit și dată de expirare; marfa deja expirată nu declanșează evenimentul. Câmpul data conține, în această ordine: alert_id, sku, warehouse, lot_number, batch_number, expiry_date, days_left, quantity și status.
warehouseeste codul depozitului în care se află lotul, saunull.lot_numberșibatch_numberpot finullcând stocul nu are lot sau serie.expiry_dateeste data expirării (YYYY-MM-DD), iardays_lefteste numărul de zile rămase în momentul ridicării alertei (0în ziua expirării).quantityeste cantitatea totală a lotului în acel depozit, în momentul ridicării alertei.statusesteopen.
{
"event": "stock.expiring",
"event_id": "8d3f1b64-2a9c-4e75-b0d8-6c7e8f9a0b1c",
"occurred_at": "2026-10-18T10:00:00+03:00",
"sent_at": "2026-10-18T10:00:01+03:00",
"data": {
"alert_id": 18,
"sku": "A-2",
"warehouse": "W1",
"lot_number": "L-77",
"batch_number": "B-77",
"expiry_date": "2026-10-28",
"days_left": 10,
"quantity": 7,
"status": "open"
}
}Alertele sunt evaluate din oră în oră, deci un eveniment poate ajunge cu până la o oră după ce stocul a atins pragul sau a intrat în fereastră.
Evenimentul order.cancelled
Se trimite când o comandă a clientului este anulată, indiferent cine a anulat-o: clientul, echipa depozitului sau magazinul online. Se trimite o singură dată pentru fiecare comandă; o a doua anulare a aceleiași comenzi nu trimite nimic. Dacă marfa deja culeasă trebuie readusă pe raft, evenimentul se trimite când anularea este încheiată. Câmpul data conține, în această ordine: order_number, external_order_id, cancelled_at, reason și cancelled_by.
order_numbereste numărul comenzii în WMS;external_order_ideste identificatorul comenzii în sistemul clientului sau în magazin, saunull.cancelled_ateste momentul anulării (același cuoccurred_at).reasoneste motivul înregistrat al anulării, saunullcând nu s-a dat unul.cancelled_byspune cine a anulat:client(clientul sau integrarea lui),staff(echipa depozitului sau un flux automat din WMS) sauplatform(anularea a venit din magazinul online).
{
"event": "order.cancelled",
"event_id": "b4e7a2d0-1c58-4f93-8a6b-7d9e0f1a2b3c",
"occurred_at": "2026-10-18T11:30:00+03:00",
"sent_at": "2026-10-18T11:30:01+03:00",
"data": {
"order_number": "CMD-5001",
"external_order_id": "EXT-5001",
"cancelled_at": "2026-10-18T11:30:00+03:00",
"reason": "Clientul a renunțat",
"cancelled_by": "client"
}
}Evenimentul order.delivered
Se trimite când o comandă trece în starea livrat, de exemplu după confirmarea curierului. Se trimite o singură dată pentru fiecare comandă. Câmpul data conține, în această ordine: order_number, external_order_id, delivered_at, awb, carrier și carrier_actual.
order_numberșiexternal_order_idau același înțeles ca laorder.cancelled.delivered_ateste momentul în care comanda a fost marcată ca livrată (același cuoccurred_at), nu momentul expedierii.awbeste numărul de expediere, saunull;carriereste curierul, saunull;carrier_actualeste curierul real care transportă coletul, saunull. Când comanda este rezervată printr-un intermediar (de exemplucoleteonline),carrierrămâne codul intermediarului, iarcarrier_actualarată curierul real (de exemplusameday); pentru restul comenzilor cele două sunt egale.
{
"event": "order.delivered",
"event_id": "c5f8b3e1-2d69-4aa4-9b7c-8e0f1a2b3c4d",
"occurred_at": "2026-10-18T14:20:00+03:00",
"sent_at": "2026-10-18T14:20:01+03:00",
"data": {
"order_number": "CMD-5002",
"external_order_id": "EXT-5002",
"delivered_at": "2026-10-18T14:20:00+03:00",
"awb": "AWB123",
"carrier": "cargus",
"carrier_actual": "cargus"
}
}Evenimentul order.held
Se trimite când o comandă a clientului ajunge efectiv în așteptare: clientul a cerut așteptarea în portal sau echipa depozitului a pus-o, iar depozitul nu va începe pregătirea ei până nu este scoasă din așteptare. O cerere a clientului pentru o comandă la care depozitul a început deja lucrul nu trimite nimic până când depozitul decide să oprească lucrul; dacă decide să termine lucrul, se trimite order.released cu outcome declined. Câmpul data conține, în această ordine: order_number, external_order_id, held_at, reason și held_by.
order_numberșiexternal_order_idau același înțeles ca laorder.cancelled.held_ateste momentul în care așteptarea a devenit activă (același cuoccurred_at): momentul cererii, sau momentul deciziei depozitului de a opri lucrul.reasoneste motivul scris de cine a pus comanda în așteptare.held_byspune cine a cerut așteptarea:client(clientul, din portal) saustaff(echipa depozitului).
{
"event": "order.held",
"event_id": "d6a9c4f2-3e7a-4bb5-8c8d-9f1a2b3c4d5e",
"occurred_at": "2026-10-19T10:05:00+03:00",
"sent_at": "2026-10-19T10:05:01+03:00",
"data": {
"order_number": "CMD-5001",
"external_order_id": "EXT-5001",
"held_at": "2026-10-19T10:05:00+03:00",
"reason": "Clientul a cerut oprirea",
"held_by": "client"
}
}Evenimentul order.released
Se trimite când o așteptare se încheie: a fost scoasă din așteptare de client sau de depozit, sau depozitul a respins o cerere a clientului pentru o comandă la care lucrul era deja început. Câmpul data conține, în această ordine: order_number, external_order_id, released_at, outcome și released_by.
order_numberșiexternal_order_idau același înțeles ca laorder.cancelled.released_ateste momentul în care așteptarea s-a încheiat (același cuoccurred_at): momentul scoaterii din așteptare, sau momentul deciziei depozitului pentrudeclined.outcomeestereleased(așteptarea a fost scoasă) saudeclined(depozitul a continuat lucrul și nu a oprit comanda).released_byspune cine a încheiat așteptarea:clientsaustaff. Pentrudeclinedeste întotdeaunastaff.
{
"event": "order.released",
"event_id": "e7bad5a3-4f8b-4cc6-9d9e-0a2b3c4d5e6f",
"occurred_at": "2026-10-19T14:40:00+03:00",
"sent_at": "2026-10-19T14:40:01+03:00",
"data": {
"order_number": "CMD-5001",
"external_order_id": "EXT-5001",
"released_at": "2026-10-19T14:40:00+03:00",
"outcome": "released",
"released_by": "client"
}
}Antete
| Antet | Conținut |
|---|---|
Content-Type | application/json |
X-Wms-Event | Numele evenimentului, de exemplu order.shipped. |
X-Wms-Event-Id | Identificatorul evenimentului (UUID), identic cu event_id din corp. Același la toate încercările, reîncercările și retrimiterile; folosiți-l pentru deduplicare. |
X-Wms-Delivery-Id | Identificator unic al încercării de livrare (UUID v4). Fiecare încercare, reîncercare sau retrimitere are altul. |
X-Wms-Timestamp | Momentul trimiterii, în secunde Unix. |
X-Wms-Signature-V2 | v2= urmat de HMAC-SHA256 în hexazecimal, calculat ca mai jos. |
X-Wms-Signature | Semnătura veche: HMAC-SHA256 în hexazecimal peste corpul brut. Se trimite doar în modul both. |
Semnătura v2
Semnătura v2 leagă corpul, identificatorul, momentul și evenimentul. Octeții semnați sunt cinci linii ASCII, separate prin câte un singur LF, fără LF după ultima:
wms-webhook-v2
<X-Wms-Delivery-Id>
<X-Wms-Timestamp>
<X-Wms-Event>
<sha256 în hexazecimal al corpului brut>Valoarea antetului este v2= urmat de HMAC-SHA256(secret, octeții de mai sus), în hexazecimal cu litere mici. Prima linie, wms-webhook-v2, separă semnătura v2 de cea veche: o semnătură veche nu poate fi prezentată ca v2.
Semnătura veche din X-Wms-Signature este HMAC-SHA256(secret, corpul brut). Nu protejează împotriva reluării cererilor; folosiți-o doar cât timp receptorul nu a fost actualizat.
Lista de verificare a receptorului
- Citiți corpul brut, exact cum a sosit. Nu îl serializați din nou.
- Dacă lipsește sau este invalid vreun antet v2 (
X-Wms-Delivery-Id,X-Wms-Timestamp,X-Wms-Event,X-Wms-Signature-V2), respingeți cererea. Nu reveniți la semnătura veche. - Recalculați semnătura v2 și comparați-o cu antetul într-un mod cu timp constant (de exemplu
hash_equalsîn PHP saucrypto.timingSafeEqualîn Node.js). - Acceptați doar cereri al căror
X-Wms-Timestampdiferă de ceasul dumneavoastră cu cel mult 300 de secunde (5 minute), în ambele sensuri. Păstrați ceasul serverului sincronizat prin NTP. - Rețineți fiecare
X-Wms-Delivery-Idcel puțin 600 de secunde (10 minute, dublul ferestrei de 300 de secunde) și ignorați repetările: răspundeți cu2xxfără a procesa din nou. - Interpretați JSON-ul abia după toate verificările de mai sus.
- Răspundeți cu
2xxcât mai repede. Orice alt cod este înregistrat ca eroare a endpointului.
Fereastra de 300 de secunde limitează cât timp poate fi reutilizată o cerere interceptată; retenția de 600 de secunde garantează că o cerere reluată în fereastră este recunoscută ca duplicat.
Modul de semnare (signature_mode)
Fiecare endpoint are un câmp signature_mode:
both(implicit): WMS trimite șiX-Wms-Signature, și antetele v2. Receptoarele existente continuă să funcționeze.v2_only: WMS trimite doar antetele v2 și nu mai trimiteX-Wms-Signature.
Treceți endpointul pe v2_only după ce receptorul verifică semnătura v2. Modul se alege în fereastra de creare sau de editare a webhook-ului, sub Semnături trimise.
Secretul
Secretul de semnare se generează la crearea endpointului și se afișează o singură dată; WMS îl păstrează criptat și nu îl mai poate arăta. Dacă l-ați pierdut, generați unul nou.
Rotirea secretului are efect imediat și nu există o perioadă de suprapunere: după rotire, livrările sunt semnate doar cu secretul nou, iar cel vechi nu mai este valid. Actualizați mai întâi receptorul, apoi rotiți secretul.
Reguli pentru destinație
Destinația este verificată la salvare, la trimiterea de test și la fiecare livrare:
- Adresa trebuie să folosească HTTPS, cu un nume de domeniu sau o adresă IP publică. Nu sunt acceptate
http, utilizator sau parolă în URL, fragmente (#) și nici nume fără punct, cum estelocalhost. - Numele este rezolvat o singură dată, iar toate răspunsurile sunt verificate. Dacă oricare dintre adrese este privată, locală, de legătură, rezervată sau de documentație, livrarea este refuzată (
address_blocked). Conexiunea se face apoi exact la adresa verificată (DNS fixat), deci o schimbare ulterioară a înregistrărilor DNS nu poate redirecționa livrarea către o rețea internă. - Redirecționările nu sunt urmate. Un răspuns
3xxeste înregistrat caredirect_blocked. Configurați adresa finală direct. - Certificatul serverului este verificat (TLS 1.2 sau mai nou). Un proxy configurat pe serverul WMS nu este folosit; livrarea este refuzată cu
proxy_unsupported. - Cererea așteaptă cel mult 3 secunde conectarea și 5 secunde în total. Din răspuns se acceptă cel mult 1 MiB, iar corpul nu este păstrat.
- După 10 eșecuri consecutive ale endpointului, acesta este oprit și trebuie reactivat manual din pagina Webhook-uri. La reactivare, adresa este verificată din nou. Evenimentele apărute cât timp endpointul este oprit nu se pierd: rămân în așteptare și se trimit după reactivare (vezi mai jos).
Reîncercări, stări și deduplicare
O livrare eșuată din partea endpointului (răspuns diferit de 2xx, depășirea timpului, eroare de transport, adresă blocată) este reîncercată automat, la intervale crescătoare după încercarea eșuată: 1 min, 5 min, 30 min, 2 h, 6 h. Sunt cel mult șase încercări în total; după a șasea eșuată, livrarea devine dead și nu mai este reîncercată automat.
| Stare | Înțeles |
|---|---|
delivered | Endpointul a răspuns cu 2xx; lanțul s-a încheiat. |
retrying | Încercarea a eșuat și urmează alta, la ora afișată în pagina Webhook-uri. Fără oră, evenimentul așteaptă reactivarea endpointului oprit. |
dead | Nu mai urmează nicio încercare automată: au fost folosite toate cele șase, sau WMS nu a putut construi evenimentul (payload_invalid, ownership_mismatch, secret_missing, proxy_unsupported, transport_unavailable). |
O refuzare din partea WMS (conținut invalid, date ale altui client) este dead imediat, nu mută contorul de erori al endpointului și nu oprește endpointul. Fiecare încercare eșuată din partea endpointului se adaugă contorului de erori consecutive.
Un membru al echipei WMS poate retrimite din pagina Webhook-uri (sau cu client-webhooks:resend) o livrare dead sau delivered. Retrimiterea este o încercare nouă, cu același event_id și același occurred_at, dar cu X-Wms-Delivery-Id și X-Wms-Timestamp noi. Conținutul se construiește din datele de acum, nu din cele de la prima trimitere, deci poate diferi de cel trimis atunci.
Deduplicați după event_id (antetul X-Wms-Event-Id). Aceeași livrare poate ajunge de mai multe ori: o reîncercare după un răspuns pierdut, o retrimitere. X-Wms-Delivery-Id se schimbă la fiecare încercare și nu este potrivit pentru asta. Rețineți fiecare event_id procesat cât timp aveți nevoie să recunoașteți un duplicat (cel puțin câteva zile, deoarece ultima reîncercare vine la aproximativ 8 ore după prima), și răspundeți cu 2xx fără a procesa din nou un event_id deja văzut.
Coduri de rezultat
WMS înregistrează pentru fiecare livrare un cod dintr-o listă închisă, fără textul erorii sau corpul răspunsului. Codurile de mai jos sunt cele afișate în pagina Webhook-uri.
| Cod | Înțeles |
|---|---|
delivered | Endpointul a răspuns cu 2xx. |
http_error | Endpointul a răspuns cu un cod HTTP de eroare (stare în coloana separată). |
redirect_blocked | Endpointul a răspuns cu 3xx; redirecționarea nu este urmată. |
url_invalid | Adresa salvată nu respectă regulile de mai sus. |
dns_unresolved | Numele de domeniu nu poate fi rezolvat. |
address_blocked | Numele sau adresa duc către o rețea privată sau specială. |
connect_failed | Conexiunea la endpoint a eșuat. |
timeout | Endpointul nu a răspuns la timp. |
tls_error | Certificatul sau conexiunea TLS sunt invalide. |
transport_error | Altă eroare de transport, inclusiv un răspuns mai mare de 1 MiB. |
proxy_unsupported | Serverul WMS are un proxy configurat, care nu este suportat. |
transport_unavailable | Transportul securizat nu este disponibil pe serverul WMS. |
internal_error | Eroare internă la trimitere. |
secret_missing | Endpointul nu are secret de semnare. |
payload_invalid | Conținutul evenimentului nu a putut fi construit. |
ownership_mismatch | Evenimentul conținea date ale altui client și nu a fost trimis. |
Primele zece coduri țin de endpoint sau de configurarea lui și numără la oprirea automată; ultimele șase țin de WMS și nu o influențează.
Alte webhook-uri trimise de WMS
Nodurile webhook din automatizări și regulile de notificare de tip webhook folosesc același transport: adresele private sunt refuzate, DNS este fixat, redirecționările nu sunt urmate și răspunsul este limitat la 1 MiB. Pentru ele, http către o adresă publică rămâne permis, pentru endpointurile configurate înainte de această regulă. Metoda unui nod de automatizare trebuie să fie GET, POST, PUT, PATCH sau DELETE; altfel nodul eșuează cu method_invalid. În jurnale și în rezultatele rulărilor apar doar codul de rezultat și originea destinației (schemă, gazdă și port), niciodată calea, parametrii sau antetele cu credențiale.