Idempotency

The ingest API has two idempotency keys that work together. Confusing them is the most common cause of duplicate state.

Surface

Key

Behavior on retry

Request

(partner_id, correlation_id)

Returns the same response shape; no re-processing.

Item

(partner_id, source_id)

Updated if source_version is higher; REPLAY if same or lower; created if absent.

Item (versioned)

(partner_id, source_id, source_version)

A submitted item with this triple already accepted returns REPLAY and does not mutate.

Request-level idempotency

Every request carries a correlation_id — a UUID v4 or v7 generated by the caller:

POST /wms-ingest/v1/master/skus?mode=upsert
X-Correlation-Id: 01J7Y6K1NQ3W2C0X4V0R5T6E7N
{ "items": [ ... ] }

A second request with the same (partner_id, correlation_id) returns the original response without re-processing the body. This makes retries on transient errors safe — the caller can replay the same request after a 500 or a connection timeout without risking duplicate inserts.

The same correlation_id reused across two different logical requests is a caller-side bug. Use a fresh UUID per logical action.

Item-level idempotency

Items inside a batch are deduplicated on (partner_id, source_id) regardless of the request’s correlation_id. This means:

  • Two requests pushing the same SKU with the same source_version → the second is REPLAY. No mutation.

  • Two requests pushing the same SKU with a higher source_version on the second → the second updates. The first stays ACCEPTED.

  • Two requests pushing the same SKU with a lower source_version on the second → the second is REPLAY (out-of-order delivery is safe).

How the two interact

Picture a batch of 100 SKUs sent twice (network hiccup, caller retried):

  • First request — all 100 items normalize; 95 ACCEPTED, 5 QUARANTINED. The response body is stored under (partner_id, correlation_id).

  • Second request with the same correlation_id — the stored response body is returned verbatim. No items are re-processed. The 5 quarantines are not re-evaluated.

  • Second request with a fresh correlation_id — items are re-evaluated. All 95 already-ACCEPTED items return REPLAY. The 5 previously-quarantined items are re-evaluated and may now succeed if the underlying dependency was fixed in the interim.

This dual-key behavior is intentional. Request-level idempotency makes raw network retries safe. Item-level idempotency lets you re-send a batch with a fresh correlation_id to retry quarantines without forcing the caller to filter out already-accepted items.

Generating correlation_id

UUID v7 is recommended:

  • Sorts by time, which makes operational triage easier (the first 8 bytes encode milliseconds since epoch).

  • 128 bits of identifier space, no collisions in practice.

UUID v4 is acceptable — fully random — and is what most existing language libraries default to. Both are supported.

import uuid
correlation_id = str(uuid.uuid4())  # or uuid7() in libraries that provide it
String correlationId = UUID.randomUUID().toString();

Retention

The receiving side retains idempotency state for 30 days by default. A correlation_id re-submitted after 30 days is treated as a fresh request — the original response is no longer cached. Within the 30-day window, the original response is returned verbatim.

source_id + source_version retention is permanent for non-tombstoned items. Tombstoned (INACTIVE) items retain their full history for audit and may be archived after the audit-log retention window.

Common mistakes

  • Regenerating correlation_id on retry. The retry layer should reuse the original. Generating a new UUID per attempt breaks request-level idempotency and causes the receiver to re-process the body.

  • Reusing correlation_id across logically distinct requests. A second logical batch needs a fresh UUID. Reuse is a bug.

  • Trusting correlation_id alone for deduplication. Item-level dedup happens regardless. A new correlation_id with the same items is safe and is the right pattern when retrying after the original request errored at the items layer.

  • Submitting an unchanged item without source_version. Without source_version, the second submit overwrites the first. With source_version, the second is a no-op REPLAY. Use source_version whenever the upstream can produce one.

Idempotency and async jobs

Submitting the same (partner_id, correlation_id) ?mode=bulk request twice returns the same job_id. Polling /jobs/{job_id} after the original has completed returns the completed state without re-running.

A new correlation_id on the same payload, however, creates a new job — the prior bulk records are not re-checked for REPLAY at the request layer (only at the item layer). For idempotent bulk submission, keep the same correlation_id across retries.