Skip to main content
You build docketing software. Your customers are paralegals and attorneys who must answer every office action on time, file every renewal, and never miss a certificate. The hard part is not the deadline math, it is getting the underlying office documents reliably: office actions the day they issue, in a form you can attach to a matter, without each of your customers standing up their own TSDR poller and getting throttled. Signa fetches those documents for you on demand and streams the files through one URL. This guide wires the documents endpoint and the media proxy into a docketing loop.

Prerequisites

  • A Signa API key with trademarks:read scope
  • The trademark IDs (or office-native identifiers) for the marks you docket
  • Somewhere to store attachments (the guide streams PDFs straight to your matter store)

1

Lazy-fetch documents on first sight of a mark

When a mark enters your docket, ask for its documents. For a USPTO mark that Signa has never synced, this first request triggers an inline TSDR metadata fetch. You do not schedule anything, the read path does it.
import { Signa } from "@signa-so/sdk";

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

const docs = await signa.trademarks.documents("tm_8kLm2nPq");
console.log(docs.source_sync?.status); // "synced" | "pending" | "unsupported"
The response is a normal list, plus a source_sync object telling you how fresh the metadata is.
2

Interpret source_sync before you trust the list

Branch on source_sync.status. It is the difference between “no documents” and “not fetched yet”.
  • synced: the list is current. Store it.
  • pending: a fetch is in flight (another request holds the lock) or upstream is still settling. data may be empty or partial. Do not record “no documents”, request again shortly.
  • unsupported: the mark’s office has no document support today (everything except USPTO). Skip it, no amount of polling changes this.
async function loadDocuments(tmId: string) {
  const docs = await signa.trademarks.documents(tmId);
  switch (docs.source_sync?.status) {
    case "synced":
      return docs.data;
    case "unsupported":
      return []; // office has no documents; stop here
    case "pending":
    default:
      return null; // not ready; caller should retry
  }
}
3

Poll to freshness with backoff

When you get pending, request again. A short bounded retry with backoff is enough, the fetch is a single upstream round-trip, not a long job.
async function documentsWhenReady(tmId: string, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    const result = await loadDocuments(tmId);
    if (result !== null) return result; // synced or unsupported
    await new Promise((r) => setTimeout(r, 500 * 2 ** i)); // 0.5s, 1s, 2s...
  }
  return []; // give up for this cycle; the next docket run retries
}
Signa collapses concurrent first-requests for the same mark into a single upstream fetch, so you do not need your own lock. If your worker fleet asks for the same never-synced mark at once, exactly one TSDR fetch happens and the rest see pending, then synced.
4

Filter to the documents that drive deadlines

Docketing cares about specific kinds. Filter server-side by document_kind and by official date so you only pull what changes a deadline.
// New office actions since your last docket run
const officeActions = await signa.trademarks.documents("tm_8kLm2nPq", {
  document_kind: "office_action",
  official_date_gte: "2025-01-01",
});

for (const oa of officeActions.data) {
  console.log(oa.official_date, oa.description, oa.url);
}
cURL
curl "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/documents?document_kind=office_action&official_date_gte=2025-01-01" \
  -H "Authorization: Bearer $SIGNA_API_KEY"
Each row carries a url. That is your handle to the file.
5

Stream the office-action PDF into the matter

Follow the row’s url to stream the file. It points at the media proxy, which serves the bytes with their real content type (application/pdf for office actions and certificates) and X-Content-Type-Options: nosniff.
for (const oa of officeActions.data) {
  const res = await fetch(oa.url); // unauthenticated, IP rate-limited
  if (res.status === 502) {
    // TSDR download budget momentarily exhausted on a cold fetch
    const retryAfter = Number(res.headers.get("Retry-After") ?? "60");
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    continue; // retry this document next pass
  }
  const pdf = Buffer.from(await res.arrayBuffer());
  await saveAttachment(oa.id, pdf); // attach to the matter by med_ id
}
Two rules for the proxy:
  • It is unauthenticated but IP rate-limited. Use it as a per-download link, not a bulk drain. Fetch a document when you need to attach it, not on a sweep.
  • The first download of a USPTO file is a cold fetch. The proxy pulls from TSDR under a shared budget and persists the file. When the budget is momentarily spent, you get 502 upstream_error with Retry-After (seconds). Honor it. Once persisted, later downloads serve stored bytes and never touch TSDR.
cURL
curl -L "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/media/med_019d2141-6ce9-771b-872e-bc8b20e49fcf" \
  -o office-action.pdf
6

Stay fresh with forward-only refresh

Documents are forward-only. New filings and office actions appear over time, existing rows do not mutate or disappear. That makes the refresh loop cheap: on each docket run, request documents for your active marks filtered by official_date_gte set to your last run, and you only see what is new.
async function refresh(tmId: string, sinceDate: string) {
  const docs = await signa.trademarks.documents(tmId, {
    official_date_gte: sinceDate, // YYYY-MM-DD (your last successful run date)
  });
  if (docs.source_sync?.status === "pending") return; // retry next cycle
  return docs.data; // only documents dated on or after `sinceDate`
}
Because the row id and url are stable, you can dedupe by med_ id: a document you already attached keeps the same id across runs, so re-seeing it is a no-op. Pair this with a document watch (see Monitoring) if you want to be pushed when a new office action lands instead of polling.

Putting it together

A single docket cycle for one mark:
// @docs-no-check
import { Signa } from "@signa-so/sdk";

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

async function docketCycle(tmId: string, lastRunDate: string) {
  // 1. lazy-fetch + poll to freshness
  const ready = await documentsWhenReady(tmId);
  if (ready.length === 0) return;

  // 2. only what is new since the last run
  const fresh = await signa.trademarks.documents(tmId, {
    document_kind: "office_action",
    official_date_gte: lastRunDate, // YYYY-MM-DD
  });

  // 3. stream and attach each new office action
  for (const oa of fresh.data) {
    const res = await fetch(oa.url);
    if (res.status === 502) continue; // budget; retry next cycle
    await saveAttachment(oa.id, Buffer.from(await res.arrayBuffer()));
  }
}

What you built

  • Documents pulled on demand, no TSDR poller of your own to run or throttle around.
  • A three-state read (synced, pending, unsupported) that never mistakes “not fetched yet” for “no documents”.
  • Office-action PDFs streamed straight into matters through one stable, dedupe-friendly URL.
  • A cheap forward-only refresh keyed on official date and med_ id.