Skip to main content
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 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:
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 };
}
_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.
See Batch Get Trademarks for request/response field detail, and Resilience Patterns for retrying a batch call that fails at the request level (rate limited or a transient 5xx).

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 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
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 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
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 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
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();
});
If you are on the SDK, its built-in retry logic already handles 429 with Retry-After, see Resilience Patterns. The concurrency guard above is for avoiding the 429 in the first place when you control the fan-out.

What’s next

Resilience Patterns

Retry logic, circuit breakers, and idempotent mutations for fault-tolerant integrations.

Pagination

The full cursor-pagination contract for list and search endpoints.