Skip to main content
Version: v1

Pagination, filtering, sorting

Most ODUS Core API v1 and OMS API endpoints that return lists of resources (e.g. GET /v1/payments) support pagination, filtering, and sorting to help you retrieve data efficiently.

You are reading the v1 guides. If you send requests to /v2/..., switch versions with the picker in the top bar — v2 paginates differently. Upgrading from v1 to v2 covers the move.

Pagination​

ODUS API supports two pagination strategies:

  • Cursor pagination (recommended) — opt in by providing the limit query parameter. It uses opaque cursors to page through results and stays fast and consistent even on large, frequently changing datasets.
  • Offset pagination (default) — driven by the page and pageSize query parameters. It exposes total counts and numbered pages, which is convenient for small datasets and traditional page-number navigation.

The strategy is selected by the parameters you send: if limit is present, the endpoint uses cursor pagination; otherwise it falls back to offset pagination. Every response includes a pagination.mode discriminator ("cursor" or "offset") so you can branch on the shape reliably.

Core API v1 and OMS API

Core API v1 and OMS API list endpoints both keep offset pagination as the default and cursor pagination as an opt-in. Their release cadence is separate: Core API v2 is opt-in under /v2 and paginates by cursor only, while OMS API remains on its current v1-compatible pagination surface.

The OMS API's cursor responses are identical to the ones documented below — same parameters, same pagination fields, same "cursor" discriminator. The one exception is withTotal and the totalCount it returns, which the Core API serves and the OMS API does not.

Its offset responses are the exception: they predate the discriminator and carry totalItems / totalPages instead of mode, itemCount, pageCount, hasNextPage and hasPreviousPage. Detect the shape by whether pagination.mode is present rather than assuming it always is.

GET /inventory is served from a cached provider snapshot rather than a table, so it is offset-only and ignores limit.

To use cursor pagination, provide the limit query parameter. On the first request, omit cursor:

GET /payments?limit=10
ParameterDescriptionSupported ValuesDefault Value
limitThe number of items per page. Enables cursor pagination.Positive integers less than or equal to 10010
cursorAn opaque token identifying the page boundary to read from.A cursor returned by a previous responsenone
withTotalAlso return how many items the filters match, as totalCount. Core API only.truenone

To fetch the next or previous page, pass the nextCursor or prevCursor value returned by the previous response back in the cursor parameter, keeping the same limit, sortBy, and sortDirection:

GET /payments?limit=10&cursor=eyJzIjoiY3JlYXRlZEF0IiwiZCI6ImFzYyIsIm4iOiJmb3J3YXJkIn0

A complete example that combines paging with sorting. The first request sets the page size and sort order:

GET /payments?limit=10&sortBy=createdAt&sortDirection=desc

Then use the returned nextCursor to fetch the following page, keeping limit, sortBy, and sortDirection unchanged:

GET /payments?limit=10&sortBy=createdAt&sortDirection=desc&cursor=eyJzIjoiY3JlYXRlZEF0IiwiZCI6ImRlc2MiLCJuIjoiZm9yd2FyZCJ9
note

Why cursor pagination is recommended:

  • Stable results — new or deleted records between requests won't cause items to be skipped or repeated, unlike offset pagination.
  • Consistent performance — it doesn't get slower as you page deeper.
warning

A cursor encodes the sort field and direction it was issued for. Keep sortBy and sortDirection identical across requests in the same sequence — changing them invalidates the cursor and the request will be rejected. Cursors are opaque; do not construct or modify them.

Cursor response structure​

{
"data": [
/* Array of items */
],
"pagination": {
"mode": "cursor",
"pageSize": 10,
"hasNextPage": true,
"hasPreviousPage": false,
"nextCursor": "eyJzIjoiY3JlYXRlZEF0IiwiZCI6ImFzYyIsIm4iOiJmb3J3YXJkIn0",
"prevCursor": null
}
}
  • data: An array of items returned by the endpoint.
  • pagination.mode: Always "cursor" for cursor pagination.
  • pagination.pageSize: The number of items per page.
  • pagination.hasNextPage: Indicates if there is a next page of results.
  • pagination.hasPreviousPage: Indicates if there is a previous page of results.
  • pagination.nextCursor: Opaque cursor for the next page, or null when there is no next page.
  • pagination.prevCursor: Opaque cursor for the previous page, or null when there is no previous page.
  • pagination.totalCount: How many items the filters match. Only present when you send withTotal=true. Core API only.
  • pagination.totalCountIsExact: Whether totalCount is exact. Only present when you send withTotal=true. Core API only.

Counting the matches​

Core API only

withTotal is served by the Core API. The OMS API ignores it and returns no totalCount.

Cursor pagination reports no total by default. Send withTotal=true to add one:

GET /payments?limit=10&status[eq]=cancelled&withTotal=true

Counting stops at 10,000. When more items match, totalCount holds 10,000 and totalCountIsExact is false, so show it as a bound like "over 10,000" rather than as a total — check that field before displaying the number.

The count describes everything the filters match and ignores cursor, so it does not change as you page. Request it when the filters change rather than on every page.

If you only need the total, offset pagination already reports one as itemCount without any extra parameter.

Offset pagination​

To use offset pagination, use the page and pageSize query parameters:

GET /payments?page=1&pageSize=10
ParameterDescriptionSupported ValuesDefault Value
pageThe page number to retrieve.Positive integers1
pageSizeThe number of items per page.Positive integers less than or equal to 10010
note

Invalid page or pageSize values will fail silently, returning the first page with the default size.

Offset response structure​

{
"data": [
/* Array of items */
],
"pagination": {
"mode": "offset",
"hasNextPage": true,
"hasPreviousPage": false,
"itemCount": 40,
"page": 1,
"pageCount": 4,
"pageSize": 10
}
}
  • data: An array of items returned by the endpoint.
  • pagination.mode: Always "offset" for offset pagination.
  • pagination.hasNextPage: Indicates if there is a next page of results.
  • pagination.hasPreviousPage: Indicates if there is a previous page of results.
  • pagination.itemCount: The total number of items for the resource, including all pages.
  • pagination.page: The current page number.
  • pagination.pageCount: The total number of pages available.
  • pagination.pageSize: The number of items per page.

Sorting​

To sort results, you can use the sortBy and sortDirection query parameters:

GET /payments?sortBy=createdAt&sortDirection=desc
ParameterDescriptionSupported ValuesDefault Value
sortByThe field to sort by.Any sortable fieldcreatedAt
sortDirectionThe direction of sorting.asc, desc, ASC, DESCasc

Each endpoint supports different fields for sorting, which are documented below in the Supported endpoints section.

Filtering​

To filter results, you can pass the filter criteria as a query parameter in LHS bracket notation:

GET /payments?amount[gt]=100&status[eq]=succeeded

The list of supported operators differs by data type. The following operators are supported:

OperatorDescriptionBooleanNumberStringDate
eqEqual to✓✓✓✓
neNot equal to✓✓✓✓
gtGreater than✓✓
gteGreater than or equal to✓✓
ltLess than✓✓
lteLess than or equal to✓✓
inIn✓✓✓
ninNot in✓✓✓
likeLike (partial match)✓

One more operator, contains, is not in the table because it depends on the field rather than on the data type. See Using contains operator.

Each endpoint supports different fields for filtering, which are documented below in the Supported endpoints section.

warning

All filter values come to ODUS API as strings, and the API will attempt to convert them to a valid type based on the field's data type. If the conversion fails, the filter will be ignored.

Parsing rules:

  • Boolean: only true and false are valid.
  • Number: only integers are valid. Float values are parsed as integers (e.g., 100.5 becomes 100).
  • String: any string is valid. String filters are case-insensitive (e.g., name[eq]=john will match John, JOHN, etc.).
  • Date: supports ISO 8601 date strings (e.g., 2023-10-01T00:00:00Z) and short date formats (e.g., 2023-10-01).
  • Null: use null to filter for null values (e.g., GET /payments?customer[eq]=null returns payments with no customer attached).

Building query strings​

We recommend using a QS library like qs which allows building complex query strings from objects:

Using qs to build query strings
import qs from 'qs';

const query = qs.stringify({
amount: { gt: 100 },
status: { eq: 'succeeded' },
});

const url = new URL(`https://api.odus.com/payments`);
url.search = query;

console.log(url.toString()); // https://api.odus.com/payments?amount[gt]=100&status[eq]=succeeded

Filtering by nested fields​

To filter by nested fields, you can use dot notation in the filter criteria for the supported endpoints:

GET /payments?customer.email[like]=example.com

Using in and nin operators​

To pass arrays as filter values, you can use the in and nin operators. The values should be passed individually, each in its own query parameter:

GET /payments?status[in]=succeeded&status[in]=pending
warning

Comma notation (e.g., status[in]=succeeded,pending) is not supported and will be parsed literally, which may lead to unexpected results. Always use the in operator with separate query parameters for each value.

Using contains operator​

contains is published per field rather than per data type, and it means different things on the two APIs. The API reference is authoritative: a field's parameter lists contains when the endpoint accepts it.

Sending contains on a field the endpoint does list but does not offer it on is answered with 400, naming the field. A query parameter the endpoint does not list at all is ignored, as any unrecognised parameter is — so check the spelling of the field as well as the operator.

Core API — contains checks whether a field that holds several values contains a specific one. That is supportedPaymentMethods on Gateway Profiles:

GET /gateway-profiles?supportedPaymentMethods[contains]=card

To pass multiple values to check against, use the $and operator with array notation:

GET /gateway-profiles?$and[0][supportedPaymentMethods][contains]=applepay&$and[1][supportedPaymentMethods][contains]=card

OMS API — two lists accept contains on every field they publish, where it matches a substring of the value:

GET /inventory?sku[contains]=SHIRT
GET /integrations/{id}/products?name[contains]=shirt
info

contains is not a text search anywhere else. To match text inside a string field, use like: ?name[like]=premium.

Using logical $or operator​

note

The $or operator is only supported at the root level of the query string, and one $or group cannot be nested within another. It can be combined with other root filter criteria, which are applied alongside the group with logical AND.

It is possible to to match multiple condition groups using the $or operator. Any result that matches either OR group will be included in the results:

GET /payments?$or[0][status][eq]=succeeded&$or[0][isCaptured][eq]=true&$or[1][status][eq]=succeeded&$or[1][isReversed][eq]=true&$or[1][isCaptured][eq]=true

All conditions within a single $or group are combined using logical AND. The example above is a query to find all payments that are either captured or captured and reversed afterwards.

  1. $or[0][status][eq]=succeeded&$or[0][isCaptured][eq]=true = The payment status must be succeeded and the payment must be captured.
  2. $or[1][status][eq]=succeeded&$or[1][isReversed][eq]=true&$or[1][isCaptured][eq]=true = The payment status must be succeeded and the payment must be both reversed and captured.

JSON fields filtering​

JSON fields, such as metadata, support filtering using the same operators as string fields. You can filter by specific keys within the JSON object using bracket notation. Unlike regular string fields, JSON field filters are case-sensitive — the filter value must match the stored value exactly.

Let's take this payment as an example:

{
"id": "pay_123",
"amount": 100,
"metadata": {
"field1": "value",
"field2": "value2"
}
}

You can filter by field1 in the metadata object like this:

GET /payments?metadata[field1][eq]=value
important

When filtering metadata fields with ne, nin, like, in, or comparison operators, entities without that metadata key will NOT be included in the results due to SQL NULL semantics.

For example:

  • metadata[field1][eq]=value - Only matches entities where field1 exists and equals value
  • metadata[field1][ne]=value - Only matches entities where field1 exists and does NOT equal value (entities without field1 are excluded)

To include entities without a specific metadata key, use the $or operator to create a condition group that checks for both the presence of the key with the desired value and the absence of the key:

GET /payments?$or[0][metadata][field1][ne]=test&$or[1][metadata][field1][eq]=null

This query means: "include payments where field1 is not 'test' OR where field1 doesn't exist"

Read more about metadata in the Metadata guide.

Supported endpoints​

Customers​

Endpoint: GET /customers

Sortable fields: id, createdAt, updatedAt

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
firstNameString
lastNameString
emailString
phoneNumberString
dateOfBirthDate
restrictMitBoolean
restrictCitBoolean
restrictReasonString
metadataJSON

Payments​

Endpoint: GET /payments

Sortable fields: id, createdAt, updatedAt, amount, status, currency

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
amountNumber
statusString
displayStatusString
currencyString
amountCapturedNumber
amountReversedNumber
captureMethodString
captureDelayNumber
captureAtDate
statementDescriptorString
isCapturedBoolean
isReversedBoolean
isChargebackedBoolean
isRedirectRequiredBoolean
customerString
customer.emailString
latestTransactionString
latestTransaction.processorIdString
paymentMethodString
paymentMethod.typeString
paymentMethod.card.lastFourString
paymentMethod.card.binString
paymentMethod.card.brandString
paymentMethod.card.expYearString
paymentMethod.card.expMonthString
paymentMethodOptions.card.gatewayProfileString
paymentMethodOptions.card.cascadeString
paymentMethodOptions.card.modeString
paymentMethodOptions.paypal.gatewayProfileString
metadataJSON

Subscriptions​

Endpoint: GET /subscriptions

Sortable fields: id, createdAt, updatedAt, status, currentPeriodStart, currentPeriodEnd, startDate, endDate

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
statusString
startDateDate
endDateDate
autoBillingEnabledBoolean
autoBillingDisabledAtDate
autoBillingDisabledReasonString
cancelAtPeriodEndBoolean
currentPeriodStartDate
currentPeriodEndDate
currentCycleNumber
customerString
customer.emailString
priceString
metadataJSON
setupInvoice.paymentString

Payment Methods​

Endpoint: GET /payment-methods

Sortable fields: id, createdAt, updatedAt, isDefault, customer

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
isDefaultBoolean
customerString
card.lastFourString
card.brandString
card.expYearString
card.expMonthString

Users​

Endpoint: GET /users

Sortable fields: id, createdAt, updatedAt, email

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
emailString
firstNameString
lastNameString
usernameString

Webhook Subscribers​

Endpoint: GET /webhook-subscribers

Sortable fields: id, createdAt, updatedAt

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate

Products​

Endpoint: GET /products

Sortable fields: id, createdAt, updatedAt, name, active

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
nameString
activeBoolean
metadataJSON

Prices​

Endpoint: GET /prices

Sortable fields: id, createdAt, updatedAt, amount, type

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
amountNumber
currencyString
typeString
nameString
productString
product.idString
product.nameString
activeBoolean
metadataJSON

Invoices​

Endpoint: GET /invoices

Sortable fields: id, createdAt, updatedAt, status

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
statusString
currencyString
amountNumber
voidedReasonString
periodStartDate
periodEndDate
paymentString
retryNextAtDate
retryNextGatewayString
retryPolicyString
subscriptionString
numberString

Customer Blacklist​

Endpoint: GET /blacklist

Sortable fields: id, createdAt, updatedAt, email

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
emailString
reasonString

Secret Keys​

Endpoint: GET /secret-keys

Sortable fields: id, createdAt, updatedAt

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
activeBoolean

Request Logs​

Endpoint: GET /logs

Sortable fields: id, createdAt, updatedAt

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
requestIdString
urlString
methodString
clientIpString
statusCodeNumber
initiatorString

Gateway Profiles​

Endpoint: GET /gateway-profiles

Sortable fields: id, createdAt, updatedAt

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
titleString
integrationString
activeBoolean
supportedPaymentMethodsString

supportedPaymentMethods holds several values, so it also accepts contains.

Cascades​

Endpoint: GET /cascades

Sortable fields: id, createdAt, updatedAt, active, name

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
nameString
activeBoolean
algorithmString

Jobs​

Endpoint: GET /jobs

Sortable fields: id, createdAt, updatedAt, status, type

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
statusString
typeString
progressNumber

Job Items​

Endpoint: GET /jobs/:id/items

Sortable fields: id, createdAt, updatedAt, status

Filterable fields:

FieldData Type
idString
createdAtDate
updatedAtDate
statusString