> ## Documentation Index
> Fetch the complete documentation index at: https://docs.signa.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating to the provenance release

> Upgrade your integration and the TypeScript SDK from 0.13 to 0.14, with before and after examples

The provenance release shipped on September 27, 2026, in one deploy. It is a
breaking release: update your integration and `@signa-so/sdk` 0.14.0 together.
New webhook events are `payload_version` 2 from the same day; events recorded
before then stay v1 ([details](#webhooks-and-the-events-feed)).

The [changelog entry](/changelog) lists every change. This guide walks through
them with before and after examples.

## The idea in one paragraph

Every value now says who produced it. Top-level fields are the office's own
values, `null` when the office published none. Everything Signa computes lives
under `derived`. The office's verbatim codes sit in `raw {code, label}`, and
`status.basis` says how a status was reached. A search hit, a list row, a
portfolio row and a screening hit are the same row, and the detail extends it.

## The five changes most integrations need

| # | Before                                                                            | After                                                                                          | Why it matters                                                            |
| - | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| 1 | `expiry_date` filled for USPTO records                                            | `expiry_date: null` on every USPTO record; the term end is `derived.expiry_date`               | The USPTO publishes no expiry date; the old value was Signa's computation |
| 2 | `derived.deadlines[]` with `grace_expiry`, `recurring`, `optional`, `name`        | `derived.deadlines.items[]` with `grace_expiry_date`, `is_recurring`, `is_optional`, no `name` | Deadline code reads the wrong key                                         |
| 3 | `owners[].entity_id` always set (an `ent_<owner-uuid>` placeholder when unlinked) | The linked entity, or `null`; placeholder ids are `404` on entity routes                       | Grouping by `entity_id`, entity lookups                                   |
| 4 | `us_register_type`, `renewal_due_before`, `opposition_closes_before` and `_after` | `register_type`, `renewal_due_date_lt`, `opposition_closes_lte` and `_gte`                     | Old names are now a `400`                                                 |
| 5 | Cursors from before keep working                                                  | Cursors minted before the release return `400 cursor_invalid`                                  | Long paginated walks restart once                                         |

Also: `screening_id` values change once, ETags change once (one `200` per
cached validator), and webhook handlers must accept `payload_version: 2`.

## Trademark rows

These changes apply everywhere a trademark row appears: `GET` and
`POST /v1/trademarks`, `GET /v1/trademarks/{id}`, `POST /v1/trademarks/batch`,
the owner, attorney, firm and entity trademark lists, `GET /v1/portfolios/{id}`
rows, `POST /v1/watches/preview`, image search, and screening hits
(`trademark`).

### Status

```jsonc theme={null}
// before
"status": { "primary": "active", "stage": "registered", "reason": null, "challenges": [],
            "effective_date": "2016-11-01", "source": "explicit", "raw_code": "800", "raw_label": null }
// after
"status": { "primary": "active", "stage": "registered", "reason": null, "challenges": [],
            "effective_date": "2016-11-01", "basis": "office", "raw": { "code": "800", "label": null } }
```

| Before                                                                       | After                                                                                                                                                                                                   |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status.source`: `explicit`, `event_derived`, `dispatch_derived`, `computed` | `status.basis`: `office`, `event_derived`, `dispatch_derived`, `computed` (`explicit` is now `office`)                                                                                                  |
| `status.raw_code`, `status.raw_label`                                        | `status.raw: {code, label}` or `null` (`null` on IR family rows)                                                                                                                                        |
| `raw_label` could be the code itself or wording Signa wrote                  | `raw.label` is the office's verbatim label, or `null` when we hold none for the code (for example USPTO codes today). Decode codes with [List Status Codes](/api-reference/reference/list-status-codes) |

### Dates

```jsonc theme={null}
// before (a USPTO registration renewed in 2017)
"expiry_date": "2027-11-01", "expiry_date_basis": "derived",
"renewal_due_date": "2027-11-01", "renewal_due_date_basis": "derived"
// after
"expiry_date": null, "renewal_due_date": null, "last_renewal_date": "2017-10-30",
"derived": { "expiry_date": "2027-11-01", "...": "..." }
```

* `expiry_date` and `renewal_due_date` are office-published only.
* `expiry_date_basis` and `renewal_due_date_basis` are removed.
* `last_renewal_date` is new: the last renewal the office recorded (USPTO, CIPO
  and OEPM today). In every jurisdiction the rulebook treats the renewal cycle
  it paid, and every earlier cycle, as paid, so an early-renewed registration
  no longer lists the paid cycle as due. `last_renewal_date` on a
  `POST /v1/deadlines/compute` item works the same way (it used to be read for
  Canada only).
* A paid cycle is not a missed one either. `urgency=missed` on
  `GET /v1/deadlines` and portfolio deadlines, and `include_missed: true` on
  `POST /v1/deadlines/compute`, no longer list a cycle before the office-stated
  expiry, the cycle a recorded renewal paid, or the restoration window such a
  cycle would open.
* The expiry to show is `expiry_date ?? derived.expiry_date`.
  `derived.expiry_date` is the term end before any weekend or holiday roll, so
  the filing deadline (`due_date`) can fall a day or two later.
* An IR's `expiry_date` is the expiry WIPO published on its designations.
  Known limitation: the search index does not hold it on the IR's own record
  yet, so `expiry_date_*` does not match the IR's own record and
  `sort=expiry_date` orders it with rows that have no office expiry. The next
  index rebuild fixes this.

### The `derived` block

`derived` is now on every row, search hits included (it used to be on the
detail only).

```jsonc theme={null}
// before (detail only)
"derived": {
  "ruleset_version": "2026-09-02",
  "deadlines_supported": true,
  "deadlines_unsupported_reason": null,
  "deadlines_unsupported_rules": [],
  "deadlines": [
    { "rule_id": "us_renewal_s9", "type": "combined_renewal_and_use",
      "name": "Section 8+9 Renewal + Declaration of Use", "jurisdiction_code": "US",
      "trigger_date": "2016-11-01", "trigger_field": "registration_date",
      "window_opens": "2025-11-01", "due_date": "2026-11-02", "grace_expiry": "2027-05-03",
      "recurring": true, "optional": false, "consequence_if_missed": "cancellation_and_expiration" }
  ],
  "opposition_window": null
}
// after (detail and every row)
"derived": {
  "ruleset_version": "rs_3f2a9c1b7e40",
  "as_of_date": "2026-10-02",
  "expiry_date": "2026-11-01",
  "next_deadline": { "occurrence_key": "us_renewal_s9:1", "...": "the first row whose grace period has not passed" },
  "deadlines": {
    "supported": true,
    "reason": null,
    "unsupported_rules": [],
    "items": [
      { "occurrence_key": "us_renewal_s9:1", "rule_id": "us_renewal_s9",
        "type": "combined_renewal_and_use", "jurisdiction_code": "US",
        "trigger_date": "2016-11-01", "trigger_field": "registration_date",
        "window_opens": "2025-11-01", "due_date": "2026-11-02", "due_date_adjustment": "moved",
        "grace_expiry_date": "2027-05-03", "grace_expiry_adjustment": "moved",
        "holiday_calendar": "uspto_dc", "is_recurring": true, "is_optional": false,
        "consequence_if_missed": "cancellation_and_expiration" }
    ]
  },
  "opposition_window": { "supported": false, "reason": "stage_not_opposable", "rule_id": "us_opposition" }
}
```

| Before                                                                | After                                                                                                                                                  |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `derived.ruleset_version` was a date                                  | An `rs_` content hash                                                                                                                                  |
| n/a                                                                   | `derived.as_of_date`, `derived.expiry_date`, `derived.next_deadline` (the first row of `deadlines.items` whose grace period has not passed, or `null`) |
| `derived.deadlines_supported`                                         | `derived.deadlines.supported`                                                                                                                          |
| `derived.deadlines_unsupported_reason`                                | `derived.deadlines.reason`                                                                                                                             |
| `derived.deadlines_unsupported_rules`                                 | `derived.deadlines.unsupported_rules`                                                                                                                  |
| `derived.deadlines[]`                                                 | `derived.deadlines.items[]` (rows within 20 years of `as_of_date`)                                                                                     |
| Row `name`                                                            | Removed (use `type` or `rule_id`)                                                                                                                      |
| Row `grace_expiry`                                                    | `grace_expiry_date`                                                                                                                                    |
| Row `recurring`, `optional`                                           | `is_recurring`, `is_optional`                                                                                                                          |
| n/a                                                                   | Row `occurrence_key`, `due_date_adjustment`, `grace_expiry_adjustment`, `holiday_calendar`                                                             |
| Madrid rows `jurisdiction_code: "WIPO"`                               | `"WO"` (also on `GET /v1/deadline-rules`, whose `jurisdiction` filter still accepts `WIPO` as an alias)                                                |
| Search row top-level `opposition_window`                              | Removed; use `derived.opposition_window`                                                                                                               |
| Detail `opposition_window` with null dates, or `null`; `rule_version` | Always present and two-state (below); `rule_version` removed                                                                                           |

`occurrence_key` identifies one deadline occurrence within one trademark across
reads: `rule_id:<cycle>`, or `rule_id:t<term>-<n>` when the record has no
statutory anchor date or the row is anchored on an office-stated expiry. A
one-off rule is `rule_id:1`.

`derived.opposition_window` is one of two shapes:

```jsonc theme={null}
// supported
{ "supported": true, "rule_id": "...", "trigger_event": "...", "publication_date": "...",
  "window_opens": "...", "window_closes": "...", "close_adjustment": "...",
  "office_time_zone": "...", "common_extension": "...", "reason": null }
// not supported
{ "supported": false, "reason": "stage_not_opposable", "rule_id": "us_opposition" }
```

Unsupported reasons are `unsupported_office`, `requires_office_date`,
`publication_date_kind_mismatch` and `stage_not_opposable`.

### Identity, office record and grain

| Before                                                                      | After                                                                                                                                                                                                  |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `source_primary_id`                                                         | `office_record_id` (the office's own id) and `office_url`. `office_record_id` is `null` only when Signa composed the key: an IR's own record and its WIPO-feed, Finnish and Polish designation records |
| IR `registration_number: "WO921642"`                                        | `"921642"` (the `registration_number` filter matches the unprefixed number)                                                                                                                            |
| n/a                                                                         | `grain: "mark"` or `"record"`                                                                                                                                                                          |
| `coverage`, `source_records`, `owners_mixed` were `null` on non-family rows | Absent except on IR family rows                                                                                                                                                                        |
| Row `office_updated_at`                                                     | Removed (the detail keeps `provenance.office_updated_at`, and gains `provenance.snapshot_date`)                                                                                                        |
| `territory_matches[].basis`                                                 | `territory_matches[].matched_via_kind`                                                                                                                                                                 |
| Detail `jurisdiction_code` always set                                       | Nullable, as on the row                                                                                                                                                                                |

Coverage territories:

```jsonc theme={null}
// before
{ "territory": "JP", "primary": "active", "stage": "registered", "trademark_id": "tm_2wX5yZ1a" }
// after
{ "territory_code": "JP", "status": { "primary": "active", "stage": "registered" },
  "trademark": { "id": "tm_2wX5yZ1a", "href": "/v1/trademarks/tm_2wX5yZ1a" },
  "office_url": "https://www3.wipo.int/madrid/monitor/en/?id=1654321_JP" }
```

The IR's own `WO` record is no longer listed as a territory, so
`territory_count` counts designations. IR family status is
`{primary: "mixed", reason: null, basis: "computed", raw: null}` (it was
`reason: "mixed"`, `source: "computed"`).

### Owners, attorneys, addresses, register, classes and design codes

```jsonc theme={null}
// before
{ "id": "own_R3jK9mN2", "name": "Nike, Inc.", "country_code": "US", "entity_type": "corporation",
  "role": "owner", "entity_id": "ent_019f34d6", "entity_id_type": "derived",
  "address": { "lines": ["One Bowerman Drive"], "city": "BEAVERTON", "state": "OR",
               "postal_code": "97005", "country_code": "US" } }
// after
{ "id": "own_R3jK9mN2", "name": "Nike, Inc.", "country_code": "US", "legal_form": "corporation",
  "entity_id": null, "role": "owner", "is_current": true,
  "address": { "lines": ["One Bowerman Drive"], "city": "BEAVERTON", "region": "OR",
               "postal_code": "97005", "country_code": "US", "formatted": null } }
```

| Before                                                                                                | After                                                                                                                                                                                                                                                                                                                  |
| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `owners[].entity_type`                                                                                | `owners[].legal_form`                                                                                                                                                                                                                                                                                                  |
| `owners[].entity_id` always set                                                                       | The linked entity, or `null`                                                                                                                                                                                                                                                                                           |
| `owners[].entity_id_type`                                                                             | Removed                                                                                                                                                                                                                                                                                                                |
| n/a                                                                                                   | Detail `owners[].is_current`                                                                                                                                                                                                                                                                                           |
| Address `state`; the shape varied by office                                                           | One address shape everywhere: `{lines[], city, region, postal_code, country_code, formatted}`. Lines are verbatim, and components appear only when the office supplied them                                                                                                                                            |
| Attorney `firm_id` and `firm_name` were the attorney's current firm                                   | The firm of record on the filing when known, else the attorney's current firm; `firm_name` is the name as filed when stated. The `firm_id` filter and facet still match the attorney's current firm, so about 5% of records can be returned for a different firm than their detail shows, until the next index rebuild |
| Detail `us_register {register, incontestable, standard_characters_claimed, acquired_distinctiveness}` | `register {type, is_incontestable, standard_characters_claimed, acquired_distinctiveness}` or `null`, on every row and for every office that states one                                                                                                                                                                |
| `classifications[].class_status_raw: "6"`                                                             | `classifications[].status_raw: {code: "6", label: null}`                                                                                                                                                                                                                                                               |
| `design_codes[] {code_system, code_value, edition}`                                                   | `{system, code, edition}`, with `system` one of `vienna`, `us_design_search`, `other`                                                                                                                                                                                                                                  |
| `filing_bases[].nice_class_number`                                                                    | `filing_bases[].nice_class`                                                                                                                                                                                                                                                                                            |
| `classifications[].scope` on grouped rows                                                             | Only on IR family rows                                                                                                                                                                                                                                                                                                 |
| `classifications[].nice_class` always a number                                                        | Nullable (a class the office stated without a Nice number)                                                                                                                                                                                                                                                             |

## Search and filters

| Before                                                                                                             | After                                                                                                                                                                                                                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `renewal_due_before` (`GET` and `POST` top level)                                                                  | Removed (`400`); use `renewal_due_date_lt` or `filters.renewal_due_date.lt`                                                                                                                                                                                                                                                                         |
| `us_register_type`                                                                                                 | `register_type` (any office register, case-insensitive)                                                                                                                                                                                                                                                                                             |
| `opposition_closes_before` and `_after`                                                                            | `opposition_closes_lte` and `_gte`                                                                                                                                                                                                                                                                                                                  |
| `expiry_date_*`, `renewal_due_date_*` and `sort=expiry_date` or `renewal_due_date` included Signa-computed values  | Office-published values only. USPTO records never match `expiry_date_*` (a WIPO designation for the US can, when WIPO published an expiry). An IR's own record does not match yet (see [Dates](#dates))                                                                                                                                             |
| `search_meta.fallback_reason`                                                                                      | Removed; a `search_meta.warnings[]` entry `{code: "expanded_fallback", affected_filters}` (or `affected_option`)                                                                                                                                                                                                                                    |
| n/a                                                                                                                | `search_meta.index_generation`. Cursors are bound to it: `400 cursor_invalid` once at the release, and after any future index rebuild                                                                                                                                                                                                               |
| A relevance-ranked or sorted walk ended silently at 10,000 rows                                                    | The last page inside the window carries `{code: "result_window_reached", severity: "info"}` when more rows match; never on a `sort=family_updated_at` browse                                                                                                                                                                                        |
| n/a                                                                                                                | `family_updated_at_gte`, `_gt`, `_lte`, `_lt` and `sort=family_updated_at` (grouped view only). Filter-only walks sorted on it page past 10,000 rows                                                                                                                                                                                                |
| Grouped view: `offices` or `filing_route` with another filter, or any date filter, fell back to one row per record | Per-leg filters (`offices`, `office`, `filing_route`, `filing_date_*`, `registration_date_*`, `expiry_date_*`, `registration_number`) stay grouped alone or together, and must hold on one record. They match the leg shown in `coverage.territories[]`; a dual-feed leg not shown does not match. Combined with other filters they still fall back |
| `entity_id=ent_<owner-uuid>` returned that owner's marks                                                           | An empty page; use `owner_id`                                                                                                                                                                                                                                                                                                                       |
| `entity_id` over 10,000 member owners: `422 entity_too_large`                                                      | No member cap on trademark search (`entity_group` keeps its family-walk bound)                                                                                                                                                                                                                                                                      |
| `opposition_status` and `opposition_closes_*` matched at any stage                                                 | They apply only at a stage the mark can be opposed at (the gate behind `stage_not_opposable`)                                                                                                                                                                                                                                                       |
| A page whose stored data could not be loaded was served degraded                                                   | A retryable `503 service_unavailable` with `Retry-After: 5`                                                                                                                                                                                                                                                                                         |

```bash theme={null}
# before
curl "https://api.signa.so/v1/trademarks?offices=US&us_register_type=supplemental&renewal_due_before=2027-01-01" \
  -H "Authorization: Bearer $SIGNA_API_KEY"

# after
curl "https://api.signa.so/v1/trademarks?offices=US&register_type=supplemental&renewal_due_date_lt=2027-01-01" \
  -H "Authorization: Bearer $SIGNA_API_KEY"
```

With the second call, USPTO records return nothing for the renewal filter,
because the USPTO publishes no renewal due date. For computed deadlines, use
`GET /v1/deadlines?portfolio_id=...&due_before=2027-01-01`.

For incremental sync with `sort=family_updated_at`, see
[Pagination](/api-reference/pagination#incremental-sync).

## Other endpoints

### Batch

`POST /v1/trademarks/batch` identifiers name their office with `office_code`:

```jsonc theme={null}
// before
{ "identifiers": [{ "application_number": "97123456", "office": "US" }] }
// after
{ "identifiers": [{ "application_number": "97123456", "office_code": "US" }] }
```

`office` is now a `400`. `data` answers every submitted item in input order,
and a repeated id repeats. `not_found` echoes unmatched items in input order.

### Trademark events

`GET /v1/trademarks/{id}/events`:

```jsonc theme={null}
// before
{ "id": "evt_550e8400", "object": "event", "event_type": "registration",
  "raw_code": "RN", "raw_label": "REGISTERED", "description": "REGISTERED",
  "nice_class_number": null, "sequence_no": 1 }
// after
{ "id": "hst_550e8400", "object": "trademark_event", "event_type": "registration",
  "raw": { "code": "RN", "label": "REGISTERED" }, "description": null,
  "nice_class": null, "sequence_number": 1 }
```

`description` is `null` unless it adds office text to `raw.label`. The
organization feed `GET /v1/events` keeps `object: "event"` and `evt_` ids.

USPTO event types change for two groups of entries. Stored history is retyped
after the release, with no webhook or alert:

| USPTO entry                                                         | Before                                                                                 | After                                                            |
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Notice of suit (`NOSU`)                                             | `office_action` (matched `event_type=office_action` and raised `office_action.issued`) | `litigation` (neither)                                           |
| Attorney or domestic-representative appointed, revoked or withdrawn | `address_change`                                                                       | `representative_changed`                                         |
| A representative's own address change                               | `address_change`                                                                       | `address_change`                                                 |
| Labels naming the USPTO's examining attorney                        | `address_change`                                                                       | The type the label maps to (for example `other` or `correction`) |

### Entities, owners, attorneys and firms

| Before                                                                                                                                       | After                                                                |
| -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `GET /v1/entities/ent_<owner-uuid>` (and `/trademarks`, `/family`) returned a single-owner stand-in, or the entity the owner was linked into | `404`, linked owner or not (a merged entity stays `410`)             |
| `GET /v1/entities` listed unlinked owners                                                                                                    | Materialized entities only                                           |
| `entity_id_type` on entities, owners and trademark owners                                                                                    | Removed                                                              |
| Entity `members[].link` could be `null`                                                                                                      | Never `null`                                                         |
| Owner `entity_id` always set                                                                                                                 | The linked entity, or `null`                                         |
| `entity_type` on owners (list, detail), attorney clients (`GET /v1/attorneys/{id}/clients`), entities (list, detail) and entity `members[]`  | `legal_form`                                                         |
| `GET /v1/owners?entity_type=`, `GET /v1/entities?entity_type=`                                                                               | `legal_form=` (`entity_type` is a `400` that names it)               |
| Stats `earliest_filing`, `latest_filing` (owners, attorneys, firms, firm attorney rows, office analytics)                                    | `earliest_filing_date`, `latest_filing_date` (sort values unchanged) |
| `aliases[].source_office`                                                                                                                    | `aliases[].source_office_code`                                       |
| `address` was the office's own JSON                                                                                                          | The one address shape                                                |

### Proceedings

On `GET /v1/proceedings/{id}`, `trademark.serial_number` and
`trademarks[].serial_number` are `application_number`.

### Event types

`GET /v1/event-types`:

```jsonc theme={null}
// before
{ "object": "event_type_mapping", "office_code": "US", "raw_code": "ASSIGNED", "raw_label": null, "event_type": "assignment" }
// after
{ "object": "event_type_mapping", "office_code": "US", "raw": { "code": "ASSIGNED", "label": null }, "event_type": "assignment" }
```

Rows carry `raw {code, label}` like a trademark event. The label follows the
verbatim-label rule, so WIPO rows have `label: null`. The filter is
`office_code` (as on `GET /v1/status-codes`); `office` and any other unknown
query parameter are a `400`.

### Assignments

`GET /v1/assignments`, `GET /v1/assignments/{id}` and
`GET /v1/trademarks/{id}/assignments`:

| Before                                                                | After                                                                              |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `reel_no`, `frame_no`, `release_of_reel_no`, `release_of_frame_no`    | `reel_number`, `frame_number`, `release_of_reel_number`, `release_of_frame_number` |
| n/a                                                                   | `office_code` (`"US"`)                                                             |
| Parties `nationality: "UNITED STATES"`                                | `country_code: "US"` (ISO alpha-2, `null` when the office named no country)        |
| Parties `address {address1, city, state, postcode}`                   | The one address shape                                                              |
| Properties `serial_no`, `registration_no`, `intl_reg_no`, `mark_name` | `application_number`, `registration_number`, `ir_number`, `mark_text`              |

### Deadlines, compute and reconcile

| Before                                                                                                        | After                                                                                                                                                                                                                                                          |
| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/deadlines` and portfolio deadline rows: `grace_expiry`, `description`, no `occurrence_key`           | The one deadline row (`occurrence_key`, `grace_expiry_date`, `is_recurring`, `is_optional`, adjustments, `holiday_calendar`, `consequence_if_missed`) plus `trademark_id`, `mark_text`, `office_code`, `urgency`, `effective_from`, `last_verified`, `sources` |
| `unsupported_marks[].unsupported_reason`                                                                      | `unsupported_marks[].reason`                                                                                                                                                                                                                                   |
| `POST /v1/deadlines/compute` items `unsupported_reason`; rows `name`, `grace_expiry`, `recurring`, `optional` | `reason`; rows as above (plus `status`, `days_until_*`, `urgency`, `effective_from`, `last_verified`)                                                                                                                                                          |
| `POST /v1/oppositions/compute` items `unsupported_reason` and `reason`                                        | One `reason` (`unsupported_office`, `window_not_computable`, `publication_date_kind_mismatch`)                                                                                                                                                                 |
| `POST /v1/reconcile` `computed.unsupported_reason`                                                            | `computed.reason`                                                                                                                                                                                                                                              |

### Calendar feed

In `GET /v1/portfolios/{id}/deadlines.ics`, the event UID
`<trademark uuid>-<type>-<due date>@signa.so` becomes
`tm_<id>:<occurrence_key>@signa.so`. A file downloaded after the release adds
every event again beside the ones imported earlier, so delete those once.
After that, re-importing updates an event whose due date was corrected instead
of duplicating it.

### Offices, citations, goods and services, analytics, screening and ETags

| Before                                                                | After                                                                                                                                                          |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/v1/offices` `coverage`, `coverage_computed_at`                      | `data_completeness`, `data_completeness_computed_at`; `data_completeness.expiry_date` counts office-stated expiries only                                       |
| Citations `disposition_as_of`                                         | `disposition_at`                                                                                                                                               |
| `POST /v1/goods-services/suggest` `accepted_terms[].harmonised`       | `is_harmonised`                                                                                                                                                |
| `GET /v1/analytics/offices/{code}` `earliest_filing`, `latest_filing` | `earliest_filing_date`, `latest_filing_date`                                                                                                                   |
| `screening_id`                                                        | Changes once (the receipt binds the served index generation), and again after each index rebuild                                                               |
| ETags                                                                 | New format: every cached validator gets one `200`, then `304`s resume. A trademark's ETag does not change just because `derived.as_of_date` moved to a new day |

### New: status codes

[List Status Codes](/api-reference/reference/list-status-codes)
(`GET /v1/status-codes?office_code=US`) lists each office status code with its
verbatim label and the normalised status it maps to. Unknown query parameters
are a `400`. `raw.label` is `null` when we hold no verbatim office label for
the code (for example USPTO codes today). It is also available as the MCP tool
`list_status_codes` and as `references.statusCodes()` in the SDK.

## Watches

| Before                                                                                                                                 | After                                                                                                                     |
| -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `filters.expiryDate` and `renewalDueDate` matched Signa-computed values for US records                                                 | Office-published values, like search. Existing watches change meaning at the release                                      |
| `filters.entityId` and `entityGroup` accepted `ent_<owner-uuid>`                                                                       | `400 synthetic_entity_id`, naming the replacement (the entity the owner has since been linked into, or `filters.ownerId`) |
| Existing watches on `ent_<owner-uuid>`                                                                                                 | Migrated by Signa to `filters.ownerId` or the since-linked entity; evaluation continues without a gap                     |
| An alert could be raised for a change the row does not publish (a Signa-computed date moving, or a basis change that stays non-office) | Not raised: an alert never carries an empty diff                                                                          |

A renewal WIPO publishes on the designations moves the IR's `expiry_date`
without raising a `trademark.updated` event or alert for the IR's own id. See
[Watches](/guides/monitoring/watches).

## TypeScript SDK 0.13 to 0.14

Upgrade to `@signa-so/sdk` 0.14.0. Its types follow every change above. New
exports include `TrademarkSummary` (the shared row type), `TrademarkEvent`,
`Address`, `StatusCode` and `SignaTruncatedError`. `TrademarkSearchResult`
still compiles as a deprecated alias of `TrademarkSummary`.

### Expiry, deadlines and status

```typescript theme={null}
import { Signa } from '@signa-so/sdk';

const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
const tm = await signa.trademarks.retrieve('tm_2wX5yZ1a');

// The office's expiry when it published one, else the rulebook's term end
const expiry = tm.expiry_date ?? tm.derived?.expiry_date ?? null;

// Deadlines moved into derived.deadlines.items
const deadlines = tm.derived?.deadlines;
if (deadlines?.supported) {
  for (const d of deadlines.items) {
    console.log(d.occurrence_key, d.due_date, d.grace_expiry_date);
  }
} else {
  console.log('no deadlines:', deadlines?.reason);
}

// Status provenance
if (tm.status.basis === 'office') {
  console.log(tm.status.raw?.code, tm.status.raw?.label);
}
const codes = await signa.references.statusCodes({ office_code: 'US' });

// Owners
const legalForm = tm.owners[0]?.legal_form; // was entity_type
const entityId = tm.owners[0]?.entity_id ?? null; // null when unlinked
```

### Filters, batch, event types and search meta

```typescript theme={null}
import { Signa } from '@signa-so/sdk';

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

// TrademarkListParams and the four party trademark param types
const page = await signa.trademarks.list({
  register_type: 'supplemental', // was us_register_type
  opposition_closes_gte: '2026-10-01', // was opposition_closes_after
  renewal_due_date_lt: '2027-01-01', // was renewal_due_before
});

// Search meta
const fallback = page.search_meta?.warnings?.find((w) => w.code === 'expanded_fallback');
const windowEnded = page.search_meta?.warnings?.some((w) => w.code === 'result_window_reached');
const generation = page.search_meta?.index_generation;

// Batch identifiers use office_code
await signa.trademarks.batch({
  identifiers: [{ application_number: '97123456', office_code: 'US' }],
});

// Event types: params first, request options second (was eventTypes(options))
const eventTypes = await signa.references.eventTypes({ office_code: 'US' }, { timeout: 10_000 });
```

The old filter names are not translated. On `GET` the API answers them with a
`400` naming the replacement, and `list()`'s `POST` fallback throws a
`SignaError` that names it.

Party types are now the generated API types, so `owner.stats?.latest_filing`
is `owner.stats?.latest_filing_date`. Fields the API stopped serving are gone
from the types: `entity_id_type`, `related_entities`, attorney `email` and
`phone`, and summary `registration_rate`, `grant_rate` and `latest_filing`.

### `toArray()` never truncates silently

`toArray()` throws `SignaTruncatedError` for one of two reasons:

* `client_limit`: you passed no limit and the list holds more than 10,000
  items.
* `server_window`: the API ended a relevance-ranked or sorted search at its
  10,000-row window (`has_more: false`) while more rows match. The signal is
  the last page's `result_window_reached` warning; counts alone never throw.

A limit you pass that is reached first returns normally. `for await` and
`getNextPage()` never throw; they stop where the API stops.

```typescript theme={null}
import { Signa, SignaTruncatedError } from '@signa-so/sdk';

const signa = new Signa({ api_key: process.env.SIGNA_API_KEY });
const list = await signa.trademarks.list({ q: 'nike' });

try {
  await list.toArray();
} catch (e) {
  if (e instanceof SignaTruncatedError) console.log(e.reason, e.items.length);
}

await list.toArray({ limit: 500 }); // at most 500 items, no error

// Bulk pulls past 10,000 rows: filter-only, sorted by family_updated_at
const all = await signa.trademarks
  .list({ offices: ['US'], sort: 'family_updated_at', family_updated_at_gte: '2026-01-01' })
  .then((l) => l.toArray({ limit: Infinity }));
```

### Webhook handlers

```typescript theme={null}
import type { OfficeActionIssuedEvent } from '@signa-so/sdk';

function officeActionCode(event: OfficeActionIssuedEvent): string | null {
  return event.payload_version === 2
    ? (event.data.trademark_event.raw?.code ?? null)
    : event.data.event_code;
}
```

## MCP

| Tool                                                                                                                   | Change                                                                                                                                                                                                                |
| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_trademark_events`                                                                                                 | Rows are `trademark_event` (`hst_` ids, `raw`, `nice_class`, `sequence_number`)                                                                                                                                       |
| `get_entity`, `get_entity_trademarks`, `get_entity_family`                                                             | `ent_<owner-uuid>` is not found                                                                                                                                                                                       |
| `batch_get_trademarks`                                                                                                 | Input order, duplicates repeated                                                                                                                                                                                      |
| `search_trademarks`, `get_owner_trademarks`, `get_entity_trademarks`, `get_attorney_trademarks`, `get_firm_trademarks` | New `register_type`, `opposition_status`, `opposition_closes_gte` and `_lte`                                                                                                                                          |
| `list_offices`, `get_office`                                                                                           | `coverage` and `coverage_computed_at` are `data_completeness` and `data_completeness_computed_at`, as on REST                                                                                                         |
| `get_trademark`, `get_trademark_events`, `list_status_codes`                                                           | Declare an output schema and return structured content                                                                                                                                                                |
| `list_status_codes`                                                                                                    | New                                                                                                                                                                                                                   |
| `list_event_types`                                                                                                     | Rows are `raw {code, label}` (were `raw_code` and `raw_label`; WIPO labels are `null`). The cursor is the REST one, so a cursor from an earlier walk restarts. The filter argument is `office_code` (was `office`)    |
| `list_owners`, `list_entities`                                                                                         | `entity_type` is `legal_form` (argument and rows)                                                                                                                                                                     |
| `get_deadline_rules`                                                                                                   | Madrid rules report `jurisdiction_code: "WO"` (was `WIPO`)                                                                                                                                                            |
| Every tool                                                                                                             | Input schemas are strict: an undeclared argument (a retired filter such as `us_register_type`, or a typo) is an input validation error instead of being ignored                                                       |
| Every tool                                                                                                             | A result that fails its declared output schema is an unbilled tool error (it used to be charged, then rejected by the client)                                                                                         |
| Every tool                                                                                                             | An API error (for example not found or a validation error) returns the REST error body as JSON (`{"error": {"type", "status", "title", "detail", ...}}`, with `errors[].code`) instead of `Failed to get X: ...` text |
| Every trademark row                                                                                                    | The same shape changes as REST                                                                                                                                                                                        |

## At the cutover

* Every trademark search cursor minted before the release returns
  `400 cursor_invalid`. Restart the walk without a cursor.
* Every cached ETag gets one `200`, then `304`s resume.
* `screening_id` values change once.

## Webhooks and the events feed

New events on webhooks and `GET /v1/events` are `payload_version: 2` from
September 27, 2026. Every event keeps the version it was stored with: automatic
retries, `/redeliver` and `GET /v1/events/{id}` of an event recorded before then
stay v1 for the 30-day replay window. Handle both. See
[Webhooks](/guides/monitoring/webhooks#payload-versions).

`office_action.issued`:

```jsonc theme={null}
// v1 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": null, "occurred_at": "2026-09-01T06:14:58.000Z",
  "source_date": "2026-08-29", "portfolios": [] }
// v2 data
{ "event_id": "evt_7mNo8pQr", "trademark_id": "tm_9pQs3vNk", "office_code": "US",
  "jurisdiction_code": null, "occurred_at": "2026-09-01T06:14:58.000Z", "source_date": "2026-08-29",
  "trademark_event": { "id": "hst_0198c2f1", "object": "trademark_event", "event_date": "2026-08-29",
    "event_type": "office_action", "raw": { "code": "NOA", "label": "Non-final Office Action" },
    "description": "Examiner issued a non-final refusal.", "event_scope": "right",
    "status_after_event": "examining", "nice_class": 9, "territory_code": null,
    "source_identifier": "OA-90123456-20260829", "sequence_number": 17 },
  "portfolios": [] }
```

Change field names and values in v2 (`trademark.status_changed`
`changed_fields` and `changes`, `alert.created` `alert.event.diff[]`, and REST
alerts once v2 is emitted):

| v1                                                                  | v2                                                                   |
| ------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `source_primary_id`                                                 | `office_record_id` (`null` for Signa-composed Madrid keys)           |
| `status_source` (`explicit`, ...)                                   | `status_basis` (`office`, ...)                                       |
| `expiry_date_basis`, `renewal_due_date_basis` entries               | Never published                                                      |
| `expiry_date` or `renewal_due_date` changes between computed values | Omitted; a side that is not office-stated is `null`                  |
| `status_raw_label` as stored                                        | The verbatim-label rule (`null` for code-as-label and Signa wording) |
| `registration_number` with the `WO` prefix                          | Unprefixed                                                           |

Changes recorded before the release carry no basis or code context, so those
fields are `null` or omitted in v2.

A format fix applies to both versions. On events emitted from the release on,
`data.occurred_at` is ISO 8601 UTC with milliseconds
(`2026-09-26T22:23:21.191Z`), as `GET /v1/events` serves it. It used to carry
database timestamp text (`2026-09-26 22:23:21.191924+00`), which strict ISO
parsers rejected. Retries and redeliveries of events emitted before the release
keep the old text.

## Old-client compatibility

A client built for the old shape keeps working only where it ignores the
changed fields. Reads of removed keys return `undefined`. Requests using
removed filter names or batch `office` fail with `400`. Long paginations fail
once with `400 cursor_invalid`. Entity lookups by `ent_<owner-uuid>` fail with
`404`. Nothing is silently widened: a removed filter is rejected, never
ignored.

## Upgrade checklist

1. Read expiry as `expiry_date ?? derived.expiry_date`, and deadlines from
   `derived.deadlines.items`.
2. Rename the removed filters, and batch `office` to `office_code`.
3. Treat `owners[].entity_id` as nullable, and use `owner_id` for unlinked
   owners.
4. Restart any paginated walk that spans the release.
5. Branch webhook handlers on `payload_version`.
6. Upgrade to `@signa-so/sdk` 0.14.0 ([TypeScript SDK](/sdk/typescript)).
