Skip to main content
Signa uses cursor-based pagination for all list and search endpoints. Cursors provide stable, consistent iteration over datasets that may change between requests — no skipped records, no duplicates, no performance degradation at depth.

Basic Usage

Your first request specifies a limit (items per page). The response includes a cursor and has_more flag:
curl -H "Authorization: Bearer sig_live_YOUR_KEY" \
  "https://api.signa.so/v1/trademarks?limit=50"
Response:
{
  "object": "list",
  "data": [
    { "id": "tm_7d4e1f2a", "object": "trademark", "mark_text": "SIGNA", ... },
    { "id": "tm_3b8c9d0e", "object": "trademark", "mark_text": "AURORA", ... }
  ],
  "has_more": true,
  "pagination": {
    "cursor": "eyJpZCI6IjNiOGM5ZDAw..."
  },
  "livemode": true,
  "request_id": "req_abc123"
}
To fetch the next page, pass the cursor from the previous response:
curl -H "Authorization: Bearer sig_live_YOUR_KEY" \
  "https://api.signa.so/v1/trademarks?limit=50&cursor=eyJpZCI6IjNiOGM5ZDAw..."
When has_more is false, you have reached the end of the result set. The cursor field will be null.

Pagination Parameters

ParameterTypeDefaultDescription
limitinteger20Items per page. Min 1, max 100.
cursorstringOpaque cursor from a previous response.
sortstringvariesSort field with optional - prefix for descending (e.g., -filing_date).
include_totalbooleanfalseInclude total_count in the pagination object.

Response Shape

All list endpoints return the same structure. Default response (without include_total):
{
  "object": "list",
  "data": [ ... ],
  "has_more": true,
  "pagination": {
    "cursor": "eyJpZCI6..."
  },
  "livemode": true,
  "request_id": "req_abc123"
}
With ?include_total=true:
{
  "object": "list",
  "data": [ ... ],
  "has_more": true,
  "pagination": {
    "cursor": "eyJpZCI6...",
    "total_count": 12847
  },
  "livemode": true,
  "request_id": "req_abc123"
}
Key details:
  • has_more is always at the top level (not inside pagination).
  • pagination.cursor is null when has_more is false.
  • pagination.total_count is only present when include_total=true is passed as a query parameter. It is omitted by default.

Cursor Stability

Cursors encode a position in the result set, not an offset. This means: Records inserted after your cursor was created will not appear in your current pagination session. You will not see duplicates or gaps. Records deleted after your cursor was created are silently skipped. Your page may contain fewer items than the requested limit in rare cases. Concurrent updates do not affect cursor validity. Even if the underlying data changes, your cursor continues from where it left off.
Cursors are scoped to a specific query, sort order, and filter combination. Changing any of these parameters invalidates the cursor — start a fresh pagination session instead.

Cursor Expiration

Cursors expire after 24 hours. If you pass an expired cursor, you receive a cursor_expired error:
{
  "error": {
    "type": "https://api.signa.so/errors/cursor_expired",
    "title": "Cursor expired",
    "status": 400,
    "detail": "The pagination cursor has expired. Start a new query.",
    "retryable": false,
    "retry_after": null
  },
  "request_id": "req_abc123"
}
For long-running syncs, checkpoint the cursor and resume within the 24-hour window. If the cursor expires, restart from the beginning.
Cursors are opaque strings. Do not parse, decode, or construct them. Their internal format may change without notice.

Sorting and Cursors

You can sort results using the sort parameter. The cursor encodes the sort position, so you must use the same sort value for every page in a session. Supported sort fields vary by endpoint:
EndpointSupported Sort FieldsDefault
GET /v1/trademarksfiling_date, updated_at, mark_text-filing_date
GET /v1/ownerscanonical_name, updated_atcanonical_name
POST /v1/trademarks/searchrelevance, filing_date, updated_at-relevance
Sort direction is specified with a - prefix for descending (no prefix = ascending):
GET /v1/trademarks?sort=-filing_date&limit=50
GET /v1/trademarks?sort=filing_date&limit=50    # ascending
For search endpoints, the default sort is -relevance. Changing to a date-based sort still returns all matching results, just in a different order.

Total Count

By default, the total_count is not included in the response. To include it, pass include_total=true:
GET /v1/trademarks?limit=50&include_total=true
{
  "object": "list",
  "data": [ ... ],
  "has_more": true,
  "pagination": {
    "cursor": "eyJpZCI6...",
    "total_count": 12847
  },
  "livemode": true
}
Computing the total count requires a separate database query and can be expensive on large datasets. Only request it when you need to display a progress indicator or total count in your UI. Avoid including it on every page request.

Async Iterator Pattern (TypeScript SDK)

The SDK provides an async iterator for automatic pagination. This is the recommended way to process all records:
import { Signa } from '@signa-so/sdk';

const signa = new Signa({ api_key: 'sig_live_YOUR_KEY' });

// Iterate through all trademarks matching a filter
for await (const trademark of signa.trademarks.list({
  'offices[]': ['uspto'],
  status_stage: 'registered',
})) {
  console.log(trademark.id, trademark.mark_text);
}
Under the hood, the iterator fetches pages of 100 items and yields them one at a time. When a page is exhausted, it transparently fetches the next page using the cursor. You can also collect all results into an array (use with caution on large datasets):
const allTrademarks = await signa.trademarks
  .list({ 'offices[]': ['uspto'] })
  .toArray();

console.log(`Fetched ${allTrademarks.length} trademarks`);
For more control, use the page-level iterator:
for await (const page of signa.trademarks.list({ limit: 50 }).pages()) {
  console.log(`Page with ${page.data.length} items, has_more: ${page.has_more}`);

  for (const trademark of page.data) {
    await processTrademarkRecord(trademark);
  }
}

Common Patterns

For full data syncs, store the last cursor to enable resume-on-failure:
let cursor: string | undefined = loadCheckpoint(); // from database or file

while (true) {
  const page = await signa.trademarks.list({ limit: 100, cursor });

  for (const trademark of page.data) {
    await upsertToLocalDb(trademark);
  }

  saveCheckpoint(page.pagination.cursor); // persist cursor

  if (!page.has_more) break;
  cursor = page.pagination.cursor;
}
Request the total on the first page only, then track progress as you paginate:
const firstPage = await signa.trademarks.list({
  limit: 100,
  include_total: true,
});

let processed = firstPage.data.length;
const total = firstPage.pagination.total_count;
console.log(`Processing ${processed}/${total}...`);

let cursor = firstPage.pagination.cursor;

while (cursor) {
  const page = await signa.trademarks.list({ limit: 100, cursor });
  processed += page.data.length;
  console.log(`Processing ${processed}/${total}...`);
  cursor = page.has_more ? page.pagination.cursor : undefined;
}
If processing each page is slow (e.g., enrichment), you can prefetch the next page while processing the current one:
let cursor: string | undefined;
let nextPagePromise = signa.trademarks.list({ limit: 100 });

while (true) {
  const page = await nextPagePromise;

  // Start fetching next page before processing current
  if (page.has_more && page.pagination.cursor) {
    nextPagePromise = signa.trademarks.list({
      limit: 100,
      cursor: page.pagination.cursor,
    });
  }

  await processPage(page.data);

  if (!page.has_more) break;
}