Skip to content

Fees & exchange rates

Every renewal has a cost, and that cost has a life of its own: it starts as a projection, moves with exchange rates, and eventually lands on an invoice. This page explains the single fees object Renewr uses to represent that cost everywhere, how it evolves from estimate to invoice, and how exchange-rate freezes let you pin the numbers ahead of time.

One fees object, everywhere

The API uses exactly one shape for renewal costs, wherever they appear:

Same fields, same semantics, on both sides of invoicing. A typical estimate for an EP patent's fifth annuity:

json
{
  "object": "fees",
  "feesStatus": "ESTIMATED",
  "isFxRateFrozen": false,
  "invoiceId": null,
  "eventId": "8f4c9a2e-1d3b-4c5a-8e7f-2a6b9c0d1e42",
  "patent": {
    "id": "3b8e1c9a-7f2d-4e6b-9a1c-5d8f2e7b4a10",
    "providerId": "ACME-EP-0042"
  },
  "currency": "EUR",
  "total": "1250.00",
  "components": [
    { "type": "OFFICE", "amount": "845.00" },
    { "type": "AGENT",  "amount": "150.00" },
    { "type": "RENEWR", "amount": "255.00" }
  ]
}
FieldTypeDescription
objectstringAlways "fees".
feesStatusenumESTIMATED or INVOICED; see the lifecycle below.
isFxRateFrozenbooleantrue when the exchange rates behind these amounts are frozen.
invoiceIdUUID, nullableBack-reference to the invoice these fees belong to. null while feesStatus is ESTIMATED.
eventIdUUIDThe patent event these fees belong to; use it to trace each fee line back to its event.
patentobjectThe patent, identified both ways: id (Renewr UUID) and providerId (your own matching reference, null if you never set one).
currencystringISO 4217 code of the currency you are invoiced in. All amounts in the object are in this currency.
totalstringTotal of all components, as a decimal string.
componentsarrayPer-type breakdown; each entry is { type, amount }, described below.

The component breakdown

components[] is a typed breakdown in the style of Stripe's fee_details[]:

typeCoversPresent when
OFFICEThe official maintenance fee charged by the patent office.The office charges one, which is essentially always.
AGENTThe local agent's fee for filing the payment with the office.A local agent is involved in that jurisdiction.
RENEWRRenewr's service fee.Always.
GRACE_PERIODThe statutory late surcharge the office charges after the due date.Only when the payment falls inside the grace period.

Three rules to build against:

  1. A component that does not apply is absent from the array. It is never present as null. No AGENT entry means no agent fee, full stop.
  2. Trust total, do not re-sum. The components do sum to total, but total is the authoritative figure; treat the breakdown as explanatory detail.
  3. Amounts are decimal strings. They are never floats. They are expressed in the invoice currency and scaled per currency: 2 decimals for EUR or USD ("1250.00"), 0 for JPY ("185000"). See Conventions for the full monetary format rules.

The breakdown is extensible

New component types may be added over time without notice. Sum-agnostic code that iterates components[] and ignores unknown type values will keep working; code with an exhaustive switch that throws on unknown types will not.

From estimate to invoice

feesStatus tells you which side of invoicing you are looking at:

feesStatusSource of the amountsinvoiceIdStability
ESTIMATEDComputed live from Renewr's fee matrices and current exchange rates.nullVolatile: moves with FX rates and fee-schedule updates, without notice.
INVOICEDRead from the actual invoice line.SetStable: these are the billed amounts.

GET /patents/{patentId}/events/{eventId}/fees never returns 404 for a real event: before an invoice exists you get the projected amounts (feesStatus: "ESTIMATED"), and once the event is invoiced the same call returns the billed amounts (feesStatus: "INVOICED"). Same shape, same URL, no special casing needed.

bash
curl "https://api.renewr.example/api/v2/external/patents/3b8e1c9a-7f2d-4e6b-9a1c-5d8f2e7b4a10/events/8f4c9a2e-1d3b-4c5a-8e7f-2a6b9c0d1e42/fees" \
  -H "x-api-key: $RENEWR_API_KEY"

Estimates are computed data: do not cache them on updatedAt

An ESTIMATED fees object can change without any updatedAt bump on the event, because FX rates and fee matrices move independently of the event record. Re-fetch estimates whenever you need fresh numbers. See Syncing data.

For quick scanning, feesStatus and isFxRateFrozen are also summarized directly on the patent event itself (returned by GET /patents/{patentId}/events, GET /patents/{patentId}/events/{eventId} and GET /patent-events), so you can spot invoiced or frozen events in a list without fetching each fees object. Add include=fees on GET /patent-events to get the full breakdown inline (unknown include values are silently ignored).

Reconciling invoices

An invoice aggregates renewals across many patents and events into one document. Its fees[] array reuses the exact fees shape above, with one entry per invoice line, and every line carries its eventId and patent { id, providerId } back-references:

json
{
  "object": "invoice",
  "id": "b2a7d9c4-5e1f-4a3b-8c6d-9e0f1a2b3c4d",
  "reference": "INV-2026-0187",
  "currency": "EUR",
  "fees": [
    {
      "object": "fees",
      "feesStatus": "INVOICED",
      "isFxRateFrozen": true,
      "invoiceId": "b2a7d9c4-5e1f-4a3b-8c6d-9e0f1a2b3c4d",
      "eventId": "8f4c9a2e-1d3b-4c5a-8e7f-2a6b9c0d1e42",
      "patent": {
        "id": "3b8e1c9a-7f2d-4e6b-9a1c-5d8f2e7b4a10",
        "providerId": "ACME-EP-0042"
      },
      "currency": "EUR",
      "total": "1250.00",
      "components": [
        { "type": "OFFICE", "amount": "845.00" },
        { "type": "AGENT",  "amount": "150.00" },
        { "type": "RENEWR", "amount": "255.00" }
      ]
    }
  ]
}

One call to GET /invoices/{invoiceId} is therefore self-sufficient for reconciliation: you can post every line against your own docketing system by providerId (or by Renewr id) without N follow-up lookups. List invoices with GET /invoices; the per-event invoice PDF is available via GET /patents/{patentId}/events/{eventId}/documents/invoice, as covered in Downloading documents.

Exchange-rate freezes

The problem

Renewal fees are paid to offices and agents in dozens of local currencies (JPY in Japan, USD at the USPTO, KRW in Korea), but you are invoiced in one currency. Every ESTIMATED fees object therefore embeds currency conversion, and as long as the rates are live, the estimate moves with the FX market. For budgeting a renewal campaign, that is exactly what you do not want.

The solution: freeze a month

PUT /exchange-rate-freezes/{yearMonth} captures the current exchange rates and pins them, for your account, for every event due in that month:

bash
curl -X PUT "https://api.renewr.example/api/v2/external/exchange-rate-freezes/2026-09" \
  -H "x-api-key: $RENEWR_API_KEY"

From that moment on, estimates for events with a dueDate in 2026-09 are computed with the frozen rates instead of live ones. Those fees report isFxRateFrozen: true, and, as far as currency conversion is concerned, the price you see is the price you will be invoiced. A frozen estimate no longer drifts with the FX market; only a genuine change in an office's fee schedule could still move it.

PUT is idempotent, so retry freely

Re-freezing an already-frozen month never re-captures rates: currency pairs that are already frozen for that month keep their original rates. A network timeout followed by a retry can never silently re-price your month, so PUT is always safe to repeat.

A natural pattern is to freeze the month at the start of each monthly renewal cycle, before pulling estimates and collecting decisions.

Reading freezes

GET /exchange-rate-freezes lists your frozen months. Note that this endpoint returns a bare JSON array. It does not use the usual list envelope:

json
[
  { "object": "exchangeRateFreeze", "yearMonth": "2026-08" },
  { "object": "exchangeRateFreeze", "yearMonth": "2026-09" }
]

GET /exchange-rate-freezes/{yearMonth} returns one month with the renewal events affected by its freeze:

json
{
  "object": "exchangeRateFreeze",
  "yearMonth": "2026-09",
  "events": [
    {
      "object": "frozenRateEvent",
      "clientCaseReference": "ACME-EP-0042",
      "dueDate": "2026-09-30",
      "amount": "1250.00",
      "currency": "EUR",
      "isFxRateFrozen": true,
      "hasInstruction": false
    }
  ]
}

Guards

ConditionResult
The freeze capability is not enabled for your account403 with code forbidden. Freezing is a contractual option; contact Renewr to enable it.
The month is in the past400. You cannot freeze retroactively.
The month is further ahead than your contractual freeze horizon400. Each account has a limit on how far into the future it may freeze.

All three exchange-rate-freeze endpoints, including the reads, require the exchange-rate-freezes:write API scope. See Errors for the problem-detail format these failures use.

Renewr External API v2. Access on invitation.