Developer ToolsFree generatorPDF, Excel, Word export

Paginated OData Fetch Loop (Python Generator)

Pulling a large entity from an ERP over OData in one call usually fails or times out. This generator writes a Python script that requests a page, follows the server's next link until the data ends, and writes rows to CSV with a cap that protects your system.

Fill in

Generated code
Download:

odata_export.py

import csv
import os

import requests

SERVICE = "https://erp.example.com/odata/v4"
ENTITY = "SalesOrders"
MAX_ROWS = 50000
OUTFILE = "sales_orders.csv"

params = {
    "$select": "OrderNumber,CustomerId,OrderDate,Status",
    "$filter": "Status eq 'Open'",
    "$orderby": "OrderNumber asc",
    "$top": "500",
}
headers = {
    "Authorization": "Bearer " + os.environ["ERP_ODATA_TOKEN"],
    "Accept": "application/json",
}

session = requests.Session()
url = SERVICE + "/" + ENTITY
fields = [name.strip() for name in params["$select"].split(",")]
total = 0

with open(OUTFILE, "w", newline="", encoding="utf-8") as handle:
    writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
    writer.writeheader()
    first = True
    while url and total < MAX_ROWS:
        # The nextLink already carries all query options, so send params only once
        response = session.get(
            url, headers=headers, params=params if first else None, timeout=60
        )
        response.raise_for_status()
        payload = response.json()
        for row in payload.get("value", []):
            writer.writerow(row)
            total += 1
            if total >= MAX_ROWS:
                break
        url = payload.get("@odata.nextLink")
        first = False

print("Wrote " + str(total) + " rows to " + OUTFILE)
if url:
    print("Stopped at the row cap, more pages remain")

OData v4 returns @odata.nextLink. OData v2 services such as many SAP Gateway services use d.results and d.__next instead, and $skiptoken or $skip may be needed. Server-driven paging can override your $top. Verify query option support, case sensitivity of field names and filter syntax with your ERP, and test on a non-production system.

ERP data extraction review

Optional: have an engineer check your extract design for load and completeness.

Optional. The tool and every export work without this. No spam.

How to use it

Enter the service root, entity set, fields, a filter and an order. Choose a modest page size and a hard row cap. Export the token into the environment variable and run the script on a test tenant first. Open the CSV to confirm column names. Narrow the filter before you raise the cap, because fewer rows and fewer fields are the easiest ways to reduce load on the ERP.

The method behind it

OData v4 servers return @odata.nextLink when more results exist. That link already includes the query options, so the loop sends parameters only on the first request and then just follows the link. This is more reliable than computing $skip offsets, which can skip or repeat rows if data changes between pages. Stable ordering by a key helps when a service falls back to offsets.

  • Use $select to return only the fields you need
  • Filter on indexed or key fields where possible
  • Keep page sizes moderate, a few hundred to a few thousand
  • Cap total rows during testing
  • Read-only service account for extracts

ERP notes

Dynamics 365 Finance and Operations, Business Central, SAP Gateway services and Epicor all expose OData with local differences: paging limits, cross-company flags, and how large filters are handled. Check the documented maximum page size and API capacity limits before scheduling this against production. For continuing sync, filter on a modified timestamp instead of re-reading everything.

Common mistakes

Teams send the same parameters with every page request and end up in an endless loop or duplicated rows. Others skip $select and pull wide tables, forget URL encoding for filter values, or run the extract at month end when the ERP is busiest. Always log the row count and stop at a cap.

Frequently Asked Questions

Why follow @odata.nextLink instead of using $skip?

The next link is created by the server and encodes where the next page starts, which stays correct even if rows change between requests. Offset paging with $skip can miss or repeat records on live data and gets slower on large offsets.

Does this work with SAP OData v2?

Not directly. Many v2 services return results under d.results and the next URL under d.__next, and use $skiptoken or $skip. Adjust the two lines that read the rows and the next link. The overall loop is the same.

How big should my page size be?

A few hundred to a few thousand rows is typical. Smaller pages mean more requests, larger pages risk timeouts and memory use. Servers often enforce their own maximum, so test and follow what the service returns.

Can I run this against production?

Yes if you use a read-only account, a narrow filter, a row cap, and an off-peak schedule, and you confirm API capacity limits. Test on a non-production tenant first and watch ERP performance during the first run.

Netray connects AI agents and analytics to ERP data with careful, read-only extraction. If your extracts are slow or risky, we can help redesign them.