> ## Documentation Index
> Fetch the complete documentation index at: https://docs.signa.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Bulk Operations

> Retrieve, reconcile, and paginate through large volumes of trademark data efficiently

This guide is for high-volume consumers: portfolio management tools, docketing systems, and anything else that regularly needs hundreds or thousands of trademark records rather than a handful. It covers chunked batch retrieval, diffing your own records against the register with `/v1/reconcile`, pulling large result sets efficiently, and staying under your rate limit while doing all of it concurrently.

## Chunked batch retrieval

[`POST /v1/trademarks/batch`](/api-reference/trademarks/batch-trademarks) resolves up to 100 IDs (or office-native identifiers) in a single request and counts as **one** call against your rate limit, regardless of how many IDs you send. Matched records come back in `data`; anything that did not resolve comes back in `not_found`, there is no per-item success/error status to inspect.

When you have more than 100 IDs, split them into chunks and process sequentially or with controlled concurrency:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Signa, type TrademarkBatchResponse } from "@signa-so/sdk";

  const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });

  function chunk<T>(items: T[], size: number): T[][] {
    const chunks: T[][] = [];
    for (let i = 0; i < items.length; i += size) {
      chunks.push(items.slice(i, i + size));
    }
    return chunks;
  }

  async function fetchAllTrademarks(ids: string[]) {
    const results: TrademarkBatchResponse['data'] = [];
    const missing: (string | Record<string, string>)[] = [];

    for (const batch of chunk(ids, 100)) {
      const response = await signa.trademarks.batch({ ids: batch });
      results.push(...response.data);
      if (response.not_found.length > 0) {
        missing.push(...response.not_found);
      }
    }

    return { results, missing };
  }
  ```

  ```bash cURL theme={null}
  # Split a large ID list into chunks of 100 with jq, then call batch per chunk
  jq -c '_nwise(100)' <<< "$(cat ids.json)" | while read -r ids_chunk; do
    curl -s -X POST https://api.signa.so/v1/trademarks/batch \
      -H "Authorization: Bearer $SIGNA_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{\"ids\": $ids_chunk}"
  done
  ```
</CodeGroup>

<Note>
  `_nwise` is a `jq` helper for fixed-size batching (`def _nwise(n): def n1: if length <= n then . else .[0:n], (.[n:] | n1) end; n1;`). Any language's array-chunking utility works the same way, the API side only cares that each request has 100 IDs or fewer.
</Note>

<Tip>
  See [Batch Get Trademarks](/api-reference/trademarks/batch-trademarks) for request/response field detail, and [Resilience Patterns](/guides/resilience#bulk-operation-retry) for retrying a batch call that fails at the request level (rate limited or a transient 5xx).
</Tip>

## Reconciling your own records

If you already hold trademark data (from a legacy system, a spreadsheet, or another vendor) and want to find where it has drifted from the register, use [`POST /v1/reconcile`](/api-reference/reference/reconcile) instead of fetching full records and diffing client-side. Send the fields you have per record, get a field-by-field match/mismatch back, and nothing is stored server-side.

```typescript TypeScript theme={null}
const results = await signa.reconcile.run({
  items: [
    {
      office: "uspto",
      application_number: "88123456",
      your_fields: { status: "active", owner_name: "Nike, Inc." },
    },
    // up to 100 items per call
  ],
});

for (const item of results.data) {
  if (item.result === "mismatch") {
    console.log(`${item.office}/${item.application_number}: ${item.mismatch_count} field(s) drifted`);
    for (const field of item.fields) {
      if (!field.match) {
        console.log(`  ${field.field}: yours="${field.your_value}" register="${field.register_value}"`);
      }
    }
  } else if (item.result === "not_found") {
    console.log(`${item.office}/${item.application_number}: no matching register record`);
  }
}
```

`result` is one of `match`, `mismatch`, `not_found`, or `ambiguous` (the identifier matched more than one register record). Reconcile is capped at 100 items per call just like batch, so the same chunking approach applies to a large book of matters.

## Pagination at scale

For anything larger than a single batch call, the ordinary list and search endpoints paginate with a cursor. See [Pagination](/api-reference/pagination) for the full contract (cursor stability, 24-hour expiry, sort requirements). A few things matter specifically at volume:

* Request `limit=100` (the max) instead of the default 20 to cut the number of round trips.
* Use the SDK's `for await` iteration or `toArray()` instead of a manual cursor loop, `SignaList` follows `has_more`/`cursor` for you.
* For a long-running export, checkpoint the cursor as you go. Cursors expire after 24 hours; if a run stalls past that window, restart pagination rather than trying to resume a stale cursor.
* Only pass `include_total=true` when you actually need a count to display. It costs an extra query and is unnecessary for a pure export.

```typescript TypeScript theme={null}
let processed = 0;
for await (const tm of await signa.trademarks.search({
  filters: { owner_id: "own_helios01" },
  limit: 100,
})) {
  await handleRecord(tm); // your own processing
  processed++;
}
console.log(`Processed ${processed} records`);
```

## Rate-limit-aware concurrency

Batch and reconcile calls are cheap per item, but you can still run into your plan's rate limit if you fire many of them concurrently. Every response carries a `RateLimit` header shaped `remaining=<count>, reset=<seconds>` (see [Rate Limits](/api-reference/rate-limits) for the limits themselves). A simple pattern: cap concurrency with a semaphore, and back off early when `remaining` gets low instead of waiting for a `429`.

```typescript TypeScript theme={null}
function parseRateLimitHeader(value: string | null): { remaining: number; reset: number } | null {
  if (!value) return null;
  const remaining = /remaining=(\d+)/.exec(value);
  const reset = /reset=(\d+)/.exec(value);
  if (!remaining || !reset) return null;
  return { remaining: Number(remaining[1]), reset: Number(reset[1]) };
}

async function runWithConcurrency<T, R>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<R>,
): Promise<R[]> {
  const results: R[] = new Array(items.length);
  let cursor = 0;
  let paused = false;

  async function worker() {
    while (cursor < items.length) {
      if (paused) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        continue;
      }
      const i = cursor++;
      results[i] = await fn(items[i]);
    }
  }

  await Promise.all(Array.from({ length: limit }, worker));
  return results;
}

const idChunks = chunk(allIds, 100);
await runWithConcurrency(idChunks, 5, async (ids) => {
  const response = await fetch("https://api.signa.so/v1/trademarks/batch", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SIGNA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ ids }),
  });

  const limitInfo = parseRateLimitHeader(response.headers.get("RateLimit"));
  if (limitInfo && limitInfo.remaining < 20) {
    // Getting close to the ceiling, slow down for the rest of this window
    await new Promise((resolve) => setTimeout(resolve, limitInfo.reset * 1000));
  }

  return response.json();
});
```

<Tip>
  If you are on the SDK, its built-in retry logic already handles `429` with `Retry-After`, see [Resilience Patterns](/guides/resilience). The concurrency guard above is for avoiding the `429` in the first place when you control the fan-out.
</Tip>

## What's next

<Card title="Resilience Patterns" href="/guides/resilience">
  Retry logic, circuit breakers, and idempotent mutations for fault-tolerant integrations.
</Card>

<Card title="Pagination" href="/api-reference/pagination">
  The full cursor-pagination contract for list and search endpoints.
</Card>
