Skip to content

Keeping data in sync

Synchronization has two directions, and this guide covers the read half: mirroring patents, events, and invoices from Renewr into your systems, or driving a docketing tool from Renewr data. The write half (pushing your own portfolio changes from your IPMS into Renewr) is Pushing your portfolio. The two halves compose into one loop: push what changed on your side, then pull to refresh your mirror; the cookbook recipe walks the full round trip.

For reads, the API has no webhooks: Renewr never calls your systems. Mirroring therefore means a scheduled, pull-based sync. This guide describes the timestamps the API gives you, the two pull patterns that work well in practice, and the data you must never cache.

If you are building your first integration, start with Getting started and the monthly renewal cycle. A working monthly cycle already covers most of what a sync needs.

No outbound push: design a scheduled pull

Everything in v2 is read on demand with your API key. Renewr never pushes data to you: no webhooks, no change feed, no event subscription. A production integration therefore runs on a scheduler (cron or equivalent) and re-pulls the resources it cares about.

Budget your calls

Each API key has a daily call quota, reset at midnight (server local time). Every authenticated response carries RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers so your scheduler can observe its budget; a 429 carries Retry-After instead. Size your cadences to fit comfortably inside the quota. The patterns below are designed to be cheap. See Conventions for header details and Errors for rate_limit_exceeded handling.

The timestamps you have, and their limits

Persisted resources carry createdAt and updatedAt, as ISO 8601 UTC timestamps (e.g. "2026-07-22T09:15:00.000Z"):

ResourcecreatedAt / updatedAtNotes
PatentYesMay be null on some historical records: treat null as "assume changed".
Patent eventYesAlso carries processingStatusUpdatedAt, the time the execution pipeline last moved.
InvoicecreatedAt onlyNo updatedAt: an invoice never changes once recorded (a correction is a new REGULARIZATION invoice). createdAt is filterable and sortable on GET /invoices: see pattern 3.
FeesNoComputed live while ESTIMATED; fixed from the instruction on (LOCKED); copied from the invoice once INVOICED.
InstructabilityNoComputed live on every read.
Exchange-rate freezeNoKeyed by yearMonth.

Now the honest part:

Server-side delta sync by timestamp exists for invoices only

Invoices are the one resource you can pull incrementally: GET /invoices filters and sorts on createdAt (see pattern 3). No endpoint offers filtering or sorting by updatedAt: for patents and events you cannot ask the server for "everything changed since T". The sortable endpoints are GET /patents (clientCaseReference, office, applicationDate, publicationNumber, status) and GET /invoices (issueDate, createdAt); updatedAt is on neither allowlist.

updatedAt remains useful client-side: when you mirror data, compare the fetched updatedAt against your stored copy to decide whether a row actually changed and whether to trigger downstream processing. It just cannot reduce how much you fetch.

Use the three patterns below in place of an updatedAt delta. They exploit a property of the domain: the data that changes often is bounded by time windows, the data that is unbounded changes rarely, and invoices only ever get appended.

Pattern 1: window-driven refresh for events

Renewal events are naturally scoped by due month, and GET /patent-events filters by exactly that. The state that moves daily (duePeriod, processingStatus, instructability, estimated fees) only matters for the months you are actively working. So there is no need to chase individual changes: re-pull the whole window you track and overwrite your local state:

bash
curl "https://api.renewr.example/api/v2/external/patent-events?dueMonth=2026-09&include=fees&itemsPerPage=100" \
  -H "x-api-key: $RENEWR_API_KEY"

Repeat per tracked month. A sensible window is:

  • the due months whose instruction window is open. The window opens on opensAt, the first day of the month two months before the due month, so that is the current month through two months ahead (today: 2026-07, 2026-08, 2026-09);
  • plus any earlier months you still track through grace or execution follow-up (GRACE_PERIOD events, instructed events not yet FULFILLED).

This is cheap and bounded: a few paginated calls per month tracked, regardless of portfolio size, and each response is a complete, current snapshot of that month. No reconciliation logic is needed. include=fees adds the estimated amounts in the same call (unknown include values are silently ignored). See Events and due periods for what the per-event state means, and the monthly renewal cycle for how this refresh feeds the instruction workflow.

For a single patent, GET /patents/{patentId}/events returns a bare JSON array of all the patent's events (no pagination envelope). One call gives you the complete event chain, which makes per-patent reconciliation trivial.

Pattern 2: periodic full re-list for patents (and as a safety net for invoices)

Patents are not window-scoped and change slowly: bibliographic data moves on office actions. For them, a periodic full walk is simpler and safer than any delta scheme. The same walk is the weekly safety net behind the daily incremental pull of invoices (pattern 3).

Both GET /patents and GET /invoices return the standard list envelope:

json
{
  "pagination": {
    "totalItems": 1180,
    "currentPage": 1,
    "itemsPerPage": 100,
    "totalPages": 12,
    "hasMore": true
  },
  "data": ["..."]
}

Walk it with the maximum page size (itemsPerPage defaults to 10, hard cap 100; larger values are silently clamped) until pagination.hasMore is false:

bash
page=1
while : ; do
  response=$(curl -s "https://api.renewr.example/api/v2/external/patents?itemsPerPage=100&currentPage=$page" \
    -H "x-api-key: $RENEWR_API_KEY")

  # Upsert response.data into your mirror.
  # If you store updatedAt, compare it per row to detect real changes
  # and skip no-op writes / downstream triggers.

  hasMore=$(echo "$response" | jq -r '.pagination.hasMore')
  [ "$hasMore" = "true" ] || break
  page=$((page + 1))
done

At 100 items per page, a 5 000-patent portfolio is 50 calls, a rounding error against a daily quota, even weekly. The same loop works verbatim for GET /invoices (every list entry already carries its fees[] lines); as a weekly complement to the daily window of pattern 3, it catches anything a mis-scheduled run could have skipped and detects removals.

If you also push your portfolio, the walk is where your pushed changes come back to you: applied and accepted changes are visible on the next pull, and the patent number fields read back exactly as you pushed them, so a push-then-pull round trip produces no phantom diffs in your mirror. Changes still awaiting review are not visible yet; their state lives in the sync batch, never in the patent read model.

Detecting deletions

A full walk also catches what a delta sync structurally cannot: items that disappeared from your tenant. Any locally mirrored row you did not see during a complete walk is gone upstream, so flag or remove it.

Pattern 3: incremental pull for invoices

Invoices are append-mostly, and each one records when it entered Renewr (createdAt). GET /invoices filters on the day it was recorded with an inclusive window of calendar days, createdFrom to createdTo, so a daily job asks for yesterday alone and gets exactly the invoices recorded that day, no overlap and no gap:

bash
curl "https://api.renewr.example/api/v2/external/invoices?createdFrom=2026-09-11&createdTo=2026-09-11&sort=createdAt&itemsPerPage=100" \
  -H "x-api-key: $RENEWR_API_KEY"

Window on createdAt, not on issueDate

The issue date of an IMPORTED invoice is entered by hand and can be backdated: an invoice recorded today with last month's date would never fall inside a window on issueDate. createdAt is the instant the invoice entered Renewr; it never moves and always lands in the day the invoice was recorded. issueDateFrom / issueDateTo are for reporting ("everything issued in Q3"), not for synchronization.

Bounds are plain YYYY-MM-DD days, with no time and no offset; the returned createdAt is still a full UTC timestamp. Chain consecutive runs day after day: the day following one run's createdTo is the next run's createdFrom. A window holding no invoice returns an empty page with totalItems: 0, not an error; an inverted window returns 400 validation_failed (see Conventions).

Each list entry already carries its fees[] lines, so a daily pull normally needs no per-invoice fetch. A REGULARIZATION invoice lists its own lines with signed amounts (a credit is negative), each pointing back to the original invoice through regularizedInvoiceId and regularizationReason; see Fees and FX.

Computed data: never cache on updatedAt

Some of the most-read data in the API is computed live at read time and has no timestamps at all:

  • Estimated fees: the fees object from GET /patents/{patentId}/events/{eventId}/fees while feesStatus is ESTIMATED, and the inline breakdown from include=fees on GET /patent-events. Estimates are recomputed from the current fee matrices and FX rates on every request. Both move without touching any updatedAt: an event can be byte-identical in every persisted field while its estimate has shifted.
  • Instructability: isAllowed, allowedDecisions, blockedReason, opensAt are evaluated against today on every read. An event crosses from TOO_EARLY to instructable, or loses PROCEED when it lapses, purely by the calendar advancing.

The rules that follow:

  1. Never invalidate cached estimates or instructability with updatedAt. They are not versioned by it.
  2. Re-fetch when you need current numbers: display, budgeting, and always immediately before submitting instructions. A stale allowedDecisions can turn into a 422 instruction_lapsed at POST time (see Errors).
  3. LOCKED and INVOICED amounts are stable. Once an event's fees carry feesStatus: "LOCKED" (fixed when your PROCEED instruction was executed) or "INVOICED" (copied from the invoice line), they no longer move and are safe to store permanently.

If FX drift on estimates is a problem for your budgeting, you can pin the rates for a month with PUT /exchange-rate-freezes/{yearMonth}. Frozen estimates report isFxRateFrozen: true and stop moving with the market. See Fees and FX.

Suggested cadences

CadenceWhat to pullWhy
DailyGET /patent-events for each tracked due month, with include=fees if you display amountsduePeriod, instructability, processingStatus, and estimates all move on a daily scale during the instruction window.
WeeklyFull walk of GET /patents at itemsPerPage=100Bibliographic data changes slowly; a weekly complete snapshot also detects removals.
DailyGET /invoices windowed on createdAt over the previous day (pattern 3)Catches every invoice recorded yesterday, backdated imports included, with its fees[] lines. Fetch related documents as they become available.
WeeklyFull walk of GET /invoices at itemsPerPage=100Safety net behind the daily window: catches what a missed run skipped and detects removals.
On demandGET /patents/{patentId}/events/{eventId}/fees immediately before instructing; GET /patents/{patentId} / GET /patents/{patentId}/events/{eventId} when a user drills into one recordEstimates and instructability must be current at the moment of decision. Values from the last sync may already be stale.
After each pushGET /patents/sync/{importId} until the batch is TRIAGED, then again later for review decisions on staged itemsOutcomes of a portfolio push arrive in two waves: triage within seconds, review decisions on human time.

Tune the daily window to your process: firms that instruct early in the month often refresh more frequently around their instruction date and drop to weekly elsewhere.

Re-running a sync safely

Sync jobs crash, timeouts happen, schedulers fire twice. The API is built so that re-driving a run is safe:

  • Every read is side-effect free. Re-pulling a month, re-walking a list, or re-fetching a fees object any number of times changes nothing. If a walk dies at page 7, restart it from page 1. The extra calls only cost quota.

  • Freezing a month is idempotent. PUT /exchange-rate-freezes/{yearMonth} only captures rates not already frozen for that month; a retry never re-freezes at new rates.

  • A portfolio push is replayable. Give POST /patents/sync an externalImportRef and a crashed or timed-out push can be re-sent as-is: the same reference with the same items returns the state of the batch already running instead of importing twice. See Pushing your portfolio.

  • Instruction submission is guarded twice. POST /patent-instructions is the write with immediate real-world effect: a DIRECT instruction executes immediately and synchronously and cannot be modified afterwards through the API. Two mechanisms make a re-drive safe:

    1. Pre-flight with instructability. Rebuild your candidate set from a fresh event pull and only submit events with instructability.isAllowed: true and your decision in allowedDecisions. Events already handled by the crashed run come back blockedReason: "ALREADY_INSTRUCTED" and drop out of the set. See Instructions and instructability.
    2. 409 instruction_already_set as a success marker. If you submit an already-instructed event anyway (a race, or a stale candidate set), the API rejects it with 409 Conflict, code instruction_already_set. The decision from the earlier run stands, and nothing is duplicated. In a re-drive, treat this response as "already done" rather than counting it as a failure.

    Remember the batch shape: one POST can span many patents, but only the chronologically earliest un-instructed event of each patent is accepted, meaning one event per patent per call. A re-drive loop that submits per patent, in due-date order, converges naturally.

The safe mental model

Reads are cheap and repeatable; the single consequential write is protected by a fresh-read pre-flight and a conflict response. A sync that always re-reads before it writes, and treats instruction_already_set as confirmation, cannot double-execute an instruction no matter how many times it restarts.

Renewr External API v2. Access on invitation.