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

upsert

Steady-state. A trickle of changes from the upstream. Default for steady-state operations.

bulk

Initial onboarding load — millions of rows. Returns 202 Accepted immediately with a job_id; the caller polls /jobs/{job_id}.

full-refresh

Periodic full re-sync of one collection. Items not in the payload are marked INACTIVE. Use with care; full-refresh is for restoring known good state, not for routine sync.

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_id assigned and returned.

  • Existing (partner_id, source_id) with higher incoming source_version → updated.

  • Existing (partner_id, source_id) with same or lower source_versionREPLAY, 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

PENDING

Job accepted, not yet started.

RUNNING

Workers are processing.

COMPLETED

All items processed; no errors.

COMPLETED_WITH_ERRORS

All items processed; some quarantined or rejected. Page through errors_url.

FAILED

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_version is 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 upsert instead. Sending one updated SKU as a full-refresh payload of one item will tombstone every other SKU for the partner.

  • Retry of a failed upsert batch. Use upsert with the same correlation_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

upsert

Batch of ≤ ~10k items, small payload

upsert

Onboarding load with > 10k items

bulk

Restoring an entire collection after an incident

full-refresh (with eyes open)

Periodic reconciliation pass

upsert of changed items, not full-refresh

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.