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

# Build a Docketing System on Signa

> The cross-endpoint recipe for an IPMS or docketing product: lazy-fetch office documents, interpret sync state, poll to freshness, stream office-action PDFs through the media proxy, and stay current with forward-only refresh.

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](/api-reference/trademarks/records/documents) 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)

***

<Steps>
  <Step title="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.

    ```typescript theme={null}
    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.
  </Step>

  <Step title="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.

    ```typescript theme={null}
    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
      }
    }
    ```
  </Step>

  <Step title="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.

    ```typescript theme={null}
    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`.
  </Step>

  <Step title="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.

    ```typescript theme={null}
    // 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);
    }
    ```

    ```bash cURL theme={null}
    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.
  </Step>

  <Step title="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`.

    ```typescript theme={null}
    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.

    ```bash cURL theme={null}
    curl -L "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/media/med_019d2141-6ce9-771b-872e-bc8b20e49fcf" \
      -o office-action.pdf
    ```
  </Step>

  <Step title="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.

    ```typescript theme={null}
    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](/guides/monitoring/overview)) if you want to be pushed when a new office action lands instead of polling.
  </Step>
</Steps>

## Putting it together

A single docket cycle for one mark:

```typescript theme={null}
// @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.

## Related

* [Trademark Documents](/api-reference/trademarks/records/documents): full endpoint reference
* [Trademark Media](/api-reference/trademarks/trademark-media): image proxy for logos and drawings
* [Monitoring overview](/guides/monitoring/overview): get pushed on new filings instead of polling
* [Renewal Management](/guides/use-cases/renewal-management): the deadline side of docketing
