Skip to content

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:

ResourceEndpoints
/patentsList patents, Get a patent
/patents/{patentId}/eventsList a patent's events, Get an event, Get event fees
/patents/{patentId}/events/{eventId}/documents/…Invoice PDF, Instruction receipt PDF, Official receipt PDF
/patent-eventsList events across the portfolio
/patent-instructionsPost instructions
/invoicesList invoices, Get an invoice
/exchange-rate-freezesList 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 is or has: isFxRateFrozen, isAllowed, hasMore, hasPdfDocument, hasXlsxDocument, hasInstruction.
  • Counters end with Count: claimCount.
  • Every addressable resource carries a constant object field that names its own type (self-description in the Stripe style). Use it to discriminate payloads in logs, queues, and generic handlers:
object valueReturned 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:

SuffixFormatMeaningExamples
*DateYYYY-MM-DDA calendar date: deadlines and legal facts, where the day is the unit of meaningdueDate, gracePeriodEndDate, applicationDate, grantDate, lapseDate, issueDate
*AtISO 8601 UTC, millisecond precision, Z suffixAn action timestamp: the instant something happenedcreatedAt, 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:

  1. "Not applicable" is always null. A patent whose type is unknown has "patentType": null; you will never see a sentinel value such as NOT_APPLICABLE or UNKNOWN on the wire. This applies to patentType, filingRoute, applicantType, status, and every other nullable enum.
  2. Enums are open. New values will be added within v2 without a version bump. eventType is RENEWAL today and will grow (Events and due periods); instructability.mode is DIRECT today and will grow. Code a default branch 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:

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
clientCaseReferenceyour case reference
officeoffice code
applicationDatefiling date
publicationNumberpublication number
statusIP 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:

EndpointParameterTypeNotes
getPatentsproviderIdstringExact match on your own reference; a miss returns an empty page (200) and never a 404
getPatentEventsByPerioddueMonthYYYY-MMShortcut for a whole-month window
dueFrom + dueToYYYY-MM-DDInclusive window bounds; both required together
officesrepeatable string2-character office codes: ?offices=EP&offices=WO
ownersrepeatable stringOwner display names
familyrepeatable stringFamily references
eventTypesrepeatable enumRENEWAL 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 include values and new query parameters,
  • new endpoints and resources.

Your client must therefore:

  1. Ignore unknown fields. Deserialize leniently; never fail on an unrecognized key.
  2. Tolerate unknown enum values. Use a default branch; an exhaustive match that throws will break when values are added.
  3. 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).

Renewr External API v2. Access on invitation.