Skip to content
Last updated

Pagination

Use cursor-based pagination to walk large lists without offsets or page numbers.

Overview

All /filter endpoints in the MVMNT API use cursor-based pagination to return large result sets in predictable, repeatable pages. The behavior is the same across resource types (orders, vendors, carriers, customers, etc.).

How It Works

Request Format

Filter endpoints use POST requests with pagination parameters in the request body:

POST /v1/vendors/filter
Content-Type: application/json

{
  "filter": { ... },
  "pageSize": 50,
  "cursor": null
}

Response Format

All paginated responses return a consistent structure:

{
  "data": [
    { "id": "...", "name": "..." },
    { "id": "...", "name": "..." }
  ],
  "pageInfo": {
    "pageSize": 50,
    "hasNextPage": true,
    "hasPreviousPage": false,
    "endCursor": "eyJpZCI6IjU1MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMCJ9"
  }
}
  • data contains the records for the current page.
  • pageInfo.endCursor is the token to request the next page.
  • Keep paging until pageInfo.hasNextPage is false.

Pagination Parameters

pageSize

Type: Integer Default: 50 Range: 1 - 250 Description: Target number of results to return per page (the last page may contain fewer)

{
  "pageSize": 100
}

cursor

Type: String (nullable) Default: null Description: Opaque cursor token returned as pageInfo.endCursor from the previous response

{
  "cursor": "eyJpZCI6IjU1MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMCJ9"
}

First page: Omit cursor or set to null Subsequent pages: Use endCursor from previous response

PageInfo Object

The pageInfo object in every response provides pagination metadata:

FieldTypeDescription
pageSizeintegerNumber of items returned in the current page
hasNextPagebooleantrue if another page is available for this same query
hasPreviousPagebooleantrue if this page is not the first page of the sequence
endCursorstringCursor token for the next page (null when none remains)

Basic Example

Fetch First Page

curl -X POST https://api.mvmnt.io/v1/vendors/filter \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "status": { "equalTo": "ACTIVE" }
    },
    "pageSize": 50
  }'

Response:

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "friendlyId": "V100001",
      "name": "ABC Warehouse",
      "status": "ACTIVE"
    },
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "friendlyId": "V100002",
      "name": "XYZ Storage",
      "status": "ACTIVE"
    }
    // ... 48 more results
  ],
  "pageInfo": {
    "pageSize": 50,
    "hasNextPage": true,
    "hasPreviousPage": false,
    "endCursor": "eyJpZCI6IjY2MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMSJ9"
  }
}

Fetch Next Page

Use endCursor from the previous response:

curl -X POST https://api.mvmnt.io/v1/vendors/filter \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "status": { "equalTo": "ACTIVE" }
    },
    "pageSize": 50,
    "cursor": "eyJpZCI6IjY2MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMSJ9"
  }'

Last Page

When hasNextPage is false, you've reached the end:

{
  "data": [
    // ... remaining results
  ],
  "pageInfo": {
    "pageSize": 50,
    "hasNextPage": false,
    "hasPreviousPage": true,
    "endCursor": null
  }
}

Best Practices

✅ Do

  • Use pageSize: 250 for exports/backfills: Minimizes round trips when you need to walk the full dataset
  • Treat cursors as short-lived: Use them to complete the current pagination loop, not as a saved bookmark
  • Drive the loop with hasNextPage: Stop when it’s false; don’t infer “done” from data.length
  • Keep filters identical across pages: The cursor only makes sense for the same query inputs
  • Retry on transient failures: Assume requests can fail mid-loop and retry safely

❌ Don't

  • Don't use offset-based pagination: MVMNT uses cursor-based pagination, not page numbers
  • Don't change filters mid-pagination: Mixing a cursor with different filters produces undefined results
  • Don't persist cursors: Cursors can expire or become invalid over time
  • Don't assume pageSize equals result count: The last page can be smaller than requested
  • Don't paginate when you don’t need to: If your use case can tolerate a single page, request a larger pageSize

Troubleshooting

Empty Results on First Page

Problem: data array is empty but no error

Possible Causes:

  1. Filter criteria matches no records
  2. All matching records are deleted (see Soft Deletes)

Solutions:

  1. Verify filter criteria
  2. Check if records exist using less restrictive filters
  3. Include deleted records if needed (see Filtering)

Invalid Cursor Error

Problem: API returns error: "Invalid cursor"

Possible Causes:

  1. Cursor from a different query (different filter/sort)
  2. Cursor expired (very old cursor)
  3. Malformed cursor string

Solutions:

  1. Always use cursor from the same filter query
  2. Don't persist cursors - start fresh queries with cursor: null
  3. Ensure cursor is passed as-is without modification

Duplicate Results

Problem: Same record appears in multiple pages

Possible Causes:

  1. Records were created/modified during pagination
  2. Using cursors from different queries

Solutions:

  1. Accept eventual consistency for real-time data
  2. Deduplicate results by ID on client side
  3. For consistent snapshots, consider using timestamps in filters

Page Size Ignored

Problem: Receiving different number of results than requested

Possible Causes:

  1. Last page has fewer results
  2. pageSize exceeds maximum (250)
  3. Some records filtered out after query

Solutions:

  1. Check pageInfo.hasNextPage instead of counting results
  2. Ensure pageSize is between 1-250
  3. This behavior is normal and expected

Next Steps

  • Filtering - Learn how to filter results before pagination
  • Soft Deletes - Understand how deleted records affect pagination
  • API Reference - See pagination on specific endpoints