Pagination
Most list endpoints in the Virlo API are paginated, and you always drive them with the same two knobs: limit (how many items per page) and either page (1-indexed) or offset (starting position). What differs is the shape of the response — the API has grown three pagination shapes, plus a set of endpoints that return a complete result with no pagination at all. Knowing which one an endpoint uses saves you a surprise. This guide documents all four cases.
Parameters
The same parameters apply everywhere:
- Name
limit- Type
- integer
- Description
Number of items per page. Between 1 and 100 on most endpoints. Default is 50; some list endpoints use smaller defaults (Satellite runs 25; tracking lists, sounds discovery, and agent sounds 20). Values above 100 are clamped to 100, not rejected;
limit=0or negative returns400. Tracking exceptions: creator posts accepts up to 200 and creator snapshots up to 365 — those two reject over-max values with400instead of clamping.
- Name
page- Type
- integer
- Description
The page number to fetch. 1-indexed. Default is 1. The API converts
pagetooffsetinternally where needed.
- Name
offset- Type
- integer
- Description
The starting position, as an alternative to
page. Some sub-resource endpoints (videos, slideshows, ads, outliers, runs) surfaceoffsetdirectly in the response.
Which shape does an endpoint use?
| Shape | Where the list lives | Metadata | Endpoints |
|---|---|---|---|
| A — nested + offset | Inside data, under a resource-named key (videos, slideshows, ads, outliers) | total, limit, offset (creators/outliers also add has_more) | Agent / Orbit / Comet videos, slideshows, ads, and creator-outlier lists; Satellite runs list |
B — envelope + pagination object | data is the array itself | pagination object (page, limit, total, total_pages, has_next_page, has_prev_page) | Agent / Orbit / Comet sounds & hashtags sub-resources, global /v1/sounds/*, Tracking list endpoints |
| C — count only, no total | Inside data, under a resource-named key | limit, page (or offset), count — no grand total | GET /v1/agents, GET /v1/agents/:id/runs |
| D — not paginated | Inside data | none, or a plain count | GET /v1/agents/:id/{benchmarks,affinity,activity,proposals}, GET /v1/trends/emerging |
Shapes A and C report different things and are easy to confuse. total (Shape A) is the grand total across all pages — you can compute the page count from it. count (Shape C) is only how many items came back on this page. With Shape C, keep paging until a page returns fewer items than your limit.
Shape A — nested list with offset
The list sits inside the data object under a key named after the resource (videos, slideshows, ads, outliers), alongside total, limit, and offset. This is what you get from an agent's (or Orbit/Comet's) video, slideshow, ad, and creator-outlier lists, and from the Satellite runs list. Creator/outlier lists additionally include a has_more boolean.
Creator-outlier responses also still return a deprecated camelCase hasMore alias alongside has_more. Read has_more; the alias is removed August 3, 2026.
Use limit + offset (or page) to walk the pages. offset is the starting position: page 1 = offset 0, page 2 with limit=50 = offset 50.
Shape A — GET /v1/agents/:id/videos
{
"data": {
"agent_id": "2b1f9c3d-...",
"agent_name": "Skincare Routine Research",
"total": 283,
"limit": 50,
"offset": 0,
"videos": [
{ "id": "e30b40b1-...", "url": "https://...", "views": 11166267 }
]
}
}
Shape B — envelope with a pagination object
Here data is the array, and a sibling pagination object carries the metadata. This is the shape for sounds and hashtags sub-resources (on agents, Orbit, and Comet), the global /v1/sounds/* endpoints, and the Tracking list endpoints.
total is a running lower bound on some sounds reads. The RPC-backed discovery endpoints — /v1/sounds/trending (velocity sorts videos_7d/videos_30d), /v1/sounds/breakout, and the agent / Orbit / Comet sounds sub-resources — don't run a separate count query, so total there means "at least this many" (it grows as you page) and total_pages inherits the same caveat. has_next_page is accurate on every endpoint — drive pagination with it. Endpoints backed by exact counts (sounds search, sound videos, tracking lists, hashtags) report a true grand total.
Drive it with limit + page. Use has_next_page / has_prev_page to know when to stop, and total_pages for a page count.
Shape B — GET /v1/sounds/trending
{
"data": [
{ "id": "8f6e5c50-...", "title": "Saxophones getting louder" }
],
"pagination": {
"page": 1,
"limit": 50,
"total": 850,
"total_pages": 17,
"has_next_page": true,
"has_prev_page": false
}
}
Shape C — count only, no grand total
GET /v1/agents and GET /v1/agents/:id/runs nest the array under a resource-named key inside data, with limit, page (or offset), and a count of items on the current page. There is no total, so you cannot precompute a page count.
Drive it with limit + page. Keep requesting the next page until a page returns fewer items than limit — that page is the last one.
Shape C — GET /v1/agents
{
"data": {
"limit": 50,
"page": 1,
"count": 3,
"agents": [
{ "id": "2b1f9c3d-...", "name": "Skincare Routine Research", "is_recurring": true }
]
}
}
Shape D — not paginated
A few endpoints return a complete, bounded result in one response — there is nothing to page through. Passing limit/page to them has no effect (except where a limit cap is documented on the endpoint itself).
This applies to an agent's benchmarks, affinity, activity, and proposals sub-resources, and to GET /v1/trends/emerging. Read data directly; don't look for pagination metadata.
Shape D — GET /v1/agents/:id/benchmarks
{
"data": [
{
"follower_tier": "micro",
"creator_count": 48,
"median_followers": 42000,
"median_engagement_rate": 0.071
}
]
}
All paginated shapes are driven by limit plus page (1-indexed) or offset — see the Parameters section above for the limit ranges, defaults, and the tracking-family exceptions. When writing a client, branch on the presence of a top-level pagination key (Shape B) versus total/offset inside data (Shape A) versus count without total (Shape C).
