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

# Pagination

> Page through lists with a cursor, and filter them

Every list endpoint returns results newest first, in pages. You move through pages with a cursor: the id of the last object you received.

***

## Parameters

| Parameter | Type | Description |
| - | - | - |
| `limit` | integer | Objects per page. From 1 to 100. Defaults to 25. |
| `starting_after` | string | The id of the last object on the previous page. Returns the objects after it. |

## Response

```json theme={null}
{
  "data": [
    { "id": "3b9f1c1e-6a52-4c0e-9d7e-1f6f2f0b7a11", "object": "customer", "…": "…" }
  ],
  "has_more": true
}
```

| Field | Description |
| - | - |
| `data` | The objects on this page. |
| `has_more` | `true` when another page exists. When `false`, you have reached the end. |

<Info>
  Lists use a cursor and not a page number. New rows can arrive while you are paging, and a page number would repeat or skip one. A cursor can't.
</Info>

***

## Get every page

Take the `id` of the last object in `data`, pass it as `starting_after`, and stop when `has_more` is `false`.

<CodeGroup>
  ```javascript Node.js theme={null}
  async function listAll(path, apiKey) {
    const results = [];
    let startingAfter;

    do {
      const url = new URL(`https://app.saasybill.com/api/v1${path}`);
      url.searchParams.set("limit", "100");
      if (startingAfter) url.searchParams.set("starting_after", startingAfter);

      const response = await fetch(url, {
        headers: { Authorization: `Bearer ${apiKey}` },
      });
      const page = await response.json();

      results.push(...page.data);
      startingAfter = page.has_more ? page.data.at(-1).id : undefined;
    } while (startingAfter);

    return results;
  }

  const subscriptions = await listAll("/subscriptions?status=active", process.env.SAASYBILL_API_KEY);
  ```

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

  def list_all(path, params=None):
      results, starting_after = [], None

      while True:
          query = {"limit": 100, **(params or {})}
          if starting_after:
              query["starting_after"] = starting_after

          response = requests.get(
              f"https://app.saasybill.com/api/v1{path}",
              params=query,
              headers={"Authorization": f"Bearer {os.environ['SAASYBILL_API_KEY']}"},
          )
          page = response.json()

          results.extend(page["data"])
          if not page["has_more"]:
              return results
          starting_after = page["data"][-1]["id"]

  subscriptions = list_all("/subscriptions", {"status": "active"})
  ```
</CodeGroup>

***

## Filters

Filters combine with paging. An invalid value answers `422` and names the parameter.

| Endpoint | Filter | Description |
| - | - | - |
| `GET /v1/customers` | `code` | The customer's code. |
| | `email` | The customer's email. Not case-sensitive. |
| | `status` | `active` or `inactive`. |
| `GET /v1/plans` | `code` | The plan's code. |
| | `status` | `draft`, `active`, `inactive` or `legacy`. |
| `GET /v1/charges` | `code` | The charge's code. |
| | `status` | `draft`, `active`, `inactive` or `legacy`. |
| `GET /v1/subscriptions` | `customer` | A customer id. |
| | `customer_code` | A customer code. |
| | `status` | `pending`, `active`, `complete` or `ended`. |
| `GET /v1/invoices` | `subscription` | A subscription id. |
| | `customer` | A customer id. |
| | `status` | `draft`, `awaiting_approval`, `approved`, `paid`, `voided`, `deleted`, `scheduled` or `not_in_external`. |

<Note>
  Plans and charges return active and draft items unless you set `status`. That is what can be sold. Ask for `legacy` or `inactive` explicitly to see the rest.
</Note>

<Note>
  `GET /v1/customers` lists customers only. A Xero contact that hasn't been imported as a customer in Saasybill doesn't appear.
</Note>
