Sync modes¶
Every upsert endpoint supports three sync modes, selected via the mode query parameter. Pick the mode that matches the operation, not the one that feels safest — full-refresh in particular has destructive semantics that surprise newcomers.
The three modes¶
POST /wms-ingest/v1/master/skus?mode=upsert (default)
POST /wms-ingest/v1/master/skus?mode=bulk (async; returns 202 with job_id)
POST /wms-ingest/v1/master/skus?mode=full-refresh (items not in payload are tombstoned)
Mode |
When to use |
|---|---|
|
Steady-state. A trickle of changes from the upstream. Default for steady-state operations. |
|
Initial onboarding load — millions of rows. Returns |
|
Periodic full re-sync of one collection. Items not in the payload are marked |
The bulk_async_threshold advertised by GET /capabilities tells you when a synchronous call auto-switches to async. Synchronous calls above that size return 202 with a job_id regardless of which mode you asked for.
upsert (default)¶
The default mode. Each item in the payload is either created or updated:
New
(partner_id, source_id)→ created.internal_idassigned and returned.Existing
(partner_id, source_id)with higher incomingsource_version→ updated.Existing
(partner_id, source_id)with same or lowersource_version→REPLAY, no mutation.
Synchronous response:
{
"results": [
{ "source_id": "SKU-WIDGET-RED-LG", "status": "ACCEPTED", "internal_id": "fgai-sku-01HX..." },
{ "source_id": "SKU-FOO-001", "status": "QUARANTINED", "quarantine_id": "qn-01HY...",
"reason": "Unknown UoM 'KG'. Register via /master/uoms first." }
],
"summary": { "accepted": 1, "replay": 0, "quarantined": 1, "rejected": 0 }
}
Batch size is bounded by the request body limit (default 4 MiB per call). For larger loads, use bulk.
bulk (async)¶
Use for the initial onboarding load. The request body may contain millions of items; the server returns immediately with a job descriptor:
{
"job_id": "job-01HZ...",
"status_url": "/wms-ingest/v1/jobs/job-01HZ...",
"accepted_at": "2026-05-22T03:14:01Z"
}
Poll the status_url:
{
"job_id": "job-01HZ...",
"state": "COMPLETED_WITH_ERRORS",
"counts": { "total": 124000, "accepted": 123950, "replay": 0, "quarantined": 50, "rejected": 0 },
"started_at": "2026-05-22T03:14:01Z",
"finished_at": "2026-05-22T03:21:48Z",
"errors_url": "/wms-ingest/v1/jobs/job-01HZ.../errors"
}
Job states:
State |
Meaning |
|---|---|
|
Job accepted, not yet started. |
|
Workers are processing. |
|
All items processed; no errors. |
|
All items processed; some quarantined or rejected. Page through |
|
The job itself failed (storage error, internal exception). Re-submit. |
Page through errors via errors_url until has_more = false. Errors are paginated because a single job may produce thousands.
Bulk job retention is 7 days for the job record, 30 days for the error pages. Outside that window the records are archived and not queryable through this API.
full-refresh (destructive)¶
A full-refresh says: “for this collection (e.g. SKUs) under this partner_id, the items I am sending are the complete authoritative set. Anything not in this payload should be tombstoned.” Items absent from the payload move to lifecycle: INACTIVE.
POST /wms-ingest/v1/master/skus?mode=full-refresh
{
"items": [
{ "source_id": "SKU-A", ... },
{ "source_id": "SKU-B", ... }
]
}
If FG.AI previously held SKU-A, SKU-B, and SKU-C for this partner_id, after this call SKU-C is INACTIVE.
Use cases that justify full-refresh:
Restoring a known-good state after a major data-quality incident on the upstream.
Initial onboarding when no
source_versionis available — full-refresh is a clean reset.Periodic reconciliation on a slow collection (e.g. UoMs) where the upstream’s audit guarantees the payload is complete.
Use cases that do NOT justify full-refresh:
Steady-state sync of a partial change. Use
upsertinstead. Sending one updated SKU as afull-refreshpayload of one item will tombstone every other SKU for the partner.Retry of a failed
upsertbatch. Useupsertwith the samecorrelation_id— replay is safe.
A full-refresh that tombstones a document or inventory record currently in use is not silently auto-recoverable. The next planner action against that record will fail and route to a supervisor. Avoid this.
Async jobs and idempotency¶
Submitting the same (partner_id, correlation_id) bulk request twice returns the same job_id. Re-pulling /jobs/{job_id} is safe and required for poll-mode status. Re-submitting after a confirmed COMPLETED does not re-process — it returns the original job’s state.
Choosing a mode at a glance¶
Situation |
Mode |
|---|---|
Single item changed in the upstream |
|
Batch of ≤ ~10k items, small payload |
|
Onboarding load with > 10k items |
|
Restoring an entire collection after an incident |
|
Periodic reconciliation pass |
|
When in doubt, default to upsert. It is idempotent, it is non-destructive, and it composes with source_version to give safe out-of-order delivery.