Webhooks¶
FG.AI WMS emits webhooks back to the upstream so that the source system can mirror FG.AI’s view of the world. Three events cover the common cases: document state changes, inventory adjustments, and normalization conflicts.
Events¶
Event |
When emitted |
|---|---|
|
A document moves to |
|
FG.AI reconciliation produces an inventory adjustment vs the caller’s prior baseline. |
|
An item was |
The webhook payload schemas are documented in the OpenAPI reference under /webhooks/* paths.
Delivery semantics¶
Property |
Value |
|---|---|
Direction |
FG.AI WMS → caller’s registered URL |
Transport |
HTTPS POST |
Body format |
JSON ( |
Signing |
HMAC-SHA256 over the raw body, header |
Delivery |
At-least-once, exponential-backoff retry |
Ordering |
Serialized per |
Retry schedule (typical): 0s → 5s → 30s → 2m → 10m → 1h → ... → 24h cap → DLQ. After 24h of failed retries, the event drains to FG.AI’s DLQ and a supervisor is alerted on the FG.AI side.
Verifying the signature¶
The receiver MUST verify the signature before parsing the body. Skipping verification is a serious vulnerability — anyone who can reach the webhook URL can inject events.
import hmac, hashlib
def verify(raw_body: bytes, header_value: str, secret: bytes) -> bool:
if not header_value.startswith("sha256="):
return False
expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
received = header_value[len("sha256="):]
return hmac.compare_digest(expected, received)
public static boolean verify(byte[] rawBody, String headerValue, byte[] secret) {
if (!headerValue.startsWith("sha256=")) return false;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret, "HmacSHA256"));
byte[] expected = mac.doFinal(rawBody);
String expectedHex = HexFormat.of().formatHex(expected);
String receivedHex = headerValue.substring("sha256=".length());
return MessageDigest.isEqual(expectedHex.getBytes(), receivedHex.getBytes());
}
Constant-time comparison (hmac.compare_digest / MessageDigest.isEqual) is required to prevent timing attacks.
Idempotency¶
Every webhook payload carries a correlation_id. The receiver must be idempotent on (planner_id, correlation_id) — a re-delivered webhook (network error on the original, retried by FG.AI) returns 200 OK with no duplicate effect.
planner_id is FG.AI’s identifier for the emitting service; the value is fixed per environment. The combination (planner_id, correlation_id) uniquely identifies a webhook event for the receiver’s lifetime.
Acceptable responses¶
Status |
FG.AI behavior |
|---|---|
|
Treat as success. Drop from the retry queue. |
|
Treat as terminal. Move to DLQ. Do not retry. |
|
Retry per the exponential backoff schedule. |
|
Honor |
Returning 4xx to a webhook tells FG.AI not to retry. Use this only when the receiver has logically rejected the event (e.g. the document does not exist on the upstream side); never use it for transient errors.
Event payload shapes¶
document.state-changed¶
{
"event": "document.state-changed",
"correlation_id": "01J7Y6K1NQ3W2C0X4V0R5T6E7N",
"planner_id": "fgai-wms",
"occurred_at": "2026-05-22T03:14:01Z",
"document_ref": { "type": "SHIPPER", "source_id": "SH-2026-000183", "internal_id": "fgai-doc-..." },
"from_state": "RELEASED",
"to_state": "PICKING",
"actor": { "kind": "SYSTEM", "id": "fgai-wes" }
}
inventory.adjusted¶
{
"event": "inventory.adjusted",
"correlation_id": "01J7Y6K1NQ3W2C0X4V0R5T6E7P",
"planner_id": "fgai-wms",
"occurred_at": "2026-05-22T03:14:01Z",
"warehouse_id": "WH-Tokyo-01",
"sku": "SKU-WIDGET-RED-LG",
"location": "A.12.3.1",
"lot": "LOT-2026-04-15",
"qty_delta": -3,
"reason": "CYCLE_COUNT_RECONCILE",
"ref": { "kind": "CYCLE_COUNT", "id": "cc-2026-019" }
}
master.normalization-conflict¶
{
"event": "master.normalization-conflict",
"correlation_id": "01J7Y6K1NQ3W2C0X4V0R5T6E7Q",
"planner_id": "fgai-wms",
"occurred_at": "2026-05-22T03:14:01Z",
"entity_kind": "sku",
"source_id": "SKU-FOO-001",
"quarantine_id": "qn-01HY...",
"reason": "Unknown UoM 'KG'. Register via /master/uoms first.",
"submitted_payload": {}
}
Registration¶
Webhook URLs are configured per partner_id in the FG.AI partner registry. Each event may optionally have a different URL; the default is one URL per partner_id that receives all event kinds. Multi-URL configuration is supported but adds operational complexity.
URL changes go through the partner registry and require operator approval — they are not self-service. The HMAC secret is rotated at the same time, with a 24h dual-secret window.
What if I do not subscribe to webhooks?¶
Webhooks are recommended but not required. The same information can be polled:
document.state-changed→ pollGET /documents/{type}/{source_id}(or a paginatedGET /documents?since=…query).inventory.adjusted→ poll an inventory query endpoint.master.normalization-conflict→ pollGET /quarantine?state=PENDING.
Polling is acceptable for low-volume integrations or strictly outbound-only network postures. For everything else, webhooks are dramatically more efficient and the recommended default.