Skip to main content

Overview

The Dev API currently uses two pagination models:
  • Cursor-based pagination for most resource lists
  • Page-based pagination for operational lists where page navigation is clearer
Most resource lists (products, invoices, orders) use cursor-based. Operational lists like webhook delivery logs use page-based.
Use cursor-based pagination for feed-style lists like products, invoices, orders, customers, and subscriptions.

Parameters

ParameterTypeDefaultDescription
limitinteger50Number of items per page (1-100)
cursorstring-Cursor from previous response

Basic Usage

First Request

GET /products?limit=25
Response:
{
  "data": [
    { "uniqid": "prod_001", "title": "Product A" },
    { "uniqid": "prod_002", "title": "Product B" },
    // ... 23 more items
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6MjUsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTAxIn0",
    "has_more": true
  }
}

Next Page

Use the next_cursor from the previous response:
GET /products?limit=25&cursor=eyJpZCI6MjUsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTAxIn0

Final Page

When there are no more items:
{
  "data": [
    { "uniqid": "prod_099", "title": "Product Y" },
    { "uniqid": "prod_100", "title": "Product Z" }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false
  }
}

Code Examples

Fetch All Pages

async function fetchAllProducts(): Promise<Product[]> {
  const allProducts: Product[] = [];
  let cursor: string | null = null;

  do {
    const url = new URL('https://api.shoppex.io/dev/v1/products');
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const response = await fetch(url.toString(), {
      headers: { 'Authorization': `Bearer ${API_KEY}` }
    });

    const { data, pagination } = await response.json();

    allProducts.push(...data);
    cursor = pagination.next_cursor;
  } while (cursor);

  return allProducts;
}

Async Generator (Streaming)

TypeScript
async function* fetchProductsStream(): AsyncGenerator<Product> {
  let cursor: string | null = null;

  do {
    const url = new URL('https://api.shoppex.io/dev/v1/products');
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const response = await fetch(url.toString(), {
      headers: { 'Authorization': `Bearer ${API_KEY}` }
    });

    const { data, pagination } = await response.json();

    for (const product of data) {
      yield product;
    }

    cursor = pagination.next_cursor;
  } while (cursor);
}

// Usage
for await (const product of fetchProductsStream()) {
  console.log(product.title);
}

Filtering with Pagination

Filters work alongside pagination:
# Get completed invoices, paginated
GET /invoices?filters=status:COMPLETED&limit=50

# Continue with cursor
GET /invoices?filters=status:COMPLETED&limit=50&cursor=eyJ...
Always include the same filters and sorts when following cursors. Changing them invalidates the cursor.
GET /orders?filters=status:COMPLETED,customer_email:[email protected]&sorts=-created_at
GET /customers?filters=city:Berlin&sorts=created_at

Cursor-Based Endpoints

EndpointDefault LimitMax Limit
GET /products50100
GET /invoices50100
GET /customers50100
GET /coupons50100
GET /subscriptions50100
GET /reviews50100
GET /tickets50100
GET /webhooks50100
GET /blacklist50100

Best Practices

Use Reasonable Limits

Fetching for a UI? 20-25 items is plenty. Background sync job? Crank it to 100. Larger limits increase response time.

Don't Store Cursors

Cursors are meant for immediate sequential use. They expire after 24 hours and become invalid if the underlying data changes significantly.

Handle Empty Results

An empty data array with has_more: false is valid - no items match your query.

Respect Rate Limits

When fetching all pages, add delays between requests to avoid rate limiting.