{
const idempotencyKey = randomUUID();
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(`https://api.signa.so/v1/organization/api-keys/${keyId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${process.env.SIGNA_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({ name }),
});
if (response.ok) return response.json();
if (response.status < 500) return response.json(); // permanent error
} catch {
if (attempt === maxRetries) throw new Error('Request failed after retries');
}
await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
}
}
```
The Signa TypeScript SDK sets the `Idempotency-Key` header automatically on all mutation requests. If you are using the SDK, you get idempotent retries without any extra code.
***
## Decision Tree
Use this to determine the right strategy for any failure:
```
Request failed
|
|--> Status 400/401/403/404/410/422?
| --> Permanent failure. Do not retry. Log and handle.
|
|--> Status 409 (idempotency conflict)?
| --> Same key, different body. Use a new Idempotency-Key.
|
|--> Status 429?
| --> Read Retry-After header.
| --> Wait and retry with backoff.
|
|--> Status 500/502/503/504?
| --> Retry with exponential backoff + jitter.
| --> If 3+ consecutive 5xx: trip circuit breaker.
|
|--> Network error / timeout?
--> Retry with backoff.
--> If persistent: trip circuit breaker.
```
# Search
Source: https://docs.signa.so/guides/search
Full-text, phonetic, and fuzzy trademark search
Signa's search supports full-text, phonetic, fuzzy, and prefix matching tuned specifically for trademark names.
## Choosing GET or POST
The trademarks endpoint is exposed under both `GET /v1/trademarks` and `POST /v1/trademarks`. Both methods call the same service and return the same response shape. Pick whichever fits your call site.
| Method | When to use |
| --------------------- | ---------------------------------------------------------------------------------- |
| `GET /v1/trademarks` | Quick queries, curl exploration, URL sharing, browser and wget use, simple filters |
| `POST /v1/trademarks` | Complex queries, long filter lists, aggregations, debugging |
A few advanced options live only on `POST` because they do not fit in a query string: `options.aggregations` and `options.aggregations_only`. Everything else (filters, strategies, pagination, `highlights`, `include_total`, `sort`) works identically on both.
For the full parameter reference, see [Search Trademarks](/api-reference/trademarks/search-trademarks). The sections below show concrete usage patterns you can copy into your own code.
## Search Strategies
By default, every search runs `exact` and `fuzzy` strategies simultaneously and merges the results. For comprehensive clearance searches, use all four:
| Strategy | What it does |
| ---------- | --------------------------------------------------------------------------------------- |
| `exact` | Case-insensitive exact keyword match: the entire mark text must match the query exactly |
| `phonetic` | Catches sound-alikes regardless of spelling (e.g., "SIGNA" / "CYGNA" / "SYNNA") |
| `fuzzy` | Tolerates typos and minor character differences (fuzziness is always AUTO internally) |
| `prefix` | Matches marks that start with the query text |
You can restrict which strategies are used by passing the `strategies` array (on POST) or a comma-separated `strategies=` parameter (on GET):
```bash cURL (GET) theme={null}
curl -G "https://api.signa.so/v1/trademarks" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "q=signa" \
--data-urlencode "strategies=exact,phonetic"
```
```bash cURL (POST) theme={null}
curl -X POST https://api.signa.so/v1/trademarks \
-H "Authorization: Bearer $SIGNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "signa",
"strategies": ["exact", "phonetic"]
}'
```
```typescript TypeScript theme={null}
import { Signa } from "@signa-so/sdk";
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
// GET-style: flat params via trademarks.list()
const results = await signa.trademarks.list({
q: "signa",
strategies: ["exact", "phonetic"],
});
// POST-style: same query, via trademarks.search()
const searched = await signa.trademarks.search({
query: "signa",
strategies: ["exact", "phonetic"],
});
```
If you omit `strategies`, `exact` and `fuzzy` are used. For comprehensive trademark clearance searches, pass all four strategies explicitly: `exact,phonetic,fuzzy,prefix`.
There is no user-controllable `fuzziness` parameter. Fuzzy matching always uses AUTO internally, which adjusts edit distance based on the length of the query term.
## Filtering
Narrow results with filters. On `POST`, filters are nested under a `filters` object. On `GET`, they are flat query parameters using comma-separated values for arrays and flat underscore operators for date ranges.
```bash cURL (GET) theme={null}
curl -G "https://api.signa.so/v1/trademarks" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "q=signa" \
--data-urlencode "offices=US,EM" \
--data-urlencode "nice_classes=9,42" \
--data-urlencode "status_stage=registered" \
--data-urlencode "filing_date_gte=2020-01-01"
```
```bash cURL (POST) theme={null}
curl -X POST https://api.signa.so/v1/trademarks \
-H "Authorization: Bearer $SIGNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "signa",
"filters": {
"offices": ["US", "EM"],
"nice_classes": [9, 42],
"status_stage": ["registered"],
"filing_date": { "gte": "2020-01-01" }
}
}'
```
```typescript TypeScript theme={null}
// GET-style: trademarks.list() takes filters as flat top-level params
const listed = await signa.trademarks.list({
q: "signa",
offices: ["US", "EM"],
nice_classes: [9, 42],
status_stage: ["registered"],
filing_date_gte: "2020-01-01",
});
// POST-style: trademarks.search() nests the same filters under `filters`
const searched = await signa.trademarks.search({
query: "signa",
filters: {
offices: ["US", "EM"],
nice_classes: [9, 42],
status_stage: ["registered"],
filing_date: { gte: "2020-01-01" },
},
});
```
Office codes are uppercase [WIPO ST.3](https://www.wipo.int/standards/en/st3.html) two-letter codes: `US` (USPTO), `EM` (EUIPO), `WO` (WIPO), `CA`, `SG`, `SE`, `CH`, `FR`, `AU`, `NO`. Legacy lowercase codes (`uspto`, `euipo`, ...) are accepted on requests permanently, and `EU` is accepted as an alias for `EM`. Responses always use the ST.3 form. Office display acronyms (e.g. `IPA`, `IGE-IPI`, `INPI`) are **not** an accepted input vocabulary — acronyms are not unique across offices; use ST.3 or legacy codes.
The two methods use different shapes. `signa.trademarks.list()` (GET) takes filters as flat top-level params, matching the query-string form. `signa.trademarks.search()` (POST) nests them under `filters`. Passing `offices`, `nice_classes`, `status_stage`, or other filter keys at the top level of a `POST` body returns a `validation_error` with `unrecognized_keys`. There is also no top-level `search_type` or `type` field: strategy selection happens through the `strategies` array.
Date filter operators:
| Operator | Meaning |
| -------- | -------------------------------------------- |
| `gte` | Greater than or equal (from date, inclusive) |
| `lt` | Less than (to date, exclusive) |
You can use both together for a range: `"filing_date": { "gte": "2020-01-01", "lt": "2025-01-01" }` (POST) or `filing_date_gte=2020-01-01&filing_date_lt=2025-01-01` (GET).
## Aggregations
Aggregations are `POST`-only because they do not map cleanly to query strings. Available aggregation fields:
| Field | Description |
| --------------------- | --------------------------------------------------------- |
| `office_code` | Counts by trademark office |
| `jurisdiction_code` | Counts by jurisdiction |
| `nice_classes` | Counts by Nice classification |
| `status_stage` | Counts by status stage |
| `filing_year` | Counts by filing year |
| `mark_feature_type` | Counts by mark type (word, figurative, etc.) |
| `mark_legal_category` | Counts by legal category (trademark, certification, etc.) |
| `filing_route` | Counts by filing route (direct\_national, madrid, etc.) |
| `right_kind` | Counts by right kind |
| `scope_kind` | Counts by scope kind |
```json theme={null}
{
"query": "signa",
"options": {
"aggregations": ["office_code", "status_stage", "nice_classes"]
}
}
```
To get only the counts with no result documents, add `"aggregations_only": true` to `options`.
## Response Scoring
Search results include a `relevance_score` field, which is a normalized score from 0 to 100 (higher is more relevant). Results are sorted by relevance score in descending order by default. If you specify an explicit `sort`, relevance scoring is disabled and `relevance_score` is `null`.
## Picking classes to filter on
If you do not know which Nice classes a query should be scoped to, send the
description to [`POST /v1/classifications/suggest`](/api-reference/reference/suggest-classifications)
and feed the returned `class_number` values into `nice_classes`. It takes free
text ("men's running shoes, cushioned sole") and returns ranked classes with a
confidence and a rationale, so you can show the user why a class was picked.
Ambiguous input comes back with `ambiguous: true` and a clarifying question
instead of a guess.
## Suggest
`GET /v1/trademarks/suggest` is an internal typeahead. It is not part of the published
OpenAPI spec and its shape can change without notice. Use `GET /v1/trademarks` for search.
For autocomplete and typeahead on internal keys:
```bash theme={null}
GET /v1/trademarks/suggest?q=sig&limit=10
```
# Testing Your Integration
Source: https://docs.signa.so/guides/testing
How to test your Signa integration safely.
Signa does not currently provide a dedicated sandbox environment. For integration testing, we recommend creating a **separate test organization** in the [dashboard](https://app.signa.so) with its own API key. You can then scope that key's usage, limits, and billing independently from your production organization.
## Recommended Workflow
1. **Create a test organization** from the dashboard.
2. **Issue a dedicated API key** for that organization and store it in your CI provider's secrets manager (e.g. `SIGNA_TEST_API_KEY`).
3. **Point integration tests at `https://api.signa.so`** using the test key. All keys use the format `sig_{48 hex chars}`, there is no separate base URL.
4. **Keep production keys out of CI**. Treat every API key as a credential regardless of which organization it belongs to.
## Integration Test Examples
### Basic: Verify Authentication
```typescript TypeScript (Vitest) theme={null}
import { describe, it, expect } from 'vitest';
const API_KEY = process.env.SIGNA_TEST_API_KEY!;
const BASE_URL = 'https://api.signa.so/v1';
describe('Signa API Authentication', () => {
it('should authenticate with a valid key', async () => {
const response = await fetch(`${BASE_URL}/offices`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.object).toBe('list');
});
it('should reject an invalid key', async () => {
const response = await fetch(`${BASE_URL}/offices`, {
headers: { Authorization: 'Bearer sig_invalid_key' },
});
expect(response.status).toBe(401);
});
});
```
### Search and Paginate
```typescript TypeScript (Vitest) theme={null}
import { Signa } from '@signa-so/sdk';
import { describe, it, expect } from 'vitest';
const signa = new Signa({ api_key: process.env.SIGNA_TEST_API_KEY! });
describe('Search and pagination', () => {
it('should search and paginate through results', async () => {
// First page
const page1 = await signa.trademarks.search({ query: 'health', limit: 10 });
expect(page1.data.length).toBe(10);
expect(page1.has_more).toBe(true);
// Second page (SignaList follows the cursor for you)
const page2 = await page1.getNextPage();
expect(page2.data.length).toBeGreaterThan(0);
// Verify no duplicates across pages
const page1Ids = new Set(page1.data.map((t) => t.id));
const page2Ids = page2.data.map((t) => t.id);
for (const id of page2Ids) {
expect(page1Ids.has(id)).toBe(false);
}
});
});
```
### Batch Fetch with Not-Found Handling
```typescript TypeScript (Vitest) theme={null}
import { Signa } from '@signa-so/sdk';
import { describe, it, expect } from 'vitest';
const signa = new Signa({ api_key: process.env.SIGNA_TEST_API_KEY! });
describe('Batch operations', () => {
it('should return found trademarks and list unmatched ids separately', async () => {
const response = await signa.trademarks.batch({
ids: ['tm_abc123', 'tm_nonexistent_id', 'tm_def456'],
});
// Found items come back as full trademark objects in data
expect(response.data.map((tm) => tm.id)).toEqual(['tm_abc123', 'tm_def456']);
// Ids that matched nothing are listed in not_found, not as error entries in data
expect(response.not_found).toEqual(['tm_nonexistent_id']);
});
});
```
***
## CI/CD Integration
### Environment Variables
Set these in your CI pipeline:
| Variable | Value | Description |
| -------------------- | --------- | ---------------------------------- |
| `SIGNA_TEST_API_KEY` | `sig_...` | API key for your test organization |
### GitHub Actions Example
```yaml theme={null}
name: Integration Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run test:integration
env:
SIGNA_TEST_API_KEY: ${{ secrets.SIGNA_TEST_API_KEY }}
```
Store API keys in your CI provider's secrets manager. Treat every key as a credential.
# Trademarks
Source: https://docs.signa.so/guides/trademarks
Trademark records, lifecycle stages, and Signa's 4-axis status taxonomy
Signa normalizes trademark data from every connected office into a consistent format (see [office coverage](/guides/data-freshness) for the current list). Every trademark record includes core identity fields, classifications, owners, events, and more.
## Key Fields
* **`mark_text`**: The word mark as filed (may be null for design marks)
* **`classifications`**: Array of goods/services classifications. Each item has `nice_class` (1-45) and `goods_services_text` describing what the mark covers
* **`office_code`**: Office where the mark is registered (e.g., `US`, `EM`, `WO`)
* **`filing_route`**: How the mark was filed: `direct_national`, `madrid_designation`, `direct_regional`, etc.
* **`renewal_due_date`**: Next renewal deadline (computed from jurisdiction rules)
## Status Taxonomy
Every trademark office uses its own status codes. The USPTO has 200+ numeric codes. EUIPO uses 18 uppercase strings. CIPO has 30+ text-based codes. Signa normalizes all of these into a unified 4-axis status taxonomy, and always retains the office's original status alongside it: `status.raw_code` and `status.raw_label` carry the source value on every record, and `?include=office_extensions` on [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark) returns the office-specific fields the standard schema does not model.
### The Four Axes
Every trademark status in Signa is described by four independent dimensions:
| Axis | Field | Purpose |
| -------------- | ------------------- | ------------------------------------------------- |
| **Primary** | `status.primary` | High-level classification (3 values + unknown) |
| **Stage** | `status.stage` | Where in the lifecycle the mark is (18 values) |
| **Reason** | `status.reason` | Why the mark reached its current state (9 values) |
| **Challenges** | `status.challenges` | Active legal proceedings (6 values, array) |
This multi-axis approach means you can filter with precision. A mark can be `active` (primary), `registered` (stage), with a null reason and `[opposition_pending]` in challenges: still active but facing a pending challenge.
### Primary Status
The broadest classification. Every mark falls into one of:
| Value | Meaning |
| ---------- | ---------------------------------------------------------- |
| `pending` | Application in progress, not yet registered |
| `active` | Registration is alive and in force |
| `inactive` | Registration is dead (cancelled, expired, abandoned, etc.) |
| `unknown` | Status could not be determined |
### Status Stage (18 values)
The specific lifecycle position:
**Pending stages:**
| Stage | Description |
| ---------------------- | ------------------------------------------------- |
| `filed` | Application received, not yet examined |
| `examining` | Under examination by the office |
| `pending_publication` | Approved, awaiting publication |
| `published` | Published in the official gazette |
| `opposition_period` | Publication period for opposition is open |
| `pending_opposition` | An opposition has been filed and is under review |
| `pending_cancellation` | A cancellation action has been filed |
| `pending_issuance` | Approved, awaiting formal issuance |
| `allowed` | Allowed (US-specific: Notice of Allowance issued) |
**Active stages:**
| Stage | Description |
| ------------ | ----------------------------- |
| `registered` | Active registration, in force |
**Inactive stages:**
| Stage | Description |
| ------------- | ------------------------------------------------- |
| `abandoned` | Applicant failed to respond or gave up |
| `withdrawn` | Applicant voluntarily withdrew |
| `surrendered` | Registrant voluntarily surrendered |
| `refused` | Office refused registration |
| `cancelled` | Registration cancelled (by office or third party) |
| `invalidated` | Registration declared invalid |
| `expired` | Registration expired (not renewed) |
**Fallback:**
| Stage | Description |
| --------- | ------------------- |
| `unknown` | Could not be mapped |
### Status Reason (9 values)
Why the mark reached an inactive state. Only populated for inactive marks:
| Value | Example Scenario |
| ------------- | --------------------------------------------------- |
| `refused` | Office examiner refused the application |
| `withdrawn` | Applicant withdrew the application |
| `abandoned` | Applicant failed to respond to an office action |
| `cancelled` | Registration cancelled by office or via proceeding |
| `invalidated` | Registration declared invalid after challenge |
| `expired` | Registrant did not renew |
| `surrendered` | Registrant voluntarily surrendered the registration |
| `revoked` | Registration revoked (EU-specific) |
| `other` | Reason does not fit standard categories |
### Challenge States (6 values)
Active legal proceedings. This is an **array** because a mark can face multiple simultaneous challenges:
| Value | Description |
| ---------------------- | ------------------------------------ |
| `opposition_pending` | An opposition has been filed |
| `cancellation_pending` | A cancellation proceeding is pending |
| `invalidation_pending` | An invalidation action is pending |
| `appeal_pending` | An appeal is pending |
| `court_pending` | A court proceeding is pending |
| `other_pending` | Another type of challenge is pending |
## Trademark Lifecycle
```mermaid theme={null}
stateDiagram-v2
[*] --> filed
filed --> examining
examining --> pending_publication
examining --> refused
examining --> abandoned
pending_publication --> published
published --> opposition_period
opposition_period --> pending_opposition
opposition_period --> pending_issuance
pending_opposition --> pending_issuance
pending_opposition --> refused
pending_issuance --> registered
published --> registered
examining --> allowed
allowed --> registered
allowed --> abandoned
registered --> expired
registered --> cancelled
registered --> surrendered
registered --> invalidated
filed --> withdrawn
examining --> withdrawn
published --> withdrawn
```
The diagram shows the most common transitions. Not all edges are shown: some offices allow additional paths (e.g., direct registration without publication in certain jurisdictions).
## Office-Specific Mappings
Each office's raw status codes are mapped to the canonical taxonomy above. Here are examples from major offices:
### USPTO (200+ codes)
| Raw Code | Raw Label | Stage |
| -------- | -------------------------------------- | --------------------- |
| 602 | Abandoned-Failure to Respond | `abandoned` |
| 620 | Backfile application added to database | `filed` |
| 660 | Approved for publication | `pending_publication` |
| 686 | Published for Opposition | `pending_opposition` |
| 688 | Notice of Allowance-Issued | `allowed` |
| 800 | Renewed Post Reg. | `registered` |
| 900 | Expired | `expired` |
### EUIPO (18 codes)
| Raw Code | Stage |
| ---------------------- | -------------------- |
| RECEIVED | `filed` |
| UNDER\_EXAMINATION | `examining` |
| APPLICATION\_PUBLISHED | `pending_opposition` |
| REGISTERED | `registered` |
| WITHDRAWN | `abandoned` |
| REFUSED | `abandoned` |
| CANCELLED | `cancelled` |
| EXPIRED | `expired` |
### WIPO
| Event Type | Stage |
| ---------- | ----------------------------------------------------------------- |
| BIRTH | `examining` (new designation enters examination) |
| PROCESSED | `registered` (protection granted) |
| DEATH | `cancelled`, `expired`, or `invalidated`, depending on the reason |
| PROLONG | `registered` (renewed) |
## Additional Status Fields
Beyond the four axes, the status object includes metadata:
| Field | Description |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `status.effective_date` | When the status last changed |
| `status.source` | How the status was determined: `explicit`, `event_derived`, `dispatch_derived`, `computed` |
| `status.raw_code` | The office's original status code |
| `status.raw_label` | The office's original status text |
## Filtering by Status
Use any combination of the four axes to filter trademarks:
**"Live" marks (TESS equivalent).** `status_primary` accepts a single value or a comma-separated list. Pass `active,pending` to match every mark that is either registered or still in prosecution: equivalent to the USPTO TESS "Live" filter.
```bash cURL theme={null}
# All live marks (active OR pending): TESS-style "Live" filter
curl -s "https://api.signa.so/v1/trademarks?status_primary=active,pending" \
-H "Authorization: Bearer sig_YOUR_KEY"
# All active, registered marks
curl -s "https://api.signa.so/v1/trademarks?status_primary=active&status_stage=registered" \
-H "Authorization: Bearer sig_YOUR_KEY"
# All marks with pending challenges
curl -s "https://api.signa.so/v1/trademarks?challenge_states=opposition_pending" \
-H "Authorization: Bearer sig_YOUR_KEY"
# All abandoned or withdrawn marks
curl -s "https://api.signa.so/v1/trademarks?status_stage=abandoned,withdrawn" \
-H "Authorization: Bearer sig_YOUR_KEY"
```
```typescript TypeScript theme={null}
// Live marks: active OR pending
const live = await signa.trademarks.list({
status_primary: ["active", "pending"],
});
// Active marks with pending opposition
const marks = await signa.trademarks.list({
status_primary: "active",
challenge_states: "opposition_pending",
});
// All inactive marks with reason
const inactive = await signa.trademarks.list({
status_primary: "inactive",
status_reason: "expired,cancelled",
});
```
## Search Aggregations
The search endpoint returns aggregation counts for `status_stage`:
```bash cURL theme={null}
curl -X POST https://api.signa.so/v1/trademarks \
-H "Authorization: Bearer sig_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "SIGNA",
"options": { "aggregations": ["status_stage"] }
}'
```
```typescript TypeScript theme={null}
const results = await signa.trademarks.search({
query: "SIGNA",
options: { aggregations: ["status_stage"] },
});
console.log(results.aggregations.status_stage);
// { registered: 4521, examining: 892, abandoned: 234 }
```
## Practical Scenarios
Filter for marks with a cancellation challenge:
```bash theme={null}
GET /v1/trademarks?status_primary=active&challenge_states=cancellation_pending
```
Use `status_stage` directly: `abandoned` means the applicant failed to act, while `refused` means the office denied the application. The `status_reason` field provides additional context.
If an unmapped raw code is encountered, `status_stage` is set to `unknown` and `raw_code`/`raw_label` are populated so you can still see the original office data.
Yes. A mark can be `registered` (stage) with `active` primary status while simultaneously having `cancellation_pending` or `opposition_pending` in the challenges array. The mark remains registered until the challenge is resolved.
# Build a Docketing System on Signa
Source: https://docs.signa.so/guides/use-cases/build-a-docketing-system
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)
***
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.
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
}
}
```
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`.
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.
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
```
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. Poll `/v1/trademarks/{id}/documents` on each docket run with `official_date_gte` set to the last successful run date for a forward-only refresh.
## 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
* [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
# M&A Due Diligence
Source: https://docs.signa.so/guides/use-cases/mna-due-diligence
Evaluate a target company's trademark portfolio during an acquisition. Map the owner hierarchy via GLEIF, enumerate all marks across subsidiaries, check proceedings and litigation risk, assess portfolio health, and verify the ownership transfer after close.
You are an IP counsel advising on the acquisition of **Helios Consumer Brands Inc.**, a mid-market consumer goods company. Before the deal closes, you need a complete picture of their trademark portfolio: how many marks they hold, where, in what condition, and what risks exist (pending oppositions, upcoming deadlines, lapsed registrations).
This guide shows how to perform trademark due diligence, and then verify the ownership transfer once the deal closes, using the Signa API.
## Prerequisites
* A Signa API key with `trademarks:read` scope
* The target company name or known identifiers (ticker symbol, LEI, owner ID)
***
Search for the target by name, ticker symbol, or LEI (Legal Entity Identifier). The ticker and LEI filters join through Signa's public companies data (SEC + GLEIF).
```bash cURL theme={null}
# Search by name
curl "https://api.signa.so/v1/owners?q=Helios+Consumer+Brands&limit=5" \
-H "Authorization: Bearer $SIGNA_API_KEY"
# Or search by ticker if publicly traded
curl "https://api.signa.so/v1/owners?ticker=HLCS&limit=5" \
-H "Authorization: Bearer $SIGNA_API_KEY"
# Or search by LEI
curl "https://api.signa.so/v1/owners?lei=5493001KJTIIGC8Y1R12&limit=5" \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
import Signa from "@signa-so/sdk";
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
// Try by name first
const results = await signa.owners.list({ q: "Helios Consumer Brands", limit: 5 });
// Or by ticker
// const results = await signa.owners.list({ ticker: "HLCS", limit: 5 });
const target = results.data[0];
console.log(`Target: ${target.name} (${target.id})`);
console.log(`Trademark count: ${target.trademark_count}`);
```
**Expected output:**
```json theme={null}
{
"id": "own_helios01",
"name": "Helios Consumer Brands Inc.",
"canonical_name": "HELIOS CONSUMER BRANDS INC",
"country_code": "US",
"entity_type": "corporation",
"trademark_count": 234
}
```
Acquisition targets often hold trademarks through subsidiaries. Use the GLEIF corporate relationship data to identify the full ownership tree.
```bash cURL theme={null}
# Get the target's full profile including public company data
curl https://api.signa.so/v1/owners/own_helios01 \
-H "Authorization: Bearer $SIGNA_API_KEY"
# Get corporate relationships (parent/child companies)
curl https://api.signa.so/v1/owners/own_helios01/related \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
// Get full owner detail including public company matches
const ownerDetail = await signa.owners.retrieve("own_helios01");
if (ownerDetail.companies) {
for (const pc of ownerDetail.companies) {
console.log(`Public company: ${pc.legal_name} (${pc.source}: ${pc.source_id})`);
if (pc.ticker) console.log(` Ticker: ${pc.ticker} (${pc.exchange})`);
}
}
// Get corporate hierarchy
const related = await (await signa.owners.related("own_helios01")).toArray();
console.log(`\nCorporate relationships: ${related.length}`);
for (const rel of related) {
console.log(
` ${rel.direction === "child" ? "Subsidiary" : "Parent"}: ${rel.name} (${rel.country_code})`,
);
console.log(` Relationship: ${rel.relationship_type}`);
console.log(` Ownership: ${rel.ownership_pct ? rel.ownership_pct + "%" : "unknown"}`);
console.log(` Owner ID: ${rel.related_owner_id}`);
}
```
**Expected output:**
```json theme={null}
{
"object": "list",
"data": [
{
"related_owner_id": "own_hel_eu01",
"name": "Helios Brands Europe GmbH",
"country_code": "DE",
"relationship_type": "IS_DIRECTLY_CONSOLIDATED_BY",
"direction": "child",
"ownership_pct": 100.0
},
{
"related_owner_id": "own_hel_asia01",
"name": "Helios Asia Pacific Pte Ltd",
"country_code": "SG",
"relationship_type": "IS_DIRECTLY_CONSOLIDATED_BY",
"direction": "child",
"ownership_pct": 100.0
}
]
}
```
GLEIF relationships use standardized vocabulary. `IS_DIRECTLY_CONSOLIDATED_BY` means the child entity is directly owned by the parent. Use `direction` to determine which side of the relationship the target sits on.
Collect trademarks from the target and all subsidiaries. This gives you the full scope of what is being acquired.
```bash cURL theme={null}
# Get marks from the parent
curl "https://api.signa.so/v1/owners/own_helios01/trademarks?limit=100" \
-H "Authorization: Bearer $SIGNA_API_KEY"
# Get marks from each subsidiary
curl "https://api.signa.so/v1/owners/own_hel_eu01/trademarks?limit=100" \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
// Collect all owner IDs (parent + subsidiaries)
const familyOwnerIds = [
"own_helios01",
...related.filter((r) => r.direction === "child" && r.related_owner_id).map((r) => r.related_owner_id),
];
console.log(`Corporate family: ${familyOwnerIds.length} entities`);
// Enumerate marks across the family. SignaList follows cursors automatically,
// so toArray() collects every page without a manual cursor loop.
const allMarks: Awaited>["data"] = [];
for (const ownerId of familyOwnerIds) {
const marks = await (await signa.owners.trademarks(ownerId, { limit: 100 })).toArray();
allMarks.push(...marks);
}
console.log(`Total marks across family: ${allMarks.length}`);
```
Analyze the collected marks to build a health scorecard: status distribution, jurisdiction coverage, upcoming deadlines, and risk indicators.
```typescript TypeScript theme={null}
// Status distribution
const statusDist: Record = {};
const jurisdictionDist: Record = {};
const classDist: Record = {};
for (const tm of allMarks) {
statusDist[tm.status.stage] = (statusDist[tm.status.stage] || 0) + 1;
jurisdictionDist[tm.jurisdiction_code] = (jurisdictionDist[tm.jurisdiction_code] || 0) + 1;
for (const c of tm.classifications) {
classDist[c.nice_class] = (classDist[c.nice_class] || 0) + 1;
}
}
const registered = statusDist["registered"] || 0;
const abandoned = statusDist["abandoned"] || 0;
const expired = statusDist["expired"] || 0;
const pendingCount = (statusDist["filed"] || 0) + (statusDist["examining"] || 0);
console.log("\n=== Portfolio Health Scorecard ===");
console.log(`Total marks: ${allMarks.length}`);
console.log(`Registered: ${registered} (${((registered / allMarks.length) * 100).toFixed(0)}%)`);
console.log(`Pending: ${pendingCount}`);
console.log(`Abandoned/Expired: ${abandoned + expired}`);
console.log(`Jurisdictions: ${Object.keys(jurisdictionDist).length}`);
console.log(`Nice classes covered: ${Object.keys(classDist).length}`);
console.log("\nStatus breakdown:", statusDist);
console.log("Top jurisdictions:", Object.entries(jurisdictionDist).sort((a, b) => b[1] - a[1]).slice(0, 5));
console.log("Top classes:", Object.entries(classDist).sort((a, b) => b[1] - a[1]).slice(0, 10));
```
**Expected output:**
```
=== Portfolio Health Scorecard ===
Total marks: 312
Registered: 241 (77%)
Pending: 38
Abandoned/Expired: 33
Jurisdictions: 14
Nice classes covered: 18
Status breakdown: { registered: 241, examining: 28, filed: 10, abandoned: 18, expired: 15 }
Top jurisdictions: [["us", 124], ["eu", 68], ["cn", 34], ["gb", 28], ["de", 22]]
Top classes: [["3", 89], ["5", 72], ["35", 56], ["21", 45], ["29", 38]]
```
Identify any pending oppositions, cancellations, or other proceedings that could affect the portfolio's value.
```bash cURL theme={null}
# Check proceedings where the target is a party (as respondent or opponent)
curl "https://api.signa.so/v1/proceedings?party_owner_id=own_helios01&limit=50" \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
// Check proceedings for each entity in the family
const allProceedings: Awaited>["data"] = [];
for (const ownerId of familyOwnerIds) {
const proceedings = await signa.proceedings.list({
party_owner_id: ownerId,
limit: 50,
});
allProceedings.push(...proceedings.data);
}
const pendingProceedings = allProceedings.filter((p) => p.status === "pending");
const decided = allProceedings.filter((p) => p.status.startsWith("decided"));
console.log(`\n=== Proceedings ===`);
console.log(`Total: ${allProceedings.length}`);
console.log(`Pending (active risk): ${pendingProceedings.length}`);
console.log(`Decided: ${decided.length}`);
for (const p of pendingProceedings) {
console.log(`\n [PENDING] ${p.proceeding_type} - ${p.proceeding_number}`);
console.log(` Filed: ${p.filed_date}`);
console.log(` Contested classes: ${p.contested_classes.join(", ")}`);
for (const party of p.parties) {
console.log(` ${party.role}: ${party.name}`);
}
}
```
**Expected output:**
```json theme={null}
{
"object": "list",
"data": [
{
"id": "prc_hel001",
"proceeding_type": "opposition",
"proceeding_number": "91278456",
"status": "pending",
"filed_date": "2026-01-10",
"parties": [
{ "owner_id": "own_other99", "name": "GreenGlow Naturals LLC", "role": "opponent" },
{ "owner_id": "own_helios01", "name": "Helios Consumer Brands Inc.", "role": "respondent" }
],
"contested_classes": [3, 5]
}
]
}
```
Pending proceedings are a material risk factor in M\&A. An ongoing opposition could result in loss of rights to a key brand. Make sure to flag these for the deal team and factor potential outcomes into the valuation.
Compile all findings into a structured report suitable for the deal team.
```typescript TypeScript theme={null}
const diligenceReport = {
target: {
name: ownerDetail.name,
ownerId: ownerDetail.id,
country: ownerDetail.country_code,
publicCompanies: ownerDetail.companies ?? [],
},
corporateFamily: {
entityCount: familyOwnerIds.length,
subsidiaries: related
.filter((r) => r.direction === "child")
.map((r) => ({
name: r.name,
country: r.country_code,
ownerId: r.related_owner_id,
ownershipPct: r.ownership_pct,
})),
},
portfolio: {
totalMarks: allMarks.length,
registered,
pending: pendingCount,
abandonedOrExpired: abandoned + expired,
grantRate: registered / allMarks.length,
jurisdictions: Object.keys(jurisdictionDist).length,
niceClasses: Object.keys(classDist).length,
statusBreakdown: statusDist,
},
proceedings: {
total: allProceedings.length,
pending: pendingProceedings.length,
pendingDetails: pendingProceedings.map((p) => ({
type: p.proceeding_type,
number: p.proceeding_number,
filedDate: p.filed_date,
contestedClasses: p.contested_classes,
opponent: p.parties.find((party) => party.role === "opponent")?.name,
})),
},
riskAssessment: {
overallRisk: pendingProceedings.length > 3 ? "HIGH" : pendingProceedings.length > 0 ? "MEDIUM" : "LOW",
flags: [
...(pendingProceedings.length > 0 ? [`${pendingProceedings.length} pending proceedings`] : []),
...(abandoned + expired > allMarks.length * 0.15
? ["High abandonment/expiry rate (>15%)"]
: []),
],
},
generatedAt: new Date().toISOString(),
};
console.log(JSON.stringify(diligenceReport, null, 2));
```
***
## Key risk indicators to flag
| Risk Factor | How to detect | Severity |
| ------------------------------------------ | ---------------------------------------------------------------------- | -------- |
| Pending oppositions/cancellations | `GET /v1/proceedings?party_owner_id=...&status=pending` | High |
| High abandonment rate (>15%) | Owner stats `abandonment_rate` | Medium |
| Marks approaching expiry without renewal | `deadlines[]` on `GET /v1/trademarks/{id}` across the target portfolio | High |
| Thin jurisdiction coverage | Owner stats `jurisdiction_count` vs business footprint | Medium |
| No Madrid filings for international brands | Filter `filing_route=direct_national` only | Low |
| Missing use declarations (US) | Deadline type `declaration_of_use` past its window | High |
***
## After the deal closes: verify the ownership transfer
Once the acquisition closes and you file the assignment paperwork with each office, use the same `allMarkIds` you already collected during diligence to confirm the transfer landed correctly. There is no separate "transfer" endpoint. An assignment shows up as an ordinary owner change on the trademark record once the office processes and Signa ingests it.
Batch-fetch the acquired marks (chunked to the 100-item limit) and check which ones now list the acquiring entity as owner.
```typescript TypeScript theme={null}
const allMarkIds = allMarks.map((tm) => tm.id);
const buyerOwnerId = "own_apex01";
function chunk(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;
}
let transferred = 0;
for (const ids of chunk(allMarkIds, 100)) {
const page = await signa.trademarks.batch({ ids });
for (const tm of page.data) {
if (tm.owners.some((o) => o.id === buyerOwnerId)) transferred++;
}
if (page.not_found.length > 0) {
console.warn("Not found:", page.not_found);
}
}
console.log(`Transferred: ${transferred} / ${allMarkIds.length}`);
```
Run this on a schedule (weekly is typical for a multi-month recording project) and share the ratio with the deal team. There is no per-item error status in the batch response, an ID either resolves into `data` or lands in `not_found`; a mark that no longer exists under the seller's ID (a rare renumbering case) is the only thing that would show up there.
For any individual mark, [Trademark Events](/api-reference/trademarks/trademark-events) gives you the office's own timeline, so you can see when the assignment or name change was recorded. Filter to the ownership event types rather than walking the whole timeline.
```typescript TypeScript theme={null}
const events = await signa.trademarks.events("tm_hel001", {
event_type: "assignment,name_change",
limit: 5,
});
for (const event of events.data) {
console.log(`${event.event_date} | ${event.event_type} | ${event.description}`);
}
```
***
## What's next
Make sure no deadlines are missed while transfer recordings are in progress.
Monitor any proceedings that transfer with the acquired portfolio.
# Opposition & Cancellation Tracking
Source: https://docs.signa.so/guides/use-cases/opposition-tracking
Monitor TTAB proceedings, track opposition and cancellation deadlines, detect proceeding status changes, and build a litigation dashboard across your clients' marks.
You are an IP litigation associate responsible for monitoring 30 active TTAB (Trademark Trial and Appeal Board) proceedings for your firm's clients. Some of your clients are opponents; others are respondents. You need to track proceeding status changes, flag new oppositions filed against your clients' marks, and maintain an overview of all active cases.
This guide shows how to build an opposition monitoring system using the Signa API.
## Prerequisites
* A Signa API key with `trademarks:read` scope
* Client owner IDs or a list of marks to monitor
***
Start by querying proceedings where your client appears as a party, either as the opponent (your client filed the opposition) or the respondent (someone opposed your client's mark).
```bash cURL theme={null}
# Find proceedings where your client is the respondent (someone opposed their mark)
curl "https://api.signa.so/v1/proceedings?party_owner_id=own_client01&party_role=respondent&status=pending&limit=50" \
-H "Authorization: Bearer $SIGNA_API_KEY"
# Find proceedings where your client is the opponent (they filed the opposition)
curl "https://api.signa.so/v1/proceedings?party_owner_id=own_client01&party_role=opponent&status=pending&limit=50" \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
import Signa from "@signa-so/sdk";
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
const clientOwnerIds = ["own_client01", "own_client02", "own_client03"];
// Collect all active proceedings for all clients
const allProceedings: Array>["data"][number] & { clientRole: string; clientOwnerId: string }> = [];
for (const ownerId of clientOwnerIds) {
const asRespondent = await signa.proceedings.list({
party_owner_id: ownerId,
party_role: "respondent",
status: "pending",
limit: 50,
});
const asOpponent = await signa.proceedings.list({
party_owner_id: ownerId,
party_role: "opponent",
status: "pending",
limit: 50,
});
for (const p of asRespondent.data) {
allProceedings.push({ ...p, clientRole: "respondent", clientOwnerId: ownerId });
}
for (const p of asOpponent.data) {
allProceedings.push({ ...p, clientRole: "opponent", clientOwnerId: ownerId });
}
}
console.log(`Active proceedings across all clients: ${allProceedings.length}`);
```
**Expected output:**
```json theme={null}
{
"object": "list",
"data": [
{
"id": "prc_op001",
"proceeding_type": "opposition",
"proceeding_number": "91278456",
"status": "pending",
"office_code": "US",
"filed_date": "2026-01-10",
"decision_date": null,
"parties": [
{ "owner_id": "own_other99", "name": "Apex Global Corp", "role": "opponent" },
{ "owner_id": "own_client01", "name": "Meridian Labs Inc.", "role": "respondent" }
],
"contested_classes": [9, 42],
"trademark_id": "tm_merid01"
}
]
}
```
For each proceeding, fetch the full trademark detail to understand what is at stake.
```bash cURL theme={null}
# Get the contested trademark
curl https://api.signa.so/v1/trademarks/tm_merid01 \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
// Collect unique trademark IDs from all proceedings
const trademarkIds = [...new Set(allProceedings.map((p) => p.trademark_id).filter(Boolean))];
// Batch fetch all contested marks
const contestedMarks = await signa.trademarks.batch({ ids: trademarkIds as string[] });
for (const tm of contestedMarks.data) {
console.log(`\n${tm.mark_text} (${tm.id})`);
console.log(` Office: ${tm.office_code}`);
console.log(` Status: ${tm.status.stage}`);
console.log(` Classes: ${tm.classifications.map((c) => c.nice_class).join(", ")}`);
console.log(` Owner: ${tm.owners[0]?.name}`);
console.log(` Filing date: ${tm.filing_date}`);
}
if (contestedMarks.not_found.length > 0) {
console.log("Not found:", contestedMarks.not_found);
}
```
Pull each client's pending and published marks, the stages most vulnerable to opposition, so you know which applications to watch most closely.
```bash cURL theme={null}
curl -G "https://api.signa.so/v1/owners/own_client01/trademarks" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "status_stage=examining,published,opposition_period,pending_opposition" \
--data-urlencode "limit=100"
```
```typescript TypeScript theme={null}
for (const ownerId of clientOwnerIds) {
const vulnerableMarks = await signa.owners.trademarks(ownerId, {
status_stage: ["examining", "published", "opposition_period", "pending_opposition"],
limit: 100,
});
console.log(`${ownerId}: ${vulnerableMarks.data.length} marks to monitor`);
}
```
Focus on marks in the `published`, `opposition_period`, and `examining` stages, these are the ones most vulnerable to new proceedings. Registered marks can still face cancellation petitions but the risk is lower.
Review the event timeline of a contested mark to understand how the opposition fits into the prosecution history. Events are always returned newest-first by `event_date`, there is no `sort` parameter to set.
```bash cURL theme={null}
curl "https://api.signa.so/v1/trademarks/tm_merid01/events?limit=20" \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
const events = await signa.trademarks.events("tm_merid01", { limit: 20 });
console.log("\nProsecution timeline:");
for (const event of events.data) {
console.log(
` ${event.event_date} | ${event.event_type.padEnd(20)} | ${event.description}`,
);
if (event.status_after_event) {
console.log(` ${"".padEnd(12)} Status -> ${event.status_after_event}`);
}
}
```
**Expected output:**
```
Prosecution timeline:
2026-01-10 | opposition_filed | OPPOSITION FILED
Status -> pending_opposition
2025-11-15 | publication | PUBLISHED FOR OPPOSITION
Status -> published
2025-09-20 | examination_start | APPROVED FOR PUBLICATION
2025-06-01 | filing | NEW APPLICATION FILED
Status -> filed
```
Query proceedings by type and status to build different views of your case docket.
```bash cURL theme={null}
# All opposition proceedings (any party, any status)
curl "https://api.signa.so/v1/proceedings?party_owner_id=own_client01&proceeding_type=opposition&limit=50" \
-H "Authorization: Bearer $SIGNA_API_KEY"
# Cancellation proceedings only
curl "https://api.signa.so/v1/proceedings?party_owner_id=own_client01&proceeding_type=cancellation&limit=50" \
-H "Authorization: Bearer $SIGNA_API_KEY"
# Decided proceedings (to review outcomes)
curl "https://api.signa.so/v1/proceedings?party_owner_id=own_client01&status=decided_granted&limit=50" \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
// Build a complete litigation dashboard for one client
const oppositions = await signa.proceedings.list({
party_owner_id: "own_client01",
proceeding_type: "opposition",
status: "pending",
limit: 50,
});
const cancellations = await signa.proceedings.list({
party_owner_id: "own_client01",
proceeding_type: "cancellation",
status: "pending",
limit: 50,
});
const decidedThisYear = await signa.proceedings.list({
party_owner_id: "own_client01",
decision_date_gte: "2026-01-01",
limit: 50,
});
const settled = await signa.proceedings.list({
party_owner_id: "own_client01",
status: "settled",
limit: 50,
});
console.log("\n=== Litigation Dashboard ===");
console.log(`Pending oppositions: ${oppositions.data.length}`);
console.log(`Pending cancellations: ${cancellations.data.length}`);
console.log(`Decided this year: ${decidedThisYear.data.length}`);
console.log(`Settled: ${settled.data.length}`);
```
Check [Trademark Events](/api-reference/trademarks/trademark-events) on each contested mark to catch status transitions. Data updates land daily or weekly depending on the office (see [Data Freshness & Coverage](/guides/data-freshness)), so checking once a day is enough, there is no benefit to polling more often than the underlying data changes. Compare the latest event's `status_after_event` against the value you saw on the previous check.
```typescript TypeScript theme={null}
const watchedMarkIds = new Set(allProceedings.map((p) => p.trademark_id).filter(Boolean) as string[]);
// Persist mark_id -> last-seen status between runs
const lastSeenStatus = await loadLastSeenStatus(); // from your own store
for (const markId of watchedMarkIds) {
const events = await signa.trademarks.events(markId, { limit: 1 });
const latest = events.data[0];
if (!latest?.status_after_event) continue;
const previous = lastSeenStatus.get(markId);
if (previous && previous !== latest.status_after_event) {
console.log(`[ALERT] ${markId}: ${previous} -> ${latest.status_after_event} (${latest.event_date})`);
}
lastSeenStatus.set(markId, latest.status_after_event);
}
await saveLastSeenStatus(lastSeenStatus);
```
A status change from `pending_opposition` to `registered` means the opposition was resolved in your client's favor, the mark proceeded to registration. A change to `abandoned` or `refused` would indicate the opposite outcome.
***
## Proceeding status reference
| Status | Meaning |
| ------------------ | ---------------------------------------------------- |
| `pending` | Active, awaiting decision |
| `decided_granted` | Decided in favor of the petitioner/opponent |
| `decided_rejected` | Decided in favor of the respondent |
| `withdrawn` | Petitioner/opponent withdrew the proceeding |
| `settled` | Parties reached a settlement |
| `suspended` | Proceeding paused (often pending related litigation) |
| `partial` | Mixed outcome, some grounds sustained, others denied |
| `other` | Catch-all for unusual outcomes |
***
## Monitoring going forward
Checking history daily works, but a [watch](/guides/monitoring/watches) with `trigger_events: ["trademark.status_changed"]` scoped to your clients' marks pushes an alert the moment the office records a status change, no daily check needed. For marks with a live opposition window, see [Opposition windows](/guides/monitoring/opposition-windows) for the `opposition_window_status` and `must_act_by` fields Signa attaches to every alert automatically. Fall back to the daily events check above only if you are not ready to set up watches yet.
***
## What's next
Run clearance searches to avoid triggering oppositions before you file.
Track competitor filing patterns to anticipate potential opposition actions.
Ensure contested marks do not lapse during proceedings by tracking their deadlines.
# Track any public company's trademarks
Source: https://docs.signa.so/guides/use-cases/public-company-trademarks
Go from a stock ticker to a public company's worldwide trademark portfolio, including its subsidiaries, then stand up a watch that fires on every new filing.
You cover a basket of listed companies and you want their trademarks keyed the way you already think about them: by ticker. Give Signa a ticker like `NKE` and it resolves to the underlying entity, hands you the company's worldwide portfolio (subsidiaries included), and lets you subscribe to every future filing.
This works because Signa links public-company facts onto [entities](/guides/entities): SEC tickers on the members that matched, plus a listing decoration that flows a listed parent's ticker down to its subsidiaries through the GLEIF corporate family. So a ticker is not just the parent's own marks, it is the whole listed group.
## Prerequisites
* A Signa API key with `trademarks:read` scope
* A stock ticker (uppercased, for example `NKE` or `AAPL`)
***
Filter [`GET /v1/entities`](/api-reference/parties/search-entities) by `ticker`. The match is subsidiary-inclusive, so `ticker=NKE` returns Nike **and** its subsidiary entities. Sort by `-trademark_count` to put the parent first.
```bash cURL theme={null}
curl -G "https://api.signa.so/v1/entities" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "ticker=NKE" \
--data-urlencode "sort=-trademark_count" \
--data-urlencode "limit=20"
```
```typescript TypeScript theme={null}
import { Signa } from "@signa-so/sdk";
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
const entities = await signa.entities.list({
ticker: "NKE",
sort: "-trademark_count",
limit: 20,
});
for await (const entity of entities) {
console.log(entity.id, entity.name, entity.ticker, entity.trademark_count);
}
```
The first row is the listed parent. Keep its `id` (an `ent_*`) for the next step.
Fetch the entity with [`GET /v1/entities/{id}`](/api-reference/parties/retrieve-entity). When an entity is listed or a subsidiary of a listed company, the response carries a `listing` block. The block is **omitted entirely** when there is no listing, so its absence means "no listing or unknown," never confirmed-private.
```json Listing block theme={null}
{
"id": "ent_R3jK9mN2",
"name": "Converse Inc.",
"ticker": "NKE",
"tickers": ["NKE"],
"listing": {
"status": "subsidiary_of_listed",
"ticker": "NKE",
"exch_code": "XNYS",
"lei": "INR2EJN1ERAN0W5ZP974",
"source": "inherited",
"listed_ancestor": {
"id": "ent_9Km2nPq4",
"name": "Nike, Inc.",
"ticker": "NKE"
}
}
}
```
Read it as two axes:
* **`status`**: `listed` (the entity is itself publicly listed) or `subsidiary_of_listed` (it inherits a ticker from a listed ancestor).
* **`source`**: `direct` (the ticker is the entity's own) or `inherited` (the ticker comes from the nearest listed ancestor, surfaced in `listed_ancestor`). `source` is just `status` restated: `listed` is always `direct`, `subsidiary_of_listed` is always `inherited`.
A directly-listed parent (`status: "listed"`, `source: "direct"`) has `listed_ancestor: null`. For a subsidiary, `listed_ancestor` points at the listed parent when it resolves to a live entity in our system (it is `null` when the ancestor exists only in the GLEIF graph).
Call [`GET /v1/entities/{id}/trademarks`](/api-reference/parties/entity-trademarks) on the listed parent with `include_family=true`. That expands the query to the entity and every descendant in its GLEIF family tree, so one call returns the whole listed group's marks across all 21 offices. Drop `include_family` (or set it `false`) to get just that one entity.
```bash cURL theme={null}
curl -G "https://api.signa.so/v1/entities/ent_9Km2nPq4/trademarks" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "include_family=true" \
--data-urlencode "status_primary=active" \
--data-urlencode "sort=-filing_date" \
--data-urlencode "limit=20"
```
```typescript TypeScript theme={null}
const page = await signa.entities.trademarks("ent_9Km2nPq4", {
include_family: true,
status_primary: "active",
sort: "-filing_date",
limit: 20,
});
```
The full [Search Trademarks](/api-reference/trademarks/search-trademarks) filter set applies here (status, Nice classes, jurisdiction, date ranges), so you can narrow the group's portfolio to, say, active class 25 marks filed this year. A family graph larger than the walk bounds returns `422` with `error.reason` `family_graph_too_large`.
To be notified of future filings under the ticker, add `ownerTicker` to a watch's `filters`. Watch filters share the same public-company/listing fields as search, so `ownerTicker` is subsidiary-inclusive too: `ownerTicker: "NKE"` fires on Nike's own filings and its subsidiaries' filings alike.
Every watch needs a base scope from its `watch_type` (see [Monitoring watches](/guides/monitoring/watches)); `ownerTicker` is an additional filter layered on top. A common preset scopes a `class` watch to the Nice classes you care about and narrows it to the ticker, so you only hear about the group's filings in your core classes:
```bash cURL theme={null}
curl -X POST "https://api.signa.so/v1/watches" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: watch-nke-class25-001" \
-d '{
"name": "Nike group, class 25 filings",
"watch_type": "class",
"query": {
"version": "v2",
"filters": { "niceClasses": [25], "ownerTicker": "NKE" },
"trigger_events": ["trademark.created"]
}
}'
```
Prefer a broader net? Swap `ownerTicker` for `"ownerPubliclyTraded": true` to watch new filings by **any** owner with an active listing association (a confirmed SEC ticker, or a resolved entity that is listed or a subsidiary of one), still scoped by your chosen classes. Preview the volume with [`POST /v1/watches/preview`](/api-reference/monitoring/watches/preview) before you commit, and wire delivery through a [webhook](/guides/monitoring/webhooks).
## Coverage caveats
Public-company linkage is confirmed-positive only, and this shapes how you read a result:
* **Absent is not private.** A missing ticker, `publicly_traded: false`, or an omitted `listing` block means no confirmed match was found, not that the company is confirmed private or unlisted. Never infer "private" from absence.
* **Coverage is strongest for large caps.** Linkage leans on SEC tickers and GLEIF LEI reporting, so widely-held, LEI-reporting issuers resolve best. Thinly-covered small caps and non-reporting subsidiaries are likelier to be missing an edge.
* **Subsidiary reach depends on GLEIF Level 2.** The inherited-ticker walk follows the GLEIF corporate family, which covers LEI-reporting companies only. An absent parent/subsidiary edge does not imply the absence of a corporate relationship.
Coverage is measured with the ENG-118 eval harness; the production number is pending rollout and will be cited here once it lands.
## Related
* [Entities & owners](/guides/entities): how Signa resolves and links companies
* [Entity Family](/api-reference/parties/entity-family): the GLEIF parent and subsidiaries behind `include_family`
* [Monitoring watches](/guides/monitoring/watches): the full watch filter and trigger reference
# Renewal Management
Source: https://docs.signa.so/guides/use-cases/renewal-management
Never miss a trademark deadline. Query upcoming renewals and declarations, understand grace periods, triage by urgency, and handle US-specific Section 8/15 declarations.
You are a paralegal at a mid-sized IP firm responsible for docketing renewal deadlines across 200+ client marks in 12 jurisdictions. Missing a deadline means losing rights, and potentially a malpractice claim. You need a system that surfaces every upcoming deadline with enough lead time to prepare filings.
This guide walks through building a renewal management workflow with the Signa API.
## Prerequisites
* A Signa API key with `trademarks:read` scope
* A list of trademark IDs (or office-native identifiers) for the client marks you docket
***
Not every jurisdiction has the same deadline structure. The Signa API computes deadlines based on jurisdiction-specific rule sets (rules defined for 25 jurisdictions, 26 rule sets because the US has separate domestic and Madrid sets, independent of which offices currently have live trademark record data, see the coverage note below). Start by reviewing the rules for your key jurisdictions.
```bash cURL theme={null}
# Get all deadline rules for the US
curl "https://api.signa.so/v1/deadline-rules?jurisdiction=US" \
-H "Authorization: Bearer $SIGNA_API_KEY"
# Get rules for multiple jurisdictions in one call
curl "https://api.signa.so/v1/deadline-rules?jurisdiction=US,EU,GB,DE,CA" \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
import Signa from "@signa-so/sdk";
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
const rules = await signa.references.deadlineRules({
jurisdiction: ["US", "EU", "GB", "DE", "CA"],
});
for (const rule of rules.data) {
console.log(
`[${rule.jurisdiction_code}] ${rule.name} (${rule.type}) ` +
`due year ${rule.due_year}, grace period ${rule.grace_period_months}mo`,
);
}
```
**Key differences across jurisdictions:**
| Jurisdiction | Renewal Period | Grace Period | Special Requirements |
| ------------ | -------------- | ------------ | ------------------------------------------------------------------------------------------------------ |
| US | 10 years | 6 months | Section 8 (use), Section 15 (incontestability), combined 8+9 at renewal |
| EU | 10 years | 6 months | Simple renewal only |
| GB | 10 years | 6 months | Simple renewal + restoration period (6 months post-grace) |
| DE | 10 years | 6 months | DPMA end-of-month rule (due date = last day of expiry month) |
| CA | 10 years | 6 months | Registrations from before 2019-06-17 have a 15-year initial term, then renew on the same 10-year cycle |
US marks have the most complex deadline structure. In addition to renewal, you must file a Section 8 Declaration of Use between years 5-6 after registration, and optionally a Section 15 Declaration of Incontestability at year 5. Missing the Section 8 results in cancellation, even if the mark is in active use.
Rule coverage (`GET /v1/deadline-rules`) and trademark record coverage are tracked separately: rules exist for all 25 supported jurisdictions today, while ingested trademark record data is live for a subset of offices and expanding. See [Data Freshness & Coverage](/guides/data-freshness) for which offices currently have live records.
Every `GET /v1/trademarks/{id}` response includes a `derived.deadlines` array: the computed statutory schedule, with each entry carrying `window_opens`, `due_date` and `grace_expiry` plus the rule identity (`jurisdiction_code` + `name`) that links it to [List Deadline Rules](/api-reference/reference/deadline-rules). Fetch each client mark (or use [Batch Retrieve](/api-reference/trademarks/batch-trademarks) for up to 100 at a time) and collect the deadlines that fall inside your docketing horizon.
The served rows are **dates, not countdowns**: there is no `status`, `urgency` or `days_until_due` field, because those depend on what day you read the response and would go stale inside any cache. Triage is a couple of lines against your own clock, shown below. If you would rather the server evaluate it, [Compute Deadlines](/api-reference/reference/compute-deadlines) takes an explicit `as_of_date` and returns `status`, `urgency` and `days_until_due` for each row.
```typescript TypeScript theme={null}
const clientMarkIds: string[] = [
/* your tm_... ids */
];
const horizon = "2027-09-24";
const batch = await signa.trademarks.batch({ ids: clientMarkIds });
if (batch.not_found.length > 0) {
console.warn("Not found:", batch.not_found);
}
// Triage against your own clock. `today` is an ISO date (YYYY-MM-DD), so
// the comparisons are plain string compares on ISO dates.
const today = new Date().toISOString().slice(0, 10);
function urgency(d: { due_date: string; grace_expiry: string | null }) {
if (d.due_date < today) {
return d.grace_expiry && d.grace_expiry >= today ? "in_grace" : "missed";
}
const days = Math.round(
(Date.parse(`${d.due_date}T00:00:00Z`) - Date.parse(`${today}T00:00:00Z`)) / 86_400_000,
);
if (days <= 30) return "critical";
if (days <= 180) return "upcoming";
return "routine";
}
const deadlines = batch.data
.flatMap((tm) =>
tm.derived.deadlines.map((d) => ({
trademark_id: tm.id,
mark_text: tm.mark_text,
office_code: tm.office_code,
...d,
urgency: urgency(d),
})),
)
.filter((d) => d.due_date <= horizon)
.sort((a, b) => a.due_date.localeCompare(b.due_date));
console.log(`Total deadlines before ${horizon}: ${deadlines.length}`);
const byUrgency: Record = {};
for (const d of deadlines) {
byUrgency[d.urgency] = (byUrgency[d.urgency] || 0) + 1;
}
console.log("By urgency:", byUrgency);
```
**Expected output:**
```json theme={null}
{
"object": "list",
"data": [
{
"trademark_id": "tm_abc001",
"mark_text": "BRIGHTWAVE",
"office_code": "US",
"type": "declaration_of_use",
"name": "Section 8, Declaration of Use",
"jurisdiction_code": "US",
"trigger_date": "2020-08-14",
"trigger_field": "registration_date",
"window_opens": "2025-08-14",
"due_date": "2026-08-14",
"grace_expiry": "2027-02-14",
"recurring": false,
"optional": false,
"consequence_if_missed": "cancellation",
"urgency": "upcoming"
},
{
"trademark_id": "tm_def002",
"mark_text": "MERIDIAN",
"office_code": "EM",
"type": "renewal",
"name": "EUTM renewal",
"jurisdiction_code": "EU",
"trigger_date": "2016-11-30",
"trigger_field": "filing_date",
"window_opens": "2025-11-30",
"due_date": "2026-11-30",
"grace_expiry": "2027-05-31",
"recurring": true,
"optional": false,
"consequence_if_missed": "expiration",
"urgency": "routine"
}
]
}
```
Group the collected deadlines by the `urgency` field the API already computed, no client-side date math required:
```typescript TypeScript theme={null}
const byUrgencyList: Record = {
missed: [],
in_grace: [],
critical: [],
upcoming: [],
routine: [],
};
for (const d of deadlines) {
(byUrgencyList[d.urgency] ??= []).push(d);
}
console.log(`MISSED (past grace): ${byUrgencyList.missed.length}`);
console.log(`IN GRACE PERIOD: ${byUrgencyList.in_grace.length}`);
console.log(`CRITICAL (due soon): ${byUrgencyList.critical.length}`);
console.log(`UPCOMING: ${byUrgencyList.upcoming.length}`);
console.log(`ROUTINE: ${byUrgencyList.routine.length}`);
```
| Priority | `urgency` | Action required |
| -------- | ---------- | -------------------------------------------------------- |
| P0 | `missed` | Rights likely lost. Consult counsel immediately. |
| P1 | `in_grace` | File immediately. Late fees apply. |
| P2 | `critical` | Prepare and file now, the deadline is close. |
| P3 | `upcoming` | Window may be open. Schedule for your next filing batch. |
| P4 | `routine` | No action needed yet. |
US marks require more than simple renewal. Filter the deadlines you already collected by jurisdiction and type to identify which declarations are needed.
```bash cURL theme={null}
# Get full detail including all computed deadlines
curl https://api.signa.so/v1/trademarks/tm_abc001 \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
// Filter US deadlines by type
const usDeadlines = deadlines.filter((d) => d.jurisdiction_code === "US");
const section8 = usDeadlines.filter((d) => d.type === "declaration_of_use");
const section15 = usDeadlines.filter((d) => d.type === "declaration_of_incontestability");
const combined89 = usDeadlines.filter((d) => d.type === "combined_renewal_and_use");
const renewals = usDeadlines.filter((d) => d.type === "renewal");
console.log(`\nUS Deadlines breakdown:`);
console.log(` Section 8 (Declaration of Use): ${section8.length}`);
console.log(` Section 15 (Incontestability): ${section15.length}`);
console.log(` Combined 8+9 (Renewal + Use): ${combined89.length}`);
console.log(` Simple renewals: ${renewals.length}`);
// For each Section 8, get the full trademark to check filing bases
for (const d of section8.slice(0, 3)) {
const tm = await signa.trademarks.retrieve(d.trademark_id);
console.log(`\n ${tm.mark_text} (${tm.application_number})`);
console.log(` Section 8 due: ${d.due_date} (grace: ${d.grace_expiry})`);
console.log(` Filing bases: ${tm.filing_bases.map((b) => b.basis_type).join(", ")}`);
console.log(` Classes: ${tm.classifications.map((c) => c.nice_class).join(", ")}`);
}
```
**US Declaration timeline for a mark registered on 2021-01-20:**
```
Registration ──────────────────────────────────────────────> Time
| |
v v
2021-01-20 2031-01-20
Year 5 (2026-01-20): Section 15 window opens (optional)
Year 5 (2026-01-20): Section 8 window opens
Year 6 (2027-01-20): Section 8 DUE (+ 6-month grace)
Year 10 (2031-01-20): Combined Section 8 + 9 DUE (+ 6-month grace)
```
Section 15 (incontestability) is optional but highly valuable. It eliminates most grounds for cancellation. The window opens at year 5 and remains open indefinitely, but the mark must have been in continuous use for 5 consecutive years with no pending proceedings.
***
## Deadline rules by jurisdiction
Beyond the five jurisdictions above, Signa computes deadlines for all 25 supported jurisdictions. All of them use a 6-month grace period:
| Jurisdiction | Renewal Period | Grace Period | Notes |
| ------------- | -------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
| US | 10 years | 6 months | Section 8/15 declarations, see above |
| EU | 10 years | 6 months | Simple renewal only |
| GB | 10 years | 6 months + 6 months restoration | Two-stage: grace then restoration |
| DE | 10 years | 6 months | DPMA end-of-month rule applies |
| CA | 10 years | 6 months | Pre-2019-06-17 registrations keep a 15-year initial term |
| CH | 10 years | 6 months | |
| FR | 10 years | 6 months | |
| WIPO (Madrid) | 10 years | 6 months | Per designation |
| AU | 10 years | 6 months | |
| MX | 10 years | 6 months | Renewal grace is 6 months; the separate 3rd-year Declaration of Use has no grace period |
Call [`GET /v1/deadline-rules`](/api-reference/reference/deadline-rules) for the full, current rule set, including the remaining jurisdictions (BR, BX, DK, FI, IN, IS, JP, NO, PH, PL, SE, SG, TH, TR, VN) not itemized here.
## Class coverage
A renewal review is a natural time to also check whether your registered classes still match your business. List your marks filtered to the classes you care about and diff against your target list:
```typescript TypeScript theme={null}
const targetClasses = [9, 35, 42];
const marks = await (
await signa.owners.trademarks("own_mycompany", {
nice_classes: targetClasses,
status_stage: "registered",
limit: 100,
})
).toArray();
const covered = new Set(marks.flatMap((tm) => tm.classifications.map((c) => c.nice_class)));
const gaps = targetClasses.filter((c) => !covered.has(c));
console.log("Uncovered classes:", gaps);
```
Repeat the same query per jurisdiction (add an `offices` filter) to build a jurisdiction x class coverage matrix for the portfolio.
***
## Keep watching for status changes
Refreshing this roll-up nightly from your job scheduler works, but it means you only find out about a status change when you poll. A `portfolio` [watch](/guides/monitoring/watches) with `trigger_events: ["trademark.status_changed"]` over your client marks pushes an alert the moment a mark's status flips (for example to `expired` or `cancelled`), so a missed renewal surfaces the same day the office records it instead of waiting for your next scheduled run. Pair the watch with a [webhook](/guides/monitoring/webhooks) to route it straight to your docketing queue.
If you still want a scheduled roll-up alongside the watch, run the batch fetch from step 2 nightly and diff the results against the previous run:
```typescript TypeScript theme={null}
// Persist yesterday's deadlines keyed by (trademark_id, type, due_date)
const previous = await loadYesterdaysDeadlines(); // from your own store
const todaysKeys = new Set(deadlines.map((d) => `${d.trademark_id}|${d.type}|${d.due_date}`));
const newOrChanged = deadlines.filter(
(d) => !previous.has(`${d.trademark_id}|${d.type}|${d.due_date}`),
);
console.log(`New or changed deadlines: ${newOrChanged.length}`);
await saveTodaysDeadlines(todaysKeys);
```
***
## What's next
Audit an acquired portfolio's coverage and deadlines before a deal closes.
Monitor proceedings that could affect the renewability of contested marks.
# Introduction
Source: https://docs.signa.so/index
Global trademark intelligence API
Search and retrieve trademarks from one API. production offices covering trademarks, with more added over time.
Signa normalizes filings from trademark offices worldwide into a single, consistent data model. One integration gives you full-text search, phonetic matching, owner intelligence, and computed jurisdiction-aware deadlines, so you can build trademark tools without parsing raw data from multiple government systems yourself.
## What are you building?
Full-text, phonetic, and fuzzy search across all production offices. Filter by jurisdiction, Nice class, status, owner, filing date, and more.
Query computed deadlines per mark, triage by urgency, and build a docketing workflow with grace-period awareness across 25 jurisdictions.
Normalized owner names, linked corporate parents, and attorney-client relationships across every connected office.
Browse offices, jurisdictions, Nice classifications, status taxonomies, and deadline rules for all supported offices.
## Why Signa
offices in production today, with more added over time. All normalized into a single data model, so you never parse office-specific XML, SOAP, or FTP dumps yourself.
Opposition and cancellation records linked back to every affected mark, including party rosters, proceeding status, and decision dates.
Jurisdiction-aware renewal, declaration, and opposition deadlines with rules defined for 25 jurisdictions (26 rule sets; the US has domestic and Madrid sets). Handles edge cases like DPMA end-of-month rules, Canadian legacy transitions, and Madrid Protocol designations.
Owner names are normalized and linked across filings. Public company data from SEC and GLEIF provides ticker symbols, LEI codes, and corporate parent relationships.
## Office coverage
Signa covers production trademark offices today, with full historical backfill plus daily or weekly incremental updates, and more offices added over time. Each record exposes its own `source_data_date` so you can always see exactly how current it is.
Coverage and update frequency per office.
## Quick example
Search for trademarks matching "apple" in the United States:
```bash theme={null}
curl -G "https://api.signa.so/v1/trademarks" \
-H "Authorization: Bearer sig_YOUR_KEY" \
--data-urlencode "q=apple" \
--data-urlencode "offices=US"
```
The same request also works as a `POST` with a JSON body. Use `POST` when you need aggregations or complex filter combinations.
```json theme={null}
{
"object": "list",
"data": [
{
"id": "tm_a1b2c3",
"object": "trademark",
"mark_text": "APPLE",
"relevance_score": 93,
"status": { "primary": "active", "stage": "registered" },
"office_code": "US",
"filing_date": "1977-04-11",
"classifications": [
{ "nice_class": 9, "goods_services_text": "Computer hardware; integrated circuits; semiconductors" },
{ "nice_class": 42, "goods_services_text": "Computer software design and development services" }
],
"owners": [
{ "id": "own_d4e5f6", "name": "Apple Inc.", "country_code": "US" }
]
}
],
"aggregations": {},
"has_more": true,
"pagination": { "cursor": "eyJpZCI6ImFiYyJ9" },
"request_id": "req_xyz789"
}
```
## Get started
Get your API key and make your first search in under 5 minutes.
API key format, scopes, and rotation.
Complete endpoint documentation with request/response examples for all endpoints.
Type-safe client with automatic pagination, retries, and error handling.
# Quickstart
Source: https://docs.signa.so/quickstart
Make your first API call in under 5 minutes
This guide walks you through two tiers. The first gets you a search result in 5 minutes. The second adds the TypeScript SDK, filters, pagination, and the enriched owner view.
## Tier 1: Your first search (5 minutes)
Sign up at [app.signa.so](https://app.signa.so) and create an API key from the dashboard. Your key will look like this:
```
sig_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6
```
All keys use the format `sig_{48 hex chars}`. For development or CI, create a separate test organization in the dashboard with its own key so it stays isolated from production usage and billing.
```bash cURL theme={null}
export SIGNA_API_KEY="sig_YOUR_KEY"
```
```typescript TypeScript theme={null}
// .env
SIGNA_API_KEY=sig_YOUR_KEY
```
Pass `q` for the query and comma-separated values for array filters.
```bash cURL theme={null}
curl -G "https://api.signa.so/v1/trademarks" \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "q=apple" \
--data-urlencode "offices=US"
```
```typescript TypeScript theme={null}
const url = new URL("https://api.signa.so/v1/trademarks");
url.searchParams.set("q", "apple");
url.searchParams.set("offices", "US");
const response = await fetch(url, {
headers: { "Authorization": `Bearer ${process.env.SIGNA_API_KEY}` },
});
const data = await response.json();
console.log(data);
```
For complex filter combinations or aggregations, use `POST` with a JSON body. See the [Search guide](/guides/search) for details.
Search endpoints return a list with `data` array, `has_more` for paging, and `request_id` for debugging.
```json theme={null}
{
"object": "list",
"data": [
{
"id": "tm_a1b2c3",
"object": "trademark",
"mark_text": "APPLE",
"relevance_score": 95,
"status": {
"primary": "active",
"stage": "registered"
},
"office_code": "US",
"filing_date": "1977-04-11",
"classifications": [
{ "nice_class": 9, "goods_services_text": "Computer hardware; integrated circuits; semiconductors" },
{ "nice_class": 42, "goods_services_text": "Computer software design and development services" }
],
"owners": [
{ "id": "own_d4e5f6", "name": "Apple Inc.", "country_code": "US" }
]
}
],
"aggregations": {},
"has_more": true,
"pagination": {
"cursor": "eyJpZCI6ImFiYyJ9"
},
"request_id": "req_xyz789"
}
```
Switch to `POST` and add `"options": { "aggregations": ["office_code", "status_stage", "nice_classes"] }` to the request body to get faceted counts alongside results. This is useful for building filter UIs. Aggregations are `POST`-only because they don't fit cleanly in a query string.
***
## Tier 2: SDK, filters, and pagination (15 minutes)
```bash npm theme={null}
npm install @signa-so/sdk
```
```bash pnpm theme={null}
pnpm add @signa-so/sdk
```
```bash bun theme={null}
bun add @signa-so/sdk
```
Initialize the client:
```typescript theme={null}
import { Signa } from "@signa-so/sdk";
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
```
Narrow results by office, Nice class, status, filing date, and filing route. By default, `exact` and `fuzzy` strategies run simultaneously. You can restrict or expand strategies with the `strategies` array.
```bash cURL theme={null}
curl -X POST https://api.signa.so/v1/trademarks \
-H "Authorization: Bearer $SIGNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "nova",
"strategies": ["exact", "phonetic"],
"filters": {
"offices": ["US", "EM"],
"nice_classes": [9, 42],
"status_stage": ["registered"],
"filing_date": { "gte": "2020-01-01" }
},
"options": { "aggregations": ["office_code", "nice_classes"] },
"limit": 20
}'
```
```typescript TypeScript theme={null}
const results = await signa.trademarks.search({
query: "nova",
strategies: ["exact", "phonetic"],
filters: {
offices: ["US", "EM"],
nice_classes: [9, 42],
status_stage: ["registered"],
filing_date: { gte: "2020-01-01" },
},
options: { aggregations: ["office_code", "nice_classes"] },
limit: 20,
});
console.log(`Found ${results.data.length} results on this page`);
console.log("Offices:", results.aggregations?.office_code);
```
The response includes aggregation counts you can use to build filter UIs:
```json theme={null}
{
"object": "list",
"data": [
{
"id": "tm_x7y8z9",
"mark_text": "NOVA",
"relevance_score": 89,
"status": { "primary": "active", "stage": "registered" },
"office_code": "US",
"filing_date": "2021-03-15",
"classifications": [
{ "nice_class": 9, "goods_services_text": "Downloadable mobile applications for data analytics" }
],
"owners": [
{ "id": "own_x7y8z9", "name": "Nova Technologies LLC", "country_code": "US" }
]
}
],
"aggregations": {
"office_code": { "US": 412, "EM": 189 },
"nice_classes": { "9": 347, "42": 254 }
},
"has_more": true,
"pagination": { "cursor": "eyJpZCI6Ing3eSJ9" },
"request_id": "req_abc123"
}
```
Search strategies: `exact` for full-text matches, `phonetic` to catch sound-alikes like "NOVA" / "KNOVA" / "NOWA", `fuzzy` for typo tolerance (fuzziness is always AUTO internally), and `prefix` for starts-with matching. Omit `strategies` to use the default (`exact` and `fuzzy`). For comprehensive clearance searches, use all four: `exact,phonetic,fuzzy,prefix`.
Results are cursor-based. Pass the `cursor` from one response as a query parameter or body field in the next request.
```bash cURL theme={null}
# First page
curl -X POST https://api.signa.so/v1/trademarks \
-H "Authorization: Bearer $SIGNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "nova", "limit": 20}'
# Next page (use the cursor from the previous response)
curl -X POST https://api.signa.so/v1/trademarks \
-H "Authorization: Bearer $SIGNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "nova", "limit": 20, "cursor": "eyJpZCI6Ing3eSJ9"}'
```
```typescript TypeScript theme={null}
// Automatic pagination with async iterator (list endpoint)
const marks = await signa.trademarks.list({ offices: 'US' });
for await (const mark of marks) {
console.log(mark.mark_text, mark.status.stage);
}
// Manual page-based pagination (list endpoint)
let page = await signa.trademarks.list({ offices: 'US', limit: 100 });
const allMarks = [...page.data];
while (page.has_more) {
page = await page.getNextPage();
allMarks.push(...page.data);
}
console.log(`Total: ${allMarks.length}`);
```
Retrieve a single trademark by ID to get the full record.
```bash cURL theme={null}
curl https://api.signa.so/v1/trademarks/tm_a1b2c3 \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
const mark = await signa.trademarks.retrieve("tm_a1b2c3");
console.log(mark.mark_text); // "APPLE"
console.log(mark.status.stage); // "registered"
console.log(mark.owners[0].name); // "Apple Inc."
console.log(mark.classifications.map(c => c.nice_class)); // [9, 42]
console.log(mark.registration_date); // "1978-10-31"
```
```json theme={null}
{
"id": "tm_a1b2c3",
"object": "trademark",
"mark_text": "APPLE",
"status": {
"primary": "active",
"stage": "registered"
},
"office_code": "US",
"filing_date": "1977-04-11",
"registration_date": "1978-10-31",
"classifications": [
{
"nice_class": 9,
"nice_edition": "12",
"goods_services_text": "Computer hardware; integrated circuits; semiconductors",
"goods_services_language": "en",
"status": "accepted",
"class_status_raw": "6"
},
{
"nice_class": 42,
"nice_edition": "12",
"goods_services_text": "Computer software design and development services",
"goods_services_language": "en",
"status": "accepted",
"class_status_raw": "6"
}
],
"owners": [
{ "id": "own_d4e5f6", "name": "Apple Inc.", "country_code": "US", "role": "owner" }
],
"request_id": "req_def456"
}
```
The detail level varies by endpoint: `GET /v1/trademarks/{id}` returns the full record, list endpoints return a slimmer shape with the fields most useful for result cards, and suggest endpoints return a minimal shape for autocomplete.
Every trademark detail response includes an `owners[]` array. Use the owner ID to get the full owner profile, including entity resolution data and filing statistics.
```bash cURL theme={null}
curl https://api.signa.so/v1/owners/own_d4e5f6 \
-H "Authorization: Bearer $SIGNA_API_KEY"
```
```typescript TypeScript theme={null}
const owner = await signa.owners.retrieve("own_d4e5f6");
console.log(owner.name); // "Apple Inc."
console.log(owner.country_code); // "US"
console.log(owner.stats?.trademark_count); // 1847
console.log(owner.stats?.registered_count); // 1203
console.log(owner.companies?.[0].ticker); // "AAPL"
```
```json theme={null}
{
"id": "own_d4e5f6",
"object": "owner",
"name": "Apple Inc.",
"canonical_name": "APPLE INC",
"name_original_script": null,
"country_code": "US",
"entity_type": "corporation",
"aliases": [
{ "name": "Apple Computer Inc.", "type": "former_name", "source_office": "US" }
],
"companies": [
{
"source": "sec",
"source_id": "0000320193",
"legal_name": "Apple Inc.",
"ticker": "AAPL",
"exchange": "NASDAQ",
"lei": null,
"entity_status": "active"
}
],
"stats": {
"trademark_count": 1847,
"registered_count": 1203,
"pending_count": 312,
"jurisdiction_count": 14,
"grant_rate": 0.65
},
"request_id": "req_ghi789"
}
```
***
## What's next
Phonetic matching, fuzzy search, aggregations, and filtering strategies for trademark clearance.
How Signa normalizes owner names, links corporate parents, and resolves aliases across offices.
Jurisdiction-aware renewal and declaration deadlines with rules for 25 jurisdictions.
Complete documentation for all endpoints with interactive playground.
# Deadlines You Can Docket Against
Source: https://docs.signa.so/research/deadline-verification
Verifying computed trademark deadlines against 300,000 register records
Signa Research · Version 1.0 · August 2026
Complete methodology, per-office results, every fix with its legal basis,
and the classification of every disagreement.
## Abstract
Most trademark data errors cost analysis quality. Deadline errors cost the
trademark: a missed renewal or maintenance filing does not degrade a
registration, it cancels it. Signa computes these deadlines (renewal cycles,
declarations of use, grace periods, restoration windows, and opposition
periods) from statutory rules modeled for 22 jurisdictions, with every rule
citing its legal sources and the full rule set published through the API for
inspection. Current coverage (September 2026): 25 jurisdictions; see the
[deadline-rules guide](/guides/deadline-rules).
This study measures those computations against the register itself. We froze
a sample of 31,547 registrations across ten trademark offices, then a
tenfold expanded same-seed sample of 306,896 (a strict superset of the
first, drawn with the same seed), and compared our computed deadlines against
the dates offices publish on their own records, against 27,562 renewal and
maintenance filings that actually took place, and against 34,132 real
opposition proceedings.
The engine does not guess. Where a record does not carry the input the
statute requires (three documented kinds of record) and the office has not
stated an expiry to anchor on, it says so, with the reason, instead of
computing a date. That policy lives in the product and is applied
identically in the evaluation, so the population the engine is scored on
is exactly the population it claims to compute.
Every comparable record in the study is either verified or declared: 99.69
percent of 29,102 are computed and match the office's own published date
exactly, or are explicitly declared as requiring the office's date. On the
records the engine computes (91 percent of the comparable population), the
pure statutory computation, with the office's date withheld, matches the
office's published date in 99.66 percent of cases, 99.82 percent when
weighted by how often each kind of record occurs in production. The schedule
the API actually serves, which anchors on the office's stated date where one
exists, agrees at 99.89 percent production-weighted; because that number uses
the office's date as an input, it describes what customers receive rather than
proving the rules on its own. Every disagreement was investigated and
classified by cause; 90 records disagree after the declared kinds are set
aside, and the unexplained residual remains 3 records. The study also worked
in both directions: it caught and fixed defects in our own rules, each
published with results from before and after the fix, and it identified 311
records classified as register-data errors, with per-record evidence, where
the register, not the computation, carries the wrong date.
This is a first-party evaluation: Signa selected the metrics, wrote the
evaluator, corrected the system under test, and classified the
disagreements. It has not been independently audited. The artifacts (frozen
manifests, samples, events, oppositions, and per-record results) are
published at [github.com/signa-so/research](https://github.com/signa-so/research)
so any reader can check the work.
## Results at a glance
How to read the table: the first row is what the API serves; the second and
third score the statutory computation on its own, with the office's date
withheld; "declared" means the engine stated that it needed the office's
date rather than guessing.
| What was measured | Result |
| --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Agreement of the schedule the API serves with office-published dates (anchors on the office's stated date where one exists) | **99.89%**, production-weighted |
| Records either verified exactly or explicitly declared as needing the office's date | **99.69%** of 29,102 |
| Pure statutory computation, office's date withheld, on the records the engine computes | **99.66%** of 26,607 (99.82% production-weighted) |
| Records declared rather than guessed (three documented kinds, stated in the product) | 2,495 (8.6%) |
| Real renewal and maintenance filings inside computed windows | **94.7%** of 24,091 on computed records (99.4% at USPTO); 93.2% of all 27,562 on the served schedule |
| Real opposition filings inside computed opposition windows | **90.2%** of 23,751 (98.2% at EUIPO) |
| Disagreements remaining after the declared kinds are set aside | 90 records, each classified |
| Unexplained residual after investigating every disagreement | **3 records** |
For continuity with version 0.91, which scored the engine's guesses on the
declared kinds against it: full-population exact agreement under that older
framing was 96.08 percent of 282,393 comparisons at expanded scale.
## What the study found
1. **The statute and the register audit each other.** Where a computed
deadline disagreed with the register, investigation attributed the
disagreement to the register more often than to the computation: 311
records classified as register-data errors, with per-record evidence,
against no further rule defect identified among the investigated
residuals. Computing deadlines from the law catches register errors that
a system echoing stored dates would repeat.
2. **Verification improved the product, in public.** The study surfaced
defects in our own rules and data handling. Each was fixed, tied to its
statute, and published with agreement measured before and after. No
correction was accepted on empirical fit alone; each required a statutory
or documented-data-source justification.
3. **The engine says when it cannot know.** Three kinds of record do not
carry the input the statute needs (pre-1996 Australian and pre-1999
Singapore filings under repealed cadences, and Madrid designations with
no international-registration anchor). Version 1.0 makes the engine
decline those with a stated reason instead of guessing, resolves Madrid
designations through the WIPO parent registration where one exists, and
scores itself on exactly the population it claims to compute.
4. **Deadlines are computed fresh, never stored.** Every correction
applied to every record instantly. The frozen evaluation runs on every
code change and now also pins the opposition cells, so a change that
moves even one date in the sample blocks release until it is
re-verified. In August 2026 the rules package was also put through
mutation testing: 2,946 mutants, every one killed or classified.
5. **You can check the rules yourself.** Every deadline rule, with its
legal citations and the date it was last verified, is available through
the API, and any deadline in the study can be recomputed with an API
key. See the [deadline rules guide](/guides/deadline-rules).
## Scope and limitations
Deadline rules currently cover 22 jurisdictions; for offices not yet
modeled (Korea and China among them) the API states that plainly
rather than guessing. Current coverage (September 2026): 25 jurisdictions; see
the [deadline-rules guide](/guides/deadline-rules). Opposition verification is
strongest where the underlying data is cleanest, and the report documents where
it is bounded by data quality rather than by the rules. Ground truth is the register
itself, which contains errors; the report treats that honestly in both
directions rather than assuming either side is right.
The full report contains the complete methodology, per-office tables,
statistical protocol, and every limitation stated plainly.
# Signa Research
Source: https://docs.signa.so/research/index
Studies measuring the accuracy of the data and computations behind the Signa API
Signa publishes research that measures, against independent ground truth,
the accuracy of the data and computations the API serves. Every study
follows the same discipline: samples are frozen before any comparison runs,
every number carries its sample size, every disagreement is investigated
and classified by cause, and defects found by a study are published with
results from before and after the fix.
Nothing in these reports has to be taken on faith. The rules under test are
inspectable through the API, and the measurements are reproducible with an
API key.
## Reports
Computed trademark deadlines verified against register records across
ten offices: 99.7 percent of comparable records either match the
office-published date exactly or are explicitly declared as needing the
office's date, with every disagreement investigated and classified.
Further studies (coverage and freshness, classification accuracy, entity
resolution) are in preparation.
# TypeScript SDK
Source: https://docs.signa.so/sdk/typescript
Official TypeScript SDK for the Signa API
The `@signa-so/sdk` package is a typed, ergonomic client for the Signa API: full types for every endpoint and response, automatic pagination, built-in retries, and typed error classes.
The SDK is designed for **server-side use**. A Signa API key grants full access to your org's data; putting one in browser code exposes it to every visitor. Proxy requests through your own backend instead.
## Install
```bash npm theme={null}
npm install @signa-so/sdk
```
```bash yarn theme={null}
yarn add @signa-so/sdk
```
```bash pnpm theme={null}
pnpm add @signa-so/sdk
```
```bash bun theme={null}
bun add @signa-so/sdk
```
Requires Node.js 18+ or Bun 1.0+. TypeScript 5.0+ is recommended but not required.
## Configure
```typescript theme={null}
import { Signa } from '@signa-so/sdk';
const signa = new Signa({
api_key: process.env.SIGNA_API_KEY,
// Optional overrides
base_url: 'https://api.signa.so', // default
timeout: 30_000, // 30s default
max_retries: 2, // default, retries on 429/5xx
});
```
If you omit `api_key`, the client reads `SIGNA_API_KEY` from the environment:
```bash theme={null}
export SIGNA_API_KEY=sig_your_key_here
```
```typescript theme={null}
// Uses SIGNA_API_KEY automatically
const signa = new Signa();
```
## Resources
The client organizes the API into resource namespaces. The main ones:
| Namespace | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `signa.trademarks` | Search, retrieve, batch lookup, events, proceedings, assignments, documents, citations, citedBy |
| `signa.owners` | Owner profiles, trademark portfolios, GLEIF corporate relationships |
| `signa.entities` | Resolved entities: one company across all offices, trademarks, corporate family |
| `signa.attorneys` | Attorney profiles, trademark portfolios, client lists |
| `signa.firms` | Law firm profiles, attorneys, trademark portfolios |
| `signa.proceedings` | Oppositions, cancellations, and other tribunal proceedings |
| `signa.citations` | Office-action citations across every mark: which prior marks were cited against which applications, with disposition and stage |
| `signa.suggest` | Cross-entity typeahead across trademarks, owners, attorneys, and firms |
| `signa.references` | Classifications, offices, office coverage votes, jurisdictions, event types, design codes, deadline and opposition rules |
| `signa.goodsServices` | Goods & services term catalog and AI-assisted specification drafting |
| `signa.deadlines` | Batch maintenance-deadline computations |
| `signa.oppositions` | Batch opposition-window computations |
| `signa.reconcile` | Compare your own records against register data |
| `signa.portfolios` | Portfolio CRUD, member marks, deadlines |
| `signa.watches` | Watch CRUD, pause/resume, preview, bulk create, diagnostics |
| `signa.alerts` | Read-only alert listing, retrieval, and bulk lookup |
| `signa.webhooks` | Webhook endpoint CRUD, secret rotation, test deliveries, delivery audit |
| `signa.events` | Org event stream: list and retrieve with diffs |
| `signa.organization` | Account identity, usage, API key management, request logs |
## Search and list trademarks
`search()` takes a text query with structured filters under `filters`, plus `options` for aggregations and totals:
```typescript theme={null}
const results = await signa.trademarks.search({
query: 'SIGNA',
strategies: ['exact', 'phonetic'],
filters: { jurisdictions: ['US', 'EU'], nice_classes: [9, 42] },
options: { aggregations: ['office_code'], include_total: true },
});
for (const hit of results.data) {
console.log(hit.mark_text, hit.score);
}
console.log(results.aggregations);
console.log(results.search_meta);
```
`list()` is for filter-only or simple-query listing. Its filters are flat top-level params, not nested:
```typescript theme={null}
const page = await signa.trademarks.list({
offices: ['US'],
status_primary: 'active',
filing_date_gte: '2024-01-01',
sort: '-filing_date',
limit: 50,
});
```
Both return a `SignaList`, see [Pagination](#pagination) below.
## Retrieve and batch
```typescript theme={null}
const tm = await signa.trademarks.retrieve('tm_8kLm2nPq');
console.log(tm.mark_text, tm.status.primary);
// Madrid territory coverage, mark-to-mark relationships and source provenance
// ship inline on the detail response; there is nothing extra to request.
console.log(tm.coverage?.territory_count, tm.relationships.length, tm.provenance.source_data_date);
// The prosecution timeline is unbounded, so it stays a sub-resource
const events = await signa.trademarks.events('tm_8kLm2nPq', { limit: 50 });
```
Look up multiple trademarks in one call, by Signa ID or office identifier (max 100 per call):
```typescript theme={null}
const result = await signa.trademarks.batch({ ids: ['tm_8kLm2nPq', 'tm_9jNq3rTw'] });
console.log(result.data); // trademarks that matched
console.log(result.not_found); // IDs or identifiers that didn't match
```
## Pagination
Every list and search method returns a `SignaList`, which supports three consumption patterns. The first page is fetched eagerly; later pages are fetched lazily.
**Async iteration**, the simplest approach:
```typescript theme={null}
for await (const tm of signa.trademarks.list({ offices: ['US'] })) {
console.log(tm.mark_text);
}
```
**Collect to an array** with `toArray()`. A safety cap of 10,000 items applies by default; pass `{ limit }` to change it:
```typescript theme={null}
const page = await signa.trademarks.list({ jurisdictions: ['US'], status_primary: 'active' });
const allMarks = await page.toArray(); // up to 10,000
const first500 = await page.toArray({ limit: 500 });
```
**Manual paging**, for full control over when the next request fires:
```typescript theme={null}
let page = await signa.trademarks.list({ offices: ['US'], limit: 100 });
console.log(`Page 1: ${page.data.length} items`);
console.log(`Request ID: ${page.request_id}`);
while (page.has_more) {
page = await page.getNextPage();
console.log(`Next page: ${page.data.length} items`);
}
```
`getNextPage()` returns an empty list (not an error) once there are no more pages. Every `SignaList` exposes `data`, `has_more`, `request_id`, and, on search responses, `search_meta` and `aggregations`. There is no public `pagination` field on the list itself, use `has_more` and `getNextPage()` to drive pagination rather than reaching for a cursor directly.
## Error handling
All errors extend `SignaError`. API errors (4xx/5xx) extend `SignaAPIError` and carry a typed subclass per status code:
```
SignaError
├── SignaAPIError
│ ├── BadRequestError 400
│ ├── AuthenticationError 401
│ ├── PermissionError 403
│ ├── NotFoundError 404
│ ├── ConflictError 409
│ ├── RateLimitError 429 (has retry_after)
│ └── InternalServerError 5xx
├── ConnectionError DNS, TCP, TLS failures
└── TimeoutError request exceeded timeout
```
Use `instanceof` to handle specific error types:
```typescript theme={null}
import { Signa } from '@signa-so/sdk';
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
try {
const tm = await signa.trademarks.retrieve('tm_invalid');
} catch (err) {
if (err instanceof Signa.NotFoundError) {
console.log('Trademark not found');
} else if (err instanceof Signa.RateLimitError) {
console.log(`Rate limited, retry after ${err.retry_after}s`);
} else if (err instanceof Signa.AuthenticationError) {
console.log('Invalid API key');
} else if (err instanceof Signa.SignaAPIError) {
console.log(`API error ${err.status}: ${err.message}`);
} else if (err instanceof Signa.ConnectionError) {
console.log('Network issue:', err.message);
} else if (err instanceof Signa.TimeoutError) {
console.log('Request timed out');
}
}
```
Every `SignaAPIError` carries the structured error body plus the request ID:
```typescript theme={null}
try {
await signa.trademarks.list({ offices: ['invalid_office'] });
} catch (err) {
if (err instanceof Signa.BadRequestError) {
console.log(err.error.type); // machine-readable slug, e.g. "validation_error"
console.log(err.error.detail); // human-readable detail
console.log(err.request_id); // include this in support requests
}
}
```
### Automatic retries
| Error type | Retried? |
| --------------------------- | -------------------------------------------------- |
| `ConnectionError` | Yes |
| `TimeoutError` | Yes |
| `RateLimitError` (429) | Yes, after the `retry_after` delay |
| `InternalServerError` (5xx) | Yes, unless the error body says `retryable: false` |
| `BadRequestError` (400) | No |
| `AuthenticationError` (401) | No |
| `NotFoundError` (404) | No |
An explicit `retryable: false` in the server's error body overrides the status-based rule: a deterministic failure like the watch preview's `504` timeout is not retried, since a blind retry would re-run the same over-budget work. Retries use exponential backoff with jitter.
```typescript theme={null}
const signa = new Signa({
api_key: process.env.SIGNA_API_KEY,
max_retries: 3, // default: 2, set to 0 to disable
});
```
Override retry behavior per request:
```typescript theme={null}
const tm = await signa.trademarks.retrieve('tm_8kLm2nPq', undefined, { max_retries: 0 });
```
## Timeouts
The default timeout is 30 seconds. Configure it globally or per request:
```typescript theme={null}
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY, timeout: 60_000 });
const tm = await signa.trademarks.retrieve('tm_8kLm2nPq', undefined, { timeout: 10_000 });
```
## Debug mode
```typescript theme={null}
const signa = new Signa({ api_key: process.env.SIGNA_API_KEY, debug: true });
```
Logs each request and response (method, URL, status, timing) to stderr.