Skip to main content

Change feed

Everything that changes in the WMS — a product edited, goods received, an order shipped — is written to the change feed. Your system reads the feed regularly, applies the changes and confirms how far it has read. There are no pushes from the WMS: your system asks.

Reading the feed needs the Change feed read capability. Which data types appear in the feed is listed in Integration data types. The feed carries only changes made in the WMS: what your own system imported does not come back to it.

The reading loop​

  1. Get your checkpoint — the last event you confirmed:

    GET /api/v1/integrations/checkpoints/ERP_MAIN
    {"id": 1, "externalSystemCode": "ERP_MAIN", "lastAcknowledgedEventId": 4211}

    On the very first run lastAcknowledgedEventId is missing: read from the beginning, or start from a resync.

  2. Read a page of events after that event:

    GET /api/v1/integrations/change-feed?externalSystemCode=ERP_MAIN&limit=100&fromEventId=4211
    • limit — required, from 1 to 500.
    • fromEventId — the events after this one are returned; leave it out to start from the beginning.
    • entityTypes — optional; repeat it to read only some data types: &entityTypes=RECEIPT&entityTypes=OUTBOUND_ORDER. Do not confirm events read this way — see the rules below.
    {
    "externalSystemCode": "ERP_MAIN",
    "events": [
    {
    "event": {
    "eventId": 4212,
    "entityType": "RECEIPT",
    "internalEntityId": "01a17a6f-60a6-77be-b765-b7e9341d7efe",
    "scopeType": "WAREHOUSE",
    "warehouseId": "01a16373-d6be-7746-8eab-fc8e447bfea8",
    "entityVersion": 3,
    "occurredAt": "2026-10-27T10:02:11.418Z"
    },
    "payload": {
    "id": {"int": "01a17a6f-60a6-77be-b765-b7e9341d7efe", "ext": "rcpt-2026-0105"},
    "status": "RECEIVED"
    }
    }
    ]
    }

    Events come in ascending eventId order. event.internalEntityId is the WMS identifier of the record, the same value as payload.id.int. payload is the current state of the record at the moment you read it, with identifiers as {int, ext} pairs (see Building the payload). If a record changed several times, each event brings its latest state.

  3. Apply the events in your system.

  4. Confirm the last event of the page:

    POST /api/v1/integrations/checkpoints/acknowledge
    {"checkpointId": 1, "eventId": 4212}
  5. Repeat from step 2 with fromEventId set to that event, until a page comes back empty. Then wait — a minute is usual — and start again.

Rules to rely on​

  • An event can arrive twice. If your system applies a page and stops before confirming it, the page comes again on the next run. Make applying an event safe to repeat: look the record up by its identifier and overwrite it, never add to it blindly.
  • There is one checkpoint for all data types. Read the feed without entityTypes and skip the types you do not need in your own code. If you confirm the last event of a page filtered by type, the events of the other types before it are skipped for good.
  • The checkpoint only moves forward. Confirming the event that is already confirmed is accepted and changes nothing; confirming an older event is rejected with 400.
  • Do not skip a failed event silently. Either stop and retry the page later, or put the event into your own retry queue before you confirm the page.
  • If the WMS asks for a full resync of your system, reading the feed returns 400 until you complete a resync.

Code​

The first function runs the loop above until the feed is empty. The second moves the checkpoint to the event a resync returned, so that reading continues right after the snapshot. Both use the client from API basics.

// Reads the change feed from the last confirmed event to the end, hands every
// event to `handleEvent` and confirms each page. Run it on a schedule, e.g. every minute.
// The same event can arrive more than once, so `handleEvent` must be safe to repeat.
// Your system has one checkpoint for all data types: read every type here and
// skip the ones you do not need inside `handleEvent`.
async function pollChangeFeed(handleEvent, {limit = 100} = {}) {
const code = config.externalSystem;
const {body: checkpoint} = await wmsRequest('GET', `/api/v1/integrations/checkpoints/${code}`);
let lastEventId = checkpoint.lastAcknowledgedEventId ?? null;
let handled = 0;

for (;;) {
const query = {externalSystemCode: code, limit};
if (lastEventId !== null) query.fromEventId = lastEventId; // events after this one
const {body} = await wmsRequest('GET', '/api/v1/integrations/change-feed', {query});
if (body.events.length === 0) return handled;

for (const {event, payload} of body.events) {
await handleEvent(event, payload); // throw to stop: the page stays unconfirmed
handled += 1;
}
lastEventId = body.events.at(-1).event.eventId;
await wmsRequest('POST', '/api/v1/integrations/checkpoints/acknowledge', {
body: {checkpointId: checkpoint.id, eventId: lastEventId},
});
}
}

// After a resync: moves the checkpoint to the snapshot's event, so the next
// pollChangeFeed starts right after the snapshot. Never moves it back.
async function startChangeFeedAt(eventId) {
const code = config.externalSystem;
const {body: checkpoint} = await wmsRequest('GET', `/api/v1/integrations/checkpoints/${code}`);
const current = checkpoint.lastAcknowledgedEventId ?? null;
if (current !== null && Number(current) >= Number(eventId)) return;
await wmsRequest('POST', '/api/v1/integrations/checkpoints/acknowledge', {
body: {checkpointId: checkpoint.id, eventId},
});
}