Acumatica's contract-based REST API supports $top and $skip against every endpoint under /entity/{endpointName}/{version}/, and that's the entire pagination mechanism — there's no total-record-count header, no hasMore flag, and no continuation token in the base contract API. Most of the integration bugs I get called in to fix on this API aren't exceptions or auth failures; they're a sync job that ran clean for months and quietly stopped picking up new records once a table crossed a few hundred rows. This post covers the bug shape that causes that, the pagination loop that avoids it, and when a Generic Inquiry exposed as an OData feed is the better tool than the entity REST endpoint in the first place.
The bug that doesn't throw an error
Here's how it happens: someone calls GET /entity/Default/24.200.001/SalesOrder?$top=50, gets back exactly 50 records, and treats that as the whole result set. It works in testing because the sandbox has 30 sales orders. It works in production for the first few months because the table hasn't crossed 50 rows yet. Then it crosses 50, and the integration silently stops picking up anything past the first page — no exception, no failed request, no log line. The response is valid, the status code is 200, and the data is simply incomplete. That's the shape of this bug: it isn't a crash, it's silent data loss, and it usually gets discovered by a customer asking where an order went rather than by monitoring.
The fix is not "add a bigger $top." Any fixed page size runs into the same wall once the dataset outgrows it. The endpoint has to be paginated in a loop regardless of how large you set the page.
The correct loop pattern
Since there's no count header or cursor to trust, the loop has to infer the end of the result set from the page size itself: request $top, and if the number of records that comes back is less than $top, that page was the last one. Anything else — assuming a fixed number of pages, stopping after one call, polling a count endpoint that doesn't exist — either breaks or is more complex than it needs to be.
const PAGE_SIZE = 50;
let skip = 0;
let allOrders = [];
while (true) {
const url = `${baseUrl}/entity/Default/24.200.001/SalesOrder`
+ `?$filter=Status eq 'Open'`
+ `&$select=OrderNbr,Status,CustomerID,OrderTotal`
+ `&$top=${PAGE_SIZE}&$skip=${skip}`;
const res = await fetch(url, { headers: authHeaders });
const page = await res.json();
allOrders = allOrders.concat(page);
// Fewer records than requested means this was the last page.
if (page.length < PAGE_SIZE) break;
skip += PAGE_SIZE;
}
That's the whole pattern. It costs one extra round trip at the very end (a page that returns exactly 0 records, if the total happens to be an exact multiple of your page size), which is a fine trade for never silently truncating a result set again. Keep $top reasonably small — 50 to 100 — so each request stays fast and a slow page doesn't hold a session open for longer than necessary.
It's tempting to look for a count field somewhere in the response envelope and use it to calculate pages up front. The base contract API doesn't return one. If you've seen a count-like field on a specific custom endpoint, that's something someone added deliberately — don't assume it generalizes to every entity, and don't build a sync that breaks silently if that field is ever absent.
When a GI/OData feed is the better fit
The entity-level REST API is built for transactional integration — creating and updating Sales Orders, Bills, Stock Items, the things a real business process needs to act on. If what you actually want is a reporting or export feed — a flattened, pre-joined view of data for a BI tool or a scheduled extract — a Generic Inquiry exposed via OData is usually a better starting point than paginating a raw entity endpoint. On the GI screen, checking "Expose via OData" turns that inquiry into its own OData endpoint, and it honors $skip and $top the same way you'd expect from a standard OData service — so the same paginate-until-a-short-page-comes-back loop above works against it unchanged.
The advantage isn't the pagination mechanics, which are identical either way — it's that the GI lets you shape the result server-side (joins, filters, calculated fields) instead of pulling the full entity contract and reshaping it in client code on every page. If a report needs data from three related screens, building that join once in a GI beats stitching together three separate paginated REST pulls.
Wrapping up
The failure mode with Acumatica's REST pagination isn't an error you can catch — it's a page that comes back looking completely valid while silently leaving the rest of the table behind. Treat every entity endpoint call as one page of an unknown number, loop on $top/$skip until a short page tells you you're done, and reach for a GI exposed via OData instead of the entity API when the actual need is a shaped reporting feed rather than a transactional read.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.