Best practices
Keep keys on the server
Section titled “Keep keys on the server”A Prokure API key grants full access to everything its scopes cover, for your whole company. Treat it like a database password.
- Read it from an environment variable or a secret manager at runtime.
- Never commit it — not to a repository, a
.envfile that gets checked in, a Dockerfile, or a CI config in plain text. - Never ship it to a browser or a mobile app. Anything that runs on a user’s device can have its traffic and its bundle read. If a front end needs Prokure data, proxy the call through your own backend and keep the key there.
- Rotate on suspicion, not on schedule alone. Revoking is instant and creating a replacement takes a moment.
Give each key the narrowest scopes
Section titled “Give each key the narrowest scopes”Create one key per integration, scoped to what that integration actually does. A
dashboard that only displays opportunities wants opportunities:read — adding
profile:write to it buys nothing and widens the blast radius of a leak.
Separate keys also mean revoking one does not take the others down with it.
Retry on 429 and 5xx
Section titled “Retry on 429 and 5xx”Retry only what is transient: 429 and 5xx. A 401, 403, 400, or 404
means the request or the credential is wrong, and sending it again unchanged
produces the same result.
Honour retry-after when the response carries it. Otherwise back off
exponentially, and add jitter so that a fleet of workers that all failed at the
same moment does not retry in lockstep.
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
async function fetchWithBackoff(url: string, init: RequestInit, maxAttempts = 5) { for (let attempt = 0; ; attempt++) { const response = await fetch(url, init); if (response.ok || !RETRYABLE_STATUSES.has(response.status)) return response; if (attempt >= maxAttempts - 1) return response;
const retryAfterHeader = response.headers.get("retry-after"); const exponentialDelaySeconds = 2 ** attempt; const jitterSeconds = Math.random(); const delaySeconds = retryAfterHeader ? Number(retryAfterHeader) : exponentialDelaySeconds + jitterSeconds;
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)); }}import randomimport timeimport httpx
RETRYABLE_STATUSES = {429, 500, 502, 503, 504}
def fetch_with_backoff(client: httpx.Client, url: str, max_attempts: int = 5) -> httpx.Response: for attempt in range(max_attempts): response = client.get(url) if response.is_success or response.status_code not in RETRYABLE_STATUSES: return response if attempt == max_attempts - 1: return response
retry_after = response.headers.get("retry-after") delay_seconds = float(retry_after) if retry_after else 2**attempt + random.random() time.sleep(delay_seconds)
raise AssertionError("unreachable")Poll sparingly
Section titled “Poll sparingly”New opportunities are discovered and scored a few times per business day, not continuously. Polling every few seconds finds nothing new the overwhelming majority of the time and only burns your rate-limit budget.
Poll hourly at most. Sort by discovered_at and stop once you reach an item
you have already seen, rather than re-reading the full list each time:
curl "https://app.prokure.ca/api/v1/opportunities?sort=discovered_at&limit=25" \ -H "Authorization: Bearer $PROKURE_API_KEY"Iterate pages with the cursor
Section titled “Iterate pages with the cursor”nextCursor is opaque. Pass it back verbatim as cursor, and stop when it
comes back null. Do not decode it, build one, or assume anything about its
contents — the encoding is an implementation detail and offsets are not
supported.
async function* iterateOpportunities(apiKey: string, pageSize = 100) { const baseUrl = new URL("https://app.prokure.ca/api/v1/opportunities"); baseUrl.searchParams.set("limit", String(pageSize));
let cursor: string | null = null; do { const pageUrl = new URL(baseUrl); if (cursor) pageUrl.searchParams.set("cursor", cursor);
const response = await fetch(pageUrl, { headers: { Authorization: `Bearer ${apiKey}` }, }); if (!response.ok) throw new Error(`Prokure API returned ${response.status}`);
const page = await response.json(); yield* page.items; cursor = page.nextCursor; } while (cursor);}import osimport httpx
def iterate_opportunities(page_size: int = 100): headers = {"Authorization": f"Bearer {os.environ['PROKURE_API_KEY']}"} params = {"limit": page_size}
with httpx.Client(headers=headers) as client: cursor = None while True: if cursor: params["cursor"] = cursor response = client.get("https://app.prokure.ca/api/v1/opportunities", params=params) response.raise_for_status()
page = response.json() yield from page["items"]
cursor = page["nextCursor"] if not cursor: returnlimit accepts up to 100. Larger pages mean fewer requests against the same
rate-limit budget, so prefer them for bulk reads.
Log request IDs
Section titled “Log request IDs”Every error body carries a requestId. Log it alongside the status and the
error code. When something needs investigating, that ID points at the exact
request instead of a time range.