Skip to content

Best practices

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 .env file 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.

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 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));
}
}

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:

Terminal window
curl "https://app.prokure.ca/api/v1/opportunities?sort=discovered_at&limit=25" \
-H "Authorization: Bearer $PROKURE_API_KEY"

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);
}

limit accepts up to 100. Larger pages mean fewer requests against the same rate-limit budget, so prefer them for bulk reads.

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.