Pagination and caching
Paging
Section titled “Paging”List endpoints take limit (1–200, default 50) and offset (default 0), and
return items plus the limit and offset that were applied.
def paginate(get, path, page=200): offset = 0 while True: sep = "&" if "?" in path else "?" data = get(f"{path}{sep}limit={page}&offset={offset}") items = data["items"] yield from items if len(items) < page: return offset += pageStop on a short page. Waiting for an empty page costs one extra request every time, and on a page-size boundary you need the empty page anyway - the check above handles both correctly.
Use limit=200. The cost of a request is dominated by the round trip, not the row
count, and one request of 200 is far cheaper against your
rate limit than four of 50.
Offset paging drifts
Section titled “Offset paging drifts”offset is positional, so a full pass over a changing dataset can miss or repeat
rows if something is inserted mid-pass. For inventory reporting this rarely
matters. Where it does, page by a stable key instead:
seen = set()for cert in paginate(get, "/v1/certificates"): if cert["id"] in seen: # repeated due to drift continue seen.add(cert["id"]) handle(cert)Deduplicating on id makes a full pass idempotent, which is worth the few bytes.
Filter server-side where you can
Section titled “Filter server-side where you can”| Endpoint | Filters |
|---|---|
/v1/certificates |
cert_type, org_id |
/v1/requests |
state, org_id |
state is the useful one: ?state=pending_approval returns only work waiting on
an approver, which is a much smaller set than every renewal you have ever made.
There is no server-side expiry filter, so expiry reporting means fetching and sorting locally. See monitor expiring certificates.
Cache and diff
Section titled “Cache and diff”Certificate inventories change slowly. Keep the last result and compare, rather than re-fetching to answer every internal question:
import json, pathlib
CACHE = pathlib.Path("inventory.json")
def refresh(get): current = {c["id"]: c for c in paginate(get, "/v1/certificates")} previous = json.loads(CACHE.read_text()) if CACHE.exists() else {}
added = current.keys() - previous.keys() removed = previous.keys() - current.keys() changed = { i for i in current.keys() & previous.keys() if current[i]["not_after"] != previous[i]["not_after"] or current[i]["is_revoked"] != previous[i]["is_revoked"] }
CACHE.write_text(json.dumps(current)) return added, removed, changedA changed not_after for the same id means the certificate was renewed and
re-observed. removed is worth attention: a certificate that disappeared was
either cleaned up or is no longer being reported, and those have different causes.
There are no conditional-request headers on list endpoints in v1, so this client-side diff is the mechanism.
Sensible intervals
Section titled “Sensible intervals”| What | Interval |
|---|---|
| Full inventory | Hourly is generous; daily is fine for reporting |
| A renewal you just opened | Every 15–30 seconds, briefly |
| Every renewal in the estate | Do not. Filter by state and poll that |
Expiry is measured in days. Polling the inventory every minute produces load and no new information.