Appearance
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"):
| Resource | createdAt / updatedAt | Notes |
|---|---|---|
| Patent | Yes | May be null on some historical records: treat null as "assume changed". |
| Patent event | Yes | Also carries processingStatusUpdatedAt, the time the execution pipeline last moved. |
| Invoice | createdAt only | No 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. |
| Fees | No | Computed live while ESTIMATED; fixed from the instruction on (LOCKED); copied from the invoice once INVOICED. |
| Instructability | No | Computed live on every read. |
| Exchange-rate freeze | No | Keyed 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_PERIODevents, instructed events not yetFULFILLED).
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¤tPage=$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))
doneAt 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
feesobject fromGET /patents/{patentId}/events/{eventId}/feeswhilefeesStatusisESTIMATED, and the inline breakdown frominclude=feesonGET /patent-events. Estimates are recomputed from the current fee matrices and FX rates on every request. Both move without touching anyupdatedAt: an event can be byte-identical in every persisted field while its estimate has shifted. - Instructability:
isAllowed,allowedDecisions,blockedReason,opensAtare evaluated against today on every read. An event crosses fromTOO_EARLYto instructable, or losesPROCEEDwhen it lapses, purely by the calendar advancing.
The rules that follow:
- Never invalidate cached estimates or instructability with
updatedAt. They are not versioned by it. - Re-fetch when you need current numbers: display, budgeting, and always immediately before submitting instructions. A stale
allowedDecisionscan turn into a422 instruction_lapsedat POST time (see Errors). LOCKEDandINVOICEDamounts are stable. Once an event's fees carryfeesStatus: "LOCKED"(fixed when yourPROCEEDinstruction 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
| Cadence | What to pull | Why |
|---|---|---|
| Daily | GET /patent-events for each tracked due month, with include=fees if you display amounts | duePeriod, instructability, processingStatus, and estimates all move on a daily scale during the instruction window. |
| Weekly | Full walk of GET /patents at itemsPerPage=100 | Bibliographic data changes slowly; a weekly complete snapshot also detects removals. |
| Daily | GET /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. |
| Weekly | Full walk of GET /invoices at itemsPerPage=100 | Safety net behind the daily window: catches what a missed run skipped and detects removals. |
| On demand | GET /patents/{patentId}/events/{eventId}/fees immediately before instructing; GET /patents/{patentId} / GET /patents/{patentId}/events/{eventId} when a user drills into one record | Estimates and instructability must be current at the moment of decision. Values from the last sync may already be stale. |
| After each push | GET /patents/sync/{importId} until the batch is TRIAGED, then again later for review decisions on staged items | Outcomes 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/syncanexternalImportRefand 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-instructionsis the write with immediate real-world effect: aDIRECTinstruction executes immediately and synchronously and cannot be modified afterwards through the API. Two mechanisms make a re-drive safe:- Pre-flight with instructability. Rebuild your candidate set from a fresh event pull and only submit events with
instructability.isAllowed: trueand your decision inallowedDecisions. Events already handled by the crashed run come backblockedReason: "ALREADY_INSTRUCTED"and drop out of the set. See Instructions and instructability. 409 instruction_already_setas a success marker. If you submit an already-instructed event anyway (a race, or a stale candidate set), the API rejects it with409 Conflict, codeinstruction_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.
- Pre-flight with instructability. Rebuild your candidate set from a fresh event pull and only submit events with
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.
Related pages
- Pushing your portfolio: the write half; upsert your IPMS data into Renewr
- Monthly renewal cycle: the workflow your daily window refresh feeds
- Conventions: pagination envelope, timestamps, rate-limit headers
- Errors: problem+json envelope and the stable error codes
- Instructions and instructability: the rules behind the pre-flight
- Fees and FX: why estimates move and how freezing works
- Downloading documents: fetching receipts and invoice PDFs as they appear