Skip to content
Lumean API

Pagination

Regular lists count pages from one, external voice catalogs from zero. Plus a third shape for chunking previews and a field the spec promises that does not exist.

Translation in progress

This page has not been translated into your language yet — the version below is in English. The text and the code samples are the same.

The public API has two incompatible page numbering bases and three different response shapes. There is no universal paginator: code written against GET /orders will silently skip the first page of the ElevenLabs catalog.

Shape 1: regular lists, pages from one

GET /orders, GET /templates/browse, GET /voices, GET /voices/library, GET /voices/public put an object in data:

{
  "items": [ /* … */ ],
  "current_page": 1,
  "last_page": 5,
  "per_page": 20,
  "total": 93
}

The query parameters are page (starting at 1) and per_page. The list is exhausted when current_page === last_page.

Gotcha

Four descriptions of the per_page parameter in the spec promise that “pagination metadata is in data.meta”. There is no meta field in any response schema or in any example: the counters sit directly in data, next to items. Affected: GET /voices, GET /voices/library, GET /voices/public and GET /orders/chunks/preview/{previewId}. Read data.current_page, not data.meta.current_page.

Shape 2: external voice catalogs, pages from zero

GET /voices/elevenlabs/library and GET /voices/heygen proxy third-party APIs and keep their conventions. data contains a different wrapper:

{
  "voices": [ /* … */ ],
  "total": 1420,
  "page": 0,
  "page_size": 30,
  "has_more": true
}

There are four differences at once: the array is called voices, not items; the page size is page_size, not per_page; the continuation flag is the boolean has_more, and there is no last page number at all; and page counts from zero. A fifth difference hides inside: total means different things in these two operations. For ElevenLabs it is the number of voices on the current page (the spec explicitly says to look at has_more, not total); for HeyGen it is how many voices match the filters across all pages.

Careful

page=1 in these two operations is the second page. The most common complaints, “the ElevenLabs catalog returned something different from the UI” and “the first 30 voices went missing”, are exactly this. The first page here is page=0, which is also the default.

GET /voices/elevenlabs/library depends on the external ElevenLabs API: when that API is unavailable, the operation responds with 503. A catalog crawl must survive an interruption midway: store the page number, not just the accumulated array.

Shape 3: chunking preview pages

GET /orders/chunks/preview/{previewId} resembles neither the first shape nor the second: the counters sit in a separate data.page object and combine both styles.

FieldMeaning
data.page.current_pagepage number, counting from 1
data.page.per_pagepage size (default 100, maximum 500)
data.page.totaltotal chunks in the split, across all pages
data.page.has_morewhether there are more pages; there is no last_page here

The neighboring data.summary field covers the entire text, not the current page, so you can't sum it across pages. And remember that {previewId} is a sha256 of 64 hex characters, not a UUID.

What isn't paginated at all

Not all lists are paginated, and trying to page through them looks like “the server ignores page”.

Operationdata shape
GET /templatesa flat array of all templates, no pages. For page-by-page traversal, use GET /templates/browse
GET /orders/{order}/itemsa flat array of all the order's chunks, including retries
GET /usage, GET /voices/tags, GET /voices/heygen/filtersflat arrays of entries

Ceilings and defaults

OperationParametersRangeDefault
GET /orderspage, per_pagepage ≥ 1, per_page 1..10020
GET /templates/browsepage, per_pagepage ≥ 1, per_page 1..10015
GET /orders/chunks/preview/{previewId}page, per_pagepage ≥ 1, per_page 1..500100
GET /voices/elevenlabs/librarypage, page_sizepage ≥ 0, page_size ≥ 10 / 30
GET /voices/heygenpage, page_sizepage ≥ 0, page_size 1..2000 / 55
GET /voices, /voices/library, /voices/publicper_pageno ceiling declared in the spec20

An out-of-range value returns 422 with the field in errors; it is not silently truncated.

422422 — request not accepted
What happened
The only API response that comes OUTSIDE the common envelope: `{"message": "…", "errors": {"field": ["…"]}}` — it has no `success` field. Code like `if (!res.success)` works on a 422 only by coincidence, because it is comparing against `undefined`.
What to do right now
First check whether `errors` is present. If it is, fix the request according to the listed fields. If it is not, but there is a `reason`, the request shape is not the problem, and repeating the same body gets the same refusal; read the page for that specific code.
Open the code page

Gotcha

For the three voice lists (GET /voices, GET /voices/library, GET /voices/public) the spec declares only per_page, even though the response carries current_page and last_page. In other words, they do have pages, but the parameter for moving between them is undocumented. Pass page as you would for the other shape 1 lists, and check current_page in the response to confirm it was applied.

There is no cursor pagination

Neither cursor, nor next_cursor, nor next_page_url exists in the public API: there is not a single mention of them in the entire OpenAPI spec. The trap is that the ?pag=cursor parameter passes validation and does not return 422, but is silently ignored: the list still comes back in offset form. Page through with page / per_page.

How to traverse an entire list

Offset pagination doesn't give you a consistent snapshot: the data changes while you walk through the pages. GET /orders returns the newest orders first, so a single newly created order shifts the whole listing forward by one position, and you will see the record at a page boundary twice or not at all.

  1. Deduplicate by id on the client side. This is the only safeguard that works in all three shapes. A set of already seen ids is cheaper than trying to guess the shift.
  2. Narrow the selection so that it doesn't grow. The GET /orders filters are status, task_type[], and template_id. Iterating over terminal statuses gives an almost stable set, whereas status=in_progress changes right under you.
  3. Stop on a flag, not a counter. For shape 1, stop when current_page reaches last_page; for external catalogs, when has_more === false. total is recalculated on every request and is fine for a progress indicator, but not for an exit condition.
  4. Use larger pages, but not the maximum. per_page=100 instead of 20 cuts the number of requests fivefold, and with it the risk of hitting a 429 during the crawl. Half the maximum page size is a reasonable compromise between response size and request count.
  5. Pause between pages. Crawling a large catalog is the most common way to hit the key's request-rate limit, which has no Retry-After; see Limits and the 429 code.

Do

Keep a set of seen ids, page until has_more === false or current_page === last_page, and survive a 503 from the external catalog by resuming from the same page number.

Don't

Compute the total page count as Math.ceil(total / per_page) once at the start and then blindly iterate over the numbers: total will be stale by the third page, and for zero-based catalogs the last number will be last - 1, so you will request the empty tail twice.

Feed this page to an AI

Open .mdllm.md

Updated 04/09/2026 · spec 0.0.1