> ## Documentation Index
> Fetch the complete documentation index at: https://muveya.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> Walk the paginated /v1 lists with limit and an opaque cursor, and know the order and filters of every list.

Every `/v1` list returns the same envelope, whether it is paginated or not:

```json theme={null}
{
  "data": [],
  "hasMore": true,
  "nextCursor": "NjZkMGExZjBjMGZmZWUwMDAwMDAwODAy"
}
```

| Field | Meaning |
| - | - |
| `data` | The items of this page. |
| `hasMore` | `true` when there is at least one more page. |
| `nextCursor` | The token for the next page. Present **only** when `hasMore` is `true`; otherwise the field is absent (not `null`). |

## Which lists are paginated

| Operation | Paginated | Order | Filters |
| - | - | - | - |
| `GET /v1/orders` | Yes | Oldest first, by creation time | `status` |
| `GET /v1/fulfillments` | Yes | Oldest first, by creation time | `status` |
| `GET /v1/inventory/balances` | Yes | By box id, ascending | `catalogItemId`, `warehouseId` |
| `GET /v1/clinics` | No, one page | Creation order | None |
| `GET /v1/warehouses` | No, one page | Creation order | None |
| `GET /v1/catalog/items` | No, one page | Creation order | None |
| `GET /v1/catalog/categories` | No, one page | Creation order | None |
| `GET /v1/inventory/boxes/{boxId}/movements` | No, one page | Oldest first, by `occurredAt` | None |

Lists that are not paginated return every item in a single page with `"hasMore": false` and ignore `limit` and `cursor`. Write the same loop for every list anyway: if one of them becomes paginated later, your code keeps working without changes.

## Query parameters

<ParamField query="limit" type="integer" default="50">
  How many items to return, from 1 to 200. Only digits are accepted. A value of 0, a negative number, a decimal, text or a number above 200 is refused with `400` and the code `common.invalid_request`.
</ParamField>

<ParamField query="cursor" type="string">
  The `nextCursor` of the previous page, copied exactly. A cursor that cannot be read is refused with `400` and the code `common.invalid_request`; the server never silently restarts from the first page.
</ParamField>

<ParamField query="status" type="string">
  On `GET /v1/orders` and `GET /v1/fulfillments` only. It must be one of the status values listed for that operation in the **Endpoints** group (for example `pending_approval` for orders or `dispatched` for fulfillments). Any other value is refused with `400` and the code `common.invalid_request`.
</ParamField>

<ParamField query="catalogItemId" type="string">
  On `GET /v1/inventory/balances` only: keep the boxes of one catalog item. An id that matches nothing returns an empty page.
</ParamField>

<ParamField query="warehouseId" type="string">
  On `GET /v1/inventory/balances` only: keep the boxes of one warehouse. An id that matches nothing returns an empty page.
</ParamField>

## Walk every page

<CodeGroup>
  ```bash cURL theme={null}
  # First page
  curl "https://api.muveya.com/v1/inventory/balances?limit=200&warehouseId=66d0a1f0c0ffee0000000201" \
    -H "Authorization: Bearer $MUVEYA_API_KEY"

  # Next page: same filters, plus the cursor you received
  curl "https://api.muveya.com/v1/inventory/balances?limit=200&warehouseId=66d0a1f0c0ffee0000000201&cursor=NjZkMGExZjBjMGZmZWUwMDAwMDAwODAy" \
    -H "Authorization: Bearer $MUVEYA_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const BASE_URL = "https://api.muveya.com";
  const headers = { Authorization: `Bearer ${process.env.MUVEYA_API_KEY}` };

  async function* pages(path, filters = {}) {
    let cursor;
    do {
      const url = new URL(path, BASE_URL);
      url.searchParams.set("limit", "200");
      for (const [name, value] of Object.entries(filters)) url.searchParams.set(name, value);
      if (cursor) url.searchParams.set("cursor", cursor);

      const response = await fetch(url, { headers });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const page = await response.json();
      yield page.data;
      cursor = page.hasMore ? page.nextCursor : undefined;
    } while (cursor);
  }

  let boxes = 0;
  for await (const data of pages("/v1/inventory/balances", { warehouseId: "66d0a1f0c0ffee0000000201" })) {
    boxes += data.length;
  }
  console.log(`${boxes} boxes`);
  ```

  ```python Python theme={null}
  import os
  import requests

  BASE_URL = "https://api.muveya.com"
  session = requests.Session()
  session.headers["Authorization"] = f"Bearer {os.environ['MUVEYA_API_KEY']}"

  def pages(path, **filters):
      cursor = None
      while True:
          params = {"limit": 200, **filters}
          if cursor:
              params["cursor"] = cursor
          response = session.get(f"{BASE_URL}{path}", params=params, timeout=30)
          response.raise_for_status()
          page = response.json()
          yield page["data"]
          if not page["hasMore"]:
              return
          cursor = page["nextCursor"]

  boxes = sum(len(data) for data in pages("/v1/inventory/balances", warehouseId="66d0a1f0c0ffee0000000201"))
  print(f"{boxes} boxes")
  ```
</CodeGroup>

```json Last page theme={null}
{
  "data": [
    {
      "boxId": "66d0a1f0c0ffee0000000803",
      "catalogItemId": "66d0a1f0c0ffee0000000401",
      "warehouseId": "66d0a1f0c0ffee0000000201",
      "onHand": 7,
      "reserved": 2,
      "available": 5
    }
  ],
  "hasMore": false
}
```

## Cursor rules

* **The cursor is opaque.** Treat it as a token. Do not decode it, build it or change it; its format can change without notice.
* **A cursor marks a position, not a page number.** The next page starts right after the last item you received. Items created while you walk are neither skipped nor repeated: new orders and fulfillments appear at the end, because those lists run oldest first.
* **Send the same filters on every page.** The cursor stores only the position. If you change `status`, `catalogItemId` or `warehouseId` halfway, the next page applies the new filters from that position.
* **Filters are applied on every request.** If an order changes status while you walk `GET /v1/orders?status=approved`, it can leave the filter before you reach it.
* **Use a cursor only with the operation that issued it.** A cursor from one list is not valid on another.
* **Use `limit=200` for full scans.** Fewer, larger pages use less of your [rate limit](/docs/en/api-reference/rate-limits).

## Errors

| Status | `code` | Cause |
| - | - | - |
| `400` | `common.invalid_request` | `limit` out of range or not an integer, unreadable `cursor`, or unknown `status` |
| `401` | `api_keys.invalid` | See [Authentication](/docs/en/api-reference/authentication#authentication-failures) |
| `403` | `api_keys.scope_missing` | The key lacks the scope of the list (see [Scopes](/docs/en/api-reference/scopes)) |
| `429` | `common.too_many_requests` | See [Rate limits](/docs/en/api-reference/rate-limits) |

## Related pages

* [API quickstart](/docs/en/api-reference/quickstart)
* [Webhooks](/docs/en/api-reference/webhooks): how to poll lists to stay up to date
* [Errors](/docs/en/api-reference/errors)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.