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.
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.
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.
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.
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.
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.