Best Practices
Integrations — Best Practices
The recommended way to build a safe, resilient integration.
✓ Treat credentials as secrets
An API key (crtk_live_…), OAuth access/refresh token, and client_secret are
all passwords. Store them encrypted, server-side, per customer — never in a
browser, an app bundle, or logs. If you need a reference in logs, log only the
non-secret token_prefix (crtk_live_a1b2c3d4), never the full value.
✓ Request least-privilege scopes
Ask for only the {verb}.{objectSlug} scopes you actually use — read.contacts
before create.contacts, and neither if you only read one object. The customer
sees exactly what you request on the consent screen, and a key can only ever be
granted scopes its creator holds. Verify what you got via GET /me and warn
early if a scope you need is missing.
✓ Prefer OAuth for published, multi-customer apps
A pasted API key is perfect for a self-serve, single-workspace connection. For a listed integration serving many customers, use the OAuth "Connect" flow: tokens are revocable (a customer disconnecting kills live tokens at once, not at expiry) and short-lived, which limits blast radius if one leaks.
✓ Rotate the refresh token every time
The refresh_token grant rotates — each refresh returns a new
refresh_token and invalidates the old one. Always persist the new value.
Re-presenting an already-rotated refresh token is treated as a leak and revokes
the entire token family, so a stale copy will lock you out.
✓ Handle 401 as "disconnected"
A revoked key or grant, or a lapsed expiry, returns 401 on the next call. Clear
the stored credential and prompt the customer to reconnect — don't retry a dead
credential in a loop.
✓ Verify every webhook signature
Recompute X-Corteksa-Signature (t=<unix>,v1=<hex> =
HMAC-SHA256(signing_secret, "<t>.<raw-body>")) over the raw request body,
compare in constant time, and reject timestamps older than ~5 minutes to stop
replays. Never trust a payload you haven't verified.
✓ Make webhook handlers idempotent
Deliveries retry on any non-2xx or timeout. Dedupe on the X-Corteksa-Delivery
header so a retried delivery doesn't double-apply. Respond 2xx quickly and do
heavy work asynchronously.
✓ Respect the rate limit
API-key traffic is capped per key (API_KEY_RATE_LIMIT_PER_MINUTE, default
120/min). Batch where you can, and back off on 429 rather than hammering.
✗ Don't put the token flow in the browser
Never call POST /oauth/token, hold a client_secret, or handle a
crtk_oauth_/crtk_refresh_ token in client-side code. The browser's only job
in "Connect" is the consent screen, which yields a short-lived code delivered
to your backend. Public (SPA/mobile) clients must use PKCE instead of a
secret.
✗ Don't over-grant wildcards
{verb}.* scopes are convenient for a sync-anything mapping UI, but they expand
to every object the owner can reach. Prefer explicit {verb}.{object} scopes
unless the app genuinely needs breadth.
✗ Don't re-serialize the webhook body before verifying
Signatures are computed over the exact bytes sent. Parse the JSON after you've verified the signature over the raw body — re-encoding changes the bytes and breaks the HMAC.
✗ Don't rely on scopes for row-level security
Scopes gate which endpoints a credential can reach, not which rows. Data is
still filtered per record by the acting admin's A/G/M/D level — never assume a
read.contacts scope means "all contacts". See
Backend Integration.
Next
- When something breaks → Troubleshooting
- Working code → Examples