# Pagination

> Page through BloomText API list endpoints with opaque cursors, page[limit], and page[after].

Source: https://www.bloomtext.com/developers/api/pagination/

Every list endpoint returns results a page at a time, in a stable order, with an opaque cursor for the next page.

```json filename="Response"
{
  "data": [ { "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1", "...": "..." } ],
  "pagination": {
    "next_cursor": "eyJpZCI6IjRjYmZkYjUwIn0",
    "has_more": true
  }
}
```

## Parameters

- `page[limit]` (integer): How many records to return. Defaults to `50`, maximum `100`.

- `page[after]` (string): The `next_cursor` from the previous page. Omit it for the first page.

## Response fields

- `data` (array, required): The records on this page, in stable order.

- `pagination.next_cursor` (string or null, required): Pass as `page[after]` to get the next page. `null` on the last page.

- `pagination.has_more` (boolean, required): Whether another page exists.

## Ordering

Results are ordered by creation time, oldest first, with the ID as a tiebreaker. The order never changes between requests, so retrying a page is always safe and never skips or repeats records.

## Fetch every page

```bash filename="Request"
curl "https://api.bloomtext.com/v1/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages?page[limit]=100&page[after]=eyJpZCI6IjRjYmZkYjUwIn0" \
  -H "Authorization: Bearer $BLOOMTEXT_API_KEY"
```

```js filename="paginate.mjs"
async function* listAll(path) {
  let cursor = null
  do {
    const url = new URL(`https://api.bloomtext.com/v1${path}`)
    url.searchParams.set('page[limit]', '100')
    if (cursor) url.searchParams.set('page[after]', cursor)

    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}` },
    })
    if (!response.ok) throw new Error(`BloomText API error: ${response.status}`)

    const { data, pagination } = await response.json()
    yield* data
    cursor = pagination.has_more ? pagination.next_cursor : null
  } while (cursor)
}

for await (const message of listAll('/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages')) {
  console.log(message.id)
}
```

```python filename="paginate.py"
import os

import requests

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['BLOOMTEXT_API_KEY']}"


def list_all(path):
    params = {"page[limit]": 100}
    while True:
        response = session.get(f"https://api.bloomtext.com/v1{path}", params=params)
        response.raise_for_status()
        page = response.json()
        yield from page["data"]
        if not page["pagination"]["has_more"]:
            return
        params["page[after]"] = page["pagination"]["next_cursor"]


for message in list_all("/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages"):
    print(message["id"])
```

```go filename="paginate.go"
type page struct {
	Data       []json.RawMessage `json:"data"`
	Pagination struct {
		NextCursor *string `json:"next_cursor"`
		HasMore    bool    `json:"has_more"`
	} `json:"pagination"`
}

func listAll(path string) ([]json.RawMessage, error) {
	var all []json.RawMessage
	params := url.Values{"page[limit]": {"100"}}
	for {
		req, _ := http.NewRequest("GET", "https://api.bloomtext.com/v1"+path+"?"+params.Encode(), nil)
		req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY"))
		res, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err
		}
		var p page
		err = json.NewDecoder(res.Body).Decode(&p)
		res.Body.Close()
		if err != nil {
			return nil, err
		}
		all = append(all, p.Data...)
		if !p.Pagination.HasMore {
			return all, nil
		}
		params.Set("page[after]", *p.Pagination.NextCursor)
	}
}
```

```ruby filename="paginate.rb"
require "net/http"
require "json"

def list_all(path)
  Enumerator.new do |yielder|
    params = { "page[limit]" => 100 }
    loop do
      uri = URI("https://api.bloomtext.com/v1#{path}")
      uri.query = URI.encode_www_form(params)
      request = Net::HTTP::Get.new(uri)
      request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}"
      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }

      page = JSON.parse(response.body)
      page["data"].each { |record| yielder << record }
      break unless page["pagination"]["has_more"]

      params["page[after]"] = page["pagination"]["next_cursor"]
    end
  end
end

list_all("/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages").each { |message| puts message["id"] }
```

## Tips

- **Treat cursors as opaque.** Don't parse, build, or store them long-term. A cursor is valid for 24 hours.
- **Use the biggest page you need.** `page[limit]=100` means fewer requests against your [rate limit](https://www.bloomtext.com/developers/api/rate-limits/).
- **Sync incrementally.** To pick up new messages, keep the last `next_cursor` you saw and resume from it, or use [webhooks](https://www.bloomtext.com/developers/api/webhooks/) instead of polling.
