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.