Skip to Content
ReferenceError codes

Error codes

A complete catalog of every error code Synq returns, with examples and what to do about each one.

OAuth/OIDC errors

Returned at /oauth2/authorize (as a redirect with ?error=), /oauth2/token (JSON body), /oauth2/revoke, and /oauth2/introspect. Format follows RFC 6749 §5.2:

{ "error": "invalid_grant", "error_description": "Authorization code expired" }
errorWhenWhat to do
invalid_requestMissing or malformed parameter.Check the request body matches the endpoint’s required params.
invalid_clientBad client_id / client_secret.Verify credentials; check for stale secrets after rotation.
invalid_grantAuth code expired (60s), refresh token consumed, PKCE mismatch, or device code expired.Restart the flow.
unauthorized_clientApp not allowed to use the requested grant type.Check allowedGrantTypes on the App.
unsupported_grant_typeGrant type not enabled.Same as above.
invalid_scopeScope not in app’s allowedScopes or unknown.Verify scope is registered on the Brand and on the App.
access_deniedUser denied consent.Surface a friendly “consent was denied” page.
consent_requiredprompt=none but no existing consent.Re-prompt the user with prompt=consent (or omit prompt).
login_requiredprompt=none but no active session.Re-prompt with full sign-in.
authorization_pendingDevice flow: user has not approved yet.Keep polling at interval seconds.
slow_downDevice flow: poll less often.Increase polling interval by 5 seconds.
expired_tokenDevice flow: device_code expired (10 min).Restart the device flow.

Example: handling invalid_grant on refresh

try { const tokens = await synq.refresh(refreshToken) saveSession(tokens) } catch (err) { if (err.code === 'invalid_grant') { // The refresh token is no longer valid. Force a fresh sign-in. redirectTo('/sign-in') return } throw err }

Example: handling user denial

// On /api/oauth/callback const url = new URL(req.url) const error = url.searchParams.get('error') if (error === 'access_denied') { return res.redirect('/sign-in?status=cancelled') }

REST errors

Returned by all non-OAuth endpoints. Envelope:

{ "error": { "code": "rate_limited", "message": "Too many requests", "details": { "retryAfter": 60 } } }
HTTPcodeWhen
400validation_failedBody did not pass validation. details includes per-field errors.
401unauthorizedNo bearer token, or token invalid / expired.
401invalid_tokenToken signature/exp/aud invalid.
403forbiddenAuthenticated but lacks permission. details.missing lists required permission.
404not_foundResource does not exist or caller cannot see it.
409conflictState conflict (duplicate slug, in-use role).
422unprocessable_entityBody passed validation but request semantically invalid (e.g. trying to delete the last Owner).
429rate_limitedPer-key rate limit hit. Retry-After header set.
500internal_errorSynq-side bug. Safe to retry with backoff.
502bad_gatewayDownstream dependency error (e.g. OAuth provider 500).
503service_unavailableSynq degraded. Retry with backoff.
504gateway_timeoutDownstream dependency timed out.

Example: validation_failed with field errors

{ "error": { "code": "validation_failed", "message": "Invalid request body", "details": { "fields": [ { "field": "redirectUris[0]", "code": "invalid_url", "message": "must be a valid HTTPS URL" }, { "field": "name", "code": "too_short", "message": "must be at least 3 characters" } ] } } }

Example: rate_limited

{ "error": { "code": "rate_limited", "message": "Too many requests", "details": { "retryAfter": 30, "limit": 100, "window": 60 } } }

Headers:

Retry-After: 30 RateLimit-Limit: 100 RateLimit-Remaining: 0 RateLimit-Reset: 1717000000

Respect Retry-After and back off. Don’t retry within the window.

Webhook delivery errors

Synq does not surface webhook delivery errors back to your app; it logs them and exposes them via GET /webhooks/:id/deliveries.

statusWhen
succeededYour URL returned 2xx within 10s.
failed_timeoutNo response within 10s.
failed_statusReturned non-2xx. responseCode populated.
failed_networkDNS, TLS, or transport error.
disabledWebhook is disabled (auto-disabled after 20 consecutive failures, or manually).
pendingIn the retry queue.

Returned by POST /users/me/public-keys:

codeMeaning
bad_signatureSignature does not verify against the address.
stale_challengeThe signed challenge is too old (>5 min).
replayed_nonceNonce was already used. Generate a fresh challenge.
wrong_brandThe challenge’s brand name doesn’t match the brand the user is signing in to.
wallet_in_useThis wallet is already linked to a different user.

Mental model summary

  • OAuth/OIDC errors use RFC 6749 §5.2 envelope.
  • REST errors use the { error: { code, message, details } } envelope.
  • Webhook delivery errors live in the deliveries log, not at-request.
  • For 401, distinguish “no token” (unauthorized) from “bad token” (invalid_token). The first is a sign-in prompt; the second is often a refresh-then-retry.
  • For 429, always respect Retry-After.
Last updated on