Troubleshooting
Integrations — Troubleshooting
The most common integration failures, and the one-line fix. For the platform's standard error envelope, see Errors.
401 Unauthorized
The credential is missing, malformed, revoked, or expired. The exact message tells you which:
| Message | Cause | Fix |
|---|---|---|
Unauthorized | No credential on the request, or a token that is not a crtk_live_ key or crtk_oauth_ token | Send X-Api-Key: crtk_live_… or Authorization: Bearer crtk_live_… / crtk_oauth_… |
Invalid or expired API key | Unknown, revoked, or past expires_at — or sent to another workspace's subdomain | The customer revoked it or it lapsed — reconnect with a fresh key. Check the host |
Invalid or revoked access token | OAuth token unknown or revoked | Re-run the OAuth flow (or refresh) |
Access token expired | OAuth access token older than 1 hour | Use the refresh_token grant to mint a new one |
Token is not bound to a workspace | The credential has no workspace | Create a new key or reconnect |
Treat a sudden 401 as "disconnected": clear the stored credential and prompt to reconnect. A revoked key or a revoked OAuth grant stops working on the very next call.
403 Forbidden
Authenticated, but the credential lacks the scope for that route. Scopes are
route-name strings (read.contacts, create.deals, …). The message names the
missing one: "Access denied. Required permission: read.contacts".
- Records: reading needs
read.{objectSlug}, writing needscreate/update/delete.{objectSlug}, and changing assignees needsassign.{objectSlug}. Check the granted scopes in the/meresponse and confirm the object slug (schemas are customizable). - Public webhooks: managing subscriptions requires an API key or OAuth
token — a human (JWT) caller is rejected with "Public webhooks must be
managed with an API key or an OAuth token". Creating a subscription also needs
read.{object_slug}— a webhook can only carry data the key could read. - Minting a key with a scope the creator doesn't hold → "You cannot grant scopes you don't hold: …". Grant only scopes the user creating the key has.
429 Too Many Requests
"API key rate limit exceeded" — the key went over its per-minute request limit. The count resets at the start of each clock minute. Wait for the next minute, then slow down and batch requests. This limit applies to API-key traffic only.
POST /oauth/token and POST /oauth/revoke also answer 429 above 30 requests
a minute from one IP address.
OAuth token exchange errors
A successful POST /oauth/token returns the raw RFC 6749 token JSON. An error
uses the standard error body — status and message — with
400 or 401. The message tells you what failed:
| Message | Meaning & fix |
|---|---|
code and redirect_uri are required (400) | Include both in the authorization_code body |
refresh_token is required (400) | Include refresh_token in the refresh_token body |
invalid_grant: code is invalid, expired, or already used | Codes are single-use and live 60s — exchange immediately |
invalid_grant: redirect_uri mismatch | The exchange redirect_uri must byte-match the one in the authorize request and the registered URI |
invalid_grant: client_id mismatch | The client_id differs from the one the code was issued to |
invalid_grant: PKCE required for public clients | Public clients must send code_challenge (authorize) + code_verifier (token) |
invalid_grant: PKCE verification failed | The code_verifier doesn't match the code_challenge (S256) |
invalid_client: bad client secret | Confidential client — wrong/missing client_secret |
invalid_client | Unknown/inactive client_id |
invalid_grant: refresh token / refresh token expired | Refresh token unknown, already rotated, or past 30d — reconnect. Note: presenting an already-rotated refresh token is treated as a leak and revokes the whole token family |
Consent screen errors (GET/POST /oauth/authorize)
400on the GET — unknown/inactiveclient_id,redirect_urinot an exact match of a registered URI, a requested scope the app isn't allowed, PKCE missing for a public client, orresponse_type≠code. Show an error screen; never redirect the browser (the backend withholdsredirect_toas an open-redirect defense).400on the POST — approving when none of the requested scopes are grantable by this user's permissions ("None of the requested scopes can be granted by your account"). The user needs the underlying object permissions.401— the user isn't logged in; send them to login and return to the exact consent URL with all query params preserved.
The call succeeds but returns fewer rows than expected
Scopes are only the coarse endpoint gate. Past it, every record query is
re-filtered by the acting admin's live A/G/M/D level — a key that acts as an
admin with M (mine) access sees only that admin's records, not the whole object.
Raise the admin's access level or have the key act as an admin with broader
rights.
Inbound webhooks never arrive
A subscription delivers a signed POST to your target_url (which must be
HTTPS). If nothing arrives:
- Confirm the subscription exists and lists the object and event you expect:
call
GET /webhooks/subscriptionswith the same credential that created it. - Confirm your endpoint answers with a
2xxwithin 5 seconds. A delivery is tried up to 3 times; a4xxother than408or429stops the retries at once. - Confirm the change is one that sends an event. Some bulk changes and imports do not; Webhook events lists which.
- Remember a change made with your own API key or OAuth token is intentionally not sent to that credential's own subscriptions.
If deliveries arrive but your check rejects them, verify the signature over
<t>.<raw body> with the whole signing secret, as shown in
Webhook events. See also
Webhooks.
Next
- Avoid these in the first place → Best Practices
- Every endpoint a key can reach → Integration API Reference