Appearance
API conventions
Every endpoint of the External API v2 follows the same set of conventions. Read this page once and you can predict how any resource behaves: how it is addressed, how its fields are named and typed, how lists paginate, and what may change without notice.
If you have not made your first call yet, start with Getting started and Authentication. Error behavior has its own page: Errors.
Resources and URLs
Resources are plural, kebab-case nouns under https://api.renewr.example/api/v2/external:
| Resource | Endpoints |
|---|---|
/patents | List patents, Get a patent |
/patents/{patentId}/events | List a patent's events, Get an event, Get event fees |
/patents/{patentId}/events/{eventId}/documents/… | Invoice PDF, Instruction receipt PDF, Official receipt PDF |
/patent-events | List events across the portfolio |
/patent-instructions | Post instructions |
/invoices | List invoices, Get an invoice |
/exchange-rate-freezes | List frozen months, Get a month's freeze, Freeze a month |
Path parameters are always opaque Renewr UUIDs (patentId, eventId, invoiceId). Treat them as stable identifiers with no embedded meaning; never parse or construct them. There is exactly one exception: the exchange-rate-freeze resource is keyed by its natural identifier, {yearMonth} in YYYY-MM format (for example 2026-09), because a freeze is a fact about a calendar month rather than a created object. See Fees and FX.
Your own identifiers never appear in a path. To find a patent by the reference you assigned it, filter with ?providerId= on List patents:
bash
curl "https://api.renewr.example/api/v2/external/patents?providerId=ACME-EP-000123" \
-H "x-api-key: $RENEWR_API_KEY"The match is exact and returns at most one result. An unknown providerId returns an empty page with HTTP 200. It never responds 404. See the Glossary for the difference between id, providerId, and clientCaseReference.
Fields and casing
All JSON fields are camelCase. Naming is systematic:
- Booleans start with
isorhas:isFxRateFrozen,isAllowed,hasMore,hasPdfDocument,hasXlsxDocument,hasInstruction. - Counters end with
Count:claimCount. - Every addressable resource carries a constant
objectfield that names its own type (self-description in the Stripe style). Use it to discriminate payloads in logs, queues, and generic handlers:
object value | Returned by |
|---|---|
"patent" | getPatents, getPatent |
"patentEvent" | getPatentEvents, getPatentEvent, getPatentEventsByPeriod |
"fees" | getPatentEventFees, fees[] on invoices, include=fees |
"invoice" | getInvoices, getInvoice |
"instructionReceipt" / "instruction" | postPatentInstructions |
"exchangeRateFreeze" / "frozenRateEvent" | getExchangeRateFreezes, getExchangeRateFreeze, putExchangeRateFreeze |
Fields that apply to a resource but have no value are null; they are never omitted from the payload. (The two documented exceptions, fees and instructability on events, are opt-in or contextual sub-objects and are absent rather than null when not applicable.)
Dates and timestamps
Two temporal types exist on the wire, distinguished by suffix:
| Suffix | Format | Meaning | Examples |
|---|---|---|---|
*Date | YYYY-MM-DD | A calendar date: deadlines and legal facts, where the day is the unit of meaning | dueDate, gracePeriodEndDate, applicationDate, grantDate, lapseDate, issueDate |
*At | ISO 8601 UTC, millisecond precision, Z suffix | An action timestamp: the instant something happened | createdAt, updatedAt, executedAt, processingStatusUpdatedAt |
json
{
"dueDate": "2026-09-30",
"updatedAt": "2026-07-21T14:03:22.187Z"
}The rule of thumb: deadlines are Dates, actions are At. Compare calendar dates as dates in the office's business sense; never localize a *Date through a timezone conversion.
Two calendar-valued *At fields
instructability.opensAt and scheduledForExecutionAt are calendar dates (YYYY-MM-DD) despite the At suffix. For both, only the day is meaningful. See Instructions and instructability.
Amounts and currencies
Monetary amounts are decimal strings in the major unit of their currency, scaled per ISO 4217: 2 decimals for EUR or USD ("1250.00"), 0 for JPY ("185000"). Every monetary field sits next to a currency field carrying the ISO 4217 code:
json
{
"object": "fees",
"currency": "EUR",
"total": "1250.00",
"components": [
{ "type": "OFFICE", "amount": "800.00" },
{ "type": "AGENT", "amount": "150.00" },
{ "type": "RENEWR", "amount": "300.00" }
],
"feesStatus": "ESTIMATED",
"isFxRateFrozen": false
}Never parse money as a float
"1250.00" must not become 1250.0000000001 in your ledger. Parse amounts with a decimal library (BigDecimal, decimal.Decimal, big.js, …) and keep them decimal end to end. The amounts are strings on the wire precisely so that nothing parses them as floats by accident.
Non-monetary numbers stay plain JSON numbers: vatRate (e.g. 0.2 for 20 %), claimCount, renewal.annuityYear. How estimated fees relate to invoiced fees is covered in Fees and FX.
Enums
Enum values are UPPER_SNAKE_CASE strings: RENEWAL, PROCEED, SKIP_THIS_TIME, DROP, GRACE_PERIOD, ESTIMATED, INVOICED, GENERATED.
Two rules govern every enum field:
- "Not applicable" is always
null. A patent whose type is unknown has"patentType": null; you will never see a sentinel value such asNOT_APPLICABLEorUNKNOWNon the wire. This applies topatentType,filingRoute,applicantType,status, and every other nullable enum. - Enums are open. New values will be added within v2 without a version bump.
eventTypeisRENEWALtoday and will grow (Events and due periods);instructability.modeisDIRECTtoday and will grow. Code adefaultbranch that skips or logs unknown values. A branch that throws on unknown values will break when the enum grows.
ts
switch (event.eventType) {
case 'RENEWAL':
handleRenewal(event);
break;
default:
// A kind added after you shipped. Ignore it, don't crash.
break;
}Pagination
List endpoints return a pagination envelope:
json
{
"pagination": {
"totalItems": 137,
"currentPage": 1,
"itemsPerPage": 10,
"totalPages": 14,
"hasMore": true
},
"data": [
{ "object": "invoice", "id": "8f14e45f-ea3e-4c2b-9d3a-2b0c6e4a91d7" }
]
}Drive pagination with two query parameters: currentPage (1-based, default 1) and itemsPerPage (default 10, hard cap 100). Loop while pagination.hasMore is true.
The cap is silent
Requesting itemsPerPage=500 does not fail. The server clamps to 100 and reports "itemsPerPage": 100 in the envelope. Always read the effective page size back from pagination; the value you sent may have been clamped.
Two endpoints return bare JSON arrays with no envelope, because they are bounded sub-collections that always fit in one response:
- GET /patents/{patentId}/events: a patent has a bounded number of annuity events over its life.
- GET /exchange-rate-freezes: your frozen months, from the current month onward.
Every other list (getPatents, getPatentEventsByPeriod, getInvoices) uses the envelope. For building a polling loop on top of pagination, see Syncing data.
Sorting
Sorting exists today on one endpoint: GET /patents. The syntax is sort=field for ascending and sort=-field for descending, one field per request:
bash
curl "https://api.renewr.example/api/v2/external/patents?sort=-applicationDate" \
-H "x-api-key: $RENEWR_API_KEY"The sortable fields are an exact allowlist:
| Field | |
|---|---|
clientCaseReference | your case reference |
office | office code |
applicationDate | filing date |
publicationNumber | publication number |
status | IP right status |
Any other field, including a typo, returns 400 validation_failed (see Errors). There is no multi-field sort and no updatedAt sort; omitting sort uses the server's default ordering.
Filtering
Filters are endpoint-specific query parameters:
| Endpoint | Parameter | Type | Notes |
|---|---|---|---|
| getPatents | providerId | string | Exact match on your own reference; a miss returns an empty page (200) and never a 404 |
| getPatentEventsByPeriod | dueMonth | YYYY-MM | Shortcut for a whole-month window |
dueFrom + dueTo | YYYY-MM-DD | Inclusive window bounds; both required together | |
offices | repeatable string | 2-character office codes: ?offices=EP&offices=WO | |
owners | repeatable string | Owner display names | |
family | repeatable string | Family references | |
eventTypes | repeatable enum | RENEWAL today; pass it explicitly to pin renewals-only ahead of future kinds |
The event window is required
getPatentEventsByPeriod always needs a due-date window: either dueMonth, or dueFrom and dueTo. If both forms are present, dueMonth wins. Neither complete form yields 400 validation_failed. There is no "all events, all time" query. See The monthly renewal cycle for why a month-by-month pull is the intended shape.
Include
getPatentEventsByPeriod accepts an include parameter to embed related data in one round trip. The only recognized value today is fees, which adds the estimated fee breakdown to each event:
bash
curl "https://api.renewr.example/api/v2/external/patent-events?dueMonth=2026-09&include=fees" \
-H "x-api-key: $RENEWR_API_KEY"Without include=fees, the fees field is absent from events (not null). Unknown include values are silently ignored: include=bogus returns 200 with no extra data, so a new include value added later can never break an existing integration. For an event's final invoiced breakdown, use getPatentEventFees; see Fees and FX.
Additive versioning within v2
The v2 contract evolves additively. Without notice, and without a version bump, you may see:
- new fields on existing objects,
- new values in open enums (
eventType,instructability.mode, …), - new
includevalues and new query parameters, - new endpoints and resources.
Your client must therefore:
- Ignore unknown fields. Deserialize leniently; never fail on an unrecognized key.
- Tolerate unknown enum values. Use a
defaultbranch; an exhaustive match that throws will break when values are added. - Not depend on field order in JSON objects.
What will never change within v2: existing fields are not renamed, removed, or retyped; existing enum values keep their meaning; URLs and status codes stay stable. Any breaking change ships as a new URL version (/api/v3/…) with a migration period, exactly as v1 remains available while you migrate today (see Migrating from v1 to v2).