Lists, Pagination and Filters
Every list in v4 has the same shape: one envelope, one way to page, one way to filter. A client, an SDK or an MCP tool writes that code once.
Every list paginates
Every GET on a collection paginates with limit and offset, and returns
exactly { data, limit, offset, total, hasMore, next } as a {Resource}List.
That includes collections that are small today (a gateway's user apps, a pool's members). The shared guidelines exempt "bounded" collections. v4 does not: adding pagination later is a breaking change (VER-3), and one shape means one code path in every client, SDK and MCP tool.
Code
Code
Code
The items on this page, always an array of the resource schema.
The page size that was applied.
How many items were skipped.
How many items the whole collection has.
Whether another page follows.
The relative URL of the next page, or null on the last page.
This is the shared envelope (COLL-2) exactly. In the spec it is a component named
{Resource}List (AiPoolList) with exactly these six properties, all required.
Minimum 1, default 10, maximum 100. Referenced from the shared Limit
component.
Minimum 0, default 0. Referenced from the shared Offset component.
Order
Every list has one stable, documented order, stated in the operation description. Ties are broken by ID, so pages never overlap or skip items while the collection is unchanged.
Resources are listed oldest first, unless there is a reason otherwise.
API keys and deployments are listed newest first: a client almost always wants the latest.
Filters
A filter is a query parameter named after the field it filters, in camelCase, and it matches exactly.
Code
Code
Code
Code
Code
Code
Code
Code
A person lists their own consumers with their own userId. User app admins use
anyone's.
Code
me is not a userId, and a /me route is never added.
Unknown query parameters are rejected with 400. A typo in a filter must not
silently return the unfiltered collection.
Reserved parameters
These names are reserved across v4 and mean the same everywhere. Their synonyms are rejected by lint.
A filter is named after its field.
For logs, audit events and analytics.
Lint also rejects fields: every response always has every property.
Every other collection uses limit and offset
(decided on 2026-11-05, question 10). When
v4 gets its first append-only collection (build logs, request logs, audit
events), it uses an opaque cursor with the same envelope minus offset and
total (COLL-3), and v4-list-envelope gets that variant in the same pull
request. A collection's pagination is chosen when it is introduced and never
switched.
Including more with include[]
include[] is the one query parameter that asks for more in a response. Its
values are the names of response properties that cost an extra lookup, listed as
an enum. Each one is always present and null unless it was asked for. There are
no flags of their own (includeKeys, withApiKey, expand[]).
A new flag for every extra means a new parameter for every client, SDK and MCP
tool to learn. include[] is one parameter that works the same everywhere, as
Stripe's include[]
does. A value is the property's name (consumers, apiKey), and the property's
description says when it is not null.
| Operation | include[] | Adds |
|---|---|---|
| Get, list, create and update an app | consumers | the consumers linked to the app |
| List and get the people in a user app | budget | each person's limits, as …/users/{userId}/budget returns them |
| Create a consumer | apiKey | its first API key, with the secret, which is shown only then |
On a create, asking for something the new resource does not have yet creates it:
include[]=apiKey issues the consumer's first key. The body stays the new resource
alone (bodies).
Related resources are otherwise a filtered list
(/v4/buckets/{bucketId}/ai/apps?poolId=…), or for credentials a nested one
(…/users/{userId}/consumers), and references are …Id fields. Data that is
only ever read and changed together with its owner is part of the owner's body,
not a sub-collection: a provider assignment's models are its models array,
written in the same PATCH. A provider's own models are the exception, a
read-only list at …/ai/providers/{providerId}/models: a built-in provider's
models are Zuplo's catalog for it, too many to send in every provider.
An app's consumers have no list of their own: an app has a few, one per client or
environment, and include[]=consumers returns them with the app, with the same
schema as …/consumers/{consumerId}. A consumer is created in the bucket and linked
with a POST of its consumerId to …/ai/apps/{appId}/consumers, and it is
changed and deleted at …/consumers/{consumerId}, never in the app's body.
Code
Code
Code
Code
Code
Code
Parameters, the envelope and {Resource}List naming; reserved names, camelCase, no include*, with* or expand* flags, and include[] values that are nullable properties of every success response.
Rejecting unknown query parameters (COLL-6), which request validation also does at runtime.