How does OpenAPI describe pagination in API responses? #
Pagination is one of the most common patterns in REST APIs. Any endpoint that returns a collection of resources — users, orders, products, events — must handle the case where that collection is larger than what should be returned in a single response. OpenAPI doesn’t prescribe a single pagination pattern, but it provides the schema and parameter tooling to describe any pagination approach accurately. This article walks through how to describe the most common pagination styles in OpenAPI documents.
Why Pagination Matters in API Design #
Without pagination, APIs that return large datasets force clients to receive potentially millions of records in a single response — exhausting memory, network bandwidth, and timeout budgets. Pagination divides results into manageable pages, letting clients request them incrementally. From an OpenAPI perspective, pagination affects:
- Query parameters — the inputs clients use to control which page they receive.
- Response body schemas — the shape of the paginated envelope, including the data array and metadata like total counts and cursors.
- Response headers — some APIs communicate pagination metadata via headers rather than the body.
- Link headers — the
LinkHTTP header (RFC 5988) is a standard way to communicate next/previous page URLs.
Offset-Based Pagination #
Offset pagination is the simplest approach: clients provide an offset (how many records to skip) and a limit (how many records to return).
Parameters #
parameters:
- name: limit
in: query
description: Maximum number of results to return (1–100)
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: offset
in: query
description: Number of results to skip before returning results
schema:
type: integer
minimum: 0
default: 0
Response Schema #
components:
schemas:
UserListResponse:
type: object
required: [data, total, limit, offset]
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
total:
type: integer
description: Total number of users matching the query
example: 1523
limit:
type: integer
description: Maximum number of results per page
example: 20
offset:
type: integer
description: Number of results skipped
example: 40
Offset pagination is easy to implement and understand, but it has well-known drawbacks: if records are inserted or deleted between pages, clients may see duplicates or miss records. For most use cases, it remains a pragmatic choice.
Page-Based Pagination #
Page-based pagination uses page (1-indexed page number) and per_page (page size) parameters instead of raw offsets.
Parameters #
parameters:
- name: page
in: query
description: Page number (1-indexed)
schema:
type: integer
minimum: 1
default: 1
- name: per_page
in: query
description: Number of results per page
schema:
type: integer
minimum: 1
maximum: 100
default: 25
Response Schema with Pagination Metadata #
components:
schemas:
PaginationMeta:
type: object
required: [current_page, per_page, total_pages, total_count]
properties:
current_page:
type: integer
example: 2
per_page:
type: integer
example: 25
total_pages:
type: integer
example: 61
total_count:
type: integer
example: 1523
Cursor-Based Pagination #
Cursor-based (or keyset) pagination is the most scalable approach for large, frequently-updated datasets. Instead of an offset, the API returns a cursor — an opaque token representing the position in the dataset — and the client passes this cursor to get the next page.
Parameters #
parameters:
- name: cursor
in: query
description: >
Opaque cursor returned in the previous response. Omit for the first page.
schema:
type: string
example: "eyJpZCI6MTAwfQ=="
- name: limit
in: query
description: Maximum number of results to return
schema:
type: integer
minimum: 1
maximum: 100
default: 20
Response Schema #
components:
schemas:
UserCursorResponse:
type: object
required: [data]
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
next_cursor:
type: string
nullable: true
description: >
Cursor for the next page. Null if this is the last page.
example: "eyJpZCI6MTIwfQ=="
has_more:
type: boolean
description: Whether there are more results after this page
example: true
The cursor is opaque to the client — it should not be parsed or constructed. This is important to document clearly in the description fields.
Link Header Pagination #
Many APIs (including GitHub’s API) use the HTTP Link header (RFC 5988) to communicate pagination URLs rather than embedding them in the response body.
Documenting Link Headers in OpenAPI #
responses:
"200":
description: A paginated list of repositories
headers:
Link:
description: >
Pagination links. May include `next`, `prev`, `first`, and `last` relations.
Example: `<https://api.example.com/users?page=3>; rel="next",
<https://api.example.com/users?page=1>; rel="first"`
schema:
type: string
X-Total-Count:
description: Total number of results across all pages
schema:
type: integer
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
Reusable Pagination Components #
For APIs with multiple paginated endpoints, defining reusable parameters and schemas in components avoids repetition and keeps the document consistent:
components:
parameters:
LimitParam:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
CursorParam:
name: cursor
in: query
schema:
type: string
schemas:
CursorPage:
type: object
required: [data]
properties:
data:
type: array
items: {}
next_cursor:
type: string
nullable: true
has_more:
type: boolean
Individual operations then reference these shared components:
paths:
/users:
get:
parameters:
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/CursorParam'
responses:
"200":
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/CursorPage'
- properties:
data:
items:
$ref: '#/components/schemas/User'
Documenting Pagination Behavior in Descriptions #
Schema annotations alone don’t fully describe pagination semantics. Operation and parameter description fields should explain:
- Which pagination style is used and why.
- The behavior of cursors — whether they expire, and how long they remain valid.
- Sort order — pagination is typically only meaningful when results are in a stable, deterministic order.
- Maximum page size — what happens if the client requests more than the maximum.
- Empty results — whether an empty
dataarray or a null cursor signals the end of results.
Industry Standards: JSON:API and HAL #
Some teams adopt standardized envelope formats that include pagination:
- JSON:API defines a
linksobject withfirst,last,prev, andnextURLs and ametaobject for counts. - HAL (Hypertext Application Language) embeds pagination links in
_links.
Both are describable in OpenAPI using their respective schemas in components.
Conclusion #
OpenAPI’s parameter and schema machinery handles every common pagination pattern — offset, page-based, and cursor-based — with precision. The key is consistency: choosing one pagination style across all collection endpoints, defining reusable parameter and response schemas in components, and investing in clear descriptions that explain not just what the parameters do but how pagination works as a whole. Well-documented pagination is one of the most impactful improvements an API team can make for developer experience.
Last updated on April 30, 2026.