Polling Fulfillment Requests
The GET /fulfillment-requests endpoint is how a Source discovers new work (fulfillment requests and fulfillment orders assigned to it). This guide covers the right way to poll it — and how to avoid the single most expensive mistake on this endpoint: rebuilding Flxpoint's state locally.
Why this matters:it's tempting to build a full-mirror system — polling every FR every cycle, diffing against a local database, generating your own change feed. That approach burns API quota, adds latency, and introduces staleness. There's a better way.
The short version
- Poll
GET /fulfillment-requests?status=NEWevery 60–300 seconds. - For each FR you haven't seen before, acknowledge it via
POST /fulfillment-requests/{id}/acknowledge. - Track the
idvalues you've processed in your own storage — that's the dedupe key. - Use
statusfilters to get only what you need. Never pull the full dataset on every cycle.
Why not just use the webhook?
The obvious alternative is FulfillmentRequestProcessingEvent, and for a while it looks like it works. It does not fire when an order is auto-routed and its fulfillment requests are generated automatically, which is the most common setup by a wide margin. The eligibility check runs before the purchase order has its internal status, so the event is never queued at all.
This fails silently. There is no error, no retry and no dead letter: the event simply never arrives. An integration built on it waits forever and looks healthy while doing nothing. That is the failure mode this guide exists to avoid, so poll rather than subscribe until the behaviour changes.
The webhook does fire for fulfillment requests you create or process manually. If you already subscribe to it, keep the subscription but treat it as an optimisation on top of polling, never as your only trigger.
Already polling for order-level changes? Use GET /orders instead
Recent addition.GET /ordersnow acceptsorderUpdatedAt,fulfillmentRequestsUpdatedAt, andshipmentsUpdatedAtas ISO-8601 timestamps. Results are scoped to records updated afterthe cursor — across the order, its FRs, and its shipments in one call. If your reason for polling FRs was really "detect any change on any open order", this replaces the FR poll entirely. For the exact dated history of this endpoint, see its changelog inside Stoplight.
# Once per minute, pick up everything that changed since last cursor.
curl -G "https://api.flxpoint.com/orders" \
-H "X-API-TOKEN: YOUR_TOKEN" \
--data-urlencode "orderUpdatedAt=${LAST_SEEN}" \
--data-urlencode "fulfillmentRequestsUpdatedAt=${LAST_SEEN}" \
--data-urlencode "shipmentsUpdatedAt=${LAST_SEEN}"
# Advance LAST_SEEN to the max updated timestamp in the response
# before the next call. No local processed-set needed.Use this when your integration is an order-tracking system, a reporting mirror, or a cross-system reconciliation job. Use the FR-specific flow (below) when you're the supplier actively fulfilling requests — the FR state machine (acknowledge, cancel, ship) is still how new work is claimed.
What the endpoint does (and doesn't) give you
The endpoint supports filters by status and pagination by page / size. It does not currently support an updatedAfter / modifiedSince timestamp filter — a feature request tracked by the platform team.
Until that parameter exists, combine two mechanisms:
- Status filter to narrow the working set. Most integrations care about one of a few states (NEW, ACKNOWLEDGED, CANCELLED). Filter server-side.
- Local processed-set for dedupe. A single column (
flxpoint_fr_id TEXT PRIMARY KEY) is enough. On each poll, skip any ID already in the set.
Polling cadence
| Volume | Recommended poll interval | Notes |
|---|---|---|
| < 100 FRs / day | 5 minutes | Default safe. |
| 100 – 1,000 FRs / day | 1 – 2 minutes | Still well within 2 req/s per source token. |
| 1,000+ FRs / day | 60 seconds, pagination active | Use size=100; only paginate when the first page comes back full. |
Polling more often than every 30 seconds rarely helps — Flxpoint's own upstream workflows don't propagate state that fast in the normal case.
The right implementation
// Pseudo-code — adapt to your stack.
async function pullNewFRs() {
const processed = await db.loadProcessedIds(); // Set<string>
let page = 1;
while (true) {
const res = await flxGet(`/fulfillment-requests?status=NEW&page=${page}&size=100`);
const body = await res.json();
const items = body.items ?? [];
if (items.length === 0) return;
for (const fr of items) {
if (processed.has(String(fr.id))) continue; // dedupe
await handleFR(fr); // your business logic
await db.markProcessed(fr.id); // atomic, idempotent
}
if (items.length < 100) return; // last page
page++;
}
}
// Call every N seconds from a scheduler, cron, or worker queue.
// A single caller per token — do not parallelize pulls on the same token.Acknowledgement is your state machine
Once you've acknowledged an FR, it moves out of NEW on the Flxpoint side. That's your natural fence — next time you poll status=NEW, it won't come back. Treat acknowledgement as the single source of truth for "I've got this", not a local flag.
// Acknowledge as soon as you've durably persisted the FR
// to your side. If your handler crashes before ack, the FR
// will reappear on the next poll — that's a feature, not a bug.
await flxPost(`/fulfillment-requests/${fr.id}/acknowledge`);Prefer the dedicated actions over a bare update.Updating a fulfillment request is a full-object replace: any field you omit is reset — including its state. In particular, an update that doesn't carryfulfillmentRequestStatuscan knock anACKNOWLEDGEDFR back to Unprocessed, so it reappears on your nextstatus=NEWpoll. Use the purpose-built/acknowledge, ship, and cancel endpoints to move state; if you must call update, send the complete current FR, status included.
The processing leg has dedicated transitions too (the second one shipped Aug 2026): PATCH /fulfillment-requests/{id}/processed moves Processing to Processed, and PATCH /fulfillment-requests/{id}/processing moves Unprocessed to Processing— the API equivalent of the UI's Process action. Both take a bare PATCH with no body, return 200 with an empty body, and are idempotent (repeating the call on an FR already in the target status is a no-op 200). Any other status is a 400, and suppliers configured to send FRs from the Vendor Portal reject the /processing transition because their FRs go straight to Processed. Note that /processing takes account tokens only, while /processed also accepts source tokens.
Never read state viaGET /fulfillment-requests/processing. That endpoint returns every FR in Processing and transitions all of them to Processed as a side effect. UseGET /fulfillment-requests/{id}to check a status.
Idempotency: handle replays safely
Because acknowledgement is the fence, and because your poller might retry after a crash, every handler needs to be idempotent on fr.id. Two implementations that work:
- Unique constraint on your side.
INSERT ... ON CONFLICT (flxpoint_fr_id) DO NOTHING. Second insert is a no-op. - Check-then-act inside a transaction. Look up the FR ID, skip if found, otherwise insert + downstream side effects in one transaction.
Anti-patterns to avoid
- Full mirror syncs. Pulling all FRs on every cycle to diff against a local snapshot. Wastes quota, increases latency, introduces a second source of truth. Filter by
statusand dedupe locally instead. - Polling without filters.
GET /fulfillment-requestswith no status filter returns everything visible to the token. Always pass a status. - Parallel pollers on the same token. Two processes polling the same source token will split the 2 req/s budget and both stall on pagination. One poller per token. Use a leader-election or a single worker.
- Retrying 4xx errors. A 400 on acknowledgement (e.g. trying to ack a cancelled FR) will never succeed — log it, move on, don't retry-loop.
- Processing before acknowledgement. If your handler sends data to a third-party system (an ERP, a carrier label service), wrap it in a local transaction and only ack Flxpoint after it commits. Otherwise a crash leaves you with a processed-downstream-but-unacked FR that reappears on the next poll.
When updatedAfter ships
If a filterUpdatedAfter parameter lands, the flow becomes simpler — poll with updatedAfter=<last-seen> and drop the local processed-set to just a timestamp. This guide will be updated when that happens. Subscribe to the Fulfillment Request resource page for endpoint-level changes.
Related
- Fulfillment Request — endpoint reference
- Authentication & Rate Limits (why one token per source)
- Integrating as a Source (the full Source-side flow)