Consignor and 3PL¶
FG.AI WMS v1 ships 3PL-first: every document, every inventory record, every movement carries a consignor_id that identifies the owner of the goods. Direct-tenant operators (a retailer running their own warehouse) are supported as the degenerate case (one consignor per account). The architecture lives in WMS ADR-0041; the v1 priority decision lives in WMS ADR-0044.
This page covers the contract surface for upstream integrators feeding consignor-aware data into FG.AI WMS.
The model¶
DotID account ──► 3PL operator (top-level tenant)
│
├──► Consignor A (sub-tenant — owner of goods)
├──► Consignor B (sub-tenant — owner of goods)
└──► Consignor C (sub-tenant — owner of goods)
A direct-tenant retailer is the same model with exactly one consignor whose ID equals the account self-reference. Tooling treats both shapes uniformly.
Master data — consignor registration¶
Before any consignor-scoped document or inventory record is accepted, the consignor itself must be registered.
POST /wms-ingest/v1/master/consignors?mode=upsert
{
"consignor": {
"source_id": "acme-corp",
"display_name": "Acme Corporation",
"billing_config": {
"storage_fee_per_unit_day": "0.05",
"transaction_fee_per_movement": "0.02",
"currency": "USD"
},
"contact": {
"email": "warehouse-ops@acme.example",
"phone": "+1-555-0100"
},
"active_from": "2026-06-01T00:00:00Z",
"active_until": null,
"notes": "Self-service consignor; ships ambient electronics."
}
}
billing_config shape is informational in v1; per-consignor billing flows through the v1.7 finance-service per WMS ADR-0037. Storing the configuration in v1 keeps the migration story clean.
The account-self-as-consignor degenerate case is registered automatically at account creation; you do not need to register it explicitly.
consignor_id on every document¶
Every document endpoint accepts a consignor_id field (FRN URN, or the consignor source_id if the upstream prefers natural keys — FG.AI resolves to the FRN internally per the rules in Identity mapping):
POST /wms-ingest/v1/documents/sales-orders?mode=upsert
{
"sales_order": {
"source_id": "SO-2026-00042",
"consignor_id": "acme-corp", ◄── required
"ship_to_address_ref": "...",
"lines": [
{ "sku_ref": "ACME-WIDGET-RED", "qty": 4, "uom_ref": "EA" }
],
...
}
}
If consignor_id is missing on a document body, the request is rejected with a schema validation error (MISSING_REQUIRED_CONSIGNOR_ID) — not quarantined. This is a load-bearing distinction: a missing consignor cannot be inferred or auto-resolved; the upstream must send it.
If the referenced consignor does not exist in master data, the document is quarantined (UNRESOLVED_CONSIGNOR_REFERENCE) and surfaces in the supervisor queue. See Normalization and quarantine.
Inventory scoping¶
Inventory snapshots and movements carry consignor_id per row:
POST /wms-ingest/v1/inventory/snapshot?mode=bulk&scope=FULL
{
"snapshot": {
"warehouse_ref": "wh-tokyo-01",
"items": [
{ "consignor_id": "acme-corp", "sku_ref": "ACME-WIDGET-RED", "location_ref": "BIN-A1", "qty": 120 },
{ "consignor_id": "acme-corp", "sku_ref": "ACME-WIDGET-RED", "location_ref": "BIN-A2", "qty": 40 },
{ "consignor_id": "beta-mfg", "sku_ref": "BETA-GIZMO-V2", "location_ref": "BIN-C7", "qty": 16 }
]
}
}
Two consignors can share the same SKU code (the SKU is also scoped by consignor_id). The full inventory key in v1 is (account, consignor, sku, location, lot_id?).
Segregated mode (v1 default)¶
v1 ships in segregated mode — every consignor’s goods are confined to specific zones or bins. The Location master defines this:
POST /wms-ingest/v1/master/locations?mode=upsert
{
"location": {
"source_id": "ZONE-ACME-MAIN",
"kind": "ZONE",
"parent_ref": "wh-tokyo-01",
"consignor_assignment": "acme-corp", ◄── single-consignor zone
...
}
}
A location with consignor_assignment set only accepts inventory for that consignor; putaway and pick Rules enforce the assignment.
Commingled mode (FIFO across consignor ownership in a single bin) is deferred to v1.8 per WMS ADR-0044 §4. Until then, attempts to write inventory of one consignor into a bin assigned to another are quarantined (CONSIGNOR_ASSIGNMENT_VIOLATION).
Cross-consignor transfers¶
A TransferOrder may move goods between locations within the same consignor (intra-consignor — standard saga path) or across consignor ownership (cross-consignor — requires elevated permission and a 3PL admin role per DotID consignor_admin scope rules). The contract surface is the same; the chain service distinguishes the two cases at runtime.
POST /wms-ingest/v1/documents/transfer-orders?mode=upsert
{
"transfer_order": {
"source_id": "XFR-2026-00003",
"consignor_id": "acme-corp", ◄── source consignor
"destination_consignor_id": "acme-corp", ◄── intra-consignor transfer
"from_location_ref": "BIN-A1",
"to_location_ref": "BIN-B5",
...
}
}
For cross-consignor, destination_consignor_id differs from consignor_id. v1.8 will tighten the cross-consignor permission model.
Webhooks carry consignor_id¶
The three webhook event kinds all carry the relevant consignor identifier in their payloads:
document.state-changed—payload.consignor_idinventory.adjusted— per-rowconsignor_idmaster.normalization-conflict—payload.consignor_idwhen the conflicting record is consignor-scoped (consignor entity itself, scoped SKU, etc.)
Subscribers should filter by consignor_id when projecting webhook events into their own per-consignor stores.
Anti-patterns¶
Sending documents before the consignor is registered. Results in quarantine. Always upsert /master/consignors first.
Cross-consignor inventory writes via a single batch. Each row is permission-checked independently; a row with a consignor you are not authorized for fails individually. Group writes by consignor when the upstream is a consignor-admin caller; group freely when the upstream is a 3PL operator caller.
Treating consignor_id as optional in v1 because behavior activated late. The v1 schema requires consignor_id on every document and inventory row from launch per ADR-0044 §2 Phase 1. There is no quiet-default mode.