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 fits 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>, where v1 is the
lowercase hex HMAC-SHA256(signing_secret, "<t>.<raw-body>")) over the raw
request body, keyed with the whole secret including its whsec_ prefix. Compare
in constant time, and reject a t more than ~5 minutes from your clock to
stop replays; retries are signed again, so the window does not reject them.
Never trust a payload you haven't verified. Details and a test vector:
Webhook events.
✓ Make webhook handlers idempotent
A delivery that times out (5 seconds), fails to connect, or gets a 5xx, 408
or 429 is retried, up to 3 attempts in total. No header or body field stays the
same across attempts — X-Corteksa-Delivery, t and timestamp change every
time — so do not dedupe on them. Treat a delivery as a signal: fetch the record
by record_slug and apply its current state, so running the same delivery twice
does no harm. Respond 2xx quickly and do heavy work asynchronously.
✓ Respect the rate limit
Each API key has a per-minute request limit. The count resets at the start of
each clock minute. Batch where you can, and on 429 wait for the next minute
and back off 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".
Next
- When something breaks → Troubleshooting
- Working code → Examples