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
limitquery 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
pageandpageSizequery 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 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.
Cursor pagination (recommended)
To use cursor pagination, provide the limit query parameter. On the first request, omit cursor:
GET /payments?limit=10
| Parameter | Description | Supported Values | Default Value |
|---|---|---|---|
limit | The number of items per page. Enables cursor pagination. | Positive integers less than or equal to 100 | 10 |
cursor | An opaque token identifying the page boundary to read from. | A cursor returned by a previous response | none |
withTotal | Also return how many items the filters match, as totalCount. Core API only. | true | none |
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
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.
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, ornullwhen there is no next page.pagination.prevCursor: Opaque cursor for the previous page, ornullwhen there is no previous page.pagination.totalCount: How many items the filters match. Only present when you sendwithTotal=true. Core API only.pagination.totalCountIsExact: WhethertotalCountis exact. Only present when you sendwithTotal=true. Core API only.
Counting the matches
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
| Parameter | Description | Supported Values | Default Value |
|---|---|---|---|
page | The page number to retrieve. | Positive integers | 1 |
pageSize | The number of items per page. | Positive integers less than or equal to 100 | 10 |
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
| Parameter | Description | Supported Values | Default Value |
|---|---|---|---|
sortBy | The field to sort by. | Any sortable field | createdAt |
sortDirection | The direction of sorting. | asc, desc, ASC, DESC | asc |
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:
| Operator | Description | Boolean | Number | String | Date |
|---|---|---|---|---|---|
eq | Equal to | ✓ | ✓ | ✓ | ✓ |
ne | Not equal to | ✓ | ✓ | ✓ | ✓ |
gt | Greater than | ✓ | ✓ | ||
gte | Greater than or equal to | ✓ | ✓ | ||
lt | Less than | ✓ | ✓ | ||
lte | Less than or equal to | ✓ | ✓ | ||
in | In | ✓ | ✓ | ✓ | |
nin | Not in | ✓ | ✓ | ✓ | |
like | Like (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.
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
trueandfalseare valid. - Number: only integers are valid. Float values are parsed as integers (e.g.,
100.5becomes100). - String: any string is valid. String filters are case-insensitive (e.g.,
name[eq]=johnwill matchJohn,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
nullto filter for null values (e.g.,GET /payments?customer[eq]=nullreturns payments with no customer attached).
Building query strings
We recommend using a QS library like qs which allows building complex query strings from objects:
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
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
contains is not a text search anywhere else. To match text inside a string field, use like:
?name[like]=premium.
Using logical $or operator
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.
$or[0][status][eq]=succeeded&$or[0][isCaptured][eq]=true= The payment status must besucceededand the payment must be captured.$or[1][status][eq]=succeeded&$or[1][isReversed][eq]=true&$or[1][isCaptured][eq]=true= The payment status must besucceededand 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
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 wherefield1exists and equalsvaluemetadata[field1][ne]=value- Only matches entities wherefield1exists and does NOT equalvalue(entities withoutfield1are 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:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
firstName | String |
lastName | String |
email | String |
phoneNumber | String |
dateOfBirth | Date |
restrictMit | Boolean |
restrictCit | Boolean |
restrictReason | String |
metadata | JSON |
Payments
Endpoint: GET /payments
Sortable fields: id, createdAt, updatedAt, amount, status, currency
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
amount | Number |
status | String |
displayStatus | String |
currency | String |
amountCaptured | Number |
amountReversed | Number |
captureMethod | String |
captureDelay | Number |
captureAt | Date |
statementDescriptor | String |
isCaptured | Boolean |
isReversed | Boolean |
isChargebacked | Boolean |
isRedirectRequired | Boolean |
customer | String |
customer.email | String |
latestTransaction | String |
latestTransaction.processorId | String |
paymentMethod | String |
paymentMethod.type | String |
paymentMethod.card.lastFour | String |
paymentMethod.card.bin | String |
paymentMethod.card.brand | String |
paymentMethod.card.expYear | String |
paymentMethod.card.expMonth | String |
paymentMethodOptions.card.gatewayProfile | String |
paymentMethodOptions.card.cascade | String |
paymentMethodOptions.card.mode | String |
paymentMethodOptions.paypal.gatewayProfile | String |
metadata | JSON |
Subscriptions
Endpoint: GET /subscriptions
Sortable fields: id, createdAt, updatedAt, status, currentPeriodStart, currentPeriodEnd, startDate, endDate
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
status | String |
startDate | Date |
endDate | Date |
autoBillingEnabled | Boolean |
autoBillingDisabledAt | Date |
autoBillingDisabledReason | String |
cancelAtPeriodEnd | Boolean |
currentPeriodStart | Date |
currentPeriodEnd | Date |
currentCycle | Number |
customer | String |
customer.email | String |
price | String |
metadata | JSON |
setupInvoice.payment | String |
Payment Methods
Endpoint: GET /payment-methods
Sortable fields: id, createdAt, updatedAt, isDefault, customer
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
isDefault | Boolean |
customer | String |
card.lastFour | String |
card.brand | String |
card.expYear | String |
card.expMonth | String |
Users
Endpoint: GET /users
Sortable fields: id, createdAt, updatedAt, email
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
email | String |
firstName | String |
lastName | String |
username | String |
Webhook Subscribers
Endpoint: GET /webhook-subscribers
Sortable fields: id, createdAt, updatedAt
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
Products
Endpoint: GET /products
Sortable fields: id, createdAt, updatedAt, name, active
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
name | String |
active | Boolean |
metadata | JSON |
Prices
Endpoint: GET /prices
Sortable fields: id, createdAt, updatedAt, amount, type
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
amount | Number |
currency | String |
type | String |
name | String |
product | String |
product.id | String |
product.name | String |
active | Boolean |
metadata | JSON |
Invoices
Endpoint: GET /invoices
Sortable fields: id, createdAt, updatedAt, status
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
status | String |
currency | String |
amount | Number |
voidedReason | String |
periodStart | Date |
periodEnd | Date |
payment | String |
retryNextAt | Date |
retryNextGateway | String |
retryPolicy | String |
subscription | String |
number | String |
Customer Blacklist
Endpoint: GET /blacklist
Sortable fields: id, createdAt, updatedAt, email
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
email | String |
reason | String |
Secret Keys
Endpoint: GET /secret-keys
Sortable fields: id, createdAt, updatedAt
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
active | Boolean |
Request Logs
Endpoint: GET /logs
Sortable fields: id, createdAt, updatedAt
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
requestId | String |
url | String |
method | String |
clientIp | String |
statusCode | Number |
initiator | String |
Gateway Profiles
Endpoint: GET /gateway-profiles
Sortable fields: id, createdAt, updatedAt
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
title | String |
integration | String |
active | Boolean |
supportedPaymentMethods | String |
supportedPaymentMethods holds several values, so it also accepts
contains.
Cascades
Endpoint: GET /cascades
Sortable fields: id, createdAt, updatedAt, active, name
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
name | String |
active | Boolean |
algorithm | String |
Jobs
Endpoint: GET /jobs
Sortable fields: id, createdAt, updatedAt, status, type
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
status | String |
type | String |
progress | Number |
Job Items
Endpoint: GET /jobs/:id/items
Sortable fields: id, createdAt, updatedAt, status
Filterable fields:
| Field | Data Type |
|---|---|
id | String |
createdAt | Date |
updatedAt | Date |
status | String |