Poll each stream by time window: ask for records updated since your cursor and up to the moment the run started, page through them, then move the cursor. This catches every change exactly once, even across restarts.
The polling pattern
- Read the stream's cursor. On the very first run, start from a sensible point such as 24 hours ago.
- Set
updatedSinceto the cursor minus a small overlap (2 minutes) to absorb clock skew and records written while you were paging. - Set
updatedUntilto the time the run started, so the window does not move while you page. - Page with
countandskip, sorted byupdatedAt, until a page returns fewer thancountrecords. - For each record, skip it if its event key is already processed; otherwise handle it.
- When every page is done, set the cursor to the run start time.
// poll.js – generic incremental poller
import { p17 } from './p17.js';
import * as state from './state.js';
const PAGE = 100;
const OVERLAP_MS = 2 * 60 * 1000;
export async function pollStream({ stream, path, listKey, idField, query = {}, handle }) {
const runStartedAt = new Date().toISOString();
const cursor = state.getCursor(stream) ?? new Date(Date.now() - 24 * 3600e3).toISOString();
const updatedSince = new Date(Date.parse(cursor) - OVERLAP_MS).toISOString();
for (let skip = 0; ; skip += PAGE) {
const page = await p17('GET', path, {
query: { ...query, updatedSince, updatedUntil: runStartedAt, order: 'updatedAt', count: PAGE, skip, keys: '*' },
});
const items = page[listKey] ?? [];
for (const item of items) {
const eventKey = `${stream}:${item[idField]}:${item.version ?? item.updatedAt}`;
if (state.wasProcessed(eventKey)) continue;
try {
await handle(item);
state.markProcessed(eventKey);
} catch (err) {
state.enqueueRetry(stream, { id: item[idField] }, err); // retried later, cursor still moves
}
}
if (items.length < PAGE) break;
}
state.setCursor(stream, runStartedAt);
}The same request as curl:
curl -s -G "$PIPE17_BASE_URL/shipping_requests" \ -H "X-Pipe17-Key: $PIPE17_INTEGRATION_KEY" \ --data-urlencode "status=readyForFulfillment" \ --data-urlencode "fulfillmentIntegrationId=<your integrationId>" \ --data-urlencode "updatedSince=2026-10-12T06:53:00Z" \ --data-urlencode "updatedUntil=2026-10-12T07:00:00Z" \ --data-urlencode "order=updatedAt" \ --data-urlencode "count=100" \ --data-urlencode "skip=0" \ --data-urlencode "keys=*"
Streams a 3PL connector polls
| Stream | Endpoint and filters | Interval | Handler (Chapter 7) |
|---|---|---|---|
| New and canceled work |
GET /shipping_requests with fulfillmentIntegrationId=<integrationId> and status=readyForFulfillment,canceled,canceledRestock
|
5 min | Send to your system, or answer the cancel (7.3) |
| Late cancels |
GET /shipping_requests with fulfillmentIntegrationId=<integrationId>, status=sentToFulfillment,partialFulfillment and exceptionType=eLateCancel
|
5 min | Cancel orders canceled after you received them (7.3) |
| Catalog | GET /products |
15–60 min | Create or update SKUs in your system (7.2) |
| Inbound shipments |
GET /arrivals with toLocationId=<your locations> and status=shipped
|
15 min | Create the inbound/ASN in your system (7.6) |
| Returns |
GET /returns with locationId=<your locations> and status=shipped
|
15 min | Create the RMA in your system (7.7) |
| Settings |
GET /integrations (Connector API Key) |
5 min | Refresh flow settings |
fulfillmentIntegrationId returns only the Shipping Requests routed to your integration, which is simpler and safer than filtering by location. Set updatedUntil about a minute in the past so records still being written are picked up on the next run.
Your own system needs polling too, for shipments and stock levels it does not push to you. Use the same cursor table with stream names such as wms.shipments and wms.inventory.
Polling tips
- Run one copy of each job at a time. Use a lock row or your scheduler's concurrency setting so two runs never share a window.
-
Ask for what you need. Default list responses return only a few fields. Pass
keys=*or a comma-separated field list to get line items and addresses. -
Use
updatedSince, notsince.sincefilters by creation time and misses changes to existing records. - Expect to see a record more than once. The overlap and webhooks (Chapter 6) both cause repeats; the event key makes them harmless.
Need Help?
If you need additional assistance:
- Use Ask Pippen, our AI agent, located at the top of the app page.
- Submit a support request with as much relevant detail as possible. Learn how to submit a request.
We're here to help you succeed with your operations.
Comments
0 comments