Skip to main content
Reference for the SideShift OAuth API (/api/oauth/v1): the error envelope, the stable error-code registry, the WWW-Authenticate challenge, protocol-endpoint errors, rate-limit policy + headers, and idempotency. New to the API? Start with OAuth quickstart (register, authorize, get a token) and workflows.md (end-to-end recipes). The machine-readable contract is openapi.yaml.

1. Error envelope (resource endpoints)

Every error from a resource endpoint (campaigns, contracts, creators, applications, posts, payouts, invoices, messages, webhooks, settings, invites) is returned as a structured JSON envelope:
  • code - a stable machine-readable code from the registry. Branch on this, never on message.
  • message - a human-readable explanation. Wording may change; it is for logs and humans, not control flow.
  • requestId - a req_… correlation id. Always include it when contacting support so we can find the exact request in our logs.
  • meta - an optional object with structured context (e.g. the missing scope, the conflicting field). Present only when useful; treat it as additive.
The HTTP status line always matches code (see the table below).

2. Error code registry

These are the only codes a resource endpoint emits. Status is fixed per code (single source of truth: lib/api/core/errors.ts).

unauthorized vs insufficient_scope vs forbidden

These three are routinely confused. They are distinct:
  • unauthorized (401) - the credential itself is bad: no Authorization header, an expired access token (1h TTL), or a revoked token. The fix is to re-authenticate (refresh the token, or restart the authorization flow).
  • insufficient_scope (403) - the token is valid but was not granted the scope this call needs (e.g. you hold campaigns:read and called POST /campaigns, which needs campaigns:write). The fix is to re-authorize, requesting the missing scope on the consent screen. The response carries a WWW-Authenticate header naming the required scope.
  • forbidden (403) - the action is never allowed for this grant, no matter what scope you add. This is a hard policy/capability block, e.g.:
    • a sandbox / test-mode grant attempting a money-moving or external side-effect write (payouts execute / Quick Pay, invoice create/send/void, sending a message); or
    • a policy block that is provably not cross-tenant (e.g. invoicing not available for the account, account too new).
    Re-authorizing will not fix a forbidden. Example body: "payouts:write is not available for sandbox (test-mode) grants".

Cross-tenant access is always not_found (404), never forbidden

A token is bound to exactly one company tenant. If you request a resource id that belongs to another company, the API returns 404 not_found - the same response as a genuinely nonexistent id. It is never 403. This is deliberate: you cannot use the status code to probe whether another company’s resource exists. 403 forbidden is reserved for the in-tenant policy/capability blocks described above.

3. WWW-Authenticate

On a 401 unauthorized and on a 403 insufficient_scope, the response carries an RFC 6750 / RFC 9728 WWW-Authenticate: Bearer … challenge that tells the client what to do:
  • error - the OAuth error (invalid_token / insufficient_scope).
  • scope - the scope the client must request to perform this operation. Read it, add it to your next authorization request, and have the user re-consent.
  • resource_metadata - the URL of the protected-resource metadata document (RFC 9728), which lists the authorization server and supported scopes for discovery.
A plain 403 forbidden (the hard policy block) does not carry a WWW-Authenticate header - there is no scope that would grant access.

4. Protocol endpoint errors (RFC 6749)

The auth-server endpoints - /register, /authorize, /token, /revoke, /clients/{id} - are not resource endpoints. They follow OAuth conventions and return the RFC 6749 error body instead of the envelope:
Common error values: subscription_required is a resource API error, not a protocol or token-endpoint error. A client can finish consent and receive tokens while its company subscription is inactive, but protected resource calls return 402 until the subscription is active. /authorize is special: when the client_id/redirect_uri cannot be trusted it renders an HTML error page (400) rather than redirecting; otherwise recoverable errors come back as a redirect to redirect_uri carrying error, error_description, state, and iss. The in-session consent bridge is not an RFC 6749 endpoint. It uses a small { "error", "message" } body, where error is one of: invalid_request (400), unauthorized (401), subscription_required (402), forbidden / csrf (403), not_found (404), already_used (409), or expired (410).

5. Rate limits

Limits are per rolling 60-second window, scoped by subject. Source of truth: lib/api/oauth/rate-limit.ts. Because the resource limit is per (client, company), one misbehaving client cannot exhaust the budget of a tenant it shares with other clients.

Headers

Every response carries the current window state: A 429 response (rate_limited envelope, or temporarily_unavailable on the protocol surface) additionally carries:

Handling 429

Honor Retry-After: wait at least that many seconds, then retry with exponential backoff and jitter for repeated failures. Proactively, watch X-RateLimit-Remaining and throttle yourself before you hit zero rather than hammering until you get a 429.

6. Idempotency

Every mutating resource endpoint (POST / PATCH / PUT) accepts an Idempotency-Key request header. The key is client-chosen - use a fresh UUID per logical operation. Semantics:
  • Replay (same key + same body) → the original stored response is returned (same status + body). Safe to retry after a network timeout without double-applying the operation.
  • Same key + a different body409 idempotency_conflict. A key is bound to the first request body it saw.
  • Keys are retained for roughly 24 hours, then forgotten.
Always send an Idempotency-Key on calls that move money or fire external side effects - POST /payouts/execute, POST /payouts/quick-pay, and the invoice writes (POST /invoices, /invoices/{id}/send, /invoices/{id}/void). For the payout endpoints the key is required (a request without one is rejected 400); it is the sole double-pay guard for Quick Pay. The key is also required by POST /campaigns/{id}/analytics-history so a retried multi-store import cannot apply a second logical batch; that endpoint enforces an 8–200 character key length. Note these same endpoints are also rejected with 403 forbidden for sandbox / test-mode grants (see §2) - test-mode tokens cannot move real money or fire real external effects.

Example

Reuse the same Idempotency-Key value when retrying that exact request after a timeout; generate a new one for a genuinely new payment.
See also: OAuth quickstart · workflows.md · openapi.yaml.