# MCP server Source: https://docs.signa.so/ai/mcp Connect AI agents to Signa's trademark data in natural language The Signa MCP (Model Context Protocol) server lets AI agents like Claude, ChatGPT, and Cursor search and analyze trademark data conversationally. Ask a question in plain language and get a structured answer backed by live trademark-office data, without copy-pasting API responses into chat. Every query runs as your organization, scoped exactly like the REST API. ## Connect Signa runs a **hosted** MCP server. There's no package to install and no local process to run, most clients just need one URL: ``` https://api.signa.so/mcp ``` For Claude Code, that's a single command: ```bash theme={null} claude mcp add --transport http signa https://api.signa.so/mcp ``` Your client opens a browser to sign in, and you're connected. Add Signa as a **custom connector**: **Settings → Connectors → Add custom connector**, name it `Signa`, and enter `https://api.signa.so/mcp`. Save, then click **Connect** and complete the browser login. Or edit the config file directly and restart Claude Desktop: ```json theme={null} { "mcpServers": { "signa": { "url": "https://api.signa.so/mcp", "type": "streamableHttp" } } } ``` Add to `.cursor/mcp.json` (project) or your global Cursor MCP settings: ```json theme={null} { "mcpServers": { "signa": { "url": "https://api.signa.so/mcp" } } } ``` Cursor discovers the OAuth flow automatically; **signa** appears under **MCP Tools** with a green status once connected. Requires a ChatGPT plan with MCP connectors enabled. Go to **Settings → Connectors → Create** (enable Developer mode if needed), enter `https://api.signa.so/mcp`, and choose **OAuth**. Add to `.vscode/mcp.json`, or run **MCP: Add Server** from the Command Palette: ```json theme={null} { "servers": { "signa": { "type": "http", "url": "https://api.signa.so/mcp" } } } ``` Any client that supports Streamable HTTP can connect directly to `https://api.signa.so/mcp` and will discover the OAuth flow via the `/.well-known/oauth-protected-resource` endpoint. For an API or enterprise product that doesn't support interactive OAuth, use a static Signa API key as `Authorization: Bearer sig_YOUR_KEY`. **Stdio-only client?** Bridge it with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote): ```json theme={null} { "mcpServers": { "signa": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.signa.so/mcp"] } } } ``` ### Verify the connection Ask your agent: > "Use Signa to search for NIKE trademarks in the US." You should see it call the `search_trademarks` tool and return live USPTO results. ## Authentication Most clients use **OAuth**: your client opens a browser for you to sign in at app.signa.so and approve access, then refreshes the token silently, no key to paste or rotate. For scripts, CI, or headless setups, use a Signa API key as a bearer token instead (`Authorization: Bearer sig_YOUR_KEY`, or `--header "Authorization: Bearer sig_YOUR_KEY"` when adding the server). Either way, the tools available to a session depend on its scopes: see [Tools & permissions](/ai/mcp-tools) for the full list and what each tool requires. ## What you can ask Your agent picks the right tools automatically, you don't need to name them. Edit the brand names, classes, and jurisdictions to fit your work. > "Search for trademarks similar to AURORA in Nice class 9 in the US." > "Give me the full detail on trademark `tm_8kLm2nPq`, owners, classes, status, and any proceedings." > "Who owns the most trademarks in the semiconductor space, and how big are their portfolios?" > "Create a watch for new US filings phonetically similar to 'BRIGHTLY' in classes 9 and 42, then show me its status." > "Which Nice classes should I file in for a company that sells smart home cameras and a subscription app?" > "List TTAB oppositions filed against marks owned by Apple Inc. in the last 12 months." For the full tool surface and worked recipes with the REST API, see [Tools & permissions](/ai/mcp-tools) and the [use-case guides](/guides/use-cases/trademark-clearance). # Tools & permissions Source: https://docs.signa.so/ai/mcp-tools The MCP tool surface, scopes, metering, and rate limits Your client lists every available tool automatically (`tools/list`) with full input schemas, you rarely need to call one by name. This page is the map of what's available, the scope each requires, and how calls are metered. Every tool is **org-scoped** to your authenticated session. Org context comes from your token or API key, never from a tool argument, so an agent can't read or change another organization's data. Reference-data tools (offices, jurisdictions, classifications, rules) are global public data, not org-scoped, but still require a scope like other reads. ## Search | Tool | What it does | Read/Write | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `search_trademarks` | Search global registrations by name, owner, Nice class, jurisdiction, or status. Exact, phonetic, fuzzy, and prefix matching across production offices. | Read | | `suggest` | Quick typeahead across trademarks, owners, attorneys, and firms, compact results for disambiguation. | Read | ## Trademarks | Tool | What it does | Read/Write | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `get_trademark` | Full detail for one mark by ID: owners, attorneys, classifications, media, plus inline Madrid `coverage`, mark-to-mark `relationships`, and source `provenance`. | Read | | `list_trademarks` | List marks with filtering and sorting (use `search_trademarks` for text search). | Read | | `get_trademark_events` | Prosecution event timeline for a mark (filings, publications, registrations, status changes). | Read | | `get_trademark_proceedings` | Proceedings involving a mark (oppositions, cancellations), each with its parties. | Read | | `batch_get_trademarks` | Fetch full detail for up to 100 marks by ID in one call, with a `not_found` list for anything that didn't match. | Read | ## Owners & entities | Tool | What it does | Read/Write | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------- | | `get_owner` | Owner (applicant/registrant) detail with portfolio statistics. | Read | | `list_owners` | Search and list owners, find companies or individuals who hold marks. | Read | | `get_owner_trademarks` | An owner's trademarks (summary tier) with filtering and sorting. | Read | | `get_owner_related` | Owners related via the GLEIF corporate hierarchy (parent/child). | Read | | `get_entity` | A resolved entity: the cross-office identity over per-office owner clusters, with link evidence and public-company facts. | Read | | `list_entities` | Search resolved entities (one company across all offices), filterable by public-company facts. | Read | | `get_entity_trademarks` | A resolved entity's trademarks fanned across all member owners. | Read | | `get_entity_family` | A resolved entity's direct GLEIF corporate family (parent and direct subsidiaries). | Read | ## Attorneys & firms | Tool | What it does | Read/Write | | ------------------------- | ---------------------------------------------------------------------- | ---------- | | `get_attorney` | Detail for an attorney or representative of record. | Read | | `list_attorneys` | Search and list attorneys and representatives. | Read | | `get_attorney_trademarks` | An attorney's trademarks (summary tier) with filtering and sorting. | Read | | `get_attorney_clients` | Owners an attorney represents, each with a shared-mark count. | Read | | `get_firm` | Law-firm detail, including portfolio and attorneys. | Read | | `list_firms` | Search and list law firms, ranked by portfolio size or attorney count. | Read | | `get_firm_attorneys` | A firm's attorneys, each with portfolio stats. | Read | | `get_firm_trademarks` | Trademarks handled by a firm's attorneys (summary tier). | Read | ## Proceedings | Tool | What it does | Read/Write | | ------------------ | --------------------------------------------------------------------------------------- | ---------- | | `get_proceeding` | Detail for one proceeding (opposition, cancellation, etc.) by ID (e.g. `prc_8kLm2nPq`). | Read | | `list_proceedings` | List TTAB oppositions, cancellations, and other proceedings with filtering. | Read | ## Reference data Global, public reference data. Not org-scoped, but still requires a read scope, and is logged but not billed. | Tool | What it does | Read/Write | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------- | | `list_offices` | Supported trademark offices, with jurisdiction coverage. | Read | | `get_office` | A single office by its uppercase ST.3 office code (e.g. `US`, `EM`, `WO`); legacy lowercase codes are accepted as aliases. | Read | | `list_jurisdictions` | Supported jurisdictions (ISO-2 plus regional/WIPO codes), with Madrid membership and live-office coverage. | Read | | `get_jurisdiction` | A single jurisdiction by code, including Madrid membership and member jurisdictions. | Read | | `get_deadline_rules` | Maintenance/renewal deadline rules with statutory citations; optionally per jurisdiction. | Read | | `get_opposition_rules` | Opposition window rules with citations, duration, and time zone; optionally per jurisdiction. | Read | | `list_classifications` | Nice classification classes (the 45-class WIPO standard), filterable by category or keyword. | Read | | `suggest_classifications` | Suggest Nice classes for a natural-language description, ranked with confidence and rationale. | Read | | `list_goods_services` | Browse/search the curated goods & services term catalog. | Read | | `suggest_goods_services` | Draft filing-ready goods/services wording per Nice class from a description. | Read | | `list_design_codes` | Vienna classification design codes (figurative-element taxonomy for logos/devices). | Read | | `get_design_code` | A single Vienna design code with notes and its child codes. | Read | ## Account & usage Utility tools for your own org's account and consumption. Neither billed nor counted against quota. | Tool | What it does | Read/Write | | ------------------- | -------------------------------------------------------------------------------------------- | ---------- | | `get_usage` | Current billing-period usage plus the per-endpoint-type quota and rate-limit matrix. | Read | | `get_usage_summary` | Daily usage breakdown (requests, billed units, errors, average latency). | Read | | `get_account` | Org identity (ID, name, plan, key scopes and rate-limit tier) with the plan quota breakdown. | Read | ## Monitoring Watches, alerts, and webhooks. Billed at zero request-level cost; watch capacity itself is capped by your plan. | Tool | What it does | Read/Write | | ------------------------- | ------------------------------------------------------------------------------------------- | ---------- | | `signa_watches_list` | List monitoring watches for your org (cursor-paginated). | Read | | `signa_watches_retrieve` | Retrieve a single watch by ID. | Read | | `signa_watches_create` | Create a watch (same validation as `POST /v1/watches`). | Write | | `signa_watches_update` | Update a watch (PATCH semantics, omitted fields unchanged). | Write | | `signa_watches_delete` | Deactivate a watch (soft-delete). | Write | | `pause_watch` | Pause a watch (stops new alerts). | Write | | `resume_watch` | Resume a paused watch. | Write | | `preview_watch` | Dry-run a watch query: estimate matches and preview marks. No watch is created. | Read | | `get_watch_diagnostics` | Audit-mode provenance trace for one (watch, trademark) pair. | Read | | `signa_alerts_list` | List monitoring alerts for your org. | Read | | `signa_alerts_retrieve` | Retrieve a single alert by ID. | Read | | `list_alerts_for_watch` | List alerts emitted by a single watch (cursor-paginated). | Read | | `lookup_alert` | Bulk-fetch up to 100 alerts by ID (polling pattern). | Read | | `signa_events_list` | List your org's event ledger (beta). Supports `sort=id` + `after` replay from a checkpoint. | Read | | `signa_webhooks_list` | List webhook endpoints (secrets are never returned). | Read | | `signa_webhooks_retrieve` | Retrieve one webhook endpoint (secret redacted). | Read | Webhook secrets are **never** exposed through MCP, `signa_webhooks_list` and `signa_webhooks_retrieve` always redact them. Manage secrets via the [REST API](/api-reference/monitoring/webhooks/rotate-secret) instead. ## Scopes | Scope | Grants access to | | ------------------- | -------------------------------------------------------------------------------------------------- | | `trademarks:read` | Every search, trademark, owner, entity, attorney, firm, proceeding, and reference-data tool above. | | `billing:read` | Account & usage tools: `get_usage`, `get_usage_summary`, `get_account`. | | `portfolios:manage` | Every monitoring tool, reads and writes alike: watches, alerts, and webhooks. | | `offline_access` | Keeps an OAuth session's token refreshing silently. Doesn't grant tool access on its own. | Most clients request `trademarks:read` by default, which covers every data and reference tool. Add `billing:read` if you want your agent to inspect usage and account details, and `portfolios:manage` only if you want it to read or change watches, alerts, and webhooks. If a tool needs a scope your session lacks, the call returns a clear error naming the required scope, and the rest of the session keeps working. ## Metering MCP tool calls are metered like REST requests: each call counts against your plan's quota and appears in your usage and request logs, classified the same way its REST counterpart is (search, read, monitoring, and so on). Search and read tools cost 1 unit; `suggest_goods_services` costs 2; `batch_get_trademarks` costs 1 unit per ID (up to 100). Reference-data tools are logged but not billed. Account and usage tools are neither billed nor counted. Over-quota calls return a clear error naming the limit that was hit and when it resets, instead of running the tool. The `initialize` and `tools/list` handshake calls are never metered. ## Rate limits MCP requests share a dedicated rate-limit tier of 1,000 requests per minute, separate from your plan's per-endpoint-type limits. See [Rate limits](/api-reference/rate-limits) for the full header semantics. If you hit the limit, the response includes a `Retry-After` header. ## Security * **Org isolation.** Org context is resolved from your token or API key on every request, never from a tool argument. An agent cannot read or modify another organization's data, even if prompted to. * **Least privilege.** Scopes are enforced per tool. For read-only use, issue an API key without `portfolios:manage` so the watch, alert, and webhook write tools are unavailable. * **Review tool calls.** Most MCP clients let you approve tool calls before they run. Keep approval on for any agent with `portfolios:manage`, and treat tool results as data an untrusted prompt could try to act on. ## Troubleshooting Your MCP client is not sending the access token. Re-authenticate by removing and re-adding the MCP connection. Your Signa account has not completed onboarding. Log in at [app.signa.so](https://app.signa.so) and create an organization first. Your token has expired. Most MCP clients handle refresh automatically. If the error persists, remove and re-add the connection to re-authenticate. You are sending more than 1,000 MCP requests per minute. Wait for the `Retry-After` period or reduce request frequency. # AI onboarding Source: https://docs.signa.so/ai/onboarding Point AI tools and coding agents at the Signa API Signa's docs and API are built to be read by AI agents, not just people. This page is the map: what to point an agent at, and when to use each option. ## The docs themselves Every page ships a plain-Markdown version alongside the rendered HTML. Use the menu in the top right of any page to copy it as Markdown, or open it directly in Claude, ChatGPT, Cursor, or VS Code. For a single file covering the whole site, point an agent at: ``` https://docs.signa.so/llms.txt ``` `llms.txt` lists every guide and API reference page with a one-line description, so an agent can decide what to fetch instead of crawling the site. ## The OpenAPI spec For codegen, request validation, or building your own tool definitions, the machine-readable spec is: ``` https://api.signa.so/v1/openapi.json ``` It covers every endpoint, parameter, and response shape in the [API reference](/api-reference/introduction). Feed the URL to an OpenAPI-aware codegen tool, or hand it to an agent that can fetch and reason over JSON schemas directly. ## The hosted MCP server If your AI tool speaks [MCP](https://modelcontextprotocol.io) (Claude, ChatGPT, Cursor, and most agent IDEs do), Signa runs a hosted MCP server at `https://api.signa.so/mcp`. Connect it and an agent can search, retrieve, and monitor trademarks conversationally, no client code required. See [MCP server](/ai/mcp) to connect, and [Tools & permissions](/ai/mcp-tools) for the full tool list and scopes. ## The agent skill For coding agents, install the `signa` skill in the project you're building in: ```bash theme={null} npx skills add signa-so/skills ``` It packages the API conventions an agent otherwise guesses wrong (auth, filters, the watch DSL, pagination, error handling) so generated integration code is correct on the first try. See [Agent skill](/ai/skill). ## The TypeScript SDK When an agent is writing code against Signa rather than calling it directly (a script, a backend integration, a scheduled job), point it at the `@signa-so/sdk` package. It's typed end to end, so an agent gets autocomplete and compile-time checks instead of guessing field names from example JSON. See [TypeScript SDK](/sdk/typescript). ## Which one to use * **Exploring the API or drafting a prompt**: give the agent the docs or `llms.txt`. * **Generating a client or tool definitions**: use the OpenAPI spec. * **Letting an agent act on your Signa account conversationally**: connect the MCP server. * **Having a coding agent build your integration**: install the [agent skill](/ai/skill). * **Writing a script or service that calls Signa**: use the TypeScript SDK. # Agent skill Source: https://docs.signa.so/ai/skill Install the signa skill so your coding agent writes correct Signa integrations on the first try The `signa` skill teaches coding agents (Claude Code, Cursor, Codex, Copilot, and any other agent that supports skills) how the Signa API actually works: authentication, the response envelope, search and filter conventions, the watch query DSL, pagination, and error handling. With the skill installed, an agent writes correct integration code on the first try instead of guessing field names and inventing endpoints. ## Install Run this from the project where you're building against Signa: ```bash theme={null} npx skills add signa-so/skills ``` That installs the skill into your project (for Claude Code, it lands in `.claude/skills/signa/`), where your agent picks it up automatically. The source lives at [github.com/signa-so/skills](https://github.com/signa-so/skills). ## What it covers * **Conventions agents otherwise get wrong**: Bearer auth, the list envelope, comma-separated array params, `_gte`/`_lt` date operators, `POST` filters nested under `filters`, idempotency keys, cursor pagination, and 429 handling. * **A task-to-endpoint map**: which endpoint to reach for by job to be done (clearance, portfolio pulls, monitoring, deadlines). * **The monitoring surface**: the watch query DSL and webhook signature verification, the area agents hallucinate most without it. * **Live-docs awareness**: the skill tells the agent to fetch current details from these docs (every page serves raw Markdown, and [llms.txt](https://docs.signa.so/llms.txt) is the index), so it stays accurate as the API evolves. ## Skill or MCP server? They complement each other. The [MCP server](/ai/mcp) gives an agent hosted tools to **act on Signa conversationally** (search, create watches) without writing code. The skill gives a coding agent the knowledge to **write code against the API** (scripts, backends, scheduled jobs). If your agent is producing an integration, install the skill; if it's answering questions or operating your account, connect MCP. Many teams use both. # Create API Key Source: https://docs.signa.so/api-reference/administration/create-api-key POST /v1/organization/api-keys Generate a new API key for the organization ## Overview Creates a new API key for your organization. The full key secret is returned only once in the response; it cannot be retrieved again. Store it securely immediately after creation. You can assign a name for identification, specific scopes to limit access, an optional expiration date, and key-value metadata. You cannot grant scopes the calling key does not itself hold, and an organization can have at most 25 active keys. Requires the `api-keys:manage` scope. This endpoint requires an `Idempotency-Key` header; see [Idempotency](/api-reference/introduction#idempotency). ## Request Body Human-readable name for the key, 1-255 characters (e.g. "Production Backend"). Authorized scopes (1-20 values, must be a subset of the calling key's scopes): | Scope | Grants access to | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `trademarks:read` | Trademark records, owners, attorneys, firms, proceedings — all public catalog data | | `events:read` | Internal-tagged, held event feed (`/v1/events`). Plan-gated. `alert.created` is projected; `trademark.*` is projected for portfolio marks when the projector is enabled. `portfolio_id` filters every family through the event-time membership snapshot. | | `portfolios:manage` | Customer-owned resources: portfolios (including membership references, computed deadlines, and calendar downloads), watches, alerts, webhooks. Portfolio operations use the `read` meter: 1 unit per request, writes and calendar downloads included. Your plan caps portfolios and distinct managed marks; both deadline formats require portfolios of 5,000 marks or fewer. | | `api-keys:manage` | API key creation, rotation, update, and revocation | | `organization:manage` | Organization settings (e.g. renaming the organization) | | `billing:read` | Usage, plan, and request log endpoints | Optional ISO 8601 timestamp for key expiration. Must be in the future. Omit or pass `null` for a non-expiring key. Optional key-value metadata (string values, max 50 keys). ## Response Returns the full API key object plus the one-time `key` secret, flat at the top level. Key ID (`key_*`). Always `api_key`. Key name. Full API key secret. This is the only time it is returned. First 12 characters of the key, for identification. Authorized scopes. Rate limit tier label for the key. `standard` by default. Lifecycle state: `active`, `expired`, or `revoked`. Always `active` on creation. Expiry timestamp, or `null`. Always `null` on creation. Key-value metadata. Always `null` on creation. ID of the API key that created this key. Creation timestamp. Last update timestamp. Unique request identifier for support and debugging. ```json theme={null} { "id": "key_Pe5hI9jK", "object": "api_key", "name": "Aurora Digital analytics", "key": "sig_69f99181efdb8d205c86878c5f232ee0722f22750b2cc25b", "prefix": "sig_69f99181", "scopes": ["trademarks:read", "billing:read"], "rate_limit_tier": "standard", "status": "active", "expires_at": null, "last_used_at": null, "metadata": {}, "revoked_at": null, "created_by": "key_Mc2eF6gH", "created_at": "2026-06-12T16:00:00.000Z", "updated_at": "2026-06-12T16:00:00.000Z", "request_id": "req_wP2gH8iJ" } ``` ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/organization/api-keys" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: create-analytics-key-2026-06-12" \ -d '{ "name": "Aurora Digital analytics", "scopes": ["trademarks:read", "billing:read"] }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const apiKey = await signa.organization.apiKeys.create({ name: "Aurora Digital analytics", scopes: ["trademarks:read", "billing:read"], }); // Store apiKey.key securely: it won't be returned again console.log("New key:", apiKey.key); ``` ## Errors | Status | Type | Description | | ------ | ------------------------ | ----------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Missing `name`, unknown scope values, invalid `expires_at`, or missing `Idempotency-Key` header | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `api-keys:manage`, or the request grants scopes the calling key does not hold | | 409 | `conflict` | The `Idempotency-Key` was already used with a **different** request body | | 409 | `idempotency_processing` | A request with the same `Idempotency-Key` is still in flight | | 429 | `rate_limited` | Too many requests, or the organization already has 25 active keys | Retrying with the same `Idempotency-Key` and the same body replays the cached response, including the same `key` secret, so an ambiguous network failure never mints a second credential. Duplicate key **names** are allowed and do not 409. ## Related Endpoints * [List API Keys](/api-reference/administration/list-api-keys), view all keys * [Rotate API Key](/api-reference/administration/rotate-api-key), rotate an existing key * [Get Current Organization](/api-reference/administration/get-me), organization profile # Submit Feedback Source: https://docs.signa.so/api-reference/administration/create-feedback POST /v1/feedback Report a data issue, bug, or feature request through the API ## Overview Submit structured feedback about the Signa API or its data. This is the programmatic reporting channel: use it to flag an inaccurate record, report a server error you hit, or request a capability. It is designed for both humans and AI agents acting on your behalf. Every submission is stored in your organization, posted to the Signa team for triage, and readable back so you can watch it move through its status lifecycle. Feedback is never billed (`0` units) and runs on generous rate limits, so reporting a problem never costs you anything. Any valid API key may submit feedback for its organization; no special scope is required. An `Idempotency-Key` header is required (the standard mutating-request rule), so a retried submission never creates a duplicate. ## Feedback types The required `type` discriminator routes and categorizes the report: | `type` | Use it for | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data_issue` | A record disagrees with the official registry (wrong owner name, stale status, missing field). Pair with `resource_id`, `field`, and `expected_value` to make it actionable. | | `bug` | The API behaved incorrectly (an unexpected error, a wrong response shape). Attach `request_id` so we see exactly what you saw. | | `feature_request` | A capability you want that does not exist yet. | | `other` | Anything that does not fit the above. A `message` alone is always valid. | ## Request Body One of `data_issue`, `bug`, `feature_request`, `other`. Free-form report body (1 to 5000 characters). Public ID of any referenced entity (`tm_...`, `own_...`, `att_...`, `firm_...`, `prc_...`). At submit time the server snapshots the record's current state into `context.resource`. The `request_id` of the API request where you observed the problem (`req_...`, or a client-supplied `x-request-id`; up to 64 URL-safe characters). At submit time the server snapshots the matching request log into `context.request`. For data issues: which field is wrong (for example `owner.name`). Up to 255 characters. For data issues: what the value should be. Up to 5000 characters. Optional string-to-string map for your own tags (max 50 keys). ## Submit-time enrichment When you reference a `request_id` or a `resource_id`, Signa captures an investigation snapshot into the read-only `context` object at submit time. This matters because request logs are dropped after roughly four months while feedback lives indefinitely, and because a record can be overwritten by a later sync. The snapshot preserves what you saw. Enrichment never rejects a submission. An unknown, foreign, or non-existent reference is recorded as `null` or `found: false` rather than returning an error. **`context.request`** is present when you sent `request_id`: * An object `{ route, method, status_code, error_type, error_detail, duration_ms, occurred_at }` when the request is found within your organization. * `null` when the request id is unknown or belongs to another organization (no cross-org leakage). **`context.resource`** is present when you sent `resource_id`: * `{ found: true, resource_type, office, jurisdiction, version, status_primary, source_updated_at, updated_at }` when the record exists (with `null` where a given entity type lacks a field). * `{ found: false, resource_type }` when the record does not exist. A `context` key is simply absent when the corresponding reference was not provided or the snapshot could not be captured. ## Response Returns the created feedback object with HTTP `201`. The feedback identifier (`fbk_...`). Always `feedback`. Public ID (`key_...`) of the API key that filed this report, for per-key attribution within your org. `null` if unattributed. The submitted type (`data_issue`, `bug`, `feature_request`, `other`). Lifecycle status: `open` on creation, then `acknowledged` or `resolved`. The report body you submitted. The referenced entity's public ID, echoed back, or `null`. The `request_id` you referenced, echoed verbatim. Named `request_ref` because the envelope reserves the top-level `request_id` for this API call. The field you flagged, or `null`. The value you expected, or `null`. Server-built investigation snapshots (`context.request`, `context.resource`). Read-only, never client-supplied. See [Submit-time enrichment](#submit-time-enrichment). The Signa team's note when the report is resolved, or `null`. Your string-to-string metadata map. ISO timestamp when the report was submitted. ISO timestamp of the last status change. ISO timestamp when the report was resolved, or `null`. ```json theme={null} { "id": "fbk_550e8400-e29b-41d4-a716-446655440000", "object": "feedback", "api_key_id": "key_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "type": "data_issue", "status": "open", "message": "The registered owner is out of date; this mark was assigned last year.", "resource_id": "tm_9f8e7d6c-5b4a-3210-fedc-ba9876543210", "request_ref": "req_01kpjabcdefghijkmnpqrstvwx", "field": "owner.name", "expected_value": "Acme Holdings LLC", "context": { "request": { "route": "/v1/trademarks/{id}", "method": "GET", "status_code": 200, "error_type": null, "error_detail": null, "duration_ms": 12, "occurred_at": "2026-07-10T12:34:56.000Z" }, "resource": { "found": true, "resource_type": "trademark", "office": "US", "jurisdiction": "US", "version": 3, "status_primary": "active", "source_updated_at": "2026-07-01T00:00:00.000Z", "updated_at": "2026-07-02T08:00:00.000Z" } }, "resolution_note": null, "metadata": {}, "created_at": "2026-07-10T12:40:00.000Z", "updated_at": "2026-07-10T12:40:00.000Z", "resolved_at": null, "request_id": "req_cW8oQ3rT" } ``` ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/feedback" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: data-issue-tm-9f8e7d6c-001" \ -d '{ "type": "data_issue", "message": "The registered owner is out of date; this mark was assigned last year.", "resource_id": "tm_9f8e7d6c-5b4a-3210-fedc-ba9876543210", "field": "owner.name", "expected_value": "Acme Holdings LLC" }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const feedback = await signa.feedback.create({ type: "data_issue", message: "The registered owner is out of date; this mark was assigned last year.", resource_id: "tm_9f8e7d6c-5b4a-3210-fedc-ba9876543210", field: "owner.name", expected_value: "Acme Holdings LLC", }); console.log(feedback.id, feedback.status); // fbk_... open ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------------------------------------------------------- | | 400 | `validation_error` | Missing `type`/`message`, message out of range, or a malformed `resource_id`/`request_id` | | 401 | `unauthorized` | Missing or invalid API key | | 409 | `conflict` | `Idempotency-Key` reused with a different body | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [List Feedback](/api-reference/administration/list-feedback), review everything your org has reported * [Retrieve Feedback](/api-reference/administration/get-feedback), check the status of one report * [Retrieve Request Log](/api-reference/administration/retrieve-log), look up the `request_id` you want to attach # Revoke API Key Source: https://docs.signa.so/api-reference/administration/delete-api-key DELETE /v1/organization/api-keys/{id} Soft-revoke an API key (sets revoked_at, preserves the row for audit) ## Overview Revokes an API key by setting its `revoked_at` timestamp. The key record is preserved so audit logs and historical usage records remain valid. Once revoked, requests using the key return `401 unauthorized` immediately. System keys cannot be revoked. Requires the `api-keys:manage` scope and an `Idempotency-Key` header; see [Idempotency](/api-reference/introduction#idempotency). ## Path Parameters API key ID (`key_...`). ## Response Echo of the revoked API key ID. Always `api_key`. Always `true` on success. Unique request identifier for support and debugging. ```json theme={null} { "id": "key_Mc2eF6gH", "object": "api_key", "deleted": true, "request_id": "req_zS5jK1lM" } ``` ## Code Examples ```bash cURL theme={null} curl -X DELETE "https://api.signa.so/v1/organization/api-keys/key_Mc2eF6gH" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: revoke-key-Mc2eF6gH-2026-06-12" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); await signa.organization.apiKeys.revoke("key_Mc2eF6gH"); ``` Revocation is irreversible. To roll a key without downtime, use [Rotate API Key](/api-reference/administration/rotate-api-key): the old key remains valid for a 24-hour grace period. ## Errors | Status | Type | Description | | ------ | ------------------ | --------------------------------------------------------- | | 400 | `validation_error` | Invalid API key ID or missing `Idempotency-Key` header | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `api-keys:manage` or target is a system key | | 404 | `not_found` | API key does not exist or belongs to another org | ## Related Endpoints * [Rotate API Key](/api-reference/administration/rotate-api-key), graceful key rotation * [List API Keys](/api-reference/administration/list-api-keys) # Get Credits Source: https://docs.signa.so/api-reference/administration/get-credits GET /v1/organization/credits Pooled credit balance, grant breakdown, and expiry schedule **Beta.** Credits are the single billing currency across the Signa API. This endpoint's schema is new and may evolve before general availability. ## Overview Returns the authenticated organization's pooled **credit** balance: the spendable balance across all active grants, a breakdown of remaining credits by grant type (`plan` vs `addon` vs `promo`), and the per-grant expiry schedule in consumption order (plan first, then oldest-expiry-first). Requires the `billing:read` scope. Querying credits does not consume any credits. ## Response Always `"credit_balance"`. Authoritative spendable pooled balance. Reconciles exactly: `balance = (plan + addon + promo) - reserved - pending`. May be negative under the debt floor. Remaining credits from plan grants. Remaining credits from addon grants. Remaining credits from promotional grants. Credits held by open reservations (reserved but not yet settled or refunded). Billable spend not yet attributed to specific grants by the hourly applier. Grant ID (`grant_...`). One of `plan`, `addon`, `promo`. Credits originally granted. Credits remaining on this grant (may be negative under debt). ISO timestamp for when the grant became usable. ISO timestamp for when the grant expires (exclusive). Request ID for debugging. ```json theme={null} { "object": "credit_balance", "balance": 8500, "grants": { "plan": 8000, "addon": 500, "promo": 0 }, "reserved": 0, "pending": 0, "expiry_schedule": [ { "grant_id": "grant_01HXYZPLAN", "type": "plan", "amount": 10000, "remaining": 8000, "effective_at": "2026-04-01T00:00:00.000Z", "expires_at": "2026-05-01T00:00:00.000Z" }, { "grant_id": "grant_01HXYZADDON", "type": "addon", "amount": 500, "remaining": 500, "effective_at": "2026-04-10T00:00:00.000Z", "expires_at": "2027-04-10T00:00:00.000Z" } ], "request_id": "req_abc123" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/organization/credits" \ -H "Authorization: Bearer sig_xxxxxxxxxxxx" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: "sig_xxxxxxxxxxxx" }); const credits = await signa.organization.credits(); console.log(credits.balance, credits.grants.plan, credits.grants.addon); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/organization/credits", headers={"Authorization": "Bearer sig_xxxxxxxxxxxx"}, ) credits = resp.json() ``` ## Errors | Status | Type | Description | | ------ | -------------- | ---------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `billing:read` | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Get Usage Summary](/api-reference/administration/get-usage-summary) -- daily usage breakdown in credit terms * [Get Usage](/api-reference/administration/get-usage) -- current billing period totals by endpoint type # Retrieve Feedback Source: https://docs.signa.so/api-reference/administration/get-feedback GET /v1/feedback/{id} Retrieve a single feedback item by ID ## Overview Returns a single feedback item by its `fbk_` ID. Use it to poll a report as it moves through its status lifecycle so you know your report was seen and acted on. Reads are strictly org-scoped. A feedback ID that belongs to another organization returns `404 not_found`, exactly like an ID that does not exist, so nothing leaks about other orgs' reports. Any valid API key may read; no special scope is required. Retrieving feedback does not consume metered usage. ## Status lifecycle Every report starts at `open` and moves forward as the Signa team works it: | `status` | Meaning | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `open` | Submitted and awaiting triage. This is the value on creation. | | `acknowledged` | The team has seen the report and is investigating. | | `resolved` | The report is closed. `resolution_note` explains the outcome and `resolved_at` is stamped. A "won't fix" is a `resolved` with an explanatory note (there is no separate status). | Poll this endpoint (or filter [List Feedback](/api-reference/administration/list-feedback) by `status`) to follow the transition. ## Path Parameters Feedback identifier (`fbk_...`) returned when you submitted the report. ## Response Same fields as a single [feedback object](/api-reference/administration/create-feedback#response). ```json theme={null} { "id": "fbk_550e8400-e29b-41d4-a716-446655440000", "object": "feedback", "api_key_id": "key_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "type": "data_issue", "status": "resolved", "message": "The registered owner is out of date.", "resource_id": "tm_9f8e7d6c-5b4a-3210-fedc-ba9876543210", "request_ref": "req_01kpjabcdefghijkmnpqrstvwx", "field": "owner.name", "expected_value": "Acme Holdings LLC", "context": { "resource": { "found": true, "resource_type": "trademark", "office": "US", "jurisdiction": "US", "version": 3, "status_primary": "active", "source_updated_at": "2026-07-01T00:00:00.000Z", "updated_at": "2026-07-02T08:00:00.000Z" } }, "resolution_note": "Corrected on the next USPTO sync; the assignment is now reflected.", "metadata": {}, "created_at": "2026-07-10T12:40:00.000Z", "updated_at": "2026-07-11T09:15:00.000Z", "resolved_at": "2026-07-11T09:15:00.000Z", "request_id": "req_dX9pR4sU" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/feedback/fbk_550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const feedback = await signa.feedback.retrieve( "fbk_550e8400-e29b-41d4-a716-446655440000", ); if (feedback.status === "resolved") { console.log("Resolved:", feedback.resolution_note); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------------------------------------------------- | | 400 | `validation_error` | Malformed feedback ID (not a prefixed ID at all) | | 400 | `id_type_mismatch` | A well-formed ID of the wrong type (e.g. a `tm_...` where an `fbk_...` is expected) | | 401 | `unauthorized` | Missing or invalid API key | | 404 | `not_found` | Feedback does not exist or belongs to another organization | ## Related Endpoints * [Submit Feedback](/api-reference/administration/create-feedback), file a new report * [List Feedback](/api-reference/administration/list-feedback), paginated list with type and status filters # List Request Logs Source: https://docs.signa.so/api-reference/administration/get-logs GET /v1/organization/logs List API request logs for the organization **Beta.** The request logs schema is new and may evolve. Field names should be stable, but we reserve the right to add fields or adjust response shape before general availability. ## Overview Returns a paginated list of API request logs for the authenticated organization. Filterable by date range, HTTP status code, success/failure outcome, method, API key, endpoint type, and path. Per-plan access control limits the historical window: | Plan | Retention window | | --------------------------- | ---------------- | | `free` | Last 7 days | | `starter` | Last 30 days | | `beta`, `pro`, `enterprise` | Last 90 days | Requires the `billing:read` scope. Querying logs does not consume metered usage. ## Query Parameters Start of date range (ISO 8601). Accepts `YYYY-MM-DD` or full ISO datetime. Defaults to 24 hours ago. End of date range (ISO 8601). Accepts `YYYY-MM-DD` or full ISO datetime. Defaults to now. Alias for `start_date`. If both are sent, `start_date` wins. Alias for `end_date`. If both are sent, `end_date` wins. Filter to a specific HTTP status code (100--599). Filter by outcome: `true` returns succeeded requests (status `< 400`), `false` returns failed requests (status `>= 400`). Filter by HTTP method (`GET`, `POST`, `PATCH`, `DELETE`, etc.). Filter to requests made with a specific API key (`key_...`). Filter by classified endpoint type. One of `search`, `read`, `monitoring`, `screening`, `clearance`, `check`, `image_search`, `export`, `reference`, `utility`. Case-insensitive substring match against path or request ID (max 200 characters). Items per page (1--100). Pagination cursor from a previous response. ## Response Always `"list"`. The logged request's identifier (`req_...`). Always `"request_log"`. HTTP method (`GET`, `POST`, etc.). Request path (e.g. `/v1/trademarks`). HTTP status code returned. Request duration in milliseconds. Classified endpoint type (e.g. `search`, `read`, `utility`). Credits charged against your plan for this request. Prefix of the API key used (null if the request predates prefix capture). Error type slug (null on success). Error detail message (null on success). ISO timestamp when the request was logged. Whether more pages are available. Cursor for the next page (null if this is the last page). ```json theme={null} { "object": "list", "data": [ { "id": "req_01HXYZ...", "object": "request_log", "method": "GET", "path": "/v1/trademarks", "status_code": 200, "duration_ms": 142, "endpoint_type": "search", "credits": 1, "api_key_prefix": "sig_7kMn", "error_type": null, "error_detail": null, "created_at": "2026-04-18T10:12:33Z" }, { "id": "req_01HXYA...", "object": "request_log", "method": "GET", "path": "/v1/trademarks/tm_bogus", "status_code": 404, "duration_ms": 18, "endpoint_type": "read", "credits": 0, "api_key_prefix": "sig_7kMn", "error_type": "not_found", "error_detail": "Trademark tm_bogus does not exist.", "created_at": "2026-04-18T10:11:05Z" } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6I..." }, "request_id": "req_currentCall" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/organization/logs?limit=50&status_code=429" \ -H "Authorization: Bearer sig_xxxxxxxxxxxx" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: "sig_xxxxxxxxxxxx" }); const logs = await signa.organization.logs.list({ start_date: "2026-04-17", end_date: "2026-04-18", status_code: 429, limit: 50, }); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/organization/logs", headers={"Authorization": "Bearer sig_xxxxxxxxxxxx"}, params={"status_code": 429, "limit": 50}, ) logs = resp.json()["data"] ``` ## Errors | Status | Type | Description | | ------ | --------------------- | -------------------------------------------------- | | 400 | `validation_error` | Invalid date format or `start_date > end_date` | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `billing:read` | | 403 | `plan_limit_exceeded` | Requested window exceeds your plan's log retention | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Request Log](/api-reference/administration/retrieve-log) -- single request log detail * [Get Usage Summary](/api-reference/administration/get-usage-summary) -- aggregate usage by day, endpoint type, or API key # Get Current Organization Source: https://docs.signa.so/api-reference/administration/get-me GET /v1/organization/me Retrieve the authenticated organization's profile ## Overview Returns the profile of the organization associated with the current API key. Use this to verify authentication, check your plan tier, and retrieve your organization ID. This is typically the first call made when setting up an integration to confirm the API key is valid. Works with any valid API key; no specific scope is required. ## Response Always `identity`. Organization ID (`orgn_*`). Organization name. Current plan: `beta`, `free`, `starter`, `pro`, or `enterprise`. During the beta period every organization is on `beta`. API key ID (`key_*`). First 12 characters of the key, for identification. Authorized scopes for this key. Rate limit tier label for this key. `standard` for customer keys. Unique request identifier for support and debugging. ```json theme={null} { "object": "identity", "org_id": "orgn_Lb1dE5fG", "org_name": "Aurora Digital", "plan": "beta", "api_key": { "id": "key_Mc2eF6gH", "prefix": "sig_7kMn2pQr", "scopes": ["trademarks:read", "billing:read"], "rate_limit_tier": "standard" }, "request_id": "req_tM9dE5fG" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/organization/me" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const identity = await signa.organization.me(); console.log(identity.org_name, identity.plan); ``` ## Errors | Status | Type | Description | | ------ | -------------- | -------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Get Usage](/api-reference/administration/get-usage), current billing period usage * [List API Keys](/api-reference/administration/list-api-keys), manage API keys # Get Usage Source: https://docs.signa.so/api-reference/administration/get-usage GET /v1/organization/usage Retrieve API usage statistics for the current billing period ## Overview Returns API usage for the current billing period, broken down by metered endpoint type, plus the per-endpoint-type requests-per-minute limits your plan enforces. `by_endpoint_type` reports `used` and `limit` for each metered endpoint type: `search`, `read`, `screening`, and `check`. A `limit` of `null` means unlimited on your plan; `0` means the endpoint type is not available on your plan. Requires the `billing:read` scope. ## Response Always `usage`. ISO timestamp for the billing period start (inclusive). ISO timestamp for the billing period end (exclusive). Keyed by metered endpoint type (`search`, `read`, `screening`, `check`). Each value is an object: Units used in the current billing period for this endpoint type. Plan limit for this endpoint type. `null` if unlimited, `0` if not available on the current plan. Requests-per-minute limit per endpoint type (`search`, `read`, `screening`, `monitoring`, `utility`, `reference`, `check`), as enforced by the rate limiter. See [Rate Limits](/api-reference/rate-limits) for how limits are applied. Unique request identifier for support and debugging. ```json theme={null} { "object": "usage", "billing_period": { "start": "2026-06-01T00:00:00.000Z", "end": "2026-07-01T00:00:00.000Z" }, "by_endpoint_type": { "search": { "used": 1204, "limit": 100000 }, "read": { "used": 8932, "limit": 500000 }, "screening": { "used": 112, "limit": 1000 }, "check": { "used": 46, "limit": 500000 } }, "rate_limits": { "search": 1000, "read": 10000, "screening": 100, "monitoring": 100, "utility": 1000, "reference": 1000, "check": 1000 }, "request_id": "req_aS6kL2mN" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/organization/usage" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const usage = await signa.organization.usage(); console.log("Search used:", usage.by_endpoint_type.search.used); ``` ## Errors | Status | Type | Description | | ------ | -------------- | -------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `billing:read` scope | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Get Current Organization](/api-reference/administration/get-me), organization profile and plan * [Get Usage Summary](/api-reference/administration/get-usage-summary), daily breakdown by endpoint type or API key * [List API Keys](/api-reference/administration/list-api-keys), manage API keys # Get Usage Summary Source: https://docs.signa.so/api-reference/administration/get-usage-summary GET /v1/organization/usage/summary Daily usage breakdown for the organization **Beta.** The usage summary schema is new and may evolve. Field names should be stable, but we reserve the right to add fields or adjust response shape before general availability. ## Overview Returns a pre-aggregated usage breakdown for the authenticated organization over a date range. Reads from daily rollups for fast queries, then attaches the current billing period's quota status. Group the results by `day`, `endpoint_type`, or `api_key`, and optionally filter to a single endpoint type. Requires the `billing:read` scope. Querying the summary does not consume metered usage. ## Query Parameters Start of date range (`YYYY-MM-DD`). End of date range (`YYYY-MM-DD`), inclusive. Grouping dimension. One of `day`, `endpoint_type`, or `api_key`. Restrict the summary to a specific endpoint type. Itemized metered types are `search` and `read`. `check` is shipped and quota-metered (`POST /v1/goods-services/suggest`) but not yet itemized here; others — `screening`, `clearance`, `image_search`, `export` — exist in plan config but have no shipped endpoints yet. When set, the `billing_period.credits_limit` reflects that endpoint type's specific plan allowance. ## Response Always `"list"`. ISO timestamp for the start of the rollup period (usually a day). Endpoint type for this row. API key ID (`key_...`). Present only when `group_by=api_key`. Total requests in the period. Credits consumed by billable requests in the period. Count of non-2xx responses in the period. Average request duration in milliseconds (null if no requests). Always `false` -- the summary is fully returned in one response. ISO timestamp for the billing period start (inclusive). ISO timestamp for the billing period end (exclusive -- this is the start of the next period, e.g. `2026-05-01T00:00:00.000Z` for an April period). Credits used so far this billing period (overall, or for the filtered endpoint type). Pooled credit allowance for the period (`null` if unlimited). `credits_limit - credits_used`, floored at 0 (`null` if unlimited). `true` when the date range fell back to the current billing period because both `start_date` and `end_date` were omitted. Request ID for debugging. ```json theme={null} { "object": "list", "data": [ { "period_start": "2026-04-15T00:00:00Z", "endpoint_type": "search", "request_count": 420, "credits": 420, "error_count": 2, "avg_duration_ms": 138 }, { "period_start": "2026-04-16T00:00:00Z", "endpoint_type": "search", "request_count": 512, "credits": 512, "error_count": 0, "avg_duration_ms": 121 } ], "has_more": false, "billing_period": { "start": "2026-04-01T00:00:00.000Z", "end": "2026-05-01T00:00:00.000Z", "credits_used": 932, "credits_limit": 100000, "credits_remaining": 99068, "defaulted": false }, "request_id": "req_abc123" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/organization/usage/summary?start_date=2026-04-01&end_date=2026-04-18&group_by=day&endpoint_type=search" \ -H "Authorization: Bearer sig_xxxxxxxxxxxx" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: "sig_xxxxxxxxxxxx" }); const summary = await signa.organization.usageSummary({ start_date: "2026-04-01", end_date: "2026-04-18", group_by: "day", endpoint_type: "search", }); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/organization/usage/summary", headers={"Authorization": "Bearer sig_xxxxxxxxxxxx"}, params={ "start_date": "2026-04-01", "end_date": "2026-04-18", "group_by": "day", "endpoint_type": "search", }, ) summary = resp.json() ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------------- | | 400 | `validation_error` | Missing `start_date`/`end_date`, invalid date format, or `start_date > end_date` | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `billing:read` | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Get Usage](/api-reference/administration/get-usage) -- current billing period totals by endpoint type * [List Request Logs](/api-reference/administration/get-logs) -- per-request audit trail # List API Keys Source: https://docs.signa.so/api-reference/administration/list-api-keys GET /v1/organization/api-keys List all API keys for the authenticated organization ## Overview Returns the API keys for your organization, including creation date, last used timestamp, and scopes. The raw key secret is never returned; only the `prefix` (the first 12 characters of the key) is included, for identification. Use this to audit active keys, identify unused credentials, and manage key lifecycle. Defaults to active keys only. Pass `status=revoked`, `status=expired`, or `status=all` to see revoked or expired keys for audit purposes. Requires the `api-keys:manage` scope. ## Query Parameters Lifecycle filter: `active` (non-revoked, non-expired), `revoked`, `expired`, or `all`. Items per page (max 100). Pagination cursor from a previous response. ## Response Always `list`. Key ID (`key_*`). Always `api_key`. Key name. First 12 characters of the raw key, for identification. The full secret is never returned. Authorized scopes. Rate limit tier label for the key. Lifecycle state: `active`, `expired`, or `revoked`. Expiry timestamp, or `null` for a non-expiring key. Timestamp of the last API call with this key, or `null` if never used. Key-value metadata. Revocation timestamp, or `null`. ID of the API key that created this key. Creation timestamp. Last update timestamp. Whether more pages are available. Cursor for the next page, or `null` on the last page. Unique request identifier for support and debugging. ```json theme={null} { "object": "list", "data": [ { "id": "key_Mc2eF6gH", "object": "api_key", "name": "Aurora Digital production", "prefix": "sig_7kMn2pQr", "scopes": ["trademarks:read", "billing:read"], "rate_limit_tier": "standard", "status": "active", "expires_at": null, "last_used_at": "2026-06-11T14:30:00.000Z", "metadata": {}, "revoked_at": null, "created_by": "key_Lb1dE5fG", "created_at": "2026-01-05T12:00:00.000Z", "updated_at": "2026-06-11T14:30:00.000Z" }, { "id": "key_Nd3fG7hI", "object": "api_key", "name": "Aurora Digital CI", "prefix": "sig_9pQr4sTu", "scopes": ["trademarks:read"], "rate_limit_tier": "standard", "status": "active", "expires_at": "2026-12-31T23:59:59.000Z", "last_used_at": "2026-06-10T09:15:00.000Z", "metadata": { "environment": "ci" }, "revoked_at": null, "created_by": "key_Mc2eF6gH", "created_at": "2026-02-01T12:00:00.000Z", "updated_at": "2026-06-10T09:15:00.000Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_vO1fG7hI" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/organization/api-keys" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const keys = await signa.organization.apiKeys.list(); for (const key of keys.data) { console.log(key.name, key.status, key.last_used_at); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------- | | 400 | `validation_error` | Invalid `status`, `limit`, or `cursor` value | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `api-keys:manage` | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Create API Key](/api-reference/administration/create-api-key), generate a new key * [Retrieve API Key](/api-reference/administration/retrieve-api-key), single key detail * [Rotate API Key](/api-reference/administration/rotate-api-key), rotate an existing key # List Feedback Source: https://docs.signa.so/api-reference/administration/list-feedback GET /v1/feedback List feedback your organization has submitted ## Overview Returns a cursor-paginated list of the feedback your organization has submitted, newest first. Reads are strictly org-scoped: you only ever see your own organization's reports, and all API keys within an org share visibility (the per-row `api_key_id` tells you which key filed each one). A competitor can never see what you have reported. Filterable by type, status, and created-at range, all combinable. Any valid API key may read; no special scope is required. Listing feedback does not consume metered usage. ## Query Parameters Filter by feedback type. Comma-separated for multiple: `?type=data_issue,bug`. Values: `data_issue`, `bug`, `feature_request`, `other`. Filter by lifecycle status. Comma-separated for multiple: `?status=open,acknowledged`. Values: `open`, `acknowledged`, `resolved`. Return feedback created at or after this time (`YYYY-MM-DD` or ISO 8601). Return feedback created strictly before this time (`YYYY-MM-DD` or ISO 8601). Items per page (1 to 100). Pagination cursor from a previous response. ## Response Always `list`. Same fields as a single [feedback object](/api-reference/administration/create-feedback#response): `id` (`fbk_...`), `object`, `api_key_id`, `type`, `status`, `message`, `resource_id`, `request_ref`, `field`, `expected_value`, `context`, `resolution_note`, `metadata`, `created_at`, `updated_at`, `resolved_at`. Whether more pages are available. Cursor for the next page, or `null` on the last page. The request ID of this list call itself. ```json theme={null} { "object": "list", "data": [ { "id": "fbk_550e8400-e29b-41d4-a716-446655440000", "object": "feedback", "api_key_id": "key_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "type": "data_issue", "status": "acknowledged", "message": "The registered owner is out of date.", "resource_id": "tm_9f8e7d6c-5b4a-3210-fedc-ba9876543210", "request_ref": "req_01kpjabcdefghijkmnpqrstvwx", "field": "owner.name", "expected_value": "Acme Holdings LLC", "context": { "resource": { "found": true, "resource_type": "trademark", "office": "US", "jurisdiction": "US", "version": 3, "status_primary": "active", "source_updated_at": "2026-07-01T00:00:00.000Z", "updated_at": "2026-07-02T08:00:00.000Z" } }, "resolution_note": null, "metadata": {}, "created_at": "2026-07-10T12:40:00.000Z", "updated_at": "2026-07-10T15:02:00.000Z", "resolved_at": null } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_aS6kL2mN" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/feedback?type=data_issue&status=open,acknowledged&limit=50" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); // Auto-paginating async iterator over every open data issue. const open = await signa.feedback.list({ type: "data_issue", status: "open" }); for await (const item of open) { console.log(item.id, item.status, item.message); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------- | | 400 | `validation_error` | Invalid `type`/`status` value or date format | | 401 | `unauthorized` | Missing or invalid API key | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Submit Feedback](/api-reference/administration/create-feedback), file a new report * [Retrieve Feedback](/api-reference/administration/get-feedback), fetch one report by ID # Retrieve API Key Source: https://docs.signa.so/api-reference/administration/retrieve-api-key GET /v1/organization/api-keys/{id} Retrieve a single API key by ID ## Overview Returns the metadata for a single API key. The raw key secret is never returned; only the `prefix` (the first 12 characters of the key) is included, for identification. The full secret is shown once, when the key is created or rotated. Revoked and expired keys are still retrievable here so audit history stays intact; check the `status` field. Requires the `api-keys:manage` scope. ## Path Parameters API key ID (`key_...`). ## Response API key ID. Always `api_key`. Human-readable name. First 12 characters of the raw key, for identification. The full secret is never returned. Scopes granted to this key. Rate limit tier label for the key. Lifecycle state: `active`, `expired`, or `revoked`. Expiry timestamp, or `null` for a non-expiring key. Timestamp of the last API call with this key, or `null` if never used. Key-value metadata. Revocation timestamp, or `null`. ID of the API key that created this key. Creation timestamp. Last update timestamp. Unique request identifier for support and debugging. ```json theme={null} { "id": "key_Mc2eF6gH", "object": "api_key", "name": "Aurora Digital production", "prefix": "sig_7kMn2pQr", "scopes": ["trademarks:read", "billing:read"], "rate_limit_tier": "standard", "status": "active", "expires_at": null, "last_used_at": "2026-06-11T14:30:00.000Z", "metadata": { "environment": "production" }, "revoked_at": null, "created_by": "key_Lb1dE5fG", "created_at": "2026-01-05T12:00:00.000Z", "updated_at": "2026-06-11T14:30:00.000Z", "request_id": "req_uN0eF6gH" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/organization/api-keys/key_Mc2eF6gH" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const apiKey = await signa.organization.apiKeys.retrieve("key_Mc2eF6gH"); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------------------ | | 400 | `validation_error` | Invalid API key ID | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `api-keys:manage` | | 404 | `not_found` | API key does not exist or belongs to another org | ## Related Endpoints * [List API Keys](/api-reference/administration/list-api-keys) * [Update API Key](/api-reference/administration/update-api-key) * [Rotate API Key](/api-reference/administration/rotate-api-key) # Retrieve Request Log Source: https://docs.signa.so/api-reference/administration/retrieve-log GET /v1/organization/logs/{request_id} Retrieve a single API request log by request ID **Beta.** The request logs schema is new and may evolve. Field names should be stable, but we reserve the right to add fields or adjust response shape before general availability. ## Overview Returns a single request log entry by its `request_id`. Useful for looking up the full context of a request when a caller reports an error and includes the `request_id` header. Per-plan access control limits how far back you can retrieve logs: | Plan | Retention window | | --------------------------- | ---------------- | | `free` | Last 7 days | | `starter` | Last 30 days | | `beta`, `pro`, `enterprise` | Last 90 days | Requires the `billing:read` scope. ## Path Parameters Request identifier from the `X-Request-Id` response header (e.g. `req_01HXYZ...`). ## Response The logged request's identifier (matches the `request_id` path parameter). Always `"request_log"`. HTTP method. Request path. HTTP status code returned. Request duration in milliseconds. Classified endpoint type (e.g. `search`, `read`, `utility`). Credits charged against your plan for this request. Prefix of the API key used. Error type slug (null on success). Error detail message (null on success). ISO timestamp when the request was logged. The request ID of **this** lookup call (per the standard envelope convention). To identify the logged request being retrieved, use the top-level `id` field. ```json theme={null} { "id": "req_01HXYA...", "object": "request_log", "method": "GET", "path": "/v1/trademarks/tm_bogus", "status_code": 404, "duration_ms": 18, "endpoint_type": "read", "credits": 0, "api_key_prefix": "sig_7kMn", "error_type": "not_found", "error_detail": "Trademark tm_bogus does not exist.", "created_at": "2026-04-18T10:11:05Z", "request_id": "req_01HXYZ_currentCall" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/organization/logs/req_01HXYA..." \ -H "Authorization: Bearer sig_xxxxxxxxxxxx" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: "sig_xxxxxxxxxxxx" }); const log = await signa.organization.logs.retrieve("req_01HXYA..."); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/organization/logs/req_01HXYA...", headers={"Authorization": "Bearer sig_xxxxxxxxxxxx"}, ) log = resp.json() ``` ## Errors | Status | Type | Description | | ------ | -------------- | ---------------------------------------------------------------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `billing:read` | | 404 | `not_found` | Request log does not exist, belongs to another org, or is outside your plan's retention window | ## Related Endpoints * [List Request Logs](/api-reference/administration/get-logs) -- paginated list with filters * [Get Usage Summary](/api-reference/administration/get-usage-summary) -- aggregate usage rollups # Rotate API Key Source: https://docs.signa.so/api-reference/administration/rotate-api-key POST /v1/organization/api-keys/{id}/rotate Rotate an existing API key, generating a new secret ## Overview Rotates an existing API key by creating a **new key** with a **new ID** and the same name, scopes, rate limit tier, and metadata. The old key remains valid for a fixed 24-hour grace period to allow zero-downtime credential rotation in distributed systems. If the old key had a customer-set expiry sooner than 24 hours away, that earlier expiry is kept; rotation never extends a key's life. The new key (including its new ID and secret) is returned only once in the response. Store it securely immediately. Requires the `api-keys:manage` scope, and the caller must hold every scope of the key being rotated. This endpoint requires an `Idempotency-Key` header; see [Idempotency](/api-reference/introduction#idempotency). Retrying with the same key replays the cached response, including the **same** rotated secret, so an ambiguous network failure never rotates twice. The grace period is fixed at 24 hours and is not configurable. The old key automatically stops working 24 hours after rotation, and a key in its grace period cannot be rotated again. ## Path Parameters API key ID to rotate (e.g. `key_Mc2eF6gH`). ## Response Returns the **new** API key object, flat at the top level, with the same fields as [Create API Key](/api-reference/administration/create-api-key) including the one-time `key` secret. The `id`, `prefix`, `key`, and `created_at` all belong to the new key; the `name`, `scopes`, `rate_limit_tier`, and `metadata` are inherited from the rotated key. ```json theme={null} { "id": "key_Qf6iJ0kL", "object": "api_key", "name": "Aurora Digital production", "key": "sig_b07dffe3ee95ea51d200ebb4f089a570c3e078a97d640ea6", "prefix": "sig_b07dffe3", "scopes": ["trademarks:read", "billing:read"], "rate_limit_tier": "standard", "status": "active", "expires_at": null, "last_used_at": null, "metadata": {}, "revoked_at": null, "created_by": "key_Mc2eF6gH", "created_at": "2026-06-12T08:00:00.000Z", "updated_at": "2026-06-12T08:00:00.000Z", "request_id": "req_xQ3hI9jK" } ``` ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/organization/api-keys/key_Mc2eF6gH/rotate" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: rotate-key-Mc2eF6gH-2026-06-12" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const rotated = await signa.organization.apiKeys.rotate("key_Mc2eF6gH"); // Deploy rotated.key to your services within 24 hours console.log("New key:", rotated.key); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid key ID or missing `Idempotency-Key` header | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `api-keys:manage`, the target is a system key, or the target has scopes the caller does not hold | | 404 | `not_found` | Key ID does not exist, is revoked, or is expired | | 409 | `conflict` | Key was already rotated and is still within its grace period | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [List API Keys](/api-reference/administration/list-api-keys), view all keys * [Create API Key](/api-reference/administration/create-api-key), generate a new key * [Delete API Key](/api-reference/administration/delete-api-key), immediate revocation # Update API Key Source: https://docs.signa.so/api-reference/administration/update-api-key PATCH /v1/organization/api-keys/{id} Update an API key's name, scopes, expiry, or metadata ## Overview Updates an API key. The raw key value is never affected; use [Rotate API Key](/api-reference/administration/rotate-api-key) to issue new credentials. Metadata uses **merge semantics** (`null` values remove keys). Scope changes are subject to escalation prevention: you cannot grant scopes that the calling key does not already hold. System keys cannot be modified, and revoked or expired keys return `404`. Requires the `api-keys:manage` scope and an `Idempotency-Key` header; see [Idempotency](/api-reference/introduction#idempotency). ## Path Parameters API key ID (`key_...`). ## Request Body New name (1-255 characters). Replacement scope list (at least one). Subject to escalation prevention. New expiry as an ISO 8601 timestamp, or `null` to clear. Must be in the future. Metadata patch. Values may be strings or `null`. ## Response Returns the updated API key, with the same fields as [Retrieve API Key](/api-reference/administration/retrieve-api-key). ```json theme={null} { "id": "key_Mc2eF6gH", "object": "api_key", "name": "Aurora Digital production (renamed)", "prefix": "sig_7kMn2pQr", "scopes": ["trademarks:read"], "rate_limit_tier": "standard", "status": "active", "expires_at": null, "last_used_at": "2026-06-11T14:30:00.000Z", "metadata": { "owner": "platform-team" }, "revoked_at": null, "created_by": "key_Lb1dE5fG", "created_at": "2026-01-05T12:00:00.000Z", "updated_at": "2026-06-12T10:00:00.000Z", "request_id": "req_yR4iJ0kL" } ``` ## Code Examples ```bash cURL theme={null} curl -X PATCH "https://api.signa.so/v1/organization/api-keys/key_Mc2eF6gH" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: rename-key-Mc2eF6gH-2026-06-12" \ -d '{ "name": "Aurora Digital production (renamed)", "scopes": ["trademarks:read"], "metadata": { "owner": "platform-team" } }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const updated = await signa.organization.apiKeys.update("key_Mc2eF6gH", { name: "Aurora Digital production (renamed)", scopes: ["trademarks:read"], metadata: { owner: "platform-team" }, }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ---------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Empty name, unknown scope, `expires_at` not in the future, or missing `Idempotency-Key` header | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `api-keys:manage`, target key is a system key, or scope escalation attempted | | 404 | `not_found` | API key does not exist, belongs to another org, or is revoked or expired | ## Related Endpoints * [Retrieve API Key](/api-reference/administration/retrieve-api-key) * [Delete API Key](/api-reference/administration/delete-api-key) * [Rotate API Key](/api-reference/administration/rotate-api-key) # Classification Analytics Source: https://docs.signa.so/api-reference/analytics/classification-analytics GET /v1/analytics/classifications/{class} Use this endpoint for class-level filing intelligence, such as measuring how crowded Nice class 9 is across offices and over time. Nice class from 1 through 45. ## Code Examples ```bash cURL theme={null} curl https://api.signa.so/v1/analytics/classifications/9 \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const cls = await signa.analytics.classification(9); console.log(cls.total_marks, cls.active_count); ``` ## Response Nice class number. Marks classified in this class. Counts keyed by office code. Annual filing counts. ```json Response theme={null} { "object": "classification_analytics", "nice_class": 9, "total_marks": 950000, "active_count": 720000, "by_status": { "registered": 600000, "pending": 120000, "expired": 90000, "cancelled": 60000, "abandoned": 80000 }, "by_office": { "US": 700000, "EM": 250000 }, "filing_trend": { "2025": 82000 }, "computed_at": "2026-07-12T00:00:00.000Z", "request_id": "req_example" } ``` # Market Analytics Source: https://docs.signa.so/api-reference/analytics/market GET /v1/analytics/market Use this endpoint for market sizing and total addressable filing volume across every supported trademark office. Results are precomputed and suited to dashboards and recurring analysis. ## Code Examples ```bash cURL theme={null} curl https://api.signa.so/v1/analytics/market \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const market = await signa.analytics.market(); console.log(market.total_marks, market.active_count); ``` ## Response Total trademark records. Registered and pending marks. Counts by normalized status bucket. Counts keyed by uppercase ST.3 office code. Top ten Nice classes. Each `pct` is the share of all marks that include this class. Annual filing counts. ```json Response theme={null} { "object": "market_analytics", "total_marks": 12000000, "active_count": 8300000, "by_status": { "registered": 7000000, "pending": 1300000, "expired": 1800000, "cancelled": 900000, "abandoned": 1000000 }, "by_office": { "US": 8000000, "EM": 4000000 }, "top_classes": [ { "class": 9, "count": 950000, "pct": 0.079 } ], "filing_trend": { "2025": 610000 }, "computed_at": "2026-07-12T00:00:00.000Z", "request_id": "req_example" } ``` # Office Analytics Source: https://docs.signa.so/api-reference/analytics/office-analytics GET /v1/analytics/offices/{code} Use this endpoint to benchmark one office by registration rate, leading Nice classes, and yearly filing trend. Office codes are uppercase ST.3 (legacy lowercase codes are accepted as permanent aliases). Uppercase ST.3 office code, for example `US`. ## Code Examples ```bash cURL theme={null} curl https://api.signa.so/v1/analytics/offices/US \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const office = await signa.analytics.office("US"); console.log(office.total_marks, office.registration_rate); ``` ## Response Total marks at the office. Share of concluded matters that reached registration. Leading Nice classes. Annual filing and status counts. ```json Response theme={null} { "object": "office_analytics", "code": "US", "total_marks": 8000000, "registered_count": 4800000, "pending_count": 900000, "expired_count": 1200000, "cancelled_count": 500000, "abandoned_count": 600000, "registration_rate": 0.8125, "jurisdiction_count": 1, "earliest_filing": "1870-01-01", "latest_filing": "2026-07-11", "top_classes": [ { "class": 9, "count": 700000, "pct": 0.088 } ], "yearly_trend": [ { "year": 2025, "filed": 410000, "registered": 280000, "abandoned": 35000 } ], "stats_computed_at": "2026-07-12T00:00:00.000Z", "request_id": "req_example" } ``` # Search Citations Source: https://docs.signa.so/api-reference/citations/search-citations GET /v1/citations Query examiner refusal citations across all marks by office, disposition, stage, and date ## When To Use This Use this endpoint when the question spans marks rather than sitting on one record. Each row is one citation occurrence: one prior mark an examiner cited against one application in one office action. The corpus is a record of which marks an office has actually treated as a bar to registration, and what happened next. Typical uses: pull every §2(d) citation an office issued in a date window, find every application a specific registration has been cited against, or resolve an office-printed registration number to the mark behind it. For one mark at a time, use [Trademark Citations](/api-reference/trademarks/trademark-citations) and [Trademark Cited By](/api-reference/trademarks/trademark-cited-by). **Rows are citation occurrences, not applications.** One application cited over the same prior mark in both a nonfinal and a final office action produces two rows. To count distinct applications, group by `trademark.id`, falling back to `application_ref` when `trademark` is `null`. This endpoint returns no `total_count`, so a raw row count is both the easiest number to reach for and the easiest to get wrong: it overstates how many applications a mark has actually blocked. **Coverage today is USPTO only.** Citations are extracted from USPTO office actions and cover §2(d) likelihood-of-confusion refusals. Requests filtered to any other office return an empty list, including offices whose examiners never cite prior marks at all. Check `capabilities.citations` on [List Offices](/api-reference/reference/list-offices) to see the state of any office programmatically, rather than inferring coverage from an empty response. ## Freshness Citations are extracted hourly from stored office actions. Dispositions are recomputed once a day, so a newly extracted citation carries `disposition: null` until the next daily refresh, which can be up to about 24 hours. A disposition can also change on a later run as prosecution continues. `disposition_as_of` is stamped when the current disposition was first derived or last changed, and deliberately does not move when a daily run re-derives the same value. An unchanged timestamp means the disposition has been stable, not that the refresh stopped running. ## Office Coverage And `capabilities.citations` [List Offices](/api-reference/reference/list-offices) and [Retrieve Office](/api-reference/reference/retrieve-office) carry `capabilities.citations`, which is the programmatic answer to "should I expect anything here?". | Value | Meaning | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `available` | Live pipeline. Citations are being served for this office today. | | `in_progress` | A pipeline build is underway. Data is not complete yet. | | `not_available` | Signa serves no citations for this office. Either the office refuses on relative grounds and there is no pipeline yet, or its regime has not been classified. A gap that may close. | | `not_applicable` | The office does not refuse on relative grounds ex officio, so it never issues a citation-bearing refusal at all. | `not_applicable` covers the opposition-only registries, where earlier-rights conflicts are left to third parties rather than raised by the examiner. The EUIPO (`EM`), INPI France (`FR`), the Swiss IPI (`CH`) and the UKIPO (`GB`) all work this way, and the DPMA (`DE`) and the UIBM (`IT`) are classified the same way. WIPO (`WO`) is `not_applicable` for a narrower reason: the International Bureau does formalities only and never examines the mark, so substantive refusals come from the designated national offices under their own codes. An empty citation list for a `not_applicable` office is not a coverage gap, and it will not become non-empty. Those codes are illustrative, not a fixed list. `capabilities.citations` on [List Offices](/api-reference/reference/list-offices) is the authoritative, current answer for every office, and it is the one to read programmatically rather than hard-coding office codes from this page. ## Query Parameters All filters are optional and combine with AND. An unfiltered request is allowed and returns the newest citations across the corpus. Filter by issuing office, comma-separated (e.g. `US` or `US,EM`). Values are uppercase ST.3 office codes; legacy lowercase codes (e.g. `uspto`, and `eu` for EUIPO) are accepted as permanent aliases, and the two forms can be mixed in one list. Up to 100 values. An unrecognized code returns `200` with an empty list rather than an error, so polling for a not-yet-covered office is safe. Filter by how the citation resolved, comma-separated. One or more of `citation_issued`, `maintained`, `withdrawn`, `abandoned_after`, `published`. Rows whose disposition has not been computed yet are excluded when this filter is supplied. Filter by the stage the citation reached, comma-separated. One or both of `nonfinal`, `final`. `final` includes citations first raised in a nonfinal action that a later final action maintained. Filter by refusal ground, comma-separated. `2d` is the only value today, the USPTO §2(d) likelihood-of-confusion ground. This vocabulary is office-scoped rather than canonical across offices. Restrict to citations issued against one citing application (`tm_...`). Restrict to citations naming one prior mark (`tm_...`). This matches on the resolved link and additionally on that mark's own application and registration numbers for unlinked citations from the same office, the same predicate [Trademark Cited By](/api-reference/trademarks/trademark-cited-by) uses, so the two views return the same rows. Exact cited reference, as an application or registration number. Punctuation is ignored, digits are matched. **Requires `offices`**: references are only unique within an office, so an office-less lookup is rejected with `400`. Use this to go from a number an examiner printed to the citations that name it, including citations Signa has not linked to a record. Office action date >= (`YYYY-MM-DD`). Office action date \<= (`YYYY-MM-DD`). `-action_date` for newest first, `action_date` for oldest first. No other sort fields. Sort is bound into the pagination cursor, so it cannot be changed mid-walk. Page size (1-100). Opaque cursor from the previous response's `pagination.cursor`. Undated actions sort last in both directions, with `id` as the final tiebreaker. ## Response Always `list`. Citation ID (`cit_...`). Always `citation`. Office that issued the action, uppercase ST.3 (e.g. `US`). Application number the office action was issued against, as printed by the office. Summary of the citing application (`id`, `mark_text`, `office_code`, `application_number`, `registration_number`, `status_primary`). `null` when the citation carries no persisted link to a Signa record for that application, which is not the same as the application being absent from the register: resolution runs once, at extraction time. The prior mark reference the examiner cited. Whether `cited_ref` is a `registration` or an `application` number. This label is inferred from how the citing office action worded the reference, and it is occasionally wrong: a serial can be labelled as a registration, or the reverse. Signa matches the number against both namespaces regardless of the label, so a mislabelled reference still resolves. Treat it as a hint, and when `cited_trademark` is present read that as the authoritative identification of the cited mark. Summary of the cited prior mark, same fields as `trademark`. `null` means the citation is matched by reference only, with `cited_ref` as the sole identifier. Office-local refusal ground. `2d` is the USPTO §2(d) likelihood-of-confusion ground. ISO date of the office action (`YYYY-MM-DD`). `nonfinal` or `final`. This is the stage the citation reached, not provenance about `source_document_id`. A citation first raised in a nonfinal action reads `final` once a later dated final action maintained the refusal, so `final` does not mean the row was extracted from a final action. How the citation resolved. See the table below. `null` while pending the daily refresh. ISO 8601 timestamp of when the current `disposition` was first derived or last changed. It does not advance when a daily run re-derives the same value, so a stale-looking timestamp means the disposition has been stable. The office action the citation was extracted from (`med_...`), retrievable through [Trademark Documents](/api-reference/trademarks/records/documents) on the citing mark. Every citation served today carries one. The field is nullable so a citation derived without a stored source document can be represented. Documents are only addressable under a trademark, so when `trademark` is `null` there is no route that resolves this id. Whether more citations are available. Cursor for the next page. Unique request identifier for support and debugging. ### Disposition Values A citation carries exactly one disposition, derived from the citing application's prosecution events and from whether the citation reappeared in subsequent office actions. **The disposition is one verdict per (application, cited reference) pair.** It is computed across that pair's whole occurrence history and then written onto every occurrence row for the pair. It is not per row, and it is not per date. `action_stage` does vary from row to row, but it records the stage that occurrence of the citation reached, not the kind of document the row was extracted from: a citation first raised in a nonfinal action is upgraded to `final` once a later dated final action maintains the refusal, so a `final` row can still carry a nonfinal `source_document_id`. `disposition` is per pair and describes the citation's fate overall. So a row with `action_stage: nonfinal` alongside `disposition: maintained` is expected, not a data error. It means a different occurrence of this same citation, against the same application, reached a final action. Filter `action_stage=final` if you want the occurrences that reached final, remembering that those rows include upgraded ones whose `source_document_id` is a nonfinal action; read `disposition` from any row of the pair if you want the pair's outcome. `published` and `abandoned_after` are evaluated against the pair's earliest office action date rather than against the individual row's date, which is another reason every row of a pair carries the same answer. | Value | Meaning | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `published` | The application published or registered after the citation date. The citation did not stop it. | | `maintained` | The refusal reached a final office action. The citation held. | | `abandoned_after` | The application went abandoned after the citation date and was not revived. | | `withdrawn` | A later citation-bearing office action for the same application dropped this reference, with no publication yet. The examiner backed off. | | `citation_issued` | No later signal yet. The application is still in prosecution, or its event record does not yet support a stronger label. | | `null` | Not computed yet. Filled on the daily refresh. | When more than one signal applies, the strongest one wins, in this order: `published`, `maintained`, `abandoned_after`, `withdrawn`, `citation_issued`. ## Worked Example: Finding The Marks That Actually Block A clearance search tells you which registrations look similar. It does not tell you which ones an examiner has ever used to refuse somebody. This endpoint does, and the disposition filter is what separates the two. Ask for `disposition=maintained,abandoned_after`: ```bash theme={null} curl -G "https://api.signa.so/v1/citations" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "offices=US" \ --data-urlencode "disposition=maintained,abandoned_after" \ --data-urlencode "action_date_gte=2023-01-01" \ --data-urlencode "limit=100" ``` Those two values are the ones that matter, for different reasons: * `maintained` means the examiner did not back down. The refusal was carried into a final office action, so the office has committed to the position that this prior mark bars the application. It is the cleanest evidence that a mark is a live obstacle. * `abandoned_after` means the application died after being cited and was never revived. The applicant walked away rather than fight, which is the practical outcome a clearance search is trying to predict, even though the office never ruled. And the ones to leave out, deliberately: * `withdrawn` means the examiner dropped the citation in a later action. The prior mark was raised and then set aside, so it is weak evidence of blocking, and sometimes evidence of the opposite. * `published` means the application went on to publish or register anyway. The cited mark did not stop it. * `citation_issued` means prosecution has not produced a signal yet. Including it mixes unresolved cases into a set you are treating as resolved. To turn that into a per-mark verdict during clearance, run it from the cited mark rather than across the corpus. Take a candidate conflict from a search, then ask how that mark has behaved as a prior right: ```bash theme={null} curl -G "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/cited-by" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "disposition=maintained,abandoned_after" \ --data-urlencode "limit=100" ``` Count distinct applications, not rows. A row is one citation occurrence in one office action, so an application cited in both a nonfinal and a final action contributes two rows. Group by `trademark.id`, falling back to `application_ref` when `trademark` is `null`. A registration that blocked a dozen distinct applications in the last three years is an owner whose mark the office reaches for routinely. One with none, despite years on the register and plenty of similar filings around it, is a much softer conflict. Pair it with the unfiltered `cited-by` set to get a rate rather than a raw number: a mark cited against twenty applications where eighteen citations were `withdrawn` or `published` is not the same risk as a mark cited against five where all five stuck. Two caveats worth building around. Rows with `cited_trademark: null` are real citations that Signa could not link to a record, so count them; on the per-mark `cited-by` endpoint they are already matched by reference for you. And `abandoned_after` is a censored observation, not a ruling: the application stopped, and the citation is one plausible reason among several. ## Example Response ```json theme={null} { "object": "list", "data": [ { "id": "cit_0198c2f1-7e2a-7b34-9c11-3d5f8a2b4c6e", "object": "citation", "office_code": "US", "application_ref": "98123456", "trademark": { "id": "tm_019f34d6-2000-7777-8888-000000000001", "mark_text": "ACME BREW", "office_code": "US", "application_number": "98123456", "registration_number": null, "status_primary": "pending" }, "cited_ref": "5567890", "cited_ref_type": "registration", "cited_trademark": { "id": "tm_8kLm2nPq", "mark_text": "ACME", "office_code": "US", "application_number": "87999999", "registration_number": "5567890", "status_primary": "active" }, "refusal_type": "2d", "action_date": "2026-01-15", "action_stage": "final", "disposition": "maintained", "disposition_as_of": "2026-08-30T13:30:00.000Z", "source_document_id": "med_0198c2f1-7e2a-7b34-9c11-3d5f8a2b4c6e" } ], "has_more": true, "pagination": { "cursor": "eyJ2IjoxLCJ0IjoiY2l0YXRpb25zIn0" }, "request_id": "req_cV2nL8pQ" } ``` ## Code Examples ```bash cURL theme={null} curl -G "https://api.signa.so/v1/citations" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" \ --data-urlencode "offices=US" \ --data-urlencode "disposition=maintained,abandoned_after" \ --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 }); // Comma-separated on the wire; the SDK accepts an array and joins it. const citations = await signa.citations.list({ offices: "US", disposition: ["maintained", "abandoned_after"], limit: 20, }); for (const citation of citations.data) { console.log(citation.action_date, citation.cited_ref, citation.disposition); } ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/citations", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, params={ "offices": "US", "disposition": "maintained,abandoned_after", "limit": 20, }, ) ``` ### Resolve An Office-Printed Number `cited_ref` plus `offices` turns a number on an office action into the citations that name it, and into the mark behind it when Signa holds the record. ```bash cURL theme={null} curl -G "https://api.signa.so/v1/citations" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" \ --data-urlencode "offices=US" \ --data-urlencode "cited_ref=5567890" ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | `cited_ref` supplied without `offices`, blank `cited_ref`, `action_date_gte` later than `action_date_lte`, bad enum value, a malformed `trademark_id` / `cited_trademark_id`, or unknown query parameter | | 400 | `id_type_mismatch` | `trademark_id` or `cited_trademark_id` is a well-formed ID of another type (e.g. `own_...`) rather than a `tm_...` | | 400 | `cursor_expired` | Cursor is blank, expired, tampered with, or was issued for a different endpoint | | 400 | `cursor_invalid` | Cursor was issued under a different `sort` value. Restart pagination from the first page, or keep the `sort` the cursor was issued under | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 429 | `rate_limited` | Rate limit exceeded | An unrecognized `offices` value is not an error. It returns `200` with an empty list. ## Related Endpoints * [Trademark Citations](/api-reference/trademarks/trademark-citations): prior marks cited against one application * [Trademark Cited By](/api-reference/trademarks/trademark-cited-by): applications one mark was cited against * [List Offices](/api-reference/reference/list-offices): per-office `capabilities.citations` state * [Search Proceedings](/api-reference/proceedings/search-proceedings): contested proceedings, the third-party counterpart to examiner refusals * [Use Case: Trademark Clearance](/guides/use-cases/trademark-clearance): where citation history fits in a clearance workflow # Compare Marks Source: https://docs.signa.so/api-reference/compare/compare POST /v1/compare Score one candidate against up to 10 specific trademark conflicts (alpha, internal testing) **Alpha, internal testing.** Compare is not open to customers yet, and the risk bands are still being calibrated. These endpoints run on internal and design-partner keys during internal testing and their request and response shapes can still change. Email [support@signa.so](mailto:support@signa.so) to join the early access list. Compare returns an informational comparison, not legal advice or a registrability determination. ## Overview Screening **finds** possible conflicts for a candidate. Compare **scores** pairs you already chose: one candidate against 1–10 register records or inline marks. Each request-ordered result includes the same coarse risk-band vocabulary used by served screening, plus numeric string, phonetic, and goods/services measurements. Compare does not return an overall or confusion-probability score. The endpoint is synchronous and deterministic within the reported rule, mapping, resolver, and corpus versions. Each request is billed at **10 units**, regardless of the number of conflicts (up to 10). Optionally supply an `Idempotency-Key`; retrying the same body with the same key replays the original response without billing the comparison again. ## Request Supply the candidate either as an existing `trademark_id` or as an inline `{ mark, use }` object. Each conflict is either a `trademark_id` or an inline `{ mark, nice_classes, goods_services?, status? }` object. These alternatives are mutually exclusive. ```bash theme={null} curl --request POST https://api.signa.so/v1/compare \ --header "Authorization: Bearer $SIGNA_API_KEY" \ --header "Idempotency-Key: compare-kofee-001" \ --header "Content-Type: application/json" \ --data '{ "candidate": { "mark": "KOFEE", "use": { "nice_classes": [30, 43], "business_description": "Coffee beans and cafe services" } }, "conflicts": [ { "trademark_id": "tm_abc123" }, { "mark": "COFFEE HOUSE", "nice_classes": [30, 43], "goods_services": "Coffee and restaurant services", "status": "active" } ], "jurisdictions": ["US"], "offices": ["US"] }' ``` | Field | Type | Notes | | ------------------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `candidate` | object | **Required.** Exactly one of `{ trademark_id }` or `{ mark, use }`. Mark text is 2–200 characters. | | `candidate.use.nice_classes` | number\[] | Intended Nice classes, 1–45. | | `candidate.use.goods_services` | object\[] | Structured lines shaped as `{ nice_class?, text }`; text is at most 500 characters. | | `candidate.use.business_description` | string | Plain-English intended use, up to 2,000 characters. | | `conflicts` | object\[] | **Required.** 1–10 items. Each is exactly `{ trademark_id }` or an inline conflict. | | `conflicts[].nice_classes` | number\[] | Required for inline conflicts; at least one class, 1–45. | | `conflicts[].goods_services` | string | Optional goods/services text, up to 2,000 characters. | | `conflicts[].status` | string | Optional closed vocabulary: `active`, `pending`, `inactive`, or `unknown`; defaults to `unknown`. Unknown is treated as live so missing status never lowers risk. | | `jurisdictions` | string\[] | Optional ISO 3166-1 alpha-2 territory codes such as `US` and `EU`; trimmed, uppercased, maximum 30. | | `offices` | string\[] | Optional office codes such as `US` and `EM`; trimmed, maximum 10 characters each and 30 items. ST.3 codes and legacy internal codes (`uspto`, `euipo`) resolve to one canonical office identity and duplicates are collapsed; unrecognized tokens pass through as-is. | ## Response ```json theme={null} { "object": "list", "comparison": { "candidate": { "mark": "KOFEE", "normalized": "kofee", "effective_nice_classes": [30, 43] }, "rules_version": "compare-v1.1", "band_rules_version": "screening-v1", "mapping_version": "mark-search-v3", "resolver": { "engine_version": "resolver-v1", "ruleset_version": "rules-v1", "catalog_version": "nice-12" }, "as_of": "2026-07-13T00:00:00.000Z", "warnings": [], "disclaimer": "This comparison is informational, not legal advice or a registrability determination; the band table is under active recalibration." }, "data": [ { "object": "comparison_result", "comparison_status": "ok", "review_recommended": true, "warnings": [], "conflict": { "trademark_id": "tm_abc123", "mark": "COFFEE", "nice_classes": [30], "status_as_evaluated": "active", "record_as_of": "2026-07-13T00:00:00.000Z" }, "risk_level": "medium", "reason_codes": ["phonetic_mark", "same_nice_class"], "mark_match": { "level": "strong", "matched_levels": ["phonetic_strong"] }, "goods_services": { "level": "same_class", "overlap_classes": [30], "text_similarity": 0.8 }, "similarity": { "string": 0.67, "phonetic": 1, "semantic": null } } ], "summary": { "high": 0, "medium": 1, "low": 0, "none": 0, "not_comparable": 0, "review_recommended": 1 }, "has_more": false, "pagination": { "cursor": null }, "request_id": "req_…" } ``` `comparison_status`, risk levels, and match levels are closed vocabularies. `reason_codes`, `data[].warnings`, and `comparison.warnings` are open vocabularies; tolerate new values. `matched_levels` names engine clauses for the pair. Screening calls its corresponding post-sensitivity-gate field `admitted_match_levels`; Compare has no sensitivity admission gate. `similarity.semantic` is reserved and currently always `null`. `review_recommended` is `true` exactly when the comparison succeeded and at least one mark-similarity clause fired. It deliberately includes dead-mark and `below_screening_band` pairs: it is a conservative human-review triage bit, independent of `risk_level`. `summary.review_recommended` counts those results. An unknown conflict ID degrades to `not_found`. A conflict whose database record exists but cannot be compared, because it is design-only or its search document is still being indexed, degrades to `not_comparable`; index lag is reported in that item's `warnings`. The other batch items still succeed. The The five risk/status buckets in `summary` count every requested conflict exactly once; `review_recommended` is an additional, overlapping count. ### Error codes Compare can return `insufficient_use_profile` when the candidate's intended use cannot be resolved, `batch_too_large` when more than 10 conflicts are supplied, or `not_screenable` when a candidate trademark has no screenable mark text. ### Why can `risk_level` be null? An `ok` comparison has no band only when no mark-similarity clause fired. Those pairs count in `summary.none`: | Reason code | `risk_level` | Meaning | | ---------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `no_mark_similarity` | `null` | The marks did not match any meaningful name-similarity clause, so assigning a risk band would invent a signal. | | `below_screening_band` | `low` | A clause matched, but the pair's mark and goods/services combination falls below the served screening band table; review is still recommended. | ## Suggest → screen → compare Use classification suggestion to turn plain English into classes, screening to find candidates, then Compare to inspect a selected pair. This sequence uses `jq` to pass results between calls: ```bash theme={null} # 1. Turn the intended use into suggested Nice classes. curl --silent --request POST https://api.signa.so/v1/classifications/suggest \ --header "Authorization: Bearer $SIGNA_API_KEY" \ --header "Content-Type: application/json" \ --data '{"description":"Coffee beans and cafe services"}' > /tmp/signa-classes.json CLASSES=$(jq -r '[.classes[].class_number] | join(",")' /tmp/signa-classes.json) # 2. Find possible conflicts. curl --silent --get https://api.signa.so/v1/screening \ --header "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=KOFEE" \ --data-urlencode "nice_classes=$CLASSES" \ --data-urlencode "jurisdictions=US" > /tmp/signa-screen.json CONFLICT_ID=$(jq -r '.data[0].trademark.id' /tmp/signa-screen.json) CLASS_JSON=$(jq '[.classes[].class_number]' /tmp/signa-classes.json) # 3. Score the selected pair. jq -n --arg id "$CONFLICT_ID" --argjson classes "$CLASS_JSON" '{ candidate: { mark: "KOFEE", use: {nice_classes: $classes, business_description: "Coffee beans and cafe services"} }, conflicts: [{trademark_id: $id}], jurisdictions: ["US"] }' | curl --request POST https://api.signa.so/v1/compare \ --header "Authorization: Bearer $SIGNA_API_KEY" \ --header "Content-Type: application/json" \ --data-binary @- ``` ## Intent-to-use candidates You do not need to know the Nice system before comparing a proposed mark. Describe the planned business in ordinary language, call [Suggest Classifications](/api-reference/reference/suggest-classifications), review its ranked classes, then place those class numbers and the original description in `candidate.use`. Compare resolves that intent into the effective classes echoed in `comparison.candidate.effective_nice_classes` before scoring every pair. ## SDK ```typescript theme={null} const result = await signa.compare.compare({ candidate: { mark: 'KOFEE', use: { nice_classes: [30, 43], business_description: 'Coffee beans and cafe services', }, }, conflicts: [{ trademark_id: 'tm_abc123' }], jurisdictions: ['US'], }); console.log(result.data[0]?.risk_level, result.data[0]?.similarity); ``` ## Limitations * Compare v1 is text-only; it does not compare logos, designs, or images. * Numeric dimensions are measurements beneath the band, not a legal conclusion or an overall confusion probability. * The band table is under active recalibration. ## Compare vs Screening divergence FAQ Compare and [Screening](/api-reference/screening/screen) use the same mark and goods/services levels. Compare must never return a less alarming band for a pair that the equivalent broad screen surfaced. A few differences are expected because Screening searches a corpus while Compare evaluates a chosen pair. ### Why can Screening say `medium` when Compare says `high`? Screening can demote a weak or crowded mark using corpus-wide evidence that a pair-only comparison cannot see. The Screening hit then includes `weak_mark_demoted` or `crowded_field`. Compare deliberately gives the more conservative reading, and it can be only one band higher. ```bash theme={null} SCREEN=$(curl -sG https://api.signa.so/v1/screening \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=YOUR_MARK" --data-urlencode "nice_classes=30" \ --data-urlencode "sensitivity=broad" --data-urlencode "include=all") ID=$(jq -r '.data[0].trademark.id' <<<"$SCREEN") jq -n --arg id "$ID" '{candidate:{mark:"YOUR_MARK",use:{nice_classes:[30]}},conflicts:[{trademark_id:$id}]}' | curl -s https://api.signa.so/v1/compare -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Content-Type: application/json" --data-binary @- ``` **What to do:** treat Compare's higher band as the conservative pair reading; retain Screening's strength reason codes in your audit trail. ### Why does Compare score a pair that default Screening did not show? Compare evaluates every match tier. Screening defaults to `standard`, while a `fuzzy_2`-only match is admitted only at `broad` sensitivity. ```bash theme={null} curl -sG https://api.signa.so/v1/screening \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=QUORUM" --data-urlencode "nice_classes=9" \ --data-urlencode "sensitivity=broad" --data-urlencode "include=all" curl -s https://api.signa.so/v1/compare \ -H "Authorization: Bearer $SIGNA_API_KEY" -H "Content-Type: application/json" \ -d '{"candidate":{"mark":"QUORUM","use":{"nice_classes":[9]}},"conflicts":[{"mark":"QUXRUZ","nice_classes":[9]}]}' ``` **What to do:** use `sensitivity=broad&include=all` when validating Screening against Compare. ### Why does a Madrid-family result change when I add a jurisdiction? Madrid families can contain both live and dead territories. An unscoped call uses family-wide liveness; a scoped call evaluates only the requested territories. Passing a jurisdiction to one surface but not the other therefore changes the record state being evaluated. ```bash theme={null} curl -sG https://api.signa.so/v1/screening \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "trademark_id=$CANDIDATE_ID" \ --data-urlencode "jurisdictions=EU" --data-urlencode "sensitivity=broad" \ --data-urlencode "include=all" curl -s https://api.signa.so/v1/compare \ -H "Authorization: Bearer $SIGNA_API_KEY" -H "Content-Type: application/json" \ -d "{\"candidate\":{\"trademark_id\":\"$CANDIDATE_ID\"},\"conflicts\":[{\"trademark_id\":\"$CONFLICT_ID\"}],\"jurisdictions\":[\"EU\"]}" ``` **What to do:** pass the same `jurisdictions` (and `offices`, when used) to both calls. ### Why can Compare score an identical mark in an unrelated class? Supplying `nice_classes` to Screening is a retrieval hard filter, so a record outside those classes is never surfaced. Compare does not use class as a retrieval filter: it evaluates the supplied pair and an identical, live mark in an unrelated class can score `medium`. ```bash theme={null} curl -sG https://api.signa.so/v1/screening \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=CLASHSCOPE" --data-urlencode "nice_classes=30" \ --data-urlencode "sensitivity=broad" --data-urlencode "include=all" curl -s https://api.signa.so/v1/compare \ -H "Authorization: Bearer $SIGNA_API_KEY" -H "Content-Type: application/json" \ -d '{"candidate":{"mark":"CLASHSCOPE","use":{"nice_classes":[30]}},"conflicts":[{"mark":"CLASHSCOPE","nice_classes":[9]}]}' ``` **What to do:** omit the Screening class filter when you need discovery across all classes, then use Compare for pair-level class context. The response disclaimer is authoritative: **This comparison is informational, not legal advice or a registrability determination; the band table is under active recalibration.** # Errors Source: https://docs.signa.so/api-reference/errors Error response format, catalog, and idempotency Every Signa API error follows a consistent structure inspired by RFC 9457. Errors are wrapped under an `"error"` key with machine-readable fields for programmatic handling. ## Error Response Structure ```json theme={null} { "error": { "type": "not_found", "title": "Resource not found", "status": 404, "detail": "Trademark tm_xxx does not exist.", "instance": "/v1/trademarks/tm_xxx", "suggestion": "Check the trademark ID. Use GET /v1/trademarks?q=... to find marks by text.", "retryable": false, "retry_after": null }, "request_id": "req_abc123" } ``` | Field | Type | Description | | ------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------- | | `type` | string | Snake\_case error type slug (stable identifier for programmatic use, e.g. `not_found`, `validation_error`, `rate_limited`) | | `title` | string | Human-readable error title | | `status` | integer | HTTP status code | | `detail` | string | Specific error description for this instance | | `instance` | string | Request path that caused the error | | `suggestion` | string | Actionable fix guidance | | `retryable` | boolean | Whether the client should retry | | `retry_after` | integer or null | Seconds to wait before retry | | `request_id` | string | Unique request identifier for support (top-level, alongside `error`) | *** ## Error Catalog | Status | Type Slug | Retryable | Title | Example Message | Common Cause | Fix | | ------ | ------------------------- | --------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | No | Validation failed | "Parameter 'limit' must be between 1 and 100." | Invalid query parameter, missing required field, malformed JSON body | Check the parameter types and ranges. See the endpoint documentation for accepted values. | | 400 | `id_type_mismatch` | No | ID type mismatch | "Expected an owner ID (own\_\*) but received tm\_abc123." | Using a trademark ID where an owner ID is expected (or vice versa) | Use the correct ID prefix for the endpoint. Owner endpoints expect `own_*`, trademark endpoints expect `tm_*`, etc. | | 400 | `cursor_expired` | No | Cursor expired | "The pagination cursor has expired. Start a new query." | The cursor is blank, malformed, tampered with, issued for a different endpoint, or was signed under a key that has since been rotated. Cursors carry no expiry clock, so this is not an age limit | Start a fresh pagination session from the first page. Cursors have no fixed lifetime, so handle this on any page rather than planning around a window. | | 400 | `cursor_invalid` | No | Cursor query mismatch, or Cursor page size mismatch | "This cursor was generated for a different query, sort order, or filter set." | A cursor was replayed against a request that changed one of the parameters *that endpoint* binds into its cursor. Trademark search binds `sort`, `limit`, filters and query text; the citations endpoints bind only the endpoint and the sort direction. Check the endpoint's reference page for what its cursor binds | Restart pagination from the first page under the new parameters, or keep every parameter identical across pages. Branch on the `type` slug, never on `title`. Distinct from `cursor_expired`, which means the cursor is blank, stale, tampered with, or from another endpoint. | | 401 | `unauthorized` | No | Authentication required | "Invalid API key." | Missing `Authorization` header, invalid key format, expired key, revoked key | Check that you are sending `Authorization: Bearer sig_YOUR_KEY` with a valid key. Verify the key has not expired or been revoked. | | 403 | `forbidden` | No | Insufficient scope | "API key does not have the 'trademarks:read' scope." | The API key is valid but lacks the required scope for this endpoint | Create a new API key with the required scopes, or update the existing key's scopes via `PATCH /v1/organization/api-keys/{id}`. | | 403 | `plan_upgrade_required` | No | Plan upgrade required | "Credit packs is not available on your current plan. Upgrade to access this feature." | A billing feature your plan does not include: buying credit packs or enabling auto-top-up on the `free` plan. Note this is *not* how portfolio and watch caps surface: hitting one of those returns 409 `resource_quota_exceeded` | Upgrade your plan to gain access. Contact sales if you need this capability enabled. Not retryable: the response will not change without a plan change. | | 403 | `plan_gated` | No | Feature not on your plan | "The 'image\_search' capability is not included in your current plan." | The endpoint *type* (capability) is not in your plan's gating matrix at all, so neither quota nor rate limit apply; `endpoint_type` names it. Emitted identically by quota enforcement and by the rate limiter for a zero-RPM plan entry (for example `image_search`, `clearance`, `export` or `listing_screening` on `free`/`starter`) | Upgrade your plan. Not retryable: the response will not change without a plan change. Branch on `endpoint_type` to tell which capability is gated. | | 403 | `edge_rejected` | No | Request rejected by edge | "Request rejected by edge security rules." | The request matched an edge security rule before reaching the API (generated at the edge; `request_id` is `req_unknown`) | Check the request for patterns that resemble attacks (raw SQL fragments, script tags, path traversal). If a legitimate request is blocked, see [Edge-generated errors](#edge-generated-errors) below. | | 404 | `not_found` | No | Resource not found | "Trademark tm\_xxx does not exist." | The ID does not exist, was deleted, or belongs to a different resource type | Verify the ID is correct. Use search to find the resource if you do not have the exact ID. | | 409 | `conflict` | No | Conflict | "Idempotency-Key has been used with a different request target or body." | Reusing an `Idempotency-Key` header against a different URL or with a different request body | Generate a new unique idempotency key for each distinct request. If retrying the same request, use the same key AND the same URL AND the same body. | | 409 | `idempotency_processing` | Yes | Idempotency key in use | "A request with this idempotency key is currently being processed." | A retry with the same `Idempotency-Key` landed while the original request was still in flight | Wait briefly for the original request to finish, then retry with the same key to receive its cached result. | | 409 | `external_ref_conflict` | No | External reference conflict | "External reference "ACME-2024-001" is already bound to a different trademark in this portfolio." | An `external_ref` supplied when adding trademarks to a portfolio is already bound to a different trademark there, or the trademark already carries a different reference (references are immutable once set) | Choose a different `external_ref`, or remove the trademark from the portfolio and add it again to rebind. The whole add batch is rolled back, so nothing was written. | | 409 | `resource_quota_exceeded` | No | Resource quota exceeded | "Your plan allows 3 portfolios. Delete an existing portfolio or upgrade your plan to add more." | The organization is at a per-plan resource cap: portfolios, distinct managed marks across all portfolios, or watches. Distinct from the 429 `quota_exceeded`, which is the monthly per-request counter | Free a slot (delete a resource, or remove marks from portfolios) or upgrade your plan. Branch on `quota_scope` (`portfolios`, `managed_marks`, `watches`) and read `quota_limit`, `quota_used` and, on batch writes, `quota_attempted` to decide how many to retry with. Batch writes roll back entirely, so nothing was written. | | 409 | `portfolio_in_use` | No | Portfolio in use | "Delete or un-scope the referencing webhook endpoints before deleting this portfolio: whe\_abc123." | `DELETE /v1/portfolios/{id}` was called while webhook endpoints are still scoped to that portfolio | Delete the endpoints listed in `endpoint_ids`, or clear their `portfolio_id`, then retry the deletion. | | 410 | `entity_merged` | No | Entity merged | "Owner own\_old123 has been merged into own\_abc123." | The owner/attorney was deduplicated, or a resolved entity (`ent_*`) was fused into another. The old ID is permanently redirected. | Follow the `merged_into` field in the error response to the canonical entity. Update any cached references. | | 413 | `payload_too_large` | No | Payload too large | "Request body exceeds the 8 KB limit for this endpoint." | The request body exceeded a size limit. Bodies over 8 KB are rejected at the edge (`request_id` is `req_unknown`) on every endpoint except image upload (`POST /v1/trademarks/search/image`), which has its own larger API-enforced limit. 8 KB is the effective request body ceiling everywhere else. | Reduce the body size. For bulk lookups, use the batch endpoints within their documented limits. | | 422 | `entity_too_large` | No | Entity too large | "This entity spans 12,431 member owners. Listing trademarks across members is limited to entities with 10,000 member owners or fewer." | An `entity_id` / `entity_group` filter, or `GET /v1/entities/{id}/trademarks`, hit a cap. | Narrow the query, or query a specific member via `GET /v1/owners/{id}/trademarks`. Not retryable as-is. | | 422 | `portfolio_too_large` | No | Portfolio too large for deadline computation | "This portfolio has 6214 marks. Deadline computation is limited to portfolios with 5,000 marks or fewer. Split it into smaller portfolios and request deadlines for each." | A portfolio holding more than 5,000 marks was sent to `GET /v1/portfolios/{id}/deadlines` or `GET /v1/portfolios/{id}/deadlines.ics`, both of which compute deadlines across every mark | Split the portfolio into smaller ones and request each separately. Narrowing `due_before` or `type` does not help, because both filters are applied after computation, and the calendar download enforces the identical cap. | | 429 | `rate_limited` | Yes | Rate limit exceeded | "Rate limit exceeded. Retry after 30 seconds." | Too many requests within the rate limit window | Wait for `retry_after` seconds, then retry. See [Rate Limits](/api-reference/rate-limits). | | 500 | `internal_error` | Yes | Internal server error | "An unexpected error occurred." | Server-side bug or transient failure | Retry with exponential backoff. If the error persists, report it via [`POST /v1/feedback`](/api-reference/administration/create-feedback) with the response's `request_id` (or email [support@signa.so](mailto:support@signa.so)). | | 502 | `upstream_error` | Yes | Bad gateway | "The upstream service failed to respond." | A backend dependency or office connector returned an invalid or failed response | Retry shortly with backoff. If it persists, report it via [`POST /v1/feedback`](/api-reference/administration/create-feedback) with the response's `request_id` (or email [support@signa.so](mailto:support@signa.so)). | | 503 | `service_unavailable` | Yes | Service unavailable | "Database is temporarily unavailable." | A backend dependency is down | Wait for `retry_after` seconds, then retry. If the errors persist, report it via [`POST /v1/feedback`](/api-reference/administration/create-feedback) with the `request_id` (or email [support@signa.so](mailto:support@signa.so)). | | 504 | `timeout` | Yes | Query timeout | "The request took too long to process. Please retry or narrow your filters." | A database query exceeded the server-side statement timeout because the filters were too broad or the result set too large | Retry, or narrow your filters to reduce work. If it persists, report it via [`POST /v1/feedback`](/api-reference/administration/create-feedback) with the response's `request_id` (or email [support@signa.so](mailto:support@signa.so)). | | 504 | `preview_timeout` | No | Preview Timeout | "The preview could not produce a result within the server-side time budget." | A `POST /v1/watches/preview` query was too heavy to evaluate within the roughly 20 second budget | Do not blind-retry (`retryable: false`). Narrow the query: add office or jurisdiction filters, reduce `trial_window_days`, or scope `filters.trademarkIds`. See [Preview Watch](/api-reference/monitoring/watches/preview). | `entity_too_large` carries an `error.reason` discriminator so you can branch without parsing the message: `member_owners_too_large` means the entity or group resolved to more member owners than the cap (the error also carries `member_count` and `member_count_limit`), and `family_graph_too_large` means the GLEIF family-graph walk exceeded its node or depth bound before owners were counted, which only fires on `?entity_group=` (the error carries `related_entity_limit` and `depth_limit`). Either way, narrow the query with additional filters, query a specific member directly, or use `?entity_id=` for a single entity instead of `?entity_group=`; the caps bound fan-out cost and are not retryable as-is. *** ## Edge-generated errors A small set of errors is generated at the edge (CDN and security layer), before the request reaches the API. They use the same JSON envelope, with three differences: * `request_id` is a static sentinel: the edge cannot mint per-request IDs. Security blocks, oversized payloads, and edge rate limits use `req_unknown`; the static `502`/`503` pages served when the API origin is unreachable use `req_edge_static_502` and `req_edge_static_503`. * The `instance` and `suggestion` fields are omitted: the edge cannot know them for your specific request. * To correlate an edge error with support, quote the `x-amz-cf-id` response header and the timestamp of the request instead of a `request_id`. Edge-generated errors you may see: `edge_rejected` (403), `payload_too_large` (413, for bodies over 8 KB on any endpoint except image upload), `rate_limited` (429, when the edge rate limit trips before the API's own limiter), and static `bad_gateway` (502) / `service_unavailable` (503) pages when the API origin is unreachable. **A few edge cases return HTML, not JSON.** A request URI longer than the CDN's length cap returns `414` with an HTML body. Oversized or malformed request headers return `400` with an HTML body (from the load balancer), or `494` with an HTML body when they exceed the CDN's larger header cap. These responses come from infrastructure request parsers and cannot be converted to the JSON envelope. If you hit one with a request you believe is valid, email [support@signa.so](mailto:support@signa.so) with the `x-amz-cf-id` response header (when present) and the timestamp. *** ## Handling Errors Errors return non-2xx status codes. The response body contains a structured `error` object and a top-level `request_id`, with the HTML exceptions noted in [Edge-generated errors](#edge-generated-errors) above (oversized URIs return `414`, and oversized or malformed headers return `400` or `494`, all with HTML bodies). Robust clients check the `Content-Type` header before parsing the body as JSON. ```bash theme={null} curl -s -w "\nHTTP Status: %{http_code}\n" \ https://api.signa.so/v1/trademarks/tm_nonexistent \ -H "Authorization: Bearer sig_YOUR_KEY" ``` The TypeScript SDK exposes typed error classes for each status code. See the [TypeScript SDK](/sdk/typescript#error-handling) reference for `instanceof` patterns and automatic retries. ### Reporting a persistent server error Server faults (`500 internal_error`, `502 upstream_error`, `503 service_unavailable`, `504 timeout`) carry a `suggestion` that points you at the feedback channel. If retrying with backoff does not clear the error, submit it to [`POST /v1/feedback`](/api-reference/administration/create-feedback) with the response's `request_id`. Signa snapshots the matching request log onto the report, so the team can investigate exactly what you hit. Client errors (`4xx`) never carry this hint, they are fixed on your side by correcting the request. See [Submit Feedback](/api-reference/administration/create-feedback) for the full flow. *** ## Idempotency and `409 Conflict` Mutating requests (`PATCH`, `DELETE`, and non-exempt `POST`) require an `Idempotency-Key` header (1-255 characters of `[A-Za-z0-9_-]`): ``` PATCH /v1/organization/api-keys/key_abc123 Idempotency-Key: rename-prod-key-2026-03-24-001 ``` A key is bound to the exact request it first executed: the request URL **and** the request body. **Same key + same URL + same body** = cached response replayed (safe to retry). Replays carry an `Idempotent-Replayed: true` response header and preserve the original `request_id`. **Same key + different body, or the same key against a different URL** = `409 conflict` error. Reusing one key across `DELETE /v1/watches/wat_1` and `DELETE /v1/watches/wat_2` conflicts rather than replaying the first delete — generate a fresh key for each distinct request. **Same key while the first request is still in flight** = `409 idempotency_processing`: wait for the original to finish, then retry. Idempotency keys are scoped to your organization and expire after 24 hours. Only successful (2xx) responses are cached, so a failed request never pins you to its error. ```bash theme={null} curl -s -X PATCH https://api.signa.so/v1/organization/api-keys/key_abc123 \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: my-unique-key-123" \ -d '{"name": "Production key (renamed)"}' ``` A handful of read-shaped POST endpoints are **exempt by design**: they exist as POSTs only because their query bodies are too large or complex for a URL, and they create nothing: * `POST /v1/trademarks` (search) * `POST /v1/trademarks/batch` (batch lookup) * `POST /v1/classifications/suggest` and `POST /v1/goods-services/suggest` (read-shaped suggestions) * `POST /v1/watches/preview` (dry-run match count) * `POST /v1/alerts/lookup` (bulk read by IDs) On exempt endpoints the header is optional and not replayed, but if you do send one, its **format is still validated** (1-255 characters of `[A-Za-z0-9_-]`); a malformed value returns `400` even there. `POST /v1/organization/api-keys` and `POST /v1/organization/api-keys/{id}/rotate` are **not** exempt. Replay is the correct semantic for endpoints that mint one-time secrets: retrying the same `Idempotency-Key` returns the **same** secret instead of minting a second credential. See [Resilience and Retries → Idempotent Requests](/guides/resilience#idempotent-requests) for the full exemption list and a worked retry example. *** ## Troubleshooting by Symptom **Check your Authorization header format.** It must be `Authorization: Bearer sig_YOUR_KEY` (with the `Bearer` prefix). Common mistakes: * Missing `Bearer` prefix: `Authorization: sig_YOUR_KEY` * Wrong key prefix: only keys of the form `sig_{48 hex chars}` are accepted. Any other prefix is rejected with 401 before any database lookup. * Expired key: check `expires_at` on your API key * Revoked key: check the API key status in your dashboard Your API key is valid but does not have the required scope for this endpoint. Each endpoint requires specific scopes: * Trademark search, list, and entity GET endpoints need `trademarks:read` * The event feed (`/v1/events`) needs `events:read` * Watches, alerts, webhooks, and portfolios need `portfolios:manage` * API key management needs `api-keys:manage` * Usage, plan, and log endpoints need `billing:read` Create a new key with the needed scopes or update your existing key. Common causes: * **Missing required filter:** `GET /v1/trademarks` requires at least one filter (no unscoped list queries). * **Wrong date format:** Use ISO 8601 (`2026-03-24` or `2026-03-24T12:00:00Z`). * **Array syntax:** Bracketed keys (e.g. `offices[]=US`) are not accepted; repeated keys (e.g. `offices=US&offices=EM`) are accepted as a fallback, but the canonical form is comma-separated (`offices=US,EM`). * **Date range syntax:** Use flat underscore operators, not brackets: `?filing_date_gte=2020-01-01&filing_date_lt=2025-01-01`. * **Boolean values:** Only the literal strings `true` and `false` are accepted. `1`, `0`, `yes`, `no`, and `TRUE` all return a validation error. * **application\_number without office:** When filtering by `application_number` or `registration_number`, you must also specify `office`. * **Missing Idempotency-Key:** Mutating requests (`PATCH`, `DELETE`, and non-exempt `POST`s) require this header. Read-shaped POSTs (search, batch, suggest, watch preview, alert lookup) do not. Entity resolution runs periodically. When two owner records are identified as the same entity, one is merged into the other. The `410` response includes a `merged_into` field pointing to the canonical record. Update your cached ID to the new one. All trademarks previously associated with the old owner are now under the canonical record. **Resolved entities (`ent_*`) can also 410.** When two resolved entities are fused, the loser's `ent_` id returns `410 entity_merged` with the successor in `merged_into`: follow that field to the canonical record. An entity id for an owner that has since been linked to a company profile does **not** 410; it transparently resolves to the real entity (the returned `id` may differ from the one you requested). This is expected behavior due to the eventual consistency model. Search results are eventually consistent (typically under 30 seconds lag after a write), while detail endpoints are immediately consistent. If you just updated a record, wait a few seconds and retry the search. For time-sensitive workflows, use the detail endpoint as the source of truth. For bulk lookups, use [Batch Retrieve](/api-reference/trademarks/batch-trademarks) (up to 100 IDs per request) instead of individual GET requests. This counts as one request against your rate limit. For search-heavy workloads, consider caching results on your side, using cursor pagination instead of re-executing searches, or upgrading your plan. This indicates a transient backend issue. The error is retryable: wait for the `retry_after` period and retry with exponential backoff. If 503 errors persist for more than a few minutes, email [support@signa.so](mailto:support@signa.so) with a `request_id` from one of the failed calls. # Estimate Fees Source: https://docs.signa.so/api-reference/fees/estimate POST /v1/fees/estimate Estimate official fees for one transaction and identify unresolved components Submit one transaction per request. The response is a `fee_estimate` object at the top level, with no `data` wrapper. Requires `trademarks:read`, costs **one check unit per request**, and does not require an `Idempotency-Key`. The endpoint does not persist a filing or take payment. ```typescript theme={null} const estimate = await signa.fees.estimate({ office: 'US', route: 'direct', action: 'filing', classes: [9, 42], }); console.log(estimate.complete, estimate.total_minor, estimate.currency); ``` `office` takes `US`, `EM`, `GB`, or `WO`. The platform office codes `USPTO`, `EUIPO`, `UKIPO`, and `WIPO`, and the `EU` alias for `EM`, are accepted as aliases for them, in any case, and the response echoes the resolved code. Designated offices in `designations[]` resolve the same way, so two spellings of one office are rejected as a duplicate rather than counted twice. Any other office returns an incomplete estimate rather than a guessed amount. `classes` accepts an integer count from 1 to 45 or a unique list of Nice class numbers from 1 to 45. Duplicate classes, unknown fields, invalid calendar dates, and batch envelopes are rejected. All amounts are native currency minor units, without currency conversion. `subtotal_minor` sums included components. **`total_minor` is null unless the estimate is complete.** Read `lines[]`, `omissions[]`, and `assumptions[]` before using the amount. A successful HTTP response can contain unsupported offices, unsupported actions, missing inputs, unsupported dates, or unverified components; these are explicit line statuses rather than guessed charges or zeros. Each included line identifies its fee rule and schedule, effective and verification dates, and source citations with snapshot paths and SHA-256 checksums. `dataset_version` identifies the compiled dataset, and `as_of` is the selected `payment_date`, defaulting to the server's UTC calculation date. The response uses `private, no-store` caching. ## Madrid requests Use `office: 'WO'` and identify designated offices separately: ```typescript theme={null} const estimate = await signa.fees.estimate({ office: 'WO', route: 'madrid', action: 'filing', classes: [9, 42], designations: ['EM', 'GB'], colour: false, applicant: { ldc_eligible: false }, }); ``` The verified dataset does not currently include individual designation fee amounts. This example therefore returns a partial subtotal for verified components, unresolved designation lines, and a null total. Do not treat the Madrid basic fee as the full application cost. `designations` is valid only for WO Madrid filing, renewal, or subsequent designation requests. `colour` and `applicant` are valid only for WO Madrid filing. Set `applicant.ldc_eligible: true` only when the caller attests that **all predicates in the WIPO least-developed-country reduction footnote hold for every applicant**. This is an eligibility attestation, not a country-code shortcut. ## Dates and transition rules Use `payment_date` to supply the payment date; omission uses the server calculation date. GB renewal requests can supply `renewal_due_date`. GB filing requests can supply `application_date` and `application_in_payment_grace`. Missing facts needed to select a schedule are reported explicitly. Dates older than verified applicability coverage return unsupported components rather than extrapolated fees. `late` requests a supported late-payment calculation where the dataset contains verified rules. ```typescript theme={null} const estimate = await signa.fees.estimate({ office: 'GB', route: 'direct', action: 'renewal', classes: 2, payment_date: '2026-03-31', renewal_due_date: '2026-04-01', }); ``` # List Fees Source: https://docs.signa.so/api-reference/fees/list GET /v1/fees Browse verified official fees with native currency amounts and source snapshots Returns the complete, bounded fee catalog, optionally filtered by `office` and `action`. The response is a flat list with `has_more: false` and a null pagination cursor. This is unbilled reference data and requires `trademarks:read`. ```typescript theme={null} const fees = await signa.fees.list({ office: 'US', action: 'filing' }); for (const fee of fees.data) { console.log(fee.fee_id, fee.amount_minor, fee.currency); } ``` Office codes are `US`, `EM`, `GB`, and `WO`. The platform office codes `USPTO`, `EUIPO`, `UKIPO`, and `WIPO`, and the `EU` alias for `EM`, are accepted as aliases for them, in any case. Unsupported offices return an empty catalog. Catalog rows describe verified components, not guaranteed complete transaction costs. Use [Estimate Fees](/api-reference/fees/estimate) to resolve class counts, dates, routes, and omissions. Amounts use **native currency minor units**: `35000` USD means USD 350.00. No currency conversion is applied. Each row includes its fee schedule, effective dates, verification date and expiry, applicability conditions, and `sources[]` containing the source URL, citation, quote, snapshot path, and SHA-256 checksum. A null amount means the row describes a conditional formula or linked fee rather than a fixed amount. A null `classes_included_in_base` means the class allowance is not established or is not relevant; it must not be interpreted as zero included classes. The catalog supports `ETag` and `If-None-Match`; unchanged requests return `304`. Publicly cacheable catalog responses omit `request_id` from the body. Estimate responses include it. # Health Check Source: https://docs.signa.so/api-reference/health GET /v1/health Unauthenticated liveness and readiness endpoints for monitoring Signa from your side ## Overview Three unauthenticated endpoints support customer-side uptime monitoring: | Endpoint | Purpose | Typical latency | | ------------------- | ----------------------------------------------------------- | --------------- | | `GET /v1/health` | Liveness alias under the `/v1` prefix. | Under 5 ms | | `GET /health/live` | Liveness: the process is up. | Under 5 ms | | `GET /health/ready` | Readiness: database, cache, and search index are reachable. | 50-200 ms | `/v1/health` and `/health/live` are aliases: both return `{ "status": "ok" }` with HTTP 200 whenever the API process is alive. `/health/ready` runs three real dependency checks and returns per-dependency status and latency. None of the three require an API key or count against any quota or rate limit. ## Suggested poll interval **60 seconds.** More frequent polling adds noise without improving signal. ## Response: `GET /v1/health` (and `/health/live`) ```json theme={null} { "status": "ok" } ``` Always 200 if the process is running. If the process is down you get a connection error or a 5xx from the load balancer instead. ## Response: `GET /health/ready` One of: * `ok`: all three dependencies reachable. * `degraded`: a non-critical dependency (cache or search index) is down. The API still serves most requests; some features (search, rate-limit headers) may error individually. * `unhealthy`: the database is unreachable. The API cannot serve most requests. Returns HTTP 503. * `shutting_down`: the task is draining for a graceful stop. Returns HTTP 503 so load balancers route around it. Always `"core-api"`. Process uptime in milliseconds. `ok` or `error`. Probe latency in ms (omitted on error). `ok` or `error`. Probe latency in ms (omitted on error). `ok` or `error`. Probe latency in ms (omitted on error). ## What this endpoint does not cover `/health/ready` reflects API, database, cache, and search readiness. It does not reflect: * **Alert delivery.** If your watches stop firing alerts but `/health/ready` is `ok`, that's a sign of a data-sync or evaluation issue, not an API outage. See [Troubleshooting](/guides/monitoring/troubleshooting). * **A specific office's data freshness** (e.g. USPTO). Poll [`GET /v1/offices`](/api-reference/reference/list-offices) and check each office's last successful sync instead. * **Your webhook receiver.** That's on your side by definition. ## Examples ```bash cURL theme={null} # Liveness alias under /v1 curl https://api.signa.so/v1/health # -> {"status":"ok"} # Readiness: what your monitoring agent should hit every 60s curl https://api.signa.so/health/ready # -> {"status":"ok","service":"core-api","uptime_ms":12345,"dependencies":{...}} ``` ```ts TypeScript theme={null} // No SDK helper for this: these endpoints are unauthenticated, hit them with fetch. const res = await fetch('https://api.signa.so/health/ready'); const ready = await res.json(); if (ready.status !== 'ok') { alert(`Signa API ${ready.status}: postgres=${ready.dependencies.postgres.status}`); } ``` ## See also * [Troubleshooting](/guides/monitoring/troubleshooting): what to check when alerts stop firing despite `/health/ready` being `ok`. * [List Offices](/api-reference/reference/list-offices): per-office sync freshness. # Introduction Source: https://docs.signa.so/api-reference/introduction Base URL, authentication, request and response format The Signa API is a RESTful JSON API for trademark search, retrieval, and reference data. All endpoints are versioned under `/v1/`. Signa is in beta. Response shapes are stable within `/v1/`, but may still evolve without a major version bump while we finish the initial rollout. ## Base URL ``` https://api.signa.so/v1 ``` ## Authentication All API requests require a Bearer token in the `Authorization` header: ```bash theme={null} Authorization: Bearer sig_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` API keys use the format `sig_{48 hex chars}` and carry one or more scopes (e.g. `trademarks:read`, `portfolios:manage`) that control what they can access. Keys are meant for server-to-server use: make calls from your backend, never from client-side code a user can inspect. See [Authentication & Keys](/guides/authentication) for creating, rotating, and scoping keys. ## Request Format * **Content-Type:** `application/json` for request bodies * **Query params:** `snake_case` (e.g., `?status_stage=registered`) * **Arrays:** comma-separated values (e.g., `?jurisdictions=US,EU`) * **Date ranges:** flat underscore operators (e.g., `?filing_date_gte=2020-01-01&filing_date_lt=2025-01-01`) * **Booleans:** literal strings `true` or `false` (values like `1`, `yes`, or `TRUE` are rejected) * **Dates:** ISO 8601 (`2026-03-19T12:00:00Z`) or date-only (`2026-03-19`) ## Response Format Single-resource endpoints return the resource at the top level: ```json theme={null} { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "SIGNA", "request_id": "req_abc123" } ``` List endpoints wrap results in a standard envelope with `has_more` at the top level and a `pagination` object: ```json theme={null} { "object": "list", "data": [...], "has_more": true, "pagination": { "cursor": "eyJ..." }, "request_id": "req_abc123" } ``` Every response includes a top-level `request_id`. Include it when contacting support. ## Status Codes | Code | Description | | ----- | -------------------------------------------------------------------------------------------- | | `200` | Success | | `201` | Created | | `400` | Bad request (validation error) | | `401` | Unauthorized (missing or invalid API key) | | `403` | Forbidden (insufficient scopes) | | `404` | Not found | | `409` | Conflict (idempotency key reused with a different body, or already in progress) | | `410` | Gone (record merged into another; see [Errors](/api-reference/errors)) | | `422` | Unprocessable (request is well-formed but exceeds a processing limit, e.g. entity too large) | | `429` | Rate limited | | `500` | Internal server error | | `503` | Service unavailable | | `504` | Gateway timeout (a bounded server-side computation exceeded its time budget) | ## Idempotency Mutating requests (`POST`, `PATCH`, `DELETE` that create or change a resource) require an `Idempotency-Key` header: any unique string, 1 to 255 characters of letters, numbers, dashes, and underscores. ``` POST /v1/organization/api-keys Idempotency-Key: create-prod-key-2026-03-24-001 ``` * **Same key, same URL, same body:** the cached response is replayed (safe to retry). * **Same key with a different request body, or against a different URL:** `409 conflict`. A key is bound to the exact request it first ran, so reusing one key across `DELETE /v1/watches/wat_1` and `DELETE /v1/watches/wat_2` is a conflict, not a replay. Generate a fresh key per distinct request. * **Same key while the original request is still in flight:** `409 idempotency_processing`; retry shortly. Keys are scoped to your organization and expire after 24 hours. A handful of read-shaped `POST` endpoints (search, batch lookup, suggest, watch preview, alert lookup) don't create anything and are exempt: the header is optional there, though its format is still validated if you send one. See [Errors → Idempotency and 409 Conflict](/api-reference/errors#idempotency-and-409-conflict) for the full exemption list. ## Next Steps Cursor-based iteration with stable ordering. Monthly quota pools, per-minute limits, headers, and 429 handling. RFC 9457-inspired error format and catalog. # List Alerts Source: https://docs.signa.so/api-reference/monitoring/alerts/list GET /v1/alerts List alerts for the calling org with cursor pagination ## Overview Returns alerts produced by any of the org's watches. Alerts are immutable; there's no `PATCH` or batch state operation. For polling clients, the recommended pattern is: 1. Pull pages (newest first) until you reach an `id` you've already processed. 2. Persist alert IDs in your own store as the system of record for "open work" or "acknowledged." Requires the `portfolios:manage` scope. ## Query Parameters Page size (1-100). Opaque cursor from the previous response. Filter by `normal`, `high`, or `critical`. Filter by `trademark.created`, `trademark.updated`, `trademark.status_changed`, `trademark.retracted`, or `trademark.corrected`. `all` (default) returns alerts from every evaluation revision of a watch; `current` returns only alerts emitted under each watch's current query. ## Response Always `"list"`. Array of `Alert` objects. Alert ID (`alt_*`). Always `"alert"`. Wire schema version, e.g. `"2026-06-01"`. Source watch summary: `id` (`wat_*`), `name`, `type`. Passthrough value set on the watch's metadata; `null` if unset. One of the five event types above. Short human-readable description, e.g. `"Status changed: pending → registered"`. Field-level change entries: `path`, `op` (`set`, `unset`, or `changed`), and `from` / `to` for parent fields. `path` is a **public trademark field name** — `status`, `mark_text`, `publication_date`, `ir_number`, or a changed child collection (`owners`, `classifications`, `media`, …). Status is flattened: `status.primary` is `status`, the rest are `status_`. Fields Signa does not publish are omitted rather than renamed, so every `path` resolves against [the trademark resource](/api-reference/trademarks/retrieve-trademark). See [Webhooks: diff field names](/guides/monitoring/webhooks#diff-field-names). Present and `true` when the diff was too large and some entries were dropped or clamped. Absent otherwise. Why the trademark matched, for watches that score matches. `null` for pure-filter watches. Match explanation. Similarity score, when applicable. How the score was computed. Snapshot of the trademark at the time of the alert. Trademark ID (`tm_*`). Word mark. Mark feature type. Issuing office. `primary` and `stage`. ISO date. ISO date. Nice classes on file at alert time. Owner name at alert time. When the underlying change occurred. `self`: link to the full trademark record. `normal`, `high`, or `critical`. One of `open`, `closing_soon`, `critical`, `closed`. Computed action deadline (ISO date). `occurred_at`, `ingested_at` (omitted for older alerts with no linked change), `created_at`. `trademark` and `watch`: links to the full resources. The watch's evaluation revision this alert was emitted under. Whether more pages exist. Cursor envelope. Request identifier. ## Errors | Status | `type` | When | | ------ | ------------------ | --------------------------------------------------------------- | | 400 | `validation_error` | Invalid `severity`, `event_type`, `epoch`, `limit`, or `cursor` | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/alerts?severity=critical&limit=50" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const alerts = await signa.alerts.list({ severity: "critical", limit: 50 }); for await (const a of alerts) { if (a.deadline.severity === "critical") console.log("escalate:", a.id); } ``` ```json theme={null} { "object": "list", "data": [ { "id": "alt_4tYpL2Qn", "object": "alert", "schema_version": "2026-06-01", "watch": { "id": "wat_8kLm2nPq", "name": "Nike owner watch", "type": "owner" }, "customer_reference": null, "event": { "type": "trademark.status_changed", "summary": "Status stage changed: published → registered", "diff": [ { "path": "status_stage", "op": "changed", "from": "published", "to": "registered" } ] }, "match": null, "trademark": { "id": "tm_9vXq3Rmt", "mark_text": "NIKE", "mark_feature_type": "word", "office_code": "US", "status": { "primary": "active", "stage": "registered" }, "filing_date": "2024-02-01", "registration_date": "2026-07-01", "nice_classes": [25, 28], "owner_name": "Nike, Inc.", "as_of": "2026-07-05T09:10:00.000Z", "links": { "self": "/v1/trademarks/tm_9vXq3Rmt" } }, "deadline": { "severity": "high", "opposition_window_status": "open", "must_act_by": "2026-09-04" }, "timestamps": { "occurred_at": "2026-07-05T09:10:00.000Z", "ingested_at": "2026-07-05T09:12:00.000Z", "created_at": "2026-07-05T09:12:30.000Z" }, "links": { "trademark": "/v1/trademarks/tm_9vXq3Rmt", "watch": "/v1/watches/wat_8kLm2nPq" }, "evaluation_epoch": 0 } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_2mR8vNkT" } ``` ## Related Endpoints * [Retrieve Alert](/api-reference/monitoring/alerts/retrieve) - fetch a single alert * [Lookup Alerts](/api-reference/monitoring/alerts/lookup) - bulk-fetch alerts by ID * [List Alerts for Watch](/api-reference/monitoring/alerts/list-for-watch) - alerts scoped to one watch * [Watch Diagnostics](/api-reference/monitoring/watches/diagnostics) - explain why a specific trademark did or didn't alert # List Alerts for Watch Source: https://docs.signa.so/api-reference/monitoring/alerts/list-for-watch GET /v1/watches/{id}/alerts Alerts scoped to a single watch ## Overview Requires the `portfolios:manage` scope. ## Path Parameters Watch ID (`wat_*`). ## Query Parameters Page size (1-100). Opaque cursor. ## Response Same envelope as [List Alerts](/api-reference/monitoring/alerts/list), filtered to alerts produced by this watch. ## Errors | Status | `type` | When | | ------ | ----------- | --------------------------------------------- | | 404 | `not_found` | Watch doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/watches/wat_8kLm2nPq/alerts" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const alerts = await signa.watches.listAlerts("wat_8kLm2nPq", { limit: 50 }); ``` ```json theme={null} { "object": "list", "data": [ { "id": "alt_4tYpL2Qn", "object": "alert", "schema_version": "2026-06-01", "watch": { "id": "wat_8kLm2nPq", "name": "Nike owner watch", "type": "owner" }, "customer_reference": null, "event": { "type": "trademark.status_changed", "summary": "Status stage changed: published → registered", "diff": [ { "path": "status_stage", "op": "changed", "from": "published", "to": "registered" } ] }, "match": null, "trademark": { "id": "tm_9vXq3Rmt", "mark_text": "NIKE", "mark_feature_type": "word", "office_code": "US", "status": { "primary": "active", "stage": "registered" }, "filing_date": "2024-02-01", "registration_date": "2026-07-01", "nice_classes": [25, 28], "owner_name": "Nike, Inc.", "as_of": "2026-07-05T09:10:00.000Z", "links": { "self": "/v1/trademarks/tm_9vXq3Rmt" } }, "deadline": { "severity": "high", "opposition_window_status": "open", "must_act_by": "2026-09-04" }, "timestamps": { "occurred_at": "2026-07-05T09:10:00.000Z", "ingested_at": "2026-07-05T09:12:00.000Z", "created_at": "2026-07-05T09:12:30.000Z" }, "links": { "trademark": "/v1/trademarks/tm_9vXq3Rmt", "watch": "/v1/watches/wat_8kLm2nPq" }, "evaluation_epoch": 0 } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_9pQ2vXpL" } ``` ## Related Endpoints * [Retrieve Watch](/api-reference/monitoring/watches/retrieve) - the watch these alerts belong to * [List Alerts](/api-reference/monitoring/alerts/list) - alerts across all your watches # Lookup Alerts Source: https://docs.signa.so/api-reference/monitoring/alerts/lookup POST /v1/alerts/lookup Bulk-fetch alerts by ID, capped at 100 per call ## Overview Polling pattern: when your webhook handler skips a delivery (endpoint offline, crash, hand-off between workers), persist the alert IDs you've seen and reconcile by calling this endpoint to confirm end-to-end delivery. Malformed IDs, or IDs of the wrong type, fail the whole request with `400`, with each offending entry called out by index (`ids[3]`). IDs that are well-formed but unknown, including IDs belonging to another org, are silently dropped from the result rather than erroring, so any gap between what you sent and what came back is real. `GET /v1/alerts/{id}` for a single unknown or foreign ID still returns `404`. Requires the `portfolios:manage` scope. Lookup is a read-shaped operation, so the `Idempotency-Key` header is not required. Sending one, as in the example below, is always safe. ## Body Parameters 1-100 alert IDs (`alt_*`). ## Response Always `"list"`. Array of `Alert` objects (see [List Alerts](/api-reference/monitoring/alerts/list) for the shape). Order isn't guaranteed to match `ids`. Request identifier. ## Errors | Status | `type` | When | | ------ | ------------------ | ---------------------------------------------------------------------------------- | | 400 | `validation_error` | `ids` is empty, has more than 100 entries, or contains a malformed / wrong-type ID | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/alerts/lookup" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: reconcile-alerts-2026-06-12" \ -d '{ "ids": ["alt_4tYpL2Qn", "alt_3vXq7RmT"] }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const alerts = await signa.alerts.lookup(["alt_4tYpL2Qn", "alt_3vXq7RmT"]); ``` ```json theme={null} { "object": "list", "data": [ { "id": "alt_4tYpL2Qn", "object": "alert", "schema_version": "2026-06-01", "watch": { "id": "wat_8kLm2nPq", "name": "Nike owner watch", "type": "owner" }, "customer_reference": null, "event": { "type": "trademark.status_changed", "summary": "Status stage changed: published → registered", "diff": [ { "path": "status_stage", "op": "changed", "from": "published", "to": "registered" } ] }, "match": null, "trademark": { "id": "tm_9vXq3Rmt", "mark_text": "NIKE", "mark_feature_type": "word", "office_code": "US", "status": { "primary": "active", "stage": "registered" }, "filing_date": "2024-02-01", "registration_date": "2026-07-01", "nice_classes": [25, 28], "owner_name": "Nike, Inc.", "as_of": "2026-07-05T09:10:00.000Z", "links": { "self": "/v1/trademarks/tm_9vXq3Rmt" } }, "deadline": { "severity": "high", "opposition_window_status": "open", "must_act_by": "2026-09-04" }, "timestamps": { "occurred_at": "2026-07-05T09:10:00.000Z", "ingested_at": "2026-07-05T09:12:00.000Z", "created_at": "2026-07-05T09:12:30.000Z" }, "links": { "trademark": "/v1/trademarks/tm_9vXq3Rmt", "watch": "/v1/watches/wat_8kLm2nPq" }, "evaluation_epoch": 0 } ], "request_id": "req_5nRvXq2T" } ``` ## Related Endpoints * [List Alerts](/api-reference/monitoring/alerts/list) - browse alerts with filters and pagination * [Retrieve Alert](/api-reference/monitoring/alerts/retrieve) - fetch a single alert by ID # Retrieve Alert Source: https://docs.signa.so/api-reference/monitoring/alerts/retrieve GET /v1/alerts/{id} Get a single alert by ID ## Overview Requires the `portfolios:manage` scope. ## Path Parameters Alert ID (`alt_*`). ## Response Returns a single `Alert` (see [List Alerts](/api-reference/monitoring/alerts/list) for the shape). ## Errors | Status | `type` | When | | ------ | ----------- | --------------------------------------------- | | 404 | `not_found` | Alert doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/alerts/alt_4tYpL2Qn" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const alert = await signa.alerts.retrieve("alt_4tYpL2Qn"); ``` ```json theme={null} { "id": "alt_4tYpL2Qn", "object": "alert", "schema_version": "2026-06-01", "watch": { "id": "wat_8kLm2nPq", "name": "Nike owner watch", "type": "owner" }, "customer_reference": null, "event": { "type": "trademark.status_changed", "summary": "Status stage changed: published → registered", "diff": [ { "path": "status_stage", "op": "changed", "from": "published", "to": "registered" } ] }, "match": null, "trademark": { "id": "tm_9vXq3Rmt", "mark_text": "NIKE", "mark_feature_type": "word", "office_code": "US", "status": { "primary": "active", "stage": "registered" }, "filing_date": "2024-02-01", "registration_date": "2026-07-01", "nice_classes": [25, 28], "owner_name": "Nike, Inc.", "as_of": "2026-07-05T09:10:00.000Z", "links": { "self": "/v1/trademarks/tm_9vXq3Rmt" } }, "deadline": { "severity": "high", "opposition_window_status": "open", "must_act_by": "2026-09-04" }, "timestamps": { "occurred_at": "2026-07-05T09:10:00.000Z", "ingested_at": "2026-07-05T09:12:00.000Z", "created_at": "2026-07-05T09:12:30.000Z" }, "links": { "trademark": "/v1/trademarks/tm_9vXq3Rmt", "watch": "/v1/watches/wat_8kLm2nPq" }, "evaluation_epoch": 0, "request_id": "req_3vXq7RmT" } ``` ## Related Endpoints * [List Alerts](/api-reference/monitoring/alerts/list) - browse alerts with filters and pagination * [Lookup Alerts](/api-reference/monitoring/alerts/lookup) - bulk-fetch alerts by ID * [Watch Diagnostics](/api-reference/monitoring/watches/diagnostics) - explain why a specific trademark did or didn't alert # List Events Source: https://docs.signa.so/api-reference/monitoring/events/list GET /v1/events Read your organization's event ledger, with a replay checkpoint **Beta.** The event ledger and its replay rail are stable enough to build on, but the per-event `data` payloads are versioned by `payload_version` and their shape is not yet frozen. Pin `payload_version` in your consumer and read the [changelog](/changelog) before upgrading. ## Overview Every webhook Signa sends you is also a row in your organization's event ledger, addressable by the same `evt_*` id that arrives in the `webhook-id` header. The ledger is append-only and retained for **30 days**. Two ways to read it: * **Newest first** (`sort=-id`, the default) with `cursor` — a browse/tail feed. * **Oldest first** (`sort=id`) with `after` — a durable replay from a checkpoint you persist. This is the recovery path when your receiver was down: store the last `evt_*` you processed, and ask for everything after it. Requires the `events:read` scope. ## Query Parameters Page size (1-100). Opaque cursor from the previous response. Cannot be combined with `after`. `-id` (newest first) or `id` (oldest first). `after` requires `id`. Exclusive `evt_*` replay checkpoint. Valid only with `sort=id`. Returns `410 replay_window_expired` when the checkpoint has aged out of the 30-day window. Filter by event type(s), comma-separated: `alert.created`, `trademark.status_changed`, `office_action.issued`. Filter by office code(s), comma-separated. WIPO ST.3 uppercase (`US`, `EM`); legacy acronym slugs (`uspto`, `euipo`) and `EU` are also accepted. Filter by trademark ID (`tm_*`). ISO date or timestamp — events recorded by Signa at or after this time. Filter by the portfolio membership snapshot taken when the event was recorded. Applies to **every** family, `alert.created` included: an alert is in the portfolio only when its matched mark was a member at event time. Later membership changes never rewrite history. Requires `portfolios:manage`. ## Response Always `"list"`. Array of event summaries. Event ID (`evt_*`, opaque). The same value arrives in the `webhook-id` header of the matching delivery. Always `"event"`. Event slug, e.g. `trademark.status_changed`. The mark this event is about (`tm_*`), or `null` for families with no trademark identity. WIPO ST.3 office code, or `null`. When **Signa** produced the event: ingestion time for `trademark.*` and `office_action.*`, alert-creation time for `alert.created`. Not the office's own date — that is `source_date` on the detail body. When the ledger row was written. Alias of `recorded_at`. Whether another page exists. `{ cursor }` — opaque next-page cursor, `null` when exhausted. ## Errors | Status | `type` | When | | ------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | | 400 | `validation_error` | Bad query parameter — `after` blank, `after` with `cursor`, `after` without `sort=id`, unknown `office_code` | | 400 | `cursor_expired` / `cursor_invalid` | Malformed/tampered cursor, or one reused with different bound parameters | | 403 | `forbidden` | Missing `events:read` (or `portfolios:manage` when using `portfolio_id`) | | 404 | `not_found` | `portfolio_id` doesn't exist or belongs to another org | | 410 | `replay_window_expired` | The `after` checkpoint is older than the 30-day retention window | ## Code Examples ```bash cURL theme={null} # Tail the feed curl "https://api.signa.so/v1/events?limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" # Replay from a stored checkpoint curl "https://api.signa.so/v1/events?sort=id&after=evt_3D7rZ9&limit=100" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```ts TypeScript theme={null} // Replay everything you missed, oldest first. const page = await signa.events.list({ sort: 'id', after: lastProcessedId, limit: 100 }); for await (const event of page) { await handle(event); } ``` ```json 200 theme={null} { "object": "list", "data": [ { "id": "evt_3D7rZ9", "object": "event", "type": "trademark.status_changed", "trademark_id": "tm_9vXq3Rmt", "office_code": "US", "occurred_at": "2026-08-14T09:10:00.000Z", "recorded_at": "2026-08-14T09:10:02.000Z", "created_at": "2026-08-14T09:10:02.000Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_3vXq7RmT" } ``` ## Related Endpoints * [Retrieve Event](/api-reference/monitoring/events/retrieve) - full detail with field-level before/after diffs * [List Alerts](/api-reference/monitoring/alerts/list) - the alert resource an `alert.created` event wraps * [Webhooks](/guides/monitoring/webhooks) - the push rail carrying the same `evt_*` ids # Retrieve Event Source: https://docs.signa.so/api-reference/monitoring/events/retrieve GET /v1/events/{id} Get one event with field-level before/after diffs **Beta.** The `data` payload is versioned by `payload_version` and its shape is not yet frozen. See [List Events](/api-reference/monitoring/events/list). ## Overview Returns the full body for one ledger row — the same body the matching webhook delivery carried, so you can verify or re-read a delivery you already received. Trademark-family events include field-level diffs; `alert.created` renders its stored versioned payload. Requires the `events:read` scope. ## Path Parameters Event ID (`evt_*`). A raw numeric ID returns `400 validation_error`; a valid public ID of another type returns `400 id_type_mismatch`. ## Response Beyond the summary fields on [List Events](/api-reference/monitoring/events/list), the detail body carries: The aggregate version this event was recorded at. The fields that changed, in **public trademark field names** — `status`, `mark_text`, `publication_date`, `ir_number`, or a changed child collection (`owners`, `classifications`). Fields Signa does not publish are omitted rather than renamed. `{ [field]: { before, after } }`, keyed by the same public names as `changed_fields`. The office-reported data date this event is from (`YYYY-MM-DD`), or `null` when the feed reports none. Day precision, and the meaning varies by office: USPTO transaction date, WIPO gazette date, snapshot offices the crawl date. Distinct from `occurred_at`, which is when Signa produced the event. The portfolios the mark belonged to when the event was recorded, each `{ id, external_ref }`, frozen at record time. Empty when the mark was in no portfolio. Absent on `alert.created`. ## Errors | Status | `type` | When | | ------ | --------------------------------------- | --------------------------------------------------------------------------------- | | 400 | `validation_error` / `id_type_mismatch` | Malformed or wrong-type event ID | | 403 | `forbidden` | Missing `events:read` | | 404 | `not_found` | Event doesn't exist, has aged out of the 30-day window, or belongs to another org | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/events/evt_3D7rZ9" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```ts TypeScript theme={null} const event = await signa.events.retrieve('evt_3D7rZ9'); ``` ## Related Endpoints * [List Events](/api-reference/monitoring/events/list) - browse or replay the ledger * [Retrieve Alert](/api-reference/monitoring/alerts/retrieve) - the alert an `alert.created` event wraps # Watch Attestation Source: https://docs.signa.so/api-reference/monitoring/watches/attestation GET /v1/watches/{id}/attestation Fetch the monthly proof-of-monitoring record for a watch, as verifiable JSON or a filable PDF ## Overview A filable, monthly record that a watch was actually evaluated during a period, the office-data horizon it covered, and the outcome, including the negative outcome. It is the artifact a firm keeps on file to show that a monitoring instruction was in force and was executed. The artifact is computed **deterministically** from the evaluation receipts recorded at the time, so re-fetching a closed period months later produces the same bytes and the same `content_hash`. Read-only: calling it never changes anything. Requires the `portfolios:manage` scope. **What it certifies, and what it does not.** This record certifies that evaluation occurred against the stated data horizons. It does **not** certify that every relevant mark was surfaced, since recall is bounded by office coverage and match strategy. That distinction is written into the `statement` field of every artifact and printed on the PDF. ## Path Parameters Watch ID (`wat_*`). ## Query Parameters UTC calendar month as `YYYY-MM`. Defaults to the previous (most recently closed) month. Response format. Defaults to `json`. Use `pdf` for the business record. An explicit `format` wins over the `Accept` header. Set `true` to get an interim artifact for the current, still-open month. The result carries `period.partial: true` and its figures can still change. Default `false`. ## The PDF business record Pass `?format=pdf`, or send `Accept: application/pdf`. The response is a PDF attachment named `{watch-name}-{YYYY-MM}-attestation.pdf`. The PDF is rendered from the same artifact as the JSON, so the two never disagree. It leads with the summary figures and the attested statement, then the per-office coverage table and any disclosed gaps, then the configuration and integrity hash. The JSON is the copy you verify against; the PDF is the copy you file. ## Response Always `watch_attestation`. Artifact schema version (e.g. `2026-08-01`). Bumped on any additive field change. The monitoring instruction the record attests to. Watch ID (`wat_*`). Display only. **Excluded** from `content_hash`, since it is mutable via `PATCH`. Stable `sha256` hash of the watch query and trigger events. What the fingerprint describes: the configuration as it exists **when you fetch**. A true evaluated-time snapshot needs configuration history that is not stored yet, so the two flags below keep this honest. Lifecycle events this watch fires on. `true` when the configuration changed **during** the period. `epochs_in_period` lists the evaluation epochs seen. `true` when the configuration changed **after** the period closed, so the fingerprint no longer describes the configuration that was in force. Generation-time fact, excluded from `content_hash`. Evaluation epochs observed in the period. Present when more than one occurred. Inclusive UTC start of the month. **Exclusive** UTC end (midnight on the first of the next month). `true` for an interim artifact of a still-open month. Figures can still change. One entry per office in the watch's scope. Uppercase ST.3 code (e.g. `US`, `EM`). Full office name. | Value | Meaning | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `evaluated` | A covered office with real evaluation claims. | | `no_evaluations` | A covered office with **zero** evaluations in the period: zero counts, no coverage claims, and a full-period gap. An un-evaluated month never reads as a clean silent month. | | `unsupported` | An in-scope office Signa does not ingest. **No** evaluation fields are returned at all. | Evaluations performed for this office. Sync runs the evaluations consumed. Volume of office change weighed **against your query** this period. Not the size of the office register, and not a claim about how many records exist. Matches found. Alerts produced. Start of the office-data horizon covered. Office **data** time evaluations reached, frozen on the receipt at evaluation time. `null` on receipts predating coverage snapshots, which are disclosed as a gap rather than presented as covered. How the coverage horizon was derived. Disclosed coverage gaps. See [Coverage gaps](#coverage-gaps). Evaluations across all offices. Alerts across all offices. Cross-check of the recorded alert counts against the stored alert rows for the period. `mismatch` is disclosed rather than silently reconciled. The plain-language attestation, including the limit of what it certifies. Version of the internal freshness-target table used to decide what counted as a gap. Present (`paused` or `disabled`) when the watch is not active **at generation time**. Pause history within the period is not reconstructable today. Excluded from `content_hash`. When this copy was produced. Excluded from `content_hash`. `sha256` over the canonical artifact. See [Verifying a filed record](#verifying-a-filed-record). Request identifier. ## Coverage gaps A gap is an interval where the office-data coverage the watch had evaluated through fell behind the internal freshness target for that office. Gaps are reconstructed from the receipts, not recorded by hand. When coverage first went stale. When coverage recovered, or the period end if it never did. `office_lagging` means coverage was stale beyond the target. `evaluation_failed` is reserved for a future version: failed evaluations write no receipt, so they are not reconstructed as gaps today. `true` when coverage caught back up **inside** the period. `false` means it was still behind when the period closed. That is a statement about this period only, not a claim that it stayed behind afterwards. Gap disclosure is **permanent**. Once a period's artifact records a gap it records it forever, even after coverage recovers, because it is a fact about what happened. Two shapes worth knowing: * A covered office with **zero evaluations** carries a single gap spanning the whole period. * Evaluations recorded **before coverage snapshots existed** cannot state what horizon they covered, so they are disclosed as incomplete coverage evidence with a `null` `coverage_through`, never presented as covered. ## Unsupported offices If a watch is scoped to an office Signa does not ingest, that office appears with `status: "unsupported"` and **no evaluation claims at all**. Signa will never render a clean, zero-alert attestation for an office it does not cover, because that would read as "we looked and found nothing" when the truth is "we do not cover this office yet". If **every** office in the scope is unsupported, the endpoint returns `422 attestation_unsupported_scope` rather than an empty artifact. ## Verifying a filed record `content_hash` is a `sha256` over the canonical, sorted-key artifact, excluding the fields that legitimately differ between two fetches of the same period: `generated_at`, `request_id`, `content_hash` itself, `watch_status_at_generation`, `watch.name`, and `watch.configuration_changed_since_period`. To verify a filed copy, re-fetch the same closed period and compare hashes. A match proves the figures were not altered after issue. This proves **integrity**, not third-party **authenticity**. Signed and RFC 3161 timestamped documents are on the roadmap. Fetch and file the artifact promptly after the month closes, before changing the watch. A filed copy with both `configuration_changed_*` flags `false` pins the fingerprint to the period unambiguously. ## Retention Attestations are available for closed months within the last **25 months**. Beyond that, the backing evaluation receipts have aged out and the endpoint returns `410 attestation_expired`. ## Errors | Status | `type` | When | | ------ | ------------------------------- | ---------------------------------------------------------------------------------------- | | 400 | `validation_error` | `period` is not a valid `YYYY-MM` label | | 403 | `forbidden` | Caller lacks `portfolios:manage` | | 404 | `not_found` | The watch doesn't exist or belongs to another org | | 410 | `attestation_expired` | The period is beyond the 25-month retention window | | 422 | `attestation_period_open` | The period is the current, still-open month. Pass `partial=true` for an interim artifact | | 422 | `attestation_unsupported_scope` | Every office in the watch's scope is one Signa does not ingest | ## Code Examples ```bash cURL theme={null} # Previous closed month, JSON curl "https://api.signa.so/v1/watches/wat_8kLm2nPq/attestation" \ -H "Authorization: Bearer sig_YOUR_KEY" # A specific month, as the filable PDF curl "https://api.signa.so/v1/watches/wat_8kLm2nPq/attestation?period=2026-07&format=pdf" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -o july-attestation.pdf ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const record = await signa.watches.attestation("wat_8kLm2nPq", { period: "2026-07" }); console.log(record.statement); // Any gap the period disclosed, per office. for (const office of record.offices) { for (const gap of office.gaps ?? []) { console.log(office.office_code, gap.from, gap.through, gap.resolved); } } ``` ```json Closed month with a disclosed gap theme={null} { "object": "watch_attestation", "schema_version": "2026-08-01", "watch": { "id": "wat_8kLm2nPq", "name": "ACME Corp, class 9 similarity watch", "query_fingerprint": "sha256:9f2c41ab7e5d0c3388a1b6ee42d9f70c1a4b8e5537cc9d21e0f6a4b3c8d71e59", "fingerprint_basis": "current_configuration", "trigger_events": ["trademark.created", "trademark.published"], "configuration_changed_in_period": false, "configuration_changed_since_period": false }, "period": { "start": "2026-07-01T00:00:00.000Z", "end": "2026-08-01T00:00:00.000Z", "partial": false }, "offices": [ { "office_code": "US", "office_name": "United States Patent and Trademark Office", "status": "evaluated", "evaluations_count": 31, "sync_runs_evaluated": ["018f9b2e-0000-7000-8000-000000000001"], "changes_evaluated": 412083, "match_count": 2, "alerts_emitted": 1, "coverage_from": "2026-06-30T23:59:59.999Z", "coverage_through": "2026-07-31T23:59:59.999Z", "coverage_basis": "source_dates", "gaps": [ { "from": "2026-07-11T00:00:00.000Z", "through": "2026-07-14T00:00:00.000Z", "reason": "office_lagging", "resolved": true } ] }, { "office_code": "JP", "office_name": "Japan Patent Office", "status": "unsupported" } ], "totals": { "evaluations": 31, "alerts_emitted": 1 }, "reconciliation": "consistent", "statement": "This watch was evaluated 31 times during the period, covering office data through the per-office coverage timestamps stated above. 1 alert was produced. Coverage gaps, if any, are disclosed per office. This attestation certifies that evaluation occurred against the stated data horizons. It does not certify that every relevant mark was surfaced.", "slo_reference": "2026-08-01", "generated_at": "2026-08-01T09:00:00.000Z", "content_hash": "sha256:4d1e8a7c93b25f60ae1c7d4488f302b95e6a1cc07df3b28e94a6501fbb27c3d1", "request_id": "req_2mR8vNkT" } ``` ```json Period still open theme={null} { "request_id": "req_7hTn4kLp", "error": { "type": "attestation_period_open", "title": "Attestation period is still open", "detail": "The period 2026-08 is the current (in-progress) UTC month. A final attestation is only produced for closed calendar months.", "retryable": false, "suggestion": "Request a previous (closed) month, or pass `partial=true` to get an interim artifact marked `\"partial\": true`." } } ``` ## Related Endpoints * [Watch Diagnostics](/api-reference/monitoring/watches/diagnostics) - explain a single expected alert * [Retrieve Watch](/api-reference/monitoring/watches/retrieve) - the watch this record attests to * [Attestations guide](/guides/monitoring/attestations) - how firms use the artifact * [Monitoring status guide](/guides/monitoring/monitoring-status) - live coverage, as opposed to the monthly record # Bulk Create Watches Source: https://docs.signa.so/api-reference/monitoring/watches/bulk POST /v1/watches/bulk Create up to 100 watches in a single call ## Overview Bulk-create variant of [Create Watch](/api-reference/monitoring/watches/create). The full set is validated upfront; there are no partial inserts. Counts against your plan's watch limit. Requires the `portfolios:manage` scope. ## Body Parameters 1-100 watch specifications. Each item has the same shape as the body of [Create Watch](/api-reference/monitoring/watches/create). ## Response Always `"list"`. Array of created `Watch` objects, in input order. Request identifier. Filter keys inside `query.filters` are camelCase (`niceClasses`, `jurisdictions`, `ownerId`, ...). Unknown keys, including snake\_case typos like `nice_classes`, are rejected with `400`, as is `query.match` in any form. See [the canonical query reference](/guides/monitoring/watches#the-query-dsl). ## Errors | Status | `type` | When | | ------ | ------------------------- | ------------------------------------------------------- | | 400 | `validation_error` | Any item fails validation (the whole batch is rejected) | | 409 | `resource_quota_exceeded` | Your plan's watch limit is exceeded | | 413 | `payload_too_large` | Combined payload exceeds the per-request limit | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/watches/bulk" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: bulk-class9-2026-06-12" \ -d '{ "watches": [ { "name": "Class 9 filings (US)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US"] } } }, { "name": "Class 9 filings (EU)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["EU"] } } } ] }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const created = await signa.watches.bulk({ watches: [ { name: "Class 9 filings (US)", watch_type: "class", query: { version: "v2", filters: { niceClasses: [9], jurisdictions: ["US"] } }, }, { name: "Class 9 filings (EU)", watch_type: "class", query: { version: "v2", filters: { niceClasses: [9], jurisdictions: ["EU"] } }, }, ], }); ``` ```json theme={null} { "object": "list", "data": [ { "id": "wat_8kLm2nPq", "object": "watch", "name": "Class 9 filings (US)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US"] } }, "delivery_mode": "always_per_alert", "status": "active", "alert_count_24h": null, "last_alerted_at": null, "metadata": {}, "created_at": "2026-05-11T10:00:00.000Z", "updated_at": "2026-05-11T10:00:00.000Z" }, { "id": "wat_3vXq7RmT", "object": "watch", "name": "Class 9 filings (EU)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["EU"] } }, "delivery_mode": "always_per_alert", "status": "active", "alert_count_24h": null, "last_alerted_at": null, "metadata": {}, "created_at": "2026-05-11T10:00:00.000Z", "updated_at": "2026-05-11T10:00:00.000Z" } ], "request_id": "req_5tQmR2vX" } ``` ## Related Endpoints * [Create Watch](/api-reference/monitoring/watches/create) - create a single watch * [Preview Watch](/api-reference/monitoring/watches/preview) - check match volume before bulk-creating * [List Watches](/api-reference/monitoring/watches/list) - list the resulting watches # Create Watch Source: https://docs.signa.so/api-reference/monitoring/watches/create POST /v1/watches Create a watch that fires alerts when matching trademarks change ## Overview Creates a saved monitor that runs on every data sync. Five watch types (`mark`, `portfolio`, `owner`, `class`, `similarity`) share a single `query` shape; `watch_type` selects which scoping field is required. Your plan's watch limit is enforced before insert. To see what a watch would catch before you create it, use [Preview Watch](/api-reference/monitoring/watches/preview), which returns the actual matching marks by default. Requires the `portfolios:manage` scope. ## Body Parameters Display name (1-255 chars). One of `mark`, `portfolio`, `owner`, `class`, `similarity`. Each type requires a specific field to be populated, see the [Watches guide](/guides/monitoring/watches) for the table. Watch query DSL. Non-empty string. Use `"v2"`. Whitespace-separated keyword query (max 20 keywords, each 3+ chars, no stop words). Required for `watch_type: "similarity"`. Filter object. Keys are camelCase (`trademarkIds`, `ownerId`, `niceClasses`, `jurisdictions`, `offices`, `statusPrimary`, ...); unknown keys, including snake\_case typos like `nice_classes`, are rejected with `400`. See the [canonical filter-key list](/guides/monitoring/watches#the-query-dsl). Watches keep office-based scoping and do not apply the protection-scope jurisdiction expansion used by search and screening, so `filters.jurisdictions: ["DE"]` watches the German office only and does not surface EU trade marks. Subset of `trademark.created`, `trademark.updated`, `trademark.status_changed`, `trademark.retracted`, `trademark.corrected`. Default: `trademark.created`, `trademark.updated`, `trademark.status_changed`. Similarity watches only. Non-empty subset of `exact`, `phonetic`, `fuzzy`, `prefix`. Default: `exact` and `fuzzy`. Similarity watches only. One of `exact`, `normalized`, `fuzzy`, `phonetic`, gating which match attribution tiers can fire an alert (`exact` is narrowest, `phonetic` is broadest). Omit it and every tier can fire. See the [Watches guide](/guides/monitoring/watches#min_match_tier-similarity-only) for tier meanings. Not currently supported — rejected on write with a `400`. Scores are informational (see `match_score` on alerts). Contact support for calibrated-band thresholds when available. Currently only `always_per_alert` is supported — one delivery per alert; any other value returns `400`. Digest modes are planned but not yet available. See [`delivery_mode`](/guides/monitoring/watches#delivery_mode--per-alert-delivery-today). Free-form metadata (max 8KB). ## Response Returns the created `Watch` with status `201`. Watch ID (`wat_*`). Always `"watch"`. Display name. One of the five types. Saved query DSL. Delivery cadence. Initial status: `active`. Null on create. Null on create. Echoed back. ISO timestamp. ISO timestamp. Request identifier. ## Errors | Status | `type` | When | | ------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid `query` (forbidden DSL keys, unknown `filters` keys, `query.match` in any form, stop words, too many keywords), a `watch_type` constraint violation (e.g. `portfolio` without `filters.trademarkIds`), or a missing `Idempotency-Key` header | | 409 | `resource_quota_exceeded` | Your plan's watch limit is exceeded | | 413 | `payload_too_large` | `query` payload exceeds 256 KB | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/watches" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: create-class9-watch-2026-06-12" \ -d '{ "name": "Class 9 filings (US/EU)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US", "EU"] }, "trigger_events": ["trademark.created", "trademark.status_changed"] }, "delivery_mode": "always_per_alert" }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const watch = await signa.watches.create({ name: "Class 9 filings (US/EU)", watch_type: "class", query: { version: "v2", filters: { niceClasses: [9], jurisdictions: ["US", "EU"], }, trigger_events: ["trademark.created", "trademark.status_changed"], }, delivery_mode: "always_per_alert", }); ``` ```json theme={null} { "id": "wat_8kLm2nPq", "object": "watch", "name": "Class 9 filings (US/EU)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US", "EU"] }, "trigger_events": ["trademark.created", "trademark.status_changed"] }, "delivery_mode": "always_per_alert", "status": "active", "alert_count_24h": null, "last_alerted_at": null, "metadata": {}, "created_at": "2026-05-11T10:00:00.000Z", "updated_at": "2026-05-11T10:00:00.000Z", "request_id": "req_5nRvXq2T" } ``` ## Related Endpoints * [Preview Watch](/api-reference/monitoring/watches/preview) - see what a query would catch before creating it * [Bulk Create Watches](/api-reference/monitoring/watches/bulk) - create up to 100 watches at once * [Update Watch](/api-reference/monitoring/watches/update) - change a watch's query, name, or status * [Watches guide](/guides/monitoring/watches) - watch types and the query DSL # Delete Watch Source: https://docs.signa.so/api-reference/monitoring/watches/delete DELETE /v1/watches/{id} Stop a watch from evaluating ## Overview Deleting a watch stops it from evaluating. Existing alerts it already produced remain retrievable and still reference the watch by ID, they aren't deleted along with it. Requires the `portfolios:manage` scope. ## Path Parameters Watch ID (`wat_*`). ## Response The deleted watch ID. Always `"watch"`. Always `true`. Request identifier. A deleted watch no longer appears in [List Watches](/api-reference/monitoring/watches/list). ## Errors | Status | `type` | When | | ------ | ----------- | --------------------------------------------- | | 404 | `not_found` | Watch doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl -X DELETE "https://api.signa.so/v1/watches/wat_8kLm2nPq" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: delete-wat-8kLm2nPq-2026-06-12" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); await signa.watches.delete("wat_8kLm2nPq"); ``` ```json theme={null} { "id": "wat_8kLm2nPq", "object": "watch", "deleted": true, "request_id": "req_9pQ2vXpL" } ``` ## Related Endpoints * [List Watches](/api-reference/monitoring/watches/list) - deleted watches are excluded * [Retrieve Alert](/api-reference/monitoring/alerts/retrieve) - alerts from a deleted watch remain retrievable * [Pause Watch](/api-reference/monitoring/watches/pause) - stop evaluation without deleting the watch # Watch Diagnostics Source: https://docs.signa.so/api-reference/monitoring/watches/diagnostics GET /v1/watches/{id}/diagnostics Check whether a watch fired, or should have fired, an alert for a specific trademark ## Overview Answers the question every monitoring customer eventually asks: "I expected an alert for this trademark. Why didn't I get one?" Given a watch and a trademark, this endpoint walks through evaluation step by step and reports where it stopped, or confirms that an alert fired. It's read-only: calling it never changes anything. Requires the `portfolios:manage` scope. ## Path Parameters Watch ID (`wat_*`). ## Query Parameters Trademark ID (`tm_*`) to check against this watch. A trademark ID from another org returns `404`. ## Response Echoed watch ID (`wat_*`). Echoed trademark ID (`tm_*`). Uppercase ST.3 code of the office that issued the trademark (e.g. `US`, `EM`). `true` if Signa evaluated this trademark against the watch within the current data window. `false` means either the change wasn't recorded in time, or it has aged past the window. See `reason`. `true` if this watch's filters include the trademark's office. `true` if a change record for this trademark exists within the data window. A recorded change is a prerequisite for a match. The lifecycle event associated with the match: `trademark.created`, `trademark.updated`, `trademark.status_changed`, `trademark.retracted`, or `trademark.corrected`. `true` if `trigger_event_type` is included in the watch's `query.trigger_events` filter. `false` explains a match that was silently dropped. The persisted search relevance score (`match_score`) from the most recent alert for this (watch, trademark). `null` when no alert exists or the watch has no scored (`q`) clause. Informational only. The watch's stored `query.score_threshold`, surfaced only for legacy watches that still carry one. **Inert** — no longer gates matching, and rejected on new writes. Prefer `min_match_tier`. `null` when unset. The watch's effective `query.min_match_tier`: which match attribution tiers can fire an alert. `null` when the watch doesn't gate by tier (every tier can fire). See [`min_match_tier`](/guides/monitoring/watches#min_match_tier-similarity-only) for tier meanings. `true` if an alert exists for this (watch, trademark) pair. Alert ID (`alt_*`) when one fired, otherwise `null`. Retrieve it with [Get Alert](/api-reference/monitoring/alerts/retrieve), or cross-reference it against [List webhook deliveries](/api-reference/monitoring/webhooks/list-deliveries) to confirm your endpoint received it. Human-readable explanation for the outcome. | Value | Meaning | | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `alert fired` | An alert exists for this pair. `alert_id` is set. | | `watch does not include office {code}` | This watch's filters don't cover the trademark's office. | | `trademark evaluated more than {N} days ago; provenance no longer available` | The change is older than the data window; Signa can no longer explain the outcome. | | `trademark not in candidacy window for the most recent {office} sync` | No change was recorded recently enough for evaluation to consider this trademark. | | `trigger event {type} not in watch.trigger_events` | The change happened, but its event type is excluded by this watch's filter. | | `would alert but rolled into digest` | The watch's delivery mode resolved to a digest instead of an immediate alert. | | `no matching reason available` | None of the above applied. Contact support if you see this. | The delivery mode that applied. Today this is `per_alert` or `null`, since `always_per_alert` is the only mode a watch can be created with. Computed opposition-window state for this trademark. `null` whenever no window can be cited: the trademark has no publication date, no opposition rule is modeled for its office and filing route, or the window could not be computed and there is no alert row to fall back on. ISO date: last day to file. Stable, opaque slug of the opposition rule cited (e.g. `us_opposition`), and the join key to [List Opposition Rules](/api-reference/reference/opposition-rules). Effectively always present when the `opposition` block is present, since an unmodeled office yields a `null` block rather than a block with a null `rule_id`. It is still emitted when the window itself could not be computed (`rule_source` and `rule_version` null), so a degraded block stays resolvable; the only path that yields `null` is that degraded path, when the rule lookup itself came back empty. Human-readable name of the source the rule cites (e.g. `"15 U.S.C. § 1063(a)"`). For display, not identity. Date the rule was last verified against its sources. Whether the close date behind `must_act_by` was checked against the office's holiday calendar, and whether that check moved it. `not_checked` means no calendar stands behind it: the close falls outside the calendar's pinned coverage years, the rule carries no calendar, or there is no publication date. A rollover only ever moves a close **later**, so a `not_checked` date is correct-or-early. `null` on the degraded path, where no window was produced at all (alongside a null `rule_source` / `rule_version`). This is the surface to check when you need to know whether a stored `must_act_by` is calendar-verified: alert rows themselves do not carry the marker. Note the deliberate divergence when it reads `not_checked`: for the 20 days after that close, search buckets the mark as OPEN (a rollover could only have moved the close later) while `POST /v1/oppositions/compute` reports `closed` against the un-rolled date. Read the window as possibly still open and confirm with the office. See [opposition windows](/guides/monitoring/opposition-windows). Coarse window status. Retention horizons for the data behind this response. See [retention windows](/guides/monitoring/troubleshooting) for how they interact with alerts and webhook deliveries. Days of change history available. Days of webhook delivery history available. Days alerts remain retrievable. The smallest of the windows above. Past this horizon, `evaluated` is `false`. Request identifier. These fields appear in the response but describe internal evaluation state. Safe to ignore when debugging alert delivery. * `lease_state`: internal evaluation state; safe to ignore. * `evaluation_epoch`: internal evaluation state; safe to ignore. * `replay_epoch_origin`: internal evaluation state; safe to ignore. * `opensearch_score`: internal evaluation state; safe to ignore (currently always `null`). * `last_relevant_sync_run`: internal evaluation state; safe to ignore. ## Errors | Status | `type` | When | | ------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | `trademark_id` query parameter missing | | 403 | `forbidden` | Caller lacks `portfolios:manage` | | 404 | `not_found` | The watch or trademark doesn't exist, belongs to another org, or the trademark is out of the watch's scope with no alert on record. These cases return the same response so an ID guess can't disclose another org's data. | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/watches/wat_8kLm2nPq/diagnostics?trademark_id=tm_9vXq3Rmt" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const trace = await signa.watches.diagnostics("wat_8kLm2nPq", { trademarkId: "tm_9vXq3Rmt", }); console.log(trace.reason); ``` ```json Alert fired theme={null} { "watch_id": "wat_8kLm2nPq", "trademark_id": "tm_9vXq3Rmt", "office_code": "US", "evaluated": true, "office_in_scope": true, "candidacy_passed": true, "trigger_event_type": "trademark.status_changed", "trigger_event_in_filter": true, "score_threshold": null, "min_match_tier": null, "alert_fired": true, "reason": "alert fired", "delivery_mode_effective": "per_alert", "lease_state": "released", "evaluation_epoch": 0, "replay_epoch_origin": null, "last_relevant_sync_run": { "id": "018f9b2e-0000-7000-8000-000000000001", "office_code": "US", "completed_at": "2026-07-06T04:12:00.000Z", "search_indexed_at": "2026-07-06T04:18:00.000Z" }, "opensearch_score": null, "alert_id": "alt_4tYpL2Qn", "opposition": { "must_act_by": "2026-09-04", "rule_id": "us_opposition", "rule_source": "15 U.S.C. § 1063(a)", "rule_version": "2026-05-06", "close_adjustment": "unchanged", "window_status": "open" }, "data_window": { "trademark_changes_retention_days": 90, "deliveries_retention_days": 30, "alerts_retention_days": 90, "diagnostic_freshness_horizon_days": 90 }, "request_id": "req_2mR8vNkT" } ``` ```json No alert, filtered out theme={null} { "watch_id": "wat_8kLm2nPq", "trademark_id": "tm_9vXq3Rmt", "office_code": "EM", "evaluated": true, "office_in_scope": true, "candidacy_passed": true, "trigger_event_type": "trademark.updated", "trigger_event_in_filter": false, "score_threshold": null, "min_match_tier": null, "alert_fired": false, "reason": "trigger event trademark.updated not in watch.trigger_events", "delivery_mode_effective": null, "lease_state": "released", "evaluation_epoch": 0, "replay_epoch_origin": null, "last_relevant_sync_run": { "id": "018f9b2e-0000-7000-8000-000000000002", "office_code": "EM", "completed_at": "2026-07-06T02:40:00.000Z", "search_indexed_at": "2026-07-06T02:45:00.000Z" }, "opensearch_score": null, "alert_id": null, "opposition": null, "data_window": { "trademark_changes_retention_days": 90, "deliveries_retention_days": 30, "alerts_retention_days": 90, "diagnostic_freshness_horizon_days": 90 }, "request_id": "req_3vXq7RmT" } ``` ## Related Endpoints * [Retrieve Alert](/api-reference/monitoring/alerts/retrieve) - fetch the alert this diagnosis references * [List webhook deliveries](/api-reference/monitoring/webhooks/list-deliveries) - confirm your endpoint received the alert * [Watches guide](/guides/monitoring/watches) - watch types and the query DSL * [Monitoring troubleshooting](/guides/monitoring/troubleshooting) - retention windows and debugging walkthrough # List Watches Source: https://docs.signa.so/api-reference/monitoring/watches/list GET /v1/watches List watches for the calling organization, with cursor pagination ## Overview Returns the calling org's watches with opaque cursor pagination. Requires the `portfolios:manage` scope. ## Query Parameters Page size (1-100). Opaque cursor returned in the previous response. Filter by status: `active` or `paused`. There's a third status, `disabled`, that a watch moves to when it's [deleted](/api-reference/monitoring/watches/delete); disabled watches never appear in this list, so `disabled` isn't a valid filter value here. ## Response Always `"list"`. Array of `Watch` summaries (see [Create Watch](/api-reference/monitoring/watches/create) for the shape). `alert_count_24h` is always `null` on list responses; use [Retrieve Watch](/api-reference/monitoring/watches/retrieve) for the per-watch count. True when more pages exist. Opaque cursor for the next page. Request identifier. ## Errors | Status | `type` | When | | ------ | ------------------ | -------------------------------------- | | 400 | `validation_error` | Invalid `status`, `limit`, or `cursor` | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/watches?status=active&limit=50" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const watches = await signa.watches.list({ status: "active", limit: 50 }); for await (const w of watches) { console.log(w.id, w.name, w.watch_type); } ``` ```json theme={null} { "object": "list", "data": [ { "id": "wat_8kLm2nPq", "object": "watch", "name": "Class 9 filings (US/EU)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US", "EU"] }, "trigger_events": ["trademark.created", "trademark.status_changed"] }, "delivery_mode": "always_per_alert", "status": "active", "alert_count_24h": null, "last_alerted_at": "2026-07-05T09:12:00.000Z", "metadata": {}, "created_at": "2026-05-11T10:00:00.000Z", "updated_at": "2026-05-11T10:00:00.000Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_6tYpL2Qn" } ``` ## Related Endpoints * [Create Watch](/api-reference/monitoring/watches/create) - create a new watch * [Retrieve Watch](/api-reference/monitoring/watches/retrieve) - get a single watch, including its 24h alert count * [Bulk Create Watches](/api-reference/monitoring/watches/bulk) - create up to 100 watches at once # Monitoring Status Source: https://docs.signa.so/api-reference/monitoring/watches/monitoring-status GET /v1/monitoring/status One org-level snapshot of watch health, per-office pipeline freshness, and end-to-end pipeline verification ## Overview A single, read-only snapshot of your whole monitoring program. It answers two different questions in one response: * **How are my watches doing?** The `watches` block is a rollup of every watch in your organization, counted by health state. It is scoped strictly to your org. * **How fresh is Signa's data for each office?** The `offices[]` block is Signa's global per-office ingestion freshness. It is the same for every organization, computed once and shared through a 60 second cache, and it is not filtered to the offices your watches cover. A third block, `pipeline`, carries the continuous end-to-end verification gauges. Every timestamp in it is null-honest: a missing or stale check is reported as `null`, never as an old value dressed up as a fresh verification. Requires the `portfolios:manage` scope. Calling it never changes anything. **`slo_hours` is an internal freshness target, not an SLA.** Each office carries an `slo_hours` value derived from that office's `update_cadence`. It is the ceiling Signa uses to decide `pipeline_status`: coverage within `slo_hours` is `ok`, coverage older than that (or no coverage signal at all) is `lagging`. It is an operational default that Signa can tune as an office's publishing behaviour changes. It is not a published service level and not a contractual commitment. ## Query Parameters None. The response is always the full snapshot. ## How to use it **Gate a "no conflicts" report.** Before you tell a client that a watch surfaced nothing, check that every office the watch covers reports `pipeline_status: "ok"`. A `lagging` office means the register data behind that negative result is older than Signa's own freshness target, so the report should say so or wait. **Drive a dashboard tile.** The response is one call, small, and safe to poll. Show `watches.by_health` as a status strip and `offices[]` as a freshness table. Because `offices[]` is global, you can cache it across users and orgs. **Run your own alerting.** Poll on a schedule and alert when `watches.by_health.degraded > 0`, when an office you rely on flips to `lagging`, or when `pipeline.last_verified_end_to_end_at` turns `null`. This is the same signal the dashboard Monitoring page reads. For the meaning of each health state and the reasoning behind the null-honest gauges, read the [Monitoring status guide](/guides/monitoring/monitoring-status). ## Response Always `monitoring_status`. Org-scoped aggregate over all of your non-deleted watches. Count of your non-deleted watches. Count of watches in each health state. All six keys are always present; a state with no watches reports `0`. Every in-scope office is evaluated and current. Coverage has stopped advancing beyond the expected window (office ingestion behind, or an evaluation backlog). Usually resolves on its own. The watch itself needs attention: repeated evaluation errors, or an entity filter that no longer resolves. Paused by you. No new evaluation happens. Active but not yet evaluated, still within the office refresh window. The scope includes an office Signa does not yet ingest. Global per-office pipeline status, one entry per live office. Not org-specific: identical for every organization, computed once and shared through a 60 second cache. WIPO ST.3 office code, uppercase (`US`, `EM`). `ok` when `coverage_through` is within `slo_hours` of now. `lagging` when coverage is older than that, or when there is no coverage signal at all. Office-data time through which Signa has processed this office. This is source data time, never wall-clock. `null` when no eligible run carries a coverage signal. Newest completed pull from the office, regardless of whether it is indexed yet. When the latest eligible run for this office became searchable. Internal freshness ceiling in hours, derived from the office cadence. This is the number `pipeline_status` is measured against. Not a published SLA. Sync runs for this office in the last 30 days that exceeded the evaluator's per-run change budget, were declined without evaluation, and have no audited re-drive recorded. Ingestion still happened, so `coverage_through` can look healthy; a non-zero value means changes from those runs were not evaluated against any watch. The affected windows appear as `budget_declined` gaps in [attestations](/api-reference/monitoring/watches/attestation). Continuous pipeline-verification gauges. Both timestamps follow one rule: a stale value is never presented as a fresh verification. When a synthetic change last passed through the live pipeline end to end. `null` when no check has been recorded or the most recent one is older than the freshness horizon. When the representative real-record path last saw an office-originated change. A separate, slower signal than the synthetic heartbeat. Same null rule. The mechanism behind the timestamps. Request identifier for support. ## Errors | Status | `type` | When | | ------ | ----------- | -------------------------------- | | 403 | `forbidden` | Caller lacks `portfolios:manage` | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/monitoring/status" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} // The SDK does not wrap this endpoint yet; call it directly. const res = await fetch("https://api.signa.so/v1/monitoring/status", { headers: { Authorization: `Bearer ${process.env.SIGNA_API_KEY}` }, }); const status = await res.json(); if (status.watches.by_health.degraded > 0) { notifyOps(`${status.watches.by_health.degraded} watches degraded`); } const lagging = status.offices.filter((o) => o.pipeline_status === "lagging"); for (const office of lagging) { console.log(office.office_code, "coverage through", office.coverage_through); } ``` ```json Healthy program with one lagging office theme={null} { "object": "monitoring_status", "watches": { "total": 412, "by_health": { "healthy": 401, "lagging": 6, "degraded": 2, "paused": 3, "pending": 0, "unsupported": 0 } }, "offices": [ { "office_code": "US", "pipeline_status": "ok", "coverage_through": "2026-09-13T23:59:59.000Z", "last_completed_sync_at": "2026-09-14T06:10:00.000Z", "last_search_indexed_at": "2026-09-14T06:24:00.000Z", "slo_hours": 48, "declined_runs_unresolved": 0 }, { "office_code": "CA", "pipeline_status": "lagging", "coverage_through": "2026-09-01T23:59:59.000Z", "last_completed_sync_at": "2026-09-13T05:40:00.000Z", "last_search_indexed_at": "2026-09-13T05:52:00.000Z", "slo_hours": 240, "declined_runs_unresolved": 1 } ], "pipeline": { "last_verified_end_to_end_at": "2026-09-14T08:00:00.000Z", "last_real_record_alert_at": "2026-09-14T02:14:00.000Z", "verification": "canary" }, "request_id": "req_2mR8vNkT" } ``` ## Related Endpoints * [Watch Attestation](/api-reference/monitoring/watches/attestation) - the monthly, per-watch record, as opposed to this live snapshot * [Watch Diagnostics](/api-reference/monitoring/watches/diagnostics) - explain a single expected alert * [List Watches](/api-reference/monitoring/watches/list) - the individual watches behind `watches.by_health` * [Monitoring status guide](/guides/monitoring/monitoring-status) - health states and the null-honest rule # Pause Watch Source: https://docs.signa.so/api-reference/monitoring/watches/pause POST /v1/watches/{id}/pause Pause evaluation: no alerts fire until resumed ## Overview Pausing preserves the watch and its history. Signa stops evaluating it on each data sync until you [resume](/api-reference/monitoring/watches/resume) it. Requires the `portfolios:manage` scope. ## Path Parameters Watch ID (`wat_*`). ## Response Returns the updated `Watch` with `status: "paused"`. ## Errors | Status | `type` | When | | ------ | ----------- | --------------------------------------------- | | 404 | `not_found` | Watch doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/watches/wat_8kLm2nPq/pause" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: pause-wat-8kLm2nPq-2026-06-12" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); await signa.watches.pause("wat_8kLm2nPq"); ``` ```json theme={null} { "id": "wat_8kLm2nPq", "object": "watch", "name": "Class 9 filings (US/EU)", "watch_type": "class", "status": "paused", "alert_count_24h": 2, "last_alerted_at": "2026-07-05T09:12:00.000Z", "updated_at": "2026-07-06T08:30:00.000Z", "request_id": "req_2mR8vNkT" } ``` ## Related Endpoints * [Resume Watch](/api-reference/monitoring/watches/resume) - restart evaluation * [Update Watch](/api-reference/monitoring/watches/update) - the same status change via `PATCH` * [Delete Watch](/api-reference/monitoring/watches/delete) - stop evaluation permanently # Preview Watch Source: https://docs.signa.so/api-reference/monitoring/watches/preview POST /v1/watches/preview Dry-run a watch query: get a match count without creating a watch ## Overview Returns the number of trademarks that would have alerted if this query had been a live watch over the last `trial_window_days` (default 7). Uses the same evaluation logic as a live watch, so the count is a faithful preview, not a separate query engine. Use it before [Create Watch](/api-reference/monitoring/watches/create) to estimate volume and tune `strategies` / `min_match_tier` for similarity watches. Requires the `portfolios:manage` scope. Preview is a read-shaped operation, so the `Idempotency-Key` header is not required. Sending one, as in the example below, is always safe. ## Body Parameters Same DSL as [Create Watch](/api-reference/monitoring/watches/create), see [the query reference](/guides/monitoring/watches#the-query-dsl). ID-bearing filters (`filters.trademarkIds`, `filters.ownerId`, ...) accept the same `tm_*` / `own_*` prefixed forms as create. Backtest window in days (1-365). Default 7. Skip the matching marks and return just `estimated_match_count`. Default `false`. Page size for `results` (1-50). Default 20. Ignored when `count_only` is `true`. ## Response Always `"watch_preview"`. Trademarks that would have alerted in the trial window. A page of the actual matching trademarks, in the same summary shape as [search results](/api-reference/trademarks/search-trademarks). Omitted when `count_only` is `true`. Whether more matches exist beyond `results`. Omitted when `count_only` is `true`. Effective page size used for `results`. Omitted when `count_only` is `true`. Present only when `estimated_match_count` is an upper bound rather than an exact count (the value is `"candidacy_upper_bound"`). This happens when the underlying scan is large enough to hit the server-side cap, or the time budget runs out after some matches were already found. Absent when the count is exact. Echo of the requested window. Request identifier. ## Latency and limits Preview runs synchronously with a server-side time budget of about 20 seconds. `class`, `mark`, and `owner` previews typically complete in a few seconds; `similarity` previews over broad scopes and long windows are the heaviest and can approach the budget. * If the budget runs out after some matches were found, the response is a `200` with a partial count and `estimate_basis: "candidacy_upper_bound"`. * If it runs out before any usable result exists, the response is a `504` (see Errors). * Concurrent previews are limited per organization; exceeding the limit returns `429` with a `Retry-After` header. See [Rate limits](/api-reference/rate-limits) for header semantics. ## Errors | Status | `type` | When | | ------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 400 | `validation_error` | Invalid `query` (see [Create Watch](/api-reference/monitoring/watches/create)) | | 413 | `payload_too_large` | `query` payload exceeds 256 KB | | 429 | `rate_limited` | Another preview is already running for your organization, or the server's preview capacity is saturated. Honor `Retry-After` (about 20 seconds); the SDK retries automatically. | | 504 | `preview_timeout` | The time budget expired before any usable result existed. The response carries `retryable: false`; narrow the query (fewer offices, shorter `trial_window_days`, tighter filters) instead of retrying. | ```json 504 preview_timeout theme={null} { "error": { "type": "preview_timeout", "title": "Preview Timeout", "status": 504, "detail": "The preview could not produce a result within the server-side time budget.", "suggestion": "Narrow the watch query, add office or jurisdiction filters, reduce trial_window_days, or scope filters.trademarkIds, and retry.", "retryable": false }, "request_id": "req_8mQ2vXpL" } ``` ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/watches/preview" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: preview-owner-watch-2026-06-12" \ -d '{ "query": { "version": "v2", "filters": { "ownerId": "own_7pQmX3Lv" } }, "trial_window_days": 30 }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const preview = await signa.watches.preview({ query: { version: "v2", filters: { ownerId: "own_7pQmX3Lv" }, }, trial_window_days: 30, }); console.log(`${preview.estimated_match_count} alerts in the last 30 days`); ``` ```json Exact count theme={null} { "object": "watch_preview", "estimated_match_count": 17, "trial_window_days": 30, "request_id": "req_8mQ2vXpL" } ``` ```json Upper-bound estimate theme={null} { "object": "watch_preview", "estimated_match_count": 50000, "estimate_basis": "candidacy_upper_bound", "trial_window_days": 30, "request_id": "req_4nRvXq2T" } ``` ## Related Endpoints * [Create Watch](/api-reference/monitoring/watches/create) - turn a preview into a live watch * [Bulk Create Watches](/api-reference/monitoring/watches/bulk) - create up to 100 watches at once * [Rate limits](/api-reference/rate-limits) - request and concurrency limits # Resume Watch Source: https://docs.signa.so/api-reference/monitoring/watches/resume POST /v1/watches/{id}/resume Resume a paused watch ## Overview Requires the `portfolios:manage` scope. ## Path Parameters Watch ID (`wat_*`). ## Response Returns the updated `Watch` with `status: "active"`. ## Errors | Status | `type` | When | | ------ | ----------- | --------------------------------------------- | | 404 | `not_found` | Watch doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/watches/wat_8kLm2nPq/resume" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: resume-wat-8kLm2nPq-2026-06-12" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); await signa.watches.resume("wat_8kLm2nPq"); ``` ```json theme={null} { "id": "wat_8kLm2nPq", "object": "watch", "name": "Class 9 filings (US/EU)", "watch_type": "class", "status": "active", "alert_count_24h": 2, "last_alerted_at": "2026-07-05T09:12:00.000Z", "updated_at": "2026-07-06T08:31:00.000Z", "request_id": "req_4tYpL2Qn" } ``` ## Related Endpoints * [Pause Watch](/api-reference/monitoring/watches/pause) - stop evaluation without deleting the watch * [Update Watch](/api-reference/monitoring/watches/update) - the same status change via `PATCH` # Retrieve Watch Source: https://docs.signa.so/api-reference/monitoring/watches/retrieve GET /v1/watches/{id} Get a single watch by ID, including its 24-hour alert volume ## Overview Requires the `portfolios:manage` scope. ## Path Parameters Watch ID (`wat_*`). ## Response Returns the full `Watch` (see [Create Watch](/api-reference/monitoring/watches/create) for the shape), with `alert_count_24h` populated from the last 24 hours of alerts produced by this watch. ## Errors | Status | `type` | When | | ------ | ----------- | --------------------------------------------- | | 404 | `not_found` | Watch doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/watches/wat_8kLm2nPq" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const watch = await signa.watches.retrieve("wat_8kLm2nPq"); ``` ```json theme={null} { "id": "wat_8kLm2nPq", "object": "watch", "name": "Class 9 filings (US/EU)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US", "EU"] }, "trigger_events": ["trademark.created", "trademark.status_changed"] }, "delivery_mode": "always_per_alert", "status": "active", "alert_count_24h": 2, "last_alerted_at": "2026-07-05T09:12:00.000Z", "metadata": {}, "created_at": "2026-05-11T10:00:00.000Z", "updated_at": "2026-05-11T10:00:00.000Z", "request_id": "req_7pQmX3Lv" } ``` ## Related Endpoints * [List Watches](/api-reference/monitoring/watches/list) - `alert_count_24h` is always `null` there * [List Alerts for Watch](/api-reference/monitoring/alerts/list-for-watch) - the alerts behind that count * [Watch Diagnostics](/api-reference/monitoring/watches/diagnostics) - explain a specific match outcome # Update Watch Source: https://docs.signa.so/api-reference/monitoring/watches/update PATCH /v1/watches/{id} Update a watch (PATCH semantics: partial body) ## Overview Partial update: omitted fields are unchanged. Requires the `portfolios:manage` scope. ## Path Parameters Watch ID (`wat_*`). ## Body Parameters All fields optional. 1-255 chars. Replace the entire saved query DSL. Same validation rules as [Create Watch](/api-reference/monitoring/watches/create). Only `always_per_alert` is accepted (same as [Create Watch](/api-reference/monitoring/watches/create)). `active` or `paused`. Use [Pause](/api-reference/monitoring/watches/pause) / [Resume](/api-reference/monitoring/watches/resume) instead when you just want to toggle evaluation, they're equivalent but read more clearly in your own code. A third status, `disabled`, exists but isn't settable here: it's the state a watch moves to when you [delete](/api-reference/monitoring/watches/delete) it, and it's excluded from [List Watches](/api-reference/monitoring/watches/list) by default. Free-form metadata. ## Response Returns the updated `Watch`. ## Errors | Status | `type` | When | | ------ | ------------------ | --------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid `query`, an unsupported `delivery_mode`, or an unsupported `status` value | | 404 | `not_found` | Watch doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl -X PATCH "https://api.signa.so/v1/watches/wat_8kLm2nPq" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: rename-watch-2026-06-12" \ -d '{ "name": "Class 9 filings (US only)" }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); await signa.watches.update("wat_8kLm2nPq", { name: "Class 9 filings (US only)", }); ``` ```json theme={null} { "id": "wat_8kLm2nPq", "object": "watch", "name": "Class 9 filings (US only)", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US"] }, "trigger_events": ["trademark.created", "trademark.status_changed"] }, "delivery_mode": "always_per_alert", "status": "active", "alert_count_24h": 2, "last_alerted_at": "2026-07-05T09:12:00.000Z", "metadata": {}, "created_at": "2026-05-11T10:00:00.000Z", "updated_at": "2026-07-06T08:30:00.000Z", "request_id": "req_3vXq7RmT" } ``` ## Related Endpoints * [Retrieve Watch](/api-reference/monitoring/watches/retrieve) - fetch the current state of a watch * [Pause Watch](/api-reference/monitoring/watches/pause) - stop evaluation without deleting the watch * [Resume Watch](/api-reference/monitoring/watches/resume) - restart evaluation * [Delete Watch](/api-reference/monitoring/watches/delete) - stop evaluation permanently # Create Webhook Source: https://docs.signa.so/api-reference/monitoring/webhooks/create POST /v1/webhooks Register a URL that receives signed alert event POSTs ## Overview Registers a webhook endpoint and returns the signing `secret` once in the response. Store it immediately: list and retrieve responses redact it. A newly created endpoint's `status` is always `active`. Requires the `portfolios:manage` scope. ## Body Parameters HTTPS endpoint (HTTP is allowed in non-production environments only). Max 2048 chars. Optional human-readable description (max 2000 chars). 1-20 event types: `alert.created`, `trademark.status_changed`, and `office_action.issued`. Any other value returns `400`. The last two are accepted for subscription but are not yet emitted — they begin delivering when the API events projector is enabled. `webhook.test` is delivered only when you call [Test Webhook](/api-reference/monitoring/webhooks/test); you can't subscribe to it. Optional `ptf_*` scope. Applies to every event type, `alert.created` included: the endpoint receives an event only when its mark was a member of this portfolio at event time. Leave it unset to receive alerts for marks you don't own. Free-form metadata. ## Response Endpoint ID (`whk_*`). Always `"webhook_endpoint"`. Echo of input. Echo of input. Echo of input. Portfolio scope applied to every event type, or null. Always `"active"` on create. Starts at `1`. Plaintext signing secret. Returned only on this response and on [rotate-secret](/api-reference/monitoring/webhooks/rotate-secret). `0` on create. Null on create. Null on create. Echoed back. ISO timestamp. ISO timestamp. Request identifier. ## Errors | Status | `type` | When | | ------ | ------------------ | --------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid URL, unknown event type in `enabled_events`, or an HTTP URL in production | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/webhooks" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: create-prod-webhook-2026-06-12" \ -d '{ "url": "https://hooks.auroradigital.example.com/signa", "description": "Production alerts for Aurora Digital", "enabled_events": ["alert.created"] }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const wh = await signa.webhooks.create({ url: "https://hooks.auroradigital.example.com/signa", description: "Production alerts for Aurora Digital", enabled_events: ["alert.created"], }); // Store wh.secret in your secrets manager now: it is not returned again. console.log(wh.secret); ``` ```json theme={null} { "id": "whk_2mR8vNkT", "object": "webhook_endpoint", "url": "https://hooks.auroradigital.example.com/signa", "description": "Production alerts for Aurora Digital", "enabled_events": ["alert.created"], "portfolio_id": null, "status": "active", "secret_version": 1, "secret": "whsec_8f2a1c9e4b7d0a3f6c5e2b1d9a8f7e6c", "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "metadata": {}, "created_at": "2026-07-06T08:00:00.000Z", "updated_at": "2026-07-06T08:00:00.000Z", "request_id": "req_4tYpL2Qn" } ``` ## Related Endpoints * [Test Webhook](/api-reference/monitoring/webhooks/test) - send a synthetic ping before you go live * [Rotate Webhook Secret](/api-reference/monitoring/webhooks/rotate-secret) - roll the signing secret * [List Webhooks](/api-reference/monitoring/webhooks/list) - list your endpoints * [Webhooks guide](/guides/monitoring/webhooks) - signature verification and delivery behavior # Delete Webhook Source: https://docs.signa.so/api-reference/monitoring/webhooks/delete DELETE /v1/webhooks/{id} Stop an endpoint from receiving deliveries ## Overview Pending deliveries to the endpoint are cancelled; no further deliveries are attempted. Requires the `portfolios:manage` scope. ## Path Parameters Endpoint ID (`whk_*`). ## Response Endpoint ID. Always `"webhook_endpoint"`. Always `true`. Request identifier. ## Errors | Status | `type` | When | | ------ | ----------- | ------------------------------------------------ | | 404 | `not_found` | Endpoint doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl -X DELETE "https://api.signa.so/v1/webhooks/whk_2mR8vNkT" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: delete-whk-2mR8vNkT-2026-06-12" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); await signa.webhooks.delete("whk_2mR8vNkT"); ``` ```json theme={null} { "id": "whk_2mR8vNkT", "object": "webhook_endpoint", "deleted": true, "request_id": "req_7pQmX3Lv" } ``` ## Related Endpoints * [List Webhooks](/api-reference/monitoring/webhooks/list) - deleted endpoints are excluded * [Create Webhook](/api-reference/monitoring/webhooks/create) - register a new endpoint # List Webhooks Source: https://docs.signa.so/api-reference/monitoring/webhooks/list GET /v1/webhooks List webhook endpoints (secrets redacted) ## Overview Requires the `portfolios:manage` scope. ## Query Parameters Page size (1-100). Opaque cursor. ## Response `object: "list"` envelope of `Webhook` objects (see [Create Webhook](/api-reference/monitoring/webhooks/create) for the shape), with `secret` redacted. ## Errors | Status | `type` | When | | ------ | ------------------ | --------------------------- | | 400 | `validation_error` | Invalid `limit` or `cursor` | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/webhooks" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const endpoints = await signa.webhooks.list(); ``` ```json theme={null} { "object": "list", "data": [ { "id": "whk_2mR8vNkT", "object": "webhook_endpoint", "url": "https://hooks.auroradigital.example.com/signa", "description": "Production alerts for Aurora Digital", "enabled_events": ["alert.created"], "portfolio_id": null, "status": "active", "secret_version": 1, "consecutive_failures": 0, "last_success_at": "2026-07-05T09:12:31.000Z", "last_failure_at": null, "metadata": {}, "created_at": "2026-07-06T08:00:00.000Z", "updated_at": "2026-07-06T08:00:00.000Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_5tQmR2vX" } ``` ## Related Endpoints * [Create Webhook](/api-reference/monitoring/webhooks/create) - register a new endpoint * [Retrieve Webhook](/api-reference/monitoring/webhooks/retrieve) - get a single endpoint # List Deliveries Source: https://docs.signa.so/api-reference/monitoring/webhooks/list-deliveries GET /v1/webhooks/{id}/deliveries Audit log of delivery attempts for an endpoint ## Overview Requires the `portfolios:manage` scope. ## Path Parameters Endpoint ID (`whk_*`). ## Query Parameters Page size (1-100). Opaque cursor. ISO 8601 timestamp. Only deliveries with `created_at >= since` are returned. Useful for incremental polling: set `since` to the last `created_at` you saw and walk pages until exhausted. Composes cleanly with `cursor`. ## Response Always `"list"`. Array of `WebhookDelivery` rows. Raw UUID of the attempt row. Always `"webhook_delivery"`. Source endpoint (`whk_*`). Source alert (`alt_*`), when applicable. Stable event identifier, same value as the `webhook-id` header on the delivery. Customer events use an `evt_*` ID. Synthetic `webhook.test` deliveries use a `test_*` ID. Event type (e.g. `alert.created`). Retry attempt number (1-indexed). Opaque `dlv_*` identifier for this specific attempt. One of `pending`, `delivered`, `failed`, or `exhausted`. `exhausted` is the terminal state after all retry attempts have failed. Response code from your endpoint. First 1KB of the response body, truncated. ISO timestamp embedded in the `webhook-timestamp` header. ISO timestamp of the next scheduled retry. When the delivery succeeded. When the attempt was scheduled. Whether more pages exist. Cursor envelope. Request identifier. Delivery rows are retained for 30 days; queries for `since` values older than that return an empty list rather than an error. For alert history beyond 30 days, use [List Alerts](/api-reference/monitoring/alerts/list). ## Errors | Status | `type` | When | | ------ | ------------------ | ------------------------------------------------ | | 400 | `validation_error` | `since` is not a valid ISO 8601 timestamp | | 404 | `not_found` | Endpoint doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/webhooks/whk_2mR8vNkT/deliveries?since=2026-06-01T00:00:00Z" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const deliveries = await signa.webhooks.listDeliveries("whk_2mR8vNkT", { since: "2026-06-01T00:00:00Z", }); ``` ```json theme={null} { "object": "list", "data": [ { "id": "018f9b2e-0000-7000-8000-000000000010", "object": "webhook_delivery", "endpoint_id": "whk_2mR8vNkT", "alert_id": "alt_4tYpL2Qn", "event_id": "evt_8kLm2nPq", "event_type": "alert.created", "attempt": 1, "delivery_attempt_id": "dlv_9f3c1a7b2e4d6580", "status": "delivered", "http_status": 200, "response_body": "ok", "signature_timestamp": "2026-07-05T09:12:31.000Z", "next_retry_at": null, "delivered_at": "2026-07-05T09:12:31.500Z", "created_at": "2026-07-05T09:12:31.000Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_8mQ2vXpL" } ``` ## Related Endpoints * [Retrieve Delivery](/api-reference/monitoring/webhooks/retrieve-delivery) - fetch a single delivery attempt * [Redeliver Delivery](/api-reference/monitoring/webhooks/redeliver) - manually re-attempt a delivery * [Watch Diagnostics](/api-reference/monitoring/watches/diagnostics) - cross-reference `alert_id` against why an alert fired # Redeliver Delivery Source: https://docs.signa.so/api-reference/monitoring/webhooks/redeliver POST /v1/webhooks/{id}/deliveries/{did}/redeliver Manually re-attempt a delivery for an existing event ## Overview Queues a new delivery attempt for the same underlying event as the original delivery. The new attempt carries a fresh `webhook-timestamp` (so freshness checks on your side pass) and a new `delivery_attempt_id`, but the same `webhook-id` as the original, so your handler can still dedupe on it. This endpoint is billable. Requires the `portfolios:manage` scope. ## Only terminal deliveries are redeliverable Redelivery is accepted only when the delivery's automatic retry chain has reached a **terminal** state: `delivered`, `exhausted`, or a `failed` attempt with no scheduled retry. If the chain is still live — the attempt is `pending`, or `failed` with an automatic retry still scheduled — the request returns **409** with error type `redelivery_conflict`. The automatic retry schedule (7 attempts over \~24 hours) is already working that delivery; redelivering mid-chain would race it. Poll [`GET /v1/webhooks/{id}/deliveries/{did}`](/api-reference/monitoring/webhooks/retrieve-delivery) until the attempt is terminal, then redeliver. Repeated redelivery of the same terminal attempt is idempotent: the API returns the existing queued attempt's `delivery_attempt_id` instead of creating a duplicate. ## Path Parameters Endpoint ID (`whk_*`). Original delivery attempt UUID. ## Response Always `"webhook_redelivery"`. Opaque `dlv_*` identifier for this specific attempt. Request identifier. ## Errors | Status | `type` | When | | ------ | ----------- | ------------------------------------------------------------- | | 404 | `not_found` | Endpoint or delivery doesn't exist, or belongs to another org | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/webhooks/whk_2mR8vNkT/deliveries/018f9b2e-0000-7000-8000-000000000010/redeliver" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: redeliver-018f9b2e-2026-06-12" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const result = await signa.webhooks.redeliver( "whk_2mR8vNkT", "018f9b2e-0000-7000-8000-000000000010", ); ``` ```json theme={null} { "object": "webhook_redelivery", "delivery_attempt_id": "dlv_9f3c1a7b2e4d6580", "request_id": "req_4nRvXq2T" } ``` ## Related Endpoints * [List Deliveries](/api-reference/monitoring/webhooks/list-deliveries) - find the delivery to redeliver * [Test Webhook](/api-reference/monitoring/webhooks/test) - send a synthetic ping instead of replaying a real event # Retrieve Webhook Source: https://docs.signa.so/api-reference/monitoring/webhooks/retrieve GET /v1/webhooks/{id} Retrieve a single endpoint (secret redacted) ## Overview Requires the `portfolios:manage` scope. ## Path Parameters Endpoint ID (`whk_*`). ## Response A single `Webhook` (see [Create Webhook](/api-reference/monitoring/webhooks/create) for the shape), secret redacted. To get a fresh signing secret, use [Rotate Secret](/api-reference/monitoring/webhooks/rotate-secret). ## Errors | Status | `type` | When | | ------ | ----------- | ------------------------------------------------ | | 404 | `not_found` | Endpoint doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/webhooks/whk_2mR8vNkT" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const wh = await signa.webhooks.retrieve("whk_2mR8vNkT"); ``` ```json theme={null} { "id": "whk_2mR8vNkT", "object": "webhook_endpoint", "url": "https://hooks.auroradigital.example.com/signa", "description": "Production alerts for Aurora Digital", "enabled_events": ["alert.created"], "portfolio_id": null, "status": "active", "secret_version": 1, "consecutive_failures": 0, "last_success_at": "2026-07-05T09:12:31.000Z", "last_failure_at": null, "metadata": {}, "created_at": "2026-07-06T08:00:00.000Z", "updated_at": "2026-07-06T08:00:00.000Z", "request_id": "req_6tYpL2Qn" } ``` ## Related Endpoints * [Update Webhook](/api-reference/monitoring/webhooks/update) - change URL, events, or status * [Rotate Webhook Secret](/api-reference/monitoring/webhooks/rotate-secret) - get a fresh signing secret * [List Deliveries](/api-reference/monitoring/webhooks/list-deliveries) - delivery history for this endpoint # Retrieve Delivery Source: https://docs.signa.so/api-reference/monitoring/webhooks/retrieve-delivery GET /v1/webhooks/{id}/deliveries/{did} Retrieve a single delivery attempt ## Overview Requires the `portfolios:manage` scope. ## Path Parameters Endpoint ID (`whk_*`). Delivery attempt UUID (raw, not prefixed). ## Response A single `WebhookDelivery` row (see [List Deliveries](/api-reference/monitoring/webhooks/list-deliveries) for the shape). ## Errors | Status | `type` | When | | ------ | ----------- | ------------------------------------------------------------- | | 404 | `not_found` | Endpoint or delivery doesn't exist, or belongs to another org | ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/webhooks/whk_2mR8vNkT/deliveries/018f9b2e-0000-7000-8000-000000000010" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const delivery = await signa.webhooks.retrieveDelivery( "whk_2mR8vNkT", "018f9b2e-0000-7000-8000-000000000010", ); ``` ```json theme={null} { "id": "018f9b2e-0000-7000-8000-000000000010", "object": "webhook_delivery", "endpoint_id": "whk_2mR8vNkT", "alert_id": "alt_4tYpL2Qn", "event_id": "evt_8kLm2nPq", "event_type": "alert.created", "attempt": 1, "delivery_attempt_id": "dlv_9f3c1a7b2e4d6580", "status": "delivered", "http_status": 200, "response_body": "ok", "signature_timestamp": "2026-07-05T09:12:31.000Z", "next_retry_at": null, "delivered_at": "2026-07-05T09:12:31.500Z", "created_at": "2026-07-05T09:12:31.000Z", "request_id": "req_9pQ2vXpL" } ``` Customer events use an `evt_*` event ID. Synthetic `webhook.test` deliveries use a `test_*` ID. ## Related Endpoints * [List Deliveries](/api-reference/monitoring/webhooks/list-deliveries) - browse all delivery attempts for an endpoint * [Redeliver Delivery](/api-reference/monitoring/webhooks/redeliver) - manually re-attempt this delivery # Rotate Webhook Secret Source: https://docs.signa.so/api-reference/monitoring/webhooks/rotate-secret POST /v1/webhooks/{id}/rotate-secret Roll the signing secret with a 24-hour overlap window ## Overview Rotates the signing secret. The new secret is returned once in the response. The previous secret remains valid for 24 hours, during which every delivery is signed with both: `webhook-signature: v1, v1,` (space-separated, per the Standard Webhooks spec). Update your verifier to the new secret any time within the window; no deliveries are missed. A second rotation attempt while that window is still open returns `409` unless you pass `force: true`. Requires the `portfolios:manage` scope. ## Path Parameters Endpoint ID (`whk_*`). ## Query Parameters Convenience alias for `force` in the body. `?force=true` is equivalent to `{"force": true}` in the JSON body. ## Body Parameters Emergency use only. When `true`, skips the 24-hour overlap window and immediately invalidates the previous secret. Any receiver still using the previous secret fails signature verification on the next delivery. Without `force`, rotating while the previous secret is still within its 24-hour window returns `409`. Optional human-readable reason (max 500 chars), recorded on the audit event when `force: true`. Ignored otherwise. ## Response A `Webhook` with the new `secret` and bumped `secret_version`. ## Errors | Status | `type` | When | | ------ | ----------- | -------------------------------------------------------------------------------------- | | 404 | `not_found` | Endpoint doesn't exist or belongs to another org | | 409 | `conflict` | The previous secret's 24-hour overlap window is still active and `force` wasn't `true` | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/webhooks/whk_2mR8vNkT/rotate-secret" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: rotate-whk-2mR8vNkT-2026-06-12" ``` ```bash cURL (force) theme={null} curl -X POST "https://api.signa.so/v1/webhooks/whk_2mR8vNkT/rotate-secret" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: force-rotate-whk-2mR8vNkT-2026-06-12" \ -d '{"force": true, "reason": "Secret leaked in a client-side log"}' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const rotated = await signa.webhooks.rotateSecret("whk_2mR8vNkT"); // Store rotated.secret in your secrets manager: it is not returned again. console.log(rotated.secret); ``` ```json theme={null} { "id": "whk_2mR8vNkT", "object": "webhook_endpoint", "url": "https://hooks.auroradigital.example.com/signa", "description": "Production alerts for Aurora Digital", "enabled_events": ["alert.created"], "status": "active", "secret_version": 2, "secret": "whsec_1a2b3c4d5e6f7081920a1b2c3d4e5f60", "consecutive_failures": 0, "last_success_at": "2026-07-05T09:12:31.000Z", "last_failure_at": null, "metadata": {}, "created_at": "2026-07-06T08:00:00.000Z", "updated_at": "2026-07-06T09:00:00.000Z", "request_id": "req_3vXq7RmT" } ``` ## Related Endpoints * [Retrieve Webhook](/api-reference/monitoring/webhooks/retrieve) - check `secret_version` on an endpoint * [Webhooks guide](/guides/monitoring/webhooks) - signature verification # Test Webhook Source: https://docs.signa.so/api-reference/monitoring/webhooks/test POST /v1/webhooks/{id}/test Send a synthetic webhook.test ping (does not affect auto-disable) ## Overview Sends a synthetic `webhook.test` event through the same signing and retry pipeline as production deliveries, but a failed test does not count toward auto-disable. Use it to probe an endpoint without risking your production deliveries getting disabled. This endpoint isn't billed. Requires the `portfolios:manage` scope. No receiver yet? See [Testing deliveries before you have a receiver](/guides/monitoring/webhooks#testing-deliveries-before-you-have-a-receiver) for a pattern that lets you inspect real signed deliveries in minutes. ## Path Parameters Endpoint ID (`whk_*`). ## Response Always `"webhook_test"`. Opaque `dlv_*` identifier for this specific attempt. Track it via [List Deliveries](/api-reference/monitoring/webhooks/list-deliveries). Request identifier. ## Errors | Status | `type` | When | | ------ | ----------- | ------------------------------------------------ | | 404 | `not_found` | Endpoint doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/webhooks/whk_2mR8vNkT/test" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: test-whk-2mR8vNkT-2026-06-12" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const result = await signa.webhooks.test("whk_2mR8vNkT"); console.log(result.delivery_attempt_id); ``` ```json theme={null} { "object": "webhook_test", "delivery_attempt_id": "dlv_9f3c1a7b2e4d6580", "request_id": "req_5nRvXq2T" } ``` ## Related Endpoints * [List Deliveries](/api-reference/monitoring/webhooks/list-deliveries) - confirm the test ping arrived * [Create Webhook](/api-reference/monitoring/webhooks/create) - register a new endpoint # Update Webhook Source: https://docs.signa.so/api-reference/monitoring/webhooks/update PATCH /v1/webhooks/{id} Update an endpoint's URL, description, events, or status ## Overview Partial update: omitted fields are unchanged. Requires the `portfolios:manage` scope. ## Path Parameters Endpoint ID (`whk_*`). ## Body Parameters All fields optional. New URL (HTTPS in production). Description, or `null` to clear it. Replace the enabled-events list. Accepted values: `alert.created`, `trademark.status_changed`, `office_action.issued`. Set a `ptf_*` scope, or null to clear it. The scope applies to every event type, `alert.created` included: the endpoint receives an event only when its mark was a member of the portfolio at event time. `active` or `disabled`. Re-enabling an endpoint that was auto-disabled after repeated delivery failures resets `consecutive_failures` to `0`. Free-form metadata. ## Response The updated `Webhook` (secret redacted). ## Errors | Status | `type` | When | | ------ | ------------------ | ------------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid URL, unknown event type in `enabled_events`, or an unsupported `status` value | | 404 | `not_found` | Endpoint doesn't exist or belongs to another org | ## Code Examples ```bash cURL theme={null} curl -X PATCH "https://api.signa.so/v1/webhooks/whk_2mR8vNkT" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: reenable-whk-2mR8vNkT-2026-06-12" \ -d '{ "status": "active" }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); await signa.webhooks.update("whk_2mR8vNkT", { status: "active" }); ``` ```json theme={null} { "id": "whk_2mR8vNkT", "object": "webhook_endpoint", "url": "https://hooks.auroradigital.example.com/signa", "description": "Production alerts for Aurora Digital", "enabled_events": ["alert.created"], "portfolio_id": null, "status": "active", "secret_version": 1, "consecutive_failures": 0, "last_success_at": "2026-07-05T09:12:31.000Z", "last_failure_at": "2026-07-06T02:00:00.000Z", "metadata": {}, "created_at": "2026-07-06T08:00:00.000Z", "updated_at": "2026-07-06T09:15:00.000Z", "request_id": "req_8mQ2vXpL" } ``` ## Related Endpoints * [Retrieve Webhook](/api-reference/monitoring/webhooks/retrieve) - check current status and failure counts * [Test Webhook](/api-reference/monitoring/webhooks/test) - confirm an endpoint works after re-enabling it # Pagination Source: https://docs.signa.so/api-reference/pagination Cursor-based pagination for navigating large result sets Signa uses **cursor-based pagination** for all list and search endpoints. Cursors provide stable, consistent iteration over datasets that may change between requests: no skipped records, no duplicates, no performance degradation at depth. **Fixed-size bulk endpoints don't paginate.** Endpoints whose result size is bounded by the request itself never cursor, but their envelopes differ: bulk create ([`POST /v1/watches/bulk`](/api-reference/monitoring/watches/bulk)) and bulk lookup ([`POST /v1/alerts/lookup`](/api-reference/monitoring/alerts/lookup)) return an `object: "list"` envelope with `data` only (no `has_more`, no `pagination`), while batch get ([`POST /v1/trademarks/batch`](/api-reference/trademarks/batch-trademarks)) keeps the full list envelope with the fields pinned: `has_more` is always `false` and `pagination.cursor` is always `null`. [`GET /v1/offices/votes`](/api-reference/reference/list-office-votes) is bounded the same way — the votable vocabulary caps the list — so it accepts `limit` and `cursor` for generic list-client compatibility but **ignores** them: every vote comes back in one page with `has_more: false` and `pagination.cursor: null`. ## Basic Usage Your first request specifies a `limit` (items per page). The response includes a `cursor` and `has_more` flag: ```bash theme={null} curl -H "Authorization: Bearer sig_YOUR_KEY" \ "https://api.signa.so/v1/trademarks?limit=50" ``` **Response:** ```json theme={null} { "object": "list", "data": [ { "id": "tm_7d4e1f2a", "object": "trademark", "mark_text": "SIGNA", ... }, { "id": "tm_3b8c9d0e", "object": "trademark", "mark_text": "AURORA", ... } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6IjNiOGM5ZDAw..." }, "request_id": "req_abc123" } ``` To fetch the next page, pass the `cursor` from the previous response: ```bash theme={null} curl -H "Authorization: Bearer sig_YOUR_KEY" \ "https://api.signa.so/v1/trademarks?limit=50&cursor=eyJpZCI6IjNiOGM5ZDAw..." ``` When `has_more` is `false`, you have reached the end of the result set. The `cursor` field will be `null`. *** ## Parameters | Parameter | Type | Default | Description | | --------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `limit` | integer | 20 | Items per page. Min 1, max 100. | | `cursor` | string | (none) | Opaque cursor from a previous response. | | `sort` | string | varies | Sort field with optional `-` prefix for descending (e.g., `-filing_date`). | | `include_total` | boolean | `false` | Include `total_count` in the pagination object. Not accepted everywhere. The rule of thumb is that counted search and entity listings take it and the rest do not. Examples that accept it, not an exhaustive list: `/v1/trademarks`, `/v1/owners`, `/v1/attorneys`, `/v1/firms`, `/v1/entities`, the `/{id}/trademarks` listings under an owner, attorney, firm or entity, and `/v1/jurisdictions`. Endpoints that do not support it generally reject it with a `400` unknown-parameter error rather than ignoring it, so you can detect support by sending it once. Either way `pagination.total_count` is present only when the endpoint accepted the parameter, so read the field rather than assuming it. Check the endpoint's reference page. | *** ## Response Shape All list endpoints return the same structure. **Default response (without `include_total`):** ```json theme={null} { "object": "list", "data": [ ... ], "has_more": true, "pagination": { "cursor": "eyJpZCI6..." }, "request_id": "req_abc123" } ``` **With `?include_total=true`:** ```json theme={null} { "object": "list", "data": [ ... ], "has_more": true, "pagination": { "cursor": "eyJpZCI6...", "total_count": 12847, "total_count_approximate": false }, "request_id": "req_abc123" } ``` Key details: * `has_more` is always at the **top level** (not inside `pagination`). * `pagination.cursor` is `null` when `has_more` is `false`. * `pagination.total_count` is **only present** when `include_total=true` is passed as a query parameter. It is omitted by default. * `pagination.total_count_approximate` is emitted alongside `total_count` whenever the total is present. It is `false` when the count is exact and `true` when the count was capped (for example, the search index caps total hits at 10,000 for deep searches). Treat any exact count as a hard number and any approximate count as "at least this many". *** ## Cursor Stability Cursors encode a position in the result set, not an offset. This means: **Records inserted after your cursor was created** will not appear in your current pagination session. You will not see duplicates or gaps. **Records deleted after your cursor was created** are silently skipped. Your page may contain fewer items than the requested `limit` in rare cases. **Concurrent updates** do not affect cursor validity. Even if the underlying data changes, your cursor continues from where it left off. Cursors are scoped to a specific query, sort order, and filter combination. Changing any of these parameters invalidates the cursor: start a fresh pagination session instead. *** ## Cursor Invalidation Cursors have no fixed lifetime. Nothing in a cursor records when it was issued and nothing ages one out, so a cursor you checkpointed a week ago may still work. What can happen at any time is invalidation, and how it surfaces depends on the endpoint. **Signed cursors.** Most list endpoints (trademark search, the entity lists, events, citations, assignments, webhooks and the rest of the org-scoped surfaces) mint signed cursors. A malformed, tampered-with, or wrong-endpoint cursor is rejected, and so is any cursor minted under a cursor format that has since changed. Most of these endpoints also reject an explicitly blank `cursor=` rather than quietly restarting at page one, so send the parameter only when you have a value for it. All of these surface as `cursor_expired`: ```json theme={null} { "error": { "type": "cursor_expired", "title": "Cursor expired", "status": 400, "detail": "The pagination cursor has expired. Start a new query.", "retryable": false, "retry_after": null }, "request_id": "req_abc123" } ``` Rotating the signing key does not invalidate cursors by itself. The API verifies an incoming cursor against an ordered list of accepted keys, so a cursor signed with a previous key keeps verifying for as long as that key is still configured as a fallback. Cursors stop verifying once the key that signed them is dropped from that list. Treat `cursor_expired` as something that can occur on any page rather than a deadline you can plan around: handle it wherever you paginate and restart from the first page. For long-running syncs, checkpoint your own progress alongside the cursor (the last `updated_at` or date you processed, for example) so a restart can skip what you already have. **Cached reference catalogs behave differently.** `GET /v1/jurisdictions`, `GET /v1/classifications`, `GET /v1/offices` and `GET /v1/event-types` page through a fully cached catalog in memory, and their cursor is a plain catalog key (a jurisdiction code, a class number) rather than a signed token. These endpoints never return `cursor_expired`. A cursor value that is not in the catalog, including one carried over from another endpoint, is treated as past-the-end and returns `200` with an empty `data` array, and a blank `cursor=` starts again at the first page. So do not read "no error" as "more data" there: follow `has_more` and `pagination.cursor` exactly as returned, because a foreign cursor truncates the walk silently and a blank one silently restarts it. A cursor replayed against a request that changed one of the parameters the endpoint binds into its cursor is a different error: `cursor_invalid`, also a `400`. What is bound varies by endpoint. Trademark search binds `sort`, `limit`, the filter set and the query text; the citations endpoints bind only the endpoint and the sort direction, so changing a filter or `limit` mid-walk is accepted there. See the endpoint's reference page for its exact contract, and keep every parameter identical across pages if you want one rule that holds everywhere. Branch on both slugs if you retry pagination automatically. In each case the fix is the same, restart from the first page under the new parameters. Cursors are opaque strings. Do not parse, decode, or construct them. Their internal format may change without notice. *** ## Sorting You can sort results using the `sort` parameter. The cursor encodes the sort position, so **you must use the same `sort` value for every page in a session**. Supported sort fields vary by endpoint: | Endpoint | Supported Sort Fields | Default | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `GET` and `POST /v1/trademarks` | `filing_date`, `registration_date`, `expiry_date`, `renewal_due_date`, `updated_at`, `publication_date`, `termination_date`, `office_code`, `jurisdiction_code` | Relevance when `q` is present; unordered (fast) when no `q` | | `GET /v1/owners` | `name`, `updated_at` | `name` | Sort direction is specified with a `-` prefix for descending (no prefix = ascending): ```bash theme={null} GET /v1/trademarks?sort=-filing_date&limit=50 GET /v1/trademarks?sort=filing_date&limit=50 # ascending ``` When a text query (`q`) is provided, the default sort is by relevance. When only filters are used (no `q`), results are returned in index order (fast, unordered). Providing an explicit `sort` overrides the default in both cases. *** ## Total Count By default, the `total_count` is not included in the response. To include it on most list endpoints, pass `include_total=true` (the fixed-size endpoints in the note above don't paginate, so they don't take it): ```bash theme={null} GET /v1/trademarks?q=nike&limit=50&include_total=true GET /v1/owners?q=nike&limit=50&include_total=true GET /v1/attorneys?q=smith&limit=50&include_total=true GET /v1/firms?q=smith&limit=50&include_total=true GET /v1/jurisdictions?include_total=true ``` ```json theme={null} { "object": "list", "data": [ ... ], "has_more": true, "pagination": { "cursor": "eyJpZCI6...", "total_count": 12847, "total_count_approximate": false } } ``` The shape is the same for every list endpoint: * `pagination.total_count`: the count itself (integer, only present when `include_total=true`). * `pagination.total_count_approximate`: `false` when the count is exact; `true` when the underlying engine capped the count (the search index caps at 10,000 for deep search queries). Computing the total count requires a separate database or search-index query and can be expensive on large datasets. Only request it when you need to display a progress indicator or total count in your UI. Avoid including it on every page request. *** ## SDK Support The TypeScript SDK wraps pagination with async iterators, `toArray()`, and manual page control. See the [TypeScript SDK](/sdk/typescript) reference for usage patterns. # Attorney Analytics Source: https://docs.signa.so/api-reference/parties/attorney-analytics GET /v1/attorneys/{id}/analytics Retrieve a composed analytics report for an attorney ## When to use this Use this endpoint when evaluating counsel for a new filing or comparing an opponent's representation history. It summarizes the attorney's live trademark portfolio footprint. Reports are invalidated on party updates, with a 24-hour TTL backstop; `Server-Timing` reports `cache;desc="hit"` or `cache;desc="miss"`. ## Path Parameters Attorney ID (`att_*`). ## Code Examples ```bash cURL theme={null} curl https://api.signa.so/v1/attorneys/att_K8mN2R3j/analytics \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const report = await signa.attorneys.analytics("att_K8mN2R3j"); console.log(report.portfolio.mark_count, report.portfolio.active_count); ``` ```json Response theme={null} { "object": "analytics_report", "subject": { "id": "att_K8mN2R3j", "object": "attorney", "name": "JANE DOE" }, "portfolio": { "mark_count": 318, "active_count": 260, "dead_count": 58, "status_distribution": { "registered": 260, "abandoned": 58 }, "class_distribution": { "35": 90 }, "jurisdiction_spread": { "US": 318 }, "filing_trend": { "2025": 31 } }, "litigation": null, "transactions": null, "generated_at": "2026-07-11T08:00:00.000Z" } ``` `litigation` and `transactions` are always `null` because the data model has no attorney litigation or transaction grain. Owner/entity transaction reports expose conveyance-type buckets, assignee-side `acquired_count`, assignor-side `divested_count`, first/last recording dates, and lien counts. A security interest is unreleased until a non-purged release points back to its reel/frame; `marks_with_liens` deduplicates marks on those unreleased recordings. Acquirers use owner/entity transaction blocks to gauge brand-transaction history, and lenders use them to check for unreleased liens before extending credit. See [list assignments](/api-reference/records/transactions/search-assignments), [get an assignment](/api-reference/records/transactions/retrieve-assignment), and the [chain-of-title and lien diligence guide](/guides/chain-of-title-lien-diligence) for recording-level detail. # Attorney Clients Source: https://docs.signa.so/api-reference/parties/attorney-clients GET /v1/attorneys/{id}/clients List the owners (clients) represented by an attorney across all trademarks ## Overview Returns the owners that an attorney has represented on trademark filings. Each row reports the number of marks shared between the attorney and that owner, sorted by `shared_trademark_count` descending. ## Path Parameters Attorney ID (`att_...`). ## Query Parameters Page size (1-100). Pagination cursor from a previous response. ## Response Array of client owner summary records. Owner ID (`own_...`). Display name of the owner. Normalized name used for search and deduplication. Owner country code. Owner entity type (e.g. `corporation`, `individual`, `partnership`). Number of trademarks where this attorney represents this owner. ```json theme={null} { "object": "list", "data": [ { "id": "own_R3jK9mN2", "object": "owner", "name": "Apple Inc.", "canonical_name": "APPLE INC", "country_code": "US", "entity_type": "corporation", "shared_trademark_count": 47 } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_xyz" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/attorneys/att_3kPq9X/clients?limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const clients = await signa.attorneys.clients("att_3kPq9X", { limit: 20 }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------- | | 400 | `validation_error` | Invalid attorney ID | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Attorney not found | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Attorney](/api-reference/parties/retrieve-attorney): full attorney detail * [Attorney Trademarks](/api-reference/parties/attorney-trademarks): marks handled by this attorney * [Search Owners](/api-reference/parties/search-owners): search owners independently # Attorney Trademarks Source: https://docs.signa.so/api-reference/parties/attorney-trademarks GET /v1/attorneys/{id}/trademarks List an attorney's trademarks with the standard trademark filters ## Overview Returns a paginated list of trademarks an attorney has prosecuted, scoped automatically to the parent attorney. Use this for attorney portfolio views, client roster auditing, or competitive intelligence on prosecution counsel. When neither `sort` nor `q` is supplied, results default to most-recent-first (`-filing_date`); a `q` text query instead ranks by relevance. Arrays are comma-separated, date ranges use flat `_gte`, `_gt`, `_lte`, and `_lt` operators, and booleans must be the literal strings `true` or `false`. ## Path Parameters Attorney ID (`att_...`). ## Query Parameters The full [Search Trademarks](/api-reference/trademarks/search-trademarks) filter set applies (status, mark classification, identifiers, geography, Nice/Vienna codes, date ranges, public-company filters, and boolean flags), plus: Search query text within this attorney's trademarks. Sort field(s), comma-separated, `-` prefix for descending. Filter to marks owned by a specific owner (`own_...`). Filter to marks handled through a specific firm (`firm_...`). Comma-separated optional row projections. `full_goods_services` returns full `classifications[].goods_services_text` instead of the truncated summary text. Sparse top-level field projection, comma-separated. `id` and `object` are always retained. Unknown names return `400`. Items per page (1-100). Pagination cursor from a previous response. ## Response Returns a `list` envelope of trademark summary objects belonging to this attorney. ```json theme={null} { "object": "list", "data": [ { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "AURORA", "status": { "primary": "active", "stage": "registered" }, "office_code": "US", "jurisdiction_code": "US", "filing_date": "2023-04-12", "registration_date": "2024-09-18", "classifications": [ { "nice_class": 9, "goods_services_text": "Downloadable software for digital image processing" }, { "nice_class": 35, "goods_services_text": "Online retail store services featuring digital media" }, { "nice_class": 42, "goods_services_text": "Software as a service (SaaS) featuring media management tools" } ], "owners": [ { "id": "own_7nQp3xYz", "name": "Aurora Digital Inc.", "country_code": "US", "entity_id": "ent_7nQp3xYz", "entity_id_type": "derived" } ] } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6..." }, "request_id": "req_xyz" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/attorneys/att_3kPq9X/trademarks?status_stage=registered&offices=US,EM&nice_classes=9,42&limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const page = await signa.attorneys.trademarks("att_3kPq9X", { status_stage: ["registered"], offices: ["US", "EM"], nice_classes: [9, 42], limit: 20, }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------- | | 400 | `validation_error` | Invalid query parameter or date range | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Attorney ID does not exist | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Attorney](/api-reference/parties/retrieve-attorney): attorney profile and stats * [Attorney Clients](/api-reference/parties/attorney-clients): owners represented by this attorney * [Owner Trademarks](/api-reference/parties/owner-trademarks): same view scoped to an owner * [Search Trademarks](/api-reference/trademarks/search-trademarks): search across all attorneys # Entity Analytics Source: https://docs.signa.so/api-reference/parties/entity-analytics GET /v1/entities/{id}/analytics Retrieve a composed analytics report across an entity's resolved owner family ## When to use this Audit a conglomerate's global trademark footprint across all its resolved subsidiaries in one call. The report deduplicates marks shared by multiple member owners and groups Madrid designations under their International Registration. Reports use a Valkey cache with a 24-hour TTL backstop and are invalidated by the nightly stats-compute `entity.updated` wave. Proceedings ingestion does not emit owner-grain events directly, so a newly ingested proceeding appears within about 24 hours. `Server-Timing` reports `cache;desc="hit"` or `cache;desc="miss"`. ## Path Parameters Entity ID (`ent_*`). ## Code Examples ```bash cURL theme={null} curl https://api.signa.so/v1/entities/ent_R3jK9mN2/analytics \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const report = await signa.entities.analytics("ent_R3jK9mN2"); console.log(report.portfolio.mark_count, report.portfolio.active_count); ``` ```json Response theme={null} { "object": "analytics_report", "subject": { "id": "ent_R3jK9mN2", "object": "entity", "name": "ACME HOLDINGS" }, "portfolio": { "mark_count": 1842, "active_count": 1510, "dead_count": 318, "status_distribution": { "registered": 1510, "abandoned": 318, "unknown": 14 }, "class_distribution": { "9": 520 }, "jurisdiction_spread": { "US": 940 }, "filing_trend": { "2025": 96 } }, "litigation": { "subject": { "owner_id": null, "entity_id": "ent_R3jK9mN2" }, "proceedings_total": 7, "as_challenger": { "total": 5, "by_type": { "opposition": 4, "cancellation": 1 }, "outcomes": { "challenger_won": 3, "challenger_lost": 1, "settled": 1 }, "decided": 4, "win_rate": 0.75, "avg_duration_days": 108.5 }, "as_defendant": { "total": 2, "by_type": { "opposition": 2 }, "outcomes": { "challenger_lost": 1, "pending": 1 }, "decided": 1, "win_rate": 1, "avg_duration_days": 92 } }, "transactions": { "transaction_count": 4, "by_type": { "assignment": 2, "security_interest": 1, "release": 1 }, "unreleased_security_interests": 0, "marks_with_liens": 0, "acquired_count": 2, "divested_count": 1, "first_transaction_date": "2019-06-15", "last_transaction_date": "2024-03-01" }, "generated_at": "2026-07-12T08:00:00.000Z" } ``` ## Litigation fields `as_challenger` covers disputes the entity's member owners brought as opponents or petitioners. `as_defendant` covers disputes against marks those owners currently hold. `win_rate` is wins divided by `decided`, and is `null` when `decided` is zero. `decided` excludes settled, withdrawn, and pending proceedings. `avg_duration_days` is the mean number of days from filing to decision when both dates are available. ## Transaction fields `by_type` groups distinct family recordings into conveyance-type buckets. `acquired_count` counts recordings with a family owner as assignee; `divested_count` counts those with one as assignor. The first and last dates bound the recorded family transaction history. `unreleased_security_interests` counts security-interest recordings without a non-purged release pointing back to the same reel/frame, and `marks_with_liens` deduplicates marks attached to those recordings. Reel/frame linkage is authoritative: a security interest remains unreleased until a release recording points to it. Acquirers use this block to gauge a brand family's transaction history, while lenders check for unreleased liens before extending credit. See [list assignments](/api-reference/records/transactions/search-assignments), [get an assignment](/api-reference/records/transactions/retrieve-assignment), and the [chain-of-title and lien diligence guide](/guides/chain-of-title-lien-diligence) for recording-level detail. ## Large entities Entities resolve to at most 10,000 live member owners. Larger portfolios return `422 entity_too_large` with `error.reason` set to `member_owners_too_large`, plus `member_count` and `member_count_limit`. If resolution exceeds the bounded corporate family graph walk instead, the same `422 entity_too_large` response uses `error.reason: family_graph_too_large`, plus `related_entity_limit` and `depth_limit`. # Entity Family Source: https://docs.signa.so/api-reference/parties/entity-family GET /v1/entities/{id}/family GLEIF corporate parent and direct subsidiaries of a resolved entity ## Overview Returns the **GLEIF-curated direct family** of an entity: its corporate parent and its direct subsidiaries, one level deep. This is the curated, direct-consolidation view of corporate structure, distinct from the raw, all-relationship-types graph at [Owner Related](/api-reference/parties/owner-related). Family edges come from GLEIF Level 2 (the `IS_DIRECTLY_CONSOLIDATED_BY` relationship). Coverage is therefore limited to LEI-reporting companies: an **absent edge does not mean the entity has no parent or subsidiaries**, only that none is recorded in GLEIF. An entity not linked to any others, or a company profile with no GLEIF edges, returns `{ parent: null, children: [] }` (a valid `200`, not a `404`). "Family" is a **group-level** relationship: related companies in a corporate tree. It is never an identity claim: `Pfizer Inc (US)` and `Pfizer AG (CH)` are distinct entities connected by family, not the same entity. ## Path Parameters Entity ID (e.g. `ent_R3jK9mN2`), including the derived `ent_` form. ## Response Always `entity_family`. The corporate parent, or `null` when none is recorded. Parent entity ID (`ent_*`). Always `entity`. Parent display name. ISO country code. Always `parent`. Always `gleif`. Direct subsidiaries, sorted by name. Each carries `relationship: "direct_subsidiary"` and `source: "gleif"`. Always `gleif`. A human-readable note that edges are GLEIF Level 2 and cover LEI-reporting companies only. Unique request identifier for support. ```json theme={null} { "object": "entity_family", "parent": null, "children": [ { "id": "ent_Qz8mN3rT", "object": "entity", "name": "Apple Distribution International Limited", "country_code": "IE", "relationship": "direct_subsidiary", "source": "gleif" } ], "source": "gleif", "coverage_caveat": "Family edges are sourced from GLEIF Level 2 (IS_DIRECTLY_CONSOLIDATED_BY) and cover LEI-reporting companies only. An absent edge does not imply the absence of a corporate relationship.", "request_id": "req_xyz789" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/entities/ent_R3jK9mN2/family" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const family = await signa.entities.family("ent_R3jK9mN2"); console.log(family.parent?.name, family.children.length); ``` To list trademarks across the **whole** corporate group (parent + all descendants), use the `entity_group` filter on [Search Trademarks](/api-reference/trademarks/search-trademarks): `GET /v1/trademarks?entity_group=ent_...`. ## Errors | Status | Type | Description | | ------ | --------------- | -------------------------------------------------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Entity ID does not exist (or a derived id for a suppressed or merged-away owner) | | 410 | `entity_merged` | The entity was fused into another; follow `merged_into` | ## Related Endpoints * [Retrieve Entity](/api-reference/parties/retrieve-entity): entity detail with members * [Entity Trademarks](/api-reference/parties/entity-trademarks): marks across all member owners * [Owner Related](/api-reference/parties/owner-related): the raw owner-level GLEIF graph (all relationship types, both directions) # Entity Trademarks Source: https://docs.signa.so/api-reference/parties/entity-trademarks GET /v1/entities/{id}/trademarks List all trademarks across every member owner of a resolved entity ## Overview Returns a paginated, summary-tier list of trademarks held by **all member owners** of the entity, the global-portfolio view. Where [Owner Trademarks](/api-reference/parties/owner-trademarks) is one office's record, this fans out across every office the entity has been resolved into, so a single call returns the company's worldwide portfolio. When neither `sort` nor `q` is supplied, results default to most-recent-first (`-filing_date`); a `q` text query instead ranks by relevance. Arrays are comma-separated, date ranges use flat `_gte` / `_gt` / `_lte` / `_lt` operators, and booleans must be the literal strings `true` or `false`. A derived `ent_` id is accepted and degrades to that one owner's marks. ## Path Parameters Entity ID (e.g. `ent_R3jK9mN2`), including the derived `ent_` form. ## Query Parameters The full [Search Trademarks](/api-reference/trademarks/search-trademarks) filter set applies (status, mark classification, identifiers, geography, Nice/Vienna codes, date ranges, public-company filters, and boolean flags), plus: Relevance text search within the entity's marks. When `true`, expands the portfolio to this entity **and all of its GLEIF family-tree descendants** (a bounded, cycle-safe downward walk via `parent_entity_id`), then runs the same query over the combined member-owner set. Default `false` returns this entity only. A family graph larger than the walk bounds returns `422` with `error.reason` `family_graph_too_large`. Use this to pull a listed parent and every subsidiary in one call. Sort field(s), comma-separated, `-` prefix for descending. Comma-separated optional row projections. `full_goods_services` returns full `classifications[].goods_services_text` instead of the truncated summary text. Sparse top-level field projection, comma-separated. `id` and `object` are always retained. Unknown names return `400`. Items per page (1-100). Pagination cursor from a previous response. ## Response Array of trademark summary objects across all member owners. Each row's `owners[]` carries `entity_id` and `entity_id_type` so you can confirm the grouping. Whether more marks are available. Cursor for the next page. ```json theme={null} { "object": "list", "data": [ { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "APPLE", "status": { "primary": "active", "stage": "registered" }, "office_code": "US", "jurisdiction_code": "US", "filing_date": "2018-04-12", "owners": [ { "id": "own_R3jK9mN2", "name": "Apple Inc.", "country_code": "US", "entity_id": "ent_R3jK9mN2", "entity_id_type": "resolved" } ] } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6ImFiYyJ9" }, "request_id": "req_fY5pQ1rS" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/entities/ent_R3jK9mN2/trademarks?status_primary=active&limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const page = await signa.entities.trademarks("ent_R3jK9mN2", { status_primary: "active", limit: 20, }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid query parameters or date range | | 401 | `unauthorized` | Missing or invalid API key | | 404 | `not_found` | Entity ID does not exist (or a derived id for a suppressed or merged-away owner) | | 410 | `entity_merged` | The entity was fused into another; follow `merged_into` | | 422 | `entity_too_large` | The entity resolves to more than 10,000 member owners. `error.reason` is `member_owners_too_large` (carries `member_count` / `member_count_limit`). The sibling `family_graph_too_large` reason is fired only by `?entity_group=` family walks on [Search Trademarks](/api-reference/trademarks/search-trademarks), not by this per-entity endpoint. | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Entity](/api-reference/parties/retrieve-entity): entity detail with members * [Entity Family](/api-reference/parties/entity-family): GLEIF corporate parent and subsidiaries * [Search Trademarks](/api-reference/trademarks/search-trademarks): search across all entities (supports `entity_id` and `entity_group`) # Firm Analytics Source: https://docs.signa.so/api-reference/parties/firm-analytics GET /v1/firms/{id}/analytics Retrieve a composed analytics report for a firm ## When to use this Use this endpoint to compare firms for portfolio prosecution work or understand which jurisdictions and classes dominate a firm's practice. It returns the firm's live trademark portfolio in one report. Reports are invalidated on party updates, with a 24-hour TTL backstop; `Server-Timing` reports `cache;desc="hit"` or `cache;desc="miss"`. ## Path Parameters Firm ID (`firm_*`). ## Code Examples ```bash cURL theme={null} curl https://api.signa.so/v1/firms/firm_N2R3jK8m/analytics \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const report = await signa.firms.analytics("firm_N2R3jK8m"); console.log(report.portfolio.mark_count, report.portfolio.active_count); ``` ```json Response theme={null} { "object": "analytics_report", "subject": { "id": "firm_N2R3jK8m", "object": "firm", "name": "EXAMPLE LLP" }, "portfolio": { "mark_count": 842, "active_count": 700, "dead_count": 142, "status_distribution": { "registered": 700, "expired": 142 }, "class_distribution": { "42": 180 }, "jurisdiction_spread": { "US": 620, "EU": 222 }, "filing_trend": { "2025": 95 } }, "litigation": null, "transactions": null, "generated_at": "2026-07-11T08:00:00.000Z" } ``` `litigation` and `transactions` are always `null` because the data model has no firm litigation or transaction grain. Owner/entity transaction reports expose conveyance-type buckets, assignee-side `acquired_count`, assignor-side `divested_count`, first/last recording dates, and lien counts. A security interest is unreleased until a non-purged release points back to its reel/frame; `marks_with_liens` deduplicates marks on those unreleased recordings. Acquirers use owner/entity transaction blocks to gauge brand-transaction history, and lenders use them to check for unreleased liens before extending credit. See [list assignments](/api-reference/records/transactions/search-assignments), [get an assignment](/api-reference/records/transactions/retrieve-assignment), and the [chain-of-title and lien diligence guide](/guides/chain-of-title-lien-diligence) for recording-level detail. # Firm Attorneys Source: https://docs.signa.so/api-reference/parties/firm-attorneys GET /v1/firms/{id}/attorneys List the attorneys at a specific law firm ## Overview Returns the attorneys associated with a firm. Each row includes the attorney's filing volume, grant rate, and most recent filing date so you can rank within the firm. ## Path Parameters Firm ID (`firm_...`). ## Query Parameters Page size (1-100). Pagination cursor from a previous response. ## Response Array of attorney records at this firm. Attorney ID (`att_...`). Display name of the attorney. Normalized name used for entity resolution. Firm display name. Two-letter country code. Total trademarks attributed to this attorney. Share of concluded prosecutions (registered + expired + cancelled) that were ever granted (0-1). ISO date of the most recent filing handled. ISO 8601 record creation timestamp. ISO 8601 last-updated timestamp. ```json theme={null} { "object": "list", "data": [ { "id": "att_3kPq9X", "object": "attorney", "name": "Jane R. Smith", "canonical_name": "JANE R SMITH", "firm_name": "Smith & Associates LLP", "country_code": "US", "trademark_count": 1284, "grant_rate": 0.81, "latest_filing": "2026-04-08", "created_at": "2024-09-12T11:42:01Z", "updated_at": "2026-04-09T03:18:22Z" } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6..." }, "request_id": "req_xyz" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/firms/firm_2kLm9X/attorneys?limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const page = await signa.firms.attorneys("firm_2kLm9X", { limit: 20 }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------- | | 400 | `validation_error` | Invalid firm ID | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Firm not found | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Firm](/api-reference/parties/retrieve-firm): full firm detail * [Firm Trademarks](/api-reference/parties/firm-trademarks): marks handled by this firm * [Search Attorneys](/api-reference/parties/search-attorneys): search attorneys across firms # Firm Trademarks Source: https://docs.signa.so/api-reference/parties/firm-trademarks GET /v1/firms/{id}/trademarks List a firm's trademarks with the standard trademark filters ## Overview Returns a paginated list of trademarks where one of the attorneys belongs to the specified firm. Use this for firm-level competitive intelligence, client portfolio sweeps, or auditing prosecution patterns. When neither `sort` nor `q` is supplied, results default to most-recent-first (`-filing_date`); a `q` text query instead ranks by relevance. Arrays are comma-separated, date ranges use flat `_gte`, `_gt`, `_lte`, and `_lt` operators, and booleans must be the literal strings `true` or `false`. ## Path Parameters Firm ID (`firm_...`). ## Query Parameters The full [Search Trademarks](/api-reference/trademarks/search-trademarks) filter set applies (status, mark classification, identifiers, geography, Nice/Vienna codes, date ranges, public-company filters, and boolean flags), plus: Search query text within this firm's trademarks. Sort field(s), comma-separated, `-` prefix for descending. Filter to marks owned by a specific owner (`own_...`). Filter to marks prosecuted by a specific attorney at this firm (`att_...`). Comma-separated optional row projections. `full_goods_services` returns full `classifications[].goods_services_text` instead of the truncated summary text. Sparse top-level field projection, comma-separated. `id` and `object` are always retained. Unknown names return `400`. Items per page (1-100). Pagination cursor from a previous response. ## Response Returns a `list` envelope of trademark summary objects. ```json theme={null} { "object": "list", "data": [ { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "AURORA", "status": { "primary": "active", "stage": "registered" }, "office_code": "US", "jurisdiction_code": "US", "filing_date": "2023-04-12", "registration_date": "2024-09-18", "classifications": [ { "nice_class": 9, "goods_services_text": "Downloadable software for digital image processing" }, { "nice_class": 35, "goods_services_text": "Online retail store services featuring digital media" }, { "nice_class": 42, "goods_services_text": "Software as a service (SaaS) featuring media management tools" } ], "owners": [ { "id": "own_7nQp3xYz", "name": "Aurora Digital Inc.", "country_code": "US", "entity_id": "ent_7nQp3xYz", "entity_id_type": "derived" } ] } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6..." }, "request_id": "req_xyz" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/firms/firm_2kLm9X/trademarks?status_stage=registered&filing_date_gte=2024-01-01&limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const page = await signa.firms.trademarks("firm_2kLm9X", { status_stage: ["registered"], filing_date_gte: "2024-01-01", limit: 20, }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------- | | 400 | `validation_error` | Invalid query parameter or date range | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Firm ID does not exist | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Firm](/api-reference/parties/retrieve-firm): firm profile and stats * [Firm Attorneys](/api-reference/parties/firm-attorneys): attorneys at this firm * [Attorney Trademarks](/api-reference/parties/attorney-trademarks): same view scoped to one attorney # Owner Analytics Source: https://docs.signa.so/api-reference/parties/owner-analytics GET /v1/owners/{id}/analytics Retrieve a composed analytics report for an owner ## When to use this Use this endpoint to size a competitor's trademark portfolio before filing an opposition or planning clearance work. It returns current portfolio totals and distributions in one request. Reports use a Valkey cache with a 24-hour TTL backstop and are invalidated by the nightly stats-compute `owner.updated` wave. Proceedings ingestion does not emit owner-grain events directly, so a newly ingested proceeding appears within about 24 hours. `Server-Timing` reports `cache;desc="hit"` or `cache;desc="miss"`. ## Path Parameters Owner ID (`own_*`). ## Code Examples ```bash cURL theme={null} curl https://api.signa.so/v1/owners/own_R3jK9mN2/analytics \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const report = await signa.owners.analytics("own_R3jK9mN2"); console.log(report.portfolio.mark_count, report.portfolio.active_count); ``` ```json Response theme={null} { "object": "analytics_report", "subject": { "id": "own_R3jK9mN2", "object": "owner", "name": "APPLE INC" }, "portfolio": { "mark_count": 1204, "active_count": 980, "dead_count": 211, "status_distribution": { "registered": 980, "abandoned": 211, "unknown": 13 }, "class_distribution": { "9": 410 }, "jurisdiction_spread": { "US": 860 }, "filing_trend": { "2025": 72 } }, "litigation": { "subject": { "owner_id": "own_R3jK9mN2", "entity_id": null }, "proceedings_total": 3, "as_challenger": { "total": 2, "by_type": { "opposition": 1, "cancellation": 1 }, "outcomes": { "challenger_won": 1, "settled": 1 }, "decided": 1, "win_rate": 1, "avg_duration_days": 124.5 }, "as_defendant": { "total": 1, "by_type": { "opposition": 1 }, "outcomes": { "challenger_lost": 1 }, "decided": 1, "win_rate": 1, "avg_duration_days": 87 } }, "transactions": { "transaction_count": 4, "by_type": { "assignment": 2, "security_interest": 1, "release": 1 }, "unreleased_security_interests": 0, "marks_with_liens": 0, "acquired_count": 2, "divested_count": 1, "first_transaction_date": "2019-06-15", "last_transaction_date": "2024-03-01" }, "generated_at": "2026-07-11T08:00:00.000Z" } ``` ## Litigation fields `as_challenger` covers disputes the owner brought as an opponent or petitioner. `as_defendant` covers disputes against marks the owner currently holds. `win_rate` is wins divided by `decided`, and is `null` when `decided` is zero. `decided` excludes settled, withdrawn, and pending proceedings. `avg_duration_days` is the mean number of days from filing to decision when both dates are available. ## Transaction fields `by_type` groups distinct recordings into conveyance-type buckets. `acquired_count` counts recordings where the owner is an assignee; `divested_count` counts those where it is an assignor. The first and last dates bound the recorded transaction history. `unreleased_security_interests` counts security-interest recordings without a non-purged release pointing back to the same reel/frame, and `marks_with_liens` deduplicates marks attached to those recordings. Reel/frame linkage is authoritative: a security interest remains unreleased until a release recording points to it. Acquirers use this block to gauge a brand's transaction history, while lenders check for unreleased liens before extending credit. See [list assignments](/api-reference/records/transactions/search-assignments), [get an assignment](/api-reference/records/transactions/retrieve-assignment), and the [chain-of-title and lien diligence guide](/guides/chain-of-title-lien-diligence) for recording-level detail. # Owner Related Entities Source: https://docs.signa.so/api-reference/parties/owner-related GET /v1/owners/{id}/related Find entities related to an owner through corporate structure ## Overview Returns entities related to the specified owner. Relationships are primarily sourced from GLEIF Level 2 corporate parent/subsidiary data. Use this for M\&A due diligence, corporate family mapping, and understanding ownership structures behind trademark portfolios. ## Path Parameters Owner ID (e.g., `own_Rj3kL9mN`) ## Query Parameters Filter by relationship direction: `parent` or `child`. Omit to return both. Items per page (max 100) Pagination cursor from previous response ## Response Owner ID of the related entity (`own_*`) Name of the related entity ISO country code GLEIF relationship type (e.g., `IS_DIRECTLY_CONSOLIDATED_BY`) Relationship status (e.g., `ACTIVE`) Ownership percentage (may be null) ISO date when the relationship started ISO date when the relationship ended (null if current) `parent` or `child` Data source (e.g., `gleif_level2`) ```json theme={null} { "object": "list", "data": [ { "related_owner_id": "own_Tk5nM7pQ", "name": "Aurora Holdings Corp.", "country_code": "US", "relationship_type": "IS_DIRECTLY_CONSOLIDATED_BY", "relationship_status": "ACTIVE", "ownership_pct": null, "period_start": "2020-01-15", "period_end": null, "direction": "parent", "source": "gleif_level2" }, { "related_owner_id": "own_Vm8pN3qR", "name": "Aurora Digital GmbH", "country_code": "DE", "relationship_type": "IS_DIRECTLY_CONSOLIDATED_BY", "relationship_status": "ACTIVE", "ownership_pct": null, "period_start": "2018-06-01", "period_end": null, "direction": "child", "source": "gleif_level2" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_gZ6qR2sT" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/owners/own_Rj3kL9mN/related?direction=parent" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const related = await signa.owners.related("own_Rj3kL9mN"); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------- | | 400 | `validation_error` | Invalid `direction` value | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Owner ID does not exist | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Owner](/api-reference/parties/retrieve-owner): owner profile and stats * [Owner Trademarks](/api-reference/parties/owner-trademarks): marks owned by this owner * [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark): individual trademark detail # Owner Trademarks Source: https://docs.signa.so/api-reference/parties/owner-trademarks GET /v1/owners/{id}/trademarks List a trademark owner's portfolio with the standard trademark filters ## Overview Returns a paginated list of trademarks held by the specified owner. Use this for portfolio views, competitor intelligence, or auditing an owner's IP holdings across jurisdictions. When neither `sort` nor `q` is supplied, results default to most-recent-first (`-filing_date`); a `q` text query instead ranks by relevance. Arrays are comma-separated, date ranges use flat `_gte`, `_gt`, `_lte`, and `_lt` operators, and booleans must be the literal strings `true` or `false`. ## Path Parameters Owner ID (e.g. `own_R3jK9mN2`). ## Query Parameters The full [Search Trademarks](/api-reference/trademarks/search-trademarks) filter set applies (status, mark classification, identifiers, geography, Nice/Vienna codes, date ranges, public-company filters, and boolean flags), plus: Search query text within this owner's trademarks. Sort field(s), comma-separated, `-` prefix for descending. Filter to marks prosecuted by a specific attorney (`att_...`). Filter to marks handled by a specific firm (`firm_...`). Comma-separated optional row projections. `full_goods_services` returns full `classifications[].goods_services_text` instead of the truncated summary text. Sparse top-level field projection, comma-separated. `id` and `object` are always retained. Unknown names return `400`. Items per page (1-100). Pagination cursor from a previous response. ## Response Array of trademark summary objects belonging to this owner. ```json theme={null} { "object": "list", "data": [ { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "AURORA", "status": { "primary": "active", "stage": "registered" }, "office_code": "US", "jurisdiction_code": "US", "filing_date": "2023-04-12", "registration_date": "2024-09-18", "classifications": [ { "nice_class": 9, "goods_services_text": "Downloadable software for digital image processing" }, { "nice_class": 35, "goods_services_text": "Online retail store services featuring digital media" }, { "nice_class": 42, "goods_services_text": "Software as a service (SaaS) featuring media management tools" } ], "owners": [ { "id": "own_7nQp3xYz", "name": "Aurora Digital Inc.", "country_code": "US", "entity_id": "ent_7nQp3xYz", "entity_id_type": "derived", "companies": [ { "source": "sec", "ticker": "AUR", "exchange": "NASDAQ", "lei": null, "entity_status": "active" } ] } ] }, { "id": "tm_Qr6nK9jL", "object": "trademark", "mark_text": "AURORA DIGITAL", "status": { "primary": "active", "stage": "registered" }, "office_code": "EM", "jurisdiction_code": "EU", "filing_date": "2023-07-20", "registration_date": "2024-11-05", "classifications": [ { "nice_class": 9, "goods_services_text": "Downloadable software for digital image processing" }, { "nice_class": 42, "goods_services_text": "Software as a service (SaaS) featuring media management tools" } ], "owners": [ { "id": "own_7nQp3xYz", "name": "Aurora Digital Inc.", "country_code": "US", "entity_id": "ent_7nQp3xYz", "entity_id_type": "derived" } ] } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6ImFiYyJ9" }, "request_id": "req_fY5pQ1rS" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/owners/own_R3jK9mN2/trademarks?status_stage=registered&offices=US,EM&nice_classes=9,42&limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const page = await signa.owners.trademarks("own_R3jK9mN2", { status_stage: ["registered"], offices: ["US", "EM"], nice_classes: [9, 42], limit: 20, }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------- | | 400 | `validation_error` | Invalid query parameters or date range | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Owner ID does not exist | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Owner](/api-reference/parties/retrieve-owner): owner profile and stats * [Owner Related](/api-reference/parties/owner-related): related entities (subsidiaries, parent companies) * [Search Trademarks](/api-reference/trademarks/search-trademarks): search across all owners # Retrieve Attorney Source: https://docs.signa.so/api-reference/parties/retrieve-attorney GET /v1/attorneys/{id} Retrieve an attorney profile by ID ## Overview Returns the full detail tier for a single attorney: canonical name, firm linkage, country, address, aliases, alternate identifiers (bar numbers and similar office-issued IDs), and computed practice statistics. There are no `?include=` parameters on this endpoint, the full detail is always returned. If the attorney record was merged into another (entity resolution), the endpoint returns `410 entity_merged` with the new ID in the error body. ## Path Parameters Attorney ID prefixed with `att_` (e.g. `att_3kPq9X`). ## Response Attorney ID prefixed with `att_`. Always `attorney`. Display name of the attorney. Normalized name used for search and deduplication. Firm ID prefixed with `firm_`, when the attorney is linked to a firm. Convenience denormalization of the firm's display name. ISO 3166-1 alpha-2 country code. Postal address as reported by the source office. Shape varies by office: most return a structured object (`lines[]`, `city`, `state`, `postal_code`, `country_code`); some offices provide only a free-text string. Total trademarks represented at mark grain. Count of active trademarks (registered + pending). Always present; 0 when stats not yet computed. Alternate name observed in source data. Alias source (e.g. office-reported, normalized). Office that produced this alias. Alternate office-issued identifiers (bar numbers, attorney docket IDs). Total trademarks represented. Counts marks: a Madrid IR family counts once, matching the default grouped `GET /v1/trademarks` listings. Currently registered marks, counted per office record. For Madrid-heavy portfolios this may exceed `trademark_count`, since `trademark_count` counts distinct marks while status counts are per office record. Marks in pending status. Expired marks. Cancelled marks. Abandoned marks. Share of concluded prosecutions (registered + expired + cancelled) that were ever granted. Share of marks abandoned. Distinct jurisdictions. Earliest filing date observed. Latest filing date observed. Average prosecution duration in days. Timestamp when the stats snapshot was computed. ISO 8601 creation timestamp. ISO 8601 last update timestamp. Unique request identifier for support and debugging. To enumerate an attorney's clients, use the dedicated [`GET /v1/attorneys/:id/clients`](/api-reference/parties/attorney-clients) endpoint. ```json Response theme={null} { "id": "att_3kPq9X", "object": "attorney", "name": "Jane Smith", "canonical_name": "JANE SMITH", "firm_id": "firm_2kLm9X", "firm_name": "Smith & Partners LLP", "country_code": "US", "trademark_count": 342, "active_count": 328, "address": { "lines": ["100 Patent Way"], "city": "Alexandria", "state": "VA", "postal_code": "22314", "country_code": "US" }, "aliases": [ { "name": "Jane M Smith", "type": "office_reported", "source_office": "US" } ], "identifiers": [ { "type": "uspto_attorney_docket", "value": "JS-12345", "source_office": "US" } ], "stats": { "trademark_count": 342, "registered_count": 287, "pending_count": 41, "expired_count": 8, "cancelled_count": 4, "abandoned_count": 2, "grant_rate": 0.84, "abandonment_rate": 0.006, "jurisdiction_count": 7, "earliest_filing": "2014-03-22", "latest_filing": "2025-09-30", "avg_prosecution_days": 312, "computed_at": "2026-04-08T14:23:11Z" }, "created_at": "2023-01-11T08:00:00Z", "updated_at": "2026-04-08T14:23:11Z", "request_id": "req_xyz789" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/attorneys/att_3kPq9X" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const attorney = await signa.attorneys.retrieve("att_3kPq9X"); console.log(attorney.name, attorney.firm_id); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid `id` format | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Attorney ID does not exist | | 410 | `entity_merged` | Attorney was merged into another. The new ID is returned in the error detail. | | 429 | `rate_limited` | Rate limit exceeded | ## Related Endpoints * [Search Attorneys](/api-reference/parties/search-attorneys): filtered listing of attorneys * [Attorney Trademarks](/api-reference/parties/attorney-trademarks): portfolio of marks an attorney represents * [Attorney Clients](/api-reference/parties/attorney-clients): owners represented by an attorney * [Retrieve Firm](/api-reference/parties/retrieve-firm): detail for the linked firm # Retrieve Entity Source: https://docs.signa.so/api-reference/parties/retrieve-entity GET /v1/entities/{id} Retrieve a resolved entity with its member owners and link evidence ## Overview Retrieve a single company profile by ID. A resolved entity embeds its member owners (one per office) with the per-link evidence that justified each link. Public-company facts (`publicly_traded`, `ticker`, `tickers`, `lei`, `has_lei`) are **aggregated over the member companies** and returned on the entity: the union of every member's SEC/GLEIF matches, so the profile carries a company's ticker even when only one office record does. An `ent_*` id can point to either a **resolved** entity (linked across offices) or a **derived** placeholder for an owner that isn't linked to any others (`ent_`). Both resolve here. Passing a derived id for an owner that has since been linked returns the real entity, so the response `id` may differ from the one you requested. A cached derived id never 404s. ## Path Parameters Entity ID (e.g. `ent_R3jK9mN2`), including the derived `ent_` form. ## Response Entity ID (`ent_*`). May differ from the requested id when a derived id resolved to its real entity. Always `entity`. The entity's display name, chosen from its member records. ISO 3166-1 alpha-2 country code. Entity type (e.g. `corporation`). `resolved` when this is a company profile linked across offices, `derived` when it's a placeholder for an owner not yet linked to any others. Whether the entity is publicly listed: any active SEC company with a ticker across members, or the entity is itself listed or a subsidiary of a listed company. A derived entity exposes the owner's own public-company facts when it's matched to one; otherwise `false`. `false` means no confirmed listing, not confirmed-private. The entity's ticker, the first of `tickers`; `null` when none. Deduped, uppercased tickers: member-company SEC tickers unioned with the entity's own listing ticker, whether direct or inherited from a listed ancestor (so this includes an inherited subsidiary ticker). Direct-vs-inherited provenance lives in the `listing` block below. For a derived entity, the owner's own tickers when matched to a public company; otherwise empty. The entity's GLEIF LEI, the first aggregated member LEI. Whether any GLEIF LEI is present across member companies (for a derived entity, the owner's own, when matched). Distinct trademarks across all members (co-owned marks counted once). Count of active trademarks (registered + pending), computed live across all member owners; always \<= trademark\_count. Number of member owners. GLEIF parent entity id, when present (resolved entities only). Public-listing decoration. **Present only when the entity is listed or a subsidiary of a listed company**; the whole block is omitted otherwise (an absent block means "no listing / unknown", never confirmed-private). `listed` when the entity is itself publicly listed, or `subsidiary_of_listed` when it inherits a ticker from a listed ancestor. The entity's own ticker (`listed`) or the inherited ancestor ticker (`subsidiary_of_listed`). Exchange code of the listing, when known. LEI of the listed company: the entity's own when `listed`, else the listed ancestor's. Derived from status: `direct` for `listed`, `inherited` for `subsidiary_of_listed`. The nearest listed ancestor, when it resolves to a live entity in our system. `null` for a directly-listed entity, or when the ancestor exists only in the GLEIF graph. The listed ancestor entity id (`ent_*`). The listed ancestor's name. The listed ancestor's own ticker. Owner ID (`own_*`). Always `owner`. Owner display name (as filed). Normalized name. ISO country code. Entity type. Office this owner record belongs to. How this member was linked into the entity (`null` on a derived entity's self-member, since it was never linked). How the link was established. One of `international_registration` (shared Madrid IR number), `shared_identifier` (shared office identifier), `public_company` (linked via shared public-company records), `portfolio_overlap` (portfolio-overlap heuristic), `manual_review` (confirmed by review), or `other`. Coarse confidence band: `high`, `medium`, or `low`. `null` when the link is not scored. Whether the link was confirmed by review (vs an automated match). Matched canonical Madrid IR numbers (when `method` is `international_registration`). Matched office-identifier values (when `method` is `shared_identifier`). Last change to the entity or its membership (drives the ETag). Unique request identifier for support. The `link` field is a small, stable projection: a link method, a confidence band, and matched identifiers, when applicable. The details of how the match was made aren't returned through the API. ```json Response theme={null} { "id": "ent_R3jK9mN2", "object": "entity", "name": "Apple Inc.", "country_code": "US", "entity_type": "corporation", "entity_id_type": "resolved", "publicly_traded": true, "ticker": "AAPL", "tickers": ["AAPL"], "lei": "HWUPKR0MPOU8FGXBT394", "has_lei": true, "trademark_count": 2314, "active_count": 2017, "member_count": 3, "parent_entity_id": null, "members": [ { "id": "own_R3jK9mN2", "object": "owner", "name": "Apple Inc.", "canonical_name": "APPLE INC", "country_code": "US", "entity_type": "corporation", "office_code": "US", "link": { "method": "public_company", "match_strength": "high", "reviewed": false } }, { "id": "own_Qz8mN3rT", "object": "owner", "name": "APPLE INC.", "canonical_name": "APPLE INC", "country_code": null, "entity_type": "corporation", "office_code": "EM", "link": { "method": "international_registration", "match_strength": "high", "reviewed": false, "matched_irs": ["1104235"] } } ], "updated_at": "2026-06-10T00:00:00Z", "request_id": "req_xyz789" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/entities/ent_R3jK9mN2" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const entity = await signa.entities.retrieve("ent_R3jK9mN2"); console.log(entity.name, entity.members.length); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------------------------------------------ | | 400 | `id_type_mismatch` | An ID of another type was supplied | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | No such entity, or a derived id for a suppressed or merged-away owner | | 410 | `entity_merged` | The entity was fused into another; follow `merged_into` to the successor | ## Related Endpoints * [Search Entities](/api-reference/parties/search-entities): search and filter entities * [Entity Trademarks](/api-reference/parties/entity-trademarks): marks across all member owners * [Entity Family](/api-reference/parties/entity-family): GLEIF corporate parent and subsidiaries # Retrieve Firm Source: https://docs.signa.so/api-reference/parties/retrieve-firm GET /v1/firms/{id} Retrieve a law firm profile by ID ## Overview Returns the detail tier for a single law firm: name, canonical name, country, aliases, attorney count, trademark count, active count, grant rate, and latest filing date. Optional `?include=attorneys` embeds the firm's attorneys. ## Recent changes `?include=stats` has been removed (beta breaking change). Firm counts remain always-on top-level fields, including the new `active_count` field. If the firm record was merged into another (entity resolution), the endpoint returns `410 entity_merged` with the new ID in the error body. ## Path Parameters Firm ID prefixed with `firm_` (e.g. `firm_2kLm9X`). ## Query Parameters Comma-separated relations to embed. Valid values: * `attorneys`: first page of attorneys at the firm with `attorneys_has_more` ## Response Firm ID prefixed with `firm_`. Always `firm`. Display name of the firm. Normalized name used for search and deduplication. ISO 3166-1 alpha-2 country code. Alternate name. Alias source. Office that produced this alias. Total attorneys linked to this firm. Total trademarks where this firm appears as representative. Counts marks: a Madrid IR family counts once, matching the default grouped `GET /v1/trademarks` listings. Count of active trademarks (registered + pending). Always present; 0 when stats not yet computed. Share of concluded prosecutions (registered + expired + cancelled) that were ever granted. Latest filing date observed for this firm. ISO 8601 creation timestamp. ISO 8601 last update timestamp. Returned when `?include=attorneys`. Paired with `attorneys_has_more`. Unique request identifier for support and debugging. ```json Response theme={null} { "id": "firm_2kLm9X", "object": "firm", "name": "Smith & Partners LLP", "canonical_name": "SMITH AND PARTNERS LLP", "country_code": "US", "aliases": [ { "name": "Smith Partners", "type": "office_reported", "source_office": "US" } ], "attorney_count": 23, "trademark_count": 4521, "active_count": 4302, "grant_rate": 0.86, "latest_filing": "2026-04-02", "created_at": "2023-01-11T08:00:00Z", "updated_at": "2026-04-08T14:23:11Z", "request_id": "req_xyz789" } ``` ## Code Examples ```bash cURL theme={null} curl -G "https://api.signa.so/v1/firms/firm_2kLm9X" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "include=attorneys" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const firm = await signa.firms.retrieve("firm_2kLm9X", { include: ["attorneys"], }); console.log(firm.name, firm.attorney_count, firm.trademark_count); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid `id` format or unknown `include` value | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Firm ID does not exist | | 410 | `entity_merged` | Firm was merged into another. The new ID is returned in the error detail. | | 429 | `rate_limited` | Rate limit exceeded | ## Related Endpoints * [Search Firms](/api-reference/parties/search-firms): filtered listing of firms * [Firm Attorneys](/api-reference/parties/firm-attorneys): attorneys at a firm * [Firm Trademarks](/api-reference/parties/firm-trademarks): portfolio of marks the firm represents # Retrieve Owner Source: https://docs.signa.so/api-reference/parties/retrieve-owner GET /v1/owners/{id} Retrieve an owner profile by ID ## Overview Returns the full detail tier for a single owner: canonical name, address, aliases, alternate identifiers, entity linkage, public-company matches (when available), and computed portfolio statistics. If the owner record was merged into another (entity resolution), the endpoint returns `410 entity_merged` with the new ID in the error body. ## Path Parameters Owner ID (e.g. `own_R3jK9mN2`). ## Response Owner ID (`own_*`) Always `"owner"` Display name of the owner Normalized name used for search and deduplication Owner name in original script (may be null) ISO country code Entity type (e.g. `corporation`, `individual`) Postal address as reported by the source office. Shape varies by office: most return a structured object (`lines[]`, `city`, `state`, `postal_code`, `country_code`); some offices provide only a free-text string. The id of the company profile this owner belongs to (`ent_*`). Always present, so you can group owners by `entity_id` unconditionally. See [Retrieve Entity](/api-reference/parties/retrieve-entity). `resolved` when this owner is linked to a company profile shared with owner records from other offices, `derived` when it isn't linked to any yet (the id is a stable placeholder based on this owner alone). Whether the (possibly entity-inherited) company set includes an active SEC company with a ticker. A `false` value means **no confirmed match**, not confirmed-private. First SEC ticker across the (possibly inherited) company set. First GLEIF LEI across the (possibly inherited) company set. Whether any GLEIF LEI is present across the (possibly inherited) company set. `entity` when the company facts are inherited across the linked company profile's members, or `direct` when the owner isn't linked and is reporting only its own matches. Alias name Alias type Office that reported this alias External identifiers (SEC CIK, LEI, etc.) Omitted when the owner has no matched company records. When the owner is linked to a company profile, this is the **union of all linked members' company matches**, including the owner's own. Data source (e.g. `sec`, `gleif`) Source-specific identifier Company legal name from the source record Stock ticker symbol for SEC matches Stock exchange for SEC matches Legal Entity Identifier for GLEIF matches `active`, `inactive`, or `delisted` Match confidence, when available When the match was verified, when available Verifier identifier, when available Total trademarks owned at mark grain. Count of active trademarks (registered + pending). Always present; 0 when stats not yet computed. Total trademarks owned. Counts marks: a Madrid IR family counts once, matching the default grouped `GET /v1/trademarks` listings. Currently registered trademarks, counted per office record. For Madrid-heavy portfolios this may exceed `trademark_count`, since `trademark_count` counts distinct marks while status counts are per office record. Trademarks with pending status Expired trademarks Cancelled trademarks Abandoned trademarks Share of concluded prosecutions (registered + expired + cancelled) that were ever granted. Ratio of abandoned to total trademarks Number of distinct jurisdictions Earliest filing date (ISO 8601) Most recent filing date (ISO 8601) When stats were last computed (ISO 8601) ISO 8601 creation timestamp. ISO 8601 last update timestamp. Unique request identifier for support and debugging. **Public-company facts are served through the owner's company profile.** When an owner is linked to a company profile shared with other office records, `publicly_traded`, `ticker`, `lei`, `has_lei`, and `companies[]` reflect the **profile's aggregated** facts, the union of every linked member's SEC/GLEIF matches. So a non-US office record that itself carries no ticker still surfaces the company's ticker and LEI when a linked sibling record is matched. `companies_source` is `entity` in that case and `direct` for an owner with no links. ```json Response theme={null} { "id": "own_R3jK9mN2", "object": "owner", "name": "Apple Inc.", "canonical_name": "APPLE INC", "name_original_script": null, "country_code": "US", "entity_type": "corporation", "address": { "lines": ["One Apple Park Way"], "city": "Cupertino", "state": "CA", "postal_code": "95014", "country_code": "US" }, "entity_id": "ent_R3jK9mN2", "entity_id_type": "resolved", "publicly_traded": true, "ticker": "AAPL", "lei": "HWUPKR0MPOU8FGXBT394", "has_lei": true, "companies_source": "entity", "aliases": [ { "name": "Apple Computer Inc.", "type": "former_name", "source_office": "US" }, { "name": "Apple Computer, Inc.", "type": "former_name", "source_office": "US" } ], "identifiers": [ { "type": "sec_cik", "value": "320193" } ], "companies": [ { "source": "sec", "source_id": "0000320193", "legal_name": "Apple Inc.", "ticker": "AAPL", "exchange": "NASDAQ", "lei": null, "entity_status": "active" }, { "source": "gleif", "source_id": "HWUPKR0MPOU8FGXBT394", "legal_name": "APPLE INC.", "ticker": null, "exchange": null, "lei": "HWUPKR0MPOU8FGXBT394", "entity_status": "active" } ], "trademark_count": 1847, "active_count": 1290, "stats": { "trademark_count": 1847, "registered_count": 1203, "pending_count": 87, "expired_count": 312, "cancelled_count": 45, "abandoned_count": 200, "grant_rate": 0.651, "abandonment_rate": 0.108, "jurisdiction_count": 5, "earliest_filing": "1985-04-01", "latest_filing": "2026-03-15", "computed_at": "2026-03-24T00:00:00Z" }, "created_at": "2023-01-11T08:00:00Z", "updated_at": "2026-03-24T00:00:00Z", "request_id": "req_xyz789" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/owners/own_R3jK9mN2" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const owner = await signa.owners.retrieve("own_R3jK9mN2"); console.log(owner.name, owner.stats?.trademark_count); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid `id` format | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Owner ID does not exist | | 410 | `entity_merged` | Owner was merged into another. The new ID is returned in the error detail. | | 429 | `rate_limited` | Rate limit exceeded | ## Related Endpoints * [Search Owners](/api-reference/parties/search-owners): filtered listing of owners * [Owner Trademarks](/api-reference/parties/owner-trademarks): portfolio of marks owned * [Owner Related](/api-reference/parties/owner-related): corporate parent and subsidiary relationships * [Retrieve Entity](/api-reference/parties/retrieve-entity): the cross-office company profile this owner belongs to # Search Attorneys Source: https://docs.signa.so/api-reference/parties/search-attorneys GET /v1/attorneys Search attorneys representing trademark owners across all offices ## Overview Returns a paginated list of attorneys derived from the trademark records Signa has ingested. Use this for attorney lookup, prosecution counsel discovery, or building a competitive view of who is filing for whom. Search supports trigram-based fuzzy name matching. ## Query Parameters Fuzzy attorney name search. Restrict to attorneys at a specific firm (`firm_...`). ISO 3166-1 alpha-2 country code filter. Sort field. One of `-trademark_count`, `trademark_count`, `-grant_rate`, `grant_rate`, `-avg_prosecution_days`, `avg_prosecution_days`, `-latest_filing`, `latest_filing`, `-name`, `name`. Page size (1-50). Pagination cursor from a previous response. ## Response Array of attorney summary records. Attorney ID (`att_...`). Display name of the attorney. Normalized name used for entity resolution. Affiliated firm name when known. Two-letter country code. Total trademarks attributed to this attorney. Counts marks: a Madrid IR family counts once, matching the default grouped `GET /v1/trademarks` listings. Count of active trademarks (registered + pending). Always present; 0 when stats not yet computed. ```json theme={null} { "object": "list", "data": [ { "id": "att_3kPq9X", "object": "attorney", "name": "Jane R. Smith", "canonical_name": "JANE R SMITH", "firm_name": "Smith & Associates LLP", "country_code": "US", "trademark_count": 1284, "active_count": 1097 } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6Imp..." }, "request_id": "req_xyz" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/attorneys?q=smith&country_code=US&sort=-trademark_count&limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const page = await signa.attorneys.list({ q: "smith", country_code: "US", sort: "-trademark_count", limit: 20, }); ``` ## Errors | Status | Type | Description | | ------ | --------------------- | --------------------------------------------------------------------- | | 400 | `validation_error` | Invalid query parameter | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 429 | `rate_limited` | Too many requests | | 503 | `service_unavailable` | Attorney search is temporarily unavailable; retry after `Retry-After` | ## Related Endpoints * [Retrieve Attorney](/api-reference/parties/retrieve-attorney): full attorney detail * [Attorney Trademarks](/api-reference/parties/attorney-trademarks): marks handled by an attorney * [Attorney Clients](/api-reference/parties/attorney-clients): owners represented by an attorney * [Search Firms](/api-reference/parties/search-firms): search firms instead # Search Entities Source: https://docs.signa.so/api-reference/parties/search-entities GET /v1/entities Search resolved entities, the cross-office identity over per-office owner records ## Overview Returns a paginated, summary-tier list of **resolved entities**. An entity is one real-world company linked across offices: `Apple Inc (USPTO)`, `Apple Inc (EUIPO)`, and `Apple Inc (CIPO)` are one entity, not three owners. Use this to find a company once instead of reconciling per-office owner fragments yourself. The `publicly_traded`, `ticker`, and `has_lei` filters match on the entity's **aggregated** public-company facts (the union of its members' SEC/GLEIF links), so they catch a company even when only one of its office records carries the ticker. **Entities or owners, which one do I use?** Entities are resolved real-world organizations (one company, linked across every office). Start here for company-level questions like "everything this company owns, everywhere." [Owners](/api-reference/parties/search-owners) are the raw office-level record parties, where the same company appears once per office and per name variant. Reach for owners when you need provenance or want to display the exact record as an office filed it. This list returns resolved (cross-office) entities only. An owner that isn't linked to any others doesn't appear here, but you can still look it up directly at [Retrieve Entity](/api-reference/parties/retrieve-entity) using its derived id, or find it via [Search Owners](/api-reference/parties/search-owners). If search is temporarily unavailable, the endpoint returns `503` with no fallback list. ## Query Parameters Name typeahead / relevance search. When present, results default to relevance order. Filter by ISO 3166-1 alpha-2 country code (e.g. `US`, `GB`). Filter by entity type (e.g. `corporation`, `individual`). `true` returns entities that are publicly listed: either a member owner is matched to an active SEC company with a ticker, or the entity is itself listed or a subsidiary of a listed company. `false` means no confirmed listing, not confirmed private. Strict `true`/`false` only. Exact stock ticker (uppercased server-side). Subsidiary-inclusive: matches the entity's direct tickers (member SEC companies and its own listing) **and** inherited tickers, so `ticker=NKE` returns Nike and its subsidiary entities. `true` returns entities with a GLEIF LEI. Strict `true`/`false` only. Exact GLEIF Legal Entity Identifier. Sort field with optional `-` prefix. One of `-trademark_count`, `trademark_count`, `-name`, `name`, `-member_count`, `member_count`. Default: `-trademark_count` (`-relevance` when `q` is set). When `true`, includes a total count in `pagination.total_count`. The count is exact only up to 10,000; beyond that `pagination.total_count_approximate` is `true`. Page size, between 1 and 100. Opaque cursor returned in the previous response's `pagination.cursor`. ## Response Always `list`. Entity ID prefixed with `ent_`. Always `entity`. The entity's display name, chosen from its member records. ISO 3166-1 alpha-2 country code. Entity type (e.g. `corporation`). Always `resolved` on this list (see the Note above). Whether the aggregated company set includes an active SEC company with a ticker. First SEC ticker across the entity's members. First GLEIF LEI across the entity's members. Distinct trademarks across all member owners (co-owned marks counted once). Count of active trademarks (registered + pending). Always present; 0 when stats not yet computed. Number of member owners. Whether more entities are available. Cursor for the next page (signed). Unique request identifier for support and debugging. ```json Response theme={null} { "object": "list", "data": [ { "id": "ent_R3jK9mN2", "object": "entity", "name": "Apple Inc.", "country_code": "US", "entity_type": "corporation", "entity_id_type": "resolved", "publicly_traded": true, "ticker": "AAPL", "lei": "HWUPKR0MPOU8FGXBT394", "trademark_count": 2314, "active_count": 1988, "member_count": 3 } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_xyz789" } ``` ## Code Examples ```bash cURL theme={null} curl -G "https://api.signa.so/v1/entities" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=apple" \ --data-urlencode "publicly_traded=true" \ --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({ q: "apple", publicly_traded: true, limit: 20, }); for await (const entity of entities) { console.log(entity.name, entity.ticker, entity.trademark_count); } ``` ## Errors | Status | Type | Description | | ------ | --------------------- | --------------------------------------------------------- | | 400 | `validation_error` | Invalid sort, unknown filter value, or non-strict boolean | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 429 | `rate_limited` | Rate limit exceeded | | 503 | `service_unavailable` | Entity search is temporarily unavailable | ## Related Endpoints * [Retrieve Entity](/api-reference/parties/retrieve-entity): full entity detail with members * [Entity Trademarks](/api-reference/parties/entity-trademarks): marks across all member owners * [Entity Family](/api-reference/parties/entity-family): GLEIF corporate parent and subsidiaries # Search Firms Source: https://docs.signa.so/api-reference/parties/search-firms GET /v1/firms Search law firms with trademark prosecution activity ## Overview Returns a paginated list of law firms derived from attorney affiliations on ingested trademarks. Filter by country, attorney count, filing volume, and sort by activity metrics. ## Recent changes The beta `?include=stats` option has been removed; always-on summary fields now carry the supported firm counts. ## Query Parameters Case-insensitive substring search on firm name. ISO 3166-1 alpha-2 country code filter. Minimum number of attorneys at the firm. Minimum number of trademark filings handled. Sort field. One of `-trademark_count`, `trademark_count`, `-attorney_count`, `attorney_count`, `-grant_rate`, `grant_rate`, `-name`, `name`. Page size (1-50). Pagination cursor from a previous response. ## Response Array of firm summary records. Firm ID (`firm_...`). Display name of the firm. Normalized name used for entity resolution. Two-letter country code. Number of distinct attorneys at the firm. Total trademarks handled by the firm's attorneys. Counts marks: a Madrid IR family counts once, matching the default grouped `GET /v1/trademarks` listings. Count of active trademarks (registered + pending). Always present; 0 when stats not yet computed. Share of concluded prosecutions (registered + expired + cancelled) that were ever granted (0-1). ISO date of the most recent filing handled. ISO 8601 timestamp when this firm record was created. ```json theme={null} { "object": "list", "data": [ { "id": "firm_2kLm9X", "object": "firm", "name": "Smith & Associates LLP", "canonical_name": "SMITH & ASSOCIATES LLP", "country_code": "US", "attorney_count": 42, "trademark_count": 15823, "active_count": 13640, "grant_rate": 0.78, "latest_filing": "2026-04-08", "created_at": "2024-09-12T11:42:01Z" } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6..." }, "request_id": "req_xyz" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/firms?country_code=US&min_filings=500&sort=-trademark_count&limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const page = await signa.firms.list({ country_code: "US", min_filings: 500, sort: "-trademark_count", limit: 20, }); ``` ## Errors | Status | Type | Description | | ------ | --------------------- | ----------------------------------------------------------------- | | 400 | `validation_error` | Invalid query parameter | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 429 | `rate_limited` | Too many requests | | 503 | `service_unavailable` | Firm search is temporarily unavailable; retry after `Retry-After` | ## Related Endpoints * [Retrieve Firm](/api-reference/parties/retrieve-firm): full firm detail with optional includes * [Firm Attorneys](/api-reference/parties/firm-attorneys): attorneys at a firm * [Firm Trademarks](/api-reference/parties/firm-trademarks): marks handled by a firm * [Search Attorneys](/api-reference/parties/search-attorneys): search attorneys instead # Search Owners Source: https://docs.signa.so/api-reference/parties/search-owners GET /v1/owners Search trademark owners with filtering, public-company joins, and cursor pagination ## Overview Returns a paginated, summary-tier list of trademark owners. Supports fuzzy name search, country and entity-type filters, plus virtual filters that join public-company enrichment to find owners with SEC ticker or GLEIF LEI matches. Suppressed owners (placeholder records, frozen entities) are always excluded. **Owners or entities, which one do I use?** Owners are the raw office-level record parties: the same company appears once per office and per name variant, exactly as each office filed it. Use owners for provenance or exact-record display. For company-level questions ("everything this company owns, everywhere"), start with [Entities](/api-reference/parties/search-entities), the resolved real-world organizations that link these owner records together. ## Query Parameters Fuzzy name search via trigram similarity. Tolerant of typos and word reordering. Filter by ISO 3166-1 alpha-2 country code (e.g. `US`, `GB`). Filter by entity type (e.g. `corporation`, `individual`, `partnership`). Virtual filter: matches owners linked to a public company with the given stock ticker (e.g. `AAPL`). Virtual filter: matches owners linked to a GLEIF Legal Entity Identifier. Virtual filter: `true` returns owners matched to an active SEC company with a ticker. `false` means no confirmed active ticker match, not confirmed private. Strict `true`/`false` only. Virtual filter: `true` returns owners matched to a GLEIF legal entity. `false` means no confirmed LEI match. Strict `true`/`false` only. Sort field with optional `-` prefix for descending. One of `-trademark_count`, `trademark_count`, `-grant_rate`, `grant_rate`, `-latest_filing`, `latest_filing`, `-name`, `name`. Default: `-trademark_count`. When `true`, includes an accurate total count in `pagination.total_count`. Page size, between 1 and 100. Opaque cursor returned in the previous response's `pagination.cursor`. ## Response Always `list`. Owner ID prefixed with `own_`. Always `owner`. Display name of the owner. Normalized name used for entity resolution and deduplication. ISO 3166-1 alpha-2 country code. Entity type (e.g. `corporation`, `individual`). Total trademarks owned. Counts marks: a Madrid IR family counts once, matching the default grouped `GET /v1/trademarks` listings. Count of active trademarks (registered + pending). Always present; 0 when stats not yet computed. The id of the company profile this owner belongs to (`ent_*`), or a derived placeholder id when it isn't linked to one yet. See [Retrieve Entity](/api-reference/parties/retrieve-entity). `resolved` or `derived`. On the list, this can briefly show `derived` for an owner that's already linked elsewhere, until the list catches up; [Retrieve Owner](/api-reference/parties/retrieve-owner) detail always reflects the current link. Whether more owners are available. Cursor for the next page. Unique request identifier for support and debugging. ```json Response theme={null} { "object": "list", "data": [ { "id": "own_R3jK9mN2", "object": "owner", "name": "Apple Inc.", "canonical_name": "APPLE INC", "country_code": "US", "entity_type": "corporation", "trademark_count": 1847, "active_count": 1604, "entity_id": "ent_R3jK9mN2", "entity_id_type": "resolved" }, { "id": "own_Yk8mN2pQ", "object": "owner", "name": "Alphabet Inc.", "canonical_name": "ALPHABET INC", "country_code": "US", "entity_type": "corporation", "trademark_count": 1521, "active_count": 1310, "entity_id": "ent_Yk8mN2pQ", "entity_id_type": "derived" } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6Im93bl9SM2pLOW1OMiIsInNvcnQiOjE4NDd9" }, "request_id": "req_xyz789" } ``` ## Code Examples ```bash cURL theme={null} curl -G "https://api.signa.so/v1/owners" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "country_code=US" \ --data-urlencode "publicly_traded=true" \ --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 owners = await signa.owners.list({ country_code: "US", publicly_traded: true, sort: "-trademark_count", limit: 20, }); for await (const owner of owners) { console.log(owner.name, owner.trademark_count); } ``` ## Errors | Status | Type | Description | | ------ | --------------------- | -------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid sort, unknown filter value, or non-strict boolean for `publicly_traded` or `has_lei` | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 429 | `rate_limited` | Rate limit exceeded | | 503 | `service_unavailable` | Owner search is temporarily unavailable; retry after `Retry-After` | ## Related Endpoints * [Retrieve Owner](/api-reference/parties/retrieve-owner): full owner detail * [Owner Trademarks](/api-reference/parties/owner-trademarks): portfolio of marks owned * [Owner Related](/api-reference/parties/owner-related): corporate parent and subsidiary relationships # Add Trademarks Source: https://docs.signa.so/api-reference/portfolios/add-trademarks POST /v1/portfolios/{id}/trademarks Add trademarks with optional external docketing references See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Request Requires an `Idempotency-Key` header and exactly one of these request shapes, with **1–100 entries per request**: ```json theme={null} { "trademark_ids": ["tm_01900000-0000-7000-8000-000000000001"] } ``` ```json theme={null} { "items": [ { "trademark_id": "tm_01900000-0000-7000-8000-000000000001", "external_ref": "ACME-2024-001" } ] } ``` Use real `tm_` IDs returned by Signa. Plain `trademark_ids` silently deduplicates repeated IDs and creates new memberships without a reference. The `items` shape rejects repeated trademark IDs or references with 400 `validation_error`. ## Response Unknown trademark IDs are counted under `not_found`; existing memberships are counted under `already_in_portfolio`. The result also includes `added`. ## External References `external_ref` is your reference for this membership: optional, trimmed, 1–255 characters, case-sensitive, and unique within the portfolio. You can attach a reference to an existing membership that has none. Once set, it is immutable: remove the membership and re-add it to bind a different reference. Reusing a reference for another mark, or changing an existing reference, returns 409 `external_ref_conflict` and rolls back the entire batch. ## Errors Exceeding the organization's distinct managed-marks cap returns 409 `resource_quota_exceeded` with `quota_scope: "managed_marks"`; the entire batch is rolled back. Adding an already-managed mark to another folder does not consume another managed-mark slot. Concurrent updates can return retryable 503 `service_unavailable`; retry the same request with the same idempotency key. # Create Portfolio Source: https://docs.signa.so/api-reference/portfolios/create POST /v1/portfolios Create a folder of trademark records See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Overview A portfolio groups Signa trademark records into a customer-owned folder. It does not create matters, tasks, or completion state. New portfolios start empty; use [Add Trademarks](/api-reference/portfolios/add-trademarks) to populate them. ## Request Send a unique `name` (1–255 characters), optional `description` (up to 2,000 characters), and optional string-valued `metadata` (up to 50 keys). Requires an `Idempotency-Key` header. ## Response The created portfolio at the top level, with a `ptf_` ID and HTTP 201. ## Errors The portfolio-count limit is separate from the managed-marks allowance: free 3, beta 20, starter 10, pro 50, enterprise unlimited. Exceeding it returns 409 `resource_quota_exceeded` with `quota_scope: "portfolios"`. A duplicate name returns 409 `conflict`. # List Portfolio Deadlines Source: https://docs.signa.so/api-reference/portfolios/deadlines GET /v1/portfolios/{id}/deadlines List deadlines computed from a portfolio’s trademark records See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Overview Computes deadlines on demand and returns a collection with `data`, `has_more`, and `pagination.cursor`. Each row identifies its `trademark_id` and statutory `rule_id`, with the deadline type, due date, and jurisdiction, plus the `effective_from`, `last_verified`, `sources[]`, `trigger_date` and `trigger_field` provenance fields. `sources[]` carries the statutory citations behind the rule, matching [Compute Deadlines](/api-reference/reference/compute-deadlines). This endpoint is an alias of [List Deadlines](/api-reference/reference/list-deadlines) with `portfolio_id` fixed to `{id}`: same rows, same filters, same cursor. The one deliberate difference is scope: this path requires `portfolios:manage` alone, while `GET /v1/deadlines?portfolio_id=...` requires `trademarks:read` as its base scope and `portfolios:manage` on top. A key holding only `portfolios:manage` therefore reaches portfolio deadlines here and nowhere else. This endpoint's scope is unchanged from the day it shipped, and adding a required scope to a published operation would break every such key. Marks belonging to the synthetic canary office are excluded: they carry no jurisdictional deadline rules, so no row could be acted on. This matches every other deadline surface — `GET /v1/deadlines`, `POST /v1/deadlines/compute`, the calendar download and the trademark detail route all treat those marks as non-retrievable. For a calendar file, use [Download Portfolio Calendar](/api-reference/portfolios/deadlines-ics). ## Query Parameters Use `due_before` to bound the due date (default: 720 days after the computation date), a comma-separated `type` to filter deadline types, `urgency` (`critical`, `upcoming`, `routine`, `in_grace`, `missed`), and `limit` (default 20, maximum 100) plus the returned opaque `cursor` to page through results. `due_before` must be an ISO 8601 calendar date, `YYYY-MM-DD` (for example `2027-06-30`). Anything else — a bare year, a datetime, free text — is rejected with 400 `validation_error`. `type` is a closed set (`renewal`, `declaration_of_use`, `combined_renewal_and_use`, `declaration_of_incontestability`, `restoration`, `international_renewal`); an unrecognized value is also a 400 `validation_error` rather than a silent empty page. The list is forward-looking by default and omits deadlines whose grace period has already closed; `urgency=missed` selects exactly those rows instead. The signed `cursor` pins the computation date and binds the selector and filters, so keep those parameters unchanged while paging. Cursors issued before this endpoint moved to the shared deadline list return `400 cursor_expired`; restart at page one. ## Errors A `due_before` that is not `YYYY-MM-DD`, or a `type` or `urgency` outside its enum, returns 400 `validation_error`. A cursor that fails to decode or verify — malformed, unsigned, tampered, or minted for another endpoint — returns 400 `cursor_expired`. A cursor that verifies but whose bound portfolio or filters have changed returns 400 `cursor_invalid`. More than **5,000 marks in this portfolio** returns 422 `portfolio_too_large`. Split it into smaller portfolios and request each separately. A narrower date or type filter does not avoid this cap, and the ICS download has the same cap. # Download Portfolio Calendar Source: https://docs.signa.so/api-reference/portfolios/deadlines-ics GET /v1/portfolios/{id}/deadlines.ics Download computed deadlines as an iCalendar file See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Overview Returns a one-time RFC 5545 calendar file with `Content-Type: text/calendar` and `Content-Disposition: attachment`. Download it using your bearer API key, then import the file into your calendar application. **This is not a subscribable calendar URL**: the endpoint requires an Authorization header. Importing the file does not keep it synchronized with later changes. The file carries the same deadlines as the [JSON list](/api-reference/portfolios/deadlines), so marks belonging to the synthetic canary office are excluded here too: they carry no jurisdictional deadline rules, and no event could be acted on. ## Query Parameters Optional `reminder_days` is a comma-separated list of offsets, each from 1 to 3,650 days. The default is `90,30,7`. These become alarms inside the downloaded file; Signa does not send calendar reminders from this endpoint. ## Code Example ```bash theme={null} curl "https://api.signa.so/v1/portfolios/$PORTFOLIO_ID/deadlines.ics" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --output portfolio-deadlines.ics ``` ## Errors More than **5,000 marks in this portfolio** returns 422 `portfolio_too_large`, just like the [JSON deadlines list](/api-reference/portfolios/deadlines). Split the portfolio into smaller folders; switching formats does not bypass the limit. # Delete Portfolio Source: https://docs.signa.so/api-reference/portfolios/delete DELETE /v1/portfolios/{id} Delete a portfolio and its membership associations See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Overview Requires an `Idempotency-Key` header. Deleting a portfolio removes its membership associations and references; the underlying trademark records remain available. A mark stops counting toward your managed-marks allowance only when none of your organization's portfolios contains it. ## Response A successful deletion returns HTTP 200 with `id`, `object: "portfolio"`, and `deleted: true`. ## Errors An unknown portfolio or one belonging to another organization returns 404. Active webhook references block deletion with 409 `portfolio_in_use`, which lists the referencing endpoints in `endpoint_ids`. # List Portfolios Source: https://docs.signa.so/api-reference/portfolios/list GET /v1/portfolios List the portfolios belonging to your organization See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Overview Returns a collection with top-level `data`, `has_more`, and `pagination.cursor`. Portfolios from other organizations are excluded. Each portfolio includes `trademark_count`; use [Retrieve Portfolio](/api-reference/portfolios/retrieve) for its membership list. ## Pagination Use `limit` (default 20, maximum 100) and pass the returned opaque `cursor` to request the next page. ## Sorting Sorting defaults to `-created_at`. Supported fields are `created_at`, `updated_at`, and `name`, each optionally prefixed with `-` for descending order. # Remove Trademarks Source: https://docs.signa.so/api-reference/portfolios/remove-trademarks POST /v1/portfolios/{id}/trademarks/remove Remove memberships by trademark ID or external reference See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Request Requires an `Idempotency-Key` header and exactly one of `trademark_ids` or `external_refs`, with **1–100 entries per request**. For example: ```json theme={null} { "external_refs": ["ACME-2024-001"] } ``` Both shapes silently deduplicate repeated values. Unknown IDs or references match nothing. References use case-sensitive comparison after trimming and are scoped to the requested portfolio. ## Response The response includes `removed` at the top level. ## Effects Removal deletes the membership and its reference. It does not delete the trademark record. A managed-mark slot is released only when no other portfolio in your organization contains that trademark. Remove, then add again to rebind an immutable `external_ref`. ## Errors An unknown or foreign portfolio returns 404. # Retrieve Portfolio Source: https://docs.signa.so/api-reference/portfolios/retrieve GET /v1/portfolios/{id} Retrieve a portfolio and its embedded trademark membership list See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Overview Returns the portfolio at the top level with an embedded `trademarks` collection. The embedded list includes membership fields `added_at`, `added_by`, and `external_ref` alongside each trademark summary. Use `limit` (default 20, maximum 100\) and `cursor` to page through that collection. `limit=0` skips the list when no reference filter is supplied. ## External References Use `?external_ref=ACME-2024-001` to look up your own membership reference. The comparison is case-sensitive after trimming and returns at most one membership; an unknown reference returns an empty embedded list, not a missing portfolio. The filter remains effective with `limit=0`. References are scoped to this portfolio, not to the trademark globally. ## Errors An unknown portfolio or one belonging to another organization returns 404. # Update Portfolio Source: https://docs.signa.so/api-reference/portfolios/update PATCH /v1/portfolios/{id} Update a portfolio name, description, or metadata See [portfolio limits and references](/guides/portfolios) for current caps and `external_ref` semantics. ## Overview Updates a portfolio's own attributes. Membership references are changed through the [add/remove operations](/api-reference/portfolios/add-trademarks), not through portfolio metadata. ## Request Send the fields to change and an `Idempotency-Key` header. `name` must remain non-empty and unique within your organization. Set `description` to `null` to clear it. `metadata` merges with existing keys; a `null` value removes that key. ## Response The updated portfolio at the top level. ## Errors An unknown or foreign portfolio returns 404; a duplicate name returns 409 `conflict`. # Retrieve Proceeding Source: https://docs.signa.so/api-reference/proceedings/retrieve-proceeding GET /v1/proceedings/{id} Retrieve a single trademark proceeding with parties, classes, and decision metadata ## When To Use This Use this endpoint when you already have a proceeding ID and need the full dispute record for docket review, risk analysis, or client reporting. For example, an IP litigation team can fetch a decided TTAB opposition to see the parties, contested classes, office decision summary, normalized outcome, and how long the case ran. For an end-to-end workflow, see [Opposition & Dispute Intelligence](/guides/opposition-dispute-intelligence). ## Path Parameters Proceeding ID (`prc_...`). ## Response Proceeding ID (`prc_...`). Always `proceeding`. Parent trademark ID (`tm_...`), when available. Type (`opposition`, `cancellation`, `appeal`, etc.). Office-issued proceeding identifier, when available. Current status, when available. Normalized outcome from the challenger perspective, or `null` when not available. | Value | Meaning | | ------------------ | ------------------------------------------------------------------ | | `challenger_won` | The challenger prevailed, derived from `decided_granted`. | | `challenger_lost` | The challenger did not prevail, derived from `decided_rejected`. | | `mixed` | The decision was partial or text indicates an in-part disposition. | | `settled` | The proceeding status is `settled`. | | `withdrawn` | The proceeding status is `withdrawn`. | | `default_judgment` | The challenger won through a default or procedural failure signal. | | `pending` | The proceeding is still `pending` or `suspended`. | | `other` | The status is `other` or does not map to a more specific outcome. | Number of days between `filed_date` and `decision_date`. `null` when either date is missing. Uppercase WIPO ST.3 office code for the parent trademark (e.g. `US`, `EM`), when available. ISO date the proceeding was initiated, when available. ISO date of decision when concluded, when available. Outcome string when decided. Array of parties. Each has `owner_id`, `name`, `role` (`opponent`, `petitioner`, `respondent`, `intervener`, `other`), and `country_code`. Nice classes contested in the proceeding, when known. Free-text summary, when provided. Summary of the parent trademark (`id`, `object`, `mark_text`, `serial_number`, `office_code`), or `null` when unavailable. ISO timestamp for when Signa created the proceeding record. ISO timestamp for the latest Signa update to the proceeding record. ## Example Request ```bash cURL theme={null} curl "https://api.signa.so/v1/proceedings/prc_019f34d6-aaaa-7777-8888-aaaaaaaaaaaa" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` ## Example Response ```json theme={null} { "id": "prc_019f34d6-aaaa-7777-8888-aaaaaaaaaaaa", "object": "proceeding", "trademark_id": "tm_019f34d6-2000-7777-8888-000000000001", "proceeding_type": "opposition", "proceeding_number": "91265432", "status": "decided_granted", "outcome": "challenger_won", "duration_days": 231, "office_code": "US", "filed_date": "2024-01-18", "decision_date": "2024-09-05", "decision_outcome": "Opposition Sustained", "contested_classes": [9, 42], "description": "Opposition proceeding", "created_at": "2024-01-18T10:00:00.000Z", "updated_at": "2024-09-05T10:00:00.000Z", "parties": [ { "owner_id": "own_019f34d6-1111-7777-8888-111111111111", "name": "Apex Outdoor Group LLC", "role": "opponent", "country_code": "US" }, { "owner_id": "own_019f34d6-2222-7777-8888-222222222222", "name": "Meridian Labs Inc.", "role": "respondent", "country_code": "US" } ], "trademark": { "id": "tm_019f34d6-2000-7777-8888-000000000001", "object": "trademark", "mark_text": "MERIDIAN CORE", "serial_number": "98123456", "office_code": "US" }, "request_id": "req_xyz" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/proceedings/prc_019f34d6-aaaa-7777-8888-aaaaaaaaaaaa" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const proceeding = await signa.proceedings.retrieve("prc_019f34d6-aaaa-7777-8888-aaaaaaaaaaaa"); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/proceedings/prc_019f34d6-aaaa-7777-8888-aaaaaaaaaaaa", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, ) ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------- | | 400 | `validation_error` | Invalid proceeding ID | | 401 | `unauthorized` | Missing or invalid API key | | 404 | `not_found` | Proceeding not found | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Search Proceedings](/api-reference/proceedings/search-proceedings): search proceedings with filters * [Trademark Proceedings](/api-reference/trademarks/trademark-proceedings): proceedings scoped to a mark * [Opposition & Dispute Intelligence](/guides/opposition-dispute-intelligence): workflow for profiling an opposer # Search Proceedings Source: https://docs.signa.so/api-reference/proceedings/search-proceedings GET /v1/proceedings Search trademark proceedings (oppositions, cancellations, appeals) across all marks ## When To Use This Use this endpoint when you need a cross-mark view of trademark disputes, such as profiling an opponent's history, checking whether a party usually settles or wins, or building a filtered litigation timeline for a class, office, or date window. At least one filter is required, and `aggregations` adds bucket maps over the exact same filtered set returned by the list query. For an end-to-end workflow, see [Opposition & Dispute Intelligence](/guides/opposition-dispute-intelligence). ## Query Parameters At least one of the following must be supplied: `contested_class`, `trademark_id`, `proceeding_type`, `status`, `q`, `party_owner_id`, `party_entity_id`, `entity_id`, `party_role`, `office_code`, `filed_date_gte`, `filed_date_lt`, `decision_date_gte`, `decision_date_lt`. `aggregations` alone does not satisfy the filter requirement. Nice class involved in the dispute (1-45). Restrict to proceedings on a specific trademark (`tm_...`). Type of proceeding. One of `opposition`, `cancellation`, `revocation`, `invalidity`, `appeal`, `non_use_removal`, `court_action`, `other`. Status. One of `pending`, `decided_granted`, `decided_rejected`, `withdrawn`, `settled`, `suspended`, `partial`, `other`. Search opponent / party name. Filter by a party owner ID (`own_...`). Filter by a resolved or derived entity ID (`ent_...`). The API expands the entity to its member owner IDs and matches proceedings where any member owner is a party. Alias for `party_entity_id`. Filter by party role. One of `opponent`, `petitioner`, `respondent`, `intervener`, `other`. Uppercase ST.3 office code (e.g. `US`, `EM`). Legacy lowercase codes (e.g. `uspto`, and `eu` for EUIPO) are accepted as permanent aliases. Filed date >= (YYYY-MM-DD). Filed date \< (YYYY-MM-DD). Must be after `filed_date_gte`. Decision date >= (YYYY-MM-DD). Decision date \< (YYYY-MM-DD). Must be after `decision_date_gte`. Comma-separated bucket maps to include in the response. Allowed values: `outcome`, `party_role`, `nice_class`, `office_code`, `filed_year`. Sort order. One of `-filed_date`, `filed_date`, `-decided_date`, `decided_date`. Page size (1-100). Pagination cursor from a previous response. ## Response Array of proceeding summary records. Present when requested. Shape: `{ aggregation_name: { bucket_key: count } }`. Each item carries `id` (`prc_...`), `trademark_id`, `proceeding_type`, `proceeding_number`, `status`, `outcome`, `duration_days`, `office_code`, `filed_date`, `decision_date`, `decision_outcome`, `contested_classes`, and `description`. The nullable item fields are `proceeding_number`, `status`, `outcome`, `duration_days`, `filed_date`, `decision_date`, `decision_outcome`, `contested_classes`, and `description` (`trademark_id` and `office_code` are always present). See [`Get Proceeding`](/api-reference/proceedings/retrieve-proceeding) for parties, outcome meanings, and the trademark cross-reference. Aggregation buckets are computed in SQL over the exact filtered proceedings set. `outcome`, `office_code`, and `filed_year` are single-valued dimensions, so their bucket sums reconcile with the filtered list count when their values are present. `nice_class` counts a proceeding once per contested class, and `party_role` counts a proceeding once per distinct role represented in its parties, so those bucket sums can be larger than the list count. ## Example Request ```bash cURL theme={null} curl -G "https://api.signa.so/v1/proceedings" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "party_entity_id=ent_019f34d6-3333-7777-8888-333333333333" \ --data-urlencode "office_code=US" \ --data-urlencode "aggregations=outcome,party_role,nice_class,office_code,filed_year" \ --data-urlencode "limit=20" ``` ## Example Response ```json theme={null} { "object": "list", "data": [ { "id": "prc_019f34d6-1111-7777-8888-111111111111", "object": "proceeding", "trademark_id": "tm_019f34d6-aaaa-7777-8888-111111111111", "proceeding_type": "opposition", "proceeding_number": "91265432", "status": "decided_granted", "outcome": "challenger_won", "duration_days": 184, "office_code": "US", "filed_date": "2023-03-01", "decision_date": "2023-09-01", "decision_outcome": "Opposition Sustained", "contested_classes": [9, 42], "description": "Opposition proceeding", "created_at": "2023-03-01T10:00:00.000Z", "updated_at": "2023-09-01T10:00:00.000Z" }, { "id": "prc_019f34d6-2222-7777-8888-222222222222", "object": "proceeding", "trademark_id": "tm_019f34d6-bbbb-7777-8888-222222222222", "proceeding_type": "opposition", "proceeding_number": "91270001", "status": "settled", "outcome": "settled", "duration_days": null, "office_code": "US", "filed_date": "2023-06-12", "decision_date": null, "decision_outcome": null, "contested_classes": [9], "description": "Opposition proceeding", "created_at": "2023-06-12T10:00:00.000Z", "updated_at": "2023-06-12T10:00:00.000Z" } ], "has_more": false, "pagination": { "cursor": null }, "aggregations": { "outcome": { "challenger_won": 1, "settled": 1 }, "party_role": { "opponent": 2, "respondent": 2 }, "office_code": { "US": 2 }, "filed_year": { "2023": 2 }, "nice_class": { "9": 2, "42": 1 } }, "request_id": "req_xyz" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/proceedings?proceeding_type=opposition&status=pending&office_code=US&filed_date_gte=2025-01-01&limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" curl "https://api.signa.so/v1/proceedings?party_entity_id=ent_019f34d6-3333-7777-8888-333333333333&office_code=US&aggregations=outcome,party_role,office_code,filed_year,nice_class" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const page = await signa.proceedings.list({ party_entity_id: "ent_019f34d6-3333-7777-8888-333333333333", office_code: "US", aggregations: ["outcome", "party_role", "office_code", "filed_year", "nice_class"], limit: 20, }); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/proceedings", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, params={ "party_entity_id": "ent_019f34d6-3333-7777-8888-333333333333", "office_code": "US", "aggregations": "outcome,party_role,office_code,filed_year,nice_class", "limit": 20, }, ) ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | No filter supplied, invalid date range, or unsupported parameter value | | 401 | `unauthorized` | Missing or invalid API key | | 422 | `entity_too_large` | `party_entity_id` / `entity_id` resolved to more member owners than the cap (`error.reason: member_owners_too_large`; carries `member_count` / `member_count_limit`) | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Proceeding](/api-reference/proceedings/retrieve-proceeding): full proceeding detail * [Trademark Proceedings](/api-reference/trademarks/trademark-proceedings): proceedings scoped to one mark * [Opposition & Dispute Intelligence](/guides/opposition-dispute-intelligence): workflow for profiling an opposer * [Use Case: Opposition Tracking](/guides/use-cases/opposition-tracking): monitoring active disputes # Rate Limits & Quotas Source: https://docs.signa.so/api-reference/rate-limits Beta quota pools, per-minute rate limits, response headers, and 429 handling Signa enforces two independent controls per organization: * **Monthly quotas** cap how many requests you can make against each pool over a billing period. Quotas reset on your billing anchor day (usually the first of the month). * **Rate limits** cap how many requests per minute you can make against each endpoint type. Rate limits slide in real time and are separate from monthly quotas: a single request counts against both. All API keys under the same organization share one quota and one rate-limit budget per pool. ## Monthly Quota Pools (Beta) Endpoints are grouped into pools. Each pool has its own monthly allowance. During beta, every organization is on the same plan with the following limits: | Pool | Beta monthly quota | Rate limit | Endpoints | | -------------- | -------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **search** | 100,000 | 1,000/min | `GET`/`POST /v1/trademarks`, `GET /v1/owners`, `GET /v1/attorneys`, `GET /v1/firms`, `GET /v1/entities`, `GET /v1/entities/{id}/trademarks`, `POST /v1/classifications/suggest` | | **read** | 500,000 | 10,000/min | `GET /v1/trademarks/{id}` and its sub-resources (`events`, `proceedings`, `assignments`, `documents`, `citations`, `cited-by`), `POST /v1/trademarks/batch`, `GET /v1/owners/{id}`, `GET /v1/attorneys/{id}`, `GET /v1/firms/{id}`, `GET /v1/entities/{id}`, `GET /v1/entities/{id}/family`, `GET /v1/proceedings`, `GET /v1/proceedings/{id}`, `GET /v1/citations`, `GET /v1/events`, portfolio endpoints | | **screening** | 1,000 | 100/min | `GET /v1/screening` | | **check** | 500,000 | 1,000/min | `POST /v1/goods-services/suggest`, `POST /v1/deadlines/compute`, `POST /v1/oppositions/compute`, `POST /v1/reconcile` | | **monitoring** | Unmetered (per-watch resource quota instead) | 100/min | `/v1/watches/*`, `/v1/alerts/*`, `/v1/webhooks/*` (except test and delivery reads, which are utility) | | **reference** | Unmetered | 1,000/min | `GET /v1/offices`, `GET /v1/jurisdictions`, `GET /v1/classifications`, `GET /v1/design-codes`, `GET /v1/deadline-rules`, `GET /v1/opposition-rules` | | **utility** | Unmetered | 1,000/min | `GET /v1/organization/*` | The MCP server (`/mcp`) is metered separately at 1,000 requests/min per organization; see [MCP tools & permissions](/ai/mcp-tools). The [health check endpoints](/api-reference/health) and `GET /v1/openapi.json` are unauthenticated and are not subject to any per-organization quota or rate limit. ### Pool rules of thumb * **List and search endpoints** (returning many results, ranked or filtered) are **search**. * **Detail endpoints** (returning one resource, or a batch of known IDs) are **read**. * **Static taxonomies** (offices, classifications, design codes) are **reference** and never count against a monthly quota. * **Dashboard routes** (usage, plan, API key management) are **utility** and never count against a monthly quota. Health and docs endpoints are unauthenticated and bypass rate limiting and quota entirely. To see your current usage per pool, call [Get Usage](/api-reference/administration/get-usage). Your plan limits are also included in every response under `by_endpoint_type[*].limit`. *** ## Rate Limits Each pool has a per-minute limit that applies regardless of HTTP method. This is the number you'll see in the `RateLimit-Policy` header and the one you'll normally hit. One notable exception: `POST /v1/trademarks/batch` bills against the **read** pool (10,000/min), but because it's a `POST` request its effective limit is **1,000/min**, the same ceiling that applies to other mutating requests. If you're bulk-hydrating IDs, pace batch calls accordingly. ### Concurrent wildcard searches Trademark searches that run a leading-wildcard scan (`match=contains`, `match=ends_with`, or any request with `mark_text_not_contains`) are also limited to **4 in flight at a time per organization**, independent of the per-minute budget. The cap covers `GET` and `POST /v1/trademarks` plus the party sub-resources (`/v1/owners/{id}/trademarks`, `/v1/attorneys/{id}/trademarks`, `/v1/firms/{id}/trademarks`, `/v1/entities/{id}/trademarks`), which all draw on the same organization budget. A fifth concurrent wildcard search returns `429` with `rate_limit.policy: "search-inflight"`, `Retry-After: 1`, `RateLimit: remaining=0, reset=1`, `X-Search-Concurrency-Limit: 4` and `X-Search-Concurrency-Remaining: 0` (`RateLimit-Policy` is unchanged: this is a concurrency cap, not a per-minute quota); retry as soon as one of your in-flight searches completes. Other match modes (`similar`, `exact`, `starts_with`) are not affected. *** ## Rate Limit Headers API responses include IETF-standard rate limit headers so you can monitor your usage in real time. One exception: the CDN-cached reference-data routes (`/v1/offices`, `/v1/jurisdictions`, `/v1/classifications`, `/v1/design-codes`) omit the `RateLimit-*` headers, since they carry per-organization values that must not leak into a shared cache hit. ``` HTTP/1.1 200 OK RateLimit-Policy: 1000;w=60 RateLimit: remaining=994, reset=30 ``` | Header | Format | Description | | ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RateLimit-Policy` | `{limit};w={windowSec}` | The limit in effect for this request. May list more than one comma-separated policy when more than one applies to the same request; `RateLimit` always reports the binding one. | | `RateLimit` | `remaining={N}, reset={seconds}` | Remaining requests and seconds until reset for the policy closest to being exhausted. | | `Retry-After` | `{seconds}` | Only present on `429` responses. Number of seconds to wait before retrying. | Monitor the `remaining` value proactively. If it drops below 10% of your limit, slow down requests rather than hitting 429 errors. *** ## Daily Sub-Caps In addition to the monthly quota, each metered pool has a daily sub-cap set at **10% of the monthly limit**. Its purpose is to prevent a single client from exhausting an entire month's allowance in minutes. | Pool | Beta monthly | Beta daily | | --------- | ------------ | ---------- | | search | 100,000 | 10,000 | | read | 500,000 | 50,000 | | screening | 1,000 | 100 | | check | 500,000 | 50,000 | The daily counter resets at **UTC midnight**. ### Daily quota headers Every metered response includes three additional headers alongside the rate-limit headers: | Header | Format | Description | | ------------------------- | ------------------ | ------------------------------------------------- | | `X-Quota-Daily-Limit` | integer | Total daily units allowed for this pool. | | `X-Quota-Daily-Remaining` | integer | Remaining daily units. | | `X-Quota-Daily-Reset` | ISO 8601 timestamp | Next UTC midnight, when the daily counter resets. | `X-Quota-Limit`, `X-Quota-Remaining`, and `X-Quota-Reset` report the monthly counter using the same shape. These headers count quota units (per day or per month), not requests per minute, so don't confuse them with `RateLimit-*`. They're omitted on unmetered pools (reference, utility) and on `404` responses, since neither spends quota. ### 429 with `quota_scope` If a request exceeds either cap, you receive a `429` with `error.type = "quota_exceeded"`. The `error.quota_scope` field tells you which counter was breached: ```json theme={null} { "error": { "type": "quota_exceeded", "title": "Daily quota exceeded", "detail": "You have used 10000 of 10000 units today. Quota resets at 2026-05-13T00:00:00.000Z.", "quota_scope": "daily", "quota_limit": 10000, "quota_used": 10000, "quota_resets_at": "2026-05-13T00:00:00.000Z" }, "request_id": "req_..." } ``` `quota_scope` is `"monthly"` or `"daily"`. `error.type` stays `"quota_exceeded"` regardless; switch on `quota_scope` if you need to distinguish. *** ## 429 Response When you exceed your rate limit, the API returns a `429 Too Many Requests` status with details about when you can retry: ``` HTTP/1.1 429 Too Many Requests RateLimit-Policy: 1000;w=60 RateLimit: remaining=0, reset=12 Retry-After: 12 ``` ```json theme={null} { "error": { "type": "rate_limited", "title": "Rate limit exceeded", "status": 429, "detail": "Rate limit exceeded. Retry after 12 seconds.", "retryable": true, "retry_after": 12 }, "request_id": "req_abc123" } ``` The `Retry-After` response header and the `retry_after` field in the body both contain the number of seconds to wait. Ignoring `429` responses and continuing to send requests will not help: those requests are also rejected. In extreme cases, sustained limit violations may result in a temporary block of your API key. *** ## Monitoring Usage Check your current billing period usage and rate limit status with [Get Usage](/api-reference/administration/get-usage): ```bash theme={null} GET /v1/organization/usage Authorization: Bearer sig_YOUR_KEY ``` ```json theme={null} { "object": "usage", "billing_period": { "start": "2026-04-01T00:00:00Z", "end": "2026-04-30T23:59:59Z" }, "by_endpoint_type": { "search": { "used": 1204, "limit": 100000 }, "read": { "used": 8932, "limit": 500000 }, "screening": { "used": 3, "limit": 1000 }, "check": { "used": 40, "limit": 500000 } }, "rate_limits": { "search": 1000, "read": 10000, "screening": 100, "check": 1000, "monitoring": 100, "utility": 1000, "reference": 1000 }, "request_id": "req_abc123" } ``` `by_endpoint_type` reports `used` and `limit` for every metered pool: `search`, `read`, `screening`, and `check`. A `limit` of `null` means unlimited; `0` means the pool is not allowed on your plan. `rate_limits` is the per-endpoint-type requests-per-minute map the limiter enforces, including the unmetered pools (`monitoring`, `utility`, `reference`) that still carry a per-minute ceiling even though they don't spend a monthly quota. Avoid polling `/v1/organization/usage` in a tight loop. It is classified **utility**: unmetered against any monthly quota, but still subject to the utility per-minute rate limit (1,000/min on beta). *** ## Handling 429 in Code The Signa TypeScript SDK handles 429 responses automatically with built-in retry logic; see [SDK Error Handling](/sdk/typescript#error-handling). If you are implementing your own retry logic, wait for the `Retry-After` duration and retry with exponential backoff. *** ## Best Practices A single batch request of 100 IDs counts as 1 request against your rate limit, compared to 100 individual GET requests. See [Batch Retrieve](/api-reference/trademarks/batch-trademarks). Using `If-None-Match` headers with ETags avoids downloading unchanged response bodies, saving bandwidth and processing time. `304 Not Modified` responses still count against your rate limit, but they are significantly cheaper for both client and server. See the [caching guide](/guides/caching) for implementation patterns. If you are periodically checking for trademark status changes, use [Trademark Events](/api-reference/trademarks/trademark-events) on specific marks rather than re-running broad searches. Bursting 500 requests in the first second of a window is more likely to trigger rate limiting than spreading them evenly across the minute. If you need to process a large batch, add a small delay (50-100ms) between requests. If your application has both a user-facing dashboard and a background sync job, create separate API keys for each. This prevents a background job from exhausting the rate limit that your dashboard users depend on. *** ## Need Higher Limits? If you are consistently hitting rate limits, reach out at [support@signa.so](mailto:support@signa.so) to discuss higher limits tailored to your workload. # Retrieve Assignment Source: https://docs.signa.so/api-reference/records/transactions/retrieve-assignment GET /v1/assignments/{id} Retrieve one recorded trademark transaction with parties and affected marks ## When To Use This Use this endpoint when you already have an assignment ID and need the full parties plus affected marks for one recorded transaction. A diligence team can pull a security-interest record to identify the borrower, lender, execution date, reel/frame, and every trademark covered by the filing. For release pairing across a mark or owner, see [Chain-of-title & Lien Diligence](/guides/chain-of-title-lien-diligence). ## Path Parameters Assignment ID (`asg_...`). ## Response Assignment ID (`asg_...`). Always `assignment`. USPTO assignment reel number. USPTO assignment frame number. Normalized conveyance type, such as `assignment`, `security_interest`, or `release`. Office conveyance text. Date the transaction was recorded. Date the source office last updated this assignment record. Number of pages in the recorded assignment document, when known. Reel number released by this record, when the conveyance is a release. Frame number released by this record, when the conveyance is a release. Correspondent payload from the assignment record, when available. A pass-through object of the office-provided fields — a `name` plus address fields such as `address1`–`address4` (which keys are present varies by record). Parties to the recorded transaction. Each party has `role`, `name`, `owner_id`, `execution_date`, `date_acknowledged`, `legal_entity_text`, `nationality`, and `address`. `address` is a pass-through object of the office-provided party address fields (such as `address1`–`address2`, `city`, `state`, `postcode`), or `null` when the office recorded none. `date_acknowledged` is the date the party acknowledged the conveyance, or `null`. Affected trademark records. Each property has `trademark_id`, `serial_no`, `registration_no`, `intl_reg_no`, and `mark_name`. ISO timestamp for when Signa created the assignment record. ISO timestamp for the latest Signa update to the assignment record. ## Example Request ```bash cURL theme={null} curl "https://api.signa.so/v1/assignments/asg_cacc6877-1e78-4290-9c5d-1b5c4ef832f7" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` ## Example Response ```json theme={null} { "id": "asg_cacc6877-1e78-4290-9c5d-1b5c4ef832f7", "object": "assignment", "reel_no": "9252", "frame_no": "0333", "conveyance_type": "security_interest", "conveyance_text": "SECURITY INTEREST", "recorded_date": "2026-04-30", "last_update_date": "2026-07-07", "page_count": 15, "release_of_reel_no": null, "release_of_frame_no": null, "created_at": "2026-07-09T06:29:56.742Z", "updated_at": "2026-07-09T06:29:56.742Z", "correspondent": { "name": "Beth Frankenfield", "address1": "1735 Market Street 51st Fl.", "address2": "Philadelphia, PA 19103" }, "parties": [ { "role": "assignee", "name": "PNC BANK, NATIONAL ASSOCIATION", "owner_id": null, "execution_date": null, "date_acknowledged": null, "legal_entity_text": "NATIONAL BANKING ASSOCIATION", "nationality": "UNITED STATES", "address": { "address1": "1600 MARKET STREET", "city": "PHILADELPHIA", "state": "PENNSYLVANIA", "postcode": "19103" } }, { "role": "assignor", "name": "PMC BIOGENIX (KOREA) LTD.", "owner_id": null, "execution_date": "2026-04-24", "date_acknowledged": null, "legal_entity_text": "CORPORATION", "nationality": "KOREA, REPUBLIC OF", "address": null } ], "properties": [ { "trademark_id": null, "serial_no": "98352651", "registration_no": "7806315", "intl_reg_no": null, "mark_name": "ARMOSLIP" } ], "request_id": "req_01kx2v1xdwhpsazba6p3effxm9" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/assignments/asg_cacc6877-1e78-4290-9c5d-1b5c4ef832f7" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const assignment = await signa.assignments.retrieve( "asg_cacc6877-1e78-4290-9c5d-1b5c4ef832f7", ); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/assignments/asg_cacc6877-1e78-4290-9c5d-1b5c4ef832f7", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, ) ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------- | | 400 | `validation_error` | Invalid assignment ID | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | Missing `trademarks:read` scope | | 404 | `not_found` | Assignment not found | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Search Assignments](/api-reference/records/transactions/search-assignments): find recorded transactions by owner, entity, trademark, or type * [Trademark Assignments](/api-reference/trademarks/trademark-assignments): chain of title for one mark * [Chain-of-title & Lien Diligence](/guides/chain-of-title-lien-diligence): pair security interests with releases # Search Assignments Source: https://docs.signa.so/api-reference/records/transactions/search-assignments GET /v1/assignments Search recorded trademark transfers, liens, and releases ## When To Use This Use this endpoint when a brand-transaction analyst, lender, or counsel needs to filter recorded transfers and security interests across an owner or corporate family. For example, a lender checking existing liens before brand-backed lending can replace a manual UCC plus USPTO dual search with `owner_id` or `entity_id` plus `type=security_interest`. This endpoint always requires a filter and is never a firehose. For an end-to-end workflow, see [Chain-of-title & Lien Diligence](/guides/chain-of-title-lien-diligence). ## Query Parameters At least one of `owner_id`, `entity_id`, `trademark_id`, `conveyance_type`, `type`, `recorded_date_gte`, `recorded_date_lt`, or `role` must be supplied. Filter by resolved owner ID (`own_...`). Matches assignments where the owner appears as any party unless `role` is also supplied. Filter by resolved entity ID (`ent_...`) across member owner records. Filter by affected trademark ID (`tm_...`). Comma-separated conveyance types. Allowed values: `assignment`, `security_interest`, `release`, `merger`, `name_change`, `license`, `partial_assignment`, `correction`, `entity_conversion`, `other`. Alias for `conveyance_type`. Use either `type` or `conveyance_type`, not both. Recorded date >= (YYYY-MM-DD). Recorded date \< (YYYY-MM-DD). Must be after `recorded_date_gte`. Filter by party role. One of `assignor`, `assignee`. Sort order. One of `-recorded_date`, `recorded_date`. Page size (1-100). Pagination cursor from a previous response. ## Response Array of assignment summary records. Each item carries `id`, `object`, `reel_no`, `frame_no`, `conveyance_type`, `conveyance_text`, `recorded_date`, `last_update_date`, `page_count`, `release_of_reel_no`, `release_of_frame_no`, `created_at`, and `updated_at`. Use [Retrieve Assignment](/api-reference/records/transactions/retrieve-assignment) when you need parties and affected marks. ## Example Request ```bash cURL theme={null} curl -G "https://api.signa.so/v1/assignments" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "owner_id=own_019f20b5-0a66-76c2-968e-d35f9eb1eb87" \ --data-urlencode "type=security_interest" \ --data-urlencode "recorded_date_gte=2026-04-27" \ --data-urlencode "recorded_date_lt=2026-04-28" \ --data-urlencode "limit=20" ``` ## Example Response ```json theme={null} { "object": "list", "data": [ { "id": "asg_15b6d53f-3dfd-4714-8623-e369d0f2aad6", "object": "assignment", "reel_no": "9248", "frame_no": "0297", "conveyance_type": "security_interest", "conveyance_text": "SECURITY INTEREST", "recorded_date": "2026-04-27", "last_update_date": "2026-06-30", "page_count": 6, "release_of_reel_no": null, "release_of_frame_no": null, "created_at": "2026-07-09T06:29:33.527Z", "updated_at": "2026-07-09T06:29:33.527Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_01kx3pgq4xmn84ze3g8ydreag9" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/assignments?owner_id=own_019f20b5-0a66-76c2-968e-d35f9eb1eb87&type=security_interest&limit=20" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const liens = await signa.assignments.list({ owner_id: "own_019f20b5-0a66-76c2-968e-d35f9eb1eb87", type: "security_interest", limit: 20, }); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/assignments", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, params={ "owner_id": "own_019f20b5-0a66-76c2-968e-d35f9eb1eb87", "type": "security_interest", "limit": 20, }, ) ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------------------------------------------------------------------------------ | | 400 | `validation_error` | No filter supplied, invalid date range, unsupported parameter, or both `type` and `conveyance_type` supplied | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | Missing `trademarks:read` scope | | 422 | `entity_too_large` | `entity_id` resolved to more member owners than the cap | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Assignment](/api-reference/records/transactions/retrieve-assignment): full parties and affected marks for one transaction * [Trademark Assignments](/api-reference/trademarks/trademark-assignments): chain of title for one mark * [Chain-of-title & Lien Diligence](/guides/chain-of-title-lien-diligence): recipe for owner, lien, and release checks # Cast Office Votes Source: https://docs.signa.so/api-reference/reference/cast-office-votes POST /v1/offices/votes Vote for trademark offices you want on Signa ## Overview Tell us which trademark offices your organization wants covered next — votes directly drive our coverage roadmap. Vote for as many offices as you like: each office counts **once per organization**, no matter how many times (or through how many API keys) you vote, so re-votes are harmless no-ops. The office-vote endpoints are the one write surface in the reference-data section — `POST` and `DELETE` both take the usual write ceremony: an `Idempotency-Key` header is required, and the request counts against the write rate bucket (responses carry the standard `RateLimit-*` headers). Voting is free — `POST` and `DELETE` bill 0 units. Reading your votes back with [List Office Votes](/api-reference/reference/list-office-votes) bills 1 read unit. Retrying with the **same** `Idempotency-Key` replays the cached response of that attempt; a **new** logical attempt — re-casting a vote you previously retracted, say — needs a **fresh** key. Either way the once-per-org, once-per-office counting is enforced server-side, so a fresh key can never double-count you. ## Votable codes Office codes are WIPO ST.3 two-letter codes. You can vote for: * **Any ISO 3166-1 country code** — `CL`, `MX`, `IN`, … (`UK` is accepted and canonicalized to `GB`, `EL` to `GR`). * **Intergovernmental offices with no ISO country code**: `BX` (Benelux/BOIP), `AP` (ARIPO), `OA` (OAPI) — plus `XK` (Kosovo — ST.3-assigned, no ISO code), which is a national office rather than an IGO but is votable on the same footing. * Legacy lowercase office codes are accepted as aliases and canonicalized — `ukipo` → `GB`, `dpma` → `DE`. Two kinds of code are rejected: * **Offices already covered by Signa** — including `EM` (EUIPO) and `WO` (WIPO), which are covered today. Voting for one returns `400` with an `office_already_live` error entry. ([List Offices](/api-reference/reference/list-offices) shows the offices currently **serving** data — an office that is covered but temporarily offline is absent from it, yet still not votable, so the rejection is the authoritative answer.) A dependent territory has no register of its own, so it resolves through the office that registers its marks — `PR`/`GU` → USPTO, `RE`/`GF` → INPI-FR, `SJ` → NIPO, `CX`/`CC`/`NF` → IP Australia — and a vote for it is a vote for that parent office: rejected as already covered when the parent is covered, otherwise stored under the parent's code. * **Codes with no trademark register of their own** — uninhabited or non-filing territories such as `AQ` (Antarctica), `BV`, `HM`, `TF`, `GS`, `IO`, `UM`, and `PN`. These return `400` with a `no_trademark_register` error entry: there is nothing to connect, so there is nothing to vote for. Validation is all-or-nothing: if any code in the batch is unknown, already covered, or has no register, nothing is recorded. ## Body Parameters Office codes to vote for (1–50). Any case; aliases are canonicalized. Codes your organization already voted for are no-ops. ## Response A list of the stored votes for every requested code — newly cast or pre-existing alike. `created_at` is when your organization **first** voted for that office. Always `list`. Always `office_vote`. Canonical uppercase ST.3 office code. `live` (now on Signa — your ask landed), `roadmap` (modeled, connector not shipping yet), or `not_covered` (not modeled yet). When your organization first voted for this office. Always `false` — the votable vocabulary is bounded, so results are a single page. ```json theme={null} { "object": "list", "data": [ { "object": "office_vote", "office_code": "CL", "office_status": "not_covered", "created_at": "2026-08-29T12:00:00.000Z" }, { "object": "office_vote", "office_code": "MX", "office_status": "not_covered", "created_at": "2026-08-29T12:00:00.000Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_01kabc..." } ``` ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/offices/votes" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: office-votes-cl-mx-001" \ -d '{ "office_codes": ["CL", "MX"] }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const votes = await signa.references.castOfficeVotes({ office_codes: ["CL", "MX"] }); for (const vote of votes.data) { console.log(vote.office_code, vote.office_status, vote.created_at); } ``` ## Errors | Status | Type | Description | | ------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | One or more codes rejected: `unknown_office_code` (not a known office or country code), `office_already_live` (already covered by Signa, including territories of a covered office), `no_trademark_register` (no register of its own, e.g. `AQ`). Also an empty or over-50 `office_codes` array | | 400 | `validation_error` | Missing `Idempotency-Key` header | | 401 | `unauthorized` | Missing or invalid API key | | 409 | `conflict` | `Idempotency-Key` reused with a different body | | 409 | `idempotency_processing` | A request with the same `Idempotency-Key` is still in flight | | 429 | `rate_limited` | Too many requests | Every rejected code comes back as its own entry in `errors[]`, each with `field: "office_codes"` and one of the codes above. ## Related Endpoints * [List Office Votes](/api-reference/reference/list-office-votes), read your organization's votes back * [Retract Office Vote](/api-reference/reference/retract-office-vote), remove a vote * [List Offices](/api-reference/reference/list-offices), the offices already covered by Signa # Compute Deadlines Source: https://docs.signa.so/api-reference/reference/compute-deadlines POST /v1/deadlines/compute Compute trademark maintenance and prosecution deadlines without persisting data ## Overview Compute maintenance deadlines for any set of trademarks (renewal windows, declarations of use, grace periods, and what happens if each is missed) without storing anything in Signa. Send the facts you already have: jurisdiction, filing and registration dates, status. Signa returns the exact dated obligations, computed from the same rules that power its trademark data. A missed renewal can cost a registration, and this is the deadline logic you'd otherwise build and maintain office by office, kept current behind a single request. Optionally send `facts` on an item to also compute **prosecution deadlines**: office action response periods and statement of use clocks, resolved per fact instance. Every computed prosecution date ships with the rule that produced it, the era it belongs to, when that rule was last verified, and the pinpoint sources behind it. Pair it with [List Deadline Rules](/api-reference/reference/deadline-rules) to inspect the underlying rules, renewal cycles, and statutory citations behind each computation. `facts` is additive. For caller-supplied facts items, both prosecution fields (`support` and `prosecution_deadlines`) appear only on items that sent `facts`, and sending `facts: []` opts in to the capability report without supplying any facts. Omitting `facts` does not mean the item is identical to what it was before this release: the same release adds rule identity, so every item now carries `sources`, `as_of` and `computed_at`, and every `deadlines[]` row carries `rule_id`, `effective_from`, `last_verified`, `due_date_adjustment`, `grace_expiry_adjustment` and `holiday_calendar` — with or without `facts`. ## Prosecution coverage Prosecution deadlines are not uniformly derivable, and the response says which of three states you are in rather than flattening them together. Read `support[].status` on each item: | `status` | Meaning | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `computed` | A modelled rule produced or retired a date. | | `stated` | No modelled rule exists, but the office stated the deadline itself, and Signa surfaces that date with the notification as its authority. | | `facts_required` | A rule exists, verified, but nothing triggered it. Supply the trigger fact. | | `unsupported` | No rule is modelled for this office, route, and deadline type. | What that means per office today: | Office | Office action response | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | USPTO | Rules modelled for direct (Section 1/44) and Section 66(a) filings, plus post registration refusals and the statement of use ladder. | | EUIPO | Rules verified and modelled, but the EUIPO trade marks feed publishes no examination or refusal correspondence at all, so Signa has no EUIPO office action to read. Expect `facts_required` until you send an `office_action` fact. | | Madrid (WIPO) | No Madrid level rule exists and none will. The Madrid System does not set the holder response period; it is designated office law. Use the modelled USPTO and EUIPO designated office rules, or send a `stated_deadline` fact carrying the period the office declared under Rule 17(2)(vii) to (viii). | Send `{ "id": "tm_..." }` to resolve a stored trademark’s maintenance inputs and prosecution event facts server-side. Stored facts use events dated on or before `as_of_date`. The engine reports `facts_required` when available events cannot establish a deadline. Caller-supplied facts items remain supported and can be mixed with ID items; an item must use exactly one shape. Computation does not persist data. Madrid provisional refusals are being added to that history. Signa records each designated office refusal from the WIPO ROMARIN feed as an `office_action` event with `event_scope: "designation"` and the `territory_code` of the designation it refuses, so a mark's refusal sequence (provisional refusal, then any Rule 18ter confirmation or further decision) is queryable per territory instead of collapsing into a single current status. When the feed names the classes a partial refusal hit, Signa stores one event per class with `nice_class_number` set; when it does not, the partial refusal is a single event with `nice_class_number` null. Coverage is filling in rather than complete. ROMARIN is a daily changed-marks feed and no historical backfill has been run, so an international registration gains its refusal history the next time it appears in a daily file. Treat an empty refusal history as "not yet loaded", not as "no refusal". The refusal date is there; the response period is not. ROMARIN publishes no time limit on a refusal record, so the Rule 17(2)(vii) period still has to come from the notification itself, as a `stated_deadline` fact. ## Stored trademarks ```typescript theme={null} const results = await signa.deadlines.compute({ items: [{ id: 'tm_12345678-1234-1234-1234-123456789012' }], }); for (const result of results.data) { if (result.object === 'deadline_computation_error') { console.log(result.id, result.error.type); } else { console.log(result.deadlines, result.prosecution_deadlines); } } ``` ID items always include the prosecution capability report, even when no usable events are stored. Historical `as_of_date` filters the stored events by date; it does not reconstruct a historical version of the trademark record. ## Request Body Marks to compute, max 1,000 items. Results are returned in the same order, with one result per input item. A missing or non-retrievable ID produces a `deadline_computation_error` with `id` and `error: { type: "not_found", detail }`; other items still compute. Every item costs one check unit, including an unknown ID. Use either a stored ID object (`{ "id": "tm_..." }`) or the facts object below. Deadline jurisdiction code, e.g. `US`, `EU`, or `WIPO`. Values are normalized to uppercase. Filing route. One of `national`, `regional`, or `madrid`. Defaults to `national`. Trademark status. Defaults to `registered`. Accepted values are `filed`, `examining`, `pending_publication`, `published`, `opposition_period`, `pending_opposition`, `pending_cancellation`, `pending_issuance`, `registered`, `allowed`, `abandoned`, `withdrawn`, `surrendered`, `refused`, `cancelled`, `invalidated`, `expired`, or `unknown`. Filing date as `YYYY-MM-DD`. Earliest Paris/WTO convention priority date as `YYYY-MM-DD`. Read only by jurisdictions whose statute registers a convention application AS OF that date, so that the statutory term runs from it. India (Trade Marks Act 1999 s. 154(2)) is the only one today; everywhere else it is ignored. Omit it and India falls back to `filing_date` (s. 23(1)). Registration date as `YYYY-MM-DD`. Grant date as `YYYY-MM-DD`. When omitted, the `registration_date` you send in the same item is used as the grant date for BR and MX items with `filing_route: "national"` (the default when `filing_route` is omitted); an explicit `grant_date` always wins. An item carrying `international_registration_date` is treated as Madrid-provenanced and never takes the fallback. Send `filing_route: "madrid"` for designations. Current expiry date as `YYYY-MM-DD`. Known renewal due date as `YYYY-MM-DD`. Date the registration was last renewed, as `YYYY-MM-DD`. Read only by jurisdictions whose transitional rules turn on **when** a renewal was effected rather than on when the cycle fell due. Canada is the only one today. Trademarks Regulations (SOR/2018-227) s. 159 opens the filing window at the period start for "the first renewal, on or after the coming-into-force day" of 17 June 2019, so a Canadian registration whose term ended shortly before that day but whose fee was paid in the grace period on or after it is the s. 159 renewal, and the renewal after it is an ordinary s. 76 one. Omit it and Canada falls back to the cycle due date, which is right except in that cohort. Ignored everywhere else. Madrid designation date as `YYYY-MM-DD`. Protection grant date as `YYYY-MM-DD`. Madrid international registration date as `YYYY-MM-DD`. The maintenance filing under examination. Required by post registration refusal response rules, whose deadline is six months from issuance **or** the end of the statutory Section 8 / 71 / 9 filing window, whichever is later (37 CFR 2.163(b), 2.184(b)(1), 7.39(a)). Never inferred: nothing on an office action says which filing period is being examined, and deriving the window end from the registration date would fabricate a date years past the legally applicable period. Without it the engine returns `maintenance_window_not_established`. One of `section_8`, `section_71`, `section_9`. Inclusive last day of that filing period **excluding any grace period**, as `YYYY-MM-DD`: the Section 8(a)(1) or 8(a)(2) window end, the Section 71(a)(1) or 71(a)(2) window end, or, for Section 9, the registration expiration date. **Do not add the grace period.** Each of the three regulations pairs its "whichever is later" floor with a grace carve-out that only reads coherently if the floor stopped short of grace: the registration is cancelled or expires "unless time remains in the grace period" (37 CFR 2.163(c), 7.39(b), 2.184(b)(1)). A grace-inclusive date pushes `effective_due_date` up to six months past what the regulation allows, and the floor is applied unconditionally, so you would be told to respond after the registration is already cancelled or expired. Time remaining in the grace period is a separate right: file a **complete new** Section 8 or 71 affidavit, or a complete new Section 9 renewal application, with the grace surcharge. It does not extend the refusal response deadline, and Signa does not compute it here. Prosecution fact instances for this mark, max 100 per item and 1,000 across the batch. Each fact is discriminated by `kind` and carries an `id` that is unique within the item; linkage fields (`responds_to_office_action_id`, `responds_to_notice_of_allowance_id`, and so on) address other facts by that `id`. Supported kinds: `office_action`, `oa_response`, `oa_extension`, `notice_of_allowance`, `sou`, `sou_extension`, `suspension`, `abandonment`, `petition_to_revive`, `stated_deadline`. `office_action`. Caller assigned, unique within the item. One of `nonfinal`, `final`, `subsequent_final`, `examiner_amendment`, `ea_priority_combined`, `examiners_statement`, `priority_action`, `suspension_letter`, `suspension_inquiry`. The date the deadline runs from, as `YYYY-MM-DD`. For a Section 66(a) provisional refusal this is the date the USPTO sent the action to the International Bureau (TMEP 1904.02(h)), not the drafted date. How that date was determined: `mailed_row`, `ib_transmittal_row`, `written_row`, or `caller_supplied` (the default). Each rule accepts only its own bases, so a mismatch returns `date_basis_not_accepted` rather than a wrong date. Defaults to `false`. The deadline printed on the action. 37 CFR 2.62(a) makes it authoritative over the rule period when present. Examiner draft date. Section 66(a) only: the date the refusal was transmitted to the International Bureau. Defaults to `false`. `notice_of_allowance`. Caller assigned, unique within the item. Issue date as `YYYY-MM-DD`. Defaults to `false`. The notice this one replaces, when known. When set, this notice sets no Section 1(d) clock. `stated_deadline`. Caller assigned, unique within the item. ST.3 code of the office that stated the deadline. The date the office stated. Supply this, `stated_period`, or both. `{ "value": 15, "unit": "day" }`. A period alone resolves only when `basis` is `explicit_dates`; otherwise the anchor date is not stored and the engine returns `stated_deadline_not_resolvable` rather than counting from a date that merely happens to be at hand. The declared calculation basis (Madrid Rule 17(2)(viii)): `office_issue_date`, `office_to_ib_send_date`, `ib_to_holder_transmittal_date`, `holder_receipt_date`, or `explicit_dates`. Required with `basis: "explicit_dates"`, and rejected otherwise. Required with `basis: "explicit_dates"`, and rejected otherwise. The refusal this deadline was stated in, when the link is known. `filed_date` (required), `responds_to_office_action_id`, `is_request_for_reconsideration`, `linkage` (`inferred` by default, or `ambiguous`). `requested_date`, `granted_date`, `responds_to_office_action_id`, `linkage`. `filed_date` (required), `accepted_date`, `responds_to_notice_of_allowance_id`. `ordinal` (1 to 5, or null), `filed_date`, `granted_date`, `extends_notice_of_allowance_id`, `linkage`. `subtype` (`letter`, `inquiry`, `periodic_check`, required), `operative_date` (required), `responded_date`. `reason` (required), `abandoned_date` (required), `notice_date`, `follows_office_action_id`, `follows_notice_of_allowance_id`, `linkage`. `filed_date`, `granted_date`. Calendar date to compute relative to, as `YYYY-MM-DD`. Defaults to today. How many years ahead to include. Minimum `1`, maximum `15`, default `5`. Include optional deadlines such as US Section 15 incontestability. Defaults to `true`. Include deadlines already past their grace period. Defaults to `false`. ## Response A standard list response with `data: DeadlineComputation[]`. Pagination is not used; `data[i]` corresponds to `items[i]`. Always `list`. Always `deadline_computation`. Jurisdiction used for the computation. One of `national`, `regional`, or `madrid`. `true` when Signa has a **maintenance** deadline configuration for the jurisdiction. This has never been a statement about prosecution, and it is not one now: a US mark with no office action facts still has a renewal schedule. Per deadline type capability lives on `support`. `unsupported_jurisdiction` when no rule configuration exists for the jurisdiction; `requires_office_date` when the record does not carry the input the statute needs (Australian direct marks filed before 1996-01-01, Singapore direct marks filed before 1999-01-15, or a Madrid designation with no international-registration date) and no `expiry_date` was supplied to anchor on. Supply the office-stated `expiry_date` and the schedule is computed from it. Otherwise `null`. Stable, permanent slug of the statutory rule that produced this row, e.g. `us_declaration_of_use_s8`. Use it as the join key to [List Deadline Rules](/api-reference/reference/deadline-rules). Treat it as **opaque**: do not parse it, split it, or infer a jurisdiction from it. It is deliberately not a prefixed entity ID (`tm_`, `own_`) because it names a statutory rule, not a stored entity. Earliest date from which the rule's modelled content is authoritative, as `YYYY-MM-DD`. `null` when the source corpus does not establish one, which is the common case. This is not a versioning pivot: rules whose content changed by era (for example a renewal term that changed with a statute) are modelled internally and still report `null` here. Date Signa last checked this rule against its statutory sources, as `YYYY-MM-DD`. Carried from the jurisdiction configuration the rule belongs to. Deadline type code, e.g. `declaration_of_use` or `renewal`. Human-readable deadline name. Jurisdiction for this deadline. Date used as the computation trigger. Input field used as the trigger, e.g. `registration_date`. First filing date for the deadline window. Statutory due date. End of the grace period after the due date. Deadline status: `future`, `window_open`, `due_soon`, `in_grace`, or `missed`. Calendar days from `as_of_date` to `due_date`. Calendar days from `as_of_date` to `grace_expiry`. Urgency bucket: `critical`, `upcoming`, `routine`, `in_grace`, or `missed`. Outcome if the deadline plus grace is missed. One of `expiration` (the right ends by operation of law when the term plus any grace runs out), `removal_from_register` (the office strikes the entry as the statutory consequence), `cancellation` (the office cancels a still-running registration for a missed maintenance filing, US §8/§71), `cancellation_and_expiration` (combined US §8+§9), `expungement` (Canada's statutory word), `lapse_of_international_registration` (Madrid), `permanent_loss` (restoration rules: the last remedy has passed) or `none` (optional filing). Whether a remedy exists afterwards is said by the presence of a `restoration` rule, not by this value. See the [deadline rules guide](/guides/deadline-rules#consequence-vocabulary). Whether the deadline repeats in later cycles. Whether the filing is optional. What moved `due_date` off the plain statutory arithmetic. One of `moved` (the business-day roll changed the date), `unchanged` (a calendar was consulted and the date was already a business day), `not_checked`, or `month_end_overflow` (the period ran into the following calendar month because its end day number does not exist in the target one). See [Business-day adjustment](#business-day-adjustment), and [Accuracy limits](/guides/deadline-rules#accuracy-limits) for the bounded set of cases where a served date can be LATER than the true one. Same vocabulary, for `grace_expiry`. It is evaluated independently of `due_date_adjustment`, so the two can differ on the same row. Identifier of the holiday calendar **configured for this office**, e.g. `uspto_dc` or `ipos_singapore`. `null` when the jurisdiction does not opt into business-day adjustment or no calendar is wired for it. This is a calendar identifier, not an office code, and it describes configuration, not whether a given date was actually rolled — read the two `*_adjustment` fields for that. The statutory sources of the jurisdiction configuration this computation used. Provenance is per jurisdiction, not per rule: `us_declaration_of_use_s8`, `us_declaration_of_incontestability_s15` and `us_renewal_s9` all rest on the same three USPTO citations, so they are carried once per item rather than once per rule or once per row. For detail about one rule, join a row's `rule_id` to [Deadline Rules](/api-reference/reference/deadline-rules). Empty `[]` only when no configuration exists for the jurisdiction; an item declined by the computability gate still reports its jurisdiction's citations. Human-readable name of the source Signa verified the configuration against, e.g. `TMEP § 1604.04 — Time for Filing §8 Declaration`. Canonical URL for the source. The resolved as-of date actually used, as `YYYY-MM-DD`. Echoes `as_of_date` when you sent one, and reports the server's date when you did not. Store it with the result and you can replay the same computation later and get the same clock-dependent fields (`status`, `urgency`, `days_until_due`, `days_until_grace_expiry`). ISO-8601 UTC timestamp of when the computation ran. Wall-clock provenance for the response; it is `as_of`, not `computed_at`, that drives the arithmetic. Per deadline type capability. Present only when the item sent `facts`. One entry for `maintenance` plus one for each of `oa_response`, `suspension_inquiry_response`, `post_registration_oa_response`, and `sou`. SOU-extension facts adjust the `sou` deadline rather than producing a separate deadline type. `maintenance`, `oa_response`, `suspension_inquiry_response`, `post_registration_oa_response`, or `sou`. `computed`, `stated`, `facts_required`, or `unsupported`. See [Prosecution coverage](#prosecution-coverage). `false` only when `status` is `unsupported`. The engine reason code, when there is one, for example `date_basis_not_accepted`, `maintenance_window_not_established`, or `unsupported_office`. Treat this as an open enum. The modelled rules behind this deadline type. Present for prosecution types, omitted for `maintenance`. One result per prosecution trigger instance, plus any office stated deadline that hangs off none of them. Present only when the item sent `facts`. Always `prosecution_deadline`. `computed`, `closed`, `stated`, `not_applicable`, `insufficient_facts`, `ambiguous`, or `unsupported_jurisdiction`. Which prosecution deadline this is. `null` when no rule was selected. The `facts[].id` whose date started this clock. The date the clock ran from. `mailed_row`, `ib_transmittal_row`, `written_row`, or `caller_supplied`. The unextended deadline. Set when a linked, rule permitted extension applies. The date to act on: the extended date if any, raised to any statutory floor. What the business-day roll did to this row. `moved`: the office holiday calendar was consulted and rolled the date forward off a day the office is shut. `unchanged`: the calendar was consulted and the date was already an open day. `not_checked`: no calendar verdict stands behind the date, so what you have is the raw statutory anniversary. `not_checked` covers three cases: the date falls outside the office calendar's pinned coverage years, the rule ships without a roll rule, or the office stated the date itself (a stated deadline is published exactly as stated and never rolled). An un-rolled date is never LATER than the true deadline, so it can be early but cannot cause a missed one. `null` exactly when no date was produced. Same vocabulary as the maintenance `due_date_adjustment`, minus `month_end_overflow`, which is a Mexican maintenance rule with no prosecution counterpart. Where the unextended deadline came from: `rule_period`, `stated_deadline`, or `rule_absolute_cap`. A stated date the engine refused for exceeding the rule absolute cap, kept as structured provenance rather than prose. The fact that retired this deadline. `response`, `abandonment`, or `sou`. For `outcome: "stated"`, the fact that carried the office stated date. The declared calculation basis (Madrid Rule 17(2)(viii)). `explicit`, `inferred`, or `ambiguous`. `true` when the anchor used a low confidence date basis. Non null exactly when the outcome produced no date. Machine readable notes about the resolution. The rule that produced the date. Non null for every computed or closed deadline: a computed date that cannot cite its rule does not ship. `null` for `stated` outcomes, which cite `stated_by_fact_id` and `stated_basis` instead, and for declines made before a rule was selected. Stable slug, for example `uspto_domestic_oa_response@2022-12-03`. `statute`, `regulation`, `office_practice`, or `office_notice`. Some shipped values exist only in practice material: US month arithmetic is TMEP 310 only, and the CFR and USC are silent on months. When the regime began. Inclusive last day the regime was in force, or `null` while still in force. Which date on the case selected this era. Not the same as `effective_from`. When the rule was last verified against source. Each carries `citation`, `url`, `section`, `quoted_text`, `snapshot_sha256`, `authority_level`, and `last_checked`. Always `false`. Always `{ "cursor": null }`. Unique request identifier for support and debugging. Terminal-dead statuses (`abandoned`, `withdrawn`, `surrendered`, `invalidated`, and `refused`, except the WIPO international registration record itself under a provisional refusal) return `supported: true` with an empty `deadlines` array. `expired` and `cancelled` marks return only their still-actionable rows: open late-renewal windows for `expired`, and restoration windows where the jurisdiction models them (GB, SG, IS, JP, IN). ## Rule provenance Every computed row names the rule that produced it, so a date is never an unattributable number. * **`rule_id`** is stable and permanent. Once published, a slug is never renamed or repointed at different content, so it is safe to persist alongside your own records and to join against the rule catalog. It is **opaque**: `us_renewal_s9` and `eu_renewal` look parseable, but the grammar is not part of the contract. Compare it for equality, nothing else. * **`effective_from`** answers "from when is this modelling authoritative?", not "when did the statute change". It is `null` for most rules, and non-null only where the corpus establishes a genuine boundary, for example a declaration-of-use requirement introduced on a known date, or a cohort the engine deliberately declines to compute before (Australian direct marks before 1996-01-01, Singapore direct marks before 1999-01-15). `effective_from` is a statement about the **rule**, and the date it should be compared against is **rule-specific**. Do not apply it as a blanket `trigger_date < effective_from` test: that flags correct rows as unreliable. The comparison date is the row's `trigger_date` for the statutory-commencement and computability-cohort rules (`mx_declaration_of_use_3yr`, `au_renewal`, `sg_renewal`) — and the engine already gates on it, so a row that exists at all is normally inside range. For `no_renewal` it is the **renewal expiry being reconstructed**, that is the row's own `due_date`, because the boundary bounds the modelled renewal *window* rather than the mark's trigger date: a Norwegian mark filed in 2018 with a 2028 renewal is entirely inside the modelled regime even though its `trigger_date` precedes 2024-03-02. That boundary is exact. It is the transitional cutover in FOR-2023-02-17-230, under which the previous 12-month window still governed registrations expiring before 2024-03-02, rather than the 2023-03-01 commencement of the amending statute — so a row whose `due_date` is on or after `effective_from` reports the window that actually applied, and one before it does not. For opposition rules (on [Compute Oppositions](/api-reference/reference/compute-oppositions)) it is the publication date the window runs from. If you cannot make the rule-specific comparison, show `effective_from` as provenance rather than deriving a reliability flag from it. * **`last_verified`** is when Signa last checked the rule against its sources. It moves on verification passes even when nothing about the rule changed. * **`sources`** carries the statutory citations for the jurisdiction configuration behind the item, once per item rather than once per rule or once per row. ## Business-day adjustment Some offices roll a deadline that lands on a weekend or public holiday forward to the next business day. Where Signa models that, the computed date is already rolled, and the row reports what happened: | Value | Meaning | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `moved` | A calendar was consulted and the roll changed this date. | | `unchanged` | A calendar was consulted and this date was already a business day. | | `not_checked` | No adjustment was applied. Either the jurisdiction does not opt into business-day adjustment, no calendar is wired for it, or the date falls outside the wired calendar's coverage window (calendars cover a finite range of years, so far-future dates fall off the end). | | `month_end_overflow` | The period is counted in months or years and its end day number does not exist in the target month, so it ran into the **following** calendar month instead of clamping to the short month's last day. Mexico only (Reglamento LPI art. 4o, párrafo segundo). It subsumes the roll: the same paragraph names the first *business* day of that month, so where a date carries this value the business-day roll has already been applied to it and is not separately reported. | `due_date` and `grace_expiry` are evaluated separately, so one can be `moved` while the other is `not_checked` on the same row. `holiday_calendar` describes **configuration**, not what happened to a given date. Where a calendar is wired it stays non-null on every row for that office, including rows whose dates report `not_checked`, so `holiday_calendar: "ipos_singapore"` next to `grace_expiry_adjustment: "not_checked"` reads correctly as "a calendar is configured, but it does not cover that date". `holiday_calendar: null` means no calendar is configured at all, and every adjustment on that row is therefore `not_checked`. `window_opens` is never business-day adjusted in its own right. It is the first date a filing is accepted, not a deadline, so it is reported as the statute states it. One inherited exception: a restoration row's `window_opens` is the parent renewal's grace expiry, which has already been rolled where that jurisdiction opts into business-day adjustment (Singapore and Japan today; Great Britain, Iceland and India do not opt in, so their restoration windows open on an unrolled date). ```json theme={null} { "object": "list", "data": [ { "object": "deadline_computation", "jurisdiction_code": "US", "filing_route": "national", "supported": true, "unsupported_reason": null, "deadlines": [ { "rule_id": "us_declaration_of_use_s8", "effective_from": null, "last_verified": "2026-03-10", "type": "declaration_of_use", "name": "Section 8 — Declaration of Use", "jurisdiction_code": "US", "trigger_date": "2020-03-10", "trigger_field": "registration_date", "window_opens": "2025-03-10", "due_date": "2026-03-10", "grace_expiry": "2026-09-10", "status": "window_open", "days_until_due": 248, "days_until_grace_expiry": 432, "urgency": "routine", "consequence_if_missed": "cancellation", "recurring": false, "optional": false, "due_date_adjustment": "unchanged", "grace_expiry_adjustment": "unchanged", "holiday_calendar": "uspto_dc" }, { "rule_id": "us_declaration_of_incontestability_s15", "effective_from": null, "last_verified": "2026-03-10", "type": "declaration_of_incontestability", "name": "Section 15 — Declaration of Incontestability", "jurisdiction_code": "US", "trigger_date": "2020-03-10", "trigger_field": "registration_date", "window_opens": "2025-03-10", "due_date": "2026-03-10", "grace_expiry": "2026-03-10", "status": "window_open", "days_until_due": 248, "days_until_grace_expiry": 248, "urgency": "routine", "consequence_if_missed": "none", "recurring": false, "optional": true, "due_date_adjustment": "unchanged", "grace_expiry_adjustment": "unchanged", "holiday_calendar": "uspto_dc" }, { "rule_id": "us_renewal_s9", "effective_from": null, "last_verified": "2026-03-10", "type": "combined_renewal_and_use", "name": "Section 8+9 — Renewal + Declaration of Use", "jurisdiction_code": "US", "trigger_date": "2020-03-10", "trigger_field": "registration_date", "window_opens": "2029-03-10", "due_date": "2030-03-11", "grace_expiry": "2030-09-10", "status": "future", "days_until_due": 1710, "days_until_grace_expiry": 1893, "urgency": "routine", "consequence_if_missed": "cancellation_and_expiration", "recurring": true, "optional": false, "due_date_adjustment": "moved", "grace_expiry_adjustment": "unchanged", "holiday_calendar": "uspto_dc" } ], "sources": [ { "citation": "USPTO — Keeping Your Registration Alive", "url": "https://www.uspto.gov/trademarks/maintain/keeping-your-registration-alive" }, { "citation": "15 U.S.C. § 1058(a) — registration term and affidavit requirement", "url": "https://www.govinfo.gov/content/pkg/USCODE-2024-title15/html/USCODE-2024-title15-chap22-subchapI-sec1058.htm" }, { "citation": "15 U.S.C. § 1059(a) — renewal and grace period", "url": "https://www.govinfo.gov/content/pkg/USCODE-2024-title15/html/USCODE-2024-title15-chap22-subchapI-sec1059.htm" }, { "citation": "15 U.S.C. § 1065(3) — incontestability filing period", "url": "https://www.govinfo.gov/content/pkg/USCODE-2024-title15/html/USCODE-2024-title15-chap22-subchapI-sec1065.htm" }, { "citation": "37 CFR § 2.160(a)(3) — Section 8 grace period", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "37 CFR § 2.164(b) — cancellation after missed Section 8 period", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "TMEP § 1604.04 — Time for Filing §8 Declaration", "url": "https://tmep.uspto.gov/RDMS/TMEP/print?href=TMEP-1600d1e1.html&version=current" }, { "citation": "37 CFR § 2.167(f) — incontestability filing period", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "37 CFR § 2.182 — renewal filing window", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "37 CFR § 2.185(b) — expiration after missed renewal period", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "37 CFR § 2.196 — weekend and federal-holiday rollover", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "TMEP § 1606.03 — renewal filing window", "url": "https://tmep.uspto.gov/RDMS/TMEP/print?href=TMEP-1600d1e1.html&version=current" } ], "as_of": "2025-07-05", "computed_at": "2025-07-05T12:34:56.789Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_bT9kM3nP" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/deadlines/compute" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "jurisdiction": "US", "filing_date": "2019-01-15", "registration_date": "2020-03-10" } ], "as_of_date": "2025-07-05" }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const results = await signa.deadlines.compute({ items: [{ jurisdiction: 'US', filing_date: '2019-01-15', registration_date: '2020-03-10' }], as_of_date: '2025-07-05' }); for (const computation of results.data) { if (computation.object === 'deadline_computation') { console.log(computation.jurisdiction_code, computation.deadlines.length); } } ``` ### Computing an office action response deadline ```bash cURL theme={null} curl "https://api.signa.so/v1/deadlines/compute" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "as_of_date": "2024-03-01", "items": [ { "jurisdiction": "US", "filing_route": "national", "status": "examining", "filing_date": "2023-01-15", "facts": [ { "kind": "office_action", "id": "oa-1", "family": "nonfinal", "operative_date": "2024-02-01", "operative_date_basis": "mailed_row" } ] } ] }' ``` ```typescript TypeScript theme={null} const results = await signa.deadlines.compute({ as_of_date: '2024-03-01', items: [{ jurisdiction: 'US', filing_route: 'national', status: 'examining', filing_date: '2023-01-15', facts: [{ kind: 'office_action', id: 'oa-1', family: 'nonfinal', operative_date: '2024-02-01', operative_date_basis: 'mailed_row', }], }], }); const [computation] = results.data; for (const deadline of computation.prosecution_deadlines ?? []) { console.log(deadline.effective_due_date, deadline.rule?.rule_id); } ``` Response fragment: ```json theme={null} { "object": "prosecution_deadline", "outcome": "computed", "deadline_type": "oa_response", "trigger_fact_id": "oa-1", "trigger_date": "2024-02-01", "trigger_date_basis": "mailed_row", "due_date": "2024-05-01", "extended_due_date": null, "effective_due_date": "2024-05-01", "due_date_adjustment": "unchanged", "base_source": "rule_period", "reason": null, "rule": { "rule_id": "uspto_domestic_oa_response@2022-12-03", "authority_level": "office_practice", "effective_from": "2022-12-03", "effective_until": null, "applicability_basis": "office_action_issue_date", "last_verified": "2026-08-31", "sources": [ { "citation": "37 CFR 2.62", "url": "https://www.ecfr.gov/current/title-37/section-2.62", "section": "37 CFR 2.62(a), (a)(1)-(2)", "quoted_text": "(a) Deadline. Each Office action shall set forth the deadline for response. (1) Response periods. Unless the applicant is notified otherwise in an Office action, the response periods for an Office action are as follows: (i) Three months from the issue date, for an Office action in an application under section 1 and/or section 44 of the Act; …", "snapshot_sha256": "7cf3d3eb3df61779b85328dd62ef86563d5fffcc31c57f432b9482473276eed7", "authority_level": "regulation", "last_checked": "2026-08-31" } ] } } ``` The fragment above abbreviates two fields for length. `rule.sources` carries **six** entries on this rule (37 CFR 2.62, 37 CFR 2.6, TMEP 310, 37 CFR 2.196, 37 CFR 2.66, and 37 CFR 2.6 again for the petition-to-revive fee), and each `quoted_text` is the full statutory extract, truncated here at the trailing `…`. The row itself also carries `linkage_quality` (`"explicit"` here), `low_confidence` (`false`), `notes` (`[]`) and the fields that are null on this example: `rejected_stated_due_date`, `closed_by_fact_id`, `closed_by`, `stated_by_fact_id` and `stated_basis`. ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Malformed body, missing `items`, invalid date format, invalid enum value, or `horizon_years` outside `1..15` | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 400 | `validation_error` | More than 100 `facts` on one item, a duplicate fact `id` within an item, or a `stated_deadline` fact that names neither a date nor a period | | 422 | `batch_too_large` | More than 1,000 items, or more than 1,000 `facts` across the batch. The error carries `item_count` and `item_count_limit` | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [List Deadline Rules](/api-reference/reference/deadline-rules), inspect the rule corpus and statutory citations * [List Jurisdictions](/api-reference/reference/list-jurisdictions), jurisdiction overview # Compute Oppositions Source: https://docs.signa.so/api-reference/reference/compute-oppositions POST /v1/oppositions/compute Compute trademark opposition windows from publication dates ## Overview Compute the opposition window for a published mark (when it opens, when it closes, and how much time is left) from a publication date, without storing anything. Opposition windows are short, unforgiving, and specific to each office. When a conflicting mark publishes, the oppose-by date is what separates a cheap, procedural opposition now from a costly cancellation later. Send a publication date and Signa returns the window, its current status, any routine extensions, and the office rule behind it. Pair it with [List Opposition Rules](/api-reference/reference/opposition-rules) to inspect the office rules, trigger events, time zones, extensions, and citations behind each computation. This endpoint computes the forward-looking opposition window (dates and timing) from a publication date. It does not return opposition cases that have actually been filed. For filed opposition, cancellation, and appeal cases, use [Search Proceedings](/api-reference/proceedings/search-proceedings). ## Request Body Publications to compute, max 1,000 items. Results are returned in the same order, with one `opposition_window` per input item. Uppercase ST.3 trademark office code, e.g. `US`, `EM`, or `CA`. Legacy lowercase codes (e.g. `uspto`, and `eu` for EUIPO) are accepted as permanent aliases. Filing route. One of `national`, `regional`, or `madrid`. Defaults to `national`. The response reports the office rule discriminator as `domestic` or `madrid_designation`. Publication calendar date as `YYYY-MM-DD`. Omit when the publication date is unknown; the response status will be `unknown`. Application filing calendar date as `YYYY-MM-DD`. Read only by rules whose era pivot is keyed on the filing date rather than the publication date (Türkiye: an application filed before 2017-01-10 keeps the former 3-month window even when published later). Omit it and such a rule falls back to the publication date, which yields the shorter modern window; every other rule ignores it. Which publication `publication_date` is. One of `office_journal`, `wipo_gazette`, `office_register`, or `unknown`. A Madrid designation has two publications that can fill one date field: WIPO's Gazette of International Marks, and the designated office's own journal republication. They are routinely months apart, and offices run the opposition clock from different ones. FR, CH and DE run from the WIPO Gazette; AU, CA, EU, FI, NO, PL, SG and US run from their own journal. Feed a rule the other office's date and you get a plausible date that is not the statutory bar. Supply this field and a rule that needs the other publication returns `supported: true` with `status: "unknown"`, null window fields and `reason: "publication_date_kind_mismatch"`, keeping `rule_id`, `office_code`, `trigger_event`, `rule_version` and `source` so you can see exactly which rule declined and why. `unknown` is **not** the same as omitting the field. It is a positive statement that you looked and the source did not establish which publication the date is, and on an adjudicated Madrid rule that unestablished provenance is refused for the same reason a mismatch is — same `status: "unknown"`, same `reason`. There is no third outcome where an `unknown` provenance yields dates. Omitting the field is the only value-free option: the window is computed exactly as before, because no claim about provenance was made. Ignored on the `national` and `regional` filing routes, where there is no second candidate publication. See [Opposition windows](/guides/monitoring/opposition-windows#madrid-designations-which-publication-starts-the-clock) for the per-office table. Office-local calendar date to compute relative to, as `YYYY-MM-DD`. Defaults to today. ## Response A standard list response with `data: OppositionWindowComputation[]`. Pagination is not used; `data[i]` corresponds to `items[i]`. Always `list`. Always `opposition_window`. `true` when an opposition rule was found for the office and filing route and applied. That is a statement about rule resolution, not about dates: omit `publication_date` and the item is still `supported: true`, with `status: "unknown"` and null window fields, because there is no trigger to compute from. `false` when Signa has no opposition rule for the office and route, when a rule exists but does not reach the publication date, or when the window could not be computed. `unsupported_office`, `window_not_computable`, or `null` on supported items. `unsupported_office` covers two cases: no rule exists for the office and route, and a rule exists for the office and route but does not reach this publication date. `rule_id` tells them apart, and `effective_from` names the boundary in the second case. Stable, permanent slug of the opposition rule used for this item, e.g. `eu_opposition`. Use it as the join key to [List Opposition Rules](/api-reference/reference/opposition-rules). Treat it as **opaque**: do not parse it or infer an office from it. It is deliberately not a prefixed entity ID (`tm_`, `own_`) because it names a statutory rule, not a stored entity. Populated whenever a rule was resolved for the office and route, including on `supported: false` items whose window could not be computed. `null` only when no rule exists for that office and route. Earliest date from which the rule's modelled content is authoritative, as `YYYY-MM-DD`. `null` when the source corpus does not establish one, which is the common case. Reported alongside `rule_id`, including on unsupported items: a publication date earlier than `effective_from` is outside the rule's reach and returns `unsupported_reason: "unsupported_office"` with `effective_from` naming the boundary. Era pivots inside a rule (content that varies by publication date) are modelled internally and are not reported here. Jurisdiction for the office rule, e.g. `EU`. Uppercase ST.3 office code used for the computation, e.g. `EM`. Rule discriminator: `domestic` or `madrid_designation`. Publication date used as the trigger. First day the opposition window is open. Last day to oppose, after office-calendar rollover. Read it together with `close_adjustment`, which says whether a calendar rollover was actually applied. Whether `window_closes` was checked against the office's holiday calendar, and whether that check moved it. `moved`: the close fell on a day the office is shut and was rolled forward to the next business day. `unchanged`: the calendar was consulted and the arithmetic close was already a business day. `not_checked`: **no calendar check stands behind this date**. The close falls outside the calendar's pinned coverage years (a per-notice calendar such as China's or India's advertises only the years whose annual notice is published), the rule ships without a calendar, or there is no publication date. A rollover can only move a close **later**, never earlier, so a `not_checked` date is correct-or-early: safe to act on, but not a decided answer. If you derive open/closed from the dates yourself, treat `not_checked` as indeterminate for the 20 days after `window_closes`. `null` only when no window was computed. One of `not_started`, `open`, `closed`, or `unknown`. Computed against `as_of_date`; where `close_adjustment` is `not_checked` it is derived from the un-rolled close, so a window Signa reports `closed` within 20 days of that date may in fact still be open. Days until `window_opens` when the status is `not_started`. Days remaining in the window when known. IANA office time zone for display. `application_publication`, `registration_publication`, or `international_designation_publication`. Date Signa last verified this rule against its sources, as `YYYY-MM-DD`. Same value as `last_verified` on [List Opposition Rules](/api-reference/reference/opposition-rules); it moves on a verification pass even when nothing about the rule changed. `null` on unsupported items. Why a resolved rule declined to compute dates, as a snake\_case code. `null` on every normal window, and on unsupported items (`unsupported_reason` carries those). One value is emitted today: `publication_date_kind_mismatch` — the `publication_date_kind` you sent is not the publication the matched Madrid rule runs from, including the explicit `unknown`. A missing `publication_date` stays reason-less: `status: "unknown"` with a null `publication_date` already says that. Treat the vocabulary as open: branch on the codes you know and fall through on the rest. Extension length in days. Extension length in months. Whether the extension is granted automatically on timely request. Human-readable note for UI surfacing. Statutory or office citation. Authoritative URL for the citation. Always `false`. Always `{ "cursor": null }`. Unique request identifier for support and debugging. Every item carries the same key set, supported or not: unsupported items null the window fields rather than omitting them. `rule_id` and `effective_from` are the exception. They report the rule Signa resolved for the office and filing route, and that resolution either succeeded or it did not, independent of whether a window came out of it. So an unsupported item still carries them whenever a rule was found, which keeps it joinable to [List Opposition Rules](/api-reference/reference/opposition-rules). They are `null` only when no rule exists for that office and route. Three cases end in `supported: false`: * **No rule for the office or route.** `unsupported_reason: "unsupported_office"`, `rule_id: null`, `effective_from: null`. * **A rule exists but does not reach the publication date.** Also `unsupported_reason: "unsupported_office"`, but `rule_id` names the rule and `effective_from` names the date it starts from. A Vietnamese publication from 2022 reports `rule_id: "vn_opposition"` with `effective_from: "2023-01-01"`, because opposition under art. 112a only applies from that date. Recompute with a publication date on or after `effective_from` and the same rule returns a window. * **The window could not be computed.** This is now a NARROW case: a close date past the office calendar's covered years that falls in the 20 days **before** the date you are computing as of, where a rollover could still be holding the window open and guessing either way would be wrong. `unsupported_reason: "window_not_computable"`, affecting that item only. `rule_id` and `effective_from` are populated; every other window field, including `rule_version`, is `null`. Every OTHER out-of-coverage close, meaning one in the future, one falling exactly on the as-of date, and one more than 20 days before it, is **published un-rolled** with `close_adjustment: "not_checked"` rather than declined. Silence on a rights-losing deadline is worse than an early date, and an un-rolled close is never later than the true one. This is what a calendar built from annual office notices (China, India) does for every window closing in a year whose notice is not yet published. Two more shapes are *not* unsupported items, and both come back `supported: true` with `unsupported_reason: null`, `status: "unknown"`, and `window_opens`, `window_closes`, `days_until_open` and `days_remaining` all `null`. `rule_id`, `effective_from`, `rule_version`, `source`, `trigger_event` and the office fields are populated as usual in each. `reason` is what tells them apart. * **No publication date.** Omit `publication_date` and the rule still resolves and applies; there is simply no trigger to compute from. `reason` stays `null` — the null `publication_date` already says it. * **The wrong publication.** Send a `publication_date_kind` that is not the one the matched Madrid rule runs from — or the explicit `unknown` — and the rule declines rather than dating the window from a publication it does not recognise. `reason` is `publication_date_kind_mismatch`. Do not read `supported: true` as "there are dates": check `status` or the window fields. ```json theme={null} { "object": "list", "data": [ { "object": "opposition_window", "supported": true, "unsupported_reason": null, "rule_id": "eu_opposition", "effective_from": null, "jurisdiction_code": "EU", "office_code": "EM", "filing_route": "domestic", "publication_date": "2026-05-01", "window_opens": "2026-05-02", "window_closes": "2026-08-03", "status": "open", "days_until_open": null, "days_remaining": 29, "office_time_zone": "Europe/Madrid", "trigger_event": "application_publication", "rule_version": "2026-05-06", "common_extension": null, "source": { "citation": "Article 46(1) EUTMR (Regulation (EU) 2017/1001)", "url": "https://eur-lex.europa.eu/eli/reg/2017/1001/oj/eng" }, "reason": null } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_bT9kM3nP" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/oppositions/compute" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "office": "EM", "publication_date": "2026-05-01" } ], "as_of_date": "2026-07-05" }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const windows = await signa.oppositions.compute({ items: [{ office: "EM", publication_date: '2026-05-01' }] }); for (const window of windows.data) { console.log(window.office_code, window.status, window.window_closes); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Malformed body, missing `items`, invalid date format, invalid enum value, or unknown body field | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 422 | `batch_too_large` | More than 1,000 items. The error carries `item_count` and `item_count_limit` | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [List Opposition Rules](/api-reference/reference/opposition-rules), inspect the office rule corpus and statutory citations * [Search Proceedings](/api-reference/proceedings/search-proceedings), filed opposition, cancellation, and appeal cases * [Trademark Proceedings](/api-reference/trademarks/trademark-proceedings), proceedings scoped to one mark # List Deadline Rules Source: https://docs.signa.so/api-reference/reference/deadline-rules GET /v1/deadline-rules List all maintenance deadline rules across modeled jurisdictions ## Overview Returns a flat list of maintenance deadline rules (renewal cycles, declarations of use, incontestability filings, and similar) across every modeled jurisdiction. Each rule is self-contained: it carries its parent jurisdiction's `renewal_period_years` and statutory `sources`, so you do not need a separate parent fetch to render a rule. This endpoint exposes the same rule corpus that powers [Compute Deadlines](/api-reference/reference/compute-deadlines) and the `derived.deadlines` block on a trademark detail response. Use it to build educational UI, validate deadline computations client-side, or show statutory citations alongside an alert. Every rule carries a `rule_id`, and every computed deadline row reports the `rule_id` that produced it, so any date Signa returns can be resolved back to the rule and citations behind it. Two related vocabularies appear on this endpoint, and they are distinct: * **Filing routes** (the `filing_route` query parameter): `national`, `regional`, `madrid`. This is how a mark was filed. * **Rule scope tags** (the `applies_to` response field): `domestic`, `madrid_only`, `all`, or `null`. This is which filings a rule covers. `domestic` covers the `national` and `regional` routes, `madrid_only` covers the `madrid` route, and `all` or `null` covers every route. ## Query Parameters Filter by jurisdiction code (uppercase ISO-2 plus `WIPO`). Comma-separated for multiple values, e.g. `?jurisdiction=US,EU,GB`. Filter by filing route. Accepted values: `national`, `regional`, `madrid`. Comma-separated for multiple values. A rule passes the filter when its `applies_to` scope covers the requested route: `domestic` rules pass for `national` or `regional`, `madrid_only` rules pass for `madrid`, and `all` or `null` rules pass for any value. Multiple values union the matching sets. ## Response A standard list response with `data: DeadlineRule[]`. Pagination is not used: the corpus is small and returned in a single page, in a fixed, stable order. This endpoint is publicly cacheable, so the response never includes `request_id`. The list is one flat object type. Maintenance rules and restoration rules are both `object: "deadline_rule"` and are told apart by `type`, so a client can render the whole catalog with one code path. Always `list` Always `deadline_rule` Stable, permanent slug identifying this rule, e.g. `us_declaration_of_use_s8`. Once published a slug is never renamed or repointed, so it is safe to persist and to join against `rule_id` on [Compute Deadlines](/api-reference/reference/compute-deadlines) rows, trademark `derived.deadlines[]` rows, and portfolio deadline rows. Treat it as **opaque**: the slug looks parseable but its grammar is not part of the contract, so compare it for equality and nothing else. It is deliberately not a prefixed entity ID (`tm_`, `own_`) because it names a statutory rule, not a stored entity. Earliest date from which this rule's modelled content is authoritative, as `YYYY-MM-DD`. `null` when the source corpus does not establish one, which is the common case. It is not a change log: rules whose content varies by era are modelled internally and still report `null`. Date Signa last checked this rule against its statutory sources, as `YYYY-MM-DD`. Carried from the parent configuration, so every rule belonging to the same configuration shares it. A jurisdiction with route-specific configurations can therefore report different values per route — US domestic rules report `2026-03-10` while US Madrid rules report `2026-07-11`. It moves on a verification pass even when nothing about the rule changed. Whether this rule's dates roll forward to the next working day when they land on a weekend or public holiday. Reported per rule: for a jurisdiction's national rules it is the jurisdiction's own opt-in; for a `madrid_only` renewal (`mx_renewal_madrid`, `ca_renewal_madrid`) it is always `true`, because that period is the International Bureau's and rolls under Madrid Regulations Rule 4(4) whatever the designated office does with its own dates. Identifier of the holiday calendar this rule's dates roll against, e.g. `uspto_dc` or `ipos_singapore`. Reported per rule: the office's wired calendar for its national rules, and `wipo_geneva` for a `madrid_only` renewal. `null` when the jurisdiction does not opt into business-day adjustment or no calendar is wired for it. This is a calendar identifier, not an office code. Whether the roll reported by `weekend_holiday_rule` is limited to the grace expiry. When `true`, the renewal **due date** is served exactly where the statutory arithmetic put it and is never moved to the next working day, so it can land on a Saturday or a public holiday. That is deliberate: in those jurisdictions a late-filing surcharge keys on the calendar end of the registration period rather than on a deadline, so moving the due date forward would advertise a surcharge-free filing day after the term had already ended. `true` for AU (Trade Marks Act 1995 Schedule 9 item 10) and NO (varemerkeloven § 33 første ledd); `false` everywhere else, including jurisdictions that do not roll at all. Jurisdiction this rule belongs to (uppercase ISO-2 or `WIPO`) Human-readable rule name Deadline type code: `renewal`, `declaration_of_use`, `combined_renewal_and_use`, `declaration_of_incontestability`, `restoration`, or `international_renewal` Date field this rule is computed from: `registration_date`, `filing_date`, `grant_date`, `protection_grant_date`, or `intl_registration_date` Years from trigger date when the deadline falls. On a `restoration` rule this instead names the **renewal cycle the restoration rescues**, not a restoration due date; the restoration window itself is `window_months` long, anchored as described under [Restoration rules](#restoration-rules). Year when the filing window opens (`null` if the window opens by months instead) Months before the due date when the filing window opens (`null` if the window opens by years) Grace period after the due date (may incur late fees) What happens if the deadline plus grace passes. One of `expiration` (the right ends by operation of law when the term plus any grace runs out), `removal_from_register` (the office strikes the entry as the statutory consequence), `cancellation` (the office cancels a still-running registration for a missed maintenance filing, US §8/§71), `cancellation_and_expiration` (combined US §8+§9), `expungement` (Canada's statutory word), `lapse_of_international_registration` (Madrid), `permanent_loss` (restoration rules: the last remedy has passed) or `none` (optional filing). Whether a remedy exists afterwards is said by the presence of a `restoration` rule, not by this value. See the [deadline rules guide](/guides/deadline-rules#consequence-vocabulary). Whether this deadline repeats on each renewal cycle Years between recurrences (`null` if not recurring) Whether filing is optional (e.g. US Section 15 incontestability) Rule scope tag: `domestic` (national and regional routes), `madrid_only` (madrid route), `all` or `null` (every route) Renewal cycle length for the parent jurisdiction Length of the restoration window in months. Non-null only on `type: "restoration"` rules; `null` on every maintenance rule. Statutory anchor the restoration window runs from: `grace_expiry` or `removal_date`. Non-null only on `type: "restoration"` rules. It records the statute, not the arithmetic Signa performs. Computed restoration rows always anchor `window_opens` on the renewal grace expiry, so `removal_date` rules are a conservative approximation. See [Restoration rules](#restoration-rules) below. Statutory or office citation Authoritative URL for the citation Always `false`, no pagination Standard pagination envelope (`cursor: null`) ```json theme={null} { "object": "list", "data": [ { "object": "deadline_rule", "rule_id": "us_declaration_of_use_s8", "effective_from": null, "last_verified": "2026-03-10", "weekend_holiday_rule": true, "holiday_calendar": "uspto_dc", "grace_expiry_rolls_only": false, "jurisdiction_code": "US", "name": "Section 8 — Declaration of Use", "type": "declaration_of_use", "trigger": "registration_date", "due_year": 6, "earliest_filing_year": 5, "earliest_filing_months_before": null, "grace_period_months": 6, "consequence_if_missed": "cancellation", "recurring": false, "recurring_interval_years": null, "optional": false, "applies_to": "domestic", "renewal_period_years": 10, "window_months": null, "starts_from": null, "sources": [ { "citation": "USPTO — Keeping Your Registration Alive", "url": "https://www.uspto.gov/trademarks/maintain/keeping-your-registration-alive" }, { "citation": "15 U.S.C. § 1058(a) — registration term and affidavit requirement", "url": "https://www.govinfo.gov/content/pkg/USCODE-2024-title15/html/USCODE-2024-title15-chap22-subchapI-sec1058.htm" }, { "citation": "15 U.S.C. § 1059(a) — renewal and grace period", "url": "https://www.govinfo.gov/content/pkg/USCODE-2024-title15/html/USCODE-2024-title15-chap22-subchapI-sec1059.htm" }, { "citation": "15 U.S.C. § 1065(3) — incontestability filing period", "url": "https://www.govinfo.gov/content/pkg/USCODE-2024-title15/html/USCODE-2024-title15-chap22-subchapI-sec1065.htm" }, { "citation": "37 CFR § 2.160(a)(3) — Section 8 grace period", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "37 CFR § 2.164(b) — cancellation after missed Section 8 period", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "TMEP § 1604.04 — Time for Filing §8 Declaration", "url": "https://tmep.uspto.gov/RDMS/TMEP/print?href=TMEP-1600d1e1.html&version=current" }, { "citation": "37 CFR § 2.167(f) — incontestability filing period", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "37 CFR § 2.182 — renewal filing window", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "37 CFR § 2.185(b) — expiration after missed renewal period", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "37 CFR § 2.196 — weekend and federal-holiday rollover", "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-08-28/title-37.xml?part=2" }, { "citation": "TMEP § 1606.03 — renewal filing window", "url": "https://tmep.uspto.gov/RDMS/TMEP/print?href=TMEP-1600d1e1.html&version=current" } ] } ], "has_more": false, "pagination": { "cursor": null } } ``` ## Restoration rules Five jurisdictions (GB, SG, IS, JP, IN) model a restoration window: a last-resort filing that can revive a registration after the renewal grace period has already expired. Those rules are part of this catalog, as `type: "restoration"` rows, so every `rule_id` the compute engine can emit is resolvable here. A restoration row is the same flat `deadline_rule` object as any other, with three differences worth reading carefully: * **`due_year` names the renewal cycle the restoration rescues**, not a restoration due date. The restoration window itself is `window_months` months long, anchored as described below. * **`starts_from` records the statutory anchor, not the anchor Signa computes from.** The statute names either the end of the late-renewal period (`grace_expiry`, Iceland, Japan and India) or the date the registration was removed from the register (`removal_date`, UK and Singapore). Removal is an office act that happens after grace expiry and is **not carried on a trademark record**, so computed restoration rows on [Compute Deadlines](/api-reference/reference/compute-deadlines) and in `derived.deadlines[]` always anchor `window_opens` on the renewal's grace expiry. That is exact for `grace_expiry` rules (IS, JP, IN) and a deliberately conservative approximation for `removal_date` rules (GB, SG), matching UKIPO's published practice of removing six months after the renewal date. Do not compute `removal_date` + `window_months` from your own removal data and expect it to match: it lands later than Signa's `due_date` by the removal lag, and Signa can under-report at the close of the window by that same lag. * **`grace_period_months` is always `0`.** Restoration has no grace period of its own; it *is* the grace of last resort. The maintenance-only fields are still present and truthful on these rows (`earliest_filing_year` and `earliest_filing_months_before` are `null`, `recurring` is `false`, `optional` is `false`, `applies_to` is `null`), so no field is silently omitted. Because `applies_to` is `null`, restoration rows match **every** `filing_route` filter value, exactly like any other unscoped rule. They are filtered normally by `jurisdiction`. ```json theme={null} { "object": "deadline_rule", "rule_id": "gb_restoration", "effective_from": null, "last_verified": "2026-03-10", "weekend_holiday_rule": false, "holiday_calendar": null, "grace_expiry_rolls_only": false, "jurisdiction_code": "GB", "name": "Restoration — GB", "type": "restoration", "trigger": "filing_date", "due_year": 10, "earliest_filing_year": null, "earliest_filing_months_before": null, "grace_period_months": 0, "consequence_if_missed": "permanent_loss", "recurring": false, "recurring_interval_years": null, "optional": false, "applies_to": null, "renewal_period_years": 10, "window_months": 6, "starts_from": "removal_date", "sources": [ { "citation": "Trade Marks Act 1994 s.40(3) — filing date deemed registration date", "url": "https://www.legislation.gov.uk/ukpga/1994/26/data.xml" }, { "citation": "Trade Marks Act 1994 s.43(3) — renewal and late-renewal periods", "url": "https://www.legislation.gov.uk/ukpga/1994/26/data.xml" }, { "citation": "Trade Marks Rules 2008 r.35 — renewal filing window", "url": "https://www.legislation.gov.uk/uksi/2008/1797/data.xml" }, { "citation": "Trade Marks Rules 2008 r.36(2) — delayed renewal without removal", "url": "https://www.legislation.gov.uk/uksi/2008/1797/data.xml" }, { "citation": "Trade Marks Rules 2008 r.37(1) — restoration from removal date", "url": "https://www.legislation.gov.uk/uksi/2008/1797/data.xml" }, { "citation": "UKIPO Manual — restoration up to one year after renewal date", "url": "https://www.gov.uk/guidance/trade-marks-manual/register-maintenance" }, { "citation": "GOV.UK — renew your trade mark", "url": "https://www.gov.uk/renew-your-trade-mark" } ] } ``` ## Ordering The catalog is returned in a fixed order: jurisdiction configurations in registry order, each config's maintenance rules in rule order, and that config's restoration row (when it has one) immediately after its maintenance rules. The order is stable across deploys and cache fills, so you can rely on it, but `rule_id` is the identity to key on, not the position. ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/deadline-rules?jurisdiction=US" \ -H "Authorization: Bearer sig_YOUR_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" }); for (const rule of rules.data) { console.log(rule.name, rule.due_year, rule.consequence_if_missed); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------------- | | 400 | `validation_error` | Invalid `filing_route` value | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Compute Deadlines](/api-reference/reference/compute-deadlines), compute per-mark maintenance deadlines from these rules * [List Opposition Rules](/api-reference/reference/opposition-rules), opposition window rules * [List Jurisdictions](/api-reference/reference/list-jurisdictions), jurisdiction overview # List Classifications Source: https://docs.signa.so/api-reference/reference/list-classifications GET /v1/classifications List all Nice classification classes with descriptions ## Overview Returns the complete Nice Classification system used to categorize goods and services in trademark applications. Each class includes its number, title, and description. The Nice Classification is maintained by WIPO and updated annually; Signa always reflects the current edition. This reference endpoint is useful for building class pickers, validating user input, and displaying human-readable class descriptions alongside trademark data. ## Query Parameters Filter by classification type: `goods` (classes 1-34) or `services` (classes 35-45). Search classification titles, descriptions, and terms (case-insensitive contains, max 200 characters). Max results per page (max 500). The default returns all 45 classes in one page. Pagination cursor from `pagination.cursor` of the previous page. ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. Always `list`. Nice class number (1-45) Always `classification` Official Nice class heading, the full title string published by WIPO Full Nice class scope description `goods` (classes 1-34) or `services` (classes 35-45) Whether more pages are available. Cursor for the next page, or `null` on the last page. ```json theme={null} { "object": "list", "data": [ { "class_number": 25, "object": "classification", "title": "Clothing, footwear, headwear", "description": "Clothing, footwear, headwear", "category": "goods" }, { "class_number": 35, "object": "classification", "title": "Advertising; business management, organization and administration; office functions", "description": "Advertising; business management, organization and administration; office functions.", "category": "services" } ], "has_more": false, "pagination": { "cursor": null } } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/classifications?type=services" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const classifications = await signa.references.classifications({ q: "software" }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------- | | 400 | `validation_error` | Invalid `type` parameter | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` scope | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Classification](/api-reference/reference/retrieve-classification), single class detail * [Suggest Classifications](/api-reference/reference/suggest-classifications), AI-assisted class suggestion from a description * [Suggest Goods & Services](/api-reference/reference/suggest-goods-services), draft filing-ready wording per class * [List Goods & Services](/api-reference/reference/list-goods-services), browse and search the pre-approved term catalog * [Search Trademarks](/api-reference/trademarks/search-trademarks), filter or search by Nice class # List Deadlines Source: https://docs.signa.so/api-reference/reference/list-deadlines GET /v1/deadlines List computed maintenance deadlines for a portfolio or trademark Supply exactly one of `portfolio_id` or `trademark_id`. Every request requires `trademarks:read`; a `portfolio_id` request additionally requires `portfolios:manage`, and a key missing either scope gets 403 `forbidden`. Portfolio requests return 404 for a portfolio outside your organization. Trademark requests accept any retrievable mark. Each page costs **one read unit**, with a default of 20 and a maximum of 100 rows. Deadlines are computed when requested. Rows carry `rule_id`, `effective_from`, `last_verified`, `sources[]`, `trigger_date`, and `trigger_field`, alongside trademark identity, due date, window opening, grace expiry and urgency. `sources[]` holds the statutory citations behind the rule, the same ones [Compute Deadlines](/api-reference/reference/compute-deadlines) returns; join `rule_id` to [List Deadline Rules](/api-reference/reference/deadline-rules) for the full rule record. For prosecution deadlines, use [Compute Deadlines](/api-reference/reference/compute-deadlines). Filter with a comma-separated `type`, an inclusive `due_before` date (`YYYY-MM-DD`), or `urgency` (`critical`, `upcoming`, `routine`, `in_grace`, `missed`). The list is forward-looking by default and omits deadlines whose grace period has already closed; `urgency=missed` selects exactly those rows instead. The default cutoff is 720 days after the computation date, within a five-year computation horizon. Portfolios are capped at 5,000 marks; split larger portfolios. Results sort by `due_date`, `trademark_id`, then `rule_id`, all ascending. Follow `pagination.cursor` while `has_more` is true. The signed cursor pins the computation date and binds the selector and filters. Keep those parameters unchanged on the next page. A cursor that fails to decode or verify — malformed, unsigned, minted for another endpoint, or an old offset cursor — returns `400 cursor_expired`; a cursor that verifies but whose selector or filters have changed returns `400 cursor_invalid`. Either way, restart at page one. Records and portfolio membership may change between pages: the cursor pins the clock, not a historical snapshot. `GET /v1/portfolios/{id}/deadlines` is an alias using the same rows, filters and cursor. Both paths preserve the portfolio list's inclusive cutoff semantics, and a cursor minted on one can be followed on the other. The alias differs in one deliberate way: it requires `portfolios:manage` alone, without the base `trademarks:read` this path also requires, because its scope is unchanged from the day it shipped and adding a required scope to a published operation would break existing keys. ```typescript theme={null} const deadlines = await signa.deadlines.list({ portfolio_id: 'ptf_12345678-1234-1234-1234-123456789012', type: ['renewal', 'international_renewal'], limit: 100, }); for await (const deadline of deadlines) { console.log(deadline.trademark_id, deadline.due_date, deadline.rule_id); } ``` A single row looks like this: ```json theme={null} { "trademark_id": "tm_019d2141-6ce9-771b-872e-bc8b20e49fcf", "rule_id": "us_declaration_of_use_s8", "effective_from": null, "last_verified": "2026-03-10", "mark_text": "EXAMPLE", "office_code": "US", "type": "declaration_of_use", "due_date": "2026-03-10", "grace_expiry": "2026-09-10", "window_opens": "2025-03-10", "jurisdiction_code": "US", "description": "Declaration of Use (Section 8)", "trigger_date": "2020-03-10", "trigger_field": "registration_date", "urgency": "upcoming", "sources": [ { "citation": "15 U.S.C. § 1058(a) — registration term and affidavit requirement", "url": "https://www.law.cornell.edu/uscode/text/15/1058" } ] } ``` # List Design Codes Source: https://docs.signa.so/api-reference/reference/list-design-codes GET /v1/design-codes List Vienna figurative classification codes ## Overview Lists Vienna classification design codes used to describe figurative elements in trademarks. Filter by depth (1 = category, 2 = division, 3 = section) or search descriptions. Use this to build Vienna code pickers. ## Query Parameters Substring search across descriptions (case-insensitive, max 200 chars). Filter by depth: `1` (category), `2` (division), or `3` (section). Two-digit category code filter (e.g. `03`). Max results (1-500). Pagination cursor (the code of the last item returned). ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. Always `list`. Array of Vienna code records ordered by code. Vienna code (e.g. `03`, `03.05`, `03.05.01`). Always `design_code`. Two-digit category code (e.g. `03`). Division-level code (e.g. `03.05`). `null` for depth-1 codes. Section-level code (e.g. `03.05.01`). `null` for depth-1 and depth-2 codes. Depth (1, 2, or 3). Description text. Whether more pages are available. Cursor for the next page, or `null` on the last page. ```json theme={null} { "object": "list", "data": [ { "code": "03.01.01", "object": "design_code", "category": "03", "division": "03.01", "section": "03.01.01", "depth": 3, "description": "Lions" } ], "has_more": true, "pagination": { "cursor": "03.01.01" } } ``` ## Code Examples ```bash cURL theme={null} curl -G "https://api.signa.so/v1/design-codes" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=lion" \ --data-urlencode "depth=3" \ --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 codes = await signa.references.designCodes({ q: "lion", depth: 3, limit: 20, }); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------- | | 400 | `validation_error` | Invalid depth or query length | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` scope | ## Related Endpoints * [Retrieve Design Code](/api-reference/reference/retrieve-design-code), single code with children * [Search Trademarks](/api-reference/trademarks/search-trademarks), search by `vienna_codes` # List Event Types Source: https://docs.signa.so/api-reference/reference/list-event-types GET /v1/event-types List per-office event type code mappings ## Overview Returns per-office event type code mappings. Each entry maps a raw office-specific event code to a canonical event type. Use this to populate filter dropdowns, validate event data, or build event-type legend displays. ## Query Parameters Filter mappings by office code (e.g. `WO`). Omit to return all offices. Max results per page (max 500). Pagination cursor from `pagination.cursor` of the previous page. ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. Always `list`. Always `event_type_mapping` Uppercase ST.3 office code (e.g. `WO`) Office-specific raw event code Office-specific human-readable label Canonical event type (e.g. `registration`, `renewal`, `correction`) Scope of the event (e.g. `designation` for events that apply to a single Madrid designation) Additional notes about this mapping Whether more pages are available. Cursor for the next page, or `null` on the last page. ```json theme={null} { "object": "list", "data": [ { "object": "event_type_mapping", "office_code": "WO", "raw_code": "AMDN", "raw_label": "Amendment notification", "event_type": "correction", "event_scope": null, "notes": "Amendment notification" }, { "object": "event_type_mapping", "office_code": "WO", "raw_code": "CBN1", "raw_label": "Confirmation of protection granted (total)", "event_type": "registration", "event_scope": "designation", "notes": "Confirmation of protection granted (total)" } ], "has_more": true, "pagination": { "cursor": "WO|CBN1" } } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/event-types?office=WO" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const eventTypes = await signa.references.eventTypes(); for (const mapping of eventTypes.data) { console.log(mapping.raw_code, "->", mapping.event_type); } ``` ## Errors | Status | Type | Description | | ------ | -------------- | ------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` scope | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Trademark Events](/api-reference/trademarks/trademark-events), event timeline for a mark * [Trademarks & lifecycle guide](/guides/trademarks), canonical status stages # List Goods & Services Source: https://docs.signa.so/api-reference/reference/list-goods-services GET /v1/goods-services Browse and search accepted goods & services terms across one Nice class or all 45 ## Overview Browse and search 96,000+ pre-approved goods & services descriptions from the Harmonised Database (HDB) and the USPTO ID Manual. Use this to power goods/services autocomplete in filing tools (pass `class` to scope results to a single Nice class), or to discover which classes accept a particular term (pass `q` without `class` for a cross-class search). For background on how office acceptance works and the data sources behind this endpoint, see the [Classifications & Goods/Services guide](/guides/classifications). At least one of `q` or `class` must be provided. For semantic class discovery from a natural-language business description (e.g. "SaaS tool for HR teams"), use [Suggest Classifications](/api-reference/reference/suggest-classifications). For a drafted goods/services specification with filing-ready wording per class, use [Suggest Goods & Services](/api-reference/reference/suggest-goods-services). ## Query Parameters Substring search across term text (2-200 characters). Optional if `class` is set. Returns `400 validation_error` if neither `q` nor `class` is provided. Nice class number (1-45). Optional if `q` is set. Returns `400` if outside the 1-45 range, or `404` if the class number is in range but not loaded in reference data. Language code (e.g. `en`, `de`, `fr`, `es`, `it`, `ja`). When `true`, only return terms on the TMClass harmonised list. Max results (1-100). Pagination cursor from `pagination.cursor` of the previous page. ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. Always `list`. Array of term records ordered by term key. The term text. Stable normalized key for the term. Nice class this term is filed under (1-45). Present on every result so cross-class searches render without follow-up fetches. TMClass taxonomy path identifier, when available. Whether the term is on the harmonised list. Offices that accept this term (e.g. `USPTO`, `EUIPO`). Source catalog (e.g. `tmclass`, `uspto_idm`). Language code. Whether more pages are available. Cursor for the next page, or `null` on the last page. ```json theme={null} { "object": "list", "data": [ { "term": "3D animation software", "term_key": "3d animation software", "class_number": 9, "taxonomy_path": "8729051", "is_harmonised": true, "accepted_offices": ["EUIPO"], "source": "tmclass", "language": "en" } ], "has_more": true, "pagination": { "cursor": "3d animation software" } } ``` ## Code Examples ### Scoped autocomplete (user already picked a class) ```bash cURL theme={null} curl -G "https://api.signa.so/v1/goods-services" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=mobile applications" \ --data-urlencode "class=9" \ --data-urlencode "harmonised_only=true" \ --data-urlencode "limit=10" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const terms = await signa.goodsServices.list({ q: "mobile applications", class: 9, harmonised_only: true, limit: 10, }); ``` ### Cross-class discovery (which classes accept this term?) Pass only `q` (omit `class`) and inspect `class_number` on each result to see which classes contain the wording. ```bash cURL theme={null} curl -G "https://api.signa.so/v1/goods-services" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=poster" \ --data-urlencode "limit=25" ``` ```typescript TypeScript theme={null} const matches = await signa.goodsServices.list({ q: "poster", limit: 25 }); for (const t of matches.data) { console.log(`Class ${t.class_number}: ${t.term}`); } ``` ### Browse all terms in a class Pass only `class` (omit `q`) to page through the full list of accepted wording for one class. ```typescript TypeScript theme={null} let page = await signa.goodsServices.list({ class: 25, limit: 50 }); for (const t of page.data) console.log(t.term); while (page.has_more) { page = await page.getNextPage(); for (const t of page.data) console.log(t.term); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Neither `q` nor `class` provided, `q` shorter than 2 characters, or `class` outside the 1-45 range | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` scope | | 404 | `not_found` | `class` is in range but not loaded in reference data | ## Related Endpoints * [Suggest Goods & Services](/api-reference/reference/suggest-goods-services), drafts a filing-ready specification from a description * [Suggest Classifications](/api-reference/reference/suggest-classifications), lighter tier, classes only * [Retrieve Classification](/api-reference/reference/retrieve-classification) * [List Classifications](/api-reference/reference/list-classifications) # List Jurisdictions Source: https://docs.signa.so/api-reference/reference/list-jurisdictions GET /v1/jurisdictions List all trademark jurisdictions with office coverage ## Overview Returns every jurisdiction Signa tracks, whether or not it has a live office connection yet. Each entry reports the jurisdiction's scope, Madrid Protocol membership, and its office coverage status. Use this to build jurisdiction pickers and to tell customers which jurisdictions have live data today versus which are recognized but not yet covered. ## Query Parameters Max results per page (max 500). Pagination cursor, the jurisdiction `code` of the last item on the previous page. When `true`, includes `pagination.total_count` (exact, the full catalog is loaded server-side). ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. Always `list`. ISO 3166-1 alpha-2 jurisdiction code, or a WIPO/regional code such as `EU`. Always `jurisdiction`. Jurisdiction name. Office code for this jurisdiction's trademark office, or `null` when `office_status` is not `live`. `live` when the office connection is shipping data, `roadmap` when an office is planned but not yet live, `not_covered` when no office is associated with this jurisdiction. `national`, `regional`, or `international`. Whether the jurisdiction is a Madrid Protocol member. Whether more pages are available. Cursor for the next page, or `null` on the last page. Exact jurisdiction count. Only present when `include_total=true`. Always `false` when present, the count is exact. ```json theme={null} { "object": "list", "data": [ { "code": "EU", "object": "jurisdiction", "name": "European Union", "office_code": "EM", "office_status": "live", "scope_type": "regional", "madrid_member": true }, { "code": "AE", "object": "jurisdiction", "name": "United Arab Emirates", "office_code": null, "office_status": "not_covered", "scope_type": "national", "madrid_member": true } ], "has_more": true, "pagination": { "cursor": "AE" } } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/jurisdictions?limit=50&include_total=true" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const jurisdictions = await signa.references.jurisdictions(); const live = jurisdictions.data.filter((j) => j.office_status === "live"); ``` ## Errors | Status | Type | Description | | ------ | -------------- | ------------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Jurisdiction](/api-reference/reference/retrieve-jurisdiction), single jurisdiction detail * [List Offices](/api-reference/reference/list-offices), live office coverage * [Madrid Protocol guide](/guides/madrid-protocol) # List Office Votes Source: https://docs.signa.so/api-reference/reference/list-office-votes GET /v1/offices/votes List your organization's office-coverage votes ## Overview Returns every office your organization has voted for, alphabetical by office code. Watch `office_status`: when an office you voted for goes live, its votes flip to `live` — your ask landed. The votable vocabulary is bounded — ISO 3166-1 country codes plus `BX`, `AP`, `OA`, see [Cast Office Votes](/api-reference/reference/cast-office-votes) — so the list is always a single page: `has_more` is always `false` and `pagination.cursor` is always `null`. Like `GET /v1/feedback`, this list bills 1 read unit; casting and retracting votes bill 0 units. Responses carry the standard `RateLimit-*` headers. ## Query Parameters Accepted for compatibility with generic list clients and SDK pagination helpers, then ignored — every vote comes back in one page. Must still be a well-formed page size (1–100). Accepted and ignored, same reason. There is never a next page to fetch. Any other query parameter is rejected with `400 validation_error`. Responses are `private, no-store` — votes are org-scoped, never shared cache. ## Response Always `list`. Always `office_vote`. Canonical uppercase ST.3 office code. `live`, `roadmap`, or `not_covered` — see [Cast Office Votes](/api-reference/reference/cast-office-votes). When your organization first voted for this office. Always `false`. ```json theme={null} { "object": "list", "data": [ { "object": "office_vote", "office_code": "CL", "office_status": "not_covered", "created_at": "2026-08-29T12:00:00.000Z" }, { "object": "office_vote", "office_code": "GB", "office_status": "roadmap", "created_at": "2026-08-12T09:30:00.000Z" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_01kabc..." } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/offices/votes" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const votes = await signa.references.officeVotes(); for (const vote of votes.data) { console.log(vote.office_code, vote.office_status); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ---------------------------------------------------------- | | 400 | `validation_error` | An unsupported query parameter, or a `limit` outside 1–100 | | 401 | `unauthorized` | Missing or invalid API key | | 429 | `rate_limited` | Too many requests | Casting a vote can also fail validation with `unknown_office_code`, `office_already_live`, or `no_trademark_register` — see [Cast Office Votes](/api-reference/reference/cast-office-votes). ## Related Endpoints * [Cast Office Votes](/api-reference/reference/cast-office-votes), vote for the offices you want next * [Retract Office Vote](/api-reference/reference/retract-office-vote), remove a vote * [List Offices](/api-reference/reference/list-offices), the offices already covered by Signa # List Offices Source: https://docs.signa.so/api-reference/reference/list-offices GET /v1/offices List all supported trademark offices ## Overview Returns the trademark offices that are currently live on the Signa platform, with per-office sync metadata: update cadence, total marks, last successful sync time, and field coverage percentages. Offices that are not yet live are omitted from this response. ## Query Parameters Max results per page (max 500). Pagination cursor, the office `code` of the last item on the previous page. ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. Always `list`. Uppercase ST.3 office code, e.g. `US`, `EM`. Legacy lowercase codes (e.g. `uspto`, and `eu` for EUIPO) are accepted as permanent aliases on requests. Always `office`. The pre-ST.3 internal office code (e.g. `uspto`). Accepted as a permanent request alias. WIPO ST.3 code — equal to `code`; kept for the transition. Full office name. Name of the office's country or region. ISO jurisdiction code for this office, e.g. `US`, `EU`. Office website URL. `live` when the office is shipping data normally, `paused` when it is temporarily offline. How often this office's data is refreshed: `daily`, `weekly`, `monthly`, `on_demand`, or `null` when not yet scheduled. Total trademark records currently held for this office. ISO 8601 timestamp of the last successful data sync, or `null`. What Signa can serve for this office beyond the core record, independent of whether any data is present today. Examiner refusal citations for this office (see [Search Citations](/api-reference/citations/search-citations)). One of `available` (live pipeline), `in_progress` (build underway), `not_available` (no citations are served for this office: either it refuses on relative grounds and there is no pipeline yet, or its regime has not been classified), or `not_applicable` (the office does not refuse on relative grounds ex officio, so it never issues a citation-bearing refusal). Today only `US` is `available`. Per-field completeness percentages for this office, rounded from 0 to 100. `null` means coverage has not been computed yet. Records with image media. Records with goods/services text on classifications. Records with Vienna or USPTO design search codes. Records with a publication date. Records with a registration number. Records with a registration date. Records with an expiry date. Records with priority-claim data. Records with seniority-claim data. Records with filing-basis data. Records with first-use dates. Records linked to at least one attorney/representative. Records linked to at least one owner. Records with a status effective date. ISO 8601 timestamp of the stats run that computed `coverage`, or `null`. Whether more pages are available. Cursor for the next page, or `null` on the last page. ```json theme={null} { "object": "list", "data": [ { "code": "EM", "object": "office", "legacy_code": "euipo", "st3_code": "EM", "name": "European Union Intellectual Property Office", "country_name": "European Union", "jurisdiction_code": "EU", "website_url": "https://euipo.europa.eu", "status": "live", "update_cadence": "daily", "total_marks": 3202739, "last_synced_at": "2026-07-06T07:03:32.937Z", "capabilities": { "citations": "not_applicable" }, "coverage": { "images": 64, "goods_services_text": 76, "design_codes": 15, "publication_date": 81, "registration_number": 94, "registration_date": 92, "expiry_date": 88, "priority_claims": 7, "seniority_claims": 3, "filing_basis": 31, "first_use_date": 24, "attorney_linkage": 68, "owner_linkage": 99, "status_effective_date": 73 }, "coverage_computed_at": "2026-07-06T08:00:00.000Z" }, { "code": "US", "object": "office", "legacy_code": "uspto", "st3_code": "US", "name": "United States Patent and Trademark Office", "country_name": "United States of America", "jurisdiction_code": "US", "website_url": "https://www.uspto.gov", "status": "live", "update_cadence": "daily", "total_marks": 14175925, "last_synced_at": "2026-07-06T13:04:22.321Z", "capabilities": { "citations": "available" }, "coverage": null, "coverage_computed_at": null } ], "has_more": true, "pagination": { "cursor": "US" } } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/offices?limit=10" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const offices = await signa.references.offices(); for (const office of offices.data) { console.log(office.code, office.total_marks, office.last_synced_at); } ``` ## Errors | Status | Type | Description | | ------ | -------------- | ------------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Retrieve Office](/api-reference/reference/retrieve-office), single office detail * [Cast Office Votes](/api-reference/reference/cast-office-votes), vote for an office that is not covered yet * [List Jurisdictions](/api-reference/reference/list-jurisdictions), jurisdiction overview * [Data Freshness](/guides/data-freshness), sync cadence and coverage by office # List Opposition Rules Source: https://docs.signa.so/api-reference/reference/opposition-rules GET /v1/opposition-rules List all opposition window rules across modeled offices ## Overview Returns a flat list of opposition window rules: the statutory windows during which third parties may oppose a published trademark application or registration. Each rule includes the office's local time zone, holiday calendar (used for close-date rollover), statutory citations, and any common (non-statutory but routinely granted) extensions. Each rule carries a stable `rule_id`, and every computed opposition window reports the `rule_id` that produced it, so any window Signa returns can be resolved back to the rule and citations behind it. The rule is also addressable by the combination of `office_code` and `filing_route`, which is what the compute endpoint looks up. This endpoint exposes the same rule corpus that powers [Compute Oppositions](/api-reference/reference/compute-oppositions) and the `derived.opposition_window` field on a trademark detail response. Use it to build educational UI, surface citations alongside a watch alert, or document what to expect for a given office. ## Query Parameters Filter by jurisdiction code (uppercase ISO-2). Comma-separated for multiple values. Filter by uppercase ST.3 office code (as returned by `/v1/offices`). Comma-separated for multiple values, e.g. `?office=US,EM`. Legacy lowercase codes (e.g. `uspto`, and `eu` for EUIPO) are accepted as permanent aliases. Filter by the rule's filing route discriminator. Accepted values: `domestic`, `madrid_designation`. Comma-separated for multiple values. Filter by trigger event. Accepted values: `application_publication`, `registration_publication`, `international_designation_publication`. Comma-separated for multiple values. ## Response A standard list response with `data: OppositionRule[]`. The corpus is small and returned in a single page. This endpoint is publicly cacheable, so the response never includes `request_id`. Always `list` Always `opposition_rule` Stable, permanent slug identifying this rule, e.g. `eu_opposition_madrid`. Once published a slug is never renamed or repointed, so it is safe to persist and to join against `rule_id` on [Compute Oppositions](/api-reference/reference/compute-oppositions) items, trademark `derived.opposition_window`, and watch diagnostics. Treat it as **opaque**: the slug looks parseable but its grammar is not part of the contract, so compare it for equality and nothing else. It is deliberately not a prefixed entity ID (`tm_`, `own_`) because it names a statutory rule, not a stored entity. Earliest date from which this rule's modelled content is authoritative, as `YYYY-MM-DD`. `null` when the source corpus does not establish one, which is the common case. Related to but not the same as `applicable_from_date`: `applicable_from_date` is the statutory commencement the engine gates on, so a publication dated before it yields no window at all rather than a different rule's window, while `effective_from` is a statement about the modelling. (Rule lookup is a single match on office plus filing route, so there is never an alternate era rule to fall back to; where a window length changes over time, that variation is modelled *inside* the one rule rather than by a second rule.) Where a rule carries a cutover the two agree: in the current opposition corpus every non-null `effective_from` equals its rule's `applicable_from_date`, and every rule without a cutover reports `effective_from: null`. Date Signa last checked this rule against its statutory sources, as `YYYY-MM-DD`. This is the same value the compute endpoint returns as `rule_version`; it moves on a verification pass even when nothing about the rule changed. Jurisdiction (uppercase ISO-2) Uppercase ST.3 office code (e.g. `US`, `EM`, `CA`) Either `domestic` or `madrid_designation` Which event starts the opposition clock: `application_publication`, `registration_publication`, or `international_designation_publication` Window duration in months (`null` when expressed in days) Window duration in days (`null` when expressed in months) Months to wait after publication before the window opens (e.g. EUIPO Madrid-EU has 1 month between Bulletin Part M republication and window opening) Where the clock starts: `date_of_publication` (day zero is the publication day), `day_after` (the publication day is not counted), or `first_of_following_month` (the window opens on the first day of the month AFTER the publication month: how several offices draft the window against a WIPO Gazette issue, which carries a month rather than a service date) How the close is derived when **the current era** opens on `first_of_following_month` with a window expressed in months; `null` for every other rule, where the close is anchored on the publication day-number instead. It is **also `null` for a rule that only opens that way in its EARLIER era**, which reports the convention under `window_ends_on_before`. `corresponding_day_of_start` means the period ends on the same-numbered day of the Nth month (a window opening 1 August with `window_months: 3` closes 1 November); `day_before_corresponding_day_of_start` means it ends the day before, i.e. on the last day of the Nth month (31 October). Both are real statutory readings and they differ by exactly one day, so the field says which one the office applies rather than defaulting. Date this rule changed regime, as `YYYY-MM-DD`. Half-open: a publication **strictly before** it is governed by the four `_before` fields below, one on or after it by the current-era fields. `null` when the rule has never changed, which is most of them. One rule (`tr_opposition`) compares the FILING date against this boundary rather than the publication date, under its transitional provision, so pass `filing_date` to [Compute Opposition Windows](/api-reference/reference/compute-oppositions) to get that reading. **Not every `window_change_date` is a statutory date.** Most are: the day the instrument itself says the new regime starts. A few are **serving** pivots, set earlier than commencement on purpose because the statute left a straddling cohort unaddressed and Signa publishes the shorter of the two possible windows. `cn_opposition` is the current example. China's 2026 Trademark Law cuts the window from three months to two and commences on **1 January 2027**, but says nothing about a period already running that day, so `window_change_date` is **2026-10-01**, the first publication whose three months would still be open at commencement. Every publication from that day is served two months. A close published too early costs a would-be opponent nothing they can lose by relying on it; one published too late costs the right itself. Read a `window_change_date` that precedes the cited instrument's commencement as a conservative serving choice, not as a claim about when the law changed, and re-check it once the office publishes its transitional guidance. `window_starts_on` for publications before `window_change_date`, when the change moved the start. `null` when the change moved only the length, or when there is no change, in which case `window_starts_on` governs both eras. `bx_opposition` is the example: it ran from the first of the following month until 30 September 2013 and from the publication day itself thereafter. `window_months` for publications before `window_change_date`. `null` when the earlier era counted in days, when only the start convention changed, or when there is no change. `window_days` for publications before `window_change_date`. Exactly one of `window_months_before` / `window_days_before` is non-null when the change moved the length, and a change may switch units: `kr_opposition` went from two months to 30 days on 22 July 2025. `window_ends_on` for the earlier era, when THAT era opens on the first of the following month with a month-based window. `bx_opposition` reports `corresponding_day_of_start` here and `null` in `window_ends_on`. What a month-counted close date does when the publication's day number does not exist in the target month. `last_day` clamps to that month's last day, which is what every office except Mexico does and is the value returned for them. `first_business_day_of_following_month` runs the period into the next calendar month and ends it on that month's first business day: an MX application published 31 March 2026 closes 4 May 2026, not 30 April (Reglamento LPI art. 4o, párrafo segundo, read together with `holiday_calendar` for the business-day half). Only `mx_opposition` returns it today, and it never applies to a window expressed in days. IANA office time zone (display only; date math uses calendar dates) Holiday calendar key used for close-date rollover, e.g. `uspto_dc` Extension length in days Extension length in months `true` if granted automatically on timely request, `false` if discretionary but routinely granted Human-readable note for UI surfacing Statutory commencement this rule is gated on. A publication before this date yields no window at all — `supported: false` on [Compute Oppositions](/api-reference/reference/compute-oppositions) — not a different rule's window: lookup is a single match on office + filing route. `null` means the rule has no commencement gate and always applies. Statutory citation, e.g. `Article 196(2) EUTMR` Authoritative URL for the citation Always `false` Standard pagination envelope (`cursor: null`) ```json theme={null} { "object": "list", "data": [ { "object": "opposition_rule", "rule_id": "eu_opposition_madrid", "effective_from": null, "last_verified": "2026-05-06", "jurisdiction_code": "EU", "office_code": "EM", "filing_route": "madrid_designation", "trigger_event": "international_designation_publication", "window_months": 3, "window_days": null, "window_starts_offset_months": 1, "window_starts_on": "date_of_publication", "window_ends_on": null, "window_change_date": null, "window_starts_on_before": null, "window_months_before": null, "window_days_before": null, "window_ends_on_before": null, "month_end_overflow": "last_day", "office_time_zone": "Europe/Madrid", "holiday_calendar": "euipo_alicante", "common_extension": null, "applicable_from_date": null, "sources": [ { "citation": "Article 196(2) EUTMR (Regulation (EU) 2017/1001) — opposition to IRs designating EU; period of three months begins one month after the publication pursuant to Article 190(1)", "url": "https://eur-lex.europa.eu/eli/reg/2017/1001/oj/eng" }, { "citation": "Article 190(1) EUTMR — EUIPO republication in EUTM Bulletin Part M", "url": "https://eur-lex.europa.eu/eli/reg/2017/1001/oj/eng" }, { "citation": "Article 67 Commission Delegated Regulation (EU) 2018/625", "url": "https://eur-lex.europa.eu/eli/reg_del/2018/625/oj" }, { "citation": "Article 69 Commission Delegated Regulation (EU) 2018/625", "url": "https://eur-lex.europa.eu/eli/reg_del/2018/625/oj" } ] } ], "has_more": false, "pagination": { "cursor": null } } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/opposition-rules?office=EM&filing_route=madrid_designation" \ -H "Authorization: Bearer sig_YOUR_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.oppositionRules({ office: "EM", filing_route: "madrid_designation" }); for (const rule of rules.data) { console.log(rule.office_code, rule.filing_route, rule.window_months); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------------- | | 400 | `validation_error` | Invalid `filing_route` or `trigger_event` value | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Compute Oppositions](/api-reference/reference/compute-oppositions), compute per-publication opposition windows from these rules * [List Deadline Rules](/api-reference/reference/deadline-rules), maintenance deadline rules (renewals, declarations) * [List Offices](/api-reference/reference/list-offices), office overview # Reconcile Source: https://docs.signa.so/api-reference/reference/reconcile POST /v1/reconcile Diff your trademark records against Signa's register data ## Overview Compare the trademark records you already hold against Signa's register data and get a field-by-field diff back. Nothing is stored. Records drift from the register over time: statuses change, owners change, registration numbers get assigned, and renewal dates move. Send the fields you have and Signa tells you, per record and per field, exactly where you still match the register and where you've fallen out of sync. Use it to surface exceptions, run scheduled drift checks, or audit a book of matters during onboarding. Records that aren't found, or that match more than one register entry, come back as per-item outcomes in the response, never a request-level error. ## Request Body Records to reconcile, max 100 items. Results are returned in the same order, with one `reconciliation` per input item. Uppercase ST.3 trademark office code, e.g. `US`, `EM`, or `CA`. Legacy lowercase codes (e.g. `uspto`, and `eu` for EUIPO) are accepted as permanent aliases. Office-native application number. Provide at least one of `application_number` or `registration_number`. Office-native registration number. Provide at least one of `application_number` or `registration_number`. Fields from your system to compare. Must include at least one key and rejects unknown keys. Caller status value. Primary statuses compare against `status_primary`; stage statuses compare against `status_stage`; unknown values are reported as mismatches. Owner name from your system. Compared with normalized legal suffix, punctuation, case, and diacritic folding. Mark text from your system. Compared case-insensitively with collapsed whitespace. Filing date as `YYYY-MM-DD`. Registration date as `YYYY-MM-DD`. Expiry date as `YYYY-MM-DD`. Renewal due date as `YYYY-MM-DD`. Registration number to compare against the register value. Nice class numbers. Compared as a set. Your docketed maintenance deadlines for this mark, 1 to 10 entries. Requires `verdict_detail: true`. Each entry produces its own `field_verdicts[]` row and is matched independently against the computed schedule. Deadline type in the rulebook vocabulary: `renewal`, `declaration_of_use`, `combined_renewal_and_use`, `declaration_of_incontestability`, `restoration`, or `international_renewal`. Same values the deadlines API emits. The due date you have on docket, as `YYYY-MM-DD`. Opt in to the three-source per-field verdict. Each found item gains `field_verdicts[]`, a `computed` context block and a `lookup` echo. Everything v1 is unchanged: `fields[]`, `result`, `mismatch_count`, and billing are identical with the flag on and off. Required to send `your_fields.docketed_deadlines`. ## Response A standard list response with `data: Reconciliation[]`. Pagination is not used; `data[i]` corresponds to `items[i]`. Always `list`. Always `reconciliation`. Office used for lookup. Application number supplied on the input item, or `null`. Registration number supplied on the input item, or `null`. One of `match`, `mismatch`, `not_found`, or `ambiguous`. Signa trademark ID (`tm_*`) when exactly one register record resolved. Register record update timestamp as ISO 8601, or `null` when unresolved. Compared field: `status`, `owner_name`, `mark_text`, `filing_date`, `registration_date`, `expiry_date`, `renewal_due_date`, `registration_number`, or `nice_classes`. Value supplied in `your_fields`. Normalized register value compared against your value. Whether the field matched after field-specific normalization. Number of `fields[]` entries where `match` is `false`. Present only when `result` is `ambiguous`. The lookup stops as soon as a second register record matches, so the value is always `2`. It signals "more than one match", not an exact count. Present only when `verdict_detail: true`. How your identifiers were interpreted: `office` (resolved ST.3 code), `office_input` (the token you sent, verbatim), `application_number` and `registration_number` (the values used in the lookup), `matched_on` (`application_number`, `registration_number`, or `both`), and `identifier_normalization`. Present on every item, including `not_found` and `ambiguous`, where it is most useful. Present only when `verdict_detail: true` and the record was found. The computed-schedule context: `jurisdiction_code`, `filing_route` (`national`, `regional`, or `madrid`), `supported`, `unsupported_reason` (`unsupported_jurisdiction`, `requires_office_date`, or `null`), `sources` (statutory citations), `as_of`, `computed_at`, and `horizon_years`. Present only when `verdict_detail: true`. One three-source verdict per field you supplied. See [Three-source verdicts](#three-source-verdicts-verdict-detail) below. Present only when `verdict_detail: true`. Totals over `field_verdicts[]` with every verdict key explicit, zeros included: `agrees`, `caller_differs`, `register_differs_from_computed`, `all_differ`, `unsupported_jurisdiction`, `insufficient_data`, `not_computable`. Always `false`. Always `{ "cursor": null }`. Unique request identifier for support and debugging. `not_found` is soft: the request still returns HTTP `200`, with `result: "not_found"` for that item and an empty `fields` array. `ambiguous` means the identifier lookup resolved to more than one register record, such as an `(office, application_number)` pair that is not unique; Signa does not silently choose one. ```json theme={null} { "object": "list", "data": [ { "object": "reconciliation", "office": "US", "application_number": "88123456", "registration_number": null, "result": "mismatch", "trademark_id": "tm_8kLm2nPq", "register_last_updated_at": "2026-07-05T12:00:00.000Z", "fields": [ { "field": "status", "your_value": "active", "register_value": "active", "match": true }, { "field": "owner_name", "your_value": "Nike, Inc.", "register_value": "Nike Innovate C.V.", "match": false } ], "mismatch_count": 1 } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_bT9kM3nP" } ``` ## Three-source verdicts (`verdict_detail`) Send `verdict_detail: true` to turn a drift check into a docket audit. For every field you supply, Signa compares three sources: your value, the register's value, and the value Signa's deadline rulebook computes for the same mark (the same computation `GET /v1/trademarks/{id}` serves under `derived.deadlines`, with missed obligations included and a 10-year horizon that extends per record, up to 20 years, so the schedule always reaches the register's stated expiry). Enabling the flag changes nothing you already rely on. `fields[]`, `result` and `mismatch_count` are byte-identical with the flag on and off, and billing is unchanged (1 unit per item). A verdict where you are right and the register or the rulebook disagrees never flips `result` to `mismatch`. Each `field_verdicts[]` entry carries `field`, `your_value`, `register_value`, `computed_value`, `basis`, `verdict`, `computed_unavailable_reason`, and `computed_provenance` (the `rule_id`, `effective_from`, `last_verified`, `due_date_adjustment`, `grace_expiry_adjustment` and `holiday_calendar` behind the computed date, spelled exactly as the deadlines API spells them). Rows for `docketed_deadlines` entries use `field: "docketed_deadline"` with sibling `deadline_type` and `index` (your array position); both are `null` on the nine standard field rows. ### A docketed-only request always reads `match` Send a `your_fields` that contains only `docketed_deadlines` and the item comes back with `result: "match"`, an empty `fields[]` and `mismatch_count: 0`. That is the designed outcome. v1 semantics are frozen: `result`, `fields[]` and `mismatch_count` describe the nine standard fields and nothing else, so a docketed deadline can never move any of them. The audit you asked for lives exclusively in `field_verdicts[]` and `verdict_counts{}`, with one row per deadline you sent. Read the verdicts rather than `result`. ### `basis`: which sources answered | `basis` | Register produced a value | Computed produced a value | | ----------------------- | ------------------------- | ------------------------- | | `register_and_computed` | yes | yes | | `register` | yes | no | | `computed` | no | yes | | `none` | no | no | When the computed side produced nothing, `computed_unavailable_reason` says why: `not_modelled` (the rulebook has no concept of this field, true for everything except `expiry_date`, `renewal_due_date` and docketed deadlines), `unsupported_jurisdiction` (no rulebook coverage for this record's jurisdiction and route), or `insufficient_data` (coverage exists but the record lacks the statutory input, or no matching obligation was emitted). ### The seven verdicts | `verdict` | Reading | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `agrees` | Every source that could answer agrees with you | | `caller_differs` | The sources that answered agree with each other and you are the outlier | | `register_differs_from_computed` | The office and Signa's rules disagree; you match one of them. Both values are returned and you decide which to trust | | `all_differ` | Three distinct values. The highest-signal cell in a docket audit | | `unsupported_jurisdiction` | No rulebook coverage, and the register column is also empty, so nothing could be compared | | `insufficient_data` | Coverage exists but the computation declined or found no matching obligation, and the register column is also empty | | `not_computable` | No source could answer: the register column is empty and the rulebook does not model the field | Two behaviors worth knowing: * `unsupported_jurisdiction` and `insufficient_data` are verdicts only when the register is also silent. When the register did answer, the verdict describes that comparison (`agrees` or `caller_differs`) and the computed-side gap is reported on `computed_unavailable_reason` instead. Nothing is hidden; the verdict names the comparison that actually happened. * `not_computable` also fires for a register-only field whose column is empty, for example `mark_text` on a device mark. ### How the computed value is chosen For `expiry_date` and `renewal_due_date`, the computed counterpart is the mark's current term end: the earliest computed renewal-family obligation due on or after today, falling back to the most recent past one (the cycle currently in force). The rulebook treats expiry and renewal-due as one date under two names. For `docketed_deadlines` entries, matching is deliberately about the obligation you docketed: within your entry's `type`, the occurrence whose due date is nearest your date wins (ties go to the earlier one). A docketed `renewal` matches the whole renewal family, including a US combined Section 8 and 9 filing and a WIPO international renewal. The matched rule is echoed in `computed_provenance.rule_id` so the pairing is checkable. ### Identifier matching is exact `lookup.identifier_normalization` is `none` today: the identifiers you send are matched exactly, with no trimming, leading-zero handling or separator stripping. The echo exists so office-specific normalization can ship later without a shape change. `lookup.matched_on` shows which identifiers were combined into the lookup, which also explains `ambiguous` results when you sent both. ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/reconcile" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "office": "US", "application_number": "88123456", "your_fields": { "status": "active", "owner_name": "Nike, Inc." } } ] }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const results = await signa.reconcile.run({ items: [{ office: "US", application_number: '88123456', your_fields: { status: 'active', owner_name: 'Nike, Inc.' } }] }); for (const result of results.data) { console.log(result.result, result.mismatch_count); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | --------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Malformed body, missing identifier, empty `your_fields`, invalid date format, or unknown body field | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 422 | `batch_too_large` | More than 100 items. The error carries `item_count` and `item_count_limit` | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Batch Retrieve](/api-reference/trademarks/batch-trademarks), hydrate known IDs or office-native identifiers * [Search Trademarks](/api-reference/trademarks/search-trademarks), search the normalized register # Retract Office Vote Source: https://docs.signa.so/api-reference/reference/retract-office-vote DELETE /v1/offices/votes/{code} Retract your organization's vote for an office ## Overview Removes your organization's vote for one office. `DELETE` accepts a **wider** vocabulary than casting: any ST.3 / ISO 3166-1 two-letter code (plus `BX`, `AP`, `OA`, aliases canonicalized) resolves here. Only an unrecognizable token returns `400`. Codes that casting rejects — an office Signa already covers, or a territory with no trademark register of its own (`AQ`, `BV`, …) — still resolve and still delete, so a row stored under an earlier vocabulary is never stranded; when you have no vote for the code, you get `404`, not `400`. A territory resolves to the office that registers its marks, so retracting `PR` targets the USPTO vote and `RE` the INPI-FR one — and because the code is canonicalized first, the `404` detail names the parent office (`US`, `FR`), not the territory you sent. Retracting is free — `DELETE` bills 0 units, as does casting; only [List Office Votes](/api-reference/reference/list-office-votes) bills (1 read unit). Responses carry the standard `RateLimit-*` headers. Requires an `Idempotency-Key` header: retrying with the **same** key replays the cached response of that attempt (so a replayed retract still reads `deleted: true`, not `404`), while a **new** logical attempt — retracting again after re-casting the vote — needs a **fresh** key. The vote itself is stored once per organization per office, so the server's state is the same either way. ## Path Parameters Office code — ST.3 / ISO, any case or accepted alias (`UK` retracts the `GB` vote). ## Response Always `office_vote`. Canonical uppercase ST.3 code of the retracted vote. Always `true`. Returns `404 not_found` when your organization has no vote for the office (`"Your organization has no vote for CL."`), and `400 validation_error` for unknown codes. ```json theme={null} { "object": "office_vote", "office_code": "CL", "deleted": true, "request_id": "req_01kabc..." } ``` ## Code Examples ```bash cURL theme={null} curl -X DELETE "https://api.signa.so/v1/offices/votes/CL" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Idempotency-Key: retract-office-vote-br-001" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const retracted = await signa.references.retractOfficeVote("CL"); console.log(retracted.office_code, retracted.deleted); // CL true ``` ## Errors | Status | Type | Description | | ------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | `unknown_office_code` — the only vocabulary rejection on retract. Covered offices and register-less codes (`AQ`, `BV`, …) resolve normally and delete | | 400 | `validation_error` | Missing `Idempotency-Key` header | | 401 | `unauthorized` | Missing or invalid API key | | 404 | `not_found` | Your organization has no vote for this office | | 409 | `conflict` | `Idempotency-Key` reused with a different body | | 409 | `idempotency_processing` | A request with the same `Idempotency-Key` is still in flight | | 429 | `rate_limited` | Too many requests | The `404` detail reads `"Your organization has no vote for CL."` — the code is canonicalized first, so retracting `uk` when you have no `GB` vote reports `GB`, and retracting `PR` reports `US`. ## Related Endpoints * [Cast Office Votes](/api-reference/reference/cast-office-votes), vote for the offices you want next * [List Office Votes](/api-reference/reference/list-office-votes), read your organization's votes back * [List Offices](/api-reference/reference/list-offices), the offices already covered by Signa # Retrieve Classification Source: https://docs.signa.so/api-reference/reference/retrieve-classification GET /v1/classifications/{class_number} Retrieve a single Nice classification with its full term list ## Overview Returns a single Nice classification (1-45) with its title, description, category (goods or services), and the canonical list of terms registered to that class. ## Path Parameters Nice class number (1-45). ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. Nice class number. Always `classification`. Class title. Class description. `goods` or `services`. Canonical term list. ```json theme={null} { "class_number": 25, "object": "classification", "title": "Clothing, footwear, headwear", "description": "Clothing, footwear, headwear", "category": "goods", "terms": ["footwear*", "sports shoes", "sandals", "hats"] } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/classifications/25" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const cls = await signa.references.classification(25); ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------- | | 400 | `validation_error` | Class number outside 1-45 | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` scope | | 404 | `not_found` | Class not loaded in reference data | ## Related Endpoints * [List Classifications](/api-reference/reference/list-classifications) * [List Goods & Services](/api-reference/reference/list-goods-services), browse and search the pre-approved terms catalog * [Suggest Classifications](/api-reference/reference/suggest-classifications), AI-assisted class suggestion (classes only) * [Suggest Goods & Services](/api-reference/reference/suggest-goods-services), classes with filing-ready wording per class # Retrieve Design Code Source: https://docs.signa.so/api-reference/reference/retrieve-design-code GET /v1/design-codes/{code} Retrieve a single Vienna classification code with notes and child codes ## Overview Returns a single Vienna classification code along with any child codes at the next depth level. Use this when navigating the Vienna hierarchy interactively (e.g. drill down from `03` to `03.05` to `03.05.01`). ## Path Parameters Vienna code at any depth: `03` (category), `03.05` (division), `03.05.01` (section). The undotted form (`030501`) and unpadded segments (`3.5.1`) are also accepted and normalized. ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. The requested Vienna code, normalized to the zero-padded dotted form. Always `design_code`. Two-digit category code (e.g. `03`). Division-level code (e.g. `03.05`). `null` for depth-1 codes. Section-level code (e.g. `03.05.01`). `null` for depth-1 and depth-2 codes. Depth (1, 2, or 3). Description text. Additional Vienna notes when published by WIPO. Child codes at the next depth, each with `code`, `description`, and `depth`. Empty for depth-3 (leaf) codes. ```json theme={null} { "code": "03.05", "object": "design_code", "category": "03", "division": "03.05", "section": null, "depth": 2, "description": "Quadrupeds (series V), quadrumana", "notes": null, "children": [ { "code": "03.05.01", "description": "Rabbits, hares", "depth": 3 }, { "code": "03.05.03", "description": "Squirrels", "depth": 3 } ] } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/design-codes/03.05" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const code = await signa.references.designCode("03.05"); ``` ## Errors | Status | Type | Description | | ------ | -------------- | ------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` scope | | 404 | `not_found` | Vienna code does not exist | ## Related Endpoints * [List Design Codes](/api-reference/reference/list-design-codes) * [Search Trademarks](/api-reference/trademarks/search-trademarks), filter by `vienna_codes` # Retrieve Jurisdiction Source: https://docs.signa.so/api-reference/reference/retrieve-jurisdiction GET /v1/jurisdictions/{code} Retrieve a single jurisdiction with Madrid Protocol membership details ## Overview Returns a single jurisdiction record. For regional jurisdictions like the EU, the response includes the member jurisdictions. For Madrid Protocol members, the WIPO contracting party code is included so you can map between national filings and Madrid designations. ## Path Parameters Jurisdiction code (e.g. `US`, `EU`, `GB`, `DE`). ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. Jurisdiction code. Always `jurisdiction`. Jurisdiction name. Office code for this jurisdiction's trademark office, or `null` when `office_status` is not `live`. `live` when the office connection is shipping data, `roadmap` when an office is planned but not yet live, `not_covered` when no office is associated with this jurisdiction. `national`, `regional`, or `international`. Whether the jurisdiction is a Madrid Protocol member. For regional jurisdictions, the list of member country codes. `null` for jurisdictions with no members list. WIPO contracting party code for Madrid filings, or `null` when not applicable. ```json theme={null} { "code": "EU", "object": "jurisdiction", "name": "European Union", "office_code": "EM", "office_status": "live", "scope_type": "regional", "madrid_member": true, "member_jurisdictions": ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"], "wipo_contracting_party_code": "EM" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/jurisdictions/EU" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const jurisdiction = await signa.references.jurisdiction("EU"); ``` ## Errors | Status | Type | Description | | ------ | -------------- | ------------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 404 | `not_found` | Jurisdiction code not recognized | ## Related Endpoints * [List Jurisdictions](/api-reference/reference/list-jurisdictions) * [Retrieve Office](/api-reference/reference/retrieve-office) * [Deadline Rules](/api-reference/reference/deadline-rules), maintenance rules across all jurisdictions * [Opposition Rules](/api-reference/reference/opposition-rules), third-party opposition windows * [Madrid Protocol guide](/guides/madrid-protocol) # Retrieve Office Source: https://docs.signa.so/api-reference/reference/retrieve-office GET /v1/offices/{code} Retrieve a single trademark office with sync metadata ## Overview Returns a single trademark office record: update cadence, total marks ingested, last successful sync timestamp, and field coverage percentages. Use this to display data freshness and data completeness for a specific office. Offices that are not live return `404`. ## Path Parameters Uppercase ST.3 office code, e.g. `US`, `EM`, `WO`. Legacy lowercase codes (e.g. `uspto`, and `eu` for EUIPO) are accepted as permanent aliases. This is not the jurisdiction code, use [Retrieve Jurisdiction](/api-reference/reference/retrieve-jurisdiction) for ISO codes like `US` or `EU`. ## Response This endpoint is publicly cacheable, so the response never includes `request_id`. The object has the same fields as an item in [List Offices](/api-reference/reference/list-offices). Uppercase ST.3 office code. Always `office`. The pre-ST.3 internal office code (e.g. `uspto`). Accepted as a permanent request alias. WIPO ST.3 code — equal to `code`; kept for the transition. Full office name. Name of the office's country or region. ISO jurisdiction code for this office, e.g. `US`. Office website URL. `live` when the office is shipping data normally, `paused` when it is temporarily offline. How often this office's data is refreshed: `daily`, `weekly`, `monthly`, `on_demand`, or `null` when not yet scheduled. Total trademark records currently held for this office. ISO 8601 timestamp of the last successful data sync, or `null`. What Signa can serve for this office beyond the core record. `capabilities.citations` is one of `available`, `in_progress`, `not_available`, or `not_applicable`. See [List Offices](/api-reference/reference/list-offices) for what each value means. Per-field completeness percentages for this office, rounded from 0 to 100. See [List Offices](/api-reference/reference/list-offices) for the full field list. ISO 8601 timestamp of the stats run that computed `coverage`, or `null`. ```json theme={null} { "code": "US", "object": "office", "legacy_code": "uspto", "st3_code": "US", "name": "United States Patent and Trademark Office", "country_name": "United States of America", "jurisdiction_code": "US", "website_url": "https://www.uspto.gov", "status": "live", "update_cadence": "daily", "total_marks": 14175925, "last_synced_at": "2026-07-06T13:04:22.321Z", "capabilities": { "citations": "available" }, "coverage": { "images": 64, "goods_services_text": 76, "design_codes": 15, "publication_date": 81, "registration_number": 94, "registration_date": 92, "expiry_date": 88, "priority_claims": 7, "seniority_claims": 3, "filing_basis": 31, "first_use_date": 24, "attorney_linkage": 68, "owner_linkage": 99, "status_effective_date": 73 }, "coverage_computed_at": "2026-07-06T14:00:00.000Z" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/offices/US" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const office = await signa.references.office("US"); console.log(office.total_marks, office.last_synced_at); ``` ## Errors | Status | Type | Description | | ------ | -------------- | ----------------------------------------------------- | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key missing the `trademarks:read` scope | | 404 | `not_found` | Office code not recognized, or the office is not live | ## Related Endpoints * [List Offices](/api-reference/reference/list-offices), all supported offices * [Search Citations](/api-reference/citations/search-citations), the endpoint `capabilities.citations` describes * [Cast Office Votes](/api-reference/reference/cast-office-votes), vote for an office that is not covered yet * [Retrieve Jurisdiction](/api-reference/reference/retrieve-jurisdiction) * [Data Freshness](/guides/data-freshness) # Suggest Classifications Source: https://docs.signa.so/api-reference/reference/suggest-classifications POST /v1/classifications/suggest Suggest Nice classes for a description of goods, services, or a business (classes only, no wording) ## Overview Turn a description of goods, services, or a business into ranked Nice classification classes with confidence and rationale. Ideal when you just need to know *which* classes apply: filter UIs, watch setup, clearance pre-screening, or portfolio analysis. Works for company descriptions ("SaaS tool for HR teams"), product listings ("men's athletic running shoes, cushioned sole"), service offerings ("on-site SEO audits"), and anything in between. Ambiguous inputs like "coffee company" come back with an `ambiguous` flag, a clarifying question, and classes for every plausible interpretation, so you can surface the question to your user and re-submit. Need paste-ready goods/services wording per class for a filing? Use [Suggest Goods & Services](/api-reference/reference/suggest-goods-services) instead: same input, but returns each class with grounded `accepted_terms` from the pre-approved catalog. **Use this to sharpen a search before you run it.** A text query alone returns every mark that looks alike, whatever it covers. Send the goods or services description here first, then pass the returned `class_number` values as the `nice_classes` filter on [Search Trademarks](/api-reference/trademarks/search-trademarks), on a [watch](/api-reference/monitoring/watches/create), or on a screening request. Results come back scoped to the classes the mark would actually conflict in, so the hits you get are the ones worth reading. For background on Nice classes and office acceptance, see the [Classifications and Goods/Services guide](/guides/classifications). ## Body Parameters Description of the goods, services, or business (3-500 characters). Natural-language, no special format. Nice Classification is a WIPO international standard: the 45 classes are the same in every member jurisdiction, so this endpoint deliberately takes no jurisdiction hint. If you need jurisdiction-biased goods/services *wording*, use [Suggest Goods & Services](/api-reference/reference/suggest-goods-services), which accepts an optional `jurisdiction_code` that influences term selection (not the class list). ## Response Always `classification_suggestion`. The description you submitted, echoed back. Version tag of the classifier that produced this response. Ranked suggested classes. Each class contains the fields below. Nice class number (1-45). Canonical class title. One of `high`, `medium`, or `low`. 1-indexed position in the returned ordering. One of `core`, `conditional`, or `adjacent`. When `recommendation_type` is `conditional`, the condition under which this class applies. Otherwise `null`. Short explanation of why this class was suggested. `true` when the description has two or more materially different filing strategies. When `ambiguous` is `true`, a question to ask the user. Otherwise `null`. Unique request identifier for support and debugging. ```json theme={null} { "object": "classification_suggestion", "query": "e-commerce company selling sneakers", "classifier_version": "classify-v1.0", "classes": [ { "class_number": 25, "title": "Clothing, footwear, headwear", "confidence": "high", "rank": 1, "recommendation_type": "core", "conditional_on": null, "rationale": "Sneakers are athletic footwear, squarely in class 25." }, { "class_number": 35, "title": "Advertising", "confidence": "medium", "rank": 2, "recommendation_type": "conditional", "conditional_on": "brand used for the storefront, not only the products", "rationale": "Operating an online store is retail services." } ], "ambiguous": false, "clarification_question": null, "request_id": "req_cU3lM7nP" } ``` ## Code Examples ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/classifications/suggest" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "e-commerce company selling sneakers" }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const result = await signa.references.suggestClassifications({ description: "e-commerce company selling sneakers", }); // result.classes is a ranked list of { class_number, confidence, rationale, ... } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | --------------------------------------------------------------- | | 400 | `validation_error` | `description` missing, shorter than 3, or longer than 500 chars | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` scope | | 502 | `upstream_error` | Classification service temporarily unavailable | ## Related Endpoints * [Suggest Goods & Services](/api-reference/reference/suggest-goods-services), same input, returns classes with filing-ready wording * [List Goods & Services](/api-reference/reference/list-goods-services), browse and search the pre-approved terms catalog * [Search Trademarks](/api-reference/trademarks/search-trademarks), filter results with the suggested classes via `nice_classes` * [List Classifications](/api-reference/reference/list-classifications) * [Retrieve Classification](/api-reference/reference/retrieve-classification) # Suggest Goods & Services Source: https://docs.signa.so/api-reference/reference/suggest-goods-services POST /v1/goods-services/suggest Draft a goods/services specification from a description, with filing-ready wording per class ## Overview Draft a goods/services specification from a natural-language description. Returns ranked Nice classes **with filing-ready wording per class**, grounded in the pre-approved catalog (Harmonised Database + USPTO ID Manual). Every term in the response is an actual accepted term; invented wording never appears. Use this to power: * Filing wizards that auto-draft the goods/services block of an application * Attorney drafting tools * Multi-class application flows where each class is a separate line item Just need the class numbers (no wording)? Use [Suggest Classifications](/api-reference/reference/suggest-classifications) instead: same input, lighter response, billed at 1 unit. Pass `class_number` in the body to restrict the response to a single, already-chosen class. This is useful when you're refining wording inside a class the user has already picked. For background on Nice classes and office acceptance, see the [Classifications and Goods/Services guide](/guides/classifications). ## Body Parameters Description of the goods, services, or business (3-500 characters). Natural-language, no special format. Optional ISO-3166-1 alpha-2 jurisdiction code (e.g. `US`, `EU`, `GB`). Biases retail-vs-product interpretation toward local practice. Optional Nice class (1-45). When set, scopes the response to that single class. Useful for refining wording when the class has already been chosen. ## Response Always `goods_services_suggestion`. The description you submitted, echoed back. Version tag of the classifier that produced this response. Ranked suggested classes, each with filing-ready accepted terms. Nice class number (1-45). Canonical class title. One of `high`, `medium`, or `low`. 1-indexed position in the returned ordering. One of `core`, `conditional`, or `adjacent`. When `recommendation_type` is `conditional`, the condition under which this class applies. Otherwise `null`. Short explanation of why this class was suggested. Paste-ready goods/services terms for this class, drawn only from the pre-approved catalog. `true` when the description has two or more materially different filing strategies. When `ambiguous` is `true`, a question to ask the user. Otherwise `null`. Unique request identifier for support and debugging. Each item in `accepted_terms` has this shape: | Field | Type | Description | | ------------------ | --------- | ------------------------------------------------------ | | `term` | string | The canonical term text. | | `source` | string | Source catalog (e.g. `tmclass`, `uspto_idm`). | | `accepted_offices` | string\[] | Offices that accept this term (e.g. `USPTO`, `EUIPO`). | | `harmonised` | boolean | Whether the term is on the TMClass harmonised list. | ```json theme={null} { "object": "goods_services_suggestion", "query": "e-commerce company selling sneakers", "classifier_version": "classify-v1.0", "classes": [ { "class_number": 25, "title": "Clothing, footwear, headwear", "confidence": "high", "rank": 1, "recommendation_type": "core", "conditional_on": null, "rationale": "Sneakers are athletic footwear, squarely in class 25.", "accepted_terms": [ { "term": "Running shoes", "source": "tmclass", "accepted_offices": ["EUIPO"], "harmonised": true }, { "term": "Athletic footwear", "source": "uspto_idm", "accepted_offices": ["USPTO"], "harmonised": false } ] }, { "class_number": 35, "title": "Advertising", "confidence": "medium", "rank": 2, "recommendation_type": "conditional", "conditional_on": "brand used for the storefront, not only the products", "rationale": "Operating an online store is retail services.", "accepted_terms": [] } ], "ambiguous": false, "clarification_question": null, "request_id": "req_dV4mN8pQ" } ``` ## Code Examples ### Full draft (let the classifier pick the classes) ```bash cURL theme={null} curl -X POST "https://api.signa.so/v1/goods-services/suggest" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "e-commerce company selling sneakers", "jurisdiction_code": "US" }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const spec = await signa.goodsServices.suggest({ description: "e-commerce company selling sneakers", jurisdiction_code: "US", }); for (const cls of spec.classes) { console.log(`Class ${cls.class_number}: ${cls.title}`); for (const term of cls.accepted_terms) { console.log(` ${term.term}`); } } ``` ### Refine wording for a known class Pass `class_number` when the applicant has already decided which class to file under and only needs the wording. ```typescript TypeScript theme={null} const refined = await signa.goodsServices.suggest({ description: "men's running shoes for marathon training", class_number: 25, }); // refined.classes has a single entry for class 25 with accepted terms. ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------------------------- | | 400 | `validation_error` | `description` missing, shorter than 3, longer than 500 chars, or `class_number` outside 1-45 | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` scope | | 502 | `upstream_error` | Classification service temporarily unavailable | ## Related Endpoints * [Suggest Classifications](/api-reference/reference/suggest-classifications), same input, lighter response, billed at 1 unit * [List Goods & Services](/api-reference/reference/list-goods-services), browse and search the pre-approved terms catalog directly * [List Classifications](/api-reference/reference/list-classifications) * [Retrieve Classification](/api-reference/reference/retrieve-classification) # Validate Goods & Services Source: https://docs.signa.so/api-reference/reference/validate-goods-services POST /v1/goods-services/validate Check whether wording appears in the pre-approved catalogs available for requested offices `listed` means the wording appears in that office's pre-approved catalog, exactly or after punctuation normalization (`match.type` says which). `not_listed` means we cannot show it there: it is an **absence of evidence, not a refusal**. These catalogs record positive membership only, so this endpoint cannot predict whether an office will allow wording that is absent. Catalog answers are available only for `US`, `EM`, `JP`, `KR`, and `CN`. Every other recognized office returns `unsupported_office` with reason `no_catalog_data`; the endpoint does not imply broader coverage. ## Request Send 1–100 items and 1–10 office codes. Only English (`en`) is available. Duplicate office aliases collapse and response office codes use ST.3. Offices to report on. Catalog answers exist only for `US`, `EM`, `JP`, `KR`, and `CN`. Catalog language. Only `en` is available in v1. Wording to check, in response order. Each item requires `term` and may include `class_number` from 1–45. Supplying `class_number` fixes the class used for candidate wording when the submitted term is not listed. Without one, the endpoint may match or infer a class and search that class; candidates are empty only when no class can be resolved or candidate search is not applicable. If the wording exists in another class, the item returns `class_mismatch` and `class_matches` names the class where the catalog wording actually lives. If no class is supplied and several classes match, the status is `ambiguous_class` and `class_matches` carries those rows. `Idempotency-Key` is optional. When supplied, it is honoured for replay; it is never required. Billing is per item. `fee_impact` is reserved and always `null` in v1. ```bash theme={null} curl -X POST "https://api.signa.so/v1/goods-services/validate" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "offices": ["US", "GB"], "language": "en", "items": [{ "term": "Flowmeters for use in oil pipelines", "class_number": 9 }] }' ``` ```typescript theme={null} const result = await signa.goodsServices.validate({ offices: ['US', 'GB'], items: [ { term: 'Flowmeters for use in oil pipelines', class_number: 9, }, ], }); ``` ## Candidate relations Candidates are advisory catalog wording from the supplied or resolved class: * `contained_in_input` means the wording appears as a contiguous run of words in what the customer wrote. * `similar` means a lexical neighbour. It also includes rows demoted by the word-boundary check: a trigram match can be close without being a phrase the customer typed. * `template` means the fixed wording of a USPTO drafting template appears in order. The placeholder and possible fill are in `template.slots`. Every candidate has `span` and `template`; they are `null` when not applicable. Candidate wording is advisory, not a guarantee of acceptance. ## Coverage of your wording `coverage` describes how many whitespace-separated tokens are supported by an exact/normalized match or by returned whole-word candidates. `uncovered` preserves the remaining wording as maximal runs. Coverage is **descriptive evidence, not an acceptance verdict**; connector words between supported spans can count structurally, while similar and template candidates never count as verified coverage. ## Template candidates A `template` candidate is a drafting aid from the USPTO ID Manual. Fill its placeholders using `template.slots`; `filled_with` is `null` when the submitted wording cannot fill a slot unambiguously. Templates are **never `listed` matches**, even when their fixed wording appears in the input. ## Inferred classes When an unclassified miss can be mapped to one Nice class, `class_source` is `inferred` and `class_inference` reports the method and confidence. This is a suggestion, not a classification decision. Supply `class_number` on a later request to override it. ## Warnings (indefinite wording) `warnings` identifies wording covered by guidance for a requested office and cites the guideline in `source`. `objection` means the cited guidance calls the construction indefinite or unclear; `caution` means acceptability depends on context. A warning is guidance, **not a prediction that an examiner will refuse the wording**. ## Catalog freshness (`as_of`) Each `summary.offices[].catalog_freshness[]` entry pairs a `catalogs` id with `as_of`, the UTC date that source was last refreshed. It tells you how fresh the evidence is—and how stale an absence may be. `null` means freshness is unknown; it never means the catalog is current. ## Candidate search completeness Inspect `candidate_search` before interpreting `candidates`. `complete` means the search ran, so an empty array is a real absence of nearby wording in the held catalog. `not_run` means candidates were not applicable. `truncated` means the time budget expired before this item's chunk was issued, and `degraded` means a database error prevented a reliable search; neither means that no candidate exists. `candidate_search_reason` explains every non-`complete` result. Summary counts roll up complete, truncated, and degraded items. ```json theme={null} { "object": "list", "summary": { "language": "en", "matching_policy": "lexical-v1", "item_count": 1, "unclassified_item_count": 0, "inferred_item_count": 0, "candidate_search": { "complete_item_count": 1, "truncated_item_count": 0, "degraded_item_count": 0 }, "warning_count": { "objection": 1, "caution": 0 }, "offices": [ { "office_code": "US", "supported": true, "catalogs": ["uspto_idm"], "catalog_freshness": [{ "source": "uspto_idm", "as_of": "2026-08-30" }], "listed_item_count": 0 }, { "office_code": "GB", "supported": false, "catalogs": [], "catalog_freshness": [], "listed_item_count": null } ], "classes": [{ "class_number": 9, "item_count": 1 }], "fee_impact": null }, "data": [{ "object": "goods_services_validation", "term": "Flowmeters for use in oil pipelines", "class_number": 9, "class_source": "provided", "class_inference": null, "status": "not_listed", "match": null, "class_matches": [], "verdicts": [ { "office_code": "US", "status": "not_listed", "reason": "not_in_catalog" }, { "office_code": "GB", "status": "unsupported_office", "reason": "no_catalog_data" } ], "warnings": [{ "rule_id": "us-open-ended-etc", "office": "US", "severity": "objection", "reason": "The office's guidelines treat “etc.” as indefinite wording.", "source": { "title": "USPTO Trademark Manual of Examining Procedure — May 2026", "section": "TMEP §1402.03(a)", "url": "https://tmep.uspto.gov/" } }], "candidates": [ { "term": "Flowmeters for use in {indicate equipment or field of use}", "term_key": "flowmeters for use in {indicate equipment or field of use}", "class_number": 9, "relation": "template", "score": 1, "score_metric": "strict_word_similarity", "is_harmonised": false, "listed_by": ["US"], "sources": ["uspto_idm"], "span": { "start": 0, "end": 4 }, "template": { "pattern": "Flowmeters for use in {indicate equipment or field of use}", "slots": [{ "placeholder": "indicate equipment or field of use", "filled_with": "oil pipelines" }] } } ], "coverage": { "tokens": 7, "covered_tokens": 0, "uncovered": ["Flowmeters for use in oil pipelines"] }, "candidate_search": "complete", "candidate_search_reason": null }], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_abc123" } ``` ## Statuses | Level | Status | Meaning | | ------ | -------------------- | -------------------------------------------------------------------------------------------------------------- | | Item | `listed` | The wording appears in a catalog Signa holds for the resolved class; use `verdicts` for each requested office. | | Item | `not_listed` | The wording could not be shown in the catalogs held for that class. This is not a refusal. | | Item | `class_mismatch` | The wording is catalogued in another class; inspect `class_matches`. | | Item | `ambiguous_class` | No class was supplied and catalog rows exist in several classes. | | Office | `unsupported_office` | No catalog data is held for that office; reason is `no_catalog_data`. | ## Errors | Status | Type | Description | | ------ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | 400 | `validation_error` | Invalid office, language, item count, term, or class number | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks `trademarks:read` | | 429 | `rate_limited`, `insufficient_credits`, `quota_exceeded` | Rate limit reached, or not enough credits/quota for the batch (a 100-item batch reserves 100 units up front) | # Check a Listing Source: https://docs.signa.so/api-reference/screening/check-listing POST /v1/screening/listings Screen a whole product listing for trademark conflicts (alpha, internal testing) **Alpha, internal testing.** Listing checks are not open to customers yet. These endpoints run on internal and design-partner keys during internal testing and their request and response shapes can still change. Email [support@signa.so](mailto:support@signa.so) to join the early access list. A listing check returns a risk signal, not legal advice. **Known limitation (alpha).** The current build over-flags listings that contain common dictionary words, so it returns `caution` for the large majority of ordinary listings. A verdict-separation redesign is in progress to restore precision. Until it lands, treat a `caution` verdict as no-signal, not evidence of risk, and do not rely on the endpoint for automated enforcement decisions. Real-world validation against the full production register superseded the earlier precision numbers measured on a small synthetic test index. ## Overview `POST /v1/screening/listings` answers *"is this LISTING risky?"* — one level up from [screening a name](/api-reference/screening/screen). You send the commerce text of a product listing (title, brand, description, keywords) and it: 1. **Extracts** candidate marks deterministically (no LLM at request time), with provenance (source field + character span per candidate). 2. **Fans out** each candidate through the screening core. 3. **Aggregates** to one listing-level verdict (`clear` / `caution` / `high_risk`), deduplicating conflicts across candidates. Billed **flat at 25 units per checked listing**, regardless of internal fan-out. ## Request ```json theme={null} { "title": "Vintage AirPods Case — Handmade Leather", "brand": "LeatherCraft Co", "description": "Compatible with AirPods Pro. Genuine leather.", "keywords": ["airpods case", "leather", "handmade"], "nice_classes": [18], "jurisdictions": ["US"], "sensitivity": "standard" } ``` Only `title` is required. Fields are **generic** — there are no marketplace-specific fields (Etsy/Amazon mappings live in docs recipes, not the schema). | Field | Type | Notes | | --------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `title` | string | **Required.** Split on hard delimiters, then n-grammed. | | `brand` | string | Always screened, never suppressed. | | `description` | string | High-precision markers only (quoted / ™®-adjacent / "compatible with X"). | | `keywords` | string\[] | Platform-neutral tags/keywords/search terms. | | `category` | string | Context only — never itself a candidate. | | `nice_classes` | number\[] | Hard filter + class-conditioned descriptive suppression. | | `jurisdictions` | string\[] | Territory codes (e.g. `US`, `EU`). Protection-scope, applied unconditionally as in [Screen a Name](/api-reference/screening/screen#coverage): a country request pulls in EU-wide rights that protect there, and a regional request pulls in every member state's national register. There is no `territory_match` opt-out on screening. | | `offices` | string\[] | Registering office codes, uppercase ST.3 (e.g. `US`). | | `include` | `live` \| `all` | `live` (default) keeps live marks. | | `sensitivity` | `strict` \| `standard` \| `broad` | Primary-tier threshold; the title/keyword tier is always `strict`. | ## Response A bespoke envelope: a leading `listing` decision block, an evidence-first `matches[]` payload, the back-compatible `data[]` of `listing_candidate` (each conflict is byte-identical to a [screening](/api-reference/screening/screen) hit), and a trailing `suppressed[]` audit list explaining why candidates were not screened. ```json theme={null} { "object": "list", "listing": { "verdict": "high_risk", "summary": { "high": 1, "medium": 0, "low": 0, "informational": 0 }, "candidates_screened": 3, "rules_version": "screening-v1", "verdict_version": "listing-verdict-v8", "extractor_version": "listing-extractor-v1:…", "selector_version": "listing-selector-v1:…", "as_of": "2026-07-01T00:00:00Z", "warnings": [], "analysis_status": "complete", "auto_approve_eligible": false, "class_provenance": { "source": "provided_unverified", "confidence": null }, "resolved_nice_classes": [18], "dictionaries_version": "…", "decision_asset_version": "a1b2c3d4e5f60718" }, "matches": [ { "object": "listing_match", "disposition": "actionable", "reason_codes": ["exact_mark", "live_status"], "rights_domain": "trademark", "goods_services_text_available": true, "candidate": { "term": "AirPods", "normalized": "airpods", "tier": "S", "field": "keywords" }, "registration": { "object": "screening_hit", "risk_level": "high", "…": "…" } } ], "data": [ { "object": "listing_candidate", "term": "AirPods", "normalized": "airpods", "tier": "S", "sources": [{ "field": "keywords", "span": [0, 7], "keyword_index": 0 }], "verdict": "high_risk", "nice_classes_used": [18], "hits": [ { "object": "screening_hit", "risk_level": "high", "…": "…" } ] } ], "suppressed": [ { "object": "suppressed_candidate", "term": "vintage", "field": "title", "span": [0, 7], "reason": "jargon" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_…" } ``` ### Reading the result `analysis_status` is a closed completeness enum and is orthogonal to the tri-state `verdict`. Automated approval must key off `auto_approve_eligible` (equivalent to `verdict === "clear"` **and** `analysis_status === "complete"`) and must not use `verdict` alone. Incomplete or unsupported analysis is never auto-approvable. `class_provenance` explains where the class set came from; `provided_unverified` means the caller supplied it and Signa has not verified it against a platform taxonomy. `resolved_nice_classes` is the listing-level set actually screened. `dictionaries_version` and `decision_asset_version` make the deterministic decision assets auditable. There is **no public `risk_score`**. Listing checks cover English only. When the title and brand are majority non-Latin (CJK, Thai, Arabic, or Cyrillic), the endpoint returns `analysis_status: "unsupported_language"`, floors the verdict to `caution`, and does not run the text through English screening. ### Safety guarantees * **Never a false clear.** Any search-backend error → `503` (never a partial "clear"). If the candidate budget is exceeded, the listing can never be `clear` — it is forced to `caution` with a `candidate_budget_capped` warning. * **Coverage-gated clear.** A would-be `clear` is forced to `caution` (with an `incomplete_office_coverage` warning) when a requested territory's register is not covered live in Signa, on the same protection-scope basis as [Screen a Name](/api-reference/screening/screen#coverage). The listing verdict enum stays `clear` / `caution` / `high_risk`, so the incomplete-coverage case surfaces as `caution` here rather than a distinct verdict. * **Informational-only conflicts.** A tier-S *identical name in an unrelated class* (e.g. a famous word used descriptively) is surfaced with `"informational": true` but does not escalate the listing verdict. ## SDK ```typescript theme={null} const result = await signa.screening.checkListing({ title: "Vintage AirPods Case — Handmade Leather", nice_classes: [18], }); if (!result.listing.auto_approve_eligible) { for (const match of result.matches) { console.log(match.disposition, match.reason_codes, match.registration); } } ``` # Screen a Name Source: https://docs.signa.so/api-reference/screening/screen GET /v1/screening Run a fast, deterministic knockout screen for one candidate name (alpha, internal testing) **Alpha, internal testing.** Screening is not open to customers yet. These endpoints run on internal and design-partner keys during internal testing and their request and response shapes can still change. Email [support@signa.so](mailto:support@signa.so) to join the early access list. Screening returns a risk signal, not legal advice. ## Overview `GET /v1/screening` answers *"could this candidate name conflict with an existing mark?"* Send a name and, optionally, its intended Nice classes or goods/services. Screening searches live register coverage, assigns each surfaced conflict a coarse band, and derives one request-level verdict. The endpoint is synchronous, deterministic within its rules and corpus versions, and text-only. It never returns a numeric similarity score. A `clear` verdict means that no high- or medium-band conflict was found; it does **not** mean that the mark is registrable. Each request is billed at **10 units**. ## Request ```bash theme={null} curl --get https://api.signa.so/v1/screening \ --header "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=KOFEE" \ --data-urlencode "nice_classes=30,43" \ --data-urlencode "jurisdictions=US,EU" \ --data-urlencode "sensitivity=standard" ``` | Parameter | Type | Notes | | ---------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `q` | string | Candidate mark text; 2–200 non-whitespace characters. Exactly one of `q` or `trademark_id` is required. | | `trademark_id` | string | Existing register record (`tm_...`) to use as the candidate. Exactly one of `q` or `trademark_id` is required. | | `nice_classes` | number\[] | Intended Nice classes, 1–45. Sent as a comma-separated list. Acts as a hard class filter. | | `goods_services` | string | Free-text intended goods/services, up to 2,000 characters. Used for class inference when classes are omitted and for goods/services matching when supported by the active resolver. | | `jurisdictions` | string\[] | Territory codes such as `US` and `EU`. Default: all live-coverage jurisdictions. Screening always applies protection-scope expansion, and unlike [Search Trademarks](/api-reference/trademarks/search-trademarks) there is no `territory_match` opt-out: a request for a country pulls in EU-wide rights that protect there (a EUTM can block a French launch), and a request for a region (`EU`) pulls in **both** the regional register and every member state's national register (a French national mark can block an EU launch). Screening always errs toward inclusion. | | `offices` | string\[] | Registering office codes, uppercase ST.3, such as `US` and `EM`. Default: all live offices. | | `include` | `live` \| `all` | `live` by default; `all` also includes dead marks. | | `sensitivity` | `strict` \| `standard` \| `broad` | Match-tier admission threshold. `strict` admits exact/normalized matches; `standard` adds close fuzzy and strong phonetic matches; `broad` adds weaker fuzzy matches. | | `limit` | number | Page size from 1–50. Default: 20. | Array query parameters use comma-separated values, without brackets. ## Screen an existing mark Use `trademark_id` instead of `q` for register-to-register discovery. Screening uses the record's mark text and, when you omit both `nice_classes` and `goods_services`, its own Nice classes as the intended-use profile. The candidate's collapsed family is removed before conflicts are banded, so it cannot appear as its own result. ```bash theme={null} curl --get https://api.signa.so/v1/screening \ --header "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "trademark_id=tm_0190f2d5-8e4b-7c1a-a8d3-123456789abc" ``` The response echoes the source in `screening.candidate.trademark_id`. Unknown records return `404 not_found`; records without textual mark content, such as design-only marks, return `422 not_screenable`. ## Bands and verdicts Every item in `data[]` is a `screening_hit` with one closed risk band: | Band | Meaning | | -------- | ----------------------------------------------------------------------------- | | `high` | Strongest combination of name match, goods/services overlap, and live status. | | `medium` | A meaningful conflict signal that needs review. | | `low` | A surfaced but lower-priority signal, including admitted dead-mark results. | The leading `screening.verdict` summarizes the full banded result set, not only the returned page: | Verdict | Rule | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `high_risk` | At least one high-band conflict exists. | | `caution` | No high-band conflict exists, but at least one medium-band conflict exists. | | `needs_review` | No high- or medium-band conflict was found, but the screen could not be completed to the standard required to say `clear`. The usual cause is incomplete register coverage for a requested territory; a stale office corpus, a capped analysis horizon, a failed analysis lane, an unsupported language, low-confidence class inference, or a coverage assessment that could not be read at all produce it too. When a coverage assessment is present, the [`coverage`](#coverage) block carries the details; it can be absent when the assessment itself failed. | | `clear` | No high- or medium-band conflict exists **and** every requested territory's register is covered live in Signa. Clear does not mean registrable. | Found conflicts always band high or medium regardless of coverage: observed risk is never suppressed. The coverage gate only ever downgrades a would-be `clear`, it never upgrades a genuine conflict. Treat `needs_review` as "we could not rule this out", not as a weaker `clear`. Hits also explain the coarse classification through `reason_codes`, `mark_match`, `goods_services_match`, `jurisdiction_match`, and a separate `litigation_risk` block. Reason codes and warnings are extensible; clients should tolerate values they do not yet recognize. ## Coverage A clearance verdict for a territory is only as trustworthy as the registers Signa can actually see there. If the national register that protects a requested territory is not connected live (for example a request scoped to `DE` when the German national register is dark), national-only marks in that territory are invisible, so a zero-conflict screen must **not** read as `clear`. The `screening.coverage` block reports exactly which registers gated the verdict. ```json theme={null} "coverage": { "offices": [ { "office": "US", "status": "complete", "as_of": "2026-07-20T00:00:00Z" }, { "office": "EM", "status": "complete", "as_of": "2026-07-20T00:00:00Z" } ], "territories": [ { "territory": "DE", "national_register": { "office": "DE", "covered": false }, "regional_registers": [{ "code": "EU", "covered": true }], "complete": false } ], "complete_for_clear": false } ``` This example is abridged to the coverage-gate axes. The served `coverage` object also always includes `classes`, `lanes`, `language`, and `horizon`, the other completeness inputs that can hold a verdict at `needs_review`. * **`offices`** is the register-freshness axis. Each entry is a public office code in WIPO **ST.3** form (`US`, `EM` for the EUIPO), with a `status` (`complete` / `partial` / `stale` / `unavailable`) and an `as_of` corpus watermark. It lists the offices that gated this verdict: the requested offices when `offices` was supplied, the national plus regional and member registers when `jurisdictions` was supplied, or every live office when the request is unscoped. * **`territories`** is the protection-scope axis, present when `jurisdictions` was supplied. Codes here are **jurisdiction** codes (`DE`, `EU`), not ST.3 office codes. For each requested territory it reports its own `national_register`, the `regional_registers` that also protect there (a country belonging to `EU` shows an `EU` entry), and whether the whole set is `complete`. * For a **regional** request (`jurisdictions=EU`) a territory entry adds `member_registers: { total, uncovered[] }`, summarizing how many member-state national registers are dark; `regional_registers` is still present but empty (the region is its own register). An EU screen stays `needs_review` while member registers are uncovered, and flips to `clear` only once they are live and fresh, the same principle as the `DE` example. * **`complete_for_clear`** is the single boolean the verdict gate reads. When it is `false`, a would-be `clear` is downgraded to `needs_review` and `screening.note` names the dark register. `coverage` is present on every screened response. When the coverage assessment itself cannot be completed, the block fails closed with `complete_for_clear: false` and an empty `offices`, which signals that coverage could not be verified and forces the verdict to `needs_review`. ## Response ```json theme={null} { "object": "list", "screening": { "verdict": "high_risk", "summary": { "high": 1, "medium": 0, "low": 0, "truncated": false }, "litigation_risk": { "high": 0, "medium": 0 }, "candidate": { "mark": "KOFEE", "normalized": "kofee" }, "rules_version": "screening-v1", "as_of": "2026-07-13T00:00:00.000Z", "admitted_match_levels": ["exact", "normalized", "fuzzy_1", "phonetic_strong"], "coverage": { "offices": [{ "office": "US", "status": "complete", "as_of": "2026-07-20T00:00:00Z" }], "territories": [ { "territory": "US", "national_register": { "office": "US", "covered": true }, "regional_registers": [], "complete": true } ], "complete_for_clear": true }, "warnings": [] }, "data": [ { "object": "screening_hit", "risk_level": "high", "reason_codes": ["exact_mark", "same_nice_class", "live_status"], "mark_match": { "level": "identical", "admitted_match_levels": ["exact"] }, "goods_services_match": { "level": "same_class", "matched_nice_classes": [30] }, "jurisdiction_match": { "requested": ["US", "EU"], "matched": ["US"] }, "litigation_risk": { "level": "unknown", "owner_publicly_traded": null, "owner_ticker": null, "prior_proceedings": null, "owner_portfolio_size": null }, "trademark": { "id": "tm_…", "object": "trademark", "mark_text": "KOFEE" } } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_…" } ``` `screening.rules_version` and `screening.as_of` identify the rules and corpus freshness behind the result. `screening.inferred_nice_classes` appears only when class inference ran (i.e. `nice_classes` was omitted from the request). ## Warnings and limitations * `warnings` is an open set. Handle known values and safely display or log new values rather than rejecting the response. * A missing goods/services description may produce `no_goods_services`; the result then has less context for class and goods/services matching. * `summary.truncated: true` means the analysis horizon was capped. It is separate from `has_more`, which describes response pagination. * Screening v1 is text-only. It does not compare logos, designs, Vienna codes, or images. * Backend failures return an error rather than a partial or false `clear`. * Screening responses are served `Cache-Control: private, no-store`. A screening result reflects live register coverage at request time, so it is not safe to cache client-side, re-run the screen rather than replaying a stored response. ## SDK ```typescript theme={null} const result = await signa.screening.screen({ q: "KOFEE", nice_classes: [30, 43], jurisdictions: ["US", "EU"], sensitivity: "standard", }); console.log(result.screening.verdict); for (const hit of result.data) { console.log(hit.trademark.mark_text, hit.risk_level); } ``` # Batch Retrieve Source: https://docs.signa.so/api-reference/trademarks/batch-trademarks POST /v1/trademarks/batch Retrieve multiple trademarks by ID or office-native identifiers in a single request ## Overview Fetch up to 100 trademarks in a single request. This is significantly more efficient than making individual `GET /v1/trademarks/{id}` calls when hydrating a list of known marks, e.g. after a search or when loading a portfolio view. The endpoint accepts either: * `ids`: an array of Signa public IDs (`tm_*`), or * `identifiers`: an array of office-native identifiers (application number + office, registration number + office, or IR number). You must supply exactly one of `ids` or `identifiers`. Items that resolve are returned in the `data` array; items that do not match any trademark are returned in the `not_found` array, so callers can reconcile the inputs against the response. ## Request Body Array of Signa trademark IDs (`tm_*`), max 100. Mutually exclusive with `identifiers`. Array of office-native identifiers, max 100. Mutually exclusive with `ids`. Each entry must include exactly one of: * `application_number` + `office` (uppercase ST.3 office code, e.g. `US`; legacy lowercase like `uspto` accepted as an alias) * `registration_number` + `office` * `ir_number` (no office; Madrid IRs are global) Office-native application number. Office-native registration number. Madrid International Registration number. Uppercase ST.3 office code. Required when using `application_number` or `registration_number`. Optional detail projections, applied to every trademark in the response. `office_extensions` includes each trademark's `office_extensions` object (raw office-specific data), which is otherwise omitted. There is no `fields=` sparse projection on batch; each returned trademark is the full detail tier. ## Response Always `list`. Array of trademark detail-tier objects matching the requested inputs. Inputs that could not be resolved. When `ids` was supplied, this is an array of the unresolved Signa IDs. When `identifiers` was supplied, this is an array of the original identifier objects that did not match. Callers should diff this against the input list to know what to retry or surface. Always `false`. Batch is not paginated. Always `{ "cursor": null }`. Unique request identifier for support and debugging. ```json Response (ids) theme={null} { "object": "list", "data": [ { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "AURORA", "mark_text_language": "en", "mark_text_script": null, "mark_feature_type": "word", "mark_legal_category": "standard", "right_kind": "trademark", "is_series_mark": false, "series_count": null, "status": { "primary": "active", "stage": "registered", "reason": null, "challenges": [], "effective_date": "2024-09-18", "source": "explicit", "raw_code": "800", "raw_label": "Registered" }, "office_code": "US", "jurisdiction_code": "US", "filing_route": "direct_national", "scope_kind": "national", "origin_office_code": null, "source_primary_id": "97123456", "application_number": "97123456", "registration_number": "7123456", "ir_number": null, "filing_date": "2023-04-12", "registration_date": "2024-09-18", "expiry_date": "2034-09-18", "expiry_date_basis": "derived", "renewal_due_date": "2034-09-18", "publication_date": "2024-06-01", "priority_date": null, "termination_date": null, "protection_effective_date": null, "designation_date": null, "dependency_period_end_date": null, "transformation_deadline_date": null, "is_retracted": false, "owners": [ { "id": "own_R3jK9mN2", "name": "Aurora Digital Inc.", "country_code": "US", "entity_type": "corporation", "role": "owner", "entity_id": "ent_019f34d6-2222-7777-8888-222222222222", "entity_id_type": "resolved", "address": { "lines": ["100 Market Street"], "city": "SAN FRANCISCO", "state": "CA", "postal_code": "94105", "country_code": "US" } } ], "attorneys": [ { "id": "att_Lp3mN7qR", "name": "Jane Smith", "firm_id": "firm_Xk9pQ2rS", "firm_name": "Smith & Associates LLP", "role": "representative", "address": { "lines": ["200 Main Street", "Suite 400"], "city": "SAN FRANCISCO", "state": "CA", "postal_code": "94105", "country_code": "US" } } ], "classifications": [ { "nice_class": 9, "nice_edition": "12", "goods_services_text": "Computer software for data analytics", "goods_services_language": "en", "status": null, "class_status_raw": null }, { "nice_class": 35, "nice_edition": "12", "goods_services_text": "Business consulting services", "goods_services_language": "en", "status": null, "class_status_raw": null }, { "nice_class": 42, "nice_edition": "12", "goods_services_text": "Cloud computing services", "goods_services_language": "en", "status": null, "class_status_raw": null } ], "design_codes": [], "text_variants": [], "statements": [], "media": [], "priority_claims": [], "filing_bases": [], "publications": [], "derived": { "ruleset_version": "2026-09-02", "deadlines_supported": true, "deadlines_unsupported_reason": null, "deadlines": [], "opposition_window": null }, "has_media": false, "primary_image_url": null, "coverage": null, "relationships": [], "events_count": 12, "proceedings_count": 0, "citations_count": 0, "provenance": { "source_data_date": "2026-04-10", "office_updated_at": "2026-04-09T00:00:00.000Z", "retrieved_at": null }, "created_at": "2025-08-01T12:00:00.000Z", "updated_at": "2026-04-10T06:00:00.000Z" } ], "not_found": ["tm_Zr5nK8jL"], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_bT9kM3nP" } ``` ```json Response (identifiers) theme={null} { "object": "list", "data": [ { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "AURORA", "mark_feature_type": "word", "mark_legal_category": "standard", "right_kind": "trademark", "status": { "primary": "active", "stage": "registered", "reason": null, "challenges": [], "effective_date": "2024-09-18", "source": "explicit", "raw_code": "800", "raw_label": "Registered" }, "office_code": "US", "jurisdiction_code": "US", "filing_route": "direct_national", "scope_kind": "national", "application_number": "97123456", "registration_number": "7123456", "filing_date": "2023-04-12", "registration_date": "2024-09-18", "expiry_date": "2034-09-18", "owners": [ { "id": "own_R3jK9mN2", "name": "Aurora Digital Inc.", "country_code": "US", "entity_type": "corporation", "role": "owner", "entity_id": "ent_019f34d6-2222-7777-8888-222222222222", "entity_id_type": "resolved", "address": { "lines": ["100 Market Street"], "city": "SAN FRANCISCO", "state": "CA", "postal_code": "94105", "country_code": "US" } } ], "classifications": [ { "nice_class": 9, "nice_edition": "12", "goods_services_text": "Computer software for data analytics", "goods_services_language": "en", "status": null, "class_status_raw": null } ], "attorneys": [], "design_codes": [], "text_variants": [], "statements": [], "media": [], "priority_claims": [], "filing_bases": [], "publications": [], "derived": { "ruleset_version": "2026-09-02", "deadlines_supported": true, "deadlines_unsupported_reason": null, "deadlines": [], "opposition_window": null }, "has_media": false, "primary_image_url": null, "coverage": null, "relationships": [], "events_count": 5, "proceedings_count": 0, "citations_count": 0, "provenance": { "source_data_date": "2026-04-10", "office_updated_at": "2026-04-09T00:00:00.000Z", "retrieved_at": null }, "created_at": "2025-08-01T12:00:00.000Z", "updated_at": "2026-04-10T06:00:00.000Z" } ], "not_found": [ { "registration_number": "9999999", "office": "US" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_cL2nP4qR" } ``` ## Code Examples ```bash cURL (ids) theme={null} curl -X POST "https://api.signa.so/v1/trademarks/batch" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["tm_8kLm2nPq", "tm_Xp4wQ7vR", "tm_Zr5nK8jL"] }' ``` ```bash cURL (identifiers) theme={null} curl -X POST "https://api.signa.so/v1/trademarks/batch" \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "identifiers": [ { "application_number": "97123456", "office": "US" }, { "registration_number": "6789012", "office": "US" }, { "ir_number": "1234567" } ] }' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const result = await signa.trademarks.batch({ identifiers: [ { application_number: "97123456", office: "US" }, { registration_number: "6789012", office: "US" }, { ir_number: "1234567" }, ], }); console.log(`Resolved ${result.data.length}, missing ${result.not_found.length}`); ``` ## When to Use Batch vs. Individual Lookups | Scenario | Recommended Approach | | ------------------------------------------- | ----------------------------------------------------------------------------- | | Display a single trademark detail page | Individual [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark) | | Hydrate a dashboard with 10-50 known IDs | Batch (this endpoint) | | Sync a portfolio of trademarks periodically | Batch, chunked into groups of 100 | | Search for trademarks matching criteria | [Search Trademarks](/api-reference/trademarks/search-trademarks) | | Iterate through all trademarks in your org | Paginated [Search Trademarks](/api-reference/trademarks/search-trademarks) | A batch request of 50 IDs counts as **one request** against your rate limit, not 50. Use batches whenever you know the IDs upfront. See [Rate Limits](/api-reference/rate-limits). Keep batch sizes at or below **100 items**. Requests with more than 100 IDs are rejected with a `400` validation error. A batch request returns HTTP `200` even if some requested items are not found. Always check the `not_found` array to reconcile against your input list. For chunking strategies when you have more than 100 IDs, see the [Bulk operations guide](/guides/bulk-operations). ## Errors | Status | Type | Description | | ------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Both `ids` and `identifiers` supplied, neither supplied, more than 100 items, or an identifier missing the required `office` field | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 429 | `rate_limited` | Rate limit exceeded | ## Related Endpoints * [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark): single trademark lookup * [Search Trademarks](/api-reference/trademarks/search-trademarks): filtered listing with pagination # Trademark Documents Source: https://docs.signa.so/api-reference/trademarks/records/documents GET /v1/trademarks/{id}/documents Office documents for a mark (office actions, certificates, filed forms, correspondence) with lazy first-request metadata fetch and a streaming file proxy ## When to use this If you run docketing or an IPMS product, you need office documents the day they issue: the non-final office action a client must answer, the registration certificate, the filed forms in the prosecution history. This endpoint returns that document list for a mark, and each row carries a `url` that streams the actual file. USPTO documents are fetched from TSDR on demand the first time you ask, so you get fresh metadata without running your own TSDR poller or fighting its throttles. The list is `document`-type media only. Logos, drawings, and specimens live on the [`media[]`](/api-reference/trademarks/retrieve-trademark) array of the trademark record, not here. A `media[].url` on a trademark record points at the same proxy and behaves the same way: an unauthenticated `200` that serves the image bytes, cache-safe for 24 hours. Safe to drop straight into an ``. Prefer the URL as returned; if you need to build one, the SDK's `trademarks.media(id, mediaId)` constructs the same URL. ## Path Parameters Trademark ID (`tm_...`). ## Query Parameters Filter by one or more document kinds, comma-separated. Values: `office_action`, `certificate`, `correspondence`, `filed_form`, `other`. Example: `document_kind=office_action,certificate`. Return documents whose official date is on or after this date (`YYYY-MM-DD`). Return documents whose official date is strictly before this date (`YYYY-MM-DD`). Must be after `official_date_gte` when both are supplied. Page size, 1 to 100. Pagination cursor from a previous response's `pagination.cursor`. ## Response A standard list envelope with one extra field, `source_sync`, describing the freshness of this mark's document metadata. Always `list`. Freshness of the document metadata for this mark. One of: * `synced`: metadata is present and current. `data` reflects the stored documents. * `pending`: a lazy fetch is in flight (another request holds the fetch lock) or upstream is still settling. `data` may be empty or partial. Request again shortly. * `unsupported`: the mark belongs to an office without document support (anything other than USPTO today). `data` is always empty and no fetch is attempted. ISO 8601 timestamp of the last successful sync, or `null` if never synced. Array of trademark document objects, newest official date first. Media ID (`med_...`). Use it against the [media proxy](#downloading-document-files) to stream the file. Always `trademark_document`. One of `office_action`, `certificate`, `correspondence`, `filed_form`, `other`. Official document date (`YYYY-MM-DD`), or `null` when the office did not supply one. Human-readable label from the office (e.g. `Nonfinal Office Action`), or `null`. Content type of the file, typically `application/pdf`. Page count when known, otherwise `null`. Absolute media-proxy URL that streams the file bytes. See [Downloading document files](#downloading-document-files). Whether more pages exist after this one. Contains `cursor` (the token for the next page, or `null`). ### First call: metadata fetch in flight The first time you request documents for a USPTO mark that has never been synced, Signa fetches the metadata inline. If another request already holds the fetch lock, or the upstream fetch is still settling, you get `status: pending` with an empty `data` array. Request again in a moment. ```json First call (pending) theme={null} { "object": "list", "source_sync": { "status": "pending", "last_synced_at": null }, "data": [], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_01kx22n3rpj1e6cmnpfbf482k6" } ``` ### Second call: synced with a document row Once the fetch completes, the list is `synced` and each row carries its media-proxy `url`. ```json Second call (synced) theme={null} { "object": "list", "source_sync": { "status": "synced", "last_synced_at": "2026-07-09T04:00:00Z" }, "data": [ { "id": "med_019d2141-6ce9-771b-872e-bc8b20e49fcf", "object": "trademark_document", "document_kind": "office_action", "official_date": "2025-11-04", "description": "Nonfinal Office Action", "mime_type": "application/pdf", "page_count": 12, "url": "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/media/med_019d2141-6ce9-771b-872e-bc8b20e49fcf" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_01kx22p7c8a0d3fjm2q9x7t4bz" } ``` For a non-USPTO mark you always get `200` with an empty list and `source_sync.status` of `unsupported`. This endpoint never returns an error status for an office that lacks document support. ## Code Examples ```bash cURL theme={null} # First call kicks off the lazy fetch curl "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/documents" \ -H "Authorization: Bearer $SIGNA_API_KEY" # Filter to office actions issued in 2025 curl "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/documents?document_kind=office_action&official_date_gte=2025-01-01&official_date_lt=2026-01-01" \ -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 docs = await signa.trademarks.documents("tm_8kLm2nPq", { document_kind: "office_action", official_date_gte: "2025-01-01", official_date_lt: "2026-01-01", }); if (docs.source_sync?.status === "pending") { // metadata still settling; request again shortly } for (const doc of docs.data) { console.log(doc.document_kind, doc.official_date, doc.url); } ``` ## Downloading document files Each document row's `url` points at the media proxy, `GET /v1/trademarks/{id}/media/{mediaId}`. Following it streams the file bytes: * The response is the raw file with its real `Content-Type` (`application/pdf` for office actions and certificates). * `X-Content-Type-Options: nosniff` is set. The proxy never relabels a file: an image row stays an image content type. * The endpoint is **unauthenticated** so it works directly in `` and ``, but it is **IP rate-limited**, so treat it as a per-download link, not a bulk hose. For a USPTO document, the first download is a cold fetch: the proxy pulls the file from TSDR under a shared download budget and persists it. When that budget is momentarily exhausted, the proxy returns `502 upstream_error` with a `Retry-After: 60` header. Honor it and retry after the stated delay. Once persisted, subsequent downloads serve the stored bytes and do not touch TSDR. ```bash cURL theme={null} # Stream an office action PDF to disk curl -L "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/media/med_019d2141-6ce9-771b-872e-bc8b20e49fcf" \ -o office-action.pdf ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ----------------------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid trademark ID, bad `document_kind`, or `official_date_gte` not before `official_date_lt` | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Trademark does not exist | The media proxy returns `502 upstream_error` (with `Retry-After`) when the TSDR download budget is exhausted, and `404 not_found` when the media record or upstream file does not exist. ## Related Endpoints * [Build a docketing system](/guides/use-cases/build-a-docketing-system): the end-to-end recipe using this endpoint * [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark): the full record, including the `media[]` image array # Retrieve Trademark Source: https://docs.signa.so/api-reference/trademarks/retrieve-trademark GET /v1/trademarks/{id} Retrieve a single trademark by ID ## Overview Returns the full detail-tier representation of a single trademark: identification, status, filing and key dates, owners, attorneys, classifications, computed deadlines, media, Madrid territory coverage, mark-to-mark relationships, and source provenance. Everything bounded is inlined — including `coverage` (per-territory breakdown for a Madrid international registration), `relationships[]` (mark-to-mark links), and `provenance` (where the record came from and when we last saw it). Only the unbounded, independently filterable children stay behind their own endpoints: events, proceedings, assignments and documents. Events, proceedings and examiner citations appear here as counts (`events_count`, `proceedings_count`, `citations_count`). ## Path Parameters Trademark ID (e.g., `tm_8kLm2nPq`) ## Query Parameters Comma-separated optional detail projections. `office_extensions` includes the `office_extensions` object (raw office-specific data), which is otherwise omitted. Sparse top-level field projection, comma-separated (e.g. `?fields=mark_text,status,owners`). `id` and `object` are always retained. Unknown field names, or nested paths like `status.primary`, return `400`. Preferred goods/services language for `classifications[]` rows, applied only to records that carry per-language variants (direct and Madrid designation records; 2-3 letters with optional 2-letter region, e.g. `en`, `eng`, or `en-US`). When multiple language variants exist for the same Nice class, Signa returns the requested language if present; groups without the requested language — and IR-parent rollup classifications, which have no per-language variants — keep all rows in default classification order. The `ETag` is a weak semantic validator (`W/"…"`), because the response includes a per-request `request_id`. Return it unchanged in `If-None-Match`; matching `304` responses preserve the weak form. The validator varies with the requested projection and language: a request with `?include=`, `?fields=`, and/or `?language=` gets a different validator than the plain request, so a cached full response never `304`s a different representation. ## Response Returns the full detail-tier representation of a trademark, including all bounded children (owners, attorneys, classifications, coverage, relationships, etc.) inline. Unbounded children (events, proceedings) appear as counts only. Maintenance deadlines and opposition windows are computed at read time from the normalized record and rule registry, and served under `derived`. Prefixed trademark ID (`tm_*`) Always `"trademark"` Word mark as filed (null for design-only marks) ISO 639-1 language code of the mark text ISO 15924 script code Mark feature type (e.g., `word`, `figurative`, `combined`) Legal category (e.g., `standard`, `certification`, `collective`) Right kind (`trademark`, `trade_name`, `geographical_indication`, etc.) Whether the mark is a series mark Number of marks in the series Primary status: `active`, `pending`, `inactive`, `unknown` Lifecycle stage, one of 18 normalized values: `filed`, `examining`, `pending_publication`, `published`, `opposition_period`, `pending_opposition`, `pending_cancellation`, `pending_issuance`, `registered`, `allowed`, `abandoned`, `withdrawn`, `surrendered`, `refused`, `cancelled`, `invalidated`, `expired`, `unknown`. See the [status taxonomy](/guides/trademarks). Reason for current status (e.g., `section_8_accepted`) Active challenge states (e.g., `opposition_pending`, `cancellation_pending`) ISO date when this status became effective How the status was determined: `explicit` (the office reported it directly), `event_derived` (derived from the prosecution event timeline), `dispatch_derived` (derived from a bulk data dispatch), or `computed` (the mark spans a mixed-status family and Signa computed a representative status) Office-native status code (e.g., `800`) Office-native status label (e.g., `Registered and renewed`) Uppercase ST.3 office code (e.g., `US`, `EM`). Null if the stored office identifier cannot be resolved (fail-closed — internal codes are never leaked). ISO 2-letter jurisdiction (e.g., `US`, `EU`) Filing route (`direct_national`, `madrid_designation`, etc.) Scope kind (`national`, `regional`, `international_registration`) Origin office for Madrid designations Source system primary identifier Application/serial number Registration number International registration number (Madrid system) ISO date ISO date ISO date Basis for the expiry date: `reported` (the office reported it directly), `derived` (Signa computed it from other dates), or `unknown` ISO date Basis for the renewal due date: `reported` (the office stated it), `derived` (the connector computed it, e.g. from the expiry date), or `unknown` (the connector has not declared a basis yet). Same vocabulary as `expiry_date_basis`. First publication date (ISO) Earliest priority date (ISO), derived from priority claims ISO date ISO date ISO date (Madrid designations) ISO date (Madrid dependency period) ISO date Whether the record has been retracted by the source office Owner ID (`own_*`) Canonical owner name ISO 2-letter country code Entity type (e.g., `corporation`, `individual`) Owner role (e.g., `owner`, `applicant`) Resolved cross-office entity ID (`ent_*`). Group an owner's marks across offices by this, and filter with `?entity_id=` to pull the entity's whole global portfolio. `resolved` when the owner is linked to a materialized entity; `derived` when it is not linked yet (a stable placeholder ID scoped to this owner). Public company records matched to this owner (`source` `sec` or `gleif`, plus `ticker`, `exchange`, `lei`, `entity_status`). Present only on grouped Madrid IR-parent detail owners; omitted on every other detail response, and omitted when there are no matches. Postal address as reported by the office. All fields optional; whole object is `null` when no address is on file. Street address lines City State / region / province Postal or ZIP code ISO 2-letter country code Attorney ID (`att_*`) Canonical attorney name Firm ID (`firm_*`) Firm name Attorney role (e.g., `representative`) Postal address as reported by the office. All fields optional; whole object is `null` when no address is on file. Same shape as the owner `address` object. Nice class number (1-45) Nice classification edition Goods/services description Language of the description Class status Raw class status from the office Office-native subclass codes, or `null` when not reported Compact list of the distinct Nice class numbers in `classifications[]` Everything Signa computes from the record, namespaced apart from what the office reported. Computed at read time from the normalized record and the rule registry. Free on every read. The `derived` block is **clock-free**: it carries statutory dates, never countdowns or "today"-relative statuses. That keeps a cached response a faithful function of the record: a `status` or `days_until_due` baked into a cacheable body goes stale silently. Derive them client-side from the dates (one line of date math), or call [Compute Deadlines](/api-reference/reference/compute-deadlines) with an explicit `as_of_date` when you want the server to evaluate statuses for you. Corpus-level stamp of the deadline ruleset that produced this block: the latest `last_verified` date across the maintenance rule configurations, an attestation date that moves on any re-verification (including provenance-only re-citations), not a change signal. It moves only when a re-verification advances the newest date in the corpus, so re-verifying a jurisdiction that is already behind the current maximum leaves it unchanged. Treat it as a coarse "how fresh is the corpus" signal, not per-rule provenance. For that, read `rule_id` on each row and resolve it against [List Deadline Rules](/api-reference/reference/deadline-rules), where the rule's own `last_verified` and `effective_from` live. `false` when no schedule could be computed. See `deadlines_unsupported_reason`. Why no schedule was computed: `unsupported_jurisdiction` (no rules modeled for the jurisdiction) or `requires_office_date` (the record does not carry the input the statute needs and the office stated no expiry date to anchor on, so the engine declines to guess rather than serve a wrong date). `null` when supported. Computed maintenance deadlines from the jurisdiction's statutory rules and the mark's lifecycle dates. Rules are authored to be non-overlapping per jurisdiction. First-class API fields, not scraped office text; they supplement office-reported dates such as `renewal_due_date`. Empty for marks with nothing left to maintain (abandoned, withdrawn, surrendered, invalidated, refused); for savable-dead marks (expired, cancelled) only the still-actionable rows appear, such as an open late-renewal window or a restoration window. Stable, permanent slug of the statutory rule that produced this row, e.g. `us_declaration_of_use_s8`. This is the row's identity and the join key to [List Deadline Rules](/api-reference/reference/deadline-rules), where the statutory citations, `last_verified` and `effective_from` live. Treat it as **opaque**: do not parse it. It is deliberately not a prefixed entity ID (`tm_`, `own_`) because it names a statutory rule, not a stored entity. Deadline type (e.g., `renewal`, `declaration_of_use`, `restoration`) Human-readable deadline name, for display. Not an identifier: use `rule_id` to identify the rule. Jurisdiction this deadline applies to The record date the computation was anchored on Which date that was: `filing_date`, `registration_date`, `grant_date`, `protection_grant_date`, `intl_registration_date`, `reported_expiry` when the ladder is anchored on the office-stated expiry, or `reported_renewal_due` when it is anchored on an office-stated renewal-due date and no expiry is stored ISO date when the filing window opens ISO date when the deadline is due ISO date when the grace period ends Whether this deadline repeats on a cycle `true` for filings that are advantageous but not required (e.g. US §15 incontestability) What happens if this deadline is missed. One of `expiration` (the right ends by operation of law when the term plus any grace runs out), `removal_from_register` (the office strikes the entry as the statutory consequence), `cancellation` (the office cancels a still-running registration for a missed maintenance filing, US §8/§71), `cancellation_and_expiration` (combined US §8+§9), `expungement` (Canada's statutory word), `lapse_of_international_registration` (Madrid), `permanent_loss` (restoration rules: the last remedy has passed) or `none` (optional filing). Whether a remedy exists afterwards is said by the presence of a `restoration` rule, not by this value. See the [deadline rules guide](/guides/deadline-rules#consequence-vocabulary). Computed opposition period for offices and routes covered by Signa's opposition rule registry: `rule_id`, `trigger_event`, `publication_date`, `window_opens`, `window_closes`, `close_adjustment`, `office_time_zone`, `rule_version`, `common_extension` and `reason`. `close_adjustment` (`moved` | `unchanged` | `not_checked`) qualifies `window_closes`: it says whether the close was checked against the office's holiday calendar and whether that check moved it. `not_checked` means no calendar stands behind the date — it falls outside the calendar's pinned coverage years, the rule carries no calendar, or there is no close date at all — and because a rollover only ever moves a close **later**, such a date is correct-or-early. This block is clock-free, so you derive open/closed yourself: treat a `not_checked` close as indeterminate for the 20 days after it. `rule_id` is the stable, opaque slug of the opposition rule used, and the join key to [List Opposition Rules](/api-reference/reference/opposition-rules). `null` means the office/route is not modeled yet or the rule engine intentionally fell back; an object whose `window_opens` and `window_closes` are both `null` means the office/route is modeled but no dates could be computed. `reason` names why in that case, as a snake\_case code, and is `null` on every normal window. One value is emitted today: `publication_date_kind_mismatch` — the record's publication-date provenance is not the publication the matched Madrid rule runs from (see [Opposition windows](/guides/monitoring/opposition-windows#madrid-designations-which-publication-starts-the-clock)). A record that simply has no publication date leaves `reason` `null`; the null `publication_date` already says that. Treat the vocabulary as open: branch on the codes you know and fall through on the rest. Vienna classification design codes Alternative text representations of the mark Disclaimers, descriptions of mark, etc. Media files (images, sounds). Each entry carries `id` (`med_*`), `media_type`, `mime_type`, `url`, and `is_primary`. `media[].url` (and `primary_image_url`) is an **unauthenticated** proxy that serves the image bytes directly: a `200` response with the image, not a redirect. It follows any upstream office redirect server-side, so there is nothing for your client to resolve. It is cache-safe for 24 hours (`Cache-Control: public, max-age=86400, immutable`) and sets cross-origin headers, so it is safe to drop straight into an `` — browsers cannot attach an `Authorization` header. Use the URL exactly as returned; do not construct it yourself. Convention priority claims Filing bases (US-specific: use in commerce, intent to use, etc.) Publication history (gazette/journal entries) Office-specific data not captured by the standard schema. Omitted by default; returned only when `?include=office_extensions` is requested. USPTO register data promoted from `office_extensions`; absent for non-USPTO records or when register data is unavailable EUIPO seniority claims promoted from trademark relationships. `country_code` is uppercased, `kind` is `national` or `international` when EUIPO provides a known raw kind, and `status` uses lowercased EUIPO vocabulary: `claimed`, `accepted`, `refused`, `partially_refused`, `abandoned`, or `deficiency`. Empty array when none exist. Per-territory coverage for a grouped Madrid international registration: the same reconciled rollup the grouped search row carries, plus the per-designation detail the search row omits. `null` on individual designation legs and on direct or regional marks, which cover exactly one territory. Number of designated territories Territory counts keyed by primary status (`active`, `pending`, `inactive`, `unknown`) Territory counts keyed by lifecycle stage Designated territory code (ISO 2-letter; WIPO's `EM` is rendered `EU`) Display name of the territory The designation record covering this territory (`tm_*`). Always present and always resolvable via [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark). Primary status in this territory Lifecycle stage in this territory Office-reported protection status (e.g. `protected`, `refused`) ISO date the territory was designated ISO date protection took effect ISO date of a provisional or final refusal ISO date the current territory status took effect Whether the territory was added after the initial international registration Treaty the designation was made under (e.g. `madrid_protocol`) Reconciliation alternates for a dual-feed territory — the WIPO-feed record folded into the winning national-feed record, or vice versa. Empty for a single-feed territory. Each entry carries `source_office_code`, `territory_code`, and an optional `trademark` reference. Mark-to-mark links. Madrid designations are **not** repeated here (they are `coverage.territories[]`), and neither are seniority claims (they are `seniority_claims[]`). Party history (previous owners, representatives, and addresses) is not a mark-to-mark link and is not surfaced. Capped at 200 entries per mark. One of `based_on_basic_application`, `based_on_basic_registration`, `divisional_parent`, `partial_assignment_from`, `merged_into`, `transformed_from`, `replaces_under_4bis`, `partial_transfer_of`, `continuation_of`. `child` when this mark is the source of the relationship, `parent` when it is the target The counterpart record (`tm_*`), or `null` when the counterpart is not a record Signa holds — most `based_on_basic_*` edges are cross-office and unresolved. Identify those by `target_office_code` + `target_record_id`. ST.3 office code of the counterpart as reported by the office. Null on `parent` edges (the counterpart is identified by `trademark_id`). Office-native identifier of the counterpart (application or registration number). Null on `parent` edges (the counterpart is identified by `trademark_id`). ST.3 office code of the counterpart — the resolved record's office when `trademark_id` is set, otherwise the office-reported `target_office_code`. Mark text of the resolved counterpart record Lifecycle stage of the resolved counterpart record ISO date the relationship took effect, when the office reports one Where this record came from and when Signa last saw it. See [Data Freshness & Coverage](/guides/data-freshness) for how often each office updates. Date of the office data file or response this record was built from The office's own last-modified timestamp for the record, when it publishes one When Signa last fetched the full detail record from the office Whether the mark has any media files Media-proxy URL for the mark's primary image; `null` when the mark has no media. Same unauthenticated image proxy as `media[].url` — a `200` with the image bytes. Total number of events (fetch via [Trademark Events](/api-reference/trademarks/trademark-events)) Total number of proceedings (fetch via [Trademark Proceedings](/api-reference/trademarks/trademark-proceedings)) Examiner refusal citations issued against this mark's application (fetch via [Trademark Citations](/api-reference/trademarks/trademark-citations)). `0` means the office is covered and this mark has no citations. `null` means citations are not counted for this office at all, which is not the same as "no citations exist". Check `capabilities.citations` on [List Offices](/api-reference/reference/list-offices) to tell which offices return a number (`available`) and which return `null`. A `null` count is also not a promise of an empty list: while a new office is being brought online, the citations endpoint can already return rows for a mark whose count is still `null`. ISO timestamp ISO timestamp Unique request identifier ```json Response theme={null} { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "NIKE", "mark_text_language": "en", "mark_text_script": null, "mark_feature_type": "word", "mark_legal_category": "standard", "right_kind": "trademark", "is_series_mark": null, "series_count": null, "status": { "primary": "active", "stage": "registered", "reason": null, "challenges": [], "effective_date": "1974-04-16", "source": "explicit", "raw_code": "800", "raw_label": null }, "office_code": "US", "jurisdiction_code": "US", "filing_route": "direct_national", "scope_kind": "national", "origin_office_code": null, "source_primary_id": "72396458", "application_number": "72396458", "registration_number": "0978952", "ir_number": null, "filing_date": "1971-02-04", "registration_date": "1974-04-16", "expiry_date": "2034-04-16", "expiry_date_basis": "derived", "renewal_due_date": "2034-04-16", "renewal_due_date_basis": "derived", "publication_date": "1974-01-29", "priority_date": null, "termination_date": null, "protection_effective_date": null, "designation_date": null, "dependency_period_end_date": null, "transformation_deadline_date": null, "is_retracted": false, "owners": [ { "id": "own_R3jK9mN2", "name": "Nike, Inc.", "country_code": "US", "entity_type": "corporation", "role": "owner", "entity_id": "ent_019f34d6-2222-7777-8888-222222222222", "entity_id_type": "resolved", "address": { "lines": ["One Bowerman Drive"], "city": "BEAVERTON", "state": "OR", "postal_code": "97005", "country_code": "US" } } ], "attorneys": [ { "id": "att_Lp3mN7qR", "name": "Katelyn Andrews", "firm_id": "firm_Xk9pQ2rS", "firm_name": "Banner & Witcoff, Ltd.", "role": "representative", "address": { "lines": ["1100 13th Street NW", "Suite 1200"], "city": "WASHINGTON", "state": "DC", "postal_code": "20005", "country_code": "US" } } ], "classifications": [ { "nice_class": 25, "nice_edition": null, "goods_services_text": "Clothing, footwear, headgear", "goods_services_language": "en", "status": "accepted", "class_status_raw": "6", "subclass_codes": null } ], "nice_classes": [25], "design_codes": [], "text_variants": [], "statements": [], "media": [ { "id": "med_9mQ4pXk2", "media_type": "image", "mime_type": "image/png", "url": "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/media/med_9mQ4pXk2", "is_primary": true } ], "priority_claims": [], "filing_bases": [ { "basis_type": "use_in_commerce", "nice_class_number": 25, "first_use_date": "1971-01-01", "first_use_in_commerce_date": "1972-01-01", "foreign_application_number": null, "foreign_country_code": null } ], "publications": [], "us_register": { "register": "principal", "acquired_distinctiveness": null, "incontestable": false, "standard_characters_claimed": false }, "seniority_claims": [], "derived": { "ruleset_version": "2026-09-02", "deadlines_supported": true, "deadlines_unsupported_reason": null, "deadlines": [ { "rule_id": "us_renewal_s9", "type": "combined_renewal_and_use", "name": "Section 8+9 — Renewal + Declaration of Use", "jurisdiction_code": "US", "trigger_date": "2024-04-16", "trigger_field": "registration_date", "window_opens": "2033-04-16", "due_date": "2034-04-17", "grace_expiry": "2034-10-16", "recurring": true, "optional": false, "consequence_if_missed": "cancellation_and_expiration" } ], "opposition_window": null }, "has_media": true, "primary_image_url": "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/media/med_9mQ4pXk2", "coverage": null, "relationships": [], "events_count": 21, "proceedings_count": 0, "citations_count": 2, "provenance": { "source_data_date": "2026-03-18", "office_updated_at": "2022-10-28T00:00:00.000Z", "retrieved_at": null }, "created_at": "2026-03-22T08:27:23.446Z", "updated_at": "2026-03-30T02:13:13.704Z", "request_id": "req_01kp4v2yycy3pq3czhxcsmn9md" } ``` ```json Madrid IR parent (abridged) theme={null} { "id": "tm_4pQ7rT2v", "object": "trademark", "mark_text": "MERIDIAN", "office_code": "WO", "jurisdiction_code": "WO", "filing_route": "madrid_ir", "scope_kind": "international_registration", "ir_number": "1654321", "origin_office_code": "US", "coverage": { "territory_count": 3, "by_primary": { "active": 2, "pending": 1 }, "by_stage": { "registered": 2, "published": 1 }, "territories": [ { "territory": "EU", "territory_name": "European Union", "trademark_id": "tm_9nR4sV8w", "primary": "active", "stage": "registered", "protection_status": "protected", "designation_date": "2022-04-11", "protection_date": "2023-02-27", "refusal_date": null, "status_effective_date": "2023-02-27", "is_subsequent_designation": false, "designation_protocol": "madrid_protocol", "source_records": [] }, { "territory": "JP", "territory_name": "Japan", "trademark_id": "tm_2wX5yZ1a", "primary": "active", "stage": "registered", "protection_status": "protected", "designation_date": "2022-04-11", "protection_date": "2023-06-09", "refusal_date": "2022-11-02", "status_effective_date": "2023-06-09", "is_subsequent_designation": false, "designation_protocol": "madrid_protocol", "source_records": [ { "source_office_code": "WO", "territory_code": "JP", "trademark": { "id": "tm_6bC3dE9f", "href": "/v1/trademarks/tm_6bC3dE9f" } } ] }, { "territory": "CA", "territory_name": "Canada", "trademark_id": "tm_8gH1jK5l", "primary": "pending", "stage": "published", "protection_status": null, "designation_date": "2024-01-30", "protection_date": null, "refusal_date": null, "status_effective_date": "2024-08-14", "is_subsequent_designation": true, "designation_protocol": "madrid_protocol", "source_records": [] } ] }, "relationships": [ { "type": "based_on_basic_registration", "direction": "child", "trademark_id": null, "target_office_code": "US", "target_record_id": "97123456", "office_code": null, "mark_text": null, "status_stage": null, "effective_date": "2022-04-11" } ], "provenance": { "source_data_date": "2026-03-18", "office_updated_at": "2026-03-17T00:00:00.000Z", "retrieved_at": "2026-03-18T04:12:55.019Z" }, "request_id": "req_01kp4v2yycy3pq3czhxcsmn9me" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/trademarks/tm_8kLm2nPq" \ -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 trademark = await signa.trademarks.retrieve("tm_8kLm2nPq"); console.log(trademark.mark_text, trademark.status.stage); ``` ## Errors | Status | Type | When | | ------ | ------------------ | ----------------------------------------- | | 400 | `validation_error` | Invalid trademark ID format | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Trademark ID does not exist | | 429 | `rate_limited` | Rate limit exceeded | ## Related Endpoints * [Search Trademarks](/api-reference/trademarks/search-trademarks): filtered search and browsing * [Batch Retrieve](/api-reference/trademarks/batch-trademarks): hydrate multiple known IDs in one request * [Trademark Events](/api-reference/trademarks/trademark-events): prosecution event timeline * [Trademark Proceedings](/api-reference/trademarks/trademark-proceedings): oppositions, cancellations, and other contested proceedings * [Trademark Citations](/api-reference/trademarks/trademark-citations): prior marks an examiner cited against this application * [Trademark Cited By](/api-reference/trademarks/trademark-cited-by): applications this mark was cited against # Search Trademarks Source: https://docs.signa.so/api-reference/trademarks/search-trademarks GET /v1/trademarks Search, filter, and browse trademarks across all supported offices ## Overview The canonical endpoint for discovering trademarks. Pass a text query to search by brand name with relevance ranking, or use filters to browse by office, status, class, date, and more. Every request must include at least one filter or a text query (`q`). ## Query Parameters Search query text (1-500 characters). When provided, results are ranked by relevance using multi-strategy matching (exact + fuzzy by default). Optional: omit for filter-only browsing. Search strategies to apply when `q` is provided, comma-separated. Any combination of `exact`, `fuzzy`, `phonetic`, `prefix`. Defaults to `exact,fuzzy` for fast results. Use `exact,phonetic,fuzzy,prefix` for comprehensive trademark clearance searches. Only applies to the default `similar` match mode. How `q` is matched against the mark text. `similar` (default) runs the ranked relevance ladder. The deterministic modes — `exact`, `starts_with`, `ends_with`, `contains` — match the folded (case- and accent-insensitive) mark text literally and return results sorted by date, with `relevance_score: null`. See [Match modes](#match-modes). Exclude marks whose folded text contains this substring (case- and accent-insensitive). Composable with any `match` mode. Results per page (1-100). Opaque pagination cursor from a previous response. When `true`, includes `pagination.total_count` in the response. Exact up to 10,000 matches; beyond that the count is capped and `pagination.total_count_approximate` is `true`. See [Pagination](/api-reference/pagination#total-count). When `true`, includes highlight snippets for `mark_text` and `owner_names` fields under the default `similar` match mode. In deterministic match modes (`exact`, `starts_with`, `ends_with`, `contains`), `highlights` is inert. How Madrid International Registrations are presented. `grouped` (default) returns one row per mark/IR, with per-territory coverage folded into that row. `expanded` returns one row per Madrid designation instead. A request that combines `expanded` with something the grouped index can't honor falls back to `grouped` automatically; when that happens, `search_meta.fallback_reason` explains why. Comma-separated optional row projections. `full_goods_services` returns full `classifications[].goods_services_text` instead of the truncated (about 280 character) summary text. Sparse top-level field projection, comma-separated (e.g. `?fields=mark_text,status,owners`). `id` and `object` are always retained. Unknown field names, or nested paths like `status.primary`, return `400`. ### Filters All filter parameters are accepted at the top level of the query string. Arrays use comma-separated values. Date ranges support `_gte`, `_gt`, `_lte`, and `_lt` suffixes. Uppercase ST.3 office codes, comma-separated (e.g. `?offices=US,EM`). Legacy lowercase codes (e.g. `uspto`, and `eu` for EUIPO) are accepted as permanent aliases. Jurisdiction codes, comma-separated (e.g. `?jurisdictions=US,EU`), selecting **rights that protect, or seek protection, in these territories**. By default this is protection-scope: a request for a country also matches regional rights whose membership covers it, so `jurisdictions=FR` returns French national marks, Madrid designations of France, **and** EU trade marks (a EUTM protects France). Requesting a regional code (`EU`) matches the regional rights themselves (EUTMs and IRs designating the EUIPO), not member-state national marks. Use `territory_match=direct` to restore literal territory-leg matching. This is distinct from `offices`, which filters on the register a mark was filed with. How `jurisdictions` matches. `protection` (default) applies protection-scope: a country request also matches regional rights whose membership covers it (a EUTM for `jurisdictions=FR`). `direct` matches only literal territory legs (national filings and Madrid designations of the exact territory), reproducing the pre-July-2026 behavior. Inert when no `jurisdictions` filter is present. Also accepted by [internal Suggest](/api-reference/trademarks/trademark-suggest) and [Image search](/api-reference/trademarks/trademark-image-search). Nice classification numbers 1-45, comma-separated (e.g. `?nice_classes=9,42`). Not sure which classes to filter on? [Suggest Classifications](/api-reference/reference/suggest-classifications) turns a plain-English description into ranked classes. USPTO design search codes, comma-separated. Namespaced filing basis codes, comma-separated (e.g. `US:1A,US:44E`). USPTO register type: `principal` or `supplemental`. Primary status: `active`, `pending`, `inactive`, `unknown`. Comma-separated for multiple. Status stage, comma-separated (e.g. `registered`, `published`, `examining`). Derived opposition window state: `open`, `not_started`, `closed`, or `unknown`. Rows whose indexed close date was never checked against an office holiday calendar (`opposition_window.close_adjustment` = `not_checked`) are **indeterminate** for the 20 days after that date, because a rollover can only push a close later: they are counted as `open` and excluded from `closed`, so a live deadline is never hidden. `not_started` and `unknown` are unaffected, because the opening date is never rolled. Opposition window close-date lower bound (YYYY-MM-DD). Opposition window close-date upper bound (YYYY-MM-DD). Seniority claim state: `claimed`, `none`, or `unknown`. Mark type: `word`, `figurative`, `combined`, `three_dimensional`. Filing route: `direct_national`, `madrid_designation`, `direct_regional`. Owner ID (`own_...`). Marks for that single per-office owner record. Owner name substring match. Resolved entity ID (`ent_...`). Returns marks across **all member owners** of the entity (every office): the global-portfolio filter. Accepts an entity id derived for an owner that hasn't been linked to a cross-office entity yet. An entity resolving to more than 10,000 member owners returns `422 entity_too_large`. See [Entities](/api-reference/parties/search-entities). Entity GROUP ID (`ent_...`). Returns marks across the whole GLEIF corporate family (root + all descendants): "all Pfizer-group marks". Group-level, never identity. Also bounded by `422 entity_too_large` (see [Errors](/api-reference/errors)). `true` to return marks whose owner has an active listing association: a confirmed active SEC ticker match, OR an owner whose resolved entity is itself listed or a subsidiary of a listed company. `false` means no confirmed listing, not confirmed private. `true` to return marks whose owner has a confirmed GLEIF LEI match. `false` means no confirmed LEI match. Exact owner ticker match, uppercased server-side (e.g. `AAPL`). Ticker matching is subsidiary-inclusive: `owner_ticker=NKE` returns Nike's own marks **and** those of its subsidiaries (for example Converse marks under Nike), because each owner's entity listing ticker, whether direct or inherited from a listed ancestor, folds into this field. Direct-vs-inherited provenance is not exposed in search; it lives on the entity `listing` block (see [Retrieve Entity](/api-reference/parties/retrieve-entity)). Exact owner LEI match, uppercased server-side. Attorney ID (`att_...`). Firm ID (`firm_...`). Filing date lower bound (YYYY-MM-DD). Filing date upper bound (exclusive). Registration date lower bound. Registration date upper bound. Expiry date lower bound. Expiry date upper bound. `true` to require at least one image. `true` to restrict to Madrid Protocol filings. ### Per-filter office support Filter completeness varies by trademark office because each source publishes different fields. Use [List Offices](/api-reference/reference/list-offices) as the source of truth: each office returns a live `coverage` block with percentages for fields that drive filters and projections, including media, goods/services text, design codes, publication dates, registration data, priority and seniority claims, filing basis, first-use dates, owner links, attorney links, and status effective dates. `opposition_status` and `opposition_closes_*` derive from indexed opposition-window dates. `opposition_status` uses a UTC calendar day for the open/closed boundary, so within about one day of a window edge it can differ slightly from the office-timezone-precise dates in the `opposition_window` response field. Use those dates for exact timing. WIPO Madrid IR publication dates are not yet covered, so IR opposition windows are treated as unknown and the response includes `search_meta.warnings[]` with code `partial_opposition_coverage`. A record with an open-ended opposition window (`window_opens` set but `window_closes` absent) is not currently assigned to the `open`, `not_started`, or `closed` buckets. The `close_adjustment` marker that drives the indeterminate band is **populated at index time**, so records indexed before it shipped carry no value and are bucketed by their dates alone, exactly as before; a reindex is what applies the band to them. `seniority_claims` is currently EUIPO-only with low coverage and always emits `partial_seniority_coverage` when used. `claimed` matches records with known seniority claims, `none` matches EUIPO records that authoritatively report no seniority claims, and `unknown` matches records with no seniority signal. Grouped-grain rows currently surface `design_codes` for `vienna` and `us_design_search` systems only. Design codes with `system='other'` are omitted at mark grain until a reindex follow-up promotes them. DB-backed embedded trademark rows, such as portfolio fallback rows that have not been hydrated from search, emit `priority_date: null` and `design_codes: []`. These are search-hydrated projections. | Parameter | Type | Description | | -------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `office` | string | Single office code shortcut | | `vienna_codes` | string | Vienna figurative codes, comma-separated | | `status_reason` | string | Status reason codes, comma-separated | | `challenge_states` | string | Active challenge states, comma-separated | | `mark_legal_category` | string | `standard`, `certification`, `collective` | | `right_kind` | string | Right kind (e.g. `trademark`) | | `scope_kind` | string | Territorial scope kind, comma-separated | | `owner_country` | string | Two-letter owner country code | | `goods_services_text` | string | Free-text search in G\&S descriptions | | `application_number` | string | Exact application number (requires `office`) | | `registration_number` | string | Exact registration number (requires `office`) | | `ir_number` | string | Madrid International Registration number | | `origin_office_code` | string | Origin office code for Madrid filings | | `renewal_due_date_gte` | string | Renewal due date lower bound | | `renewal_due_date_lt` | string | Renewal due date upper bound | | `publication_date_gte` | string | Publication date lower bound | | `publication_date_lt` | string | Publication date upper bound | | `status_effective_date_gte` | string | Status effective date lower bound | | `status_effective_date_lt` | string | Status effective date upper bound | | `priority_date_gte` | string | Priority date lower bound | | `priority_date_lt` | string | Priority date upper bound | | `first_use_anywhere_date_gte` | string | First use anywhere date lower bound | | `first_use_anywhere_date_lt` | string | First use anywhere date upper bound | | `first_use_in_commerce_date_gte` | string | First use in commerce date lower bound | | `first_use_in_commerce_date_lt` | string | First use in commerce date upper bound | | `termination_date_gte` | string | Termination date lower bound | | `termination_date_lt` | string | Termination date upper bound | | `updated_at_gte` | string | Record updated-at lower bound. Accepts `YYYY-MM-DD` (coerced to start of day UTC) or full ISO 8601 datetime (e.g. `2024-01-15T00:00:00Z`). | | `updated_at_lt` | string | Record updated-at upper bound. Accepts `YYYY-MM-DD` or full ISO 8601 datetime. | | `has_proceedings` | boolean | `true` to require at least one proceeding | | `is_retracted` | boolean | `true` to restrict to retracted marks | | `is_series_mark` | boolean | `true` to restrict to series marks | | `renewal_due_before` | string | Alias for `renewal_due_date_lt` | ## Match modes The `match` parameter controls how `q` is matched against the mark text. * **`similar`** (default) — ranked, fuzzy matching. Runs the relevance ladder (tune it with `strategies` / `ranking_profile`) and orders results by `relevance_score`. Use this for brand searches and clearance. * **`exact`**, **`starts_with`**, **`ends_with`**, **`contains`** — deterministic. Each matches the *folded* mark text (case- and accent-insensitive) literally: `exact` requires the whole mark to equal `q`, the others anchor to the start, end, or anywhere in the mark. Results are ordered by date (not relevance), every row has `relevance_score: null`, `highlights` is inert, and `search_meta.strategies_used` is `[]`. Use these when you need predictable, reproducible matching rather than ranking. The deterministic modes accept a single-character `q` (one non-empty folded char), whereas `similar` requires at least 2 folded chars and `contains` requires at least 3. They also treat the metacharacters `( ) [ ] { } | \` as **literal** text to match, so a query like `mercedes (benz)` searches for that exact string; the ranked `similar` path rejects those same characters with a `400 validation_error` (use a deterministic mode to search for them literally). `search_meta.match` echoes the mode that was applied on every response (including `similar`). **`mark_text_not_contains`** excludes any mark whose folded text contains the given substring. It composes with every match mode (including `similar`), so you can, for example, search for `sun` while excluding `sunset`. ### Worked examples ```bash cURL — contains theme={null} # Every mark whose folded text contains "cola" curl -G "https://api.signa.so/v1/trademarks" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=cola" \ --data-urlencode "match=contains" ``` ```bash cURL — exclude a substring theme={null} # Ranked "sun" search, minus anything containing "sunset" curl -G "https://api.signa.so/v1/trademarks" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=sun" \ --data-urlencode "mark_text_not_contains=sunset" ``` ### Validation rules | Rule | Result if violated | | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | Deterministic modes (`exact`/`starts_with`/`ends_with`/`contains`) require a `q` | `400 validation_error` | | Deterministic `q` must be non-empty after folding (so combining-marks-only input is rejected), but a single folded character is accepted | `400 validation_error` | | `similar` (default) requires a folded `q` of at least 2 characters | `400 validation_error` | | `contains` requires a folded `q` of at least 3 characters | `400 validation_error` | | `strategies` and `ranking_profile` are only valid with `match=similar` | `400 validation_error` | | `mark_text_not_contains` requires at least 3 folded characters (same floor as `contains`, since it builds the same substring wildcard) | `400 validation_error` | | `match` must be one of the five allowed values; `mark_text_not_contains` must be a string | `400 validation_error` | ## Response Trademark summary records: the slimmer list shape with the fields most useful for result cards. See [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark) for the full record shape returned by single-record lookups. Whether more results are available. Pass this as `?cursor=` to get the next page. Total matches. Only present when `include_total=true`. **Canonical location**: read this field across every list endpoint. Emitted alongside `total_count`. `false` when the count is exact (up to 10,000 matches); `true` when the search index capped the count at 10,000 for a deep search. Query metadata. Unique search identifier. The query text used. Search strategies that were applied. Empty `[]` for deterministic `match` modes. The [match mode](#match-modes) that was applied: `similar`, `exact`, `starts_with`, `ends_with`, or `contains`. Echoed on every response. The Madrid presentation mode actually served: `grouped` or `expanded`. See the `international_registrations` query parameter above. The `territory_match` mode actually applied: `protection` (default) or `direct`. Echoed on every response so you can confirm which matching semantics ran. Present only when the requested `international_registrations` mode couldn't be honored and the response fell back to `grouped`. Non-fatal warnings about the search. Two families share this array: **strategy-skip** warnings (a requested `strategies` value produced no clauses for the query shape, e.g. `strategies=[phonetic]` on a query too short or high-collision — carries `strategy`) and **filter-coverage** warnings (an applied filter has partial index coverage — carries `severity`, `affected_filter`, `affected_offices`, `behavior`). Filter-coverage warnings are currently emitted for `opposition_status` / `opposition_closes_*` (`partial_opposition_coverage`) and `seniority_claims` (`partial_seniority_coverage`). Stable warning code for client handling. Human-readable warning message. Present on strategy-skip warnings: the requested strategy that produced no clauses. `info` or `warning`. Present on filter-coverage warnings. Filter whose behavior is qualified by the warning. Present on filter-coverage warnings. Office codes affected by the caveat. Human-readable behavior explanation. Present on filter-coverage warnings. Query execution time in milliseconds. Faceted bucket counts. Present on `GET` when `aggregations=` is provided and on `POST` when `options.aggregations` is provided. Human-readable labels keyed by the public ids in `owner_id`, `attorney_id`, `firm_id`, and `entity_id` aggregation buckets. Omitted when no labels resolve. Normalized relevance score for the ranked `similar` mode. Present when `q` is provided; `null` for filter-only queries, when `sort` is specified, and for every row under a deterministic `match` mode (`exact`/`starts_with`/`ends_with`/`contains`). Explains relevance scoring. Omitted when `sort` is specified or under a deterministic `match` mode. Array of strategy names that matched (e.g. `["exact", "fuzzy"]`). Array of `{factor, weight}` objects describing score adjustments. URL to the primary trademark image. Format: `https://api.signa.so/v1/trademarks/{id}/media/{media_id}`. Only present when `has_media` is true, `null` otherwise. Office-reported date when the current status took effect, when available. First publication date for the mark, used as the opposition-window trigger when the office and filing route are modeled. Date the right terminated or ceased, when the office reports it. Earliest priority date projected from priority claims, when available. On DB-backed embedded rows such as portfolio marks not yet in the search index, this search-hydrated-only child-table projection may be `null`. The source office's own record timestamp. This is distinct from `updated_at`, which is Signa's sync/index timestamp. Design or figurative classification codes. Empty `[]` when none are present. On DB-backed embedded rows such as portfolio marks not yet in the search index, this search-hydrated-only child-table projection may be `[]`. Code system, such as `vienna` or `us_design_search`. Office/system code value. Computed opposition window for modeled office/route rows. `null` means the office/route is not modeled, the window could not be computed safely, or the grouped row spans multiple territories. Dates only, so a cached row stays a faithful function of the record. Compare them against your own "today", or use the `opposition_status` filter to have the server do it. Office-local opening date, or `null` when the office/route is modeled but no publication date is available. Office-local closing date, or `null` when the office/route is modeled but no publication date is available. Read it with `close_adjustment`. Whether `window_closes` was checked against the office's holiday calendar, and whether that check moved it: `moved`, `unchanged`, or `not_checked`. `not_checked` means no calendar stands behind the date: it falls outside the calendar's pinned coverage years, the rule carries no calendar, or there is no publication date. A rollover only ever moves a close **later**, so a `not_checked` date is correct-or-early; treat it as indeterminate for the 20 days after it rather than as a decided open/closed answer. Nice classifications on the row. Nice class number (1-45). Goods/services description, truncated to about 280 characters unless `include=full_goods_services` is requested. `true` when `goods_services_text` was shortened for this row. Request `include=full_goods_services` or fetch the detail record for the untruncated text. `as_filed` on grouped IR rows, where classifications reflect the filed goods and services rather than a territory-effective scope. Absent on non-IR and leg rows. Compact Nice class numbers derived from `classifications[].nice_class`. Summary-tier owner projections. Always present as an array (empty `[]` when the record has no owners on file). Owner ID (`own_*`). Owner display name. Owner country code when available. Resolved cross-office entity ID (`ent_*`) when available, so you can group owners by entity. Filter with `?entity_id=` to retrieve the entity's whole global portfolio. `resolved` when the owner has been linked to a cross-office entity; `derived` when it has not been linked yet (a stable placeholder ID scoped to this owner). Public company records linked to this owner. Omitted when there are no matched companies. `sec` or `gleif`. Ticker symbol for SEC matches. Exchange for SEC matches. Legal Entity Identifier for GLEIF matches. `active`, `inactive`, or `delisted`. Per-territory coverage rollup (`territory_count`, `by_primary`, `by_stage`, `matched_territories`, `territories[]`). Present only on grouped Madrid IR-family rows (`international_registrations=grouped`, the default); `null` on record-grain and direct/regional mark-of-one rows. [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark) returns the same rollup with the per-designation detail the search row omits. WIPO/national reconciliation alternates on grouped IR-family rows; `null` on record-grain and direct/regional mark-of-one rows. `true` when a grouped IR family's territorial owners resolve to more than one distinct holder; `null` on record-grain and direct/regional mark-of-one rows. Present only when a `jurisdictions` filter is active under the default `territory_match=protection`. Explains **why** the hit satisfied that filter: each entry maps a requested code to the stored territory it matched on and the basis of the match. Absent under `territory_match=direct` and when no `jurisdictions` filter is applied. The `jurisdictions` code the caller requested (e.g. `FR`). The stored territory the hit actually matched on (WIPO's `EM` designation code is rendered `EU`). For a EUTM returned against a `FR` request this is `EU`. `direct` when the hit carries the requested territory as a literal leg; `regional_membership` when it matched through a regional right whose membership includes the request. ### Worked example: a EUTM returned for a French request `GET /v1/trademarks?q=apple&jurisdictions=FR` runs under the default `territory_match=protection`. A EU trade mark (filed at the EUIPO, `jurisdiction_code: "EU"`) is now returned, because a EUTM protects France, and it carries: ```json theme={null} { "id": "tm_…", "mark_text": "APPLE", "office_code": "EM", "jurisdiction_code": "EU", "territory_matches": [ { "requested": "FR", "matched_via": "EU", "basis": "regional_membership" } ] } ``` A French national mark in the same result set instead carries `{ "requested": "FR", "matched_via": "FR", "basis": "direct" }`. Re-run with `&territory_match=direct` and the EUTM drops out, leaving only literal FR legs (French national filings and Madrid designations of France). ```json Response theme={null} { "object": "list", "data": [ { "id": "tm_8kLm2nPq", "object": "trademark", "mark_text": "NIKE", "relevance_score": 95, "match_explanation": { "strategies_matched": ["exact"], "boost_factors": [ { "factor": "status_active", "weight": 1.2 } ] }, "primary_image_url": null, "status": { "primary": "active", "stage": "registered" }, "office_code": "US", "jurisdiction_code": "US", "filing_date": "1971-02-04", "registration_date": "1974-04-16", "classifications": [ { "nice_class": 25, "goods_services_text": "Clothing, footwear, headgear", "goods_services_text_truncated": false } ], "nice_classes": [25], "owners": [ { "id": "own_R3jK9mN2", "name": "Nike, Inc.", "country_code": "US", "entity_id": "ent_Wp8qLd4Z", "entity_id_type": "resolved", "companies": [ { "source": "sec", "ticker": "NKE", "exchange": "NYSE", "lei": null, "entity_status": "active" }, { "source": "gleif", "ticker": null, "exchange": null, "lei": "787RXPR0UX0O0XUXPZ81", "entity_status": "active" } ] } ], "coverage": null, "source_records": null, "owners_mixed": null } ], "has_more": true, "pagination": { "cursor": "eyJpZCI6ImFiYyJ9", "total_count": 142, "total_count_approximate": false }, "search_meta": { "search_id": "srch_abc123", "query": "nike", "strategies_used": ["exact", "fuzzy"], "international_registrations": "grouped", "execution_time_ms": 15 }, "request_id": "req_xyz789" } ``` ## Code Examples ```bash cURL: search by name theme={null} curl -G "https://api.signa.so/v1/trademarks" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=nike" \ --data-urlencode "offices=US" ``` ```bash cURL: filter by office and class theme={null} curl -G "https://api.signa.so/v1/trademarks" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "offices=EM" \ --data-urlencode "nice_classes=9,42" \ --data-urlencode "status_stage=registered" ``` ```bash cURL: clearance search (all strategies) theme={null} curl -G "https://api.signa.so/v1/trademarks" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "q=nova" \ --data-urlencode "strategies=exact,phonetic,fuzzy,prefix" \ --data-urlencode "offices=US,EM" \ --data-urlencode "nice_classes=9" ``` ```bash cURL: public-company owner filter theme={null} curl -G "https://api.signa.so/v1/trademarks" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "owner_publicly_traded=true" \ --data-urlencode "owner_ticker=AAPL" \ --data-urlencode "status_stage=registered" ``` ```typescript TypeScript (SDK) theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const results = await signa.trademarks.list({ q: "nike", offices: ["US"], nice_classes: [25], limit: 10, }); for (const mark of results.data) { console.log(mark.mark_text, mark.status.stage); } ``` ## Statistics view Use TMview-style facet breakdowns when building filter UIs, dashboards, or competitive landscape views that compare trademark counts by office, class, status, or party. They provide the counts needed to show the shape of a result set without downloading every matching record. Aggregations are available on GET with `?aggregations=nice_classes,office_code`, with semantics identical to POST `options.aggregations`. Add `?aggregations_only=true` to return counts without result documents. The 15 supported dimensions are `status_stage`, `office_code`, `jurisdiction_code`, `nice_classes`, `filing_year`, `mark_feature_type`, `mark_legal_category`, `filing_route`, `right_kind`, `scope_kind`, `firm_id`, `attorney_id`, `owner_country`, `owner_id`, and `entity_id`. For id-bearing buckets (`owner_id`, `attorney_id`, `firm_id`, and `entity_id`), bucket keys are prefixed public ids. The top-level `aggregation_metadata` object maps each bucket key to a human-readable display name. ```bash theme={null} curl "https://api.signa.so/v1/trademarks?jurisdictions=US&aggregations=nice_classes,owner_id" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` ```json theme={null} { "object": "list", "data": [ { "id": "tm_550e8400-e29b-41d4-a716-446655440000", "mark_text": "ACME CLOUD" } ], "aggregations": { "nice_classes": { "9": 1234, "42": 567 }, "owner_id": { "own_550e8400-e29b-41d4-a716-446655440001": 84 } }, "aggregation_metadata": { "owner_id": { "own_550e8400-e29b-41d4-a716-446655440001": "Acme Corporation" } } } ``` ## Sort By default, results are ranked by **relevance** when `q` is provided. For filter-only queries (no `q`), results are returned in index order (fast, unordered). To explicitly sort results, use the `sort` parameter: ``` ?sort=-filing_date # newest filings first ?sort=expiry_date # earliest expiry first ?sort=mark_text # alphabetical mark text ?sort=owner_name # alphabetical first owner name ?sort=-filing_date,office_code # multi-field (max 3) ``` Available sort fields: `filing_date`, `registration_date`, `expiry_date`, `renewal_due_date`, `updated_at`, `publication_date`, `termination_date`, `office_code`, `jurisdiction_code`, `mark_text`, `owner_name`. When `sort` is specified alongside `q`, relevance scoring is bypassed: results are ordered purely by the sort field(s) and `relevance_score` will be `null`. ## Advanced: POST with JSON body For complex queries with aggregations or long filter lists, use `POST /v1/trademarks` with a JSON body. This accepts the same filters and returns the same response shape. `POST /v1/trademarks` is idempotency-**exempt** (it is a read-shaped search): the `Idempotency-Key` header is not required, and if you send one it is not enforced or replayed (the value's format is still validated). `GET` does not take one. ### POST-only features * `options.aggregations`: an **array of field names** to aggregate. Returns faceted counts for building filter UIs. Valid values: `status_stage`, `office_code`, `jurisdiction_code`, `nice_classes`, `filing_year`, `mark_feature_type`, `mark_legal_category`, `filing_route`, `right_kind`, `scope_kind`, `firm_id`, `attorney_id`, `owner_country`, `owner_id`, `entity_id`. * `options.aggregations_only`: return only counts, skip result documents ### POST example ```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", "fuzzy", "prefix"], "filters": { "offices": ["US", "EM"], "nice_classes": [9, 42], "status_stage": ["registered"], "filing_date": { "gte": "2020-01-01" } }, "options": { "aggregations": ["office_code", "nice_classes", "status_stage"] }, "limit": 20 }' ``` | Field | Type | Description | | --------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | string | Search query text (1-500 chars). Optional. | | `sort` | string | Sort field(s), same as GET `sort` parameter. | | `strategies` | string\[] | Search strategies. Default: `["exact", "fuzzy"]`. Only valid with `match: "similar"`. | | `match` | string | [Match mode](#match-modes): `similar` (default), `exact`, `starts_with`, `ends_with`, `contains`. Deterministic modes require `query` and disallow `strategies`/`ranking_profile`. | | `mark_text_not_contains` | string | Exclude marks whose folded text contains this substring. Composable with any match mode. | | `filters` | object | Same filters as GET, nested in an object. Arrays use JSON arrays, dates use `{"gte": "...", "lt": "..."}`. | | `territory_match` | string | Top-level field (not nested in `filters`): `protection` (default) or `direct`. Controls how `filters.jurisdictions` matches. See the [`territory_match`](#query-parameters) query parameter. | | `options.aggregations` | string\[] | Faceted counts across all 15 dimensions listed in Statistics view. | | `options.aggregations_only` | boolean | Return only counts, no documents. Default: `false`. | | `options.include_total` | boolean | Include accurate total count (subject to the same 10,000-match cap as `include_total` on `GET`). Default: `false`. | | `options.highlights` | boolean | Include highlight snippets. Default: `false`. | | `include` | string\[] | Optional row projections. `full_goods_services` returns full `classifications[].goods_services_text`. | | `fields` | string\[] | Sparse top-level field projection. `id` and `object` are always retained. Unknown names return `400`. | | `limit` | integer | Results per page (0-100). Default: `20`. | | `cursor` | string | Pagination cursor from previous response. | ## Errors | Status | Type | When | | ------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | 400 | `validation_error` | No filter or query supplied, invalid date range, unknown sort field | | 401 | `unauthorized` | Missing or invalid API key | | 422 | `entity_too_large` | `entity_id` or `entity_group` resolved to more members than the endpoint supports; see [Errors](/api-reference/errors) | | 429 | `rate_limited` | Rate limit exceeded ([details](/api-reference/rate-limits)) | ## Related Endpoints * [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark): full detail for a single mark * [Batch Retrieve](/api-reference/trademarks/batch-trademarks): hydrate known IDs # Trademark Assignments Source: https://docs.signa.so/api-reference/trademarks/trademark-assignments GET /v1/trademarks/{id}/assignments List recorded transfers, liens, and releases for one trademark ## When To Use This Use this endpoint when M\&A counsel or diligence teams need the recorded chain of title for one mark before an acquisition. It shows assignments, security interests, releases, and other recorded conveyances for the mark, so unreleased liens can be spotted before closing. For the cross-mark diligence workflow, see [Chain-of-title & Lien Diligence](/guides/chain-of-title-lien-diligence). ## Path Parameters Trademark ID (`tm_...`). ## Query Parameters Filter by conveyance type. One of `assignment`, `security_interest`, `release`, `merger`, `name_change`, `license`, `partial_assignment`, `correction`, `entity_conversion`, `other`. Alias for `conveyance_type`. Page size (1-100). Pagination cursor from a previous response. ## Response Assignment records for this trademark, sorted by `recorded_date` descending. Each item carries the assignment summary fields plus `correspondent`, `parties`, and `properties`. `parties[]` includes `role`, `name`, `owner_id`, `execution_date`, `date_acknowledged`, `legal_entity_text`, `nationality`, and `address` (a pass-through object of the office-provided party address fields, or `null`). `properties[]` includes the affected `trademark_id`, office serial and registration numbers, international registration number, and mark name. ## Example Request ```bash cURL theme={null} curl "https://api.signa.so/v1/trademarks/tm_11111111-1111-4111-8111-111111111111/assignments" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` ## Example Response ```json theme={null} { "object": "list", "data": [ { "id": "asg_77777777-7777-4777-8777-777777777777", "object": "assignment", "reel_no": "9120", "frame_no": "0301", "conveyance_type": "release", "conveyance_text": "RELEASE OF SECURITY INTEREST", "recorded_date": "2025-04-18", "last_update_date": "2025-04-22", "page_count": 3, "release_of_reel_no": "9102", "release_of_frame_no": "0100", "created_at": "2025-04-18T14:30:00.000Z", "updated_at": "2025-04-22T18:45:00.000Z", "correspondent": { "name": "Loan Collateral Services", "address1": "500 Delaware Avenue, Suite 720", "address2": "Wilmington, DE 19801" }, "parties": [ { "role": "assignee", "name": "Northstar Brands LLC", "owner_id": "own_22222222-2222-4222-8222-222222222222", "execution_date": "2025-04-10", "date_acknowledged": null, "legal_entity_text": "corporation", "nationality": "US", "address": { "address1": "88 Harbor Point Road", "city": "Stamford", "state": "CT", "postcode": "06902" } }, { "role": "assignor", "name": "First Continental Bank, N.A.", "owner_id": "own_44444444-4444-4444-8444-444444444444", "execution_date": "2025-04-10", "date_acknowledged": "2025-04-11", "legal_entity_text": "corporation", "nationality": "US", "address": null } ], "properties": [ { "trademark_id": "tm_11111111-1111-4111-8111-111111111111", "serial_no": "98887777", "registration_no": "6543210", "intl_reg_no": null, "mark_name": "NORTHSTAR SUPPLY" } ] }, { "id": "asg_66666666-6666-4666-8666-666666666666", "object": "assignment", "reel_no": "9102", "frame_no": "0100", "conveyance_type": "security_interest", "conveyance_text": "SECURITY INTEREST", "recorded_date": "2024-10-03", "last_update_date": "2024-10-07", "page_count": 4, "release_of_reel_no": null, "release_of_frame_no": null, "created_at": "2024-10-03T14:30:00.000Z", "updated_at": "2024-10-07T18:45:00.000Z", "correspondent": { "name": "Loan Collateral Services", "address1": "500 Delaware Avenue, Suite 720", "address2": "Wilmington, DE 19801" }, "parties": [ { "role": "assignee", "name": "First Continental Bank, N.A.", "owner_id": "own_44444444-4444-4444-8444-444444444444", "execution_date": "2024-09-30", "date_acknowledged": null, "legal_entity_text": "corporation", "nationality": "US", "address": { "address1": "1 Financial Plaza", "city": "Hartford", "state": "CT", "postcode": "06103" } }, { "role": "assignor", "name": "Northstar Brands LLC", "owner_id": "own_22222222-2222-4222-8222-222222222222", "execution_date": "2024-09-30", "date_acknowledged": "2024-10-01", "legal_entity_text": "corporation", "nationality": "US", "address": null } ], "properties": [ { "trademark_id": "tm_11111111-1111-4111-8111-111111111111", "serial_no": "98887777", "registration_no": "6543210", "intl_reg_no": null, "mark_name": "NORTHSTAR SUPPLY" } ] } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_test_fixed" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/trademarks/tm_11111111-1111-4111-8111-111111111111/assignments?type=security_interest" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const chain = await signa.trademarks.assignments( "tm_11111111-1111-4111-8111-111111111111", { type: "security_interest" }, ); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/trademarks/tm_11111111-1111-4111-8111-111111111111/assignments", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, params={"type": "security_interest"}, ) ``` ## Errors | Status | Type | Description | | ------ | ------------------ | --------------------------------------- | | 400 | `validation_error` | Invalid trademark ID or query parameter | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | Missing `trademarks:read` scope | | 404 | `not_found` | Trademark not found | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Search Assignments](/api-reference/records/transactions/search-assignments): filter recorded transactions across marks * [Retrieve Assignment](/api-reference/records/transactions/retrieve-assignment): retrieve parties and affected marks for one transaction * [Chain-of-title & Lien Diligence](/guides/chain-of-title-lien-diligence): owner and mark diligence workflow # Trademark Citations Source: https://docs.signa.so/api-reference/trademarks/trademark-citations GET /v1/trademarks/{id}/citations Prior marks an examiner cited against this application as a bar to registration ## When To Use This Use this endpoint when you are looking at one application and need to know what the examiner put in its way. Each row is one prior mark cited in one office action against this application, with the stage the citation reached and how it ultimately resolved. This is the inbound direction. For the outbound direction, the applications this mark was cited *against*, use [Trademark Cited By](/api-reference/trademarks/trademark-cited-by). **Rows are citation occurrences, not prior marks.** One prior mark cited in both a nonfinal and a final office action produces two rows. To count the distinct marks standing in this application's way, group by `cited_trademark.id`, falling back to `cited_ref` when `cited_trademark` is `null`. A raw row count overstates how many prior marks the examiner raised. **Coverage today is USPTO only.** Citations are extracted from USPTO office actions and cover §2(d) likelihood-of-confusion refusals. Other offices return an empty list. Check `capabilities.citations` on [List Offices](/api-reference/reference/list-offices) to see the state of any office programmatically, rather than inferring coverage from an empty response. ## Freshness Citations are extracted hourly from stored office actions. Dispositions are recomputed once a day, so a newly extracted citation carries `disposition: null` until the next daily refresh, which can be up to about 24 hours. A disposition can also change on a later run as prosecution continues. `disposition_as_of` is stamped when the current disposition was first derived or last changed, and deliberately does not move when a daily run re-derives the same value. An unchanged timestamp means the disposition has been stable, not that the refresh stopped running. ## Path Parameters Trademark ID (`tm_...`). This is the citing application, the mark the office action was issued against. ## Query Parameters Filter by how the citation resolved, comma-separated. One or more of `citation_issued`, `maintained`, `withdrawn`, `abandoned_after`, `published`. Rows whose disposition has not been computed yet are excluded when this filter is supplied. Filter by the stage the citation reached, comma-separated. One or both of `nonfinal`, `final`. `final` includes citations first raised in a nonfinal action that a later final action maintained. Page size (1-100). Opaque cursor from the previous response's `pagination.cursor`. Results are ordered by `action_date` descending, newest first, with undated actions last and `id` as the final tiebreaker. The order is fixed: there is no `sort` parameter. ## Response Always `list`. Citation ID (`cit_...`). Always `citation`. Office that issued the action, uppercase ST.3 (e.g. `US`). Application number the office action was issued against, as printed by the office. Summary of the citing application (`id`, `mark_text`, `office_code`, `application_number`, `registration_number`, `status_primary`). On this endpoint it is the mark in the path. The prior mark reference the examiner cited, as an application or registration number. Whether `cited_ref` is a `registration` or an `application` number. This label is inferred from how the citing office action worded the reference, and it is occasionally wrong: a serial can be labelled as a registration, or the reverse. Signa matches the number against both namespaces regardless of the label, so a mislabelled reference still resolves. Treat it as a hint, and when `cited_trademark` is present read that as the authoritative identification of the cited mark. Summary of the cited prior mark, same fields as `trademark`. `null` when the citation carries no persisted link to a Signa record, in which case `cited_ref` is the only identifier available. It does not mean the cited mark is absent from the register: resolution runs once, at extraction time, so a mark ingested after its citations were extracted keeps a null link. Read it as "matched by reference only". Office-local refusal ground. `2d` is the USPTO §2(d) likelihood-of-confusion ground. This vocabulary is office-scoped, not canonical across offices. ISO date of the office action (`YYYY-MM-DD`). `nonfinal` or `final`. This is the stage the citation reached, not provenance about `source_document_id`. A citation first raised in a nonfinal action reads `final` once a later dated final action maintained the refusal, so `final` does not mean the row was extracted from a final action. How the citation resolved. See the table below. `null` while pending the daily refresh. ISO 8601 timestamp of when the current `disposition` was first derived or last changed. It does not advance when a daily run re-derives the same value, so a stale-looking timestamp means the disposition has been stable. The office action the citation was extracted from (`med_...`), retrievable through [Trademark Documents](/api-reference/trademarks/records/documents) on the mark in the path, which is always the citing mark here. Every citation served today carries one. The field is nullable so a citation derived without a stored source document can be represented. Whether more citations are available. Cursor for the next page. Unique request identifier for support and debugging. ### Disposition Values A citation carries exactly one disposition, derived from this application's prosecution events and from whether the citation reappeared in subsequent office actions. **The disposition is one verdict per (application, cited reference) pair.** It is computed across that pair's whole occurrence history and then written onto every occurrence row for the pair. It is not per row, and it is not per date. `action_stage` does vary from row to row, but it records the stage that occurrence of the citation reached, not the kind of document the row was extracted from: a citation first raised in a nonfinal action is upgraded to `final` once a later dated final action maintains the refusal, so a `final` row can still carry a nonfinal `source_document_id`. `disposition` is per pair and describes the citation's fate overall. So a row with `action_stage: nonfinal` alongside `disposition: maintained` is expected, not a data error. It means a different occurrence of this same citation, against the same application, reached a final action. Filter `action_stage=final` if you want the occurrences that reached final, remembering that those rows include upgraded ones whose `source_document_id` is a nonfinal action; read `disposition` from any row of the pair if you want the pair's outcome. `published` and `abandoned_after` are evaluated against the pair's earliest office action date rather than against the individual row's date, which is another reason every row of a pair carries the same answer. | Value | Meaning | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `published` | The application published or registered after the citation date. The citation did not stop it. | | `maintained` | The refusal reached a final office action. The citation held. | | `abandoned_after` | The application went abandoned after the citation date and was not revived. The outcome of the citation itself is unknown, the application simply stopped. | | `withdrawn` | A later citation-bearing office action for the same application dropped this reference, with no publication yet. The examiner backed off. | | `citation_issued` | No later signal yet. The application is still in prosecution, or its event record does not yet support a stronger label. | | `null` | The disposition has not been computed yet. Recomputed on the daily refresh. | When more than one signal applies, the strongest one wins, in this order: `published`, `maintained`, `abandoned_after`, `withdrawn`, `citation_issued`. A mark that was cited, refused to final, and then abandoned reads `maintained`, because a refusal held to final is the more informative fact. The `action_stage` and the mark's own status remain on the record for anyone who needs the detail. ## Example Request ```bash cURL theme={null} curl "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/citations?action_stage=final&limit=20" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` ## Example Response ```json theme={null} { "object": "list", "data": [ { "id": "cit_0198c2f1-7e2a-7b34-9c11-3d5f8a2b4c6e", "object": "citation", "office_code": "US", "application_ref": "98123456", "trademark": { "id": "tm_8kLm2nPq", "mark_text": "ACME", "office_code": "US", "application_number": "98123456", "registration_number": null, "status_primary": "pending" }, "cited_ref": "5567890", "cited_ref_type": "registration", "cited_trademark": { "id": "tm_019f34d6-2000-7777-8888-000000000001", "mark_text": "ACME CO", "office_code": "US", "application_number": "87999999", "registration_number": "5567890", "status_primary": "active" }, "refusal_type": "2d", "action_date": "2026-01-15", "action_stage": "final", "disposition": "maintained", "disposition_as_of": "2026-08-30T13:30:00.000Z", "source_document_id": "med_0198c2f1-7e2a-7b34-9c11-3d5f8a2b4c6e" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_cV2nL8pQ" } ``` ## Code Examples ```bash cURL theme={null} curl -G "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/citations" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" \ --data-urlencode "disposition=maintained" \ --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 citations = await signa.trademarks.citations("tm_8kLm2nPq", { limit: 20 }); for (const citation of citations.data) { console.log(citation.action_date, citation.cited_ref, citation.disposition); } ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/citations", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, params={"disposition": "maintained", "limit": 20}, ) ``` ## Counting Without Listing [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark) returns `citations_count` for the same set of rows. `0` means the office is covered and this mark has no citations. `null` means citations are not counted for that office at all, which is not the same as "no citations exist". Use `capabilities.citations` on [List Offices](/api-reference/reference/list-offices) to tell the two apart. ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------------- | | 400 | `validation_error` | Malformed `id` (not a prefixed ID), unknown query parameter, or bad enum value | | 400 | `id_type_mismatch` | `id` is a well-formed ID of another type (e.g. `own_...`) rather than a `tm_...` | | 400 | `cursor_expired` | Cursor is blank, expired, or was issued for a different endpoint | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Trademark ID does not exist | | 429 | `rate_limited` | Rate limit exceeded | ## Related Endpoints * [Trademark Cited By](/api-reference/trademarks/trademark-cited-by): applications this mark was cited against * [Search Citations](/api-reference/citations/search-citations): cross-mark citation query with office, date, and disposition filters * [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark): parent mark detail, including `citations_count` * [Trademark Documents](/api-reference/trademarks/records/documents): the office actions citations are extracted from # Trademark Cited By Source: https://docs.signa.so/api-reference/trademarks/trademark-cited-by GET /v1/trademarks/{id}/cited-by Applications an examiner cited this mark against, the refusals where this mark is the prior right ## When To Use This Use this endpoint to measure how much of a blocker a mark is. Each row is one citation occurrence: this mark named as the prior right in one office action against one application. This is the outbound direction, so the marks in `trademark` are other people's applications, and the mark in the path is the reference the examiner reached for. It answers questions a search index cannot: whether the office has actually treated this mark as a bar, how often, how recently, and whether those refusals stuck. **Rows are citation occurrences, not applications.** One application cited in both a nonfinal and a final office action produces two rows for the same mark. To count distinct applications, group by `trademark.id`, falling back to `application_ref` when `trademark` is `null`. A raw row count overstates how many applications this mark has blocked. For the inbound direction, the prior marks cited against this application, use [Trademark Citations](/api-reference/trademarks/trademark-citations). **Coverage today is USPTO only.** Citations are extracted from USPTO office actions and cover §2(d) likelihood-of-confusion refusals. Other offices return an empty list. Check `capabilities.citations` on [List Offices](/api-reference/reference/list-offices) to see the state of any office programmatically, rather than inferring coverage from an empty response. ## Freshness Citations are extracted hourly from stored office actions. Dispositions are recomputed once a day, so a newly extracted citation carries `disposition: null` until the next daily refresh, which can be up to about 24 hours. A disposition can also change on a later run as prosecution continues. `disposition_as_of` is stamped when the current disposition was first derived or last changed, and deliberately does not move when a daily run re-derives the same value. An unchanged timestamp means the disposition has been stable, not that the refresh stopped running. ## Disposition Grain `disposition` is one verdict per (application, cited reference) pair. It is computed across that pair's whole occurrence history and then written onto every occurrence row for the pair. It is not per row, and it is not per date. `action_stage` does vary from row to row, but it records the stage that occurrence of the citation reached, not the kind of document the row was extracted from: a citation first raised in a nonfinal action is upgraded to `final` once a later dated final action maintains the refusal, so a `final` row can still carry a nonfinal `source_document_id`. `disposition` is per pair and describes the citation's fate overall. So a row with `action_stage: nonfinal` alongside `disposition: maintained` is expected, not a data error. It means a different occurrence of this same citation, against the same application, reached a final action. Filter `action_stage=final` if you want the occurrences that reached final, remembering that those rows include upgraded ones whose `source_document_id` is a nonfinal action; read `disposition` from any row of the pair if you want the pair's outcome. `published` and `abandoned_after` are evaluated against the pair's earliest office action date rather than against the individual row's date, which is another reason every row of a pair carries the same answer. The full value table is on [Trademark Citations](/api-reference/trademarks/trademark-citations#disposition-values). ## Rows Matched By Reference Examiners cite an application or registration number. Signa resolves that number to a mark on the register at extraction time, but some rows never resolve: the cited number may belong to a mark Signa does not hold, or the mark may have been ingested after its citations were extracted. This endpoint returns those rows too. It matches on the resolved link, and additionally on this mark's own application and registration numbers for unlinked citations from the same office, so a citation that names your number is returned even when the extraction pipeline never linked it. On such a row `cited_trademark` is `null` and `cited_ref` carries the number the examiner printed. Treat `cited_trademark: null` as "matched by reference only", not as a missing field. Dropping those rows undercounts how often the mark has been used as a bar. ## Path Parameters Trademark ID (`tm_...`). This is the cited prior mark. ## Query Parameters Filter by how the citation resolved, comma-separated. One or more of `citation_issued`, `maintained`, `withdrawn`, `abandoned_after`, `published`. Rows whose disposition has not been computed yet are excluded when this filter is supplied. Filter by the stage the citation reached, comma-separated. One or both of `nonfinal`, `final`. `final` includes citations first raised in a nonfinal action that a later final action maintained. Page size (1-100). Opaque cursor from the previous response's `pagination.cursor`. Results are ordered by `action_date` descending, newest first, with undated actions last and `id` as the final tiebreaker. The order is fixed: there is no `sort` parameter. ## Response The object is identical to the one returned by [Trademark Citations](/api-reference/trademarks/trademark-citations), read in the other direction: `trademark` is the citing application and `cited_trademark` is the mark in the path, or `null` on a reference-matched row. Always `list`. Citation ID (`cit_...`). Always `citation`. Office that issued the action, uppercase ST.3 (e.g. `US`). Application number the office action was issued against, as printed by the office. Summary of the citing application (`id`, `mark_text`, `office_code`, `application_number`, `registration_number`, `status_primary`). `null` when the citation carries no persisted link to a Signa record for that application, which is not the same as the application being absent from the register: resolution runs once, at extraction time. The reference the examiner cited. On this endpoint it is this mark's application or registration number. Whether `cited_ref` is a `registration` or an `application` number. This label is inferred from how the citing office action worded the reference, and it is occasionally wrong: a serial can be labelled as a registration, or the reverse. Signa matches the number against both namespaces regardless of the label, so a mislabelled reference still resolves. Treat it as a hint, and when `cited_trademark` is present read that as the authoritative identification of the cited mark. Summary of the cited mark. `null` on a row matched by reference only, as described above. Office-local refusal ground. `2d` is the USPTO §2(d) likelihood-of-confusion ground. ISO date of the office action (`YYYY-MM-DD`). `nonfinal` or `final`. This is the stage the citation reached, not provenance about `source_document_id`. A citation first raised in a nonfinal action reads `final` once a later dated final action maintained the refusal, so `final` does not mean the row was extracted from a final action. How the citation resolved. See [Disposition Values](/api-reference/trademarks/trademark-citations#disposition-values). `null` while pending the daily refresh. ISO 8601 timestamp of when the current `disposition` was first derived or last changed. It does not advance when a daily run re-derives the same value, so a stale-looking timestamp means the disposition has been stable. The office action the citation was extracted from (`med_...`), retrievable through [Trademark Documents](/api-reference/trademarks/records/documents) on the citing mark. Every citation served today carries one. The field is nullable so a citation derived without a stored source document can be represented. Documents are only addressable under a trademark, so when `trademark` is `null` there is no route that resolves this id. Whether more rows are available. Cursor for the next page. Unique request identifier for support and debugging. ## Example Request ```bash cURL theme={null} curl "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/cited-by?limit=20" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` ## Example Response The second row was matched by reference: the examiner cited registration `5567890`, and that citation is returned even though the extraction pipeline never linked it to a record. ```json theme={null} { "object": "list", "data": [ { "id": "cit_0198c2f1-7e2a-7b34-9c11-3d5f8a2b4c6e", "object": "citation", "office_code": "US", "application_ref": "98123456", "trademark": { "id": "tm_019f34d6-2000-7777-8888-000000000001", "mark_text": "ACME BREW", "office_code": "US", "application_number": "98123456", "registration_number": null, "status_primary": "pending" }, "cited_ref": "5567890", "cited_ref_type": "registration", "cited_trademark": { "id": "tm_8kLm2nPq", "mark_text": "ACME", "office_code": "US", "application_number": "87999999", "registration_number": "5567890", "status_primary": "active" }, "refusal_type": "2d", "action_date": "2026-01-15", "action_stage": "final", "disposition": "maintained", "disposition_as_of": "2026-08-30T13:30:00.000Z", "source_document_id": "med_0198c2f1-7e2a-7b34-9c11-3d5f8a2b4c6e" }, { "id": "cit_0198c2f1-7e2a-7b34-9c11-3d5f8a2b4c6f", "object": "citation", "office_code": "US", "application_ref": "97654321", "trademark": { "id": "tm_019f34d6-3000-7777-8888-000000000002", "mark_text": "ACMEE", "office_code": "US", "application_number": "97654321", "registration_number": null, "status_primary": "inactive" }, "cited_ref": "5567890", "cited_ref_type": "registration", "cited_trademark": null, "refusal_type": "2d", "action_date": "2025-06-02", "action_stage": "nonfinal", "disposition": "abandoned_after", "disposition_as_of": "2026-08-30T13:30:00.000Z", "source_document_id": "med_0198c2f1-7e2a-7b34-9c11-3d5f8a2b4c70" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_cV2nL8pQ" } ``` ## Code Examples ```bash cURL theme={null} curl -G "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/cited-by" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" \ --data-urlencode "disposition=maintained,abandoned_after" \ --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 citedBy = await signa.trademarks.citedBy("tm_8kLm2nPq", { limit: 20 }); for (const citation of citedBy.data) { // `trademark` is the application that was refused over this mark. const blocked = citation.trademark?.mark_text ?? citation.application_ref; console.log(citation.action_date, blocked, citation.disposition); } ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/cited-by", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, params={"disposition": "maintained,abandoned_after", "limit": 20}, ) ``` ## Errors | Status | Type | Description | | ------ | ------------------ | -------------------------------------------------------------------------------- | | 400 | `validation_error` | Malformed `id` (not a prefixed ID), unknown query parameter, or bad enum value | | 400 | `id_type_mismatch` | `id` is a well-formed ID of another type (e.g. `own_...`) rather than a `tm_...` | | 400 | `cursor_expired` | Cursor is blank, expired, or was issued for a different endpoint | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Trademark ID does not exist | | 429 | `rate_limited` | Rate limit exceeded | ## Related Endpoints * [Trademark Citations](/api-reference/trademarks/trademark-citations): prior marks cited against this application * [Search Citations](/api-reference/citations/search-citations): cross-mark citation query with office, date, and disposition filters * [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark): parent mark detail * [Use Case: Trademark Clearance](/guides/use-cases/trademark-clearance): where citation history fits in a clearance workflow # Trademark Events Source: https://docs.signa.so/api-reference/trademarks/trademark-events GET /v1/trademarks/{id}/events Paginated prosecution event timeline for a trademark ## Overview Returns the prosecution event timeline for a trademark, sourced from office records and normalized. Events cover filings, examinations, publications, registrations, renewals, assignments, oppositions, status changes, and ad-hoc letters from the office. Each event carries the canonical Signa `event_type`, the office's raw code and label, the event scope (right, class, designation, proceeding, or other), and an optional `status_after_event` snapshot. Use it to build prosecution timelines, audit ownership transfers, and reconcile renewal cycles. Results are paginated and returned in reverse chronological order (newest first). This is the per-mark prosecution timeline. It is scoped to one trademark and is distinct from any org-level event feed. ## Path Parameters Trademark ID prefixed with `tm_` (e.g. `tm_8kLm2nPq`). ## Query Parameters Filter by one or more canonical event type codes, comma-separated. Example: `event_type=registration,renewal`. Common values: `filing`, `examination_start`, `office_action`, `publication`, `opposition_filed`, `registration`, `renewal`, `assignment`, `name_change`, `cancellation_decided`. Filter by event scope. One of `right`, `class`, `designation`, `proceeding`, `other`. Earliest event date (inclusive), ISO `YYYY-MM-DD`. Latest event date (exclusive), ISO `YYYY-MM-DD`. Page size, between 1 and 100. Opaque cursor returned in the previous response's `pagination.cursor`. ## Response Always `list`. Event ID prefixed with `evt_` (e.g. `evt_550e8400-e29b-41d4-a716-446655440000`). Always `event`. ISO date when the event occurred. Canonical Signa event type code. Human-readable description. Office-native event code as received from the source. Office-native event label as received from the source. Scope of the event: `right`, `class`, `designation`, `proceeding`, or `other`. May be `null` for many events, since not all events have a scope assigned. Status stage of the mark immediately after this event, when the office reports it. Nice class number when the event is class-scoped. Designated territory code when the event is designation-scoped. Office-side identifier for the event (e.g. action key, gazette reference). Office-assigned sequence number used to deduplicate events on the same date. Whether more events are available. Cursor for the next page. Unique request identifier for support and debugging. ```json Response theme={null} { "object": "list", "data": [ { "id": "evt_550e8400-e29b-41d4-a716-446655440000", "object": "event", "event_date": "2024-09-18", "event_type": "registration", "description": "Registration certificate issued", "raw_code": "RN", "raw_label": "REGISTERED", "event_scope": "right", "status_after_event": "registered", "nice_class_number": null, "territory_code": null, "source_identifier": "uspto-statusview-2024-09-18", "sequence_no": 1 }, { "id": "evt_7d4e1f2a-3b8c-4d0e-9f1a-2b3c4d5e6f7a", "object": "event", "event_date": "2024-02-06", "event_type": "publication", "description": "Published for opposition", "raw_code": "PB", "raw_label": "PUBLISHED FOR OPPOSITION", "event_scope": "right", "status_after_event": "published", "nice_class_number": null, "territory_code": null, "source_identifier": null, "sequence_no": 1 }, { "id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "object": "event", "event_date": "2023-04-12", "event_type": "filing", "description": "Application filed", "raw_code": "NA", "raw_label": "NEW APPLICATION", "event_scope": null, "status_after_event": "filed", "nice_class_number": null, "territory_code": null, "source_identifier": null, "sequence_no": 1 } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_cV2nL8pQ" } ``` ## Code Examples ```bash cURL theme={null} curl -G "https://api.signa.so/v1/trademarks/tm_8kLm2nPq/events" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "event_type=registration,renewal" \ --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 events = await signa.trademarks.events("tm_8kLm2nPq", { event_type: "registration,renewal", limit: 20, }); for await (const event of events) { console.log(event.event_date, event.event_type, event.description); } ``` ## Errors | Status | Type | Description | | ------ | ------------------ | ------------------------------------------------------------------------------------- | | 400 | `validation_error` | Invalid `id` format, malformed date, or `event_date_gte` greater than `event_date_lt` | | 401 | `unauthorized` | Missing or invalid API key | | 403 | `forbidden` | API key lacks the `trademarks:read` scope | | 404 | `not_found` | Trademark ID does not exist | | 429 | `rate_limited` | Rate limit exceeded | ## Related Endpoints * [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark): current trademark data, including `events_count` * [Trademark Proceedings](/api-reference/trademarks/trademark-proceedings): oppositions, cancellations, and other contested proceedings on the mark # Search by Image Source: https://docs.signa.so/api-reference/trademarks/trademark-image-search POST /v1/trademarks/search/image Find visually similar trademarks by logo/image similarity (alpha, internal testing) **Alpha, internal testing.** Image search is not open to customers yet. This page documents the current shape so you can design against it. These endpoints run on internal and design-partner keys during internal testing and their request and response shapes can still change. Email [support@signa.so](mailto:support@signa.so) to join the early access list. ## Overview Upload a logo or image and get back trademarks whose figurative marks look similar — useful for clearance searches, logo lookalikes, and design-mark screening. Works alongside text search ([Search Trademarks](/api-reference/trademarks/search-trademarks)) and Signa’s internal autocomplete ([internal Suggest](/api-reference/trademarks/trademark-suggest)). The endpoint accepts a multipart image upload, an `image_url` (https, SSRF-guarded), or a `media_id` (reuse an existing mark's stored logo), and returns a ranked, mark-level list. Each hit carries a named **signals** explanation block (visual, logo\_text, exact\_dup, vienna\_overlap) and a thumbnail URL. Results are a bounded top-K (`limit` ≤ 100, no deep pagination); the query image is never stored. ## Shape ```http theme={null} POST /v1/trademarks/search/image ``` **Request (URL-based):** ```json theme={null} { "image_url": "https://example.com/logo.png", "filters": { "offices": ["US", "EM"], "nice_classes": [9, 42] }, "channels": ["visual", "logo_text", "exact_dup"], "territory_match": "protection", "limit": 20 } ``` `territory_match` (one of `protection` or `direct`, default `protection`) controls how any `filters.jurisdictions` are interpreted: under the default `protection` a country code also matches regional rights that protect it (a `FR` filter includes EU trade marks), while `direct` matches only literal territory legs. The response `search_meta` echoes the applied mode as `search_meta.territory_match`. **Request (multipart upload):** `multipart/form-data` with an `image` file part (≤ 8 MB, raster only — no SVG) and optional `filters` / `crop` / `channels` / `limit` fields (the structured ones JSON-encoded). **Request (existing logo):** `{ "media_id": "med_..." }` reuses that media's stored vector — no re-embedding. **Response:** ```json theme={null} { "object": "list", "data": [ { "object": "image_search_hit", "score": 0.94, "band": "strong", "signals": { "visual": { "score": 0.94, "band": "strong", "embedding_spec": "visual/…" }, "logo_text": null, "exact_dup": { "checksum_match": false, "phash_distance": null }, "vienna_overlap": null }, "matched_image": { "image_id": "img_abc123", "checksum": "…", "office": "US", "jurisdiction": "US", "thumbnail_url": "/v1/trademarks/image-thumbnails/img_abc123" }, "thumbnail_url": "/v1/trademarks/image-thumbnails/img_abc123", "trademark": { "id": "tm_abc123", "mark_text": "ACME", "mark_feature_type": "figurative", "office_code": "US", "status": { "primary": "active", "stage": "registered" } } } ], "has_more": false, "pagination": { "cursor": null, "total_count": 1, "total_count_approximate": false }, "search_meta": { "search_id": "imgsrch_…", "query_mode": "image_url", "channels": ["visual", "logo_text", "exact_dup"], "territory_match": "protection", "embedding_spec": "visual/…", "index_version": 1, "execution_time_ms": 42 }, "request_id": "req_xyz" } ``` ## Want early access? If image search is on your roadmap, email [support@signa.so](mailto:support@signa.so) to join the early access list. Tell us what you are matching (logos, packaging, design marks) so the first release covers the right workflows. # Trademark Proceedings Source: https://docs.signa.so/api-reference/trademarks/trademark-proceedings GET /v1/trademarks/{id}/proceedings Oppositions, cancellations, appeals, and other contested proceedings involving a trademark ## When To Use This Use this endpoint when you are reviewing one trademark and need the dispute history attached to that mark. For example, a portfolio analyst can open a high-value mark and see every opposition or cancellation, who was involved, whether the challenger won, and how long decided cases took. For an end-to-end workflow, see [Opposition & Dispute Intelligence](/guides/opposition-dispute-intelligence). ## Path Parameters Trademark ID (`tm_...`). ## Query Parameters Filter by proceeding type. One of `opposition`, `cancellation`, `revocation`, `invalidity`, `appeal`, `non_use_removal`, `court_action`, `other`. Filter by status. One of `pending`, `decided_granted`, `decided_rejected`, `withdrawn`, `settled`, `suspended`, `partial`, `other`. Page size (1-100). Pagination cursor from a previous response. ## Response Array of proceeding records. Proceeding ID (`prc_...`). Type of proceeding. Office-issued proceeding identifier when available. Current status, when available. Normalized outcome from the challenger perspective, or `null` when not available. | Value | Meaning | | ------------------ | ------------------------------------------------------------------ | | `challenger_won` | The challenger prevailed, derived from `decided_granted`. | | `challenger_lost` | The challenger did not prevail, derived from `decided_rejected`. | | `mixed` | The decision was partial or text indicates an in-part disposition. | | `settled` | The proceeding status is `settled`. | | `withdrawn` | The proceeding status is `withdrawn`. | | `default_judgment` | The challenger won through a default or procedural failure signal. | | `pending` | The proceeding is still `pending` or `suspended`. | | `other` | The status is `other` or does not map to a more specific outcome. | Number of days between `filed_date` and `decision_date`. `null` when either date is missing. ISO date the proceeding was initiated, when available. ISO date of the decision when concluded, when available. Outcome string when decided. Array of parties to the proceeding. Each party has `owner_id`, `name`, `role` (`opponent`, `petitioner`, `respondent`, `intervener`, `other`), and `country_code`. Nice classes contested in the proceeding, when known. Free-text summary, when provided by the office. ## Example Request ```bash cURL theme={null} curl "https://api.signa.so/v1/trademarks/tm_019f34d6-2000-7777-8888-000000000001/proceedings?proceeding_type=opposition&limit=20" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` ## Example Response ```json theme={null} { "object": "list", "data": [ { "id": "prc_019f34d6-aaaa-7777-8888-aaaaaaaaaaaa", "proceeding_type": "opposition", "proceeding_number": "91265432", "status": "decided_granted", "outcome": "challenger_won", "duration_days": 231, "filed_date": "2024-01-18", "decision_date": "2024-09-05", "decision_outcome": "Opposition Sustained", "parties": [ { "owner_id": "own_019f34d6-1111-7777-8888-111111111111", "name": "Apex Outdoor Group LLC", "role": "opponent", "country_code": "US" }, { "owner_id": "own_019f34d6-2222-7777-8888-222222222222", "name": "Meridian Labs Inc.", "role": "respondent", "country_code": "US" } ], "contested_classes": [9, 42], "description": "Opposition proceeding" } ], "has_more": false, "pagination": { "cursor": null }, "request_id": "req_abc" } ``` ## Code Examples ```bash cURL theme={null} curl "https://api.signa.so/v1/trademarks/tm_019f34d6-2000-7777-8888-000000000001/proceedings?proceeding_type=opposition&status=pending" \ -H "Authorization: Bearer sig_YOUR_KEY_HERE" ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const proceedings = await signa.trademarks.proceedings("tm_019f34d6-2000-7777-8888-000000000001", { proceeding_type: "opposition", status: "pending", }); ``` ```python Python theme={null} import requests resp = requests.get( "https://api.signa.so/v1/trademarks/tm_019f34d6-2000-7777-8888-000000000001/proceedings", headers={"Authorization": "Bearer sig_YOUR_KEY_HERE"}, params={"proceeding_type": "opposition", "status": "pending"}, ) ``` ## Errors | Status | Type | Description | | ------ | ------------------ | --------------------------------------- | | 400 | `validation_error` | Invalid query parameter or trademark ID | | 401 | `unauthorized` | Missing or invalid API key | | 404 | `not_found` | Trademark not found | | 429 | `rate_limited` | Too many requests | ## Related Endpoints * [Search Proceedings](/api-reference/proceedings/search-proceedings): search proceedings across all marks * [Retrieve Proceeding](/api-reference/proceedings/retrieve-proceeding): full detail for a single proceeding * [Retrieve Trademark](/api-reference/trademarks/retrieve-trademark): parent trademark detail * [Opposition & Dispute Intelligence](/guides/opposition-dispute-intelligence): workflow for profiling an opposer # Changelog Source: https://docs.signa.so/changelog API and developer-tool updates # Changelog ## September 14, 2026: Consistent trademark detail validators Trademark detail responses now label their `ETag` as weak (`W/"…"`) on both `200 OK` and `304 Not Modified`. The validator tracks equivalent trademark content; the per-request `request_id` means two otherwise unchanged responses can contain different bytes. This also gives edge-compressed and identity responses the same semantic validator without guessing the selected encoding. Send the full returned value in `If-None-Match`. Previously issued strong validators still match unchanged resources, and the response returns the weak form. Projections, language, derived data and citation counts continue to participate in cache invalidation. See [Caching & ETags](/guides/caching). ## September 14, 2026: Renewed marks stop showing the paid renewal cycle on US §8/§9 and MX renewals **A registration whose register expiry has already moved to the next term no longer carries its previous renewal as a live deadline, on every rule that ends a term.** The maintenance engine has always dropped a renewal cycle that the office's own stated expiry says was paid, but it recognised only the plain `renewal` and `international_renewal` rules. Two rules end a term under a different name and were missed: * **US Section 8 plus Section 9** (`combined_renewal_and_use`). A mark registered in 2016 and renewed through 2036 still listed the satisfied 2026 cycle, in grace and apparently actionable, for the six months after that date. * **Mexico's combined renewal and declaration of use** (`mx_renewal`, also `combined_renewal_and_use`, national route). A renewed Mexican registration lost its paid cycle the same way. Madrid designations of Mexico renew centrally at WIPO under `renewal` and were never affected. The suppression now covers every term-end rule, so the paid cycle disappears from `derived.deadlines` on `GET /v1/trademarks/{id}`, from `GET /v1/deadlines`, from the portfolio deadline list and its iCalendar view, and from `POST /v1/deadlines/compute`. `POST /v1/reconcile` already scored the stated cycle rather than the paid one, so the two now name the same occurrence instead of disagreeing. **Rows typed `renewal` and `international_renewal` are unchanged.** They were already inside the suppression, and their output is byte for byte what it was. Section 8 declarations on their own, Section 15 affidavits and every other non-renewal row are untouched too, since they are filings inside a term rather than the term end. A mark that has **not** been renewed still shows its open cycle for the whole window and grace period, because the register expiry is that cycle's own due date and there is nothing to suppress. And `POST /v1/deadlines/compute` with `include_missed: true` still returns the full history, including the paid cycle, which is what a docket audit needs. **One thing to check in your integration.** Every deadline surface computes over a finite horizon: ten years for `derived.deadlines` on trademark detail, five for `GET /v1/deadlines`, for the portfolio deadline list and for its iCalendar feed, and whatever you ask for on `POST /v1/deadlines/compute`. For a mark renewed within the last few months, the paid cycle is now gone and the next term end can sit beyond that horizon, so any of those can come back empty where they previously held one row. A mark registered 1996-06-01 and renewed through 2036-06-01, read on 2026-05-01, is the shape: the paid 2026 cycle is suppressed, and the 2036 cycle falls on 2036-06-02 after the weekend roll, thirty two days past the ten year horizon, and roughly five years past the five year one. It is a well formed empty array, not a missing field, and the mark's own `expiry_date` still carries the register's stated term end. Read the array rather than assuming it has at least one entry. **A register value we cannot place on the statutory ladder no longer suppresses a US or Mexican combined cycle.** A stated expiry has to fall within about a month of a real cycle on the rule's ladder for us to treat it as evidence that the cycle before it was paid. On `combined_renewal_and_use` — US Section 8 plus Section 9, and the Mexican renewal — that recognition is the whole test: a register carrying a term end the rules cannot produce, for example fifteen years after registration on a ten year ladder, used to delete the mark's real, live obligation from the schedule, and now leaves it alone. Those marks show the cycle actually in force, and `POST /v1/reconcile` reports the register value as differing from the computed one. **Rows typed `renewal` and `international_renewal` behave differently here, and deliberately so.** For that family an office-stated renewal date is authoritative, so the engine rebuilds the rule's cycle series through it rather than measuring it against the statutory ladder — an Indian mark whose priority date no register field carries legitimately sits months off that ladder. Taking the office at its word means every cycle before the stated date is still treated as paid, whether or not the date lands near a computed cycle. A register value far enough out that the schedule cannot reach it therefore yields an empty forward schedule on those rules, and `POST /v1/reconcile` reports the renewal field with a `computed_value` of `null` and a `computed_unavailable_reason` of `insufficient_data`, rather than a disagreement. The verdict on that field is then scored against the register alone: `agrees` when your docketed date matches the register, `caller_differs` when it does not. (`insufficient_data` is the verdict itself only when the register carries no date either.) That is unchanged from before this release. ## September 14, 2026: Docs and spec alignment: rule catalogs join Filing & docketing, alpha labels, monitoring status reference **The rule catalogs now live next to the endpoints that use them.** `GET /v1/deadline-rules` is tagged `Deadlines` and `GET /v1/opposition-rules` is tagged `Oppositions` in the OpenAPI spec, both inside the Filing & docketing tag group, and their reference pages sit under Filing & docketing, Deadlines, after the compute pages. Nothing about the endpoints changed: same paths, same responses, same free reference pool. Only the grouping moved, so a client generated from the spec files the catalogs with the deadline and opposition workflows instead of a separate Reference Data section. If your generator names client classes after tags (openapi-generator, NSwag), the two catalog operations move from the Reference Data class to the Deadlines and Oppositions classes; regenerate and update those two call sites. **`GET /v1/monitoring/status` has a reference page.** The endpoint had a guide but no API-reference entry. The new page, under Monitoring, Watches, documents the two halves of the response: `watches` is your organization's rollup by health state, and `offices[]` is Signa's global per-office ingestion freshness, identical for every organization. It also states plainly that `slo_hours` is Signa's internal freshness target for computing `pipeline_status`, not a published SLA. **Screening, compare, listing checks and image search are labelled alpha.** These endpoints run on internal and design-partner keys during internal testing and their request and response shapes can still change. The docs pages and spec descriptions now say alpha rather than beta. Email [support@signa.so](mailto:support@signa.so) to join the early access list. **Analytics navigation is simpler.** Market, office and classification analytics are grouped under one Filing Analytics heading; party analytics stay their own group. The rate-limits page inventory lists only published endpoints. ## September 11, 2026: Reconcile becomes a docket audit (`verdict_detail`) **`POST /v1/reconcile` gains an opt-in three-source verdict.** Send `verdict_detail: true` and every found item adds `field_verdicts[]` (your value vs the register vs the rulebook-computed schedule, with a seven-value verdict and full rule provenance), a `computed` context block and a `lookup` identifier echo. A new `your_fields.docketed_deadlines[]` input lets you audit the maintenance deadlines you have on docket against the computed schedule. See [Three-source verdicts](/api-reference/reference/reconcile#three-source-verdicts-verdict-detail). Entirely additive and off by default: without the flag, responses are byte-identical to before, and with the flag, `fields[]`, `result` and `mismatch_count` keep their exact v1 values. Billing is unchanged either way (1 unit per item). One error-path nuance: sending `docketed_deadlines` without the flag is a 400 `validation_error` whose message now names the flag requirement instead of the generic unknown-key rejection. ## September 11, 2026: Fee endpoints accept platform office codes **`GET /v1/fees` and `POST /v1/fees/estimate` now take the office spellings the rest of the API takes.** `office`, and each entry in `designations[]`, used to accept only the WIPO ST.3 codes `WO`, `US`, `EM` and `GB`, plus `WIPO` as an alias for `WO`. They now also accept the platform office codes `USPTO`, `EUIPO` and `UKIPO`, and the permanent `EU` alias for `EM`, in any case, exactly as `GET /v1/trademarks?offices=` does. A request that used to come back with an empty catalog or an `unsupported_office` line only because of how the office was spelled is now priced. The change is additive, and it is resolution only. Responses echo the canonical ST.3 code, so `{"office": "USPTO"}` returns `"office": "US"`. No amount, currency or catalog row changes for any request that was already priced, and an office with no verified schedule still returns an empty catalog or an explicit `unsupported_office` line rather than a guessed amount. Two spellings of one designated office now collapse to one code before the uniqueness check, so `designations: ["EM", "EU"]` is rejected as a duplicate instead of being counted twice. `dataset_version` on `POST /v1/fees/estimate` changes with this release because it covers estimator semantics as well as the fee rows. The rows themselves are untouched. ## September 6, 2026: Portfolios are public **Portfolios are now a documented resource family.** The nine portfolio operations (create, list, retrieve, update, delete, add and remove trademarks, and the JSON and iCalendar deadline views) appear in `GET /v1/openapi.json` under a `Portfolios` tag, grouped under "Filing & docketing", with [reference pages](/api-reference/portfolios/create) and a [portfolios guide](/guides/portfolios). Nothing about the endpoints themselves changed: same paths, same `portfolios:manage` scope, same `read` metering (1 unit per request, including writes and calendar downloads), same plan gating. The catalog operations `GET /v1/classifications`, `GET /v1/classifications/{classNumber}` and `GET /v1/goods-services` also move from the `Reference Data` tag to `Classification`, which is where the rest of the classification surface already lives. Tags are documentation grouping only; no path, parameter or response shape changed. Three error types that portfolios return are now in the [error reference](/api-reference/errors): 409 `resource_quota_exceeded`, 409 `portfolio_in_use` and 422 `portfolio_too_large`. **The TypeScript SDK's generated types caught up with the published spec.** `@signa-so/sdk` 0.12.0 regenerates `api-types` from the same spec build the published document uses. That adds 37 operations and 21 schemas, and removes nothing. Nine operations and two schemas are the portfolios surface. The other 28 operations and 19 schemas (watches, alerts, webhooks, events, monitoring status) were already published in `GET /v1/openapi.json` but were missing from the generated types, because SDK codegen ran against a narrower build of the spec than the one we publish. No existing operation or type changed, so the update is additive for every consumer. ## September 5, 2026: Auth hardening, `plan_gated`, prefixed watch filters Three visible changes from the auth and tenancy hardening pass (the first and third have been live since 2 September; the watch-filter change lands today). None of them alter a successful response. **`403 plan_gated` replaces a misleading `429`.** Calling a capability that is not on your plan at all (for example image search on `free`) used to return `429 rate_limited` with `retry_after: 0`, which sent SDK retry loops spinning. It now returns `403 plan_gated` with `endpoint_type` naming the capability and `retryable: false`. See the [error reference](/api-reference/errors). **Watch filters echo prefixed IDs.** `query.filters.trademarkIds`, `ownerId`, `attorneyId` and `firmId` on every watch response (REST and MCP) now come back as `tm_*` / `own_*` / `att_*` / `firm_*`, whichever form you sent. Input still accepts both, so nothing you send changes. **Stricter bearer parsing, clearer outages.** `Authorization` is single-valued (RFC 7235): a comma-joined pair of credentials is a `400`. The scheme is case-insensitive. A missing header gets a bare `WWW-Authenticate: Bearer` challenge with no error code (RFC 6750 §3.1). When Signa's own token verifier or JWKS is unreachable, MCP requests get a retryable `503 service_unavailable` instead of being told the token is invalid. ## September 3, 2026: prosecution deadlines are served outside calendar coverage **A prosecution deadline is no longer withheld because its office calendar cannot check the date.** Bounding `euipo_alicante` to the two Executive Director decisions actually pinned (2025 and 2026) had a consequence we did not intend: every EU office action mailed before 2025, and every one mailed from 1 November 2026 onward (a two-month period lands in 2027), came back `insufficient_facts` with reason `holiday_calendar_out_of_coverage` and no date at all. The same held for US office actions before 1985. Silence on a deadline whose miss abandons the application is the worst possible answer. Those cases now return `outcome: "computed"` with the raw statutory anniversary. An office action mailed 10 January 2024 at the EUIPO reports `due_date: "2024-03-10"`; one mailed 1 November 2026 reports `"2027-01-01"`. This matches what the renewal engine (`due_date_adjustment`) and the opposition engine (`close_adjustment`) have always done. An un-rolled date is never LATER than the true deadline, so it can be early but cannot cause a missed one. **New field: `due_date_adjustment` on every prosecution deadline.** It tells you which kind of date you are holding, using the same vocabulary as the renewal rows: * `moved`: the office calendar was consulted and rolled the date forward off a day the office is shut. * `unchanged`: the calendar was consulted and the date was already an open day. * `not_checked`: no calendar verdict stands behind the date. Either the date falls outside the calendar's pinned coverage, the rule ships without a roll rule, or the office stated the date itself (a stated deadline is published exactly as stated and never rolled). Treat it as the raw statutory anniversary and confirm before relying on the exact day. * `null`: no date was produced at all. `holiday_calendar_out_of_coverage` remains in the reason vocabulary but is now a backstop for a calendar-integrity failure rather than the ordinary out-of-coverage answer. No shipped rule reaches it today. **`close_adjustment` on a trademark summary row is no longer nullable.** The same marker was declared nullable on the summary row's `opposition_window` and non-nullable on the detail tier's `derived.opposition_window`. It can never be absent while its parent object exists, so the two now agree and the nullability sits where it belongs, on `opposition_window` itself. If you null-checked the field, that check is now dead code; nothing you were reading changes value. **Search and opposition-compute deliberately disagree for 20 days after an un-rolled close.** Where `close_adjustment` is `not_checked` and the close is within the last 20 days, search buckets the mark as **open** (a rollover could only have pushed the close later) while `POST /v1/oppositions/compute` reports `closed` against the un-rolled date. Search's reading is the safe one. Search documents in that band also now carry the un-rolled close rather than no opposition window at all; a full reindex populates them. ## September 3, 2026: France, Finland and Poland Madrid designations get opposition windows **Three new rules.** An international registration designating France, Finland or Poland had no opposition window at all: `POST /v1/oppositions/compute`, the `derived` block, `GET /v1/opposition-rules` and watch diagnostics all returned nothing for those rows. `fr_opposition_madrid`, `fi_opposition_madrid` and `pl_opposition_madrid` now answer. **France, two months from the WIPO Gazette.** CPI art. R. 717-1 applies the whole domestic opposition procedure to an international registration extended to France, and art. R. 717-5 names the publication the art. L. 712-4 two months run from: WIPO's Gazette, not any INPI republication. There is no first-of-the-following-month offset, unlike Switzerland and Germany. INPI's own directives work the case: an IR published in Gazette 2019/23 of 20 June 2019 has an opposition period expiring 20 August 2019. Every other mechanic is the domestic one by statute rather than by copy, including the art. R. 718-2 same-numbered-day arithmetic, the roll off weekends, jours fériés and INPI closure days, and the extra month for the exposé des moyens. One documented limit, and it is a hard gate. The rule reports `effective_from: "2009-01-01"` and `applicable_from_date: "2009-01-01"`. The version of art. R. 717-5 in force before that date ran the period from the first day of the month after INPI received the Gazette, an internal office event that no register record carries. We decline that era rather than guess it, so a French designation published before 2009 comes back `supported: false` with `unsupported_reason: "unsupported_office"` and the rule identity intact, `effective_from` naming the boundary. **Finland, two months from PRH's own publication.** Tavaramerkkilaki 544/2019 92 § 5 mom. has PRH publish the international registration in the same words 24 § uses for a national one, and 88 § 2 mom. applies the Act to an IR except where the Madrid chapter says otherwise, which it does not for opposition. So 32 §'s two months from the publication day carry over, with the 150/1930 month-end clamp and the roll off a pyhäpäivä or a working Saturday. PRH's own gazette confirms it: record W202600336 is an international registration with `oppositionPeriodStartDate` equal to its publication date. **Poland, three months from the BUP announcement.** PWP art. 152^1a ust. 1 has UPRP announce a designation of Poland in the Biuletyn Urzędu Patentowego without delay, and art. 152^6a ust. 1 gives an earlier-right holder three months from that announcement, non-restorable. The Biuletyn states the same sentence for both routes. The window opens the day after publication and closes on the same-numbered day three months later under KPA art. 57 § 3, rolling off statutory non-working days and Saturdays under § 4. The rule reports `effective_from: "2016-04-15"`, the commencement of art. 152^6a: before that date there was no pre-grant opposition against a designation of Poland at all. **Which publication your date is.** All three rules declare it, so the `publication_date_kind` guard applies. FR needs `wipo_gazette`; FI and PL need `office_journal`. Send the other one and the response is `status: "unknown"` with null dates and the rule's identity intact, rather than a date you should not act on. Omit the field and nothing changes. **Reindex required after the backfill.** These windows appear on documents that carry none today, and `opposition_window_opens` / `opposition_window_closes` are baked into search documents at index time. Request-time surfaces (`POST /v1/oppositions/compute`, the `derived` block, the SDK) are correct the moment this deploys. The `opposition_status` and `opposition_closes_after` filters will not see the new windows until French, Finnish and Polish Madrid designations carry an office publication date from the designation-publication backfill and are reindexed. No window we already serve MOVES: no publication date is reinterpreted and no existing pair of dates changes. **One thing that does change shape: `opposition_window` stops being null for WIPO-feed legs.** A designation leg that reaches us through the WIPO feed carries `office_code = "wipo"` with the designated country in `jurisdiction_code`, which never matched a rule keyed on the designated office's code. Those legs now resolve by jurisdiction, so for EVERY Madrid jurisdiction we model — not only the three new ones — `derived.opposition_window` turns from `null` into an object. Until the leg carries an office publication date the object's dates are all null and `status` is `"unknown"`, and `opposition_rule_version` in search documents becomes non-null after the next reindex where it was null before. If your integration reads `opposition_window === null` as "Signa does not model opposition for this mark", that inference no longer holds: check `status` and `window_closes` instead. A null `opposition_window` now means only that no rule matched the office, route and publication era at all. **No United Kingdom rule yet.** A UK Madrid designation still returns no window. UKIPO runs its three months from the UK Trade Marks Journal, and the current Journal source is not available to us to pin, so the rule is held rather than written from an unverifiable source. ## September 3, 2026: holiday calendars now stop where their evidence stops **Some deadline dates move EARLIER, and more of them report `not_checked`.** Every office holiday calendar Signa ships now advertises coverage only as far as the instruments actually pinned behind it. Eight of them are built from a document the office issues once a year: a decision, a notice, an acuerdo, a proclamation, an office memorandum. Those now stop at the last one pinned instead of projecting forward: | Calendar | Covered through | The instrument | | ---------------- | ------------------------ | ------------------------------------------------------- | | `euipo_alicante` | 2025-01-01 to 2026-12-31 | EUIPO Executive Director decisions EX-24-9 and EX-25-16 | | `mx_mexico` | 2026-11-30 | IMPI *Acuerdo de días inhábiles*, DOF 5769863 | | `dk_denmark` | 2026-12-31 | DKPTO *lukkedage* | | `fr_france` | 2026-12-31 | INPI Director-General closure decision | | `ph_philippines` | 2026-12-31 | Presidential holiday proclamations | | `cnipa_china` | 2026-12-31 | 国务院办公厅 annual notice | | `cgpdtm_india` | 2026-12-31 | DoPT holiday Office Memorandum | | `wipo_geneva` | 2026-12-31 | WIPO Information Notice 40/2025 | Beyond those horizons nothing is guessed. A renewal or grace date comes back UNROLLED with `due_date_adjustment` / `grace_expiry_adjustment` set to `"not_checked"`, and an opposition window is served with its close on the exact statutory day. Unrolled is never LATER than the true date, so a date we serve can be early but cannot cause a missed deadline. The calendars that are genuinely rule-computed (US, Canada, Germany, Australia, Brazil, Benelux, Switzerland, Finland, UK, Iceland, Norway, Poland, Sweden) still run to 2100, and the lunar ones (Singapore, Thailand, Vietnam, Japan, Korea, Türkiye) to 2030, unchanged. **EU: the invented years are gone, and EU dates in 2024 and earlier are no longer rolled.** `euipo_alicante` carried closure days for 2024 and for 2027-2030 that no Executive Director decision states. The 2027-2030 entries were labelled "estimate" and moved deadlines LATER: a day after Ascension, in-lieu Mondays, 4 January 2027, 6 December in four different years, and the two decisions that ARE pinned falsify the pattern they were derived from (EX-24-9 closes for Ascension, Schuman Day and the Assumption; EX-25-16 closes for none of the three and adds Santa Faz, 12 October and 2 November). The 2024 block said "per prior EX decision" and no such decision exists in our evidence. All of it is deleted. Consequences, both directions: * **EU opposition closes for 2027 and later are no longer rolled.** A window whose close lands on a weekend or a 2027 Spanish feast is now served on the raw statutory date. Example: an EU application published 30 November 2026 closes **28 February 2027** rather than 1 March 2027. * **EU dates before 2025 are outside the calendar.** An EU office-action response deadline computed from a 2024 mailing is no longer rolled against the unpinned 2024 block. *Superseded the same day:* this originally returned `insufficient_facts` with reason `holiday_calendar_out_of_coverage`. It now returns the un-rolled statutory date with `due_date_adjustment: "not_checked"`. See the entry above. **Mexico: December of the last pinned year is honest about being unknown.** IMPI declares each year-end shutdown in the FOLLOWING year's acuerdo, so the acuerdo for 2026 states every día inhábil of 2026 except the December closure. `mx_mexico` therefore stops on 30 November 2026. Before this change, an MX opposition window closing 22 December 2026 was served as a working day and reported `closed` on 24 December, while IMPI was in fact shut. It now reports `window_not_computable` until the acuerdo covering December 2026 is published. **A roll can no longer walk off the end of a calendar.** The coverage check ran only on the date being rolled, not on the days the roll walked through. A deadline on a listed year-end closure could therefore step into the following, uncovered year, where every day looks open, and stop on the first of January. WIPO 31 December 2026 reported 1 January 2027 as `moved`; DKPTO 28 December 2026 reported 4 January 2027. Both now report `not_checked` and serve the unrolled date. **Mexico closes on 25 December.** `mx_mexico` was missing Christmas Day from its recurring días inhábiles, although the one pinned acuerdo that lists a whole year without folding December into a closure block (DOF 5580658) ends «DICIEMBRE Viernes 25». MX deadlines falling on 25 December now roll to the next working day, LATER by one to four days, in every covered year. **Iceland: no change, and now only one story about why.** 24 and 31 December stay closed on `is_iceland`, on Hugverkastofan's own December 2020 notice that the office is shut all day and that deadlines falling there move to the next opening day. Five documents still described these as open days under a superseded reading; they now all state the ruling that shipped. Þorláksmessa (23 December) is declared year by year and stays OPEN. No date moves. **Reindex needed for EU and MX opposition windows.** Opposition windows are baked into search documents at index time. EU documents whose window closes in 2027 or later, and MX documents whose window closes in December 2026, carry the old rolled close until the affected records are reindexed. Request-time surfaces (`/v1/oppositions/compute`, `/v1/deadlines/compute`, the `derived` block) are correct on deploy. DK, FR, PH and IN opposition windows move only for close dates past their horizons; reindex those offices with the same pass. ## September 3, 2026: `close_adjustment` on every opposition window **New field: was this close date checked against a holiday calendar?** `close_adjustment` now appears on `POST /v1/oppositions/compute`, on the `derived.opposition_window` block of a trademark, on the `opposition_window` of every trademark summary row and on the `opposition` block of `GET /v1/watches/{id}/diagnostics`. It reads `moved` (the close fell on a day the office is shut and was rolled forward), `unchanged` (the calendar was consulted and the close was already a business day) or `not_checked`, meaning **no calendar stands behind this date**. `not_checked` is not new behaviour, only newly visible. Several office calendars are built from an annual notice (China's State Council holiday notice, India's DoPT memorandum, the Philippine proclamations, Mexico's IMPI acuerdos, WIPO's non-working-days notice), so a window closing in a year whose notice is not yet published has always been served with raw statutory arithmetic. A rollover can only move a close **later**, so an un-rolled close is correct-or-early: safe to act on, never a reason to file late. Until today nothing on the wire told the two apart. **The `opposition_status` search filter now treats an un-rolled close as indeterminate.** For the 20 days after a `not_checked` close date, a row counts as `open` and is excluded from `closed`, because the rollover that could not be applied may still be holding the window open. `not_started` and `unknown` are unchanged, because the opening date is never rolled. **Reindex required to populate.** The marker is baked into the search index at index time (`opposition_close_adjustment`, an additive keyword field, so no alias swap, no new index generation). Documents indexed before this release carry no value and are bucketed by their dates alone, exactly as they were before; the indeterminate band applies to them only after a reindex. Every request-time surface (`/v1/oppositions/compute`, the `derived` block, watch diagnostics) recomputes and is correct immediately. **Also:** `POST /v1/oppositions/compute` declines with `window_not_computable` in fewer cases. A close falling exactly ON the date you compute as of is now published un-rolled instead of declined: a rollover can only push it later, so the window is open either way. The narrow band that still declines is a close strictly 1–20 days before the as-of date. ## September 3, 2026: opposition rule catalog reports both eras **Five new fields on `GET /v1/opposition-rules`** (and the MCP `get_opposition_rules`): `window_change_date`, `window_starts_on_before`, `window_months_before`, `window_days_before` and `window_ends_on_before`. An opposition rule models a regime change inside itself, and the catalog used to serve only the current era, so a client reproducing a close date for a publication before the pivot got the wrong length, the wrong start convention, or both, with nothing on the wire to say so. A publication **strictly before** `window_change_date` is governed by the `_before` fields. All five are `null` on a rule that has never changed. (A previous entry said era behaviour was already reported through `window_starts_on` / `window_ends_on`. It was not; this is the change that makes it true.) **One existing value changes: `window_ends_on` is now era-scoped.** `bx_opposition` opens on the first of the following month only in its pre-1 October 2013 era, so its `corresponding_day_of_start` describes THAT era. It now reads `window_ends_on: null` with `window_ends_on_before: "corresponding_day_of_start"` instead of claiming the current era ends that way. The current BX era anchors the close on the publication day-number and never reads the field. BX is the only affected rule; no computed date moved. ## September 3, 2026: rule corrections (CN, PH, CH, CA, DE, MX) **China: the 2027 straddle is served the SHORTER window.** The 2026 revision of the Trademark Law cuts the opposition period from three months to two on 1 January 2027 and carries no transitional provision for a period still running on that day: art. 87(2) speaks only of registered marks. The two readings differ by up to a month, so Signa serves the shorter one to the straddling cohort: **every CN publication from 1 October 2026** (the first whose three months would still be open at commencement) is now given two months. A mark published 31 December 2026 closes **28 February 2027**, not 31 March 2027. A close published too early costs a would-be opponent nothing they can lose by relying on it; one published too late costs the right itself. The call is reversible and will flip the day a transitional notice or the revised Implementing Regulations settles the question. *(This supersedes the "Applications published in November and December 2026 keep three months" sentence in the CNIPA entry below.)* **Reindex:** CN opposition windows are index-baked, but CNIPA is a planned office with no indexed records, so there is nothing stale. **Philippines: the advisory extension is era-aware.** IPOPHL MC 2022-013 introduced the once-only 45-day extension on 30 June 2022. A window that closed BEFORE that date is now served the earlier regime instead: three 30-day extensions on motion, capped at 120 days from publication (MC 16-007 § 12). Display-only: `common_extension` never affects `window_closes`. **Switzerland (Madrid): the served citation is the operative one.** A CH Madrid window used to cite MSchG art. 31(2), whose words run the period "after the publication of the registration", the national trigger, which this rule never uses. It now cites MSchV art. 50(1), the provision that actually fixes day zero at the first of the month following the WIPO Gazette issue. No date moved; `source` on the response and `sources[0]` in the catalog change. **Version stamps.** `ca_opposition`, `bx_opposition`, `ch_opposition_madrid` and `ph_opposition` report a new `last_verified` / `rule_version` of 2026-09-03. BX and CH had moved served values in an earlier release without moving the stamp. Canada's two `common_extension` notes also now name the CIPO practice notice section in full ("section X.1, Extensions of time - benchmarks") instead of an abbreviation that read as an unfilled placeholder. No computed date moved for CA, CH, BX or DE. ## September 3, 2026: maintenance deadline corrections (EU, MX, NO, CA) Five corrections to `POST /v1/deadlines/compute`, the `derived` block on a trademark, and portfolio deadline rows. Maintenance deadlines are computed at request time, so every change below is live the moment it deploys, so **no reindex is needed and no stored date changes**. **A missed EU renewal's restitutio date moves earlier, by up to two weeks.** EUTMR Article 104(2) admits an application for re-establishment "within the year immediately following the expiry of the unobserved time limit", less the Article 53(3) further six months, so the long stop is the renewal expiry plus twelve months. Signa counted those months from the grace expiry **as served**, which had already absorbed one Article 69(1) working-day extension, and then extended the sum again. A EUTM filed 21 December 2014 was told its restitutio window ran to 5 January 2026; it runs to **22 December 2025**. EUIPO Guidelines Part E §7.4.2 states the rule the fix implements: the extension "applies only once and to the end of the basic period". The window still OPENS on the grace expiry as served. The same single-extension arithmetic now applies to every restoration window (EU, GB, IN, IS, JP, SG); where the grace expiry did not move, nothing changes. **A Mexican Madrid designation now renews on WIPO's calendar.** The renewal of an international registration designating Mexico is paid to WIPO's International Bureau under Madrid Protocol Article 7 (IMPI never receives it), so Reglamento LPI art. 4o does not reach it. An IR registered 29 February 2004 was given a Mexican due date of 1 March 2014 under art. 4o's month-end overflow, one day **after** the international registration had actually lapsed; it is now due **28 February 2014**, exactly matching the `WIPO` row for the same registration, with `holiday_calendar: "wipo_geneva"`. Mexico's three-year declaration of use is a Mexican filing on IMPI's own grant date and is unaffected. **Norwegian renewal due dates are no longer rolled.** Betalingsforskriften § 6 extends a *frist*, and the § 33 renewal request period is one, so its late end, the grace expiry, still rolls to the next virkedag. The due date is not a frist: it is the end of the registration period, and varemerkeloven § 33 første ledd tredje punktum charges the tilleggsavgift for a request delivered "etter utløpet av registreringsperioden". A due date served on the Monday would have advertised a surcharge-free filing day after the period had already ended. A Norwegian mark filed 1 May 2016 is due **1 May 2026**, not 4 May, and its `due_date_adjustment` reads `not_checked`. Grace expiries are unchanged. This is the same treatment Australia has had since the s 223A(1) wiring, and `GET /v1/deadline-rules` now says so explicitly (below). **An office-stated 29 February expiry no longer returns an empty schedule.** A mark whose office-reported expiry fell on a leap day could return **zero** deadlines: a live EUTM two months from expiry, with nothing to file against. Three separate pieces of leap-day arithmetic compounded; all three are fixed, and such a record now returns its renewal row on the office's own date. **Canada: `last_renewal_date` is a new optional field on compute.** Trademarks Regulations s. 159 opens the first post-17-June-2019 renewal window at the period start, and it keys on the **renewal**, not on the cycle's due date. A registration whose term ended shortly before that day but whose fee was paid in the grace period on or after it is the s. 159 renewal; the one after it is an ordinary s. 76 renewal. Send `last_renewal_date` on a compute item and Canada uses it. Omit it and the previous due-date behaviour is unchanged. **`GET /v1/deadline-rules` and the `get_deadline_rules` MCP tool gain `grace_expiry_rolls_only`.** A boolean, present on every row. When `true`, the jurisdiction rolls its grace expiry but never its renewal due date, which is what Australia and Norway do. `weekend_holiday_rule: true` alone could not express that, so a docketing system reading the catalog had no way to tell a scoped roll from a full one. Additive: no existing field changed. ## September 3, 2026: China (CNIPA) deadlines and opposition windows **New jurisdiction.** `POST /v1/deadlines/compute`, `POST /v1/oppositions/compute`, the `derived` block on a trademark, `GET /v1/deadline-rules` and `GET /v1/opposition-rules` now answer for Chinese marks. Three rules ship: `cn_renewal`, `cn_opposition` and `cn_opposition_madrid`. `office: "CN"` is accepted on the oppositions endpoint and comes back as `CN`. **A Chinese term counts its first day.** Under Implementing Regulations art. 12(2) a registration's ten years run from the day the office approves it and INCLUDE that day, so the term ends the day BEFORE the anniversary: a mark registered 7 March 2016 expires 6 March 2026, not 7 March. The 12-month renewal window and the 6-month grace period are measured from that expiry. Where the anniversary day does not exist in the target month, the expiry is the last day of that month instead: a mark registered 29 February 2020 expires 28 February 2030. **Opposition: three months now, two from 1 January 2027.** Trademark Law art. 33 gives three months from the preliminary-approval publication (初步审定公告, not the registration publication), counted from the day after and ending on the same day-of-month three months later. The 2026 revision of the Trademark Law cuts that to two months in its renumbered art. 36 and enters into force on 1 January 2027 (art. 87). Both eras are modelled now, selected by the publication date, and each window is served the article that states its own length. Applications published in November and December 2026 keep three months: the revision carries no transitional provision, and that is the non-retroactive reading. **Madrid designations of China use a different anchor.** CNIPA does not republish an international registration, so the window runs off the WIPO Gazette: Implementing Regulations art. 45(1) opens it on the FIRST DAY OF THE MONTH FOLLOWING the Gazette issue and runs three months to the last day of the third month, which is CNIPA's own worked example (Gazette 16 January 2012 → 1 February to 30 April 2012). Supply the Gazette issue date as the publication date for these rows. **Holiday calendar, and where it stops.** Chinese deadlines roll to the first working day when they fall on a statutory holiday, and the calendar is built from the State Council's annual holiday notice, including the make-up working weekends (调休), where a Saturday or Sunday is declared a working day. A renewal due on Friday 2 January 2026 is due on **Sunday 4 January 2026**, the first working day, not Monday the 5th. Because each year's notice is published only the preceding November, coverage runs 2024–2026: dates beyond it are reported unrolled rather than rolled against a guess: renewals with `due_date_adjustment: "not_checked"`, and an opposition window with its close on the exact statutory day. Unrolled means EARLIER than the true date, never later. The one exception is a window that closed within the last 20 days beyond coverage, where a roll could still be holding it open: there the window is returned `unsupported_reason: "window_not_computable"` with its rule identity intact rather than risk labelling a live window closed. Coverage extends when the 2027 notice is published. **Reindex: none needed today.** Opposition windows are baked into search documents at index time, so a new rule normally reaches already-indexed documents only after a reindex. Here there is nothing to reindex: CNIPA is a planned office and Signa holds no Chinese records in the search index yet, so no stored value changes and no document can be stale. Request-time surfaces (`/v1/deadlines/compute`, `/v1/oppositions/compute`, the `derived` block, the rule catalogs) answer for China on deploy, and the index-time path will pick the rules up with the first CNIPA ingestion. Should any Chinese records be indexed before then, they would need a reindex to gain `opposition_window_opens` / `opposition_window_closes`; nothing they carry would be wrong. ## September 3, 2026: Benelux opposition windows before 1 October 2013 corrected **Historical dates change for `bx_opposition`.** The two-month Benelux opposition period has not always run from the publication day. Until 30 September 2013 BVIE art. 2.14(1) counted it "te rekenen vanaf de eerste dag van de maand volgende op de publicatie van het depot", from the first day of the month FOLLOWING publication. The Protocol of 22 July 2010 (art. I.D) replaced those words with "te rekenen vanaf de publicatie", and it entered into force on 1 October 2013 (Trb. 2013, 135). Signa was applying the current text to every publication, however old. A Benelux application published on 12 June 2013 was reported as open from 12 June to 12 August 2013 when the window the law gave ran from 1 July to 1 September 2013, a close up to 30 days EARLY. Only publications before 1 October 2013 are affected, every one of those windows is long closed, and no `status` changes; what changes is the pair of dates we report about them, which matters wherever a historical window is cited in a dispute or an audit trail. The rule carries both the superseded article text and the amending Protocol as pinned, era-bounded citations, so a pre-2013 window is served the text that actually governed it. *(This entry originally said `GET /v1/opposition-rules` reported the era behaviour through `window_starts_on` / `window_ends_on`. It did not; those fields describe the current era only. The era fields shipped on 3 September 2026; see the entry at the top of this changelog. BX now reports `window_change_date: "2013-10-01"`, `window_starts_on_before: "first_of_following_month"` and `window_ends_on_before: "corresponding_day_of_start"`, with `window_ends_on: null`.)* Era selection keys on the publication date, which is the only date either text can be read on. Neither Protocol carries a transitional provision. One documented limit follows: for a mark published between 1 August and 30 September 2013 the old text's window was still running when the new text took effect, and no source says which governs. We serve the old rule, and therefore the later close, which is the conservative direction for anyone deciding whether they may still oppose. **Reindex.** Opposition windows are baked into search documents at index time, so pre-2013 Benelux documents keep the old `opposition_window_opens` / `opposition_window_closes` values until BOIP records are reindexed. Every affected window is closed and none of them can change `opposition_status`, so this is low priority and belongs with the next natural BX reindex rather than with a dedicated run. Request-time surfaces are correct on deploy. ## September 3, 2026: German Madrid designations get an opposition window **New rule `de_opposition_madrid`.** An international registration designating Germany had no opposition window at all: `POST /v1/oppositions/compute`, the `derived` block, `GET /v1/opposition-rules` and watch diagnostics all returned nothing for a DPMA Madrid row. That was honest, because the rule could not be expressed, but silent, and the deadline it hides is a hard statutory bar. MarkenG § 114(1) puts the WIPO gazette in the place of the Markenblatt publication of a national registration, and § 114(2) starts the § 42(1) three months on the **first day of the month following** the gazette's issue month. Because that start is the beginning of a day, BGB § 187(2) counts it and § 188(2) Alt. 2 ends the period the day before the corresponding day: the last day of the third month. A gazette issued in March 2026 opens the window on 1 April 2026 and closes it on 30 June 2026; an issue in November 2026 runs 1 December 2026 to 28 February 2027, which is a Sunday, so BGB § 193 and DPMAV § 18a roll it to 1 March. The fee (250 EUR, plus 50 per further sign) falls due inside the same period, and the window is not extendable. MarkenG § 91(1) sentence 2 bars even Wiedereinsetzung. Worth noting next to the Swiss correction in this same release: Germany and Switzerland start their Madrid windows on the same day and close them one day apart. MSchV art. 2 ends a Swiss period on the corresponding day, BGB § 188(2) Alt. 2 ends a German one the day before. `window_ends_on` on `GET /v1/opposition-rules` reports which reading each rule applies. No German source gives a worked example of the end day, so it is derived from the two BGB provisions and cross-checked against the DPMA's own Richtlinie W 7736, which states the start in the office's voice; the derivation is recorded in the evidence pack rather than asserted. **Reindex.** Opposition windows are baked into search documents at index time, and DE Madrid documents currently carry null windows, so the `opposition_status` and `opposition_closes_before` / `opposition_closes_after` filters will not see this rule until German Madrid designations are reindexed. Request-time surfaces are correct on deploy. As for Switzerland, the reindex only becomes meaningful once designation records carry the WIPO gazette publication date the rule anchors on. ## September 3, 2026: Swiss Madrid opposition windows close up to 30 days earlier **Computed dates change for `ch_opposition_madrid`, in the safe direction.** The opposition window against an international registration designating Switzerland does not run from the day the WIPO Gazette published the designation. MSchV art. 50(1) starts it on the **first day of the month following** the Gazette month, and MSchV art. 2 ends a month period on the same-numbered day, so the window closes on the 1st of the fourth month. The IGE publishes the worked example: a Gazette of 19 July 2022 opens the window on 1 August 2022 and closes it on 1 November 2022. Signa was computing "Gazette day plus one month" to "Gazette day plus four months" instead. That is the same answer only when the Gazette date is itself the 1st; for every other issue the close we published was up to 30 days **later** than the law allows. A Gazette of 15 January 2026 was served a close of 15 May 2026 where the true, non-extendable deadline is 1 May 2026. Later than the truth is the dangerous direction on an opposition bar, so this is a correction customers should re-docket: every Swiss Madrid close moves earlier by nought to thirty days, and every open moves to the 1st of the month. `GET /v1/opposition-rules` now reports `window_starts_on: "first_of_following_month"` and `window_ends_on: "corresponding_day_of_start"` for this rule, and `window_starts_offset_months` is `null`. Both enums are new and additive; no existing value changed meaning. Trademark responses, the oppositions compute endpoint, the `derived` block and watch evaluation recompute the window per request, so they are correct as soon as this deploys. **Reindex.** Opposition windows are baked into search documents at index time, so `opposition_status` and the `opposition_closes_before` / `opposition_closes_after` filters carry the old arithmetic until Swiss Madrid documents are reindexed. In practice there is nothing to correct yet: a designation is indexed as part of its international-registration family, and IR-family documents carry null opposition windows today, while the Madrid source we ingest publishes no per-designation publication date for the rule to anchor on. The reindex therefore belongs with the work that starts storing the WIPO Gazette publication date for designations, not with this change. ## September 3, 2026: Norway Madrid opposition, and a publication-date guard for every Madrid rule **New rule.** `no_opposition_madrid` covers an international registration designating Norway. Patentstyret publishes in Norsk varemerketidende that the IR has effect in Norway (varemerkeloven § 70 annet ledd, varemerkeforskriften § 32), and § 71 første ledd gives anyone three months from THAT kunngjøringsdag to oppose, applying § 26 correspondingly. So every mechanic of the Norwegian domestic rule carries over by statute rather than by copy: the count starts on the publication day itself, the three-month period ends on the corresponding day-number, it rolls off 24 and 31 December, Saturdays, helligdager and likestilte dager under betalingsforskriften § 6, and it cannot be extended. There is no WIPO offset — § 71 runs straight from Patentstyret's own publication, unlike EUIPO's Madrid-EU rule. The 18-month Madrid refusal limit is an obligation the office owes WIPO and never changes the three months the parties get. `GET /v1/opposition-rules`, `POST /v1/oppositions/compute` (office `NO`, filing route `madrid`) and the `derived` block on a Norwegian Madrid designation now answer instead of returning nothing. One documented limit. The rule reports `effective_from: "2010-07-01"` and `applicable_from_date: "2010-07-01"`, the day varemerkeloven — the Act § 71 belongs to — entered into force (res. 25. juni 2010 nr. 936). Before it the repealed 1961 Act § 21 gave TWO months from the kunngjøringsdag, an era we do not model on the Madrid route, and serving the modern three months to it would report a close date a month LATER than the law gave. A Norwegian designation published before 1 July 2010 therefore comes back `supported: false` rather than with a window. **No reindex required.** This adds a window where there was none; it changes no existing window. Baked `opposition_window_opens` / `opposition_window_closes` values in the search index are all still correct. Norwegian Madrid designations pick their new window up at their next natural reindex. **A guard against the wrong publication.** A Madrid designation has two publications that can fill one date field — WIPO's Gazette of the international registration, and the designated office's own journal republication — and different offices run their clock from different ones. Feeding a rule the other one silently produces a plausible-looking, wrong statutory bar. Each Madrid rule now declares which publication it needs, and `POST /v1/oppositions/compute` accepts an optional `publication_date_kind` (`office_journal`, `wipo_gazette`, `office_register`, `unknown`) saying which one your date is. When the two disagree the response is `status: "unknown"` with null dates and the rule's identity intact, rather than a date you should not act on. A new nullable `reason` field on each `POST /v1/oppositions/compute` item and on the trademark `derived.opposition_window` block says WHY it is unknown (`"publication_date_kind_mismatch"` today, null otherwise), so you can tell a contradicted publication kind from a mark that simply has no publication date yet; the field is additive and null on every response that had no reason to carry it. Omit the field and nothing changes. Today EVERY Madrid rule declares its publication: `ch`, `de` and `fr` run from the WIPO Gazette; `no`, `fi`, `pl`, `sg`, `us`, `ca`, `au`, `eu`, `jp`, `kr`, `in` and `tr` run from the designated office's own journal. Declaring it is not optional in practice. A WIPO-feed leg is resolved by jurisdiction, and the date it carries is the Gazette's — so a rule that has NOT adjudicated which publication it runs from is refused on that path outright rather than dated on a publication nobody established it uses. The four Asian Madrid rules (`jp`, `kr`, `in`, `tr`) were adjudicated for this release; each of their statutes names the office's own gazette and says the WIPO Gazette is not the trigger. ## September 3, 2026: EU restitutio, Canada's transitional renewal window, India's priority-dated term, and WIPO closure days Four deadline corrections, all computed at request time. Nothing is baked into the search index, so every one of them is live the moment this deploys and no reindex is involved. **The EU now has a restoration deadline.** `POST /v1/deadlines/compute`, `GET /v1/deadline-rules` and the `derived` block on an EU trademark return a new `restoration` row once a renewal has passed its due date, under one new `rule_id`: `eu_restitutio`. It runs from the end of the six-month grace period to twelve months after the registration expired, which is what Article 104(2) EUTMR leaves after deducting the further six months of Article 53(3) from the year following the missed time limit, and what the EUIPO Guidelines state in one sentence: the one-year period "starts on the day on which the protection ends and not on the expiry of the further 6-month grace period". Read the row as an outer date, not as a promise. Restitutio is discretionary: it is granted only where all due care required by the circumstances was taken, it carries a fee, and it must also be requested within two months of the removal of the obstacle, a date no register publishes. No existing EU date moves. **Canada's first renewal after 17 June 2019 opens far earlier than we said.** For a trademark that was on the register before that day, Trademarks Regulations s. 159 lets the renewal fee for the FIRST renewal falling due on or after it be paid from the start of the period being renewed, with no six-month early limit, and CIPO says so directly: "there is no restriction on how early the renewal fee for the first renewal after June 17, 2019 may be submitted." The engine used to report that window opening six months before the due date. `window_opens` on the affected cycle now moves back to the day of registration or of last renewal, which can be fifteen years earlier. Only that one cycle is affected; the due date, the grace expiry and every later cycle are unchanged, and the window can only widen, so nothing that was filable becomes unfilable. **An Indian trademark with a convention priority claim expires earlier than its filing date implies.** Trade Marks Act 1999 s. 154(2) registers a Paris or WTO convention application as of the date the application was made in the convention country, and s. 23(1) is expressly subject to it, so the ten-year term of s. 25(1) runs from the priority date, up to six months before the Indian filing. `POST /v1/deadlines/compute` accepts a new optional `priority_date` on each item; send it for an IN item and the renewal is computed from it, with `trigger_field` reading `priority_date` so the answer says which date it used. Omit it and India falls back to the filing date exactly as before. The field is inert for every other jurisdiction: no other pinned term provision runs from a priority date. Note the current limit: our own stored trademarks carry priority claims in a separate record, so the `derived` block, portfolio deadline lists and the iCal feed do not yet pass one and still serve the filing-date answer for that cohort, which is the later date. Where the office states an expiry, that expiry already anchors the schedule and the question does not arise. **WIPO renewal dates now move off days the International Bureau is closed.** Rule 4(4) of the Regulations under the Madrid Protocol extends a period that expires on a day the IB is not open to the public to the first subsequent day it is, and a renewal payment is a communication the IB must receive. WIPO rows now report `holiday_calendar` `wipo_geneva`, and a due date or grace expiry that falls on a weekend or on one of the eight days WIPO published for 2026 (1 January, 3 and 6 April, 14 and 25 May, 31 July, 25 and 31 December) is served on the next open day with the adjustment field reading `moved`. An international registration dated 13 June 2016, for example, was renewable "by 13 June 2026", a Saturday; the answer is now Monday 15 June 2026. The International Bureau publishes its closure list one year at a time, so the calendar covers 2026 only: dates in any other year are emitted exactly as the Rule 4(1) and 4(2) arithmetic produced them and report `not_checked` rather than a business-day guarantee we cannot make. Filing-window opening dates are never rolled, on any office. Every value above cites the instrument it comes from, with a verbatim quote and a hash-pinned snapshot, on `GET /v1/deadline-rules` and on each compute response. ## September 3, 2026: Mexican OPPOSITION windows follow the same month-overflow rule, and the overflow is now reported **Opposition close dates change for MX (IMPI). Reindex MX.** Earlier today Signa applied Reglamento de la Ley de la Propiedad Industrial art. 4o, párrafo segundo to the Mexican maintenance ladders: a period counted in months that ends on a day the target month does not have runs into the following calendar month and ends on its first business day, rather than clamping to the short month's last day. The opposition engine kept the clamp, which was stated as a limit in that release. It no longer does. Art. 4o computes «los plazos establecidos en meses o años a que se refiere el artículo 184 de la Ley», and art. 184 LPI (re-enacted word for word as LFPPI art. 21) is the Law's general period-computation article, not a maintenance-only provision. The one-month opposition period of LFPPI art. 221 is a period fixed in months by the Law, so it is inside art. 4o. An application published on 31 March 2026 now closes for opposition on **Monday 4 May 2026**, not 30 April: April has no 31st, so the period runs into May, and May 1 is Día del Trabajo with the weekend behind it. A publication on 31 August 2026 closes 1 October instead of 30 September. Publications whose day number exists in the next month are untouched. Every corrected date is LATER than the one Signa served before, so nobody who relied on the old date filed late. `GET /v1/opposition-rules` gains `month_end_overflow` on every rule, so a client that recomputes a window from the rule can reproduce the close date. It reads `last_day` everywhere except `mx_opposition`. **The overflow move is now reported.** `due_date_adjustment` and `grace_expiry_adjustment` on `POST /v1/deadlines/compute` gain a fourth value, `month_end_overflow`. Before this, an overflow that landed on an open day was indistinguishable from a date nothing had happened to: the fields described the weekend and holiday roll only, so they read `unchanged`. The new value subsumes the roll on such a date, because art. 4o para. 2 names the first BUSINESS day of the following month, so the move into the next month and the roll that completes it are one statutory step. The addition is additive: no existing value was renamed or removed. Opposition windows are baked into the search index at index time, so the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` filters on MX documents are corrected only after **MX is reindexed**. `POST /v1/oppositions/compute` and the trademark response recompute per request and are correct as soon as this deploys. ## September 3, 2026: Mexican deadlines now roll, month-overflow ends in the next month, and Madrid designations renew on the international registration **Computed dates change for MX (IMPI), on both engines. Reindex MX.** Mexican maintenance dates were published raw: a renewal that fell on a Sunday or on a day IMPI declares inhábil was served on that day. Reglamento de la Ley de la Propiedad Industrial art. 4o, the 1994 regulation the LFPPI keeps in force, says otherwise in its third paragraph, and Signa now applies it. A renewal due date, grace end or 3-year declaration-of-use window end that lands on a weekend, on a fixed Mexican holiday or on a día inhábil IMPI has published moves to the next working day, `holiday_calendar` reads `mx_mexico`, and the two adjustment fields read `moved` or `unchanged`. Filing windows never move: a window that opens on a closed day cannot be filed that day, and moving it would shorten the statutory six months. The same article's second paragraph fixes a subtler error. When a period counted in months or years ends on a day the target month does not have: a six-month grace counted from a 31st, landing on "31 February". Mexico does not clamp to the 28th the way most offices do. The period ends on the **first business day of the following calendar month**. A grace period that used to read 28 February 2033 now reads 1 March; in a leap year, 29 February 2036 becomes Monday 2 March 2036. This is the first office in the corpus to depart from the month-end clamp, and it is behind a per-office flag, so no other jurisdiction's dates move. **The IMPI holiday calendar was rebuilt from the Diario Oficial.** It carried a flat "IMPI closes 16 to 31 December, every year" block that no published acuerdo supports. IMPI sets its year-end closure annually: 18 December to 2 January for 2024, 19 December to 3 January for 2025, 22 December to 6 January for 2026. So the old calendar closed four days IMPI works and opened five days it does not, in both directions. It also omitted 5 May and 1 September, which four published acuerdos name. The calendar is now eleven recurring days plus exactly what each pinned acuerdo states, including the one-off closures of 1 October 2024, 12 December 2025, 23 February 2026 and 30 June 2026. **Madrid designations of Mexico now renew on the right date.** A designation has no independent Mexican term: it expires with the international registration and is renewed centrally at WIPO. The rule that served it was Mexico's national "renewal plus declaration of use", which the engine anchored on IMPI's grant date, and for a designation granted years after the international registration, that is a renewal date up to a decade too late. There is now a separate rule, `mx_renewal_madrid`, anchored on the international registration date, and a designation whose international registration date is unknown and whose office expiry is not on the record DECLINES with `requires_office_date` instead of publishing a guess. The declaration of use that follows a Madrid renewal runs three months from the International Bureau's notice, a date no record carries, so Signa deliberately publishes nothing for it rather than a wrong date. Every Mexican value is now cited to the Diario Oficial de la Federación itself (the primary publication) with a verbatim quote and a pinned snapshot, with the consolidated ordenjuridico.gob.mx copies kept alongside. That includes the eight IMPI acuerdos behind the calendar. Three limits are stated rather than hidden. IMPI publishes each December's closure in the preceding autumn, so a year with no published acuerdo has no closure in Signa: a date in late December 2026 or later is served un-rolled, which is earlier than the true date, never later. The opposition engine still applies the ordinary month-end clamp rather than art. 4o's overflow rule, again in the earlier direction. And the overflow move itself is not reported in the adjustment fields, which describe the weekend and holiday roll only. Trademark responses and `POST /v1/deadlines/compute` recompute per request, so they are correct as soon as this deploys. Opposition windows are baked into the search index at index time, so the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` filters on MX documents are corrected only after **MX is reindexed**. ## September 3, 2026: Australian renewal grace periods roll onto the next open day An Australian trade mark can still be renewed for six months after it expires (Trade Marks Act 1995 s 79). When the last day of that six-month period falls on a weekend, a Canberra public holiday or IP Australia's standing Christmas close-down, `POST /v1/deadlines/compute` and the `derived` block on a trademark now serve the next day the office is open, and say so in `grace_expiry_adjustment` (`moved`) and `holiday_calendar` (`au_australia`). Australian grace dates were previously served one to three days EARLY. The renewal DUE date deliberately does NOT move, and this is the only office where the two answers differ. s 223A(1) rolls "the last day of a period provided by this Act (except this section) for doing an act"; the s 79 period is one, reg 21.24A supplies the prescribed circumstance and reg 21.24C, the exclusion list, does not reach a renewal request. The twelve-month renewal REQUEST period is a different animal: it is prescribed by reg 7.3 rather than by the Act, and Schedule 9 item 10 keys the \$100-per-month surcharge on the request being made "after the expiry date to which section 79 of the Act refers": the expiry day itself. Serving a later due date would tell you that you may still renew fee-free on the Monday when the surcharge may already have attached on the Saturday. So an Australian `due_date` stays on the statutory expiry and reports `due_date_adjustment: "not_checked"` on every row, whatever day of the week it lands on: no calendar was consulted for it, and `unchanged` would claim a Saturday is a business day. Nothing else about the Australian rule changed: same `rule_id` (`au_renewal`), same ten-year term, same twelve-month window, same six-month grace, same consequence. Three provisions are newly pinned and quoted verbatim next to the field they move: s 223A(1), reg 21.24A, and reg 21.24C in full, so that the absence of a renewal request from the exclusion list is verifiable rather than asserted. Maintenance deadlines are computed per request, so this is live as soon as it deploys and no reindex is required. It supersedes the "Australia is deliberately NOT rolled" note in the entry below. ## September 3, 2026: renewal dates roll on closed office days in Norway, France and Iceland, and on BOIP's own closing days in the Benelux Four offices join the maintenance rollover: a renewal due date or grace expiry that lands on a day the office is closed is now served on the next day it is open, instead of on the exact calendar date. `POST /v1/deadlines/compute` and the `derived` block on a trademark answer with the moved date and say so in `due_date_adjustment` / `grace_expiry_adjustment` (`moved`, `unchanged` or `not_checked`) and `holiday_calendar`. Nothing about a rule id, a window length or a consequence changed; only the served dates. Each office rolls on the provision that governs its own renewal deadline, not on a generic weekend rule. Norway: betalingsforskriften § 6 første ledd (24 and 31 December, Saturdays, helligdager and days the legislation treats as equivalent), extended to varemerkeloven deadlines by § 6 tredje ledd. France: CPI R. 718-2 al. 5, applied to the renewal declaration by INPI's own renewal directives, which work the example: a mark expiring Saturday 14 May 2022 could be renewed on Monday 16 May with no late-renewal fee. Iceland: Stjórnsýslulög nr. 37/1993 8. gr., which extends a period ending on a general public holiday to the next day the authority is OPEN. Benelux: Uitvoeringsreglement regel 3.9, lid 3. Every one is pinned and quoted verbatim next to the field it moves. Two office calendars were corrected from primary sources in the same change, and both corrections can move a date on their own. The Benelux calendar is now BOIP's own closing-day list, published as a Regel van de Directeur-Generaal of 3 April 2019, rather than an assumed Dutch national-holiday set. Ten days join it: 2 January, 1 May, Europe Day, the Luxembourg and Belgian national days, Assumption, All Saints, All Souls, Armistice Day and the Belgian King's Day, and Good Friday LEAVES it, because the rule names it on neither limb and BOIP is open that day. A Benelux deadline falling on Good Friday is therefore due that day, not the following Tuesday. Iceland's 24 and 31 December STAY CLOSED, and no Icelandic date moves on that account. An earlier draft of this release opened both days: they are public holidays only FROM 13.00 under lög nr. 88/1971 6. gr. (24 December only from 18.00 as a church holiday, lög nr. 32/1997 2. gr.), so the statute alone gives a half day, and no ISIPO closure notice had been found. One was then found: Hugverkastofan's own December 2020 notice, which says the office is closed on Þorláksmessa, aðfangadagur and gamlársdagur and that every deadline falling in that period moves to the next opening day. The office's own statement settles the office's own opening days, so both remain closed, exactly as before this release. See "Philippines, India and Iceland business-day calendars re-sourced" below. Benelux opposition windows DO move, because Good Friday left `bx_benelux`. Opposition windows are materialised into search documents at index time, so `opposition_status` and the `opposition_closes_*` filters carry the corrected dates for BX records only after that office is reindexed. Every trademark response recomputes the window per request and is correct as soon as this deploys, and maintenance deadlines are computed per request throughout, so no reindex is needed for them. Australia was NOT rolled in this change, and the reason was documented rather than hidden: Trade Marks Act 1995 s 223A(1) settles the six-month post-expiry limb but not the due date, and the rollover was one switch that moved both dates. SUPERSEDED the same day by the entry above, which gives the engine a grace-only roll and turns it on for Australia. Two accuracy limits are recorded with the same candour. INPI closure decisions before 2026 were never fetched, so a French date that fell on a pre-2026 closure day is served un-rolled. BOIP publishes any FURTHER closing day for a given year on a page that is not reachable programmatically, so those ad-hoc days are not modelled either. Both under-roll rather than over-roll. ## September 3, 2026: EUIPO Article 2 days now extend time limits; `consequence_if_missed` vocabulary fixed **EU dates move later on four days a year.** The `euipo_alicante` calendar now carries BOTH lists of the Executive Director's annual decision on non-working days: the Article 1 days on which the Office is closed AND the Article 2 days on which the Office is open but ordinary post is not received (19 March, 23 June, 9 October, 8 December in EX-24-9 for 2025 and EX-25-16 for 2026). Article 69(1) EUTMDR extends a time limit expiring "on a day on which the Office is not open for receipt of documents or on which ... ordinary post is not delivered in the locality in which the Office is located ... until the first day thereafter on which the Office is open for receipt of documents and on which ordinary mail is delivered", and the Office's own decision reads it the same way (EX-24-9 recital (1)(b)). Until now only Article 1 days rolled, which served the earlier date; the served date is now the statutory one. Affected: `POST /v1/oppositions/compute` and `opposition_window` for `EM` marks whose window closes on one of those days (e.g. a Part A publication on 9 July 2026 now closes 13 October 2026, not 9 October), the EU office-action response deadlines, and the watch-evaluator's opposition windows. Opposition windows are baked into the search index at index time, so indexed `EM` documents carry the corrected close only after the EU reindex (scheduled with this release). The 2027-2030 Article 2 days are carried on the same estimate basis as the Article 1 days and are replaced each December from the published decision. **EU RENEWAL dates move on the same days.** An earlier draft of this entry said they did not, on the ground that the EU maintenance config does not business-day adjust. It does now: the same release turns on the roll for EU renewals, so a `eu_renewal` due date or grace expiry landing on any day the Alicante calendar carries, Article 1 closures and Article 2 ordinary-post days alike, is served on the next open day instead. A mark filed 19 March 2016 was served a renewal due 19 March 2026 and a grace expiry 19 September 2026; it is now due 20 March 2026 with grace to 21 September 2026, because 19 March is San José, an Article 2 day. The direction is later, and later is the safe direction for a renewal: the office is shut on the original date, so the served day is the first one on which you can actually file. `due_date_adjustment` and `grace_expiry_adjustment` read `moved` on those rows and `unchanged` on the rest, so a row always says whether a calendar was consulted. Renewal dates are not baked into the search index, so no reindex is needed for this half. **`consequence_if_missed` now means one thing per value.** The value names the mechanism the office's pinned provision prescribes: `expiration` (the right ends by operation of law), `removal_from_register` (the office strikes the entry as the statutory consequence), `cancellation` (the office cancels a still-running registration for a missed maintenance filing, US §8/§71), and restorability is said only by the presence of a `restoration` rule. Nine rules were relabelled to match their pinned text; no date moved: `pl_renewal` `cancellation` → `expiration` (PWP art. 168 ust. 1 pkt 1 "wygasa"), `fr_renewal` `cancellation` → `expiration` (CPI L. 712-1), `de_renewal` `cancellation` → `removal_from_register` (§ 47 Abs. 6 MarkenG "gelöscht"), `dk_renewal` `cancellation` → `removal_from_register` (varemærkeloven § 37 "udslettelse"), `eu_renewal` `cancellation` → `removal_from_register` (Art 53(8) EUTMR "cancel the mark from the register"), `bx_renewal` `irrevocable_loss` → `expiration` (BVIE art. 2.26(a)), `ch_renewal` `irrevocable_loss` → `removal_from_register` (MSchG art. 35(b)), `mx_declaration_of_use_3yr` `irrevocable_loss` → `expiration` (LFPPI art. 233 "caducará de pleno derecho"), `th_renewal` `removal_from_register` → `expiration` (Trademark Act s. 56 deems the registration revoked). `irrevocable_loss` is retired: no rule serves it. The full vocabulary is in the [deadline rules guide](/guides/deadline-rules#consequence-vocabulary). No wire-format change: the field stays a string and no new value was added. ## September 3, 2026: Philippines, India and Iceland business-day calendars re-sourced Three office calendars used for business-day adjustment now rest on the primary instruments that actually set the closed days, and two of them change published dates. **Philippines (`ph_philippines`, used by `ph_opposition`).** The closed days are now transcribed year by year from the presidential proclamations that declare them, Proclamations 368 s. 2023, 453, 514, 579 and 665 s. 2024 for 2024; 727 and 729 s. 2024, 839, 878 and 911 s. 2025 for 2025; 1006 s. 2025, 1189 and 1264 s. 2026 for 2026, together with the statutory fixed days (RA 9492 s. 1, RA 10966) computed for every year. Three curated days were wrong and move: Eid al-Fitr 2025 is 1 April (not 31 March), Eid al-Adha 2025 is 6 June (not 7 June) and Eid al-Fitr 2026 is 20 March (not 21 March). Days the proclamations make working days are now OPEN (25 February 2025 and 2026; 21 August 2024, observed on 23 August), and days they add are closed (9 February 2024, 12 May 2025, 31 October 2025, 2 November 2026, every Christmas Eve). For 2027 onward, until each year's proclamation is pinned, only the statutory fixed days are closed and the "Monday nearest" holidays are open: an opposition close on one of those days is published un-rolled, which is never later than the true date. The opposition extension note now cites IPOPHL Memorandum Circular 2022-013 itself: one 45-day extension, granted only once, on meritorious grounds, reckoned from the original due date, in force 30 June 2022. **India (`cgpdtm_india`, new).** Indian deadlines are now business-day adjusted. The Trade Marks Registry observes the annual DoPT Office Memorandum for central government offices; the 2024, 2025 and 2026 memoranda are pinned and their 14 compulsory holidays per year make up the calendar. `in_renewal`, `in_restoration`, `in_opposition` and `in_opposition_madrid` now report `holiday_calendar: "cgpdtm_india"` and `moved` / `unchanged` for 2024–2026 dates: a renewal due on Sunday 15 June 2025 is served as Monday 16 June; an opposition closing on Republic Day 2026 is served as 27 January. Dates after 2026 report `not_checked` until the 2027 memorandum is pinned. Three limits are documented, all in the safe direction: the three branch-local optional holidays (Mumbai, Delhi, Kolkata, Chennai and Ahmedabad differ), Ambedkar Jayanti, and any moon-sighting shift of an Islamic day are not modelled, so a deadline on one of them is published un-rolled. **Iceland (`is_iceland`).** No date changes. 24 and 31 December stay closed, now on the office's own statement (Hugverkastofan's December 2020 notice: closed on both days, deadlines falling on them move to the next opening day) and the statutes that make them half-day frídagar; 23 December stays open because the office declares it year by year. Trademark responses recompute opposition windows per request, so they are correct as soon as this deploys. The materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters are baked at index time: IN and PH search documents carry the rolled and corrected closes only after those offices are reindexed. Maintenance rows are computed per request and need no reindex. ## September 3, 2026: Korea (KIPO) deadline and opposition rules New jurisdiction. `KR` now has a maintenance config and two opposition rules, so `GET /v1/deadline-rules`, `GET /v1/opposition-rules`, `POST /v1/deadlines/compute`, `POST /v1/oppositions/compute` (office `KR`) and the `derived` block on a trademark answer for Korean marks instead of returning nothing. Three new `rule_id` values: `kr_renewal`, `kr_opposition`, `kr_opposition_madrid`. Renewal runs 10 years from the REGISTRATION date (상표법 제83조①), with a 12-month filing window before the term ends that closes on the expiry day and a 6-month window after it at 330,000 KRW per class instead of 300,000 (제84조②, 제78조③). Korean law has no restoration of a missed renewal application and no declaration of use, so neither is emitted — that absence is the finding, not a gap. Opposition is 30 days from KIPO's own 출원공고 for applications published on or after 22 July 2025 (법률 제20697호), and 2 MONTHS for publications before that day; an international registration designating Korea is opposed on the identical window, running from KIPO's publication rather than the WIPO Gazette (제180조①). Every value cites the Act, the fee rules or the KIPO publication it comes from, with a verbatim quote and a pinned snapshot. Two things about dates deserve a direct statement. First, the 22 July 2025 change is the first opposition pivot that switches UNITS — calendar months before, a day count after — and the engine now resolves the era's length with its unit; nothing moves for any other office. Second, Korean deadlines are business-day adjusted against the `kipo_korea` calendar, which this release corrects from primary sources: 1 May (노동절) is now a closed day in every year — 상표법 제16조제4호 names it, and the shipped calendar had omitted it — with a Saturday/Sunday substitute from 2026; 17 July (제헌절) is closed from 11 May 2026, when the amended 관공서의 공휴일에 관한 규정 made every 국경일 a public holiday again; and 28 September 2026 is OPEN, because the regulation substitutes a 설날/추석 day only when it falls on a Sunday and 26 September 2026 is a Saturday. A KIPO deadline that lands on one of those days moves accordingly. Four limits are documented rather than hidden. The renewal `window_opens` is one day earlier than KIPO's own count (KIPO opens the window the day AFTER expiry minus one year); it is never rolled and never a deadline. Election days and 임시공휴일 designated by government notice are not in the calendar until the notice is pinned, so a deadline landing on one is published un-rolled — earlier than the true date, never later. A renewal filed ON the expiry day is modelled as surcharge-free, which is how KIPO's example reads. And a Madrid designation is computed only when its international registration date is known: the KIPO connector must carry the IRN, and its publication date for a designation must be KIPO's own 출원공고일. Trademark responses recompute opposition windows per request, so they are correct as soon as this deploys. Only the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters are baked at index time, so KR search documents carry these values only after KR is reindexed; no KR records are indexed today, so that reindex belongs with the KIPO connector rather than with this change. ## September 3, 2026: Türkiye dates now roll, pre-2018 renewal windows and the 2017 opposition straddle corrected **Computed dates change for TR (TÜRKPATENT), on both engines.** Three of the limits documented when Türkiye landed on 2 September are closed. **Business-day adjustment is now applied.** A Turkish renewal due date, grace end or opposition close that falls on a Saturday, Sunday, one of the fixed holidays of Law 2429 (1 January, 23 April, 1 May, 19 May, 15 July, 30 August, 29 October) or a Ramazan or Kurban Bayramı day is served on the next working day, so `holiday_calendar` reads `turkpatent_turkiye` and the two adjustment fields read `moved` or `unchanged` for dates in 2024 to 2030 (`not_checked` outside that range, where the calendar is not curated). The bayram days come from Diyanet İşleri Başkanlığı's official tables for each year; the arefe half days, the afternoon of 28 October and the Presidency's ad-hoc administrative leave days are working days and never move a date. This rests on a documented convention rather than an office statement: no TÜRKPATENT source states how a last day on a non-working day is treated before the office, and the roll applies the rule the general procedural statutes give the courts (HMK 6100 m. 93/1, İYUK 2577 m. 8/2). Direction of the change: every affected date moves LATER, by one to five days (for example, a renewal due on 1 May 2026, a Friday holiday, is now served 4 May; an opposition closing on 27 May 2026, the first day of Kurban Bayramı, is now served 1 June). **Renewal cycles whose anniversary fell before 10 January 2018** now show the request window the repealed KHK 556 m. 41/3 gave them: `window_opens` and `due_date` move to the last day of the anniversary's month (up to 30 days later than before), while `grace_expiry` stays on the exact anniversary plus six months, because m. 41/5 anchors the lapse on the end of the term. Every such cycle is historical (visible with `include_missed`); no current deadline moves. **Opposition windows for applications filed before 10 January 2017 but published on or after it** now get the three months the KHK gave them (SMK geçici m. 1/1 concludes an application under the law in force on its filing date), one month later than before; the served citation follows. Windows for applications filed on or after that date are unchanged. `POST /v1/oppositions/compute` accepts an optional `filing_date` per item for this; omit it and the item falls back to the publication date, which yields the shorter modern window. This filing-date key applies to national applications (`tr_opposition`) only. Madrid designations (`tr_opposition_madrid`) stay keyed on the publication date: a designation's `filing_date` is the international registration date, and a subsequent designation of Türkiye carries no date of its own, so a filing-keyed pivot would have served a subsequent designation made after 10 January 2017 on an older international registration three months instead of the two SMK m. 18/1 gives it, one month too late. An original designation on a pre-2017 international registration published on or after 10 January 2017 is therefore served two months where the KHK arguably gave it three; every such window closed in 2017. Trademark responses and `POST /v1/deadlines/compute` recompute per request, so they are correct as soon as this deploys. **TR reindex required:** the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters read window dates baked at index time, so Turkish search documents carry the rolled and filing-keyed close dates only after TR is reindexed. ## September 3, 2026: Danish renewal dates on office closing days now roll **Computed dates change for DK (DKPTO).** A Danish renewal due date or grace end that falls on a Saturday, Sunday, a Danish public holiday or one of DKPTO's enumerated closing days is now served on the next working day, as the office's own Varemærkehåndbog states ("Er sidste rettidige indbetalingsdato en dag, hvor Patent- og Varemærkestyrelsen ikke har åbnet … så er betaling førstkommende hverdag rettidig") and as dkpto.dk/lukkedage confirms for the closing days ("På officielle lukkedage, vil alle frister automatisk blive forlænget til førstkommende hverdag"). Rows now read `holiday_calendar: "dk_denmark"` with `moved` or `unchanged` adjustments instead of `not_checked`. Direction of the change: dates move LATER only, by one to seven days (a mark registered 10 January 2017 was due 10 January 2027, a Sunday, and is now due 11 January; a renewal due 28 December 2026 rolls through the 28 to 30 December closure, New Year's Eve, New Year's Day and the weekend to 4 January 2027\). Marks whose dates already fall on a working day do not move. Computed at request time; no reindex (maintenance deadlines are not indexed). ## September 2, 2026: Philippines Renewal DAU window opens six months earlier **Computed dates change for PH (IPOPHL).** The `ph_declaration_of_use_renewal` rule (the Declaration of Actual Use due within one year of each renewal) used to open its filing window at the renewal itself. IPOPHL's Trademark Regulations of 2023 (Memorandum Circular 2023-001, in force 14 February 2023) say in Rule 206 that "The Renewal DAU may be filed within the six-month period before the expiration of the registration", so the window now opens six months before expiry - together with the renewal window - for every renewal cycle expiring on or after 14 February 2023. Cycles that expired before that day keep the old window: the change is modelled as an era pivot inside the rule, keyed on the cycle, not on today's date, and a window for a cycle expiring inside the first six months of the new regime opens on 14 February 2023 itself, because no filing could precede the Regulations that permit it. Only `window_opens` moves. `due_date` and `grace_expiry` are unchanged (one year after the renewal, no grace), and so is every other Philippine rule. The customer-visible effect is an earlier `window_open` status: a mark registered 5 January 2020 now shows its first Renewal DAU window opening 5 July 2029 instead of 5 January 2030. Under the 2017 Regulations (MC 17-010) Rule 206 was only a transitional trigger, which is why the earlier window is right for the earlier cycles; both texts are pinned with verbatim quotes, and the in-force date is pinned from WIPO Lex because the circular's own Rule 1406 carries it handwritten. Maintenance deadlines are computed per request, so no reindex is involved. ## September 2, 2026: Philippines Madrid designations get their own DAU anchors **Computed dates change for PH (IPOPHL) international registrations.** Two new `rule_id` values, `ph_declaration_of_use_3yr_madrid` and `ph_declaration_of_use_5yr_madrid`, serve marks on the `madrid` route. Under IPOPHL's Madrid Regulations (MC 17-011, Rule 20) the 3rd-year DAU of an international registration designating the Philippines runs "within three years from the date of the international registration" and the 5th-year DAU "within one (1) year from the fifth anniversary of the date of statement of grant of protection" - not from the national filing and registration dates the `ph_declaration_of_use_3yr` and `ph_declaration_of_use_5yr` rules use. Those two rules now apply to the national route only (`applies_to: domestic`); the Madrid rules read `applies_to: madrid_only`, trigger on `intl_registration_date` and `protection_grant_date` respectively, and are absent when the record does not carry that date rather than guessed from another one. A designation with no international registration date still returns no schedule at all (`requires_office_date`), as before. The Renewal DAU and Mid-Renewal DAU are unchanged for designations: Rule 20 runs them from the renewal of the registration, which for an international registration is the WIPO renewal on the international registration date the renewal row already uses. One limit is documented rather than hidden: a SUBSEQUENT designation's 3rd-year DAU legally runs from the recording of that designation, and the engine anchors it on the international registration date instead, which is never later. No reindex is involved. ## September 2, 2026: Swedish publication and expiry dates corrected (PRV connector) Two corrections to how we read PRV's bulk feed. Neither changes a stored record until the PRV backfill and reindex have run, so existing responses are unchanged at deploy time; the dates below move once that maintenance completes. **`publication_date` for national registrations since December 2022.** PRV's feed dates the registration notice by gazette issue, one day (occasionally two or three) after the registration decision. PRV counts the opposition period from the kungörelsedag, which is the registration day itself, and its own register shows the opposition period starting on that day. We now take the registration day as `publication_date` for the registration notice; the gazette issue date stays on `publications[].publication_date`. After the backfill and reindex, `publication_date` on affected Swedish marks moves one day earlier (to `registration_date`), and every Swedish opposition window computed from it (`derived.opposition_window`, the `opposition_status` and `opposition_closes_*` filters) moves one day earlier at both ends. Together with the opening-day correction below, a mark registered on 1 April now shows a window of 1 April to 1 July, matching PRV, instead of 3 April to 2 July. Marks registered before December 2022 already carried the registration day and do not move. Madrid designations of Sweden are not touched by this change. **`expiry_date` for registrations from 2019 on.** PRV reports a ten-year expiry for every registration, but our far-future date guard (built for a handful of corrupt year-2077 and year-5014 owner dates) rejected any date more than two years out, so Swedish registrations from 2019 on, and older marks whose latest renewal runs past 2028, were served with no `expiry_date` and no `renewal_due_date`. Both are now ingested as reported. Renewal deadlines for these marks anchor on PRV's own date once it is stored; where that differs from the statutory computation we used before, PRV's date wins. ## September 2, 2026: Canada (CIPO) opposition extension benchmark before December 2023 **No computed date moves.** `window_opens`, `window_closes` and `status` for Canadian marks are unchanged; this corrects the advisory `common_extension` field only, and only for the direct (non-Madrid) route. The Trademarks Opposition Board rewrote its "Practice in trademark opposition proceedings" on 1 December 2023 and cut the benchmark first extension to file a statement of opposition from 4 months (with a cooling-off extension of up to 9 months on consent) to 2 months (up to 7). The `ca_opposition` rule served the post-2023 figure for every window. The office's own transition rule keys on when the extension request was received ("requests for extensions of time received on or after December 1 will be subject to the revised extensions of time"), and a non-retroactive s. 47(1) request is received no later than the deadline it extends, so the rule now keys on the served close date: * `derived.opposition_window.common_extension` for a CA direct-route window that **closes before 1 December 2023** now reads `months: 4` (was 2) with notes naming the 2019 notice and the 9-month cooling-off figure. * A window closing **on or after 1 December 2023** keeps `months: 2`; its notes are rewritten (they no longer claim the window is "otherwise non-extendable" - the 7-month cooling-off extension exists) and name the straddle: a window that opened before 1 December 2023 and closes on or after it may have been extended under either figure depending on when the request was received. * `GET /v1/opposition-rules` continues to show the current (2-month) tier for `ca_opposition`; the rule's citations gain the superseded 2019 notice and CIPO's own side-by-side changes page, both pinned from the Wayback Machine because the office has removed them. * `ca_opposition_madrid` is untouched: Trademarks Regulations s. 125 caps the Protocol-application extension at 4 months in every era. One documented limit: the 4-month figure is verified for the notice in force from 17 June 2019. The benchmark under the 2009 notice it replaced is not pinned, so a window closing before 17 June 2019 is served the 4-month figure with notes saying so. No search filter reads `common_extension`, so no reindex is needed. ## September 2, 2026: MX oppositions before 30 August 2016 are no longer served **Computed dates change for MX (IMPI).** Mexico had no pre-grant opposition procedure until the decree published in the Diario Oficial de la Federación on 1 June 2016, whose sole transitorio puts it in force "a los 90 días naturales siguientes a su publicación" - 1 June 2016 plus 90 calendar days, i.e. 30 August 2016. The same decree introduced the pre-grant publication of the application, so an MX record carrying a publication date before that day is showing a registration publication, which never opened an opposition period. The `mx_opposition` rule now declares `applicable_from_date` and `effective_from` of `2016-08-30`, and MX publications earlier than that return no opposition window at all. Direction of the change: windows are REMOVED, never added. Nothing about a publication on or after 30 August 2016 moves - the one-month non-extendable period, its day-after start and its rollover calendar are unchanged. If you read an opposition window for an MX mark published before 30 August 2016, that window was wrong and is now absent. Trademark responses recompute the opposition window per request, so they are correct as soon as this deploys. The `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters read values materialised at index time, so until the MX index is rebuilt those filters may still match Mexican marks published before 30 August 2016 even though the returned record now carries no opposition window. ## September 2, 2026: India (CGPDTM) deadline and opposition rules New jurisdiction. `IN` now has a maintenance config and two opposition rules, so `GET /v1/deadline-rules`, `GET /v1/opposition-rules`, `POST /v1/deadlines/compute` and the `derived` block on a trademark answer for Indian marks instead of returning nothing. Four new `rule_id` values: `in_renewal`, `in_restoration`, `in_opposition`, `in_opposition_madrid`. Renewal runs 10 years from the APPLICATION date, not the grant: s. 23(1) of the Trade Marks Act 1999 registers a mark "as of the date of the making of the said application". The filing window opens a year before expiry (Rules 2017 r. 57(1)), a 6-month surcharge window follows expiry, and restoration is available after 6 and within 12 months from the expiry itself. Marks registered before the Act commenced on 15 September 2003 run 7-year periods until the first period that starts on or after that date. Opposition is 4 months from publication of the Trade Marks Journal issue that advertised the application, counted from the day after (General Clauses Act 1897 s. 9(1)) and non-extendable; it was 3 months for applications published before 8 July 2013. An international registration designating India is opposed on the same 4-month window, running from the INDIAN Journal republication rather than the WIPO Gazette. Every value cites the statute, the Rules or the CGPDTM practice document it comes from, with a verbatim quote and a pinned snapshot. Six limits are documented rather than hidden. No closed-day roll is applied to any Indian date: the law requires one but the Registry publishes no consolidated holiday list, so `holiday_calendar` is `null` and every adjustment reads `not_checked` rather than a guess. A mark claiming convention priority expires on its PRIORITY date plus ten years, which the engine cannot compute without an office-supplied expiry. A mark registered late may renew within six months of its actual registration, so its real due date can be later than the one published. A mark filed before 15 September 2003 but registered after it takes a ten-year first term, where the schedule gives seven. And nothing in the Act or Rules settles whether a renewal filed ON the expiry date avoids the surcharge; the conservative reading is modelled. Lastly, the anchor of the pre-2003 seven-year terms is not pinned to a quotable source: s. 159(6) evidences the seven-year length but not the date it runs from, and India Code's scan of the 1958 Act carries no text layer to quote, so if the filing-date anchor is wrong the direction of the error is unknown. Trademark responses recompute opposition windows per request, so they are correct as soon as this deploys. Only the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters are baked at index time, so IN search documents carry these values only after IN is reindexed; no IN records are indexed today. ## September 2, 2026: Türkiye (TÜRKPATENT) deadline and opposition rules New jurisdiction. `TR` now has a maintenance config and two opposition rules, so `GET /v1/deadline-rules`, `GET /v1/opposition-rules`, `POST /v1/deadlines/compute` and the `derived` block on a trademark answer for Turkish marks instead of returning nothing. Three new `rule_id` values: `tr_renewal`, `tr_opposition`, `tr_opposition_madrid`. Renewal runs 10 years from the FILING date, with a 6-month filing window before the term ends and a 6-month window after it against an additional fee; missing both ends the right. Turkish law has no restoration and no declaration of use, so neither is emitted — that absence is the finding, not a gap. Opposition is 2 months from the publication of the APPLICATION in the Bülten, non-extendable, and identical for Madrid designations (SMK m. 14/1 makes an international application produce the same results as a direct one); publications before 2017-01-10 get the 3 months the repealed KHK 556 gave. Every value cites the statute or the TÜRKPATENT publication it comes from, with a verbatim quote and a pinned snapshot. Five limits are documented rather than hidden. **No business-day adjustment is applied to any Turkish date**: no Turkish source states that a deadline falling on a non-working day extends before the office, and TÜRKPATENT's own 2025-03-29 notice told users to file during an administrative holiday — so `holiday_calendar` is `null` and both adjustments read `not_checked` on every row. Renewal cycles whose anniversary falls before 2018-01-10 were governed by a month-end-anchored *request* window that the engine does not yet express, so their `window_opens` and `due_date` can be up to 30 days early; every such cycle is in the past. (`grace_expiry` is not affected: the repealed KHK 556 anchored the six-month lapse on the exact end of the term, which is what we publish.) An application filed before 2017-01-10 but published after it legally had a 3-month opposition window while we publish 2 — earlier than the true date, never later. Applications published in Marka Bülteni 410 to 417, roughly 12 December 2022 to 27 March 2023, carry opposition end dates that TÜRKPATENT's 2023-04-14 notice fixed per bulletin once the periods suspended after the February 2023 earthquakes restarted; a one-off table is not a rule, so those office-fixed close dates are not modelled and the notice is pinned to keep the limit auditable. And whether a renewal request filed ON the day the term ends is still surcharge-free is stated by neither SMK m. 23/2 nor the office, so the exact anniversary is kept as the due date. Trademark responses recompute opposition windows per request, so they are correct as soon as this deploys. Only the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters are baked at index time, so TR search documents carry these values only after TR is reindexed; no TR records are indexed today, so that reindex belongs with the TÜRKPATENT connector rather than with this change. ## September 2, 2026: Japan (JPO) deadline and opposition rules New jurisdiction. `JP` now has a maintenance config and two opposition rules, so `GET /v1/deadline-rules`, `GET /v1/opposition-rules`, `POST /v1/deadlines/compute` and the `derived` block on a trademark answer for Japanese marks instead of returning nothing. Four new `rule_id` values: `jp_renewal`, `jp_restoration`, `jp_opposition`, `jp_opposition_madrid`. Renewal runs 10 years from the registration date, with a 6-month filing window before expiry and a 6-month late window after it; missing both extinguishes the registration retroactively, and a further 6-month restoration window follows. Opposition is POST-registration: 2 months from the day after the 商標掲載公報 issue, non-extendable, with the same window for Madrid designations. Deadlines falling on a JPO closed day roll to the next open day. Every value cites the statute or the JPO practice document it comes from, with a verbatim quote and a pinned snapshot. Three limits are documented rather than hidden: the published `window_opens` can be 1 to 3 days earlier than 商標法第20条第2項 allows, and `grace_expiry` can be up to 7 days early when the expiry lands on a closed day. Madrid designations are also served a `jp_restoration` row that Japanese law does not give them. All three are fixed by engine changes that ship separately. Trademark responses recompute opposition windows per request, so they are correct as soon as this deploys. Only the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters are baked at index time, so JP search documents carry these values only after JP is reindexed; no JP records are indexed today. ## September 2, 2026: opposition window opening day corrected (Benelux, Finland, Norway, Sweden) Opposition windows for BOIP, PRH Finland, Patentstyret, and PRV Sweden now open on the publication day, one day earlier than before. Their closing dates are unchanged. The served opposition `rule_version` moves to `2026-09-02`. `derived.ruleset_version` on trademark responses (backed by the internal `DEADLINE_RULESET_VERSION` constant) also moves to `2026-09-02`. It is one global stamp (the maximum `lastVerified` across all maintenance configs), so it moves for every record in every jurisdiction, including ones this release did not touch; this release bumped that maximum. The version move on its own does not imply that any computed date changed. **REINDEX required for these four offices.** Trademark responses recompute opposition windows per request, so they are correct as soon as this deploys. Only the materialised `opposition_status` filter stays stale until those offices are reindexed, because `opposition_window_opens` is baked into existing search documents; the `opposition_closes_before`/`opposition_closes_after` filters are unaffected, because no closing date moves. Benelux, Finland and Norway can be reindexed now. **Sweden caveat.** For marks registered since December 2022, the publication date we stored for PRV came from the gazette issue, which runs one day after the registration decision (40 of 40 records sampled per month). PRV counts the opposition window from the registration day itself. The connector correction is in the "Swedish publication and expiry dates corrected" entry above; Swedish opposition windows stay one day late until the PRV backfill and reindex described there have run. Sweden is reindexed after that backfill, not with the other three offices. ## September 2, 2026: German renewal dates corrected for applications pending on 14 January 2019 DE national marks filed before 14 January 2019 but registered on or after that date now expire on the exact filing anniversary. They were previously shown with a month-end deadline, which is later than the law allows (for example, 2028-10-31 instead of 2028-10-05). Marks already registered before the cutoff retain month-end expiry, and office-reported expiry dates still take priority. The correction is computed at request time; no reindex is required. Scope of the change: only DE national marks filed before 2019-01-14, registered on or after that date, and without an office-reported expiry date. Our deadline evaluation harness holds no DPMA samples, so 0 records in it move. The first renewals for this group fall in 2028 and 2029, so no affected deadline is currently past due. ## September 2, 2026: French renewal grace end corrected (moves later) French renewal grace periods previously ended early, so customers had more time than Signa showed. They now run for six months from the day after the registration expires, matching CPI R. 712-24 and R. 718-2 exactly. Scope: no French renewal grace end moves earlier. Most move one day later. A grace period whose due date falls on a month end can move up to four days later, because the old and the new end clamp to different month ends: a due date of 2026-02-28 moves its grace end from 2026-08-28 to 2026-09-01, a due date of 2026-04-30 moves two days, and 2028-02-29 moves three. A few whose old and new ends both clamp to the same month end do not move at all. The correction is computed at request time, so no reindex is required (maintenance deadlines are not indexed). ## September 2, 2026: office calendar corrections Deadline rollover calendars now match the 2026 audit of official office closures. No rule window or anchor changed. Trademark responses recompute opposition windows per request, so every office below is correct as soon as this deploys. Only the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters stay stale until the offices named below are reindexed. * **EUIPO:** 8 December 2025 and 2026 no longer roll; those are Article 2 ordinary-post-only days, not Article 1 office closures. Reindex EU opposition documents. * **CIPO:** weekend substitutes for 24 June and 30 September now roll on the following Monday. Those two need no reindex before the first affected 2028 opposition cohort. Separately, the 17–19 September 2025 office closure now rolls through the weekend to 22 September, so CA documents published 17 to 19 July 2025 carry an opposition close date up to five days early until CA is reindexed. Opposition status is unaffected: those windows are already closed. * **IPOS:** 11 August 2025 and 20 March 2026 no longer roll; the excluded days 1–16 May 2024 roll to 17 May, and 14 October 2025 rolls to 15 October. Reindex affected Singapore opposition documents. * **IP Australia:** Canberra Day, Saturday and Sunday ANZAC Day substitutes, and the standing 25 December–1 January close-down now roll. For example, 9 March 2026 rolls to 10 March, 27 April to 28 April, and 29 December to 4 January 2027. Reindex Australian opposition documents. * **IPI:** the conservative nationwide calendar now excludes canton-specific days, including 2 January, Good Friday, Easter Monday, Whit Monday, 1 May, 26 December. The governing party or representative canton is not available to the engine, so deadlines are never moved later based on the wrong canton. Reindex Swiss opposition documents. * **DKPTO:** 15 May and 28–30 December 2026 now roll as enumerated office closures. Reindex is not currently needed because Signa has no DK office documents; revisit when DK ingestion lands. * **INPI France:** 15 May 2026 now rolls to 18 May under the Director General's closure decision. Reindex French opposition documents. * **DPMA:** the one-off Berlin holiday on 8 May 2025 now rolls to 9 May. No reindex is required for the weekly weekday publication cohort. ## September 2, 2026: Era-aware opposition citations The `source` on a computed opposition window now cites the instrument that states the window actually served. For rules whose opposition period changed on a fixed date, a publication before the change cites the earlier statute and a publication on or after it cites the amending law. This covers all seven opposition rules that change their period on a date, so a pre-change publication now cites the instrument that states the period it was given: * **Australia**, direct and Madrid, published before 2013-04-15 (3 months): Trade Marks Regulations 1995 reg 22.9 item 2 for a direct application and the 2012 compilation's reg 17A.29(2)(b)(i) for an international registration, in place of the current reg 5.6(1) / reg 17A.33(1) "2 months". * **India**, published before 2013-07-08 (3 months): Trade Marks Rules 2002 r. 47(1), in place of s. 21(1) "four months". * **Thailand**, published before 2016-07-28 (90 days): s. 35 paragraph 1 of the Trademark Act B.E. 2534 as in force before Act (No. 3) B.E. 2559, in place of the current s. 35 paragraph 1 "sixty days". * **Türkiye**, direct and Madrid, published before 2017-01-10 (3 months): the repealed Decree-Law 556 art. 35/1, and WIPO Information Notice No. 19/2014 for a designation, in place of SMK 6769 art. 18/1 "iki ay" for a direct application and the WIPO Madrid Member Profile ("two months from the date of publication of the international registration") for a designation. * **Vietnam**, published before 2026-04-01 (5 months): Law 07/2022/QH15, in place of Law 131/2025/QH15, which states only the three-month period. No window dates move, the `source` shape (`citation`, `url`) is unchanged, and no reindex is required — the citation is computed at request time. ## September 2, 2026: Vietnamese opposition windows corrected (3 months since 1 April 2026) Vietnamese trademark applications published on or after 2026-04-01 now have a 3-month opposition window. These windows were previously shown 2 months too late. Trademark responses recompute opposition windows per request, so they are correct as soon as this deploys. Only the materialised `opposition_status` and `opposition_closes_before`/`opposition_closes_after` search filters stay stale until VN documents published on or after 2026-04-01 are reindexed. ## September 2, 2026: Brazilian and Mexican renewal deadlines Renewal deadlines are now computed for Brazilian and Mexican registrations where none were computed before; the grant-date trigger was never fed from stored data. The affected cohort is BR and MX national-route registrations whose stored registration date feeds that trigger; records carrying an international registration number are excluded. The deadlines are computed at request time, so no reindex is required (maintenance deadlines are not indexed). ## September 2, 2026: Icelandic renewal dates corrected IS renewal deadlines: applications filed before 2020-09-01 are now anchored on the registration date for every renewal, even when registration occurred after the reform. They were previously shown early from the filing-date anniversary. For every affected record that carries a registration date the renewal moves later; none moves earlier. Records with an office-reported expiry remain anchored on that expiry. There is a third outcome. A pre-2020 IS record that carries neither a registration date nor an office-reported expiry now returns no computed renewal at all, where it previously returned one anchored on the filing date. The filing date is the wrong anchor for that cohort, so the deadline is withheld rather than guessed; send the registration date, or the office's expiry date, to get a schedule back. This matches how the other registration-anchored pivots (SE, FI and NO) already behave. Denmark is not in that set: its pivot is keyed on the registration date rather than the filing date, so a DK record with no registration date falls through to the modern filing anchor and is served a schedule. Renewal deadlines are computed at request time, so no reindex is required. IS opposition dates did not change either: `opposition_window_opens`, `opposition_window_closes` and `opposition_rule_version` in the search index are unaffected. The index stores no rule citations at all. `GET /v1/deadline-rules` and `GET /v1/opposition-rules` serve `sources[]` straight from the deployed configuration, so the new IS primary-source citations take effect with the deploy (within the 24-hour reference-cache window), and no reindex is involved. ## September 2, 2026: Office rule reference data provenance Reference data: deadline and opposition rules for AU, BR, BX, CA, CH, DE, DK, EU, FI, FR, GB, MX, NO, PH, PL, SG, TH, US and WIPO now cite primary sources with verbatim quotes and pinned snapshots. `sources[]` on `GET /v1/deadline-rules` and `GET /v1/opposition-rules` changed for those offices: law-firm guides, publisher summaries and mirrors of primary text were replaced by the statute, regulation or office publication they paraphrased. Nothing else in those responses moved, and no computed date changed from the re-citation alone. IN, JP and TR are new rule sets rather than re-citations, and are announced separately above. For DE (DPMA) the opposition rule's five bare links to individual gesetze-im-internet section pages are replaced by eleven pinned citations: MarkenG §§ 41, 42 and 91, MarkenV § 27, BGB §§ 187, 188 and 193, DPMAV § 18a, PatKostG § 6(1) with the opposition fee numbers, and the DPMA's own opposition guideline W 7736 (Markenblatt publication, the Ereignisfrist arithmetic, and the rule that statutory periods cannot be extended). The German renewal config additionally cites PatKostG §§ 3, 5 and 7, BGB §§ 187 and 188, and three DPMA practice pages carrying the worked renewal example. No computed dates changed. For FI (PRH) both configs move to primary Finnish law. The renewal config's two remaining links — an English PRH page and a law-firm alert — are replaced by twelve citations: Tavaramerkkilaki 544/2019 §§ 16, 30, 106 and 107 (the ten years from the application date, the one-year/six-month window, removal from the register, and the 1 May 2019 commencement that pivots the anchor), the repealed 7/1964 §§ 22 and 24 for marks applied for before it, the 1930 Act on calculating time limits for the same-numbered-day arithmetic, and three Finnish PRH pages. The opposition rule's own statutory links are re-pinned (544/2019 § 32 and the 1930 Act) and joined by 544/2019 §§ 24 and 33, 7/1964 § 20 and four PRH pages. A citation to a Government Decree on Trademarks (1268/2018) was removed: that statute number belongs to an unrelated act and no decree governs the Finnish opposition period. No computed dates changed. For FR (INPI) the opposition rule loses all four of its previous links and gains thirteen pinned citations. Two of the four were wrong as well as unverifiable: the Légifrance article id cited for CPI art. L. 712-4 does not exist, and the one cited for Code de procédure civile arts. 641/642 identifies neither article (they are LEGIARTI000006411002 and LEGIARTI000006411003). The rule now cites CPI arts. L. 712-4 (both the in-force text and the pre-2019 text, with L. 712-3, showing the two months have not changed since 1992), R. 712-8 for the BOPI publication that starts the period, R. 718-2 for the same-numbered-day arithmetic and the weekend/holiday roll, R. 712-14 for the one further month allowed for the exposé des moyens, R. 712-15 for the inadmissibility of a late opposition and R. 712-12 for the absence of any restoration of it, plus INPI's opposition directives (July 2026) and its current opposition page. The renewal config additionally cites CPI arts. L. 712-1 and L. 714-5 and INPI's nullity/revocation page. Statutory text is quoted from the DILA open-data LEGI publication of each article version, because legifrance.gouv.fr blocks automated retrieval. No computed dates changed. For MX (IMPI) both configs move to primary Mexican law. The renewal config's five previous citations - a WIPO Lex catalogue page and three law-firm guides, plus WIPO Information Notice 13/2018 - become thirteen: LFPPI arts. 178, 21, 233, 237 and 238 and transitorios PRIMERO, DECIMO and DECIMO PRIMERO (the ten years from grant, the six-month renewal window and six-month grace, the declaration of use in the three months after the third year, and the 2018-08-10 and 2020-11-05 commencements), the abrogated Ley de la Propiedad Industrial art. 95 for marks applied for before 5 November 2020, IMPI's own LFPPI seminar deck, and Information Notice 13/2018 promoted in place with its quote. The opposition rule's three links are replaced by seven citations: LFPPI arts. 221, 21 and 18, Reglamento de la LPI arts. 4o and 59 TER, the abrogated LPI art. 120 showing the one-month non-extendable period has not changed since 2016, and the transitorio Único of the decree published in the DOF on 1 June 2016, which is the text that dates the 2016-08-30 gate. All three were unusable: every one of them pointed at diputados.gob.mx, which no longer answers, and the Federal Code of Civil Procedure art. 286 is not the instrument that governs IMPI time limits either. Statutory text is quoted from the ordenjuridico.gob.mx consolidated PDFs. No computed dates changed. ## September 2, 2026: The events feed is documented (beta) [`GET /v1/events`](/api-reference/monitoring/events/list) and [`GET /v1/events/{id}`](/api-reference/monitoring/events/retrieve) are now part of the published API reference and the OpenAPI document, under Monitoring. They were already live and callable with the `events:read` scope; what changed is that they are documented and supported rather than undocumented. The feed is your organization's append-only event ledger, retained 30 days. Every webhook you receive is also a row in it, addressable by the same id — the `webhook-id` header IS the `evt_*` id — which makes it the recovery path for an outage your webhook retries could not cover: persist the last id you processed, then ask for `?sort=id&after=evt_…`. A checkpoint older than the window returns `410 replay_window_expired`; see [Replay from a checkpoint](/guides/monitoring/webhooks#replay-from-a-checkpoint). **Beta means one specific thing here.** The ledger, the `evt_*` ids, the filters and the replay rail are stable enough to build on. The per-event `data` payloads are versioned by `payload_version` and their shape is **not** yet promised — pin `payload_version` in your consumer and read this changelog before upgrading. No behaviour changed: same rows, same filters, same scopes. The MCP surface adds a matching read tool, `signa_events_list`. ## September 2, 2026: Alert diffs use public field names (breaking, beta) **`data.alert.event.diff[].path` on `alert.created` webhooks and `GET /v1/alerts*` now uses public trademark field names**, the same ones `trademark.status_changed` adopted. Effective on deploy; alerts are in beta and this is the last chance to fix the vocabulary before it is frozen. | Was (internal column) | Now (public field) | | ------------------------------------- | ------------------- | | `status_primary` | `status` | | `mark_text_primary` | `mark_text` | | `publication_date_first` | `publication_date` | | `international_registration_number` | `ir_number` | | `challenge_states` | `status_challenges` | | `trademark_owners` | `owners` | | `trademark_attorneys` | `attorneys` | | `trademark_classification` | `classifications` | | `trademark_media` | `media` | | every other `trademark_*` child table | its collection name | Fields Signa tracks but does not publish (`is_primary_source`, `mark_feature_type_raw`, `mark_legal_category_raw`, `application_language`) are **dropped** rather than renamed, so a diff can be shorter than before — and every `path` you now see is a field you can read back from `GET /v1/trademarks/{id}`. `event.summary` is built from the same names, so the headline reads `"Status changed: pending → registered"` where it used to read `"Status primary changed: …"`. Redeliveries (`POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver`) and automatic retry attempts of alerts created BEFORE this change are re-emitted with the new names too — `event.summary` included, since the headline is re-derived from the translated diff — so your handler only ever has to understand one vocabulary. `GET /v1/events/{id}` applies the same projection to the stored `alert.created` body, so the pull rail and a redelivered push of the same event agree. Nothing about which alerts fire, or when, changed. ## September 2, 2026: Office dates and your docket reference on events Three additive fields on `trademark.status_changed` and `office_action.issued`, on both the webhook body and `GET /v1/events/{id}`. Nothing was renamed, no stored value changed, and `payload_version` stays `1`. **`source_date` is the office's date; `occurred_at` was always ours.** `occurred_at` is when Signa produced the event: ingestion time for `trademark.*` and `office_action.*`, alert-creation time for `alert.created`. That was already true, and it is now documented per family instead of implied. The new `source_date` is the office-reported data date of the change as we stored it (`YYYY-MM-DD`, or `null` when the feed reports none) — equal to `event_date` on office actions. The two differ by the office publication lag, typically 1 to 14 days, so **sort a docket by `source_date`** (meaningful within one office) and use `occurred_at` to answer "when did I learn this". Its meaning is honest rather than uniform: day precision, USPTO transaction date, WIPO gazette date, snapshot offices the crawl date. It is **not** a legal effective date — `changes.status_effective_date` is. We deliberately do not fold the effective date into `source_date`, because an office re-crawl can backdate it by years and your feed would travel backwards on a mark that was merely re-read. **`portfolios` carries your `external_ref`, frozen at record time.** Both families now include the portfolios the mark was in when the event was recorded, each with the reference you set on the membership: ```json theme={null} "portfolios": [{ "id": "ptf_9aB2xY", "external_ref": "MATTER-4471" }] ``` It is a snapshot, not a live lookup. Re-keying a matter reference tomorrow never rewrites an event you already received, which is what keeps a redelivery byte-identical to the original and keeps `GET /v1/events/{id}` in agreement with the body you verified the signature over. Empty array when the mark was in no portfolio; `null` `external_ref` means unknown or unset, never "cleared". `alert.created` does not carry either field. The SDK adds `EventPortfolioRef` and the new fields on `TrademarkStatusChangedData`, `OfficeActionIssuedData` and the org-event detail types. ## September 2, 2026: Webhook subscriptions, portfolio scope, and diff field names Three contract clarifications on the event surface, all landing together. **Subscribing no longer depends on our rollout state.** `enabled_events` accepts `alert.created`, `trademark.status_changed` and `office_action.issued` at all times. The last two are accepted for subscription but **not yet emitted** — they begin delivering when the API events projector is enabled. Previously the accepted set widened with our rollout, which meant the published OpenAPI document described a different API depending on the environment you generated your client against. Subscribe now and you receive them from the first delivery. **`portfolio_id` means one thing.** An endpoint's `portfolio_id`, and `GET /v1/events?portfolio_id=`, now filter **every** event type against the portfolio membership recorded when the event was recorded — `alert.created` included. Previously alerts bypassed the filter entirely, so a portfolio-scoped endpoint or query returned every alert in the organization regardless of portfolio. If you scoped an endpoint to a portfolio and relied on it still receiving all alerts, clear `portfolio_id` on that endpoint (or add a second, unscoped endpoint) — similarity, class and owner watches match marks you do not own, and those alerts have no portfolio membership. **`changed_fields` and `changes` use public field names.** The `trademark.status_changed` payload and `GET /v1/events/{id}` now key the diff by the field names on the trademark resource instead of our internal column names: `status_primary` becomes `status`, `mark_text_primary` becomes `mark_text`, `publication_date_first` becomes `publication_date`, `international_registration_number` becomes `ir_number`, `challenge_states` becomes `status_challenges`, and changed child collections read as `owners`, `classifications`, `media` and so on. Internal-only columns are omitted rather than renamed, so both lists can be shorter than before. Every name you now see is a field you can read back from `GET /v1/trademarks/{id}`. ## September 2, 2026: Richer goods/services validation evidence `POST /v1/goods-services/validate` now adds descriptive wording coverage, candidate spans and USPTO template drafting aids, suggested class inference, cited indefinite-wording warnings, catalog `as_of` freshness, and explicit candidate-search completeness. Summary fields roll up inferred items, warnings, and complete, truncated, or degraded searches. `listed` and `not_listed` keep their existing catalog-membership meaning. The TypeScript SDK exposes the new types in version 0.12.0; version 0.11.0 was never published, so no published SDK contract is broken. ## September 2, 2026: Goods/services catalog validation `POST /v1/goods-services/validate` checks submitted wording against the pre-approved catalogs available for US, EM, JP, KR, and CN. `listed` means the wording appears in that office's catalog. `not_listed` is an absence of evidence, not a refusal. Other recognized offices return `unsupported_office` with reason `no_catalog_data`. The endpoint returns one result per input item, bills per item, and can provide same-class candidate wording when `class_number` is supplied. An optional `Idempotency-Key` enables safe replay. The TypeScript SDK exposes it as `goodsServices.validate()` in version 0.11.0. ## September 2, 2026: Entity lists and suggest answer 503 when OpenSearch is unavailable `GET /v1/owners`, `/v1/attorneys`, `/v1/firms`, `/v1/entities`, `/v1/suggest` and `/v1/trademarks/suggest` no longer fall back to a degraded Postgres page or an empty result when OpenSearch is unavailable. They answer `503 service_unavailable` with a `Retry-After` header instead. The SDK retries 503 automatically. Cursors and result ordering are unchanged on the happy path. ## September 2, 2026: Saved searches removed The `/v1/saved-searches` endpoints are gone and now answer `404`. The `signa.savedSearches` SDK resource, the `SavedSearch` types, the `from_saved_search` option on `signa.watches.create` and the `ssr_` id prefix were removed with them. The feature had no production usage. Store the search body on your side and re-send it to `POST /v1/trademarks`, or create a watch with the same query. ## September 2, 2026: Swedish renewal dates corrected SE renewal deadlines: registrations applied for before 2019-01-01 are now anchored on the registration date per the transitional rule (SFS 2018:1652 p. 4 a), whenever they were registered. About 1,300 active marks change date, in both directions: roughly 1,290 move later (they were shown up to 6 years early), and 8 move earlier or gain a deadline that was previously outside the horizon (their filing-anchored cycle had already elapsed; the registration-anchored one lands sooner, by up to 2,445 days). Computed at request time, no reindex. Records with an office-reported expiry are unaffected. ## September 1, 2026: Office-action webhook events `office_action.issued` is now a subscribable, versioned semantic event. It fires once for each newly stored office action on a portfolio mark and carries the stored prosecution-history fields using public office-code vocabulary. Emission begins when the API events projector is enabled. A first detail fetch that newly inserts multiple office actions stores them without webhook emission. A single-action history may emit one `office_action.issued`: one action is indistinguishable from a genuinely new one, and a stale event is safer than silently dropping a real refusal notice. Once history is known, each later new action emits once. ## September 1, 2026: Trademark status webhooks and portfolio-scoped endpoints `trademark.status_changed` is now available as a subscribable webhook event type. It carries the spine-native event envelope; emission begins when the API events projector is enabled. Endpoints without a `portfolio_id` receive every `trademark.status_changed` event for the organization; setting a `portfolio_id` narrows those deliveries to marks in that portfolio. `POST /v1/webhooks`, `PATCH /v1/webhooks/{id}`, and `GET /v1/webhooks` now expose the additive `portfolio_id` field. ## September 1, 2026: BREAKING — webhook event envelope **This is a breaking webhook contract change.** For `alert.created`, the envelope `id` and signed `webhook-id` header now use the spine event identifier (`evt_*`) instead of the alert identifier (`alt_*`). The payload is no longer a flat merged field bag: `data` is now `{ event_id, alert_id, alert }`, with the rich alert resource nested under `data.alert`. `webhook-id` is the documented idempotency key. Migrate or reset stored idempotency keys during this upgrade so the `alt_*` to `evt_*` identity change does not cause incorrect duplicate handling. Update consumers to read paths such as `data.alert.id` and `data.alert.event.type`. ## September 1, 2026: Forward replay on `GET /v1/events` The event feed gains a second rail. The default feed keeps its shape and its rows: newest first, opaque cursor, same order (list items do gain two timestamp fields, detailed below). What is new is a forward rail a sync client can resume from, plus two behaviour changes on the existing surface that you need to know about before you upgrade. **Discard any `GET /v1/events` cursor you have stored.** Event cursors are now bound to the query that minted them. The payload carries the sort direction and a fingerprint of the filters, the limit and the sort, so a cursor can only be replayed against the identical query. Every cursor minted before this change lacks both, and now returns `400 cursor_invalid`. Cursors were always documented as opaque and short-lived, so this only affects you if you persisted one. Restart from page one without a cursor. `cursor_invalid` is a different failure from `cursor_expired`, and the split is deliberate. `cursor_expired` means the value is not a cursor: malformed, unsigned, or minted for another endpoint. Restart from page one. `cursor_invalid` means the cursor is genuinely ours but is bound to a different query than the one you just sent, usually because a filter or the limit changed mid-pagination. Restart from page one with the parameters you actually want. **`since` now filters on when Signa recorded the event, not when the office reported it.** It used to bind the row's creation timestamp; it now binds `recorded_at`, Signa's ledger clock. This is what a sync client wants. Source timestamps can be backdated by an office arbitrarily far, so filtering on them silently skips late-arriving rows that were recorded after your last poll. Every event now carries both clocks explicitly, `occurred_at` for the office and `recorded_at` for us, so nothing is lost. `created_at` stays as an alias of `recorded_at`. **`sort=id` turns the feed into a replay rail.** Pair it with `after=` to resume from a checkpoint you persisted, exclusive of the checkpoint itself. Ledger sequence is the only sortable field, and `after` is valid only with `sort=id`. `sort=-id` remains the default; its ordering and cursor behaviour are unchanged. List items gain `occurred_at` and `recorded_at`, and `trademark_id` is now nullable. ```bash theme={null} curl "https://api.signa.so/v1/events?sort=id&after=evt_3D7rZ9&limit=100" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` **A checkpoint older than the retained window is a `410`, not a silent gap.** Replay is bounded by retention. If the events after your checkpoint have been pruned, the response is `410 replay_window_expired` rather than a `200` that quietly omits them. The floor is per organization, so another tenant's retention never affects your answer. Recovery is to start a new initial sync with `sort=id` and no `after`, then persist the newest event ID you see. The TypeScript SDK maps `410` to a new `GoneError`. **`portfolio_id` is family-aware.** It filters every event type through the portfolio membership snapshot captured when the event is recorded, so later membership changes never rewrite history. That includes `alert.created`: an alert is in a portfolio's feed only when the alerting mark was in the portfolio at the moment the alert was recorded, and similarity, class and owner watches routinely match marks you do not own, which have no membership. This is identical to portfolio-scoped webhook delivery. The parameter requires `portfolios:manage` in addition to `events:read`. ## September 1, 2026: Examiner citations Three new endpoints, a new count on the trademark detail, and a per-office capability flag. Everything here is additive. The only thing that changes for an existing integration is that detail `ETag`s rotate once, at deploy. **Citations are now a first-class resource.** A citation is a prior mark an examiner cited against a pending application. `GET /v1/citations` queries them across marks, filterable by `offices`, `disposition`, `action_stage`, `refusal_type`, `trademark_id`, `cited_trademark_id`, `cited_ref` and an `action_date` range. Each row is one citation occurrence: one prior mark, one application, one office action. **And per mark, in both directions.** `GET /v1/trademarks/{id}/citations` returns what was cited against that application. `GET /v1/trademarks/{id}/cited-by` returns the applications that mark has been cited against, which is the question a portfolio owner actually asks. `cited-by` also picks up citations matched by office-printed reference where Signa has not linked the row to a record yet, and the `?cited_trademark_id=` filter on `GET /v1/citations` matches the same rows, so the two views agree. **`citations_count` on the trademark detail.** `GET /v1/trademarks/{id}` and `POST /v1/trademarks/batch` carry a new `citations_count`. It is present on every full-detail response, including the batch lookup surface, and it is nullable. Like `events_count` and `proceedings_count`, it is dropped by a sparse `?fields=` projection that does not ask for it. An integer, including `0`, means the office is covered and that is the real count. `null` means citations are not counted for that office at all, which is not the same as "there are none". **`capabilities.citations` tells you which is which.** `GET /v1/offices` and `GET /v1/offices/{code}` now carry a `capabilities` block whose `citations` field reads `available`, `in_progress`, `not_available` or `not_applicable`. `available` offices return a number in `citations_count`; every other state returns `null`. `not_applicable` is the useful one: an office that does no ex-officio relative-grounds examination never issues a citation-bearing refusal, so its empty list is a fact about the regime rather than a gap in our coverage. Branch on this field instead of inferring coverage from an empty response. Coverage today is USPTO §2(d) refusals. **SDK.** `signa.citations.list()`, `signa.trademarks.citations(id)` and `signa.trademarks.citedBy(id)`, all auto-paginating like every other list. **Detail `ETag`s rotate once.** The representation salt goes from `v3` to `v4`, so the validator format is now `"v4.{version}-{epoch}"` plus the `derived` content hash, plus a `citations_count` hash on covered offices. Every cached trademark detail body misses once and refetches. Nothing to change on your side. The extra hash exists because citations are written by the extraction pipeline, which does not touch the record's version: without it, a client holding an `ETag` would be revalidated onto a stale count forever. ## September 1, 2026: Every rule has a stable id Deadline and opposition rules now carry a permanent `rule_id`, so a computed date can be traced to the rule that produced it and joined to the catalogue that documents it. `rule_id` is an opaque slug — do not parse it — and it will not be renamed once published. Alongside it, two dates that were previously conflated are now separate. `effective_from` is the earliest date from which a rule's modelled content is authoritative, and is `null` where the corpus does not establish one. `last_verified` is when we last checked the rule against its sources; on opposition rules that is the value `rule_version` has always carried. `POST /v1/deadlines/compute` rows gain `rule_id`, `effective_from`, `last_verified` and the business-day provenance `due_date_adjustment`, `grace_expiry_adjustment` and `holiday_calendar`. The two adjustment fields are `moved`, `unchanged` or `not_checked` — `not_checked` covers a jurisdiction that does not observe the roll, an office with no wired calendar, and a date beyond a calendar's coverage, so a date we never checked is never reported as one we checked and left alone. Each item also carries the jurisdiction's `sources`, plus `as_of` and `computed_at`, which together make a response replayable as a record of what was computed and when. `POST /v1/oppositions/compute` gains `rule_id` and `effective_from`; `rule_version` is unchanged. Both catalogues expose the same three fields, and `GET /v1/deadline-rules` now also serves the restoration rules for Great Britain, Singapore and Iceland, which the engine could always compute but the catalogue never listed. Trademark detail, portfolio deadlines and watch diagnostics carry `rule_id` on each row. All fields are additive. One caching note: the trademark detail response folds a content hash of its `derived` block into the ETag, and that block now carries `rule_id`, so detail validators rotate once on this release. ## September 1, 2026: Prosecution deadlines on `/v1/deadlines/compute` `POST /v1/deadlines/compute` now computes **prosecution** deadlines as well as maintenance ones: office action response periods and statement of use clocks, resolved per fact instance. Send an optional `facts` array on any item and the result gains `prosecution_deadlines[]` and `support[]`. **This is additive.** `prosecution_deadlines[]` and `support[]` appear only on items that sent `facts`. (The same release adds rule identity to every item — see the entry above — so an item you send without `facts` is unchanged apart from those provenance fields.) **Every computed date cites its rule.** Each prosecution deadline carries the rule id, the era it belongs to (`effective_from` / `effective_until`), which case date selected that era, when the rule was last verified against source, and the sources themselves with pinpoint section, verbatim quoted text and a sha256 of the snapshotted document. A computed date that cannot cite its rule does not ship. **Support is now per deadline type, and it is honest about coverage.** A single `supported: boolean` could not say that a US mark has a renewal schedule but no office action deadline, so `support[]` reports one entry per type with a four-valued `status`: * `computed`, a modelled rule produced or retired a date * `stated`, no modelled rule exists but the office stated the deadline itself * `facts_required`, a rule exists and is verified, but you must supply the trigger * `unsupported`, no rule is modelled Today that reads: USPTO computes from stored or supplied events. EUIPO rules are verified but Signa stores no EUIPO examination events, so EUIPO is `facts_required` until you send the trigger. There is no Madrid level rule and there will not be one, because the Madrid System does not set the holder response period; it is designated office law. For an unmodelled contracting party, send a `stated_deadline` fact carrying what the office declared and the date comes back with the notification as its authority. The item level `unsupported_reason` enum widens from 2 values to 20 so both engines share one reason vocabulary. The field itself still only emits the two maintenance values; the new codes appear on `support[].reason` and `prosecution_deadlines[].reason`. Treat it as an open enum. Billing is unchanged: 1 unit per item, whether or not the item carries facts. ## August 31, 2026: Computed data moves under `derived` A response-shape change on the trademark detail and batch endpoints, plus a new provenance field. This is a breaking change: `deadlines[]` and `opposition_window` move, and three fields on each deadline row go away. We are shipping it during beta, before anyone has built on the old shape. **Everything Signa computes now lives under `derived`.** `deadlines[]` becomes `derived.deadlines[]` and `opposition_window` becomes `derived.opposition_window`. The split makes the boundary visible. Fields at the top level are what the office told us. `derived` is what we computed. Nothing about what we compute has changed, only where it sits. **Deadline rows now carry dates, not countdowns.** `status`, `urgency` and `days_until_due` are gone from the trademark response. Each row instead carries `window_opens`, `due_date` and `grace_expiry`, plus new `trigger_date`, `trigger_field`, `recurring` and `optional` fields. The reason is caching. Those three fields were functions of the day you read the response, but the response is cached and revalidated against a record-versioned `ETag`. A client that held an `ETag` and revalidated a week later got a `304` and kept serving last week's countdown, indefinitely. A value that changes without the record changing cannot live in a cacheable body. Deriving them is a couple of lines against your own clock: ```typescript theme={null} const today = new Date().toISOString().slice(0, 10); const overdue = d.due_date < today; const inGrace = overdue && d.grace_expiry !== null && d.grace_expiry >= today; ``` If you would rather the server do it, `POST /v1/deadlines/compute` takes an explicit `as_of_date` and still returns `status`, `urgency` and `days_until_due`. Its clock is an input, so it caches correctly. `GET /v1/portfolios/{id}/deadlines` is unchanged. **Opposition windows lost their clock fields too.** `derived.opposition_window` drops `status`, `days_until_open` and `days_remaining`, and adds `rule_version`. On search and list rows, `opposition_window` keeps `window_opens` and `window_closes` and drops `status`. The `opposition_status` filter is unaffected. The server still evaluates it, so filtering by `open` or `not_started` works exactly as before. **An empty schedule now says why it is empty.** `[]` used to be ambiguous between "this mark has no deadlines" and "we declined to compute one". `derived.deadlines_supported` is now `false` with a `derived.deadlines_unsupported_reason` of either `unsupported_jurisdiction` or `requires_office_date`, the same vocabulary `POST /v1/deadlines/compute` already returns. `derived.ruleset_version` stamps the rule state that produced the block. **`renewal_due_date` now declares where it came from.** It sits at the top level next to office-reported dates, but it is office-stated for some records and computed by the connector for others, and nothing said which. The new `renewal_due_date_basis` reads `reported`, `derived` or `unknown`, exactly like `expiry_date_basis`. Connectors adopt it one at a time, so expect `unknown` on records whose connector has not declared one yet. **Detail `ETag`s all rotate.** As of this release the validator format is `"v3.{version}-{epoch}"` with a content hash of the `derived` block appended, so cached copies of the old shape miss and refetch. Nothing to do on your side beyond expecting one full response per cached mark. (The `v3` salt moved to `v4` in the September 1 release below.) ## August 30, 2026: Deadline engine statutory corrections A correctness pass over the maintenance deadline engine. This is a behavior change: the same trademark can now return a different `status`, a different number of `deadlines[]` rows, and different dates on four jurisdictions. Read this before upgrading if you drive filing workflows off computed deadlines. **The due day and the last grace day are now actionable, not missed.** Status boundaries are inclusive on the actionable side. On the due date a deadline reads `due_soon`, not `missed`. On the last day of the grace period it reads `in_grace`, not `missed`. `missed` now begins the day *after* grace expiry (or the day after the due date where a rule has no grace period, such as the Mexican third-year declaration of use). If you filtered for `missed` to build a "too late to file" list, that list previously included two days you could still file on. **Refused applications no longer return a maintenance schedule.** A refused application has nothing to maintain, so `deadlines[]` is now `[]` for it. The one exception is Madrid-route records: a provisional refusal from a designated office is routinely overcome, so Madrid-route marks keep computing their schedule. **Expired and cancelled marks now return their still-actionable rows instead of an empty array.** These statuses are recoverable for a bounded window, and returning `[]` hid exactly the deadlines a customer needs at that moment. An `expired` mark now returns its open late-renewal window (the current cycle while its filing window or grace period is open) plus its restoration row where the jurisdiction provides one (GB, SG, IS). A `cancelled` mark returns its restoration row only, where modeled. Statuses that are terminally dead (`abandoned`, `withdrawn`, `surrendered`, `invalidated`, and `refused`, except the WIPO international registration record itself under a provisional refusal) still return `[]`. * **This can add rows to marks you already fetch.** A cancelled GB mark that returned no deadlines yesterday can return a restoration row today. Client code that assumed a dead mark implies an empty array should be checked. * **Rows can also disappear for expired marks without a reported expiry.** The engine will not invent a lapse it cannot corroborate: an expired mark with no office-stated expiry date is served only the cycle whose window is currently open, never a restoration row derived from an assumed earlier miss. Clients alerting on `deadlines[]` shrinking should expect this. **Restoration rows are no longer flagged optional.** Restoration is the only way back onto the register once removal is final, so `optional` is now `false` on those rows, and `include_optional=false` no longer suppresses them. If you pass `include_optional=false` on `POST /v1/deadlines/compute` to trim the response, expect restoration rows to appear where they did not before. **Four statutory anchors were corrected.** These change computed dates, not just labels. * **Denmark:** the 2019 regime split is keyed on the **registration** date, not the filing date (Lov 1533/2018 §8 stk. 3). Marks filed before 2019-01-01 but registered after it were anchored up to a year off. * **Mexico:** the 2020 regime split is keyed on the **filing** date, not the grant date (LFPPI Transitorio DÉCIMO). Applications in prosecution when the LFPPI entered into force on 2020-11-05 stay on the old filing-anchored term. * **Philippines:** the third-year declaration of use window opens at the **filing** date and runs to the third anniversary of filing (RA 8293 §124.2). It was previously computed as a single day. * **Thailand:** publications before 2016-07-28 get the **90-day** opposition window that was in force at the time. The 60-day window applies only from the 2016 amendment forward. ## July 25, 2026: Jurisdiction protection scope A precision fix to what `jurisdictions` means, plus honest screening verdicts when register coverage is incomplete. This is a behavior change: the same request can now return more results and, for screening, a new verdict value. Because it changes result sets during beta, read this before upgrading. **`jurisdictions` now selects rights that protect, or seek protection, in a territory, not just literal territory legs.** Previously `jurisdictions=FR` matched only French national filings and Madrid designations of France. It now also matches regional rights whose membership covers France, so a EU trade mark (which protects France) is included. See [Search Trademarks](/api-reference/trademarks/search-trademarks) and the [glossary](/guides/glossary). * **Result counts grow.** A country-scoped filter picks up the regional rights that protect it. On production data, `jurisdictions=FR` roughly doubles once EU trade marks are counted. Countries with no live national office but active EU coverage (for example `DE`) go from a handful of Madrid hits to full EUTM coverage. * **`territory_match=direct` restores the old behavior.** Pass it to match only literal territory legs, byte-for-byte the pre-July-2026 result set. The default is `territory_match=protection`. Requesting a regional code (`EU`) stays strict on search: it matches EUTMs and IRs designating the EUIPO, not member-state national marks. * **Why results grew is now explained.** Under the default `protection` mode, each hit returned with a `jurisdictions` filter carries `territory_matches[]`, mapping the requested code to the territory it matched on and the basis (`direct` leg or `regional_membership`). The field is absent under `territory_match=direct`. `search_meta.territory_match` echoes the mode that ran. * **Suggest and image search grow too.** `GET /v1/trademarks/suggest` and `POST /v1/trademarks/search/image` apply the same protection-scope expansion and accept `territory_match`. Suggest now also matches Madrid designations of a requested territory (an international registration designating France is suggested under `jurisdictions=FR`), and image search echoes the applied mode as `search_meta.territory_match`. * **Member-country codes are newly accepted.** Benelux member states (`BE`, `LU`, `NL`) and OAPI member states (for example `SN`) had no jurisdiction row of their own and previously returned `400`. They are now valid `jurisdictions` values, so `jurisdictions=BE` selects Benelux and EU rights that protect Belgium. * **Saved searches replay under the new semantics automatically.** A stored search with `jurisdictions=FR` is re-executed against the current matcher, so it now returns EU trade marks it did not before, with no change on your side. If you need a saved search to keep the old literal-leg behavior, re-save it with `territory_match=direct`. * **Breaking for pagination.** Because the query shape of a jurisdiction-filtered search changed, pagination cursors minted before this release on such a query now return `400 cursor_invalid`. Clients holding a stale cursor must restart pagination from the first page. * **During the rolling deploy, expect a brief mixed window.** While the release rolls out, requests may land on either old or new instances, so a jurisdiction-filtered query can alternate between the old and new result sets, and `territory_match` may return `400` from a not-yet-updated instance. Stale jurisdiction-filtered cursors invalidate as above. The window closes once every instance is on the new release; retry the request if you hit it. **Screening can now return `needs_review` when register coverage is incomplete.** A clearance verdict is only trustworthy if Signa can actually see the marks that protect a requested territory. When the register that would carry a conflict is not connected live, a zero-conflict screen must not read as `clear`. * **New verdict value.** `GET /v1/screening` verdicts are now `high_risk` / `caution` / `needs_review` / `clear`. `needs_review` is a would-be `clear` that could not be confirmed because a requested territory's register is dark or stale. Handle it as "we could not rule this out", not as a weaker clear. Found conflicts still band high or medium exactly as before, observed risk is never suppressed. * **Why this is deliberate.** Before this change, screening a common brand in a class scoped to a territory whose national register was not yet connected could return `clear` with zero conflicts, even though real national marks existed and were simply invisible to us. Returning `clear` there is a false negative on a clearance product. We would rather tell you the corpus was incomplete than imply a name is safe. * **A new `coverage` block** on the screening response reports which registers gated the verdict (offices axis in ST.3 codes, territories axis in jurisdiction codes) and a `complete_for_clear` boolean. See [Screen a Name](/api-reference/screening/screen#coverage). * **Check a Listing** gets the same protection-scope expansion, and an incomplete requested register forces a would-be `clear` to `caution` (the listing verdict enum is unchanged). * **Screening responses are no longer client-cacheable.** They are served `Cache-Control: private, no-store`. Re-run a screen rather than replaying a stored response. * **`screening_id` values changed at this epoch.** The identifier now folds the coverage inputs that can flip a verdict, so ids minted before this release do not correspond to ids minted after. Do not treat a pre-release `screening_id` as stable across this boundary. * **Watches are unchanged.** Monitoring keeps office-based scoping and does not apply the protection-scope expansion, so a watch scoped to a country still watches that country's office only (a `DE` watch does not surface EU trade marks). See [Create Watch](/api-reference/monitoring/watches/create). ## July 14, 2026 * **Compare (beta):** `POST /v1/compare`, its TypeScript SDK resource, and its developer guide complete the public-facing Intelligence surface alongside Screening. Compare scores one candidate against up to 10 selected conflicts. # Authentication Source: https://docs.signa.so/guides/authentication Get your API key, understand scopes, and rotate keys safely Every request to the Signa API needs a Bearer token in the `Authorization` header. ```bash theme={null} Authorization: Bearer sig_YOUR_KEY ``` ## Getting your API key Sign up at [app.signa.so](https://app.signa.so) and navigate to **API Keys** in the sidebar. Click **+ Create new key**, give it a name, and copy the full key immediately: it is only shown once. Manage all your keys (create, rotate, revoke) from the [API Keys dashboard](https://app.signa.so/api-keys). ## Key format API keys use the format `sig_` followed by 48 hex characters. Unrecognized prefixes are rejected with `401` before any lookup. ## Scopes Each API key has one or more scopes that control what it can access: ## Key rotation Rotate a key when you suspect it has been compromised, when a team member leaves, or as part of regular security hygiene. Rotate from the [API Keys dashboard](https://app.signa.so/api-keys) (open the **...** menu on a key and select **Rotate key**) or programmatically via the [Rotate API Key](/api-reference/administration/rotate-api-key) endpoint. Rotation issues a new key; the old key keeps working for a 24-hour grace period so you can roll the new secret out to your services without downtime. API keys are for server-to-server use only. Never ship a `sig_` key in client-side code (browser JavaScript, mobile apps, or anything a user can inspect); call the Signa API from your backend and proxy requests to any frontend that needs the data. # Bulk Operations Source: https://docs.signa.so/guides/bulk-operations Retrieve, reconcile, and paginate through large volumes of trademark data efficiently This guide is for high-volume consumers: portfolio management tools, docketing systems, and anything else that regularly needs hundreds or thousands of trademark records rather than a handful. It covers chunked batch retrieval, diffing your own records against the register with `/v1/reconcile`, pulling large result sets efficiently, and staying under your rate limit while doing all of it concurrently. ## Chunked batch retrieval [`POST /v1/trademarks/batch`](/api-reference/trademarks/batch-trademarks) resolves up to 100 IDs (or office-native identifiers) in a single request and counts as **one** call against your rate limit, regardless of how many IDs you send. Matched records come back in `data`; anything that did not resolve comes back in `not_found`, there is no per-item success/error status to inspect. When you have more than 100 IDs, split them into chunks and process sequentially or with controlled concurrency: ```typescript TypeScript theme={null} import { Signa, type TrademarkBatchResponse } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); 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; } async function fetchAllTrademarks(ids: string[]) { const results: TrademarkBatchResponse['data'] = []; const missing: (string | Record)[] = []; for (const batch of chunk(ids, 100)) { const response = await signa.trademarks.batch({ ids: batch }); results.push(...response.data); if (response.not_found.length > 0) { missing.push(...response.not_found); } } return { results, missing }; } ``` ```bash cURL theme={null} # Split a large ID list into chunks of 100 with jq, then call batch per chunk jq -c '_nwise(100)' <<< "$(cat ids.json)" | while read -r ids_chunk; do curl -s -X POST https://api.signa.so/v1/trademarks/batch \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"ids\": $ids_chunk}" done ``` `_nwise` is a `jq` helper for fixed-size batching (`def _nwise(n): def n1: if length <= n then . else .[0:n], (.[n:] | n1) end; n1;`). Any language's array-chunking utility works the same way, the API side only cares that each request has 100 IDs or fewer. See [Batch Retrieve](/api-reference/trademarks/batch-trademarks) for request/response field detail, and [Resilience Patterns](/guides/resilience#bulk-operation-retry) for retrying a batch call that fails at the request level (rate limited or a transient 5xx). ## Reconciling your own records If you already hold trademark data (from a legacy system, a spreadsheet, or another vendor) and want to find where it has drifted from the register, use [`POST /v1/reconcile`](/api-reference/reference/reconcile) instead of fetching full records and diffing client-side. Send the fields you have per record, get a field-by-field match/mismatch back, and nothing is stored server-side. ```typescript TypeScript theme={null} const results = await signa.reconcile.run({ items: [ { office: "US", application_number: "88123456", your_fields: { status: "active", owner_name: "Nike, Inc." }, }, // up to 100 items per call ], }); for (const item of results.data) { if (item.result === "mismatch") { console.log(`${item.office}/${item.application_number}: ${item.mismatch_count} field(s) drifted`); for (const field of item.fields) { if (!field.match) { console.log(` ${field.field}: yours="${field.your_value}" register="${field.register_value}"`); } } } else if (item.result === "not_found") { console.log(`${item.office}/${item.application_number}: no matching register record`); } } ``` `result` is one of `match`, `mismatch`, `not_found`, or `ambiguous` (the identifier matched more than one register record). Reconcile is capped at 100 items per call just like batch, so the same chunking approach applies to a large book of matters. ## Pagination at scale For anything larger than a single batch call, the ordinary list and search endpoints paginate with a cursor. See [Pagination](/api-reference/pagination) for the full contract (cursor stability, invalidation, sort requirements). A few things matter specifically at volume: * Request `limit=100` (the max) instead of the default 20 to cut the number of round trips. * Use the SDK's `for await` iteration or `toArray()` instead of a manual cursor loop, `SignaList` follows `has_more`/`cursor` for you. * For a long-running export, checkpoint the cursor as you go. Cursors have no fixed lifetime, but they can be invalidated at any time (a signing-key rotation, a format change), so handle `cursor_expired` on every page and restart pagination from the beginning when you see it. * Only pass `include_total=true` when you actually need a count to display. It costs an extra query and is unnecessary for a pure export. ```typescript TypeScript theme={null} let processed = 0; for await (const tm of await signa.trademarks.search({ filters: { owner_id: "own_helios01" }, limit: 100, })) { await handleRecord(tm); // your own processing processed++; } console.log(`Processed ${processed} records`); ``` ## Rate-limit-aware concurrency Batch and reconcile calls are cheap per item, but you can still run into your plan's rate limit if you fire many of them concurrently. Every response carries a `RateLimit` header shaped `remaining=, reset=` (see [Rate Limits](/api-reference/rate-limits) for the limits themselves). A simple pattern: cap concurrency with a semaphore, and back off early when `remaining` gets low instead of waiting for a `429`. ```typescript TypeScript theme={null} function parseRateLimitHeader(value: string | null): { remaining: number; reset: number } | null { if (!value) return null; const remaining = /remaining=(\d+)/.exec(value); const reset = /reset=(\d+)/.exec(value); if (!remaining || !reset) return null; return { remaining: Number(remaining[1]), reset: Number(reset[1]) }; } async function runWithConcurrency( items: T[], limit: number, fn: (item: T) => Promise, ): Promise { const results: R[] = new Array(items.length); let cursor = 0; let paused = false; async function worker() { while (cursor < items.length) { if (paused) { await new Promise((resolve) => setTimeout(resolve, 1000)); continue; } const i = cursor++; results[i] = await fn(items[i]); } } await Promise.all(Array.from({ length: limit }, worker)); return results; } const idChunks = chunk(allIds, 100); await runWithConcurrency(idChunks, 5, async (ids) => { const response = await fetch("https://api.signa.so/v1/trademarks/batch", { method: "POST", headers: { Authorization: `Bearer ${process.env.SIGNA_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ ids }), }); const limitInfo = parseRateLimitHeader(response.headers.get("RateLimit")); if (limitInfo && limitInfo.remaining < 20) { // Getting close to the ceiling, slow down for the rest of this window await new Promise((resolve) => setTimeout(resolve, limitInfo.reset * 1000)); } return response.json(); }); ``` If you are on the SDK, its built-in retry logic already handles `429` with `Retry-After`, see [Resilience Patterns](/guides/resilience). The concurrency guard above is for avoiding the `429` in the first place when you control the fan-out. ## What's next Retry logic, circuit breakers, and idempotent mutations for fault-tolerant integrations. The full cursor-pagination contract for list and search endpoints. # Caching & ETags Source: https://docs.signa.so/guides/caching Reduce redundant requests with conditional fetching and cache strategies Trademark data changes infrequently for most records. By leveraging ETags and conditional requests, you can avoid re-downloading unchanged data and speed up your application. ## How ETags Work When you fetch a resource, the response includes an `ETag` header containing a content fingerprint: ``` HTTP/1.1 200 OK ETag: W/"a3f2b7c8d1e4" Cache-Control: private, max-age=300 Content-Type: application/json { "id": "tm_7d4e1f2a", "object": "trademark", "mark_text": "SIGNA", ... } ``` Treat the ETag as an opaque string, including its `W/` prefix. Trademark detail uses a weak validator: it identifies equivalent trademark data while allowing the per-request `request_id` and content encoding to differ. Both `200` and `304` responses carry the same weak form. A previously issued strong form still works in `If-None-Match`; a matching response returns the canonical weak form. Meaningful changes to the detail data or requested projection invalidate the validator. Weak validators support conditional GETs, but do not promise byte-for-byte equality or support strong preconditions such as `If-Match` and `If-Range`. Use `X-Request-Id` for the current request's correlation ID when a `304` reuses a cached body. On subsequent requests, send the ETag back via `If-None-Match`. If the resource has not changed, you get a `304 Not Modified` with no body, saving bandwidth and processing time: ``` GET /v1/trademarks/tm_7d4e1f2a If-None-Match: W/"a3f2b7c8d1e4" HTTP/1.1 304 Not Modified ETag: W/"a3f2b7c8d1e4" ``` Rate limiting is enforced before the route runs, based on the endpoint's type and transport tier, not on the response status it eventually returns. A `304 Not Modified` counts against your rate limit exactly the same as the `200` it would have returned. ETags save you bandwidth and latency, not rate-limit budget, so they are still worth using on hot reads, just don't rely on them to raise your effective request ceiling. *** ## Endpoints That Support ETags | Endpoint | ETag Support | Typical `max-age` | | ---------------------------------- | ------------ | ----------------- | | `GET /v1/trademarks/:id` | Yes | 300 s (5 min) | | `GET /v1/owners/:id` | Yes | 300 s | | `GET /v1/attorneys/:id` | Yes | 300 s | | `GET /v1/firms/:id` | Yes | 300 s | | `GET /v1/offices` | Yes | 3600 s (1 hour) | | `GET /v1/jurisdictions` | Yes | 3600 s | | `GET /v1/trademarks` (list/search) | No | - | | `POST /v1/trademarks` (search) | No | - | | `POST /v1/trademarks/batch` | No | - | Reference data endpoints (offices, jurisdictions, classifications) change very rarely. Cache these aggressively with a long TTL. *** ## Cache-Control Headers Every cacheable response includes a `Cache-Control` header: ``` Cache-Control: private, max-age=300 ``` | Directive | Meaning | | ----------- | --------------------------------------------------------------------------------------------- | | `private` | Response is specific to this API key and must not be stored by shared caches (CDNs, proxies). | | `max-age=N` | The response is considered fresh for N seconds. After that, revalidate with `If-None-Match`. | | `no-store` | Present on mutation responses (POST, PATCH, DELETE). Do not cache. | All Signa responses include `private` because they are scoped to your organization. Never cache API responses in a shared/public cache. *** ## Conditional Request Flow ```typescript TypeScript theme={null} const cache = new Map(); async function getTrademarkCached(id: string, apiKey: string) { const url = `https://api.signa.so/v1/trademarks/${id}`; const headers: Record = { Authorization: `Bearer ${apiKey}`, }; // Send ETag if we have a cached version const cached = cache.get(id); if (cached) { headers['If-None-Match'] = cached.etag; } const response = await fetch(url, { headers }); if (response.status === 304 && cached) { // Not modified, return cached data return cached.data; } if (!response.ok) { throw new Error(`API error: ${response.status}`); } const data = await response.json(); const etag = response.headers.get('ETag'); // Store in cache if (etag) { cache.set(id, { etag, data }); } return data; } ``` *** ## Caching Strategies for Trademark Data Different types of trademark data have different change frequencies. Tailor your caching strategy accordingly. ### Reference Data (offices, jurisdictions, classifications) These change only when Signa adds a new office or jurisdiction. * **Strategy**: Cache locally with a 24-hour TTL. Revalidate with ETags daily. * **Storage**: In-memory or local file. ### Individual Trademarks Most trademark records change infrequently (a few times per year), but some change during active prosecution. * **Strategy**: Cache with the `max-age` value from the response (typically 5 min). Revalidate with ETags after expiry. * **Storage**: In-memory cache (Redis, local Map) keyed by trademark ID. ### Search Results Search results are dynamic and depend on query parameters, so they are not ETag-cacheable. * **Strategy**: Client-side TTL cache keyed by the full query hash. A 30-60 second TTL works well for typeahead and repeated searches. * **Storage**: In-memory only. Do not persist search result caches. ### List Endpoints Paginated lists may change as new records are added. * **Strategy**: Short TTL (60 s from `max-age`). Revalidate with ETags. Note that the cursor itself provides consistency within a pagination session. * **Storage**: In-memory, keyed by the full URL including query parameters. *** ## Cache Invalidation ### Analytics freshness Analytics reports use a 24-hour cache TTL as a backstop. Owner, attorney, firm, and entity updates invalidate affected reports through the search-indexer, so freshness is event-driven rather than sweep-based. Inspect `Server-Timing: cache;dur=0;desc="hit"` (or `desc="miss"`) to see whether a report was served from cache; this signal is not included in the response body. ETags handle revalidation automatically: a conditional `GET` against the same resource returns `304 Not Modified` when the record is unchanged, so your cache entry stays valid. As noted above, that request still counts against your rate limit like any other call to the endpoint, so revalidating on every read is a latency and bandwidth optimization, not a way to make more requests than your plan allows. For records you display in hot UI surfaces, don't wait for the next ETag check to notice a change: set a [watch](/guides/monitoring/watches) on those marks and invalidate the cache entry when the webhook fires. `updated_at` on the record tells you when Signa last wrote it, and `provenance.source_data_date` tells you which office publication it came from. Pair ETags for normal traffic with watches on your hottest marks. You get efficient revalidation on every read plus push-based busting on the small set of records that actually churn. *** ## Multi-Tier Caching For applications that display trademark data to end users, consider a two-tier approach: | Tier | TTL | Purpose | | ------------------------- | -------- | -------------------------------------------------------------------------------- | | **L1: In-process** | 30-60 s | Eliminates redundant API calls within a single request cycle. | | **L2: Redis / Memcached** | 5-15 min | Shares cached data across application instances. Uses ETag revalidation on miss. | ```typescript theme={null} async function getTrademark(id: string): Promise { // L1: Check in-process cache const l1 = memoryCache.get(id); if (l1) return l1; // L2: Check Redis const l2 = await redis.get(`tm:${id}`); if (l2) { const parsed = JSON.parse(l2); memoryCache.set(id, parsed, { ttl: 30_000 }); return parsed; } // L3: Fetch from API with ETag revalidation const data = await getTrademarkCached(id, apiKey); memoryCache.set(id, data, { ttl: 30_000 }); await redis.set(`tm:${id}`, JSON.stringify(data), 'EX', 900); return data; } ``` # Chain-of-title & Lien Diligence Source: https://docs.signa.so/guides/chain-of-title-lien-diligence Check recorded transfers, security interests, and releases before a brand transaction When a mark is part of an acquisition, financing, or portfolio sale, diligence teams need to know who received title, whether any lender recorded a security interest, and whether that lien was released. This recipe walks from owner-level lien filtering to a mark-level chain of title, then pairs a release to the original security-interest reel and frame. Use [Search Assignments](/api-reference/records/transactions/search-assignments) for cross-mark transaction filtering, [Trademark Assignments](/api-reference/trademarks/trademark-assignments) for one mark's chain, and [Retrieve Assignment](/api-reference/records/transactions/retrieve-assignment) for the full parties and affected marks. ## Prerequisites * A Signa API key with `trademarks:read` * The target owner ID (`own_...`) or entity ID (`ent_...`) * The trademark ID (`tm_...`) for any mark you want to inspect directly Start with `type=security_interest` and the borrower or seller owner ID. The collection endpoint is filter-required, so every request stays scoped to a diligence question. ```bash cURL theme={null} curl -G "https://api.signa.so/v1/assignments" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ --data-urlencode "owner_id=own_22222222-2222-4222-8222-222222222222" \ --data-urlencode "type=security_interest" \ --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 liens = await signa.assignments.list({ owner_id: "own_22222222-2222-4222-8222-222222222222", type: "security_interest", limit: 20, }); ``` If the borrower sits inside a larger corporate family, repeat the same query with `entity_id`. Signa expands the entity to member owners before filtering assignment parties. Retrieve the security-interest record to confirm the parties and affected marks. Save the `reel_no` and `frame_no`, because releases point back to those values. ```typescript TypeScript theme={null} const lien = await signa.assignments.retrieve( "asg_66666666-6666-4666-8666-666666666666", ); const grant = { reel_no: lien.reel_no, frame_no: lien.frame_no, lender: lien.parties.find((party) => party.role === "assignee")?.name, marks: lien.properties.map((property) => property.trademark_id), }; ``` For the worked example, the lien has `reel_no: "9102"` and `frame_no: "0100"`, with Northstar Brands LLC as assignor and First Continental Bank, N.A. as assignee. Pull the per-mark chain to see transfers, liens, and releases together in recorded-date order. ```bash cURL theme={null} curl "https://api.signa.so/v1/trademarks/tm_11111111-1111-4111-8111-111111111111/assignments" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` ```typescript TypeScript theme={null} const chain = await signa.trademarks.assignments( "tm_11111111-1111-4111-8111-111111111111", ); for await (const transaction of chain) { console.log(transaction.recorded_date, transaction.conveyance_type, transaction.reel_no, transaction.frame_no); } ``` In the example chain, `asg_55555555-5555-4555-8555-555555555555` records title moving from OldCo Consumer Products Inc. to Northstar Brands LLC. `asg_66666666-6666-4666-8666-666666666666` records the security interest. `asg_77777777-7777-4777-8777-777777777777` records a later release. A release points back to the grant it releases with `release_of_reel_no` and `release_of_frame_no`. Match those fields against the security-interest `reel_no` and `frame_no`. ```typescript TypeScript theme={null} const transactions = await chain.toArray(); const grants = transactions.filter((item) => item.conveyance_type === "security_interest"); const releases = transactions.filter((item) => item.conveyance_type === "release"); const releasePairs = grants.map((grant) => ({ grant_id: grant.id, grant_reel_no: grant.reel_no, grant_frame_no: grant.frame_no, release: releases.find( (release) => release.release_of_reel_no === grant.reel_no && release.release_of_frame_no === grant.frame_no, ) ?? null, })); ``` In the worked example, the release has `release_of_reel_no: "9102"` and `release_of_frame_no: "0100"`, so it clears the `9102/0100` security interest. If no release pairs to a grant, escalate it for legal review before treating the mark as clean collateral or clean title. Use the combined evidence to separate clean title from open issues: * A current owner should appear in a recent assignment or other transfer record. * A security interest without a matching release may require payoff, consent, or an exception in the transaction documents. * A release matching the grant reel/frame is the strongest normalized signal that the recorded lien was cleared. * If the owner is part of a larger entity, rerun the lien filter with `entity_id` to catch affiliates and office-specific owner records. # Classifications & Goods/Services Source: https://docs.signa.so/guides/classifications Understand the Nice Classification system and search 96,000+ pre-approved goods and services descriptions across offices ## Why This Matters When filing a trademark, you must specify exactly what goods or services your mark covers. This isn't freeform: each trademark office maintains a list of **pre-approved descriptions** they accept. Using the right wording reduces the risk of examiner objections, which cause delays and extra costs. The challenge: different offices accept different wording. A description accepted at the USPTO may be rejected at the EUIPO, and vice versa. Signa solves this by aggregating **96,000+ pre-approved descriptions** from the world's two largest term databases, merged into a single searchable API. ## The Nice Classification System All trademark offices worldwide use the [Nice Classification](https://www.wipo.int/en/web/classification-nice), a system of 45 classes that categorize goods and services: * **Classes 1–34**: Goods (physical products, chemicals, software, etc.) * **Classes 35–45**: Services (advertising, legal, education, etc.) Use `GET /v1/classifications?type=goods` or `?type=services` to filter, or fetch all 45 with no filter. The 45 class **headings** are universal: every office uses them. But the specific **descriptions** within each class are where offices diverge. For example, Class 9 covers "scientific and electronic apparatus." But when you file, you can't just write "Class 9." You need a specific description like: > "Downloadable computer software for managing cryptocurrency transactions" Whether that exact wording is accepted depends on which office you're filing at. ## How Office Acceptance Works There is no single universal list of accepted descriptions. Different offices accept different wording, with partial overlap between them. Signa aggregates terms from the Harmonised Database (maintained by EUIPO via TMClass, adopted by 63+ offices worldwide) and the USPTO ID Manual (the US-specific database), and merges them at query time so you query one API instead of several office-specific tools. When a term is tagged `is_harmonised: true`, it means the wording has been formally cross-accepted across all five TM5 partner offices (USPTO, EUIPO, JPO, KIPO, CNIPA): the safest choice for multi-jurisdiction filings. Selecting a harmonised or office-accepted term significantly reduces (though does not eliminate) the risk of an examiner objection, since office practice can still evolve. ## Searching for Terms Use the goods & services endpoint to search within a specific Nice class: ```bash cURL theme={null} curl -G "https://api.signa.so/v1/goods-services" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=blockchain" \ --data-urlencode "class=9" ``` ```typescript TypeScript theme={null} const terms = await signa.goodsServices.list({ q: "blockchain", class: 9, }); for (const term of terms.data) { console.log(term.term, "→", term.accepted_offices); } ``` ```json Example Response theme={null} { "object": "list", "data": [ { "term": "Downloadable computer software for managing cryptocurrency transactions on a blockchain", "term_key": "downloadable computer software for managing cryptocurrency transactions on a blockchain", "is_harmonised": false, "accepted_offices": ["USPTO"], "source": "uspto_idm", "language": "en" }, { "term": "Downloadable software for blockchain-based inventory management", "term_key": "downloadable software for blockchain-based inventory management", "is_harmonised": false, "accepted_offices": ["USPTO"], "source": "uspto_idm", "language": "en" } ], "has_more": false, "pagination": { "cursor": null } } ``` ### Understanding the Response Fields | Field | What it tells you | | ------------------ | ---------------------------------------------------------------------------------------------------------- | | `term` | The exact description text. Use this wording in your filing. | | `accepted_offices` | Which offices accept this exact wording. If you see `['USPTO', 'EUIPO']`, it works at both. | | `is_harmonised` | `true` = accepted at all TM5 offices (US, EU, Japan, Korea, China). Safest for multi-jurisdiction filings. | | `source` | Where this term came from: `tmclass` (EUIPO), `uspto_idm` (USPTO). | ### Multi-Source Merging When the same term exists in both the EUIPO and USPTO sources, Signa merges them into a single result with a combined `accepted_offices` list. For example, a term like "Acetone" that's separately accepted at EUIPO and at USPTO (plus its TM5 partners) is returned as one row with `accepted_offices: ['EUIPO', 'USPTO', 'JPO', 'KIPO', 'CNIPA']`. This happens automatically: you always see one result per unique term with the full set of offices. ## Common Patterns ### Find descriptions for a product Search with product keywords to find pre-approved descriptions: ```bash theme={null} # "What descriptions can I use for my AI legal research tool?" curl -G "https://api.signa.so/v1/goods-services" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=legal research software" \ --data-urlencode "class=42" ``` ### Find terms accepted at a specific office Filter the results client-side by `accepted_offices`: ```typescript theme={null} const terms = await signa.goodsServices.list({ q: "software", class: 9, limit: 100, }); // Terms accepted at USPTO const usptoTerms = terms.data.filter(t => t.accepted_offices.includes("USPTO") ); // Terms accepted at BOTH USPTO and EUIPO const bothOffices = terms.data.filter(t => t.accepted_offices.includes("USPTO") && t.accepted_offices.includes("EUIPO") ); ``` ### Find the safest wording for multi-jurisdiction filings Use `harmonised_only=true` to get only TM5-harmonised terms: pre-approved wording accepted by all five TM5 partner offices: ```bash theme={null} curl -G "https://api.signa.so/v1/goods-services" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=computer software" \ --data-urlencode "class=9" \ --data-urlencode "harmonised_only=true" ``` These terms carry the lowest risk of examiner objection across jurisdictions. ### Not sure which class? Three options, in order of how much context you have: 1. **Natural-language description**: use [Suggest Classifications](/api-reference/reference/suggest-classifications). Pass a business description like `"SaaS tool for HR teams"` and it returns ranked classes with confidence scores and paste-ready terms. 2. **Literal term lookup**: omit `class` on the terms endpoint to see every class where that wording is accepted: ```bash theme={null} curl -G "https://api.signa.so/v1/goods-services" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=poster" ``` Inspect `class_number` on each result to discover relevant classes. 3. **Class heading keyword**: search the 45 class headings for an obvious keyword: ```bash theme={null} curl -G "https://api.signa.so/v1/classifications" \ -H "Authorization: Bearer sig_YOUR_KEY" \ --data-urlencode "q=software" ``` Software-related goods are typically in Class 9, while software-related services are in Class 42. ## Data Coverage Signa's classification term database is updated automatically: | Source | Update frequency | Coverage | | ------------------------------------ | ---------------- | ----------------------------------------- | | Nice Classification headings | Monthly | 45 classes, \~10,000 base terms | | EUIPO TMClass (Harmonised Database) | Quarterly | 43,000+ terms accepted across 63+ offices | | USPTO ID Manual | Monthly | 70,000+ terms accepted at USPTO | | Vienna Classification (design codes) | Monthly | 2,100+ visual element codes | **Data freshness**: The Nice taxonomy updates once per year (January). Individual terms are added throughout the year by each office. Signa syncs monthly to capture additions while keeping API response times fast. ## Related Turn a business description into ranked classes + filing-ready terms Full endpoint reference with parameters and response schema Get all 45 Nice class headings Use classifications in a full clearance workflow # Compliance & Privacy Source: https://docs.signa.so/guides/compliance GDPR, data privacy, security, and professional responsibility guidance Signa serves public trademark registry data: mark identity, filing details, owner and attorney information, proceedings, and corporate linkage (SEC, GLEIF), all sourced from official government registries and public corporate-data providers. Signa does not collect or process private communications, attorney-client privileged information, financial records, or scraped social data. ## Personal data and GDPR Trademark applicants and registrants are sometimes individuals, so their name, country, and address (as published by the office) can constitute personal data under GDPR. Signa's processing of this already-public registry data relies on legitimate interest (Article 6(1)(f)), the same basis used by official trademark databases and other trademark intelligence services. If an individual's data is removed from the source registry, Signa removes it on the next sync. Send erasure or access requests to [privacy@signa.so](mailto:privacy@signa.so) with the relevant trademark ID. Enterprise customers who need a formal Data Processing Agreement can request one from [legal@signa.so](mailto:legal@signa.so). ## Query confidentiality API request logs, including search queries, are retained for 90 days and scoped strictly to your organization: no other customer or API user can see them. If your queries could reveal sensitive strategy (for example, marks a client intends to file), use API keys that are not tied to a specific matter and check your own organization's policies on using third-party search tools for that work. ## Security * **Encryption:** TLS in transit; encryption at rest for all stored data. * **Access:** Least-privilege access to production systems and data. * **Deployments:** Every deployment is audited, with access logging on administrative operations. Contact [security@signa.so](mailto:security@signa.so) for a security questionnaire or current certification status. Signa is a data platform, not a legal tool. It provides normalized registry data and computed insights (deadlines, status, entity resolution) as informational aids, not legal advice. Verify critical data points against the official registry and consult qualified trademark counsel for legal decisions. # Data Freshness & Coverage Source: https://docs.signa.so/guides/data-freshness Per-office sync schedules and consistency model Signa syncs trademark data from production offices, with more planned. Each office publishes data on its own schedule. This guide explains what to expect in terms of data freshness and how to check the current state of each source. ## Per-office sync schedule ### Production offices | Office | Code | Frequency | | ------------ | ---- | ------------------ | | USPTO | `US` | Daily | | INPI France | `FR` | Weekly | | EUIPO | `EM` | Daily | | IP Australia | `AU` | Daily | | CIPO | `CA` | Weekly | | WIPO | `WO` | Weekly per country | | IPOS | `SG` | Daily | | PRV | `SE` | Daily | | IPI | `CH` | Daily | | NIPO | `NO` | Daily | Each record carries its own `source_data_date`, the authoritative answer to "how current is this trademark?" Use it instead of guessing from the office's sync frequency. ### Planned offices Additional offices are planned and will be onboarded progressively: DPMA (Germany), UKIPO (United Kingdom), BOIP (Benelux), DKPTO (Denmark), PRH (Finland), ISIPO (Iceland), UPRP (Poland), IMPI (Mexico), JPO (Japan), KIPO (South Korea), CNIPA (China), DIP (Thailand), IP Vietnam. Sync frequencies represent the target schedule for production offices. Actual freshness depends on office uptime and data availability. Use the `source_data_date` field to determine the actual age of any individual record. ## Understanding `source_data_date` Every trademark's detail response carries a `provenance` object that tells you when the office published the data and when Signa last fetched it, alongside the two top-level Signa timestamps: | Field | Meaning | | ------------------------------ | --------------------------------------------------------------- | | `provenance.source_data_date` | When the office published this version of the data | | `provenance.office_updated_at` | The office's own last-modified timestamp, when it publishes one | | `provenance.retrieved_at` | When Signa last fetched the full detail record from the office | | `updated_at` | When Signa last wrote to this record | | `created_at` | When Signa first synced this record | For example, a USPTO record with `provenance.source_data_date: "2026-03-20"` and `updated_at: "2026-03-22T10:15:00Z"` means the data was part of the office's March 20 publication, which Signa processed on March 22. Search and list rows carry the office timestamp flat as `office_updated_at`; on the detail tier the same value lives at `provenance.office_updated_at`. ```bash cURL theme={null} # Check when a mark was last updated curl -s https://api.signa.so/v1/trademarks/tm_abc123 \ -H "Authorization: Bearer sig_YOUR_KEY" | jq '{source_data_date: .provenance.source_data_date, updated_at}' ``` ```typescript TypeScript theme={null} const tm = await signa.trademarks.retrieve("tm_abc123"); console.log(tm.provenance.source_data_date); // "2026-03-20" console.log(tm.updated_at); // "2026-03-22T10:15:00Z" ``` ## Consistency model Signa offers two consistency tiers for trademark reads. The difference matters when you read a record immediately after it was updated. ### Detail endpoints * **Consistency:** Immediate (strong read-after-write) * **Endpoints:** `GET /v1/trademarks/{id}` and other single-resource fetches * Use when you need the absolute current state, right after a scheduled sync or a correction. ### Search endpoints * **Consistency:** Eventually consistent * **Lag:** Typically under 30 seconds after a write * **Endpoints:** `GET` and `POST /v1/trademarks` (list and search) * In rare cases, such as right after a large batch of updates, lag can extend to a few minutes. **What this means in practice:** 1. When a trademark is updated, the detail endpoint reflects the change immediately. 2. The search endpoint may take up to 30 seconds to reflect the same change. 3. Searches followed by detail fetches (the typical pattern) always see consistent data, because the detail endpoint reflects every write immediately, even if search hasn't caught up yet. ```bash cURL theme={null} # Detail endpoint: always current curl -s https://api.signa.so/v1/trademarks/tm_abc123 \ -H "Authorization: Bearer sig_YOUR_KEY" # Search endpoint: eventually consistent curl -s -X POST https://api.signa.so/v1/trademarks \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "SIGNA"}' ``` ```typescript TypeScript theme={null} // Immediate consistency const detail = await signa.trademarks.retrieve("tm_abc123"); // Eventually consistent const results = await signa.trademarks.search({ query: "SIGNA" }); ``` ## Checking office status Use the reference data endpoints to check the current sync status of each office: ```bash cURL theme={null} # List all offices with their sync status curl -s https://api.signa.so/v1/offices \ -H "Authorization: Bearer sig_YOUR_KEY" # Get a specific office curl -s https://api.signa.so/v1/offices/US \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} const offices = await signa.references.offices(); for (const office of offices.data) { console.log(office.code, office.last_synced_at, office.total_marks); } ``` ## Finding recently updated records Filter by `updated_at` to find records that changed within a time window: ```bash cURL theme={null} # All marks updated in the last 24 hours SINCE=$(date -u -d '-24 hours' '+%Y-%m-%dT%H:%M:%SZ') curl -s "https://api.signa.so/v1/trademarks?updated_at_gte=$SINCE&sort=-updated_at" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const recent = await signa.trademarks.list({ updated_at_gte: since, sort: "-updated_at", }); ``` For the office's own timeline on a specific mark, use [Trademark Events](/api-reference/trademarks/trademark-events). ## Known limitations Certain offices publish data with a built-in delay. For example, some offices only publish weekly gazette updates, meaning a status change on Monday may not appear in Signa's data until the following week's publication. A full sync captures the current state of each record but may not include all historical events. Some offices only provide current snapshots without event history. Signa preserves all events it observes going forward, but events that occurred before the first sync may be missing. Not all offices make mark images available through their data feeds. Some require separate image downloads. Image availability is indicated by the `has_media` field on trademark records. Most offices provide goods/services descriptions in their local language. Signa stores the original text and language code but does not translate. Some offices (EUIPO, WIPO) provide multi-language descriptions. # Data Quality & Trust Source: https://docs.signa.so/guides/data-quality How Signa sources, verifies, and maintains trademark data Signa syncs trademark data directly from official government registries. Records are normalized to a canonical schema, deduplicated, and kept current as offices publish updates. This page explains what you can expect from the data and where to look when questions arise. ## Office coverage Signa currently syncs data from production offices covering trademark records, with more planned. See [Data Freshness & Coverage](/guides/data-freshness) for the full per-office table, sync frequency, and planned offices. Record counts are point-in-time totals from each registry. Use `GET /v1/offices` to see live statistics for each office. ## Data freshness Every trademark record carries a `provenance` object in the detail tier that tells you exactly how current the data is: ```json theme={null} { "provenance": { "source_data_date": "2026-03-18", "office_updated_at": "2026-03-17T00:00:00Z", "retrieved_at": "2026-03-18T04:00:00Z" }, "updated_at": "2026-03-18T04:00:00Z" } ``` | Field | Meaning | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `provenance.source_data_date` | The date the office's data was current. For bulk publications, this is the publication date. For API fetches, this is the fetch date. | | `provenance.office_updated_at` | The office's own last-modified timestamp for the record, when it publishes one. | | `provenance.retrieved_at` | When Signa last fetched the full detail record from the office. Null for offices that only publish bulk files. | | `updated_at` | When Signa last wrote to this record. If this is significantly newer than `source_data_date`, the record was reprocessed without new office data. | Use `updated_at` on any trademark to see when Signa last touched the record, and `source_data_date` to see when the office published that data. For time-sensitive decisions (filing deadlines, opposition windows), always cross-reference with the official registry. ## Verification guidance ### Trademark clearance When conducting a clearance search, Signa provides a strong starting point but should not be your only source. Recommended practice: 1. Search Signa for phonetic, fuzzy, and exact matches across target jurisdictions. 2. Cross-reference high-risk results against the official registry. 3. Check common-law sources separately (Signa covers registered marks only). 4. Document the search date and the `source_data_date` from each result. ### Ongoing monitoring For ongoing portfolio monitoring, Signa's daily sync frequency is sufficient for most use cases. Schedule periodic pulls against the API to detect: * Competitor filings of confusingly similar marks (re-run a search with `filing_date_gte`) * Status changes on your own marks ([Trademark Events](/api-reference/trademarks/trademark-events)) * Approaching renewal or declaration deadlines (the `deadlines[]` array on each trademark response) ### Due diligence For M\&A or investment due diligence, use Signa's batch endpoint to pull full detail-tier records for an entire portfolio, then verify critical marks (high-value registrations, marks in dispute) against official sources. Signa normalizes data from public government registries. It does not replace legal advice. Status classifications, deadline computations, and entity resolution are provided as informational tools. Always consult qualified trademark counsel for legal decisions. ## Known gaps No data source is perfect. Here are the known limitations: | Area | Detail | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Common-law marks** | Signa covers registered and pending marks only. Unregistered common-law rights are not included. | | **Image search** | Design mark similarity search (image-to-image) is planned. Currently, design marks are searchable by Vienna classification codes. | | **Attorney data (WIPO)** | Madrid Monitor provides limited attorney/representative information compared to national offices. | | **Historical events** | Some offices provide limited event history for older marks. Recent filings typically have complete event trails. | | **Office coverage** | See [Data Freshness & Coverage](/guides/data-freshness) for the current list of production and planned offices. For time-sensitive decisions, always cross-reference with the official registry. | ## Reporting data issues If you find a discrepancy between Signa and an official registry, we want to hear about it. Because Signa is API-first, you can report it through the same API you already use, no dashboard or email thread required. This is the fastest path for both people and AI agents. Submit a `data_issue` to [`POST /v1/feedback`](/api-reference/administration/create-feedback) with the fields that make it actionable without a follow-up: * `resource_id`: the Signa record that is wrong (`tm_...`, `own_...`, and so on) * `field`: which field disagrees, for example `owner.name` * `expected_value`: what the value should be, per the official registry * `message`: any context, such as the registry reference number or a link to the source * `request_id`: optionally, the request where you saw it, so we see exactly what you saw ```typescript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const report = await signa.feedback.create({ type: "data_issue", resource_id: "tm_9f8e7d6c-5b4a-3210-fedc-ba9876543210", field: "owner.name", expected_value: "Acme Holdings LLC", message: "Assigned to Acme last year per USPTO reel/frame 9999/001.", }); console.log(report.id, report.status); // fbk_... open ``` **What Signa captures.** At submit time the server snapshots the referenced record's current state (office, version, source-updated timestamp, status) and, if you passed a `request_id`, the matching request log into a read-only `context` object on the report. That evidence is preserved even after the next sync overwrites the record or the request log ages out, so your report never loses its context. **Following the status loop.** Every report starts `open`, moves to `acknowledged` when we begin investigating, and lands on `resolved` with a `resolution_note` explaining the outcome. Poll [`GET /v1/feedback/{id}`](/api-reference/administration/get-feedback), or filter [`GET /v1/feedback`](/api-reference/administration/list-feedback) by `status`, to watch a report progress. Reports are visible only to your own organization. Confirmed data issues are typically resolved within one sync cycle after a fix is deployed. Prefer email? You can still reach us at [support@signa.so](mailto:support@signa.so) with the subject line "Data Quality Report". Include the trademark ID, the official registry and reference number, the fields that differ, and a link to the official source. # Deadline Rules Source: https://docs.signa.so/guides/deadline-rules How Signa computes trademark maintenance deadlines across jurisdictions Signa includes a rule-based deadline engine that computes upcoming maintenance deadlines for every registered trademark. The engine has rules defined for 25 jurisdictions (26 rule sets; US has domestic + Madrid), covering every production office and beyond (see [office coverage](/guides/data-freshness)), producing actionable deadline objects with status, urgency, and consequence information. ## How Deadlines Are Computed Given a trademark's date fields and jurisdiction, the deadline engine returns an array of computed deadlines. Each one carries its type, due date, urgency, grace period (where applicable), and consequence-if-missed information. The same inputs always produce the same output. Each computed deadline includes: | Field | Description | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `renewal`, `declaration_of_use`, `combined_renewal_and_use`, `declaration_of_incontestability`, `restoration`, `international_renewal` | | `name` | Human-readable label (e.g., "Section 8 — Declaration of Use") | | `due_date` | The date the deadline falls | | `window_opens` | Earliest date you can file | | `grace_expiry` | Last day of grace period (if applicable) | | `status` | `future`, `window_open`, `due_soon`, `in_grace`, `missed` | | `urgency` | `routine`, `upcoming`, `critical`, `in_grace`, `missed` | | `days_until_due` | Signed integer (negative = overdue) | | `consequence_if_missed` | What happens if you miss it entirely: see [Consequence vocabulary](#consequence-vocabulary) | ### Consequence vocabulary `consequence_if_missed` names the **mechanism** the office's own provision prescribes when the deadline and any grace period pass. It is one axis only: whether a remedy exists afterwards is said by the presence or absence of a `restoration` rule for the jurisdiction, never by this value. | Value | Meaning | Examples | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `expiration` | The right ends by operation of law when its term (plus any grace) runs out; no office act is needed and any later register entry only records it. | PL ("wygasa"), BR ("extingue-se"), MX ("caducará de pleno derecho"), JP, KR, TR, TH, FR, BX, US §9 | | `removal_from_register` | The statute makes the office's striking of the entry the consequence (removed, deleted, gelöscht, udslettet, slettes, poistetaan, afmáð, "cancel the mark from the register"), usually with effect from the expiry. | EU (Art 53(8) EUTMR), DE (§ 47(6) MarkenG), DK, FI, NO, SE, IS, GB, AU, SG, IN, PH, CH, CN (商标法 第四十条 注销, which falls after the six-month 宽展期, i.e. after the term is already over) | | `cancellation` | The office cancels a registration that is still within its term, as the sanction for a missed maintenance filing. Never a third-party action. | US §8 and §71 declarations of use. Also VN, whose label rests on no pinned primary text yet and is a promotion target rather than a verified mechanism. | | `cancellation_and_expiration` | A combined US §8 + §9 filing: both consequences. | US ten-year renewal | | `expungement` | Canada's statutory word: the Registrar expunges the registration, deemed at expiry (s. 46(3)). | CA | | `lapse_of_international_registration` | The Madrid international registration lapses at WIPO and every designation falls with it. | WIPO, and the Madrid renewal limbs served on a designated office's own record — `ca_renewal_madrid` and `mx_renewal_madrid` — which are renewed at WIPO rather than at CIPO or IMPI | | `permanent_loss` | Restoration rules only: the last available remedy has passed. | GB, SG, IN, IS, JP restoration, and EU restitutio in integrum (`eu_restitutio`) | | `none` | An optional filing. | US §15 incontestability | `irrevocable_loss` was retired on 3 September 2026 (it described restorability, which the restoration rule already states); no rule serves it, and consumers that stored it should read it as the jurisdiction's current value. ### Trigger Dates Each jurisdiction defines a **trigger field**: the date from which deadlines are calculated. This varies by jurisdiction: | Trigger | Jurisdictions | | ------------------------ | ------------------------------ | | `registration_date` | US (domestic), CA, PH | | `filing_date` | EU, DE, FR, CH, BX, GB, AU, PL | | `protection_grant_date` | US (Madrid) | | `intl_registration_date` | WIPO, CA (Madrid), MX (Madrid) | Some jurisdictions changed their trigger field on a specific date. For example, Sweden switched from `registration_date` to `filing_date` on 2019-01-01. The engine handles this automatically based on the mark's filing date. ## US Trademark Deadlines The United States has the most complex deadline structure of any jurisdiction, because it requires proof of continued use in addition to renewal. ### US Domestic Marks ```mermaid theme={null} gantt title US Trademark Deadline Timeline (Domestic) dateFormat YYYY axisFormat %Y section Registration Registration Date :milestone, reg, 2025, 0d section Year 5-6 Section 8 (Use) window :active, s8w, 2030, 365d Section 8 due :milestone, s8, 2031, 0d Section 8 grace :crit, s8g, 2031, 182d Section 15 (Optional) :done, s15, 2030, 365d section Year 9-10 Section 8+9 (Renewal) window :active, s89w, 2034, 365d Section 8+9 due :milestone, s89, 2035, 0d Section 8+9 grace :crit, s89g, 2035, 182d section Year 19-20 Section 8+9 (2nd Renewal) :active, s892w, 2044, 365d Section 8+9 due :milestone, s892, 2045, 0d ``` **Section 8 — Declaration of Use (years 5-6)** * **Due:** 6 years after registration * **Window opens:** 1 year before (year 5) * **Grace period:** 6 months (with surcharge) * **Consequence if missed:** Cancellation * Required for all US domestic registrations **Section 15 — Declaration of Incontestability (years 5-6)** * **Due:** Eligible from year 5, commonly filed with Section 8 by year 6 * **Optional:** No penalty for missing it * **Benefit:** Makes the registration conclusive evidence of validity * No grace period **Section 8+9 — Renewal + Declaration of Use (every 10 years)** * **Due:** 10 years after registration, then every 10 years * **Window opens:** 1 year before * **Grace period:** 6 months (with surcharge) * **Consequence if missed:** Cancellation and expiration ### US Madrid Marks Madrid Protocol registrations designating the US use **Section 71** (the Madrid equivalent of Section 8) instead of Section 8. The trigger date is `protection_grant_date` instead of `registration_date`. Renewal is handled at WIPO, not at the USPTO. | Deadline | Trigger | Due | Grace | | ----------------------- | ----------------------- | -------------- | --------------- | | Section 71 (first) | protection\_grant\_date | Year 6 | 6 months | | Section 15 | protection\_grant\_date | Year 6 | None (optional) | | Section 71 (at renewal) | protection\_grant\_date | Every 10 years | 6 months | ```bash cURL theme={null} # Get deadlines for a specific trademark curl -s https://api.signa.so/v1/trademarks/tm_abc123 \ -H "Authorization: Bearer sig_YOUR_KEY" | jq '.derived.deadlines' ``` ```typescript TypeScript theme={null} const tm = await signa.trademarks.retrieve("tm_abc123"); for (const deadline of tm.derived.deadlines) { // Dates, not countdowns. Compare against your own clock for status. console.log(deadline.name, deadline.window_opens, deadline.due_date); } // "Section 8 — Declaration of Use" "2030-01-20" "2031-01-20" // "Section 8+9 — Renewal + Declaration of Use" "2034-01-20" "2035-01-20" ``` ## EU Trademark Deadlines The European Union has a simpler structure. No proof of use is required at renewal. | Field | Value | | ----------------- | ------------------------ | | **Trigger** | `filing_date` | | **Renewal cycle** | Every 10 years | | **Window opens** | 6 months before due date | | **Grace period** | 6 months after due date | | **Consequence** | Cancellation | | **Proof of use** | Not required at renewal | ## Jurisdiction Quirks ### Germany (DPMA): End-of-Month Rule For marks filed **before 2019-01-14**, the expiry date is the last day of the month in which the anniversary falls, not the exact anniversary date. For example, a mark filed on 2015-03-15 expires on 2025-03-31, not 2025-03-15. Marks filed on or after 2019-01-14 use the exact anniversary (standard behavior). ### Canada (CIPO): Pre-2019 15-Year Terms Canada changed its trademark law on 2019-06-17: * **Before 2019-06-17:** Initial term of 15 years, renewal every 15 years * **On or after 2019-06-17:** Initial term of 10 years, renewal every 10 years Signa applies the correct term based on the mark's registration date. **Madrid designations of Canada are a different rule.** Trademarks Regulations s. 143 states that "Section 46 of the Act does not apply in respect of a Protocol registration", and s. 144(1) gives such a registration no Canadian term at all — it is on the register "for the period that begins on the day of the registration and that ends at the moment of its cancellation or expungement". So a designation of Canada does not renew at CIPO. What falls due is the renewal of the **international registration**, at WIPO, and Signa serves it on the Canadian record as `ca_renewal_madrid`: | Field | Value | | --------------------- | ------------------------------------------------------------------ | | Trigger | `intl_registration_date` | | Due | 10 years after the international registration, then every 10 years | | Window opens | 6 months before (Madrid Regulations Rule 30(1)(b)) | | Grace | 6 months, with a surcharge (Madrid Protocol Art. 7(4)) | | Consequence if missed | `lapse_of_international_registration` | | Holiday calendar | `wipo_geneva` — the International Bureau's, under Rule 4(4) | These are the same dates `wipo_international_renewal` computes on the WIPO record for the same international registration; the rule exists so a portfolio holding the Canadian designation, and not its WIPO parent, still sees the date that governs. Note that the roll differs from the Canadian route: a CA **domestic** renewal date is never rolled (Canada's own config does not opt into the weekend/holiday roll, and reports `not_checked`), while this one is, because the period belongs to the International Bureau. ### Nordic Countries: Trigger Field Change Sweden and Denmark changed their trigger field from `registration_date` to `filing_date` on 2019-01-01, Finland on 2019-05-01. Norway made the same change earlier, on 2010-07-01. Iceland followed on 2020-09-01. The date Signa compares against the change date is not the same in every country. Signa resolves the correct trigger from the **application** date for Sweden, Finland, Norway and Iceland, and from the **registration** date for Denmark. Iceland's pivot turns on the **application** date, not the registration date: applications received before 2020-09-01 keep a 10-year term measured from the registration date for **every** renewal, even where registration was granted after the reform, while applications received on or after that day take the 10-year term from the filing date (Act No. 71/2020 art. 39(3); Regulation No. 850/2020 art. 45(1)-(2)). For a pre-2020 Icelandic record that carries neither a registration date nor an office-reported expiry, there is no anchor to compute from, and Signa returns no renewal deadline rather than one measured from the wrong field. ### Mexico: Post-Anniversary Windows Mexico has a unique Declaration of Use requirement: * **Due:** 3 years after grant * **Window opens:** The day **after** the 3rd anniversary (not before) * **Window duration:** 3 months This differs from most jurisdictions where the filing window opens before the due date. ### Philippines: Mixed Trigger DAUs The Philippines requires Declarations of Actual Use at years 3 and 5 after filing, each with different extension rules. The year-3 DAU has a 6-month extension option. ### China (CNIPA): The Term Counts Its First Day Chinese registration terms run from the day the office approves the registration and **count that day**, so the term ends the day BEFORE the anniversary (Implementing Regulations art. 12(2)). A mark registered 2016-03-07 expires 2026-03-06, and its 12-month renewal window and 6-month grace period are measured from that expiry. Where the anniversary day does not exist in the target month the expiry is the last day of that month instead: a mark registered 2020-02-29 expires 2030-02-28, not 2030-02-27. Chinese deadlines roll to the next working day when they fall on a statutory holiday, and Signa builds that calendar from the State Council's annual holiday notice, including the **make-up working weekends** (调休), where a Saturday or Sunday is declared a working day to lengthen a holiday block. A renewal due on Friday 2 January 2026 is therefore due on Sunday 4 January 2026, the first working day. Because each year's notice is only published the preceding November, the calendar covers 2024–2026 today; deadlines beyond it are reported unrolled (`due_date_adjustment: "not_checked"`), and so are opposition windows closing beyond it, published with `close_adjustment: "not_checked"` rather than guessed or withheld. An un-rolled date is never later than the true one, and silence on a rights-losing deadline is worse than an early date. The one case Signa still declines with `window_not_computable` is a close that falls in the 20 days **before** the date you are computing as of, where a rollover could still be holding the window open. A close on the as-of date itself, or more than 20 days before it, is published un-rolled like any other. The same applies to every calendar Signa builds from annual office notices: `cgpdtm_india` (one DoPT memorandum per year), `wipo_geneva`, `ph_philippines` and `mx_mexico` (IMPI's yearly días inhábiles acuerdos). Check `close_adjustment` before treating a close date as verified. Two Chinese specifics on the opposition side. The 2026 revision of the Trademark Law cuts the opposition window from three months to two on 1 January 2027 (art. 36, in force by art. 87) and says nothing about a period still running on that day. Signa serves the **shorter** window to that straddling cohort, every publication from 1 October 2026, the first whose three months would still be open on commencement, because a close published too early costs a would-be opponent nothing they can lose by relying on it, while one published too late costs the right itself. A mark published 31 December 2026 is therefore reported as closing 28 February 2027, not 31 March 2027. This is a deliberate, reversible call: it flips back the day a transitional notice or the revised Implementing Regulations settles the question. ### EU, UK, Singapore, Iceland, Japan, and India: Restoration Periods Six jurisdictions offer an additional restoration window after the grace period, during which a lapsed registration can be re-established. Missing it is a permanent loss in all six. | Code | Window | Runs from | | ---- | -------- | ------------ | | EU | 6 months | Grace expiry | | GB | 6 months | Removal date | | SG | 6 months | Removal date | | IS | 2 months | Grace expiry | | JP | 6 months | Grace expiry | | IN | 6 months | Grace expiry | The EU window is restitutio in integrum (Article 104 EUTMR). Its end date is the outer limit only: the request must also be filed within two months of the removal of the obstacle, it is granted only where all due care required by the circumstances was taken, and it carries a fee. ## Accuracy limits Signa's default is to fail in the direction that cannot cost you a right: where a source runs out, a deadline is reported unadjusted or not at all rather than guessed, and an unadjusted date is EARLIER than the true one, so acting on it is still safe. The list below is the exception set: the places where a served date can be LATER than the true one. Later is the unsafe direction, because a filing made on the served date can land after the real deadline has passed. Each is bounded, each rests on a named gap in the primary sources, and each is listed here rather than left for you to discover. | Where | The served date can be later because | Bound | | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | **CN renewals and grace expiries falling on a weekend** | Implementing Regulations art. 12(1) verifies only the roll off a statutory holiday (节假日). CNIPA is shut at weekends, but no instrument Signa has pinned says a weekend is a 节假日 for that purpose, so the plain weekend roll is the engine's general assumption rather than a verified Chinese reading. If CNIPA does not roll weekends, the true date is the weekend day itself. | Only dates that land on a Saturday or Sunday, and only within the pinned calendar years. | | **IS renewals rolling off 24 or 31 December** | L. nr. 88/1971 6. gr. and L. nr. 32/1997 2. gr. make aðfangadagur and gamlársdagur frídagar only from 13.00, so the statute alone gives a half day. Signa treats both as fully closed on the strength of Hugverkastofan's own December 2020 opening-hours statement. If the office's practice has changed since, the true deadline is the half day itself. | Only the two dates, and only where a deadline actually lands on one. | | **BX oppositions on marks published 1 August to 30 September 2013** | The Protocol of 22 July 2010 moved BVIE art. 2.14(1) from "the first day of the month following publication" to "publication", in force 1 October 2013. For that two-month cohort the old text's window was still running when the new one came into force, and no instrument says which governs. Signa keeps the publication-date pivot, which serves the old text and therefore the later close. | A closed historical cohort: publications between 2013-08-01 and 2013-09-30. | | **CN Madrid oppositions closing on or after 1 January 2027** | 商标法 (2026 修订) cuts the NATIONAL opposition window to two months from that day, but the Madrid window lives in 实施条例 第四十五条, which the statute delegates to the State Council and which is unrevised as of 3 September 2026. If the revised Regulations cut it too and Signa has not yet landed the pivot, `cn_opposition_madrid` publishes a close one month late. | Madrid designations only, and only from 2027-01-01. Watch this space: the item is on Signa's standing source-invalidation checklist. | | **CA opposition `common_extension` for windows closing before 17 June 2019** | The 4-month benchmark first extension is verified for the Trademarks Opposition Board notice in force from 17 June 2019. The benchmark under the 2009 notice it replaced is not pinned, so an earlier window is served the 4-month figure with notes saying so, and the true figure may have been shorter. | Advisory only. `common_extension` is guidance about what the Board grants; it never moves `window_opens`, `window_closes` or `status`. | Everything else that is uncertain is reported the other way. A deadline outside a calendar's pinned coverage reports `due_date_adjustment: "not_checked"` and keeps the unrolled, earlier date; an opposition window Signa cannot compute is returned `supported: false` with `unsupported_reason` rather than a guess; and a record with no usable anchor returns no deadline at all. See [Business-day adjustment](/api-reference/reference/compute-deadlines#business-day-adjustment) for how the adjustment fields say which of these happened. ## Covered Jurisdictions | Code | Office | Renewal Cycle | Trigger | Proof of Use | | ---- | ---------------- | ------------- | ------------------------- | ----------------------- | | US | USPTO (domestic) | 10 years | registration\_date | Yes (Sections 8, 8+9) | | US | USPTO (Madrid) | 10 years | protection\_grant\_date | Yes (Section 71) | | EU | EUIPO | 10 years | filing\_date | No | | DE | DPMA | 10 years | filing\_date | No | | GB | UKIPO | 10 years | filing\_date | No | | CH | IGE/IPI | 10 years | filing\_date | No | | FR | INPI | 10 years | filing\_date | No | | BX | BOIP | 10 years | filing\_date | No | | SE | PRV | 10 years | filing\_date\* | No | | CA | CIPO (domestic) | 10 years | registration\_date | No | | CA | CIPO (Madrid) | 10 years | intl\_registration\_date‡ | No | | AU | IP Australia | 10 years | filing\_date | No | | NO | NIPO | 10 years | filing\_date\* | No | | DK | DKPTO | 10 years | filing\_date\* | No | | FI | PRH | 10 years | filing\_date\* | No | | IS | ISIPO | 10 years | filing\_date\* | No | | PL | UPRP | 10 years | filing\_date | No | | TH | DIP | 10 years | filing\_date | No | | VN | IP Vietnam | 10 years | filing\_date | No | | PH | IPOPHL | 10 years | registration\_date | Yes (DAU at years 3, 5) | | BR | INPI Brazil | 10 years | grant\_date | No | | MX | IMPI | 10 years | grant\_date | Yes (DU at year 3) | | SG | IPOS | 10 years | filing\_date | No | | JP | JPO | 10 years | registration\_date | No | | IN | CGPDTM | 10 years | filing\_date | No | | TR | TURKPATENT | 10 years | filing\_date | No | | KR | KIPO | 10 years | registration\_date | No | | CN | CNIPA | 10 years | registration\_date† | No | | WIPO | WIPO | 10 years | intl\_registration\_date | No | ‡ A Madrid designation of Canada has no Canadian term: Trademarks Regulations s. 143 disapplies Act s. 46 to a Protocol registration, so the renewal that falls due is the international registration's, at WIPO. See "Canada (CIPO): Pre-2019 15-Year Terms" above. † China counts the registration day itself, so a CN registration expires the day BEFORE the anniversary (registered 2016-03-07 → expires 2026-03-06). See "China (CNIPA): The Term Counts Its First Day" above. \* Changed from `registration_date`: SE and DK on 2019-01-01, FI on 2019-05-01, NO on 2010-07-01, IS on 2020-09-01. Marks that fall before the change date keep `registration_date` for every renewal, but the date compared against the boundary differs by country: it is the **application** date for SE, FI, NO and IS, and the **registration** date for DK. Iceland's boundary is keyed on the application date (Act No. 71/2020 art. 39(3); Regulation No. 850/2020 art. 45(1)-(2)). ## Opposition windows are a separate engine Everything above is the maintenance engine: renewals, declarations of use, grace and restoration. Opposition windows run on their own rule corpus, exposed through [`POST /v1/oppositions/compute`](/api-reference/reference/compute-oppositions) and [`GET /v1/opposition-rules`](/api-reference/reference/opposition-rules). One difference matters when you compute a window for a Madrid designation rather than a national filing. A designation has two publications that can fill one date field, WIPO's Gazette of International Marks and the designated office's own journal, and different offices run their clock from different ones: FR, CH and DE from the WIPO Gazette; GB, FI, PL, NO and the rest from the office journal. Send the optional `publication_date_kind` on your compute item and a rule that needs the other publication returns `status: "unknown"` with null dates rather than a wrong statutory bar. GB has no Madrid rule yet, because the current UK Journal source is not available to pin. The per-office table is in [Opposition windows](/guides/monitoring/opposition-windows#madrid-designations-which-publication-starts-the-clock). ## Filtering by Deadlines Use `renewal_due_date` filters to find marks with an upcoming renewal deadline. `renewal_due_before` is shorthand for `renewal_due_date_lt`: ```bash cURL theme={null} # All marks with a renewal deadline before a computed cutoff date # (e.g. 90 days from today: $(date -v+90d +%Y-%m-%d) on macOS, # $(date -d '+90 days' +%Y-%m-%d) on Linux) curl -s "https://api.signa.so/v1/trademarks?renewal_due_before=" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} // Compute the cutoff instead of hardcoding a date const in90Days = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) .toISOString() .slice(0, 10); const marks = await signa.trademarks.list({ renewal_due_before: in90Days, sort: "renewal_due_date", }); ``` You can also use the full `renewal_due_date` range for renewal-specific views: ```bash cURL theme={null} curl -s "https://api.signa.so/v1/trademarks?renewal_due_date_gte=2026-01-01&renewal_due_date_lt=2027-01-01" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} const marks = await signa.trademarks.list({ renewal_due_date_gte: "2026-01-01", renewal_due_date_lt: "2027-01-01", }); ``` # Calling the API from Edge Runtimes Source: https://docs.signa.so/guides/edge-runtimes Use the Signa API from Cloudflare Workers, Deno Deploy, and Vercel Edge Functions The Signa API works from edge runtimes such as Cloudflare Workers, Deno Deploy, and Vercel Edge Functions with no special configuration. This page covers the few things worth knowing. ## User-Agent is optional Some edge runtimes send requests without a `User-Agent` header, and some strip or normalize it. Since 2026-08-27, the API accepts requests with a missing or empty `User-Agent`: they are counted for observability but never blocked solely because the `User-Agent` is missing or empty. We still recommend setting one that identifies your integration, in the `vendor-product/version` style: ``` User-Agent: brandx-labs-core/1.0 ``` A stable, recognizable value lets support trace your traffic quickly when you report an issue. The [TypeScript SDK](/sdk/typescript) sets an identifying `User-Agent` automatically, so if you use the SDK there is nothing to do. ## Keep API keys in the platform secret store Edge bundles are deployed artifacts. Never embed `sig_` keys in bundled code or public environment configuration; put them in the platform's secret store and read them at runtime: * **Cloudflare Workers:** `wrangler secret put SIGNA_API_KEY`, then read `env.SIGNA_API_KEY` * **Deno Deploy:** project environment variables, read via `Deno.env.get("SIGNA_API_KEY")` * **Vercel Edge Functions:** encrypted environment variables, read via `process.env.SIGNA_API_KEY` ## Errors are JSON, including edge-generated ones Nearly every error response follows the standard [error envelope](/api-reference/errors), including responses generated at the edge before your request reaches the API (security blocks, oversized payloads, rate limits). A few narrow exceptions return HTML instead: oversized URIs (414), and oversized or malformed request headers (400 from the load balancer, or 494 when they exceed the CDN's larger header cap), documented in the [error catalog](/api-reference/errors#edge-generated-errors). So check the `Content-Type` header before parsing: ```typescript theme={null} const res = await fetch("https://api.signa.so/v1/trademarks?q=acme&limit=5", { headers: { Authorization: `Bearer ${env.SIGNA_API_KEY}`, "User-Agent": "brandx-labs-core/1.0", }, }); if (!res.ok) { if (res.headers.get("content-type")?.includes("application/json")) { const { error, request_id } = await res.json(); console.error(`Signa API error ${error.status} (${error.type}): ${error.detail} [${request_id}]`); } else { // 414/400/494 rejected before the API; correlate via x-amz-cf-id console.error(`Edge error ${res.status} [${res.headers.get("x-amz-cf-id")}]`); } } ``` # Entities & owners Source: https://docs.signa.so/guides/entities Owners, resolved entities, and how Signa links company records across offices An **owner** is one office's record of an applicant or registrant, exactly as that office filed it. An **entity** is the same company's identity across every office: it links together the owner records that belong to it. ## Party types | Type | Prefix | What it is | | ------------ | ------- | ------------------------------------------------------------------------------------ | | **Entity** | `ent_` | The cross-office identity linking the owner records that belong to the same company. | | **Owner** | `own_` | A single office's applicant/registrant record, as that office filed it. | | **Attorney** | `att_` | Named attorney of record on a trademark. | | **Firm** | `firm_` | Law firm associated with the attorney. | Attorneys and firms appear on the trademark records they're associated with. Owners and entities each have their own API surface, described below. ## Owner or entity? Use **owners** when you need the exact per-office record: the name as filed, the office, the address on file. Use **entities** for company-level questions, such as "everything this company owns, everywhere." ```bash theme={null} GET /v1/owners/own_abc123 GET /v1/entities/ent_R3jK9mN2 ``` Every owner also has an entity view, even before it has been linked to anything else: if it isn't linked yet, its entity has exactly one member (itself). Once it is linked, the same request returns the combined entity instead, so you can always traverse from a mark to its company-level identity without checking whether a link exists first. ## Endpoints | Endpoint | Purpose | | ------------------------------------------------------------------------------ | ----------------------------------------- | | [`GET /v1/entities`](/api-reference/parties/search-entities) | Search and filter resolved entities | | [`GET /v1/entities/{id}`](/api-reference/parties/retrieve-entity) | One entity with members and link evidence | | [`GET /v1/entities/{id}/trademarks`](/api-reference/parties/entity-trademarks) | Global portfolio across all members | | [`GET /v1/entities/{id}/family`](/api-reference/parties/entity-family) | GLEIF corporate parent and subsidiaries | | [`GET /v1/owners`](/api-reference/parties/search-owners) | Search and filter owners | | [`GET /v1/owners/{id}`](/api-reference/parties/retrieve-owner) | A single per-office owner record | ## How linking works The same company often files under a different legal entity, a different language, or a different level of detail in each office. For example, Procter & Gamble's US, German, and Swiss records share almost no text a name match could use, but a shared Madrid registration number ties them together into one entity. Signa **links owner records, it never merges them.** The per-office data is never rewritten, so `own_` IDs stay stable even as new links are added later. Two owners are linked only on specific, verifiable evidence: a shared applicant identifier, a shared Madrid international registration, or a shared public company. A name match alone is never enough, since two unrelated companies can share an identical name. Each member on a resolved entity carries how it was linked: ```bash theme={null} curl -s "https://api.signa.so/v1/entities/ent_R3jK9mN2" \ -H "Authorization: Bearer sig_YOUR_KEY" | jq '.members[].link' ``` ```typescript TypeScript theme={null} import { Signa } from "@signa-so/sdk"; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const entity = await signa.entities.retrieve("ent_R3jK9mN2"); console.log(entity.name, entity.member_count, entity.tickers); ``` Each link exposes a `method` (`shared_identifier`, `international_registration`, `public_company`, `portfolio_overlap`, or `manual_review`), a coarse `match_strength` band, and whether it was `reviewed`. Only entities can be superseded, never owners: if two resolved entities are later found to be the same company and combined, requesting the old `ent_` ID returns `410 Gone` with the successor ID in `merged_into` so you can update your reference. ## Public-company enrichment Owners and entities can be linked to public-company facts from two sources: | Source | Coverage | Identifier | | ------------- | ------------------------------- | ----------------------- | | **SEC EDGAR** | \~10,000 US-listed companies | Ticker (CIK internally) | | **GLEIF** | \~2.5M legal entities worldwide | LEI | These facts are aggregated onto the entity, so it carries a company's ticker even when only one member owner was matched. Filter either surface by them: | Filter | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `ticker=AAPL` | Linked to this stock ticker. Subsidiary-inclusive: `ticker=NKE` returns Nike and its subsidiary entities | | `lei=HWUPKR0…` | Linked to this LEI | | `publicly_traded=true` | Publicly listed: a confirmed active SEC ticker match, or the entity is itself listed or a subsidiary of a listed company | | `has_lei=true` | Has a confirmed GLEIF LEI match | An entity that is a subsidiary of a listed company carries the inherited ticker on its `tickers` array, with the direct-vs-inherited provenance broken out in the entity's `listing` block (`status` is `listed` or `subsidiary_of_listed`, `source` is `direct` or `inherited`). See [Track any public company's trademarks](/guides/use-cases/public-company-trademarks) for the full recipe. ```bash theme={null} GET /v1/entities?publicly_traded=true&country_code=US&sort=-trademark_count ``` Owner-to-public-company matching is confirmed-positive only: a `false` or absent value means no match was found, not that the company is confirmed private. ## Corporate families Using GLEIF Level 2 relationship data, Signa connects an entity to its direct corporate parent and subsidiaries: ```bash theme={null} GET /v1/entities/ent_R3jK9mN2/family ``` ```typescript TypeScript theme={null} const family = await signa.entities.family("ent_R3jK9mN2"); console.log(family.parent?.name, family.children.length); ``` GLEIF Level 2 covers LEI-reporting companies only. An absent edge does not imply the absence of a corporate relationship; see `coverage_caveat` on the response. ## FAQ You requested an owner's single-member entity view before it was linked to anything else. Signa transparently resolves it to the real entity once a link exists and returns that `id`. This is expected: store the returned `id`. No. Owners are linked, not merged, so `own_` IDs are stable. Only entities are ever combined; when that happens the old `ent_` ID returns `410 Gone` with a pointer to its successor in `merged_into`. # Filing Routes & Mark Types Source: https://docs.signa.so/guides/filing-routes How trademarks are filed, classified, and how these dimensions affect search and filtering Every trademark in Signa carries three classification dimensions: how it was filed (filing route), what type of mark it is (mark feature type), and what legal category it belongs to (legal category). Understanding these dimensions is essential for building accurate search queries and interpreting results. ## Filing Routes The filing route describes the procedural path through which a trademark application was submitted. | Route | Value | Description | | ---------------------- | -------------------- | ------------------------------------------------------------------------------ | | **Direct national** | `direct_national` | Filed directly at a national office (e.g., USPTO, CIPO) | | **Direct regional** | `direct_regional` | Filed directly at a regional office (e.g., EUIPO for the EU, BOIP for Benelux) | | **Madrid IR** | `madrid_ir` | The international registration record itself (the grouped IR-family row) | | **Madrid designation** | `madrid_designation` | A country designation under a Madrid Protocol international registration | | **Transformation** | `transformation` | A national application created from a cancelled Madrid designation | | **Divisional** | `divisional` | Split from a parent application (e.g., to separate contested classes) | | **Unknown** | `unknown` | Route could not be determined from source data | ### Scope Kind Related to filing route, the `scope_kind` field classifies the territorial scope: | Scope | Description | Typical Routes | | ---------------------------- | --------------------------------- | --------------------------------------------------------- | | `national` | Protection in one country | `direct_national`, `madrid_designation`, `transformation` | | `regional` | Protection across a regional bloc | `direct_regional` | | `international_registration` | WIPO international register | `madrid_ir`, `madrid_designation` | ### Filtering by Route ```bash cURL theme={null} # All direct national filings curl -s "https://api.signa.so/v1/trademarks?filing_route=direct_national" \ -H "Authorization: Bearer sig_YOUR_KEY" # All Madrid marks curl -s "https://api.signa.so/v1/trademarks?is_madrid=true" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} // Direct national filings only const national = await signa.trademarks.list({ filing_route: "direct_national", }); // All Madrid marks const madrid = await signa.trademarks.list({ is_madrid: true, }); // Regional filings (EUIPO, BOIP) const regional = await signa.trademarks.list({ filing_route: "direct_regional", }); ``` Search aggregations include `filing_route` breakdowns: ```json theme={null} { "aggregations": { "filing_route": { "direct_national": 3800, "madrid_designation": 1547, "direct_regional": 647 } } } ``` ## Mark Feature Types (16 values) The mark feature type describes the visual or sensory form of the trademark. | Type | Value | Description | | -------------- | ------------ | -------------------------------------- | | **Word** | `word` | Text only, no stylization | | **Figurative** | `figurative` | Logo or design, no text | | **Combined** | `combined` | Text + design elements together | | **3D** | `three_d` | Three-dimensional shape | | **Color** | `color` | A specific color or color combination | | **Sound** | `sound` | Audio mark (jingle, chime) | | **Motion** | `motion` | Animated or moving mark | | **Hologram** | `hologram` | Holographic mark | | **Pattern** | `pattern` | Repeating pattern applied to a surface | | **Position** | `position` | Mark defined by its position on goods | | **Multimedia** | `multimedia` | Combination of image and sound | | **Scent** | `scent` | Olfactory mark (extremely rare) | | **Taste** | `taste` | Gustatory mark (extremely rare) | | **Texture** | `texture` | Tactile mark | | **Other** | `other` | Non-standard type | | **Unknown** | `unknown` | Type could not be determined | The vast majority of marks are `word`, `figurative`, or `combined`. Non-traditional marks (sound, scent, hologram, etc.) represent a small but growing percentage. ```bash cURL theme={null} # All figurative marks in the EU curl -s "https://api.signa.so/v1/trademarks?mark_feature_type=figurative&jurisdictions=EU" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} const logos = await signa.trademarks.list({ mark_feature_type: "figurative", jurisdictions: "EU", }); ``` ### Impact on Search The mark feature type affects how search works: * **Word marks** are matched by text similarity (phonetic, visual, conceptual) * **Figurative marks** may have `mark_text: null`: they can only be found via design codes (Vienna classification) or owner/class filters * **Combined marks** are searchable by both text and design codes ## Legal Categories (9 values) The legal category classifies the legal nature of the trademark right. | Category | Value | Description | | --------------------------- | ------------------------- | --------------------------------------------------------------- | | **Standard** | `standard` | Ordinary trademark identifying goods/services of one enterprise | | **Collective** | `collective` | Indicates membership in an association | | **Certification** | `certification` | Certifies characteristics (quality, origin, materials) | | **Collective membership** | `collective_membership` | Indicates membership without certifying quality | | **Defensive** | `defensive` | Registered to prevent others from using (AU-specific) | | **Guarantee** | `guarantee` | Guarantees quality standards (some jurisdictions) | | **Geographical indication** | `geographical_indication` | Identifies goods originating from a specific place | | **Other** | `other` | Non-standard category | | **Unknown** | `unknown` | Category could not be determined | The overwhelming majority of marks are `standard`. Collective and certification marks have special rules around who can use them and how they are enforced. ```bash cURL theme={null} # All certification marks curl -s "https://api.signa.so/v1/trademarks?mark_legal_category=certification" \ -H "Authorization: Bearer sig_YOUR_KEY" # Geographical indications curl -s "https://api.signa.so/v1/trademarks?mark_legal_category=geographical_indication" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} const certMarks = await signa.trademarks.list({ mark_legal_category: "certification", }); ``` ## Right Kind (5 values) Distinct from legal category, `right_kind` classifies the type of IP right: | Kind | Value | Description | | --------------------------- | ------------------------- | ----------------------------------------------- | | **Trademark** | `trademark` | Standard trademark | | **Trade name** | `trade_name` | Business name registration | | **Commercial notice** | `commercial_notice` | Commercial notice (some Latin American offices) | | **Geographical indication** | `geographical_indication` | GI registered as a standalone right | | **Other** | `other` | Non-standard right type | Most records are `trademark`. The `right_kind` field exists because some offices register trade names and GIs in the same database as trademarks. ## Combining Dimensions These classification fields work together for precise filtering. Some common combinations: | Use Case | Filters | | ----------------------------------- | ---------------------------------------------------------------------------- | | Word marks filed directly in the US | `mark_feature_type=word`, `filing_route=direct_national`, `jurisdictions=US` | | Madrid designations in the EU | `filing_route=madrid_designation`, `offices=EM` | | All certification marks globally | `mark_legal_category=certification` | | Figurative marks in Nice Class 25 | `mark_feature_type=figurative`, `nice_classes=25` | | GIs registered in the EU | `mark_legal_category=geographical_indication`, `jurisdictions=EU` | ```bash cURL theme={null} # Word marks filed directly in the US, Nice Class 9 curl -s "https://api.signa.so/v1/trademarks?\ mark_feature_type=word&\ filing_route=direct_national&\ jurisdictions=US&\ nice_classes=9" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} const results = await signa.trademarks.list({ mark_feature_type: "word", filing_route: "direct_national", jurisdictions: "US", nice_classes: 9, }); ``` ## Search Aggregations All three classification dimensions are available as search aggregations. Aggregations are `POST`-only, so use `signa.trademarks.search()`: ```bash cURL theme={null} curl -s -X POST https://api.signa.so/v1/trademarks \ -H "Authorization: Bearer sig_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "APPLE", "options": { "aggregations": ["mark_feature_type", "mark_legal_category", "filing_route", "right_kind", "scope_kind"] } }' ``` ```typescript TypeScript theme={null} const results = await signa.trademarks.search({ query: "APPLE", options: { aggregations: [ "mark_feature_type", "mark_legal_category", "filing_route", "right_kind", "scope_kind", ], }, }); console.log(results.aggregations.mark_feature_type); // { word: 3800, figurative: 1200, combined: 647 } ``` # Glossary Source: https://docs.signa.so/guides/glossary Trademark terminology and how it maps to the Signa API This glossary covers trademark concepts you will encounter when working with the Signa API. Each term includes a definition and a note on where it appears in API responses. ## Territory and scope **Two different axes that are easy to conflate.** A **jurisdiction** is a territory where a right has legal effect (France, the European Union). An **office** is the register a mark was filed with and is examined by (the USPTO, the EUIPO, WIPO). One office can register rights that take effect in many jurisdictions, and one jurisdiction can be protected through several offices. A EU trade mark is filed at **one** office (the EUIPO) but protects **27** jurisdictions. **In the API:** these are separate filters and are never interchangeable. `jurisdictions` selects rights that protect, or seek protection, in a territory (protection-scope by default, so `jurisdictions=FR` also returns EU trade marks). `offices` selects the register a mark was filed with. Jurisdiction codes are **uppercase** ISO/region codes (`US`, `EU`); office codes are WIPO **ST.3** codes (`US`, `EM` for the EUIPO), with legacy lowercase aliases (`uspto`, `eu`). See [Search Trademarks](/api-reference/trademarks/search-trademarks) for `territory_match`, which controls how far `jurisdictions` expands. **Every record in Signa is one of three territorial shapes, and this determines which jurisdictions it protects.** 1. **National right**: filed directly with a national office and protecting that one country (a USPTO registration protects the US, `jurisdiction_code: "US"`, `filing_route: "direct_national"`). 2. **Regional unitary right**: a single right filed with a regional office that protects every member of that region as one indivisible unit. The three modeled regions are the **EU trade mark** (EUIPO, `jurisdiction_code: "EU"`), the **Benelux** mark (`BX`, covering Belgium, the Netherlands, and Luxembourg), and the **OAPI** mark (`OA`, covering its African member states). `filing_route: "direct_regional"`. 3. **Madrid designation**: a per-country request for protection extended from a WIPO international registration to one designated territory, examined independently by that territory's office (`filing_route: "madrid_designation"`, the designated country as `jurisdiction_code`). One IR fans out into many designations. **In the API:** distinguish them with `filing_route`. Under protection-scope `jurisdictions` matching, a request for a country matches national rights, Madrid designations of that country, **and** any regional unitary right whose membership covers it. **A EU trade mark protects all 27 EU member states as a single unitary right, and nothing beyond them.** This is a common source of over-reach: "EU" is not "Europe". A EUTM does **not** extend to the wider EEA, and it does **not** cover Switzerland or Norway (not EU members). Since Brexit it does **not** cover the United Kingdom, holders instead received independent UK comparable marks, which are UKIPO rights in their own right. **In the API:** because of this, protection-scope `jurisdictions` expansion maps a EUTM onto its 27 member states but never onto `GB`, `CH`, or `NO`. A request for one of those territories will never pull in a EUTM on membership grounds. Seniority claims on a EUTM are metadata only and never widen its geographic scope (see the **Seniority** entry under International below). ## Filing **The unique identifier assigned by an office when a trademark application is filed.** Each office uses its own numbering scheme (e.g., USPTO serial numbers like `97123456`, EUIPO numbers like `018765432`). **In the API:** `application_number` on trademark objects. Requires `office_code` for uniqueness since different offices may reuse number formats. Use with `GET /v1/trademarks?application_number=97123456&office=US`. **The date the trademark application was officially received by the office.** This date establishes priority and is used to compute deadlines. In most jurisdictions, the filing date determines the start of the protection timeline. **In the API:** `filing_date` on all trademark tiers. Sortable via `?sort=-filing_date`. Filterable via `?filing_date_gte=2025-01-01`. **The legal basis on which the applicant claims the right to register.** In the US, common bases include "use in commerce" (Section 1(a)) and "intent to use" (Section 1(b)). Other bases involve foreign applications or registrations. **In the API:** `filing_bases[]` array in the detail tier. Each entry has a `basis_type` (one of `use_in_commerce`, `intent_to_use`, `foreign_application`, `foreign_registration`, `madrid_extension`, `no_basis`), optionally scoped to a specific Nice class. **How the trademark application reached the office.** A mark can be filed directly with a national office, through the Madrid Protocol for international protection, or through a regional system like the EUIPO. **In the API:** `filing_route` on summary and detail tiers. Values: `direct_national`, `direct_regional`, `madrid_designation`, `transformation`, `divisional`, `unknown`. Filterable via `?filing_route=madrid_designation`. Use `?is_madrid=true` as shorthand for all Madrid Protocol marks. **A US-specific filing basis where the applicant declares a bona fide intention to use the mark in commerce but has not yet done so.** ITU applications must eventually file a Statement of Use (or request extensions) before registration can issue. **In the API:** Appears as `basis_type: "intent_to_use"` in the `filing_bases[]` array on the detail tier. **A claim of earlier filing priority based on an application in another country.** Under the Paris Convention, an applicant who files in one member country has 6 months to file in other member countries and claim the original filing date as their priority date. **In the API:** `priority_claims[]` array in the detail tier. Each entry includes `priority_type` (e.g., `paris`), `country_code`, `application_number`, and `priority_date`. The convenience field `priority_date` on the detail tier shows the earliest priority date. ## Status **Signa's normalized lifecycle stage for a trademark.** Every office uses different status codes and labels. Signa maps them all to a consistent set of 18 stages that represent where a mark sits in its lifecycle. **In the API:** `status.stage` on all tiers. Stages: `filed`, `examining`, `pending_publication`, `published`, `opposition_period`, `pending_opposition`, `pending_cancellation`, `pending_issuance`, `registered`, `allowed`, `abandoned`, `withdrawn`, `surrendered`, `refused`, `cancelled`, `invalidated`, `expired`, `unknown`. Filterable via `?status_stage=registered,published`. **A higher-level classification of the mark's status.** While `stage` gives the specific lifecycle position, `primary` groups stages into broader categories. **In the API:** `status.primary` on summary and detail tiers. Values: `pending`, `active`, `inactive`, `unknown`. For example, `registered` stage maps to `active` primary. **A window after publication during which third parties can file an opposition to prevent registration.** The length varies by jurisdiction (e.g., 30 days in the US, 3 months in the EU). **In the API:** `status.stage: "opposition_period"` indicates the mark is currently in this window. The `publications[]` array on the detail tier includes `opposition_period_start` and `opposition_period_end` dates. **When a trademark application or registration is no longer being pursued.** This can happen because the applicant failed to respond to an office action, did not file required declarations, or voluntarily abandoned the application. **In the API:** `status.stage: "abandoned"`. The `status.reason` field may provide additional context when available from the office. ## Ownership **The entity that holds rights to the trademark.** This may be the original applicant, a subsequent assignee, or a successor entity. Signa resolves owners across offices so the same company filing in multiple jurisdictions appears as a single entity. **In the API:** `owners[]` array on every trademark, detail and summary tiers alike. The detail tier's owner has an `id` (e.g., `own_abc123`), `name`, `country_code`, `entity_type`, `role`, and `address` (postal address as reported by the office, or `null`); the summary tier's owner is a lighter projection (`id`, `name`, `country_code`, resolved `entity_id`). **The legal form of the trademark owner.** Common types include corporation, LLC, individual, partnership, government agency, and various foreign equivalents. **In the API:** `entity_type` on owner objects within the `owners[]` array. Signa maps 75+ office-specific entity codes to normalized values. **Signa's process of recognizing that the same real-world entity appears under different names or in different offices.** For example, "Apple Inc." in the US, "Apple Inc" in the EU, and "APPLE INC." in Canada are resolved to a single owner profile with unified statistics. **In the API:** Each resolved entity has a single `id`. The `GET /v1/owners/{id}` endpoint returns the canonical profile including `aliases[]` (alternative name forms) and aggregate statistics across all jurisdictions. **A parent company linked to a trademark owner through corporate ownership data.** Signa uses GLEIF Level 2 relationship data to connect subsidiaries to their parent companies. **In the API:** `GET /v1/owners/{id}/related` returns parent and subsidiary relationships. Each relationship includes `relationship_type` (e.g., `IS_DIRECTLY_CONSOLIDATED_BY`), `ownership_pct`, and `period_start`/`period_end`. ## International **An international treaty that allows trademark owners to seek protection in multiple countries through a single application filed with WIPO.** The applicant files one international registration (IR) and then "designates" individual member countries where protection is sought. **In the API:** Madrid marks have `filing_route: "madrid_designation"`. Use `?is_madrid=true` to filter for all Madrid Protocol marks. Madrid-specific fields are top-level on the detail tier: `ir_number`, `origin_office_code`, `filing_route`, `designation_date`, `dependency_period_end_date`, and `transformation_deadline_date`. There is no nested `madrid` object — the family view is the inline `coverage` block (per-territory rollup, present on IR parents, `null` on designation legs), and other mark-to-mark links are in `relationships[]`. **A request for trademark protection in a specific country under the Madrid Protocol.** Each designation is processed independently by the designated country's office, which can grant or refuse protection. **In the API:** Each designation creates a separate trademark record with `filing_route: "madrid_designation"` and the designated country as `jurisdiction_code`. The `ir_number` links all designations from the same international registration. Use `?ir_number=1234567` to find all designations from one IR. **The unique number assigned by WIPO to a Madrid Protocol international registration.** This number is globally unique and shared across all designations from the same filing. **In the API:** `ir_number` on summary and detail tiers. Filterable directly: `GET /v1/trademarks?ir_number=1234567`. Also accepted by the batch endpoint without requiring an `office` parameter (IR numbers are globally unique). **An EU-specific concept where the holder of an earlier national trademark registration can claim that registration's seniority when filing an EUTM.** If the EUTM is later cancelled, the holder retains the rights of the earlier national mark. **In the API:** Seniority claims appear in the `office_extensions` object on EUIPO detail-tier records when present. **The first 5 years after an international registration under the Madrid Protocol, during which the IR depends on the basic mark in the office of origin.** If the basic mark is cancelled during this period, the IR (and all its designations) may also be cancelled. **In the API:** `dependency_period_end_date` on the detail tier for Madrid marks. After this date, the IR stands independently. ## Procedural **A formal challenge filed by a third party against a published trademark application.** Oppositions are typically filed during the opposition period and argue that the applied-for mark should not be registered (e.g., due to likelihood of confusion with an earlier mark). **In the API:** `status.stage: "pending_opposition"` when an opposition has been filed. TTAB proceedings data is available via `GET /v1/trademarks/{id}/proceedings` for US marks. **A proceeding to remove an existing trademark registration.** Cancellation can be initiated by a third party (e.g., for non-use) or by the office. A cancelled mark loses its registration. **In the API:** `status.stage: "cancelled"` or `"pending_cancellation"`. Cancellation proceedings appear in the proceedings sub-resource. **A US-specific requirement to file a declaration that the registered mark is still in use in commerce.** Due between the 5th and 6th year after registration, and at each 10-year renewal. Failure to file results in cancellation. **In the API:** Appears in the `deadlines[]` array on the detail tier with `type: "declaration_of_use"`. Includes `due_date`, `grace_expiry`, `window_opens`, and `consequence_if_missed: "cancellation"`. **A US-specific filing that makes a trademark registration conclusive evidence of the owner's exclusive right to use the mark.** Available after 5 consecutive years of use following registration. Optional but strategically valuable. **In the API:** Appears in the `deadlines[]` array with `type: "declaration_of_incontestability"`. Note that `consequence_if_missed` is `"none"` since this is an optional filing. **The periodic maintenance filing required to keep a trademark registration alive.** Renewal periods vary by jurisdiction (e.g., 10 years in most countries, but initial terms and intervals differ). **In the API:** `renewal_due_date` is a first-class field on summary and detail tiers. The `deadlines[]` array provides full renewal detail including `grace_expiry` and `window_opens`. Filterable via `?renewal_due_date_gte=2026-01-01&renewal_due_date_lt=2027-01-01`. **An additional window after a deadline's due date during which the required action can still be taken, usually with a late fee.** Not all deadlines have grace periods, and the length varies by jurisdiction and deadline type. **In the API:** `grace_expiry` in the `deadlines[]` array. `null` when no grace period exists. The `status` field on each deadline can be `in_grace` when the mark is currently in a grace period. ## Classification **The international system for classifying goods and services for trademark registration purposes.** 45 classes total: classes 1-34 cover goods, classes 35-45 cover services. Nearly every trademark office worldwide uses the Nice system. **In the API:** `classifications[]` array on both summary and detail tiers. Each entry includes `nice_class` and `goods_services_text` on the summary tier, and adds `nice_edition`, `goods_services_language`, `status`, and `class_status_raw` on the detail tier. Filterable via `?nice_classes=9,42`. **An international system for classifying the figurative (design) elements of trademarks.** Uses a hierarchical code structure (e.g., `26.04.01` for "globe"). Used primarily for searching design marks. **In the API:** `design_codes[]` array on the detail tier. Each entry has `code_system: "vienna"`, `code_value`, and `edition`. Searchable via the search endpoint's filter parameters. **The visual form of the trademark.** Common types include word marks (text only), figurative marks (design/logo), combined marks (text + design), three-dimensional marks, sound marks, color marks, and more. **In the API:** `mark_feature_type` on summary and detail tiers. Values include `word`, `figurative`, `combined`, `three_d`, `color`, `sound`, `motion`, `hologram`, `pattern`, `position`, `multimedia`, `scent`, `taste`, `texture`, `other`, `unknown`. Filterable via `?mark_feature_type=word,combined`. **The legal type of trademark right.** Most marks are standard trademarks, but other categories include certification marks (certifying quality/origin), collective marks (used by members of a group), and geographical indications. **In the API:** `mark_legal_category` on summary and detail tiers. Values: `standard`, `collective`, `certification`, `collective_membership`, `defensive`, `guarantee`, `geographical_indication`, `other`, `unknown`. Filterable via `?mark_legal_category=certification`. **The broader IP right category.** While most records in Signa are trademarks, some offices also register service marks, trade names, and geographical indications. **In the API:** `right_kind` on summary and detail tiers. Values: `trademark`, `trade_name`, `commercial_notice`, `geographical_indication`, `other`. **The free-text description of the specific goods or services the mark covers within each Nice class.** This is the most granular description of what a trademark protects and is critical for clearance analysis. **In the API:** `goods_services_text` within each entry of the `classifications[]` array on the detail tier. Language indicated by `goods_services_language`. # Madrid Protocol Source: https://docs.signa.so/guides/madrid-protocol How Signa models international trademark registrations under the Madrid System The Madrid Protocol is an international treaty that allows trademark holders to seek protection in multiple countries through a single application filed with WIPO (the World Intellectual Property Organization). Signa models the Madrid System, including international registrations, designations, coverage status, and Madrid-specific deadlines. ## How the Madrid System Works 1. **Filing:** The applicant files an international application through their home office (the "office of origin") 2. **WIPO registration:** WIPO registers the mark in the International Register and publishes it in the Madrid Gazette 3. **Designation:** The applicant designates one or more member countries where they want protection 4. **National examination:** Each designated office examines the mark under its own law 5. **Protection or refusal:** Each office independently grants or refuses protection ## Data Model Signa models a Madrid IR as one **designation** record per designated country, plus the **IR record** itself (the international registration). Each designation is an independent trademark that can have its own status, deadlines, and events, because each designated office makes independent decisions about examination, opposition, and renewal. A mark can be protected in one country and refused in another. ### Designation | Field | Value | | ------------------- | ---------------------------------------------------------------------------- | | `filing_route` | `madrid_designation` | | `office_code` | `WO` (always: every WIPO-sourced record uses the WIPO office code) | | `jurisdiction_code` | The designated country (e.g., `GB`, `DE`): tracks where protection is sought | | `scope_kind` | `international_registration` | | `ir_number` | The WIPO registration number (links all designations from the same IR) | ### Querying an IR family `GET/POST /v1/trademarks` defaults to **grouped** result presentation (`international_registrations=grouped`): a Madrid IR family is returned as one row with a `coverage` rollup summarizing every designated territory's status. Pass `international_registrations=expanded` to get one row per designation instead. Either way, `GET /v1/trademarks?ir_number=...` returns the whole family, and `GET /v1/trademarks/{id}` on the IR parent returns the same rollup with the full per-territory breakdown inline (below). ## Querying Madrid Marks ### Find All Madrid Marks ```bash cURL theme={null} # All Madrid designations curl -s "https://api.signa.so/v1/trademarks?filing_route=madrid_designation" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} // All Madrid designations const madridMarks = await signa.trademarks.list({ filing_route: "madrid_designation", }); // Shorthand: the is_madrid filter const madridMarks2 = await signa.trademarks.list({ is_madrid: true, }); ``` ### Find by IR Number The `ir_number` is globally unique and does not require an `office` parameter: ```bash cURL theme={null} curl -s "https://api.signa.so/v1/trademarks?ir_number=1234567" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` ```typescript TypeScript theme={null} const marks = await signa.trademarks.list({ ir_number: "1234567", }); // Returns the IR family (grouped, by default) or every designation (expanded) ``` ### Coverage Territories Retrieving an IR parent returns every designated territory inline under `coverage`. There is no separate coverage fetch: `coverage.territories[]` carries the full per-territory breakdown, and `coverage.by_primary` / `coverage.by_stage` give you the rollup counts. `coverage` is `null` on an individual designation leg and on direct or regional marks, which cover exactly one territory. ```bash cURL theme={null} curl -s "https://api.signa.so/v1/trademarks/tm_abc123" \ -H "Authorization: Bearer sig_YOUR_KEY" | jq '.coverage.territories[]' ``` ```typescript TypeScript theme={null} const mark = await signa.trademarks.retrieve("tm_abc123"); for (const territory of mark.coverage?.territories ?? []) { console.log( territory.territory, territory.protection_status, territory.protection_date, territory.trademark_id, // the designation record covering this territory ); } ``` Each entry also carries `territory_name`, `designation_date`, `refusal_date`, `status_effective_date`, `is_subsequent_designation`, `designation_protocol`, and `source_records[]` (the WIPO/national reconciliation alternates for a dual-feed territory). Coverage status values: | Status | Meaning | | ----------------------- | --------------------------------------------- | | `designated` | Designation filed, examination pending | | `provisionally_refused` | Office issued a provisional refusal | | `finally_refused` | Protection definitively refused | | `protected` | Protection granted | | `withdrawn` | Designation withdrawn by the holder | | `renounced` | Holder renounced protection in this territory | | `ceased` | Protection ceased (e.g., central attack) | | `invalidated` | Protection invalidated | | `limited` | Goods/services limited in this territory | | `expired` | Protection expired (not renewed) | ## Madrid-Specific Dates Madrid marks carry additional date fields beyond the standard trademark dates: | Field | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `designation_date` | When the designation was filed at WIPO | | `protection_effective_date` | When protection took effect in the designated territory. Null until the office reports it. | | `dependency_period_end_date` | End of the 5-year dependency period tied to the base application. Currently null; derive it yourself as `intl_registration_date` + 5 years if you need it today. | | `transformation_deadline_date` | Deadline to transform a cancelled designation into a national application. Currently null; derive it yourself as the central attack date + 3 months if you need it today. | ### The Dependency Period For the first 5 years after international registration, a Madrid mark depends on its base application/registration at the office of origin. If the base mark is cancelled, refused, or surrendered during this period (a "central attack"), the international registration can be cancelled for all designated territories. ### Transformation If an international registration is cancelled due to a central attack, the holder can "transform" each designation into an independent national application within 3 months. ## Madrid-Specific Deadlines The deadline engine handles Madrid marks differently from domestic marks. Key differences: | Aspect | Domestic | Madrid | | ------------------- | ---------------------------- | ------------------------------------------------------------- | | **Renewal** | Filed at the national office | Filed at WIPO (centralized) | | **US proof of use** | Section 8 (domestic) | Section 71 (Madrid equivalent) | | **Trigger date** | Varies by jurisdiction | `protection_grant_date` (US), `intl_registration_date` (WIPO) | ### WIPO Renewal International registrations are renewed centrally at WIPO every 10 years from the international registration date. This covers all designated territories at once. | Field | Value | | --------------- | ------------------------------------- | | **Type** | `international_renewal` | | **Trigger** | `intl_registration_date` | | **Cycle** | Every 10 years | | **Consequence** | `lapse_of_international_registration` | ## Relationships `GET /v1/trademarks/{id}` returns mark-to-mark links inline as `relationships[]`: | Relationship Type | Meaning | | ----------------------------- | ------------------------------------------------------------------ | | `based_on_basic_application` | The IR is based on this national application | | `based_on_basic_registration` | The IR is based on this national registration | | `transformed_from` | This national mark was transformed from a cancelled IR designation | | `replaces_under_4bis` | Replacement under Madrid Protocol Article 4bis | The IR-to-designation link is **not** repeated here: it is `coverage.territories[].trademark_id` on the parent. `trademark_id` on a relationship is `null` when the counterpart is not a record Signa holds, which is the normal case for `based_on_basic_*` edges (the office reports them as a bare office code plus record number). Identify those by `target_office_code` and `target_record_id`. ```bash cURL theme={null} curl -s "https://api.signa.so/v1/trademarks/tm_abc123" \ -H "Authorization: Bearer sig_YOUR_KEY" | jq '.relationships[]' ``` ```typescript TypeScript theme={null} const mark = await signa.trademarks.retrieve("tm_abc123"); for (const rel of mark.relationships) { console.log(rel.type, rel.direction, rel.trademark_id ?? `${rel.target_office_code}:${rel.target_record_id}`); } ``` # Handling alerts Source: https://docs.signa.so/guides/monitoring/alert-handling Pull, push, deduplication, and reconciliation You have three ways to receive alerts, in increasing operational complexity: 1. **Polling.** A scheduled job calls [`GET /v1/alerts`](/api-reference/monitoring/alerts/list) and walks pages until it sees an `id` you've already stored. 2. **Webhook.** Register an endpoint via [`POST /v1/webhooks`](/api-reference/monitoring/webhooks/create) and let Signa push alerts to you. 3. **Webhook + reconciliation.** Push for low latency, then periodically call [`POST /v1/alerts/lookup`](/api-reference/monitoring/alerts/lookup) with the IDs you've persisted to confirm nothing was lost during a receiver outage. Pure polling is fine for low-volume internal tools. For production external workflows, prefer webhook + reconciliation. ## Deduplicate by event ID Alerts are immutable, but the same `alert.created` event can arrive multiple times. Use the stable `evt_*` value in the `webhook-id` header as your idempotency key; use `data.alert_id` for the alert resource. ```ts theme={null} const eventId = req.header('webhook-id')!; if (await alreadyProcessed(eventId)) { return res.sendStatus(200); // ack but skip } // Record processing and execute the side effect in the same transaction // so a crash between them leaves the alert unprocessed (and retriable), // not silently "done". await db.transaction(async (tx) => { await markProcessed(tx, eventId); await handle(tx, JSON.parse(req.body)); }); return res.sendStatus(200); ``` If your side effect cannot share a transaction with your dedup store (for example, a third-party API call), use a two-stage `processing -> done` state and only flip to `done` after the side effect returns success. ## Polling pattern ```ts theme={null} let cursor: string | undefined; while (true) { const page = await signa.alerts.list({ limit: 100, cursor }); for (const alert of page.data) { if (await alreadyProcessed(alert.id)) { // Caught up, older alerts have already been processed. return; } await markProcessed(alert.id); await handle(alert); } if (!page.has_more) return; cursor = page.pagination.cursor!; } ``` The `GET /v1/alerts` endpoint accepts: | Parameter | Type | Notes | | ------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `limit` | number | 1-100, default 20. | | `cursor` | string | Opaque cursor from the previous response. | | `severity` | `normal` \| `high` \| `critical` | Filter by severity. | | `event_type` | string | One of the five alert event types: `trademark.created`, `trademark.updated`, `trademark.status_changed`, `trademark.retracted`, `trademark.corrected`. | | `epoch` | `all` \| `current` | `all` (default) returns alerts across every version of the watch's query. Set to `current` to keep only alerts matching the watch's present query. | ## Reconciliation pattern For defense-in-depth on top of webhooks, run a periodic job that asks Signa "did you ever fire these IDs?", useful when you suspect a webhook outage. ```ts theme={null} const recentIds = await myStore.alertsLast48h(); const confirmed = await signa.alerts.lookup(recentIds); const seen = new Set(confirmed.map((a) => a.id)); for (const id of recentIds) { if (!seen.has(id)) reportPossibleLoss(id); } ``` `alerts.lookup()` takes an array of 1-100 prefixed alert IDs (`alt_*`) per call: ```ts theme={null} const alerts = await signa.alerts.lookup([ 'alt_8kLm2nPq', 'alt_9pQs3vNk', 'alt_7hRt4xQw', ]); // alerts is an array of Alert objects, unknown or cross-org IDs are // silently omitted, so alerts.length <= ids.length ``` The endpoint is organization-scoped. Malformed IDs, anything that is not a well-formed `alt_*` ID, fail the whole request with `400 validation_error`, listing each bad entry by index. Well-formed but unknown IDs, and IDs that belong to another organization, are silently dropped from the result. Any gap between the IDs you sent and the alerts you got back is real. ## Reading `event.diff` `event.diff` is why the alert fired, one entry per changed field: ```json theme={null} "event": { "type": "trademark.status_changed", "summary": "Status changed: pending → registered", "diff": [ { "path": "status", "op": "set", "from": "pending", "to": "registered" }, { "path": "owners", "op": "changed" } ] } ``` `path` is a **public trademark field name** — the same name the field has on [`GET /v1/trademarks/{id}`](/api-reference/trademarks/retrieve-trademark), so you can always look it up. Changed child collections appear as the collection name (`owners`, `attorneys`, `classifications`, `media`) and are opaque: `op: "changed"`, no `from`/`to`. Parent fields carry real values. Status is flattened — `status.primary` is `status`, everything else is `status_` (`status_stage`, `status_effective_date`, …). Fields Signa tracks internally but does not publish are **omitted**, not renamed, so a diff can be shorter than the underlying change. Branch on `path` for routing (`status` to the docket, `owners` to conflicts review) and read `summary` for the human line. **Beta break, effective on deploy.** `path` previously carried Signa's internal column names (`status_primary`, `mark_text_primary`, `trademark_owners`, …). See the [changelog](/changelog) for the full rename table. Redeliveries and retries of older alerts are re-emitted with the public names. ## Severity-based routing A typical production setup runs two jobs on different cadences: | Cadence | Filter | What it does | | ------------ | ------------------- | ------------------------------------------------------- | | Every minute | `severity=critical` | Pages on-call. | | Hourly | (no filter) | Catches `normal` and `high` for the daily review queue. | ```ts theme={null} const critical = await signa.alerts.list({ severity: 'critical' }); for await (const a of critical) routeToOnCallAttorney(a); ``` ## Provenance: every alert's evidence chain Each alert carries a `provenance` block so you can trace it from the office publication to the moment Signa detected it, and see whether that detection was on time. ```json theme={null} "provenance": { "sync_run": "srun_01HK...", "content_version": 7, "detection_latency_seconds": 9840, "late_detection": false } ``` | Field | Meaning | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sync_run` | The ingestion run that detected this change, as a prefixed `srun_` ID. Cross-references `last_relevant_sync_run.sync_run` on the [watch diagnostics](/api-reference/monitoring/watches/diagnostics) endpoint. **Omitted** (not `null`) when the alert predates provenance tracking and has no derivable run. | | `content_version` | The record version the alert froze against. Same value used for alert deduplication. | | `detection_latency_seconds` | `created_at - occurred_at`: the gap between when the office published the change (`timestamps.occurred_at`) and when Signa detected it (`timestamps.created_at`). `null` when the source did not publish a timestamp we could anchor to, or published a timestamp later than our detection time. | | `late_detection` | `true` when `detection_latency_seconds` exceeded Signa's internal service-level target for that office. It is always `false` when the latency is unknown: Signa never flags lateness it cannot prove. | `late_detection` is a per-office judgement. Offices that publish daily have a tight target; offices that publish weekly or monthly have a proportionally looser one. A `false` flag means "detected within the window we hold ourselves to for that office", not "instant". Use `detection_latency_seconds` for your own SLA dashboards, and `late_detection` as a ready-made trigger for exception review when Signa was slower than intended. # Attestations: proof of monitoring Source: https://docs.signa.so/guides/monitoring/attestations The filable monthly proof-of-monitoring artifact. Fetch it, file the PDF, verify the JSON by hash, and read coverage gaps honestly. An attestation is a filable, monthly record that a watch was actually evaluated during a period, with the office-data horizon it covered and the outcome (including the negative outcome). It is the artifact a law firm keeps on file to show that a monitoring instruction was in force and was executed. ```bash theme={null} curl "https://api.signa.so/v1/watches/wat_.../attestation?period=2026-06" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` Requires an API key with the `portfolios:manage` scope. The period is a UTC calendar month. Omit `period` to get the previous (most recently closed) month. ## What it proves, and what it does not An attestation certifies that **evaluation occurred** against the stated data horizons: which query was in force, how many times it ran, the sync runs it consumed, and the office-data coverage timestamps it reached. It does **not** certify that every relevant mark was surfaced. Recall is bounded by connector coverage and the match strategy you configured. This is a deliberate bright line: the attestation is evidence of diligence and process, not a guarantee of exhaustive detection. The `statement` field states this in plain language, and the wording is subject to founder and counsel review. ## The "no alert" versus "not looking" model The whole point of an attestation is to make silence provable. If you received no alert for a period, the attestation shows whether that silence means "we looked and found nothing" or "we were not looking". * **We looked and found nothing.** `evaluations_count` is greater than zero, `changes_evaluated` shows the volume assessed, `match_count` is 0, and there are no gaps. That is a real negative result you can rely on. * **We were not looking.** The period carries a `gaps[]` entry (see below), the office is marked `no_evaluations` (a supported office with zero evaluations in the period, disclosed with a full-period gap and no coverage claims), or the office is marked `unsupported`. A single office sync run that was skipped because it was too large to evaluate is disclosed the same way, as a `budget_declined` gap naming that run. Silence never implies coverage that did not happen. If the watch is paused or disabled at the moment you fetch the artifact, the response carries `watch_status_at_generation` so the current state is on the record. Pause history within the period itself is not reconstructable in this version (there is no status audit trail yet); that is a documented limitation. ## Per-office fields Each entry in `offices[]` reports one office in the watch scope. | Field | Meaning | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `evaluated` (a supported, ingested office with real claims), `no_evaluations` (a supported office with zero evaluations in the period: zero counts, no coverage claims, and a full-period gap), or `unsupported` (see below). | | `evaluations_count` | Number of successful evaluations during the period. A failed evaluation is not counted as an evaluation. | | `changes_evaluated` | Changes evaluated against your query this period (candidacy volume). This is the number of candidate changes assessed, not the size of the whole office corpus. | | `match_count` | Candidate changes that matched your query. | | `alerts_emitted` | Alerts produced from those matches. | | `coverage_from` / `coverage_through` | The office-data timestamps the watch was current through at the start and end of the period. This is office-data time, not wall-clock time. | | `coverage_basis` | Which signal the coverage timestamp came from: `source_dates` (strongest), `date_range`, or `run_completed` (weakest). | | `gaps` | Disclosed intervals where coverage fell behind (see below). | ### `changes_evaluated` is candidacy volume `changes_evaluated` is the count of candidate changes assessed against your query, summed across the evaluations in the period. It is not a claim about how many records exist at the office. It tells you the volume of change the watch weighed before deciding there was nothing to alert on. ## Coverage gaps `gaps[]` is where honest degradation is disclosed. A gap is an interval in the period where the office-data coverage the watch had evaluated through fell behind the internal freshness target for that office. Gap disclosure is permanent: it stays in the artifact for that period even after coverage recovers, because it is a fact about what happened. ```json theme={null} "gaps": [ { "from": "2026-06-11T06:00:00.000Z", "through": "2026-06-20T06:00:00.000Z", "reason": "office_lagging", "resolved": true } ] ``` * `reason: "office_lagging"` means coverage was stale beyond the office target for that interval. * `reason: "evaluation_missing"` means no evaluation ran for that interval at all, although one should have: an eligible office sync run has no evaluation receipt for this watch (a pause, a credit lock, or lease starvation), or there is no evaluation evidence for longer than the continuity backstop. Silence is never read as coverage. * `reason: "budget_declined"` means a specific office sync run was **not evaluated at all**. A run carrying an abnormally large number of changes (a bulk reload, for example) exceeds the evaluator's per-run ceiling and is declined rather than partially processed. The gap names the run in `sync_run_id` and opens at the start of that run's own office-data window, which is where the un-evaluated changes are. Later coverage does not close it: a subsequent run's data horizon says nothing about the changes in the declined run. A decline that is still outstanding when a month ends is disclosed again in the **following** month's attestation, clipped to that month, until it is closed. * `resolved: true` means coverage caught back up within the period (for `office_lagging`), or an operator closed the decline within the period (for `budget_declined`). `resolved: false` means it was still outstanding at the period boundary. * `resolution` appears on a resolved `budget_declined` gap and says **how** it was closed: `redriven` (the run was re-evaluated with a raised budget, so the changes were eventually assessed) or `suppressed` (an operator recorded the deliberate decision that this run will never be evaluated). Those are not the same claim, so we never collapse them into a bare `resolved: true`. * A declined run is disclosed **once**: it carries the richer `budget_declined` gap and is never also reported as `evaluation_missing`, even though it is by construction an eligible run without a receipt. ```json theme={null} "gaps": [ { "from": "2026-06-14T00:00:00.000Z", "through": "2026-07-01T00:00:00.000Z", "reason": "budget_declined", "resolved": false, "sync_run_id": "srun_01k4v0y2..." } ] ``` `totals.declined_runs` and `totals.declined_runs_unresolved` count these across every office in scope, and the `statement` says so in prose, so a declined run can never be lost in a per-office detail block. A period's `coverage_through` timestamps are still exactly what the evaluations recorded; they simply do not attest to a declined window, which is what the gap makes explicit. What a closed month says about **evaluation** never changes retroactively: if a declined run is re-evaluated in August, the June and July artifacts still show the gap that was open at the time. August's artifact is where the resolution appears. (Re-fetching an old period reproduces the same `content_hash` as long as the watch configuration and the office metadata have not changed since; see the integrity section below.) Evaluations that failed outright do not write a receipt and have no ledger of their own, so they are not reconstructed as gaps. The `evaluation_failed` reason is reserved for a future version. Two special gap shapes to know: * A supported office with **zero evaluations** in the period is marked `no_evaluations` and carries a single gap spanning the whole period. That gap stays `resolved: false` permanently, because nothing that happens later can evaluate a month that has already passed. The same holds for a gap that runs to the end of a closed period on an office that *was* evaluated: the silence after its last evaluation is part of that month's record, and a later evaluation does not retroactively fill it. Whether evaluation resumed afterwards is reported separately, on every supported office, as `evaluation_resumed_after_period` — a fact about now rather than about the period, and therefore excluded from `content_hash`. * Evaluations recorded **before coverage snapshots existed** cannot state what office-data horizon they covered. They are disclosed as incomplete coverage evidence (a gap with a null `coverage_through`), never presented as covered. ## Unsupported offices If a watch is scoped to an office Signa does not currently ingest, that office appears with `status: "unsupported"` and no evaluation claims at all. Signa will never render a clean, zero-alert attestation for an office it does not cover, because that would read as "we looked and found nothing" when the truth is "we do not cover this office yet". If **every** office in the scope is unsupported, the endpoint returns `422 attestation_unsupported_scope` rather than an empty artifact. ## Configuration changes and the fingerprint The `query_fingerprint` is a stable hash of the watch query and trigger events. Its `fingerprint_basis` is `current_configuration`: it describes the configuration as it exists when you fetch the artifact, because a full evaluated-time configuration history is not stored yet. Two flags keep this honest: * `configuration_changed_in_period: true` means the configuration changed **during** the period (the receipts carry more than one evaluation epoch); the `epochs_in_period` array lists them. * `configuration_changed_since_period: true` means the configuration changed **after** the period closed. The fingerprint in the artifact then describes the current configuration, not the one that was in force during the period. Practical rule for firms: fetch and file the attestation promptly after the month closes, before making configuration changes. A filed artifact with both flags `false` pins the fingerprint to the period unambiguously. ## Reconciliation The artifact cross-checks the alert counts recorded on the evaluation receipts against the alert rows stored for the period, scoped to the same supported offices the artifact reports on. When they agree, `reconciliation` is `consistent`. When they disagree, it is `mismatch`, surfaced honestly rather than silently reconciled to one number. One caveat: alerts that cannot be attributed to an office (their ingestion-run reference is null, for example because the run record aged out) are included in the cross-check total. In rare cases this can produce a `mismatch` that reflects attribution loss rather than a missing or extra alert. ## Verifying integrity with `content_hash` The `content_hash` is a SHA-256 over the canonical, sorted-key form of the artifact, excluding the fields that describe the moment of generation rather than the period: `generated_at`, `request_id`, the hash itself, `watch_status_at_generation`, `watch.name` (display-only), `watch.configuration_changed_since_period`, and `offices[].evaluation_resumed_after_period`. The artifact is computed deterministically from stored evaluation records, and everything a closed period claims about evaluation is clipped to that period, so that part of the hash is fixed the moment the period ends. To verify a filed copy: re-fetch the JSON for the same period and compare the `content_hash` to the value on the copy you filed. A match proves the artifact was not altered after issue. One caveat to know when you compare: a few inputs are still read at generation time rather than snapshotted for the period. Those are the watch's current configuration (the fingerprint and trigger events), the offices currently in scope, and office names and support status. If any of them changes after you file, a later re-fetch produces a different hash even though the period's evaluation history is untouched. That is the disclosure working as intended, not tampering: the artifact also reports `configuration_changed_since_period: true`. Keep the filed copy as the record of what was issued. This proves the **integrity** of the artifact. It is not a third-party authenticity signature. Cryptographically signed and timestamped documents (for example RFC 3161 timestamps) are on the roadmap. ## Filing the PDF For a business record you can file, request the PDF: ```bash theme={null} curl "https://api.signa.so/v1/watches/wat_.../attestation?period=2026-06&format=pdf" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -o proof-of-monitoring-2026-06.pdf ``` You can also request it with `Accept: application/pdf`. The PDF is rendered from the same data as the JSON, so the numbers match. It includes the configuration, a per-office coverage table, the gaps, the totals, the statement, and the `content_hash` with instructions for verifying it against the JSON endpoint. A typical firm workflow: fetch the PDF once a month for the closed period, file it in the matter record, and store the JSON alongside it so an auditor can re-fetch and hash-verify later. ## Periods and errors * Periods are closed UTC calendar months. The default is the previous month. * The current, in-progress month returns `422 attestation_period_open`. Pass `partial=true` to get an interim artifact marked `"partial": true`. * A period beyond the 25-month retention window returns `410 attestation_expired`. The window is rolling (day-granular, matching the evidence retention clock): once any part of a month is past the horizon, the whole month is treated as expired. File monthly, within the window. * An unknown watch, or a watch belonging to another organization, returns `404`. # Monitoring status Source: https://docs.signa.so/guides/monitoring/monitoring-status One org-level view of watch health and pipeline freshness via GET /v1/monitoring/status `GET /v1/monitoring/status` returns a single snapshot of your whole monitoring program: how your watches are doing, how fresh each connected office is, and when the pipeline was last verified end to end. It is the API behind the dashboard Monitoring page. Scope: `portfolios:manage`. ## Response shape ```json theme={null} { "object": "monitoring_status", "watches": { "total": 412, "by_health": { "healthy": 401, "lagging": 6, "degraded": 2, "paused": 3, "pending": 0, "unsupported": 0 } }, "offices": [ { "office_code": "US", "pipeline_status": "ok", "coverage_through": "2026-07-06T23:59:59Z", "last_completed_sync_at": "2026-07-07T06:10:00Z", "last_search_indexed_at": "2026-07-07T06:24:00Z", "slo_hours": 48, "declined_runs_unresolved": 0 } ], "pipeline": { "last_verified_end_to_end_at": "2026-07-07T08:00:00Z", "last_real_record_alert_at": "2026-07-07T02:14:00Z", "verification": "canary" }, "request_id": "req_..." } ``` ## Blocks ### `watches` An aggregate over every one of your watches, counted by [health state](/guides/monitoring/watches). The `by_health` object always carries all six states, so a state with no watches reports `0` rather than being omitted. `total` is the count of your non-deleted watches. This block is scoped strictly to your organization. ### `offices[]` Per connected office, global pipeline freshness (the same data for every customer, so it is safe to cache and share): | Field | Meaning | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pipeline_status` | `ok` when coverage is within the office freshness target; `lagging` when it is stale or there is no coverage signal yet. | | `coverage_through` | The office-DATA time we have processed the office through. This is source data time, never wall-clock. | | `last_completed_sync_at` | When we last finished pulling from the office. | | `last_search_indexed_at` | When that pull became searchable. | | `slo_hours` | The internal freshness target (in hours) used to compute `pipeline_status`. This is an operational default, not a contractual SLA. | | `declined_runs_unresolved` | Sync runs for this office in the last 30 days that were too large to evaluate (they exceeded the evaluator's per-run change budget) and that no one has resolved since, either by re-evaluating them or by recording that they never will be. Ingestion still happened, so `coverage_through` can look healthy; a non-zero value means the changes in those runs were not evaluated against any watch. The affected windows appear as `budget_declined` gaps in the attestations for watches scoped to this office. | Only live offices appear here. ## Null-honest pipeline verification The `pipeline` block reports two independent signals, and both follow one rule: **a stale value is never presented as a fresh verification.** * `last_verified_end_to_end_at` -- a synthetic change is injected into the live pipeline on a short interval and checked end to end. This timestamp is the last time that check passed. It ages visibly while everything is healthy. If no recent check has passed (the key is absent, or the most recent one is older than our freshness horizon), this field is `null` rather than a misleadingly old timestamp. * `last_real_record_alert_at` -- a separate, slower signal: the last time the representative real-record path actually saw an office-originated change. The synthetic heartbeat proves the bus is alive right now; this proves the true customer path is delivering real changes. Same null rule. `null` on either field means "we cannot currently assert this was verified recently", not "verified at the epoch". Treat `null` as unknown, never as fresh. `verification` is always `"canary"` and names the mechanism behind the timestamps. ## What it does not tell you This endpoint certifies that evaluation is happening and reports how fresh each office is. It does not certify that every relevant mark was surfaced -- that is bounded by your search strategy and by office coverage. For per-watch evidence of evaluation over a period, use the [attestation endpoint](/guides/monitoring/attestations). ## Reference Field-by-field documentation, error codes and the interactive playground live on the [Monitoring Status](/api-reference/monitoring/watches/monitoring-status) reference page. # Opposition windows Source: https://docs.signa.so/guides/monitoring/opposition-windows Severity, deadlines, and the four window states attached to every alert When an alert involves a published mark in a [supported jurisdiction](#coverage), Signa attaches the opposition deadline and a window state so you can route on urgency. The state transitions automatically as the deadline approaches. ## The four window states | State | When | Severity | | -------------- | -------------------------------------------------------- | ---------- | | `open` | Window is open and the deadline is 14 days or more away. | `normal` | | `closing_soon` | Less than 14 days remaining. | `high` | | `critical` | Less than 7 days remaining. | `critical` | | `closed` | The window has elapsed. | `normal` | State transitions happen on every data update that touches the office. A mark published Friday afternoon and crossing into `critical` the following week requires no extra scheduling in your application. If the mark moves into a status that ends the window early (for example abandonment), `opposition_window_status` flips to `closed` on the next sync that observes the change. ## Severity routing `severity` is automatically escalated based on the window state, which makes it easy to page on-call only when a window is genuinely close to closing. ```ts theme={null} const critical = await signa.alerts.list({ severity: 'critical' }); for await (const a of critical) { if (a.opposition_window_status === 'critical') { notifyAttorney(a.trademark_id, a.must_act_by); } } ``` For the recommended polling cadence and routing pattern, see [Severity-based routing](/guides/monitoring/alert-handling#severity-based-routing). ## Worked example: USPTO publication A new mark publishes in the USPTO Trademark Official Gazette on **2026-04-15**. In the US, the standard opposition window is 30 days from publication. | Date | What happens | `opposition_window_status` | `severity` | `must_act_by` | | ---------- | -------------------------- | -------------------------- | ---------- | ------------- | | 2026-04-15 | Mark published. | `open` | `normal` | 2026-05-15 | | 2026-05-02 | Routine sync, nothing new. | `open` | `normal` | 2026-05-15 | | 2026-05-08 | \< 14 days remaining. | `closing_soon` | `high` | 2026-05-15 | | 2026-05-12 | \< 7 days remaining. | `critical` | `critical` | 2026-05-15 | | 2026-05-16 | Window elapsed. | `closed` | `normal` | 2026-05-15 | `must_act_by` is the latest moment to file, end of day in the office's local timezone, ISO 8601. ## Madrid designations: which publication starts the clock A Madrid designation has two publications that can fill one date field. WIPO publishes the international registration in its Gazette of International Marks, and the designated office republishes the designation in its own journal. The two are routinely months apart, and offices run their opposition clock from different ones. Feed a rule the other office's date and you get a plausible-looking date that is not the statutory bar. Each Madrid rule declares which publication it needs. `POST /v1/oppositions/compute` accepts an optional `publication_date_kind` on each item saying which one your date is: | Office | Trigger publication | `publication_date_kind` it needs | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | FR (INPI) | WIPO Gazette of International Marks. CPI art. R. 717-5 runs the two months straight from the Gazette publication; INPI republishes nothing. | `wipo_gazette` | | CH (IPI) | WIPO Gazette. MSchV art. 50 al. 1 anchors on the Gazette's issue month. | `wipo_gazette` | | GB (UKIPO) | UK Trade Marks Journal. Rule pending: the current Journal source is not yet available to us, so a UK Madrid designation returns no window. | `office_journal` | | FI (PRH) | PRH's own publication of the international registration, Tavaramerkkilaki 92 § 5 mom. | `office_journal` | | PL (UPRP) | The Biuletyn Urzędu Patentowego announcement of the designation, PWP art. 152^1a ust. 1. | `office_journal` | | NO (Patentstyret) | Norsk varemerketidende, varemerkeloven § 70 annet ledd. | `office_journal` | DE runs from the WIPO Gazette like FR and CH. AU, CA, EU, SG and US all run from their own journal like GB, FI, PL and NO. When the kind you send and the kind the rule needs disagree, the response is `status: "unknown"` with null dates, rather than a date you should not act on. The item stays `supported: true` and keeps its `rule_id`, `office_code`, `trigger_event` and citation, so you can see exactly which rule declined — and a `reason` of `publication_date_kind_mismatch` says why. A window that is date-less merely because there was no publication date leaves `reason` as `null`, so the two are distinguishable on the wire. Sending `unknown` is treated the same way. It is not a way of declining to answer: it is a positive statement that you looked and the source did not establish which publication the date is, and an adjudicated Madrid rule refuses an unestablished provenance exactly as it refuses a wrong one — same `status`, same `reason`. Omitting the field entirely is the only value-free option, and nothing changes. Every caller written before the field existed keeps its current behaviour. The same `reason` field, with the same vocabulary, is on `derived.opposition_window` in [`GET /v1/trademarks/{id}`](/api-reference/trademarks/retrieve-trademark), so a stored record that declines reports it too. ## Coverage Opposition rules are wired for 25 jurisdictions: AU, BR, BX, CA, CH, DE, DK, EU, FI, FR, GB, IN, IS, JP, KR, MX, NO, PH, PL, SE, SG, TH, TR, US, VN. Fifteen of them carry a separate rule for Madrid designations alongside the domestic one: AU, CA, CH, DE, EU, FI, FR, IN, JP, KR, NO, PL, SG, TR, US. The remaining ten carry a domestic rule only; a Madrid designation into one of those offices returns `opposition_window_status` and `must_act_by` as `null` until its Madrid rule ships. GB is the deliberate hold: the rule is not written rather than guessed, because the current UK Journal source is not available to pin. There is no standalone WIPO rule, because an opposition always runs in the designated office under that office's own rules. The full registry is available via [`GET /v1/opposition-rules`](/api-reference/reference/opposition-rules). For marks in jurisdictions without a wired rule, `opposition_window_status` and `must_act_by` are both `null`. ### When `must_act_by` is not calendar-verified `must_act_by` is the statutory close after the office's holiday calendar has been applied, except where Signa has no calendar data for that year. Several calendars are built from an annual office notice (China's State Council holiday notice, India's DoPT memorandum, the Philippine presidential proclamations, Mexico's IMPI acuerdos, the WIPO non-working-days notice), so a window closing in a year whose notice is not yet published is published **un-rolled**: raw statutory arithmetic, with no rollover applied. A rollover can only move a close **later**, so such a date is correct-or-early: safe to act on, never a reason to file late. Alert rows do not carry a marker for this. To check a specific one, read [`GET /v1/watches/{id}/diagnostics`](/api-reference/monitoring/watches/diagnostics), whose `opposition.close_adjustment` reports `moved`, `unchanged` or `not_checked` for the same window. The same field is on [`POST /v1/oppositions/compute`](/api-reference/reference/compute-oppositions) and on the `derived.opposition_window` block of a trademark. ### Search and compute can disagree for 20 days after an un-rolled close One deliberate divergence, worth knowing before you reconcile two surfaces against each other. A `not_checked` close is raw statutory arithmetic, and a rollover could only ever have pushed it **later**, so for the 20 days after it the true state is genuinely unknown. * **Search** treats those 20 days as **open**. `GET /v1/trademarks` and the search index bucket a mark whose `close_adjustment` is `not_checked` into the open-window filters until 20 days past the baked close, so a window that may still be live is not filtered away from an opposer. * **`POST /v1/oppositions/compute`** reports `status: "closed"` for the same window on the same day, because it answers strictly against the un-rolled date. Neither is wrong; they answer different questions, and the safe reading is search's. When the two disagree, check `close_adjustment` first: if it is `moved` or `unchanged` the close is calendar-verified and both surfaces agree. If it is `not_checked` and the close is within 20 days, treat the window as **possibly still open** and confirm with the office. # Monitoring overview Source: https://docs.signa.so/guides/monitoring/overview Watches detect trademark events you care about. Alerts and webhooks deliver them. Signa monitoring turns office filings into a feed you can act on. You define what to watch (a specific mark, an owner, a Nice class, a similarity query) and Signa delivers an alert whenever a trademark matches, including the opposition deadline where one applies. **Alerts typically arrive within a day of office publication.** Signa evaluates every watch against each office's daily data update, so a new filing usually alerts within about a day of the office publishing it, not within minutes of the filing itself. Every delivery is one alert at a time (there is no digest or batched mode yet); if you want your own batching, dedupe by the webhook `webhook-id` header and roll alerts up on your own schedule. ## The four building blocks | | What it is | When you touch it | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | **Watch** | A saved query. Signa evaluates it against every data update and produces alerts for matches. | When you decide what to track. | | **Alert** | An immutable record of one match. Carries the trademark, the event that triggered it, severity, and a deadline if applicable. | When you process matches in your application. | | **Event** | One row in your organization's append-only ledger, `evt_*`. Every webhook you receive is also an event, addressable by the id in the `webhook-id` header. Retained 30 days. | When you need to replay what you missed, or re-read a delivery. | | **Webhook** | A signed `POST` to a URL you control, delivered every time an alert fires. | When you want push instead of polling. | You can use webhooks, polling, or both. Webhooks deliver in seconds; polling gives you a recoverable fallback, and the [events feed](/api-reference/monitoring/events/list) gives you an exact replay from a checkpoint when a receiver was down. The events feed is **beta**: the ledger, the `evt_*` ids and the replay rail are stable, but the per-event `data` payloads are versioned by `payload_version` and their shape is not yet promised. ## The five watch types Pick from five watch types: `mark`, `portfolio`, `owner`, `class`, or `similarity`. See [Watches](/guides/monitoring/watches) for the full query DSL, the required field for each type, and worked examples. ## What an alert looks like Each alert carries the matched trademark, the event that produced it, a severity, and (where applicable) a jurisdiction-aware opposition deadline. This is the canonical shape returned by [`GET /v1/alerts/{id}`](/api-reference/monitoring/alerts/retrieve) and carried under `data.alert` in every `alert.created` webhook delivery: ```json theme={null} { "id": "alt_8kLm2nPq", "object": "alert", "schema_version": "2026-06-01", "watch": { "id": "wat_7hRt4xQw", "name": "US class 9, new filings", "type": "class" }, "customer_reference": "matter-2026-0481", "event": { "type": "trademark.created", "summary": "Trademark created", "diff": [] }, "match": null, "trademark": { "id": "tm_9pQs3vNk", "mark_text": "ACME ROCKETS", "mark_feature_type": "word", "office_code": "US", "status": { "primary": "pending", "stage": "examination" }, "filing_date": "2026-05-01", "registration_date": null, "nice_classes": [9], "owner_name": "Aurora Digital, Inc.", "as_of": "2026-05-08T14:32:11.428Z", "links": { "self": "/v1/trademarks/tm_9pQs3vNk" } }, "deadline": { "severity": "critical", "opposition_window_status": "critical", "must_act_by": "2026-05-16T03:59:59.999Z" }, "timestamps": { "occurred_at": "2026-05-08T14:30:00.000Z", "ingested_at": "2026-05-08T14:31:00.000Z", "created_at": "2026-05-08T14:32:11.428Z" }, "links": { "trademark": "/v1/trademarks/tm_9pQs3vNk", "watch": "/v1/watches/wat_7hRt4xQw" } } ``` For marks in [supported jurisdictions](/guides/monitoring/opposition-windows#coverage), `deadline.opposition_window_status` and `deadline.must_act_by` are computed for you. Route on `deadline.severity` to escalate the matches that need urgent attention. See [Webhooks](/guides/monitoring/webhooks#payload-shape) for the full field reference. ## Choosing how alerts reach you | Mode | How it works | Best for | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | | **Webhook (push)** | Signa posts each alert to a URL you control. HMAC-signed, retried across up to 7 attempts with exponential backoff. | Production workflows, low latency, on-call paging. | | **Polling (pull)** | Your code calls `GET /v1/alerts` on a schedule. | Prototypes, internal tools, low volume. | | **Webhook + reconciliation** | Push for latency, plus a periodic call to [`POST /v1/alerts/lookup`](/api-reference/monitoring/alerts/lookup) to confirm nothing was lost. | Production workflows that cannot tolerate a missed alert. | See [Handling alerts](/guides/monitoring/alert-handling) for the reconciliation pattern. ## What's next Pick a watch type, build the query, preview the volume before you go live. Register an endpoint, verify signatures, handle retries and rotation. A copy-pasteable Cloudflare Worker or AWS Lambda recipe. Use the diagnostics endpoint to find out exactly why an alert didn't fire. # Pipe alerts into Slack Source: https://docs.signa.so/guides/monitoring/slack-via-webhook Copy-pasteable Cloudflare Worker and AWS Lambda recipes ## What you'll build ``` Signa webhook -> Cloudflare Worker -> Slack incoming webhook (verifies signature) (posts to channel) ``` You need: * A Slack [incoming webhook URL](https://api.slack.com/messaging/webhooks). * A Slack channel to post into. * A Signa API key with `portfolios:manage` (used to register the webhook endpoint). * A Signa watch already created, see [Watches](/guides/monitoring/watches). Your Signa API key only needs to live wherever you call `signa.webhooks.create`. The Cloudflare Worker / Lambda below verifies signatures with the webhook secret and never calls the Signa API, so it does not need a Signa API key at runtime, only `SIGNA_WEBHOOK_SECRET` and `SLACK_WEBHOOK_URL`. ## Cloudflare Worker Save as `worker.ts`: ```ts theme={null} import { verifyWebhookSignature } from '@signa-so/sdk'; export default { async fetch(req: Request, env: Env): Promise { if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 }); const body = await req.text(); const ok = verifyWebhookSignature( { 'webhook-id': req.headers.get('webhook-id') ?? '', 'webhook-timestamp': req.headers.get('webhook-timestamp') ?? '', 'webhook-signature': req.headers.get('webhook-signature') ?? '', }, body, env.SIGNA_WEBHOOK_SECRET, ); if (!ok) return new Response('invalid signature', { status: 401 }); const event = JSON.parse(body); if (event.type !== 'alert.created') return new Response('ok', { status: 200 }); const alert = event.data.alert; await fetch(env.SLACK_WEBHOOK_URL, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: alertToSlackMessage(alert) }), }); return new Response('ok', { status: 200 }); }, }; interface Env { SIGNA_WEBHOOK_SECRET: string; SLACK_WEBHOOK_URL: string; } function alertToSlackMessage(alert: Record): string { const sev = alert.deadline?.severity as string; const emoji = sev === 'critical' ? ':rotating_light:' : sev === 'high' ? ':warning:' : ':bell:'; const windowStatus = alert.deadline?.opposition_window_status; const window = windowStatus ? ` (window: ${windowStatus})` : ''; const headline = alert.event?.summary; const watchLabel = alert.watch?.name; const markLabel = alert.trademark?.mark_text; const mustActBy = alert.deadline?.must_act_by; return [ `${emoji} *Signa alert*: ${headline}${window}`, `Watch: *${watchLabel}*`, `Trademark: *${markLabel}*`, mustActBy ? `Action by: *${mustActBy}*` : null, ] .filter(Boolean) .join('\n'); } ``` The `alert.created` body is self-contained: it carries the full alert object (`event.summary`, `watch.name`, `trademark.mark_text`, `deadline`, etc.) inline, so the message above renders human-readable names with no extra API call. See [the payload shape](/guides/monitoring/webhooks#payload-shape) for the full field reference. If you still want to enrich beyond what the body carries (e.g. fetch the full goods/services list), add a `GET /v1/trademarks/{id}` or `GET /v1/watches/{id}` call. Give the Worker its own Signa API key with the minimum scope (`trademarks:read` for trademark lookups, `portfolios:manage` for watch lookups), stored in a different secret than `SIGNA_WEBHOOK_SECRET` so the two can be rotated independently. Set the env vars and deploy: ```bash theme={null} wrangler secret put SIGNA_WEBHOOK_SECRET wrangler secret put SLACK_WEBHOOK_URL wrangler deploy ``` Register the Worker URL with Signa: ```ts theme={null} import Signa from '@signa-so/sdk'; const signa = new Signa({ api_key: process.env.SIGNA_API_KEY }); const wh = await signa.webhooks.create({ url: 'https://signa-slack..workers.dev/', description: 'Slack #legal-alerts', enabled_events: ['alert.created'], }); console.log('Set SIGNA_WEBHOOK_SECRET to:', wh.secret); ``` Copy `wh.secret` into the Worker as `SIGNA_WEBHOOK_SECRET`. ## AWS Lambda + API Gateway alternative ```ts theme={null} import { verifyWebhookSignature } from '@signa-so/sdk'; import type { APIGatewayProxyHandlerV2 } from 'aws-lambda'; const formatAlert = (a: any) => `${a.event.type}: ${a.trademark.mark_text} (${a.trademark.office_code})`; export const handler: APIGatewayProxyHandlerV2 = async (event) => { const body = event.body ?? ''; const ok = verifyWebhookSignature( { 'webhook-id': event.headers['webhook-id'] ?? '', 'webhook-timestamp': event.headers['webhook-timestamp'] ?? '', 'webhook-signature': event.headers['webhook-signature'] ?? '', }, body, process.env.SIGNA_WEBHOOK_SECRET!, ); if (!ok) return { statusCode: 401, body: 'invalid signature' }; const payload = JSON.parse(body); if (payload.type === 'alert.created') { await fetch(process.env.SLACK_WEBHOOK_URL!, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: formatAlert(payload.data.alert) }), }); } return { statusCode: 200, body: 'ok' }; }; ``` Route an API Gateway `POST` to the Lambda. Set `SIGNA_WEBHOOK_SECRET` and `SLACK_WEBHOOK_URL` as Lambda env vars. ## Tips * Slack rate-limits incoming webhooks. For high-volume watches, batch with a 30-second buffer, or fan out to a topic and let multiple webhooks consume. * Slack truncates messages over 40k chars. Keep formatting compact. # Troubleshooting Source: https://docs.signa.so/guides/monitoring/troubleshooting Use the diagnostics endpoint to find out why an alert didn't fire, and monitor Signa's own uptime When an expected alert doesn't arrive, the diagnostics endpoint tells you exactly why, so you can separate query, delivery, and platform issues quickly. ## Step 1: Run the diagnostics For each (watch, trademark) pair you expected an alert for, call [`GET /v1/watches/{id}/diagnostics`](/api-reference/monitoring/watches/diagnostics): ```ts theme={null} const trace = await signa.watches.diagnostics('wat_01HK7M...', { trademarkId: 'tm_01HK7N...', }); console.log(trace.reason); ``` The `reason` field gives you the answer in plain English. The endpoint walks the evaluation steps in order and surfaces the first failure. ## Step 2: Interpret `reason` | `reason` value | Meaning | Next step | | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `alert fired` | Alert was generated. Use `alert_id` to chase webhook delivery. | See Step 3. | | `watch does not include office {code}` | The watch's `filters.offices` (or `filters.jurisdictions`) doesn't include this trademark's office. | `PATCH` the watch to add the office. The widened criteria apply automatically to future data updates. | | `trademark evaluated more than 90 days ago; provenance no longer available` | Outside the 90-day diagnostic horizon. | The trace cannot be regenerated. Check the diagnostics `data_window` object for every retention horizon, and diagnose future misses within it. If the mark still matches the watch, it will be re-evaluated on the next update going forward. | | `trademark not in candidacy window for the most recent {office} sync run` | Signa hasn't seen a change to this trademark from that office since the watch last checked it. | Run [`POST /v1/watches/preview`](/api-reference/monitoring/watches/preview) with the watch's query to confirm the trademark would match today. | | `trigger event {type} not in watch.trigger_events` | The watch's `trigger_events` filter excluded this event type. For example, you watch only `trademark.created` but this was a `trademark.updated`. | Widen `trigger_events` via `PATCH`. The new shape applies to future updates automatically. | | `no matching reason available` | Fallback. | File a support ticket with the `request_id`. This should not happen in steady state. | ## Step 3: Cross-reference webhook delivery When `alert_fired=true` but you never saw it on your receiver, follow the trace to the delivery log: ```ts theme={null} // 1. The alert fired. alert_id (alt_*) is the cross-system trace handle. const trace = await signa.watches.diagnostics('wat_01HK7M...', { trademarkId }); console.log(trace.alert_id); // 2. List delivery attempts for the endpoint subscribed to alert.created. const deliveries = await signa.webhooks.listDeliveries('whk_01HK...', { since: '2026-05-01T00:00:00Z', }); // 3. Find the delivery rows for that alert and inspect the outcome. for await (const d of deliveries) { if (d.alert_id === trace.alert_id) { console.log(d.status, d.http_status, d.error_reason, d.response_body); } } ``` Likely outcomes: * `delivered`: receiver returned 2xx but may not have stored it. Inspect `response_body` and your own logs. * `failed`: non-2xx response. `error_reason` (e.g. `non_2xx_500`) and `http_status` tell you which side broke. Up to 7 attempts total. * `exhausted`: all attempts failed. Replay manually with [`POST /v1/webhooks/{id}/deliveries/{did}/redeliver`](/api-reference/monitoring/webhooks/redeliver). * `pending`: still queued. Wait, or check [`/health/ready`](#monitor-signa-uptime) to confirm Signa is up. If the endpoint was auto-disabled mid-flight, check [`GET /v1/webhooks/{id}`](/api-reference/monitoring/webhooks/retrieve). `status='disabled'` plus `disabled_reason` (`auto_consecutive_100`, `auto_failure_rate_50_over_50`, or `manual`) explains why. Re-enable with [`PATCH /v1/webhooks/{id}`](/api-reference/monitoring/webhooks/update) once the receiver is healthy again. ## Step 4: Confirm Signa is healthy Before assuming a Signa-side bug, check readiness (see [Monitor Signa uptime](#monitor-signa-uptime) below). Retry the diagnostic flow once the platform reports `ok`. ## Monitor Signa uptime Two unauthenticated health endpoints let you monitor Signa from your own observability stack, useful for SLA reporting, status-page integration, and incident triage. | Endpoint | What it tells you | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /v1/health` | The API process is up. Always returns HTTP 200 and `{"status":"ok"}`; it does not check dependencies, so it cannot report `degraded` or `unhealthy`. | | `GET /health/ready` | The API plus its database, cache, and search dependencies are reachable. Returns `status: "ok"`, `"degraded"` (a non-critical dependency is down, most requests still work), or `"unhealthy"` (503; a critical dependency is down). | Use `/health/ready` for real uptime and incident-detection monitoring; use `/v1/health` only as a lightweight liveness ping. Both endpoints are unauthenticated, no API key is required for your monitoring agent. ```bash theme={null} curl https://api.signa.so/health/ready ``` ```json theme={null} { "status": "ok", "service": "core-api", "uptime_ms": 1234567, "dependencies": { "postgres": { "status": "ok", "latency_ms": 4 }, "valkey": { "status": "ok", "latency_ms": 1 }, "opensearch": { "status": "ok", "latency_ms": 6 } } } ``` Example synthetic check, adjust to your monitoring tool of choice. Assert only on the HTTP status code (200 vs 503), that already separates "up" (`ok` or `degraded`) from "down" (`unhealthy`); asserting the body's `status` equals exactly `"ok"` would page you on every non-critical dependency blip: ```yaml theme={null} type: api name: Signa API readiness config: request: method: GET url: https://api.signa.so/health/ready assertions: - type: statusCode operator: is target: 200 options: tick_every: 60 ``` If you want visibility into `degraded` without paging on it, log the response body's `status` field separately as a warning-level signal. A cron or uptime-checker version of the same check: ```bash theme={null} #!/usr/bin/env bash set -e RESPONSE=$(curl -fsS --max-time 5 https://api.signa.so/health/ready) STATUS=$(echo "$RESPONSE" | jq -r '.status') if [ "$STATUS" = "ok" ] || [ "$STATUS" = "degraded" ]; then exit 0 fi echo "Signa API ${STATUS}: ${RESPONSE}" >&2 exit 1 ``` ### What `/health/ready` doesn't cover `/health/ready` is API-side health only. It does not surface: * **Data freshness for a specific office.** An office falling behind on updates doesn't flip the API to `degraded`. If you watch a specific office and alerts stop firing, check the [diagnostics endpoint](/api-reference/monitoring/watches/diagnostics), `last_relevant_sync_run.completed_at` will be stale. * **Webhook delivery.** That's between Signa and your receiver. Cross-check [`GET /v1/webhooks/{id}/deliveries`](/api-reference/monitoring/webhooks/list-deliveries) for the delivery audit log. If `/health/ready` is green but you suspect a Signa-side issue with a specific watch, use the diagnostics endpoint above. Signa does not currently publish a public status page. If you maintain your own, surface `/health/ready` and the diagnostics-derived freshness signal separately, they cover different incident classes. For incident questions, email [support@signa.so](mailto:support@signa.so), or report a reproducible platform bug programmatically via [`POST /v1/feedback`](/api-reference/administration/create-feedback) (attach the `request_id` from a failed call so we see exactly what you hit). ## Retention | Data | Window | | -------------------------------------------- | ------- | | Change records (used for diagnostic lookups) | 90 days | | Webhook delivery audit | 30 days | | Alerts | 90 days | | Diagnostic freshness horizon | 90 days | Past the diagnostic horizon, `evaluated=false` and `reason` explains the freshness limit. A watch always continues to evaluate new data going forward, the horizon only limits the backward-looking diagnostic trace. # Watches Source: https://docs.signa.so/guides/monitoring/watches Pick a watch type, build the query, tune match sensitivity, preview the volume A watch is a saved query that Signa evaluates against every data update for the offices it covers. Pick the type whose required filter is the narrowest fit for what you want to track, build the query body, and use [Preview](#preview-before-you-launch) to estimate volume before going live. Creating, listing, and managing watches requires the `portfolios:manage` scope on your API key. ## Quickstart: watch a class in the US A complete path for the most common setup, "tell me about new USPTO filings in my Nice class." Everything below needs an API key with the `portfolios:manage` scope. ### 1. Preview the volume Scope to the US with `filters.jurisdictions: ["US"]` (equivalently `filters.offices: ["US"]`; jurisdictions are auto-translated to offices at create time): ```bash theme={null} curl -X POST "https://api.signa.so/v1/watches/preview" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: preview-us-class9-2026-06-12" \ -d '{ "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US"] } }, "trial_window_days": 7 }' ``` ```json theme={null} { "object": "watch_preview", "estimated_match_count": 312, "trial_window_days": 7, "request_id": "req_8kLm2nPq" } ``` \~312 alerts/week is a real feed. If the count is overwhelming, narrow with `trigger_events: ["trademark.created"]` (new filings only, drops the update and status-change churn) before creating. ### 2. Create the watch ```bash 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: create-us-class9-watch-001" \ -d '{ "name": "US class 9, new filings", "watch_type": "class", "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US"] }, "trigger_events": ["trademark.created"] } }' ``` The response is the `Watch` object, save its `id` (`wat_*`). The watch is `active` immediately and is evaluated against every USPTO data update. Mutating calls like this one require the [`Idempotency-Key` header](/guides/resilience#idempotent-requests). ### 3. Register a webhook (push) ```bash theme={null} curl -X POST "https://api.signa.so/v1/webhooks" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: create-us-class9-webhook-001" \ -d '{ "url": "https://alerts.example.com/signa", "description": "US class 9 alerts", "enabled_events": ["alert.created"] }' ``` Store the returned `secret` immediately, it is shown once. Each delivery is an HMAC-signed POST carrying the full alert object, whose `data.alert.trademark.id` you can pass straight to [`GET /v1/trademarks/{id}`](/api-reference/trademarks/retrieve-trademark): ```json theme={null} { "type": "alert.created", "id": "evt_8kLm2nPq", "payload_version": 1, "timestamp": "2026-06-12T01:14:09.412Z", "data": { "event_id": "evt_8kLm2nPq", "alert_id": "alt_8kLm2nPq", "alert": { "id": "alt_8kLm2nPq", "object": "alert", "schema_version": "2026-06-01", "watch": { "id": "wat_7hRt4xQw", "name": "US class 9, new filings", "type": "class" }, "customer_reference": null, "event": { "type": "trademark.created", "summary": "Trademark created", "diff": [] }, "match": null, "trademark": { "id": "tm_9pQs3vNk", "mark_text": "ACME ROCKETS", "office_code": "US", "status": { "primary": "pending", "stage": "examination" }, "nice_classes": [9], "links": { "self": "/v1/trademarks/tm_9pQs3vNk" } }, "deadline": { "severity": "normal", "opposition_window_status": "open", "must_act_by": "2026-07-13T03:59:59.999Z" }, "timestamps": { "occurred_at": "2026-06-12T01:10:00.000Z", "created_at": "2026-06-12T01:14:09.412Z" }, "links": { "trademark": "/v1/trademarks/tm_9pQs3vNk", "watch": "/v1/watches/wat_7hRt4xQw" } } } } ``` Verify signatures on every delivery (see [Webhooks](/guides/monitoring/webhooks#signing)) and dedupe by the `webhook-id` header. No receiver yet? Use [a tunnel or request-bin pattern](/guides/monitoring/webhooks#testing-deliveries-before-you-have-a-receiver) to develop against real deliveries. ### 4. Add a polling fallback (pull) Webhooks deliver in seconds but receivers go down. A periodic reconciliation pass over the watch's alerts catches anything you missed: ```bash theme={null} curl "https://api.signa.so/v1/watches/wat_.../alerts?limit=100" \ -H "Authorization: Bearer $SIGNA_API_KEY" ``` Compare the returned alert IDs against what your receiver recorded, and fetch any gaps with [`POST /v1/alerts/lookup`](/api-reference/monitoring/alerts/lookup) or process them directly from the list response. Page with `pagination.cursor` until `has_more` is `false`. For the full reconciliation pattern, see [Handling alerts](/guides/monitoring/alert-handling). ### Variations * **US filings for a specific competitor**: `watch_type: "owner"` with `filters: { ownerId: "own_...", jurisdictions: ["US"] }`. * **Confusingly similar US filings**: `watch_type: "similarity"` with `q: "yourmark"`, `strategies: ["exact", "phonetic", "fuzzy"]`, `min_match_tier: "phonetic"`, and `filters: { jurisdictions: ["US"] }`. * **More jurisdictions later**: `PATCH` the watch with a full replacement `query`, one watch covers the set. `PATCH` replaces the entire `query` object and re-validates it (`version`, the type's required filters, `trigger_events`), so resend every field, not just the one you're changing: ```json theme={null} { "query": { "version": "v2", "filters": { "niceClasses": [9], "jurisdictions": ["US", "EU", "GB"] }, "trigger_events": ["trademark.created"] } } ``` ## Pick a watch type | You want to... | `watch_type` | Required field | | ------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------- | | Track one specific mark you own (renewals, status drift). | `mark` | `filters.trademarkIds` (one ID) | | Watch a fixed set of marks at once (deprecated, see below). | `portfolio` | `filters.trademarkIds` (1 or more IDs) | | Track a competitor by owner. | `owner` | `filters.ownerId` | | Track a company as a resolved entity — including subsidiaries discovered later. | `owner` | `filters.entityId` or `filters.entityGroup` — see [Entity watches](#entity-watches) | | Watch new filings in a Nice class (or class set), optionally by jurisdiction. | `class` | `filters.niceClasses` | | Detect confusingly similar new filings. | `similarity` | `q` (text) | **`watch_type: "portfolio"` is deprecated.** It is still accepted, still evaluated and still alerts exactly as before, and there is no removal date — but it is no longer the direction we are building in. For marks you own the destination is a portfolio plus a webhook: membership is the rail that will carry `trademark.*` and `office_action.*`, and unlike a watch it does not have to be rewritten every time the set of marks changes. **Do not delete a `portfolio` watch yet.** `trademark.status_changed` and `office_action.issued` are accepted for subscription **today but are not yet emitted** — they start delivering when the API events projector is enabled (see [Event types](/guides/monitoring/webhooks#event-types) in the webhooks guide, which tracks that state). Until then a `portfolio` watch and its `alert.created` webhook are the only thing delivering these changes, so run the portfolio alongside the watch and retire the watch once you are receiving the direct events. Keep using `mark`, `similarity` and `owner` watches for alerts on marks you do *not* own — nothing replaces those. The `query` body uses the same vocabulary as [trademark search](/api-reference/trademarks/search-trademarks), with one casing difference: REST search query params are snake\_case (`nice_classes=9`), while watch-DSL filter keys are camelCase (`niceClasses: [9]`). If you can build the search, you can save it as a watch, just translate the key casing. ## The query DSL This is the canonical accepted shape, validated strictly, anything outside it returns `400`: ```ts theme={null} interface WatchQuery { version: string; // REQUIRED, non-empty string; new watches use "v2" q?: string; // keyword query (required for similarity) filters?: WatchFilters; // camelCase keys, see the full list below trigger_events?: WatchTriggerEvent[]; // non-empty subset of the five events; default = first three strategies?: WatchSearchStrategy[]; // similarity only, non-empty subset of exact/phonetic/fuzzy/prefix; default exact+fuzzy min_match_tier?: WatchMinMatchTier; // similarity only, gates matches by attribution tier } ``` ### `version` (required) A non-empty string. New watches use `"v2"`. Omitting `version` returns `400`. ### `q` keyword constraints * Whitespace-separated. Up to 20 keywords. Each keyword must be at least 3 characters. * Stop words are rejected with `400` to prevent watches that match too broadly. The current list is `the, and, or, not, for, a, an, of, in, on, to, is`. ### `filters`, the full key list Filter keys are camelCase. Unknown keys, including snake\_case typos like `nice_classes`, are rejected with `400` (the error message suggests the camelCase spelling when it recognizes the typo). Allowed keys: `applicationNumber`, `attorneyFirmName`, `attorneyId`, `challengeStates`, `entityGroup`, `entityId`, `expiryDate`, `filingDate`, `filingRoute`, `firmId`, `goodsServicesText`, `hasMedia`, `hasProceedings`, `irNumber`, `isMadrid`, `isRetracted`, `isSeriesMark`, `jurisdictions`, `markFeatureType`, `markLegalCategory`, `niceClasses`, `office`, `offices`, `originOfficeCode`, `ownerCountry`, `ownerHasLei`, `ownerId`, `ownerLei`, `ownerName`, `ownerPubliclyTraded`, `ownerTicker`, `publicationDate`, `registrationDate`, `registrationNumber`, `renewalDueDate`, `rightKind`, `scopeKind`, `statusPrimary`, `statusReason`, `statusStage`, `terminationDate`, `trademarkIds`, `updatedAt`, `viennaCodes`. Notes: * **ID filters accept both forms.** `trademarkIds`, `ownerId`, `attorneyId`, and `firmId` accept prefixed IDs (`tm_*`, `own_*`, `att_*`, `firm_*`) or raw UUIDs. Wrong-type or malformed IDs return `400` with the offending index in the field path. Responses always echo these filters in the prefixed form regardless of which form you sent, so a `GET` → edit → `PATCH` round-trip is lossless. * **Office codes are case-insensitive**: `filters.offices: ["uspto"]` is accepted and echoed back in the uppercase [WIPO ST.3](https://www.wipo.int/standards/en/st3.html) form (`["US"]`); legacy lowercase codes are accepted permanently as aliases. Each code must be a 2-10 character string that exists in [`GET /v1/offices`](/api-reference/reference/list-offices). * `filters.jurisdictions` (e.g. `["US", "EU"]`) is auto-translated to `filters.offices` at create time, so either spelling of scope works. Watches keep office-based scoping and do not apply the protection-scope jurisdiction expansion used by search and screening, so a watch scoped to `["DE"]` watches the German office only and does not surface EU trade marks. * `entityId` / `entityGroup` accept `ent_*` ids or raw UUIDs and are mutually exclusive — see [Entity watches](#entity-watches). * **`ownerTicker` and `ownerPubliclyTraded` are subsidiary-inclusive.** They share the same public-company/listing fields as `GET /v1/trademarks`, so a watch with `ownerTicker: "NKE"` fires on Nike's own filings **and** those of its subsidiaries (an owner's entity listing ticker, direct or inherited from a listed ancestor, folds into this field). `ownerPubliclyTraded: true` matches any owner with an active listing association (a confirmed SEC ticker, or a resolved entity that is listed or a subsidiary of a listed company); a `false` or absent value means no confirmed listing, not confirmed private. ### `trigger_events` Any non-empty subset of these five values (anything else returns `400`; an empty array is also rejected, omit the field to subscribe to the default set): **Default set** (applied when `trigger_events` is omitted): * `trademark.created` * `trademark.updated` * `trademark.status_changed` **Opt-in** (valid, but only delivered when you list them explicitly): * `trademark.retracted` * `trademark.corrected` Narrow the default set to silence events you don't care about. For example, a portfolio watch that only fires on status changes: ```ts theme={null} trigger_events: ['trademark.status_changed']; ``` Or subscribe to the default events plus retractions: ```ts theme={null} trigger_events: [ 'trademark.created', 'trademark.updated', 'trademark.status_changed', 'trademark.retracted', ]; ``` Each triggered event reaches you as an `alert.created` webhook delivery carrying the trademark event type in `data.alert.event.type` — see [Receiving `trademark.*` events](/guides/monitoring/webhooks#receiving-trademark-events) for the delivery-side view (envelope shape, the pure-flip rule for `retracted` / `corrected`, and scope caveats). **Retractions, corrections, and cancellations, in one paragraph.** `trademark.retracted` fires when the source office feed pulls or retracts a record (`is_retracted` flips false to true); `trademark.corrected` fires on the reverse flip, when a previously retracted record reappears. Both are opt-in: list them explicitly in `trigger_events`, or they never arrive. A legal cancellation is different and needs no opt-in: whenever a matching mark's status changes, including moving to `cancelled`, `withdrawn`, `abandoned`, or otherwise going dead, the watch fires a regular `trademark.status_changed` alert (subscribed by default). Any mark, owner, portfolio, or class watch already covers cancellations with no special setup. ### `strategies` (similarity only) Which search strategies feed a similarity watch's matching, a non-empty subset of `exact`, `phonetic`, `fuzzy`, `prefix`. Omit it to use the default (`exact` and `fuzzy`); an explicit empty array returns `400`. Add `phonetic` to catch sound-alikes ("NOVA" / "KNOVA" / "NOWA") or `prefix` for starts-with matching. Ignored for other watch types. ### `min_match_tier` (similarity only) Gates which similarity matches are allowed to fire an alert, by the attribution tier of the match. One of: * `exact`: only exact matches alert. * `normalized`: adds normalized, phrase, synonym, identifier, and homoglyph matches. * `fuzzy`: adds fuzzy and prefix matches. * `phonetic`: the broadest tier, adds phonetic (sound-alike) matches. Omit it and every tier, including `phonetic`, can fire an alert (the broadest gate). Ignored for other watch types. ## `delivery_mode` — per-alert delivery today `delivery_mode` is a top-level field on the **watch** (alongside `name` and `query`). Only `always_per_alert` is accepted on create and update today — any other value returns `400`. The digest modes (`digest_above_threshold`, `digest_only`) appear in the watch schema and the field is stored to eventually drive digest batching, but **digest suppression is not live**: every watch delivers one `alert.created` webhook per alert, regardless of the stored mode. Until digest modes ship, size your receiver for per-alert volume ([Preview](#preview-before-you-launch) estimates it) and batch on your side if you need digests — dedupe by `webhook-id` and roll alerts up on your own schedule. See [Known beta limitations](/guides/monitoring/beta-limitations#delivery-modes-only-always_per_alert). Use [Preview](#preview-before-you-launch) to estimate volume for different `strategies` / `min_match_tier` combinations before going live. ## `customer_reference`, your own label `customer_reference` is a top-level field on the watch (alongside `name` and `query`, not inside the query DSL), a free-text passthrough string you control. Signa never interprets it; it's there for your own correlation (a case number, internal watch ID, team name, routing key). * Set it on create, `PATCH`, or in a bulk create, max 200 characters. Pass `null` to clear it. * It's returned on the watch resource (`GET /v1/watches/{id}`). * It is frozen at emit time and echoed onto every alert the watch produces, as `customer_reference` on both the REST `Alert` resource and the `alert.created` webhook body. Changing it later affects only alerts emitted after the change; already-emitted alerts keep the value they were stamped with. ```ts theme={null} await signa.watches.create({ name: 'Aurora Digital core mark, status changes', watch_type: 'mark', customer_reference: 'matter-2026-0481', query: { version: 'v2', filters: { trademarkIds: ['tm_8kLm2nPq'] }, trigger_events: ['trademark.status_changed'], }, }); ``` ## Per-jurisdiction watches Build separate watches per jurisdiction when you need different delivery cadence per region, or when different regional teams own different jurisdictions. Otherwise pass `filters.jurisdictions: ["US", "EU", "GB"]` once and let a single watch cover the set. ```ts theme={null} await signa.watches.create({ name: 'Apple Inc, class 9 worldwide', watch_type: 'owner', query: { version: 'v2', filters: { ownerId: 'own_7hRt4xQw', niceClasses: [9], jurisdictions: ['US', 'EU', 'GB', 'CA'], }, }, }); ``` ## Entity watches An **entity** (`ent_*` id, from [`GET /v1/entities`](/api-reference/parties/search-entities)) groups the many per-office owner records that belong to one real-world company. Entity watches let you say "watch everything Unilever does" without enumerating owner records — and without re-editing the watch when Signa's entity resolution links new owner records to the company later. Two scopes, mutually exclusive (sending both returns `400`): * **`filters.entityId`** — the entity's own marks: every trademark whose *current* owners are linked to that entity, across all offices. Derived singleton ids (an `ent_*` id wrapping a single unlinked owner) are valid targets too. * **`filters.entityGroup`** — the entity's whole **GLEIF corporate family**: the ultimate parent plus all subsidiaries. Pass any family member's id; Signa climbs to the family root and covers every entity in the tree. ```ts theme={null} await signa.watches.create({ name: 'Unilever group -- all activity', watch_type: 'owner', query: { version: 'v1', filters: { entityGroup: 'ent_018f9b2e-9b6c-7c9c-b4f1-1234567890ab' }, }, }); ``` Semantics worth knowing: * **Accepted id forms.** `ent_` or a raw UUID (stored normalized). Wrong-prefix or malformed ids return `400`. For `watch_type: "owner"`, `entityId` or `entityGroup` satisfies the required-field rule in place of `ownerId`. * **Validated at create time.** A nonexistent or suppressed entity returns `400` (the watch would never fire — we reject it instead of storing a dead watch). A corporate family too large to traverse (more than 5,000 related entities or deeper than 32 levels) returns `422` with type `entity_too_large` and `reason: "family_graph_too_large"` — watch a specific entity in the family with `entityId` instead. * **Freshness is automatic.** Matching happens against each trademark's live entity linkage, refreshed continuously by Signa's entity-resolution pipeline. When a newly discovered subsidiary (or a newly linked owner record) is attached to the entity, its marks start matching your watch on their next change — no PATCH needed. `entityGroup` re-resolves the family on every evaluation, so family growth is picked up automatically. If the entity you watch is later fused into another entity, the watch follows the surviving entity on its own. * **No member cap.** Unlike `?entity_id=` on trademark search (which caps at 10,000 member owners), entity *watches* have no member-owner limit — even the largest portfolios are watchable. Only the family-graph traversal bound above applies, and only to `entityGroup`. * **Honest caveat: coverage depends on entity-resolution linkage.** An entity watch only sees marks whose owner records have been linked to the entity (or that belong to the watched singleton owner). Owner records that entity resolution hasn't linked yet — common for name variants, recent filings, or offices with sparse identifiers — won't match until they're linked. For belt-and-braces coverage of a specific known owner record, add a separate `ownerId` watch. ## Worked examples ### Track one mark ```ts theme={null} await signa.watches.create({ name: 'My core mark, status changes', watch_type: 'mark', query: { version: 'v2', filters: { trademarkIds: ['tm_8kLm2nPq'] }, trigger_events: ['trademark.status_changed'], }, }); ``` ### Track a fixed set of marks (deprecated) A portfolio plus a webhook is where this is going for marks you own — but `trademark.status_changed` / `office_action.issued` are **not emitted yet** (they are accepted for subscription now and start delivering when the API events projector is enabled, tracked in the webhooks guide's [Event types](/guides/monitoring/webhooks#event-types) table). Keep the watch until then; see the note above. ```ts theme={null} await signa.watches.create({ name: 'Q4 acquisitions portfolio', watch_type: 'portfolio', query: { version: 'v2', filters: { trademarkIds: ['tm_8kLm2nPq', 'tm_9pQs3vNk', 'tm_7hRt4xQw'], }, }, }); ``` ### Track a Nice class ```ts theme={null} await signa.watches.create({ name: 'New class-9 filings in US/EU', watch_type: 'class', query: { version: 'v2', filters: { niceClasses: [9], jurisdictions: ['US', 'EU'] }, trigger_events: ['trademark.created'], }, }); ``` ### Detect a similar mark ```ts theme={null} await signa.watches.create({ name: 'Marks similar to Nike', watch_type: 'similarity', query: { version: 'v2', q: 'Nike', filters: { niceClasses: [9, 35] }, strategies: ['exact', 'phonetic', 'fuzzy'], min_match_tier: 'phonetic', }, }); ``` ## Preview before you launch Dry-run the query against the last N days of data to estimate volume: ```ts theme={null} const preview = await signa.watches.preview({ query: myQuery, trial_window_days: 30, }); console.log(`${preview.estimated_match_count} alerts in the last 30 days`); ``` Preview uses the same logic as live watches, so the count is faithful. If you see thousands of matches, the query is too broad, tighten `filters`, narrow `strategies`, or set `min_match_tier` to a stricter tier (`exact` is the narrowest). By default it returns the actual matching marks; set `count_only: true` to get just the count. Preview semantics worth knowing before you script against it (full details on [Preview Watch](/api-reference/monitoring/watches/preview)): * **`estimate_basis`.** When the response carries `estimate_basis: "candidacy_upper_bound"`, the count is an upper-bound estimate, not an exact match count (the scan hit the server-side cap, the search backend was unreachable, or the time budget expired partway). Absent field means an exact count. * **One preview at a time.** A second concurrent preview for your org returns `429` with a `Retry-After` header. Honor it, the SDK does. * **`preview_timeout` (504).** If the time budget expires before any usable result exists you get a `preview_timeout` envelope with `retryable: false`. Narrow the query instead of retrying. * **Latency.** `class`, `mark`, and `owner` previews complete in a few seconds; broad `similarity` previews are the heaviest and may approach the budget. * **ID forms.** Preview accepts the same prefixed (`tm_*` / `own_*`) or raw-UUID ID filters as create. After you update a live watch (widen its offices, broaden `trigger_events`), the new criteria take effect automatically on the next data update, the watch re-evaluates with its current shape going forward. ## Create up to 100 at once ```ts theme={null} await signa.watches.bulk({ watches: [w1, w2, w3 /* ... */] }); ``` The whole batch validates upfront. Partial failures don't insert, it's all or nothing. ## Health and proof of monitoring Every watch carries an honest `health` block so that "no alert" is provably distinct from "not looking". `GET /v1/watches/{id}` returns the full object; `GET /v1/watches` returns just `health_status` per row (the full object would cost an extra query per row). ```json theme={null} { "id": "wat_01HK...", "health_status": "healthy", "health": { "status": "healthy", "evaluated_at": "2026-07-07T09:12:00Z", "evaluations_30d": 61, "issues": [], "offices": [ { "office_code": "US", "status": "ok", "coverage_through": "2026-07-06T23:59:59.999Z", "coverage_basis": "source_dates", "last_evaluated_at": "2026-07-07T09:12:00Z", "evaluations_30d": 30, "issue": null, "last_error": null } ] } } ``` ### Health states The watch-level `status` is the worst state across its in-scope offices. Precedence, highest first: paused, unsupported, degraded, lagging, pending, healthy. | Status | Meaning | What to do | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | `healthy` | Every in-scope office is evaluated and current. | Nothing. | | `pending` | Active watch, not yet evaluated, still within the office refresh window. Also shown right after you change a watch's `query` (per-office `issue` is `config_changed_pending`): the displayed coverage still describes the previous configuration, and the updated query has not been evaluated yet. | Wait for the next evaluation. | | `lagging` | Coverage has stopped advancing beyond the expected window. Per-office `issue` is `office_lagging` (ingestion is behind, or an evaluated office has no stated coverage) or `evaluation_backlog` (a newer indexed run has not been evaluated yet, even if the shown coverage looks recent). `never_evaluated` means an active watch aged past the window without a first evaluation, counted from when the office became available to the watch. | Usually resolves on its own. If it persists, contact support. | | `degraded` | The watch itself needs attention. Per-office `issue` is `failing` (repeated evaluation errors) or `unresolved_target` (an entity filter no longer resolves). | Check the entity filter or the stored query. | | `unsupported` | The watch scope includes an office Signa does not yet ingest (per-office `issue` is `unsupported_office`). A watch scoped only to offices we do not cover shows this, never `healthy` with zero alerts. | Remove the office from scope, or wait until we cover it. | | `paused` | The watch is paused. Coverage is shown frozen at its last value; no new evaluation happens. | Resume the watch to continue monitoring. | `issues` at the watch level is the deduplicated set of the per-office issue slugs. **`coverage_through` is office-data time, not wall-clock.** It is the timestamp of the office's own published data that the watch has been evaluated through, frozen at evaluation time. It never advances just because time passed. `last_evaluated_at` is the separate wall-clock instant of the last evaluation. When an office lags, `coverage_through` stops advancing while `last_evaluated_at` may keep moving. `coverage_basis` tells you how strong the claim is: `source_dates` (strongest, the office's own published dates) beats `date_range`, which beats `run_completed` (weakest, "the data available to us as of run completion"). Health certifies that evaluation occurred against the stated office-data horizon. It does not certify that every relevant mark was surfaced. What a watch catches is bounded by its search strategy and by connector coverage. Health is proof that we looked, and of the data horizon we looked through, not a guarantee of exhaustive recall. ## What's not allowed in `query` These are rejected with `400`: * **DSL/presentation keys** (anywhere in the query, at any depth): `function_score`, `script_score`, `script`, `sort`, `cursor`, `aggregations`, `aggs`, `highlight`. Scripting and presentation concerns don't belong in a saved monitor. * **`query.match` in any form**, both the object form (`match: {"nice_classes": [9]}`) and the string form (`match: "fuzzy"`). Matching is driven by `watch_type` plus `q` / `strategies` / `min_match_tier` (similarity) and the scoping fields under `filters`. * **Unknown `filters` keys**, including snake\_case typos of valid keys (`nice_classes` instead of `niceClasses`). See [the full key list](#filters-the-full-key-list). * **Unknown `trigger_events` values** (anything outside the five listed in [`trigger_events`](#trigger_events)) and empty `trigger_events` arrays. ## Common errors | Status | Cause | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Invalid query DSL, stop words in `q`, more than 20 keywords, a forbidden key, a missing required field for the selected `watch_type`, both `entityId` and `entityGroup` supplied, or an entity target that doesn't resolve. | | `409` | Your plan's watch limit has been reached. | | `413` | `query` exceeds 256 KB. | | `422` | `entity_too_large` — the `entityGroup` corporate family exceeds the relationship-graph walk limit. Use `entityId` on a specific family member instead. | See [Create Watch](/api-reference/monitoring/watches/create) for the full error schema. # Webhooks Source: https://docs.signa.so/guides/monitoring/webhooks Set up an endpoint, verify signatures, handle retries, rotate secrets A webhook endpoint is a URL you control that receives signed `POST` requests from Signa whenever a subscribed event fires. ## Set up an endpoint Accept `POST application/json` and verify the [Standard Webhooks](https://www.standardwebhooks.com/) headers, see [Verify signatures](#verify-signatures) below. [`POST /v1/webhooks`](/api-reference/monitoring/webhooks/create) registers your endpoint. The signing `secret` is returned once in the response, store it before the response is discarded. ```ts theme={null} const wh = await signa.webhooks.create({ url: 'https://alerts.example.com/signa', description: 'Production alert webhook', enabled_events: ['alert.created'], }); console.log('Store this secret:', wh.secret); ``` Use the SDK helper or any [Standard Webhooks](https://www.standardwebhooks.com/)-compatible library to verify the HMAC-SHA256 signature on every delivery, before you parse the body. ## Event types | Event type | When it fires | | -------------------------- | ------------------------------------------------------------------------------------------- | | `alert.created` | A watch matched a trademark. One delivery per alert. | | `trademark.status_changed` | A portfolio mark's status changed. Subscribable now, **not yet emitted**. | | `office_action.issued` | A new office action was stored for a portfolio mark. Subscribable now, **not yet emitted**. | Use `enabled_events` on [`POST /v1/webhooks`](/api-reference/monitoring/webhooks/create) or [`PATCH /v1/webhooks/{id}`](/api-reference/monitoring/webhooks/update). All three values are accepted at all times; anything else returns `400` with an `unknown event type` message naming the allowed set. `trademark.status_changed` and `office_action.issued` are accepted for subscription today but are **not yet emitted**. They start delivering when the API events projector is enabled; until then an endpoint subscribed to them receives nothing. Subscribe now if you want them from the first delivery — the accepted set does not change when the projector is turned on. ### `webhook.test` is not subscribable `webhook.test` is delivered only when you call [`POST /v1/webhooks/{id}/test`](/api-reference/monitoring/webhooks/test). The envelope matches `alert.created` but `data` is a fixed `{ "type": "ping" }` payload. Test deliveries are never retried and never count toward auto-disable, so probing a dead endpoint with `/test` is safe. ## Receiving direct semantic events Subscribe directly to `trademark.status_changed` or `office_action.issued` for register facts about marks you own. These direct events are emitted only for marks in at least one of your organization's portfolios (and only once the API events projector is enabled). An organization that monitors a mark only through watches receives corresponding watch alerts, but no direct semantic-event delivery. Set `portfolio_id` (`ptf_*`) on an endpoint to limit delivery to marks in that portfolio. **The scope applies to every event type, `alert.created` included**: an alert is delivered to a portfolio-scoped endpoint only when the mark it matched was a member of that portfolio at the time the event was recorded. Later portfolio changes never rewrite past deliveries. Leave `portfolio_id` unset to receive everything the endpoint is subscribed to, across all portfolios and all watch matches. If you want alerts for marks you do not own — the normal case for similarity, class and owner watches — leave `portfolio_id` unset on the endpoint that receives them. Two rules decide what you receive. **Recency.** An office action is delivered only if it was issued within the last 90 days. When Signa first ingests a mark, or first fetches its full prosecution history, that history is stored without any deliveries: those actions are the mark's past, not news. The window is wide enough to cover the slowest office reporting we see, so a genuine refusal always lands inside it. **One action, one event.** Offices frequently record a single action as several rows, such as a written action, its mailing, and the notification, sometimes on consecutive days. Signa clusters rows that fall within two days of each other on the same class and designation and delivers one `office_action.issued` for the group, keyed to the earliest date. If the mailed twin arrives in a later sync, it does not produce a second delivery. Two designations of the same Madrid mark, or two classes of the same refusal, remain separate actions and are delivered separately. A mark added to your portfolio today does not replay its past actions. Watch-derived trademark changes continue to arrive as `alert.created` deliveries. The following fields describe that watch path. The envelope `type` is always `alert.created`. The **derived trademark event type** is `data.alert.event.type`. Route on that nested field, not on the envelope `type`. ### The five trademark event types | `data.alert.event.type` | Derived when | Delivered by default? | | -------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `trademark.created` | A matching mark is seen for the first time. | **Yes** | | `trademark.updated` | Tracked fields changed, none of them status fields. | **Yes** | | `trademark.status_changed` | Any of the four status fields changed (`status_stage`, `status_primary`, `status_reason`, `challenge_states`). | **Yes** | | `trademark.retracted` | The upstream office feed pulled the record — a **pure** `is_retracted` false → true flip. | **Opt-in** via [`query.trigger_events`](/guides/monitoring/watches#trigger_events) | | `trademark.corrected` | A previously-retracted record reappeared — a **pure** `is_retracted` true → false flip. | **Opt-in** via [`query.trigger_events`](/guides/monitoring/watches#trigger_events) | A watch that omits `trigger_events` receives the first three. To receive `retracted` / `corrected`, list them explicitly: ```ts theme={null} await signa.watches.create({ name: 'Portfolio incl. source retractions', watch_type: 'portfolio', query: { version: 'v1', filters: { trademarkIds: ['tm_018f9b2e-9b6c-7c9c-b4f1-1234567890ab'] }, trigger_events: [ 'trademark.created', 'trademark.updated', 'trademark.status_changed', 'trademark.retracted', 'trademark.corrected', ], }, }); ``` **Pure-flip rule for `retracted` / `corrected` (deliberate).** These two are derived only when the `is_retracted` flip is the **sole** tracked change on that record update. When the flip co-occurs with any other field change, the event is derived as `trademark.updated` or `trademark.status_changed` instead — otherwise a default watch (which isn't subscribed to the opt-in events) would silently lose the alert it normally gets. In practice: a record that reappears *with* content changes surfaces as `updated` / `status_changed`, not `corrected`. ### Scope of coverage This path is watch-scoped, not a firehose: * You only receive events for marks that **match the watch's query** (its filters, offices, and — for similarity watches — score threshold). * Events are evaluated only for **watch-eligible sync runs**. Bulk backfills and historical re-ingestion runs are suppressed and never produce alerts. To widen coverage, widen the watch (or create more watches). `GET /v1/events` remains **Internal-tagged and held**, but it is populated: every `alert.created` event is projected, and `trademark.*` events are projected for marks in one of your organization's portfolios when the event projector is enabled. The surface may still change before its Internal tag is removed. On `GET /v1/events`, `portfolio_id` behaves exactly like a portfolio-scoped webhook endpoint: it membership-filters every family, `alert.created` included, against the snapshot taken when the event was recorded. ## Payload shape The envelope and `data` shape depend on the event type. ### `alert.created` ```json theme={null} { "type": "alert.created", "id": "evt_8kLm2nPq", "payload_version": 1, "timestamp": "2026-05-08T14:32:11.428Z", "data": { "event_id": "evt_8kLm2nPq", "alert_id": "alt_8kLm2nPq", "alert": { /* self-contained alert */ } } } ``` `data` contains `event_id`, `alert_id`, and the self-contained alert under `alert`. The SDK type is `AlertCreatedEvent` (`import type { AlertCreatedEvent } from '@signa-so/sdk'`). All IDs are prefixed and can be passed directly to the matching REST resources, no conversion needed. The body is self-contained: the snapshot, diff, watch name, deadline, and `customer_reference` are all inline, so you don't need to call back to the REST API to render an alert. ### `trademark.status_changed` This event uses the normal `evt_*` ID and `payload_version`. Its `data` is a trademark change, not an alert: ```json theme={null} { "type": "trademark.status_changed", "id": "evt_4fGh6jKl", "payload_version": 1, "timestamp": "2026-05-08T14:32:11.428Z", "data": { "event_id": "evt_4fGh6jKl", "trademark_id": "tm_9pQs3vNk", "office_code": "US", "jurisdiction_code": "US", "version": 12, "mark_text": "ACME ROCKETS", "status": "registered", "status_stage": "registered", "status_reason": null, "owner_name": "Aurora Digital, Inc.", "changed_fields": ["status", "status_stage"], "changes": { "status": { "before": "pending", "after": "registered" }, "status_stage": { "before": "examination", "after": "registered" } }, "portfolios": [ { "id": "ptf_9aB2xY", "external_ref": "MATTER-4471" }, { "id": "ptf_7cD4wV", "external_ref": null } ], "source_date": "2026-05-06", "occurred_at": "2026-05-08T14:30:00.000Z" } } ``` The SDK types are `TrademarkStatusChangedData` and `TrademarkStatusChangedEvent`. #### Field names in `changed_fields` and `changes` Both use the **public** field names of the trademark resource, not our internal column names, and both use the same keys. The trademark resource nests status under a `status` object while the event payload is flat, so every `status.` appears here as `status_` — except `status.primary`, which is `status`. Renames worth knowing: | Changed field | Trademark resource field | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `status` | `status.primary` | | `status_stage` | `status.stage` | | `status_reason` | `status.reason` | | `status_challenges` | `status.challenges` | | `status_effective_date`, `status_source`, `status_raw_code`, `status_raw_label` | the matching `status.*` field | | `mark_text` | `mark_text` | | `publication_date` | `publication_date` | | `ir_number` | `ir_number` | | `owners`, `attorneys`, `classifications`, `media`, `publications`, `priority_claims`, `filing_bases`, `design_codes`, `text_variants`, `statements`, `events`, `proceedings`, `relationships`, `coverage`, `office_extensions` | the matching collection changed; the entry is `{ "before": null, "after": "changed" }` | Fields we track internally but do not publish on the trademark resource are omitted from both lists rather than renamed, so `changed_fields` can be shorter than the number of columns that actually moved. Every name you see is a field you can read back from `GET /v1/trademarks/{id}`. The list is additive: new entries may appear, existing ones are not renamed within `payload_version: 1`. ### `office_action.issued` This event describes one newly stored `trademark_event` office-action row: ```json theme={null} { "type": "office_action.issued", "id": "evt_7mNo8pQr", "payload_version": 1, "timestamp": "2026-09-01T06:15:00.000Z", "data": { "event_id": "evt_7mNo8pQr", "trademark_id": "tm_9pQs3vNk", "office_code": "US", "event_date": "2026-08-29", "event_code": "NOA", "event_label": "Non-final Office Action", "scope": "right", "status_after_event": "examining", "nice_class_number": 9, "description": "Examiner issued a non-final refusal.", "source_identifier": "OA-90123456-20260829", "sequence_no": 17, "jurisdiction_code": "BR", "portfolios": [{ "id": "ptf_9aB2xY", "external_ref": "MATTER-4471" }], "source_date": "2026-08-29", "occurred_at": "2026-09-01T06:14:58.000Z" } } ``` Nullable source fields remain `null` when the office did not supply them. Office codes use the public ST.3 office vocabulary. `jurisdiction_code` is the shape-normalized designation jurisdiction (trimmed, uppercased, and limited to two letters); malformed values fail closed to `null` and are never routed through the office map. The SDK types are `OfficeActionIssuedData` and `OfficeActionIssuedEvent`. ### `occurred_at` is our clock, `source_date` is the office's Every `trademark.*` and `office_action.*` body carries both, and they answer different questions. `occurred_at` is **when Signa produced the event**. For these two families that is ingestion time: the moment we stored the change. (On `alert.created` the equivalent moment is when the alert was created — the meaning is per family, but it is always our clock, never the office's.) Nothing about it changed; it is just now written down. `source_date` is **the office-reported data date of the change as we stored it**, `YYYY-MM-DD`, or `null` when the feed reports none. On `office_action.issued` it equals `event_date`. The two differ by the office publication lag, typically 1 to 14 days. **Sort a docket by `source_date`** — meaningful within one office — **and use `occurred_at` to answer "when did I learn this".** Across offices you are comparing different kinds of date: it is day precision, and what it means varies by office (USPTO transaction date, WIPO gazette date, snapshot offices the date we crawled). **`source_date` is not a legal effective date.** For that, read `changes.status_effective_date`. We deliberately do not fold the effective date into `source_date`: an office re-crawl can backdate it by years, which would make your feed travel backwards on a mark that was merely re-read. When the effective date moves, it shows up in `changes` like any other field. ### `portfolios` — your reference, frozen at send time Both families carry `portfolios`: the portfolios that mark was in **when the event was recorded**, each with the `external_ref` you set on the membership. Empty when the mark was in none. `alert.created` does not carry it. ```json theme={null} "portfolios": [{ "id": "ptf_9aB2xY", "external_ref": "MATTER-4471" }] ``` This is a snapshot, not a live lookup. If you re-key a matter reference tomorrow, events you already received keep the value they were sent with — which is what makes a redelivery byte-identical to the original delivery, and keeps `GET /v1/events/{id}` in agreement with the webhook body you signed against. An `external_ref` of `null` means unknown or unset (including events recorded before we started snapshotting it), never "cleared". The SDK type is `EventPortfolioRef`. ### `webhook.test` Test deliveries deliberately use a `test_` ID, omit `payload_version`, and carry the stored test payload (normally `{ "type": "ping" }`): ```json theme={null} { "type": "webhook.test", "id": "test_4f6a8c2e19b07d31", "timestamp": "2026-05-08T14:32:11.428Z", "data": { "type": "ping" } } ``` Across all types, `id` equals the `webhook-id` header and `timestamp` is the signing-time ISO 8601 UTC instant. `payload_version` is present on versioned customer events and absent on `webhook.test`. ### `data.alert` fields | Field | Type | Description | | -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` (`alt_*`) | Prefixed alert ID. | | `object` | `'alert'` | Resource type discriminator. | | `schema_version` | `string` | Alert wire schema version (e.g. `'2026-06-01'`). | | `watch` | `{ id, name, type }` | The watch that fired: `id` (`wat_*`), human-readable `name`, and `type` (`mark` / `portfolio` / `owner` / `class` / `similarity`). | | `customer_reference` | `string \| null` | The watch's customer passthrough label, frozen at emit time. `null` when unset. See [Watches: `customer_reference`](/guides/monitoring/watches#customer_reference-your-own-label). | | `event` | `{ type, summary, diff[], diff_truncated? }` | Why the alert fired. `type` is the event slug; `summary` is a short human one-liner (e.g. `"Status changed: pending → registered"`); `diff` is an array of changed fields, each `{ path, op, from?, to? }` where `path` is a **public trademark field name** (see below); `diff_truncated` is `true` (and otherwise absent) when the diff was clipped to stay under the wire byte budget. | | `match` | `object \| null` | Match metadata (`reason`, `score`, `score_basis`) for similarity watches; `null` for pure-filter alerts. | | `trademark` | `object` | Inline snapshot of the matched mark: `id` (`tm_*`), `mark_text`, `mark_feature_type`, `office_code`, `status`, `filing_date`, `registration_date`, `nice_classes`, `owner_name`, `as_of`, and `links.self`. | | `deadline` | `{ severity, opposition_window_status, must_act_by }` | Severity ranking, opposition-window state, and ISO 8601 action deadline (fields are `null` when none apply). | | `timestamps` | `{ occurred_at, ingested_at?, created_at }` | When the source change occurred, when it was ingested (omitted when there's no linked change), and when the alert was created. | | `links` | `{ trademark, watch }` | Relative REST paths to the matched trademark and the watch. | #### Diff field names Every `path` is the name the field has on the [trademark resource](/api-reference/trademarks/retrieve-trademark) — `status`, `mark_text`, `publication_date`, `ir_number` — or, for a changed child collection, the collection name: `owners`, `attorneys`, `classifications`, `media`. Child-collection entries are opaque (`{ path, op: "changed" }`, no `from`/`to`); parent fields carry real `from`/`to` values. Status is flattened. The resource nests status under an object; the diff is flat, so `status.primary` is `status` and every other sub-field is `status_` (`status_stage`, `status_reason`, `status_challenges`, `status_effective_date`, `status_source`, `status_raw_code`, `status_raw_label`). Fields Signa tracks internally but does not publish never appear in the diff at all — they are dropped rather than renamed, so you can always look up a `path` in the trademark resource. Those drops happen *before* the diff size cap and are not truncation: `diff_truncated` still means only that entries or values were clipped to fit the wire budget. **Beta break, effective on deploy.** Until this change, `path` carried Signa's internal column names: `status_primary`, `mark_text_primary`, `publication_date_first`, `international_registration_number`, `challenge_states`, and raw child table names such as `trademark_owners`. They are now `status`, `mark_text`, `publication_date`, `ir_number`, `status_challenges` and `owners`. `event.summary` follows the same names, so the headline reads `"Status changed: …"` where it used to read `"Status primary changed: …"`. Redelivered and retried deliveries of alerts created BEFORE the change are re-emitted with the new names too — headline included, because it is re-derived from the translated diff — so a consumer only ever has to understand one vocabulary. The payload deliberately omits any tenant identifier. Each endpoint URL belongs to one organization, so the tenant is implicit in which endpoint received the delivery. ## Signing Every delivery is signed using HMAC-SHA256 per the [Standard Webhooks](https://www.standardwebhooks.com/) spec. Three signed headers, plus one unsigned attempt counter: | Header | Meaning | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `webhook-id` | Always equals the envelope `id`: `evt_*` for customer events and `test_*` for `webhook.test`. Stable across retries and redeliveries. Use this as your application-level idempotency key. | | `webhook-timestamp` | Unix seconds at delivery attempt time. Fresh on every retry. Reject deliveries older than 5 minutes (the SDK helper does this for you). | | `webhook-signature` | `v1,`. During rotation, two space-separated entries: `v1, v1,`. | | `webhook-attempt` | Delivery attempt number (1 = first delivery, 2 = first retry, up to 7). Not signed, see [Idempotency](#idempotency) for safe usage. | The body is canonicalized JSON (sorted keys, UTF-8, no trailing newline) before signing. Verify against the raw request bytes, not against a re-serialized version. If your framework parses, re-stringifies, or alters whitespace before your verifier sees the body, signature verification will fail. ## Verify signatures ### TypeScript / Node, SDK helper The `@signa-so/sdk` package exports a thin wrapper over [`standardwebhooks`](https://www.npmjs.com/package/standardwebhooks): ```ts theme={null} import express from 'express'; import { verifyWebhookSignature } from '@signa-so/sdk'; const app = express(); const SECRET = process.env.SIGNA_WEBHOOK_SECRET!; app.post('/signa-webhook', express.raw({ type: 'application/json' }), (req, res) => { const ok = verifyWebhookSignature( { 'webhook-id': req.header('webhook-id')!, 'webhook-timestamp': req.header('webhook-timestamp')!, 'webhook-signature': req.header('webhook-signature')!, }, req.body.toString('utf-8'), SECRET, ); if (!ok) return res.sendStatus(401); const event = JSON.parse(req.body.toString('utf-8')); console.log('alert received:', event.data.alert.id); res.sendStatus(200); }); ``` Returns `true` for a valid signature against `SECRET`, `false` on stale timestamps (more than 5 minutes of skew, enforced by the reference library), and accepts the rotation overlap (`v1, v1,`), verifying if either entry passes. The body MUST be the raw request bytes, no `JSON.parse` round trip first. Mismatched whitespace breaks the HMAC. ### TypeScript / Node, without the SDK ```ts theme={null} import { Webhook } from 'standardwebhooks'; const wh = new Webhook(process.env.SIGNA_WEBHOOK_SECRET!); try { wh.verify(rawBody, { 'webhook-id': req.header('webhook-id')!, 'webhook-timestamp': req.header('webhook-timestamp')!, 'webhook-signature': req.header('webhook-signature')!, }); } catch (err) { return res.sendStatus(401); } ``` ### Idempotency Use `webhook-id` (the value, not the body) as your application-level idempotency key. The same alert delivered twice (retry, redeliver, duplicate dispatch) carries the same `webhook-id`, so your business logic (creating tickets, sending notifications, writing to your own DB) just needs to check "have I processed this id?" Do not suppress deliveries at the infrastructure layer using either `webhook-id` alone or the tuple `(webhook-id, webhook-attempt)`. A stuck attempt can be re-POSTed in place during crash recovery with the same values for both headers, so either key can swallow a legitimate recovery delivery. Verify every request, pass it to your application, and make the business effect idempotent on `webhook-id`: ```typescript theme={null} const eventId = headers['webhook-id']; if (await eventsProcessed.has(eventId)) return; // business effect already committed await processAlert(body); await eventsProcessed.insert(eventId); ``` > **Security note:** `webhook-attempt` is not part of the signed envelope. Per the [Standard Webhooks spec](https://www.standardwebhooks.com/), only `webhook-id`, `webhook-timestamp`, and the body are signed. An attacker who replays a captured request can set any `webhook-attempt` value they like. Use it only for observability, never for deduplication or as input to a security decision. ### Common pitfalls * **Verify first, parse second.** Always validate the signature against the raw bytes before calling `JSON.parse`. Reject `401` on a failed verification and never touch the body. * **Re-serializing the body.** Verify against the bytes you received, not against `JSON.stringify(JSON.parse(body))`. Whitespace matters. * **Comma vs space in `webhook-signature`.** During rotation the header contains two entries separated by a single space. Some libraries split on commas, make sure yours follows the spec. * **Forgetting timestamp freshness.** A leaked secret plus a stale signature is replayable. The SDK helper enforces 5-minute skew automatically; if you roll your own, do the same. ## Retry policy Failed deliveries are retried with exponential backoff, 7 attempts total: | Attempt | Delay | | ------- | --------------- | | 1 | immediate | | 2 | +5s | | 3 | +25s | | 4 | +2 min | | 5 | +15 min | | 6 | +1 h | | 7 | +6 h (terminal) | Each delay carries plus or minus 20% jitter to spread retry bursts. A delivery is "failed" if the receiver returns 4xx/5xx, times out (5s connect, 10s read), or refuses TLS. After attempt 7 the delivery row's `status` is set to `exhausted` and Signa gives up. Delivery status values: | Status | Meaning | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `pending` | Queued or scheduled for retry. Not yet a final outcome. | | `delivered` | Receiver returned 2xx. | | `failed` | Last attempt failed but more retries remain. | | `exhausted` | All 7 attempts failed. Replay manually with [`POST /v1/webhooks/{id}/deliveries/{did}/redeliver`](/api-reference/monitoring/webhooks/redeliver). | ## Auto-disable An endpoint is disabled when either of two triggers fires: * **Consecutive failures.** When `consecutive_failures` reaches 100, the endpoint is disabled. * **Rolling failure rate.** Over the last 50 attempts, if the failure rate exceeds 50%, the endpoint is disabled. The rolling check only activates after 50 attempts, so a single failure on a brand-new endpoint will not disable it. A long-tail flaky endpoint can hit the rate trigger without ever hitting the consecutive count; a short hard-down outage can hit the consecutive count first. When an endpoint is disabled you'll see `status='disabled'` and a `disabled_reason` of `auto_consecutive_100`, `auto_failure_rate_50_over_50`, or `manual`. ### Re-enabling a disabled endpoint Re-enable a disabled endpoint with [`PATCH /v1/webhooks/{id}`](/api-reference/monitoring/webhooks/update) and `{"status": "active"}`. This is self-serve, you don't need to contact support. The same call also resets the auto-disable counters: `consecutive_failures` goes back to `0` and the rolling failure-rate window is cleared, and `disabled_at` / `disabled_reason` are wiped. A re-enabled endpoint therefore starts from a clean slate rather than re-disabling on its very next failed delivery. Before re-enabling, verify the receiver is healthy with [`POST /v1/webhooks/{id}/test`](/api-reference/monitoring/webhooks/test), test deliveries are free, are never retried, and never count toward auto-disable, so you can confirm the endpoint is back up without risking an immediate re-disable. ```bash theme={null} # 1. Confirm the receiver is healthy (test pings don't count toward auto-disable) curl -X POST "https://api.signa.so/v1/webhooks/whk_01HK.../test" \ -H "Authorization: Bearer $SIGNA_API_KEY" # 2. Re-enable, resets consecutive_failures + the failure-rate window curl -X PATCH "https://api.signa.so/v1/webhooks/whk_01HK..." \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status": "active"}' ``` ## Changing the URL To migrate an endpoint to a new URL (domain rename, infrastructure move), `PATCH` the endpoint with the new value: ```bash theme={null} curl -X PATCH "https://api.signa.so/v1/webhooks/whk_01HK..." \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: migrate-whk-url-2026-06-12" \ -d '{"url": "https://new.example.com/signa"}' ``` The signing secret is preserved. Future delivery attempts (including retries already scheduled) go to the new URL. Test the new URL with [`POST /v1/webhooks/{id}/test`](/api-reference/monitoring/webhooks/test) before relying on it, test deliveries are free and do not affect auto-disable counters. ## Rotation Call [`POST /v1/webhooks/{id}/rotate-secret`](/api-reference/monitoring/webhooks/rotate-secret) to roll the signing secret. For 24 hours both secrets are valid, Signa signs every delivery with both: ``` webhook-signature: v1, v1, ``` The reference Standard Webhooks library accepts either, so your verifier needs no changes during the overlap. Update your receiver to the new secret any time in the window. Calling `rotate-secret` again while the previous-secret window is still active returns `409`, so a second rotation can't silently invalidate the overlap window an in-flight receiver update depends on. ### Emergency force rotation For a suspected secret leak mid-overlap, pass `force=true` (in the request body or as `?force=true` on the URL): ```bash theme={null} curl -X POST "https://api.signa.so/v1/webhooks/whk_01HK.../rotate-secret?force=true" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: force-rotate-whk-2026-06-12" \ -d '{"reason": "Suspected secret leak, incident IR-2026-04-12"}' ``` Force rotation: * Skips the 24h overlap window (no `409`). * Immediately invalidates the previous secret. Any receiver still using it will fail signature verification on the next delivery. * Writes a `webhook.secret.force_rotated` audit log entry with the optional `reason`. Use force rotation only when the previous secret is known or suspected to be compromised. For routine rotations, wait for the overlap window to close. ## Redelivery If your receiver is down for a stretch and deliveries land in `status: "exhausted"`, replay them manually. The delivery ID is the `id` field from [`GET /v1/webhooks/{id}/deliveries`](/api-reference/monitoring/webhooks/list-deliveries), a raw UUID, not a prefixed ID: ```bash theme={null} curl -X POST "https://api.signa.so/v1/webhooks/whk_01HK.../deliveries/01890a91-7c2e-7f3a-b9d4-3e5f6a7b8c9d/redeliver" \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Idempotency-Key: redeliver-01890a91-2026-06-12" ``` Redelivery carries a fresh `webhook-timestamp` (so it passes freshness checks) but the same `webhook-id`, your idempotency-by-`webhook-id` logic continues to work. ## Replay from a checkpoint Retries cover a receiver that is briefly down. For a longer outage — or a bug that made you drop deliveries you acknowledged with a 200 — replay the [events feed](/api-reference/monitoring/events/list) instead. Every webhook you receive is also a row in your organization's event ledger, addressable by the same id: the `webhook-id` header IS the `evt_*` id. Persist the last event id you fully processed, then ask for everything after it, oldest first: ```bash theme={null} curl "https://api.signa.so/v1/events?sort=id&after=evt_3D7rZ9&limit=100" \ -H "Authorization: Bearer sig_YOUR_KEY" ``` `after` is an **exclusive** forward checkpoint and is valid only with `sort=id` (pairing it with `cursor`, or with the default `-id`, is a `400`). Walk pages with the returned `pagination.cursor` until `has_more` is `false`, and advance your stored checkpoint only after each page is durably handled — replay is idempotent by `evt_*`, so re-processing a page is safe. The ledger is retained **30 days**. A checkpoint older than that returns: ```json theme={null} { "error": { "type": "replay_window_expired", "message": "..." }, "request_id": "req_..." } ``` with HTTP `410`. Treat it as "your gap is wider than our history": drop the checkpoint, re-read the feed from the oldest available page (`?sort=id&limit=100`, no `after`), and reconcile the rest from [`POST /v1/alerts/lookup`](/api-reference/monitoring/alerts/lookup) or a [`GET /v1/alerts`](/api-reference/monitoring/alerts/list) sweep — alerts are kept for 90 days, three times the event window. The events feed is **beta**: the ledger, the ids and the replay rail are stable enough to build a recovery path on, but the per-event `data` payloads are versioned by `payload_version` and their shape is not yet promised. ## URL requirements Production endpoints must be public HTTPS URLs. Localhost, private network addresses, and link-local IPs are rejected at create time and at delivery time. To test locally, expose your receiver through a public tunnel (ngrok, Cloudflare Tunnel) and register that URL. ## Testing deliveries before you have a receiver You don't need production infrastructure to see a real signed delivery. 1. **Local receiver behind a tunnel.** Run your handler locally (say, on port 4000), expose it with `ngrok http 4000` or `cloudflared tunnel`, register the tunnel URL via [`POST /v1/webhooks`](/api-reference/monitoring/webhooks/create), and store the returned secret in your local env. Trigger a test delivery with [`POST /v1/webhooks/{id}/test`](/api-reference/monitoring/webhooks/test), the envelope shape matches `alert.created`, so your verifier exercises the same path it will in production. Test deliveries are free and do not count toward auto-disable. 2. **Request-bin style.** Point a temporary endpoint at any HTTPS request inspector (a webhook.site-style bin or your own one-file server behind a tunnel), register it, and fire a synthetic ping. You'll see the full envelope plus the `webhook-id` / `webhook-timestamp` / `webhook-signature` headers, everything you need to develop your verifier against real bytes. When you ship to production, `PATCH` the endpoint with the production URL (see [Changing the URL](#changing-the-url)), the same secret keeps working. Anything you send to a third-party request bin is visible to that service. Use pattern 2 only with the synthetic `webhook.test` ping, not with real alert traffic. ## Cost Webhook deliveries are billed per successful delivery and per redelivery. Test deliveries are free. # Portfolios Source: https://docs.signa.so/guides/portfolios Organize trademark records into folders with your own docketing references Portfolios are organization-owned folders of Signa trademark records. Use them to group a client's marks, attach your own membership references, and request computed deadlines. They do not track matters, tasks, or completion status. ## Limits The current configured allowances are: | Plan | Portfolios | Distinct managed marks across all portfolios | | ---------- | ---------- | -------------------------------------------- | | Free | 3 | 500 | | Beta | 20 | 5,000 | | Starter | 10 | 5,000 | | Pro | 50 | 50,000 | | Enterprise | Unlimited | Unlimited | Removing a trademark from your last portfolio that contains it releases its managed-mark slot. Exceeding either allowance returns 409 `resource_quota_exceeded`, with `quota_scope` identifying `portfolios` or `managed_marks`. Add and remove requests accept **at most 100 entries**. The per-portfolio deadline cap above applies to both the JSON list and the calendar download, and holds even on plans with larger managed-marks allowances; exceeding it returns 422 `portfolio_too_large`. Split large portfolios; date/type filters do not reduce the mark count for this cap. ## Membership references When [adding marks](/api-reference/portfolios/add-trademarks), use `items` to attach an optional `external_ref` to each membership. It is trimmed, 1–255 characters, case-sensitive, and unique within that portfolio. A trademark in two portfolios can carry a different reference in each. A reference can be attached to a membership that has none. Once bound, changing it or reusing it for another mark returns 409 `external_ref_conflict`; the whole add batch rolls back. Remove the membership and re-add it to bind a different reference. Look up a reference with [Retrieve Portfolio](/api-reference/portfolios/retrieve) using `?external_ref=...`, or [remove memberships](/api-reference/portfolios/remove-trademarks) by sending `external_refs`. Unknown references return an empty membership result or remove nothing. References cannot create placeholder marks outside Signa's records. ## Workflow 1. [Create a portfolio](/api-reference/portfolios/create). 2. [Add trademark IDs and references](/api-reference/portfolios/add-trademarks). 3. [Retrieve its members](/api-reference/portfolios/retrieve) or [list computed deadlines](/api-reference/portfolios/deadlines). 4. Optionally [download a calendar file](/api-reference/portfolios/deadlines-ics) and import it into your calendar. The bearer-authenticated URL is a download; calendar subscriptions and automatic synchronization are unavailable. Every POST, PATCH, and DELETE requires an `Idempotency-Key`. Retry the same target and body with the same key to replay the response for 24 hours. Reusing the key with a different target or body returns 409 `conflict`. # Resilience Patterns Source: https://docs.signa.so/guides/resilience Build fault-tolerant integrations with retry logic and circuit breakers Network failures, rate limits, and transient server errors are facts of life in distributed systems. This guide covers patterns for handling them gracefully when integrating with the Signa API. ## Transient vs. Permanent Failures Before retrying, determine whether the failure is recoverable. | Status Code | Type | Action | | ----------- | --------- | -------------------------------------------------------------------- | | `429` | Transient | Rate limited. Retry after the `Retry-After` header value. | | `500` | Transient | Internal server error. Retry with exponential backoff. | | `502` | Transient | Bad gateway. Retry with backoff (typically resolves within seconds). | | `503` | Transient | Service temporarily unavailable. Retry with longer backoff. | | `504` | Transient | Gateway timeout. Retry with backoff. | | `400` | Permanent | Validation error. Fix the request payload before resending. | | `401` | Permanent | Invalid or expired API key. Check your credentials. | | `403` | Permanent | Insufficient scopes. Update your API key permissions. | | `404` | Permanent | Resource not found. Verify the ID is correct. | | `409` | Permanent | Conflict (e.g., duplicate create). Inspect the error body. | | `410` | Permanent | Entity was merged. Follow the `merged_into` in the response. | | `422` | Permanent | Semantic error. The request is well-formed but cannot be processed. | Only retry on transient failures (4xx rate limits and 5xx server errors). Retrying permanent failures wastes your rate limit budget and will never succeed. *** ## Exponential Backoff with Jitter The standard retry strategy for transient errors. Each retry waits longer than the previous one, with random jitter to avoid thundering-herd problems when many clients retry simultaneously. **Algorithm:** ``` delay = min(max_delay, base_delay * 2^attempt) + random(0, jitter) ``` ```typescript TypeScript theme={null} interface RetryOptions { maxRetries?: number; baseDelayMs?: number; maxDelayMs?: number; } async function fetchWithRetry( url: string, options: RequestInit, { maxRetries = 3, baseDelayMs = 1000, maxDelayMs = 60_000 }: RetryOptions = {} ): Promise { for (let attempt = 0; attempt <= maxRetries; attempt++) { const response = await fetch(url, options); // Do not retry permanent failures if (response.status >= 400 && response.status < 500 && response.status !== 429) { return response; } // Success, return immediately if (response.ok) { return response; } // All retries exhausted if (attempt === maxRetries) { return response; } // Calculate delay let delayMs: number; if (response.status === 429) { // Prefer the server's Retry-After value const retryAfter = response.headers.get('Retry-After'); delayMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : baseDelayMs * 2 ** attempt; } else { delayMs = baseDelayMs * 2 ** attempt; } // Cap at max delay, add jitter delayMs = Math.min(delayMs, maxDelayMs); delayMs += Math.random() * baseDelayMs; await new Promise((resolve) => setTimeout(resolve, delayMs)); } // Unreachable, but satisfies TypeScript throw new Error('Retry loop exited unexpectedly'); } ``` *** ## Retry Backoff Schedule With the default settings (`base_delay=1s`, `max_retries=3`), the schedule looks like this: | Attempt | Base Delay | With Jitter (approx.) | Cumulative Wait | | ------- | ---------- | --------------------- | --------------- | | 1 | 1 s | 1.0 to 2.0 s | \~1.5 s | | 2 | 2 s | 2.0 to 3.0 s | \~4 s | | 3 | 4 s | 4.0 to 5.0 s | \~8.5 s | For `429` responses, the `Retry-After` header overrides the calculated base delay. Always respect this value. The Signa TypeScript SDK (`@signa-so/sdk`) has built-in retry logic with these defaults. If you are using the SDK, you get this behavior automatically. *** ## Bulk Operation Retry [`POST /v1/trademarks/batch`](/api-reference/trademarks/batch-trademarks) does not return a per-item success or error status. A requested ID either resolves into the `data` array (as a full trademark) or, if it does not match anything, into the `not_found` array. There is no partial-item failure to retry inside a single batch call, since a lookup either finds a record or it does not. What can still fail is the *request as a whole*: a `429` if you are over your rate limit, or a `5xx` if the API is having trouble. In that case, retry the whole batch with the standard backoff strategy from the section above: ```typescript TypeScript theme={null} import { Signa } from '@signa-so/sdk'; const signa = new Signa({ api_key: 'sig_YOUR_KEY' }); async function fetchBatchWithRetry(ids: string[], maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const result = await signa.trademarks.batch({ ids }); if (result.not_found.length > 0) { console.warn('Not found:', result.not_found); } return result.data; } catch (err) { if (attempt === maxRetries) throw err; const delay = 1000 * 2 ** attempt + Math.random() * 1000; await new Promise((resolve) => setTimeout(resolve, delay)); } } throw new Error('Retry loop exited unexpectedly'); } ``` See [Bulk Operations](/guides/bulk-operations) for chunking large ID lists across multiple batch calls and staying under your rate limit while doing it. *** ## Circuit Breaker Pattern For high-throughput integrations, wrap your API calls in a circuit breaker to stop sending requests when the API is consistently failing. This protects both your application and the API from cascading failures. The circuit has three states: * **Closed** (normal): Requests flow through. Failures are counted. * **Open** (tripped): All requests fail immediately without contacting the API. * **Half-open** (probing): A single test request is sent. If it succeeds, the circuit closes; if it fails, it re-opens. ```typescript theme={null} class CircuitBreaker { private state: 'closed' | 'open' | 'half-open' = 'closed'; private failureCount = 0; private lastFailureTime = 0; constructor( private readonly failureThreshold: number = 5, private readonly resetTimeoutMs: number = 30_000 ) {} async execute(fn: () => Promise): Promise { if (this.state === 'open') { // Check if enough time has passed to try again if (Date.now() - this.lastFailureTime >= this.resetTimeoutMs) { this.state = 'half-open'; } else { throw new Error('Circuit breaker is open, request blocked'); } } try { const result = await fn(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } private onSuccess(): void { this.failureCount = 0; this.state = 'closed'; } private onFailure(): void { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { this.state = 'open'; } } } // Usage const breaker = new CircuitBreaker(5, 30_000); async function getTrademarkSafe(id: string) { return breaker.execute(() => fetchWithRetry(`https://api.signa.so/v1/trademarks/${id}`, { headers: { Authorization: 'Bearer sig_YOUR_KEY' }, }) ); } ``` A circuit breaker should wrap your retry logic, not replace it. The retry function handles transient blips; the circuit breaker prevents sustained outages from overwhelming your application. *** ## Request Timeouts Always set explicit timeouts on API calls. A reasonable default for Signa endpoints: | Endpoint Type | Recommended Timeout | | ---------------------------------------------- | ------------------- | | Single resource (`GET /v1/trademarks/:id`) | 10 s | | List / search (`GET` or `POST /v1/trademarks`) | 15 s | | Batch (`POST /v1/trademarks/batch`) | 30 s | ```typescript TypeScript theme={null} const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10_000); try { const response = await fetch('https://api.signa.so/v1/trademarks/tm_abc123', { headers: { Authorization: 'Bearer sig_YOUR_KEY' }, signal: controller.signal, }); } finally { clearTimeout(timeout); } ``` ```bash cURL theme={null} curl --max-time 10 https://api.signa.so/v1/trademarks/tm_abc123 \ -H "Authorization: Bearer sig_YOUR_KEY" ``` *** ## Idempotent Requests Mutation endpoints (`PATCH`, `DELETE`, and non-exempt `POST`, see [Exempt Endpoints](#exempt-endpoints)) require an `Idempotency-Key` header. This guarantees that if a request is retried, whether because of a network timeout, a dropped connection, or an ambiguous failure, the operation only executes once. ```bash theme={null} curl -X PATCH https://api.signa.so/v1/organization/api-keys/key_abc123 \ -H "Authorization: Bearer $SIGNA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: rename-prod-key-001" \ -d '{"name": "Production key (renamed)"}' ``` ### How It Works 1. On the first request with a given key, the API processes the request normally and caches the response. 2. If the same key is sent again with the **same request body**, the API returns the cached response without re-executing the operation. Replays carry an `Idempotent-Replayed: true` response header and preserve the original `request_id` (in the body and on `x-request-id`), so you can tell a cached replay from a fresh execution. 3. If the same key is sent with a **different body**, the API returns `409 conflict`, each key is bound to a specific request body. 4. If the same key is sent while the **first request is still in flight**, the API returns `409 idempotency_processing`, wait for the original to complete, then retry with the same key to get its cached result. Cached responses are stored for **24 hours**, after which the key can be reused. **Only successful (2xx) responses are cached.** Client errors (4xx) and server errors (5xx) are never cached, so if a request fails, you can safely retry with the same idempotency key without being pinned to the error. ### Key Format * 1-255 characters * Alphanumeric, dashes, and underscores only (`[a-zA-Z0-9_-]`) * Must be unique per operation, use UUIDs, request identifiers, or a deterministic string derived from the operation (e.g., `create-api-key-{name}-{timestamp}`) ### Exempt Endpoints Read-shaped POST endpoints are **exempt by design** from the `Idempotency-Key` requirement. They exist as POSTs only because their query bodies are too large or complex for a URL, and they create nothing. Sending a key is harmless (the value's **format is still validated**, a malformed key returns `400` even here), but the middleware will not enforce or replay it. | Endpoint | Why exempt | | --------------------------------------------------------------------- | ----------------------------------------------------- | | `POST /v1/trademarks` | Read-only search, inherently safe to retry | | `POST /v1/trademarks/batch` | Read-only batch lookup, inherently safe to retry | | `POST /v1/classifications/suggest`, `POST /v1/goods-services/suggest` | Read-only suggestions, inherently safe to retry | | `POST /v1/reconcile` | Read-only diff against register data, creates nothing | | `POST /v1/deadlines/compute` | Read-only computation, creates nothing | | `POST /v1/watches/preview` | Dry-run match count, creates nothing | | `POST /v1/alerts/lookup` | Bulk read by IDs (polling pattern), creates nothing | `POST /v1/organization/api-keys` and `POST /v1/organization/api-keys/{id}/rotate` are **not** exempt, even though they return one-time secrets. Idempotency replay is exactly what you want there: a retry with the same key returns the **same** secret instead of minting a second credential. The cached secret is only ever served to the caller that supplied the original `Idempotency-Key`. Every other mutating endpoint (`PATCH`, `DELETE`, and any non-exempt `POST`) requires `Idempotency-Key` and will return `400 validation_error` if it is missing. ### Example: Safe Retry Pattern The example below demonstrates a retry loop against `PATCH /v1/organization/api-keys/{id}` (renaming a key), which **is** enforced by the idempotency middleware. ```typescript TypeScript theme={null} import { randomUUID } from 'crypto'; async function renameApiKeySafe(keyId: string, name: string, maxRetries = 2): Promise { 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.