Skip to main content
This guide is for integrators moving from the legacy, API-key-authenticated /api/v1 surface to the OAuth 2.1 /api/oauth/v1 surface. The two surfaces coexist during migration. The legacy /api/v1 API is deprecated and unmaintained: existing integrations may keep using its frozen contract while they migrate, but it receives no new features or maintenance fixes. New integrations must use OAuth for its per-scope permissions and single-tenant safety. Everything below is grounded in the two OpenAPI specs:
  • Legacy: docs/api/sideshift-api-public.yaml (and docs/api/sideshift-api-private.yaml for the restricted/Full-Access endpoints)
  • OAuth: docs/api/oauth/openapi.yaml
Both surfaces share the same production host (https://app.sideshift.app) and both require an active subscription for protected resource calls - a lapsed subscription returns 402 on either API’s resource surface.

1. Auth model

Legacy - static API key, all-or-nothing. You send your company’s API key in the x-api-key header on every request. The key is created/rotated in the dashboard under Settings → Integrations, grants access to that company’s entire /api/v1 surface (no per-endpoint permissions), and never expires until you rotate it.
New - OAuth 2.1 bearer token, scoped and tenant-bound. You exchange an OAuth grant for a short-lived (1h) access token and send it as a bearer token. The token carries only the scopes the user consented to (e.g. campaigns:read) and is bound to exactly one company tenant (company_id), so a single token can never reach across companies.
How you get a token (full walkthrough in getting-started.md):
  • Register a client once via Dynamic Client Registration (POST /register, RFC 7591) to obtain a client_id.
  • Authorization code + PKCE (GET /authorize → consent → POST /token) is the standard flow for user-facing apps. PKCE with S256 is required. The consent step is where the user picks which company the grant is bound to.
  • Refresh tokens (grant_type=refresh_token) rotate the access token without re-prompting when the client registered the refresh_token grant. offline_access is an optional OIDC signal, not a prerequisite.
  • Client credentials (grant_type=client_credentials) is available for machine-to-machine clients.
  • Access tokens are RFC 9068 at+jwt and expire after 3600s. The company’s subscription is checked when the token calls a protected resource (402 subscription_required if lapsed).
Both APIs require an active subscription. On /api/v1 a lapsed subscription is 402 { "error": "Active subscription required" }; on /api/oauth/v1 it is 402 { "error": { "code": "subscription_required", ... } } at the resource endpoints. OAuth registration, consent, and token issuance can complete while a subscription is lapsed; the issued token cannot access protected resources.

2. Pagination

Legacy - offset pagination. List endpoints take page (default 1) and limit (default 25, max 100) and return { data, page, total }. Some endpoints additionally return limit and/or totalPages (e.g. /programs, /analytics/videos, /payouts/pending). To pull a full data set you page until page * limit >= total, which risks silent truncation / drift if rows are inserted between page fetches.
New - opaque cursor-shaped pagination. List endpoints take an opaque cursor and limit (default 25, max 100) and return { data, nextCursor, hasMore }. Pass the previous response’s nextCursor back as ?cursor= to get the next page, and stop when hasMore is false (nextCursor is then null). The cursor is an encoded continuation value over the current page-based implementation, so you must still account for records changing between page fetches.
Treat nextCursor as opaque - do not parse or construct it. A null nextCursor with hasMore: false means you have read the last page.Note: GET /posts/{id}/metrics-history is an exception on both surfaces - it uses days + limit (max 500) rather than cursor/offset pagination.

3. Error shapes

Legacy - flat string error. Every error is { "error": "<message>" } with the matching HTTP status (400 invalid request, 401 invalid/missing key, 402 no active subscription, 403 restricted endpoint or cross-company, 404 not found, 429 rate limited). A few endpoints add a machine code such as LEAD_NOT_FOUND, but there is no stable, documented code registry.
New (resource endpoints) - structured envelope with a stable code. Every error from a /api/oauth/v1 resource endpoint is:
code comes from a fixed registry: invalid_request, unauthorized, insufficient_scope, forbidden, subscription_required, not_found, conflict, idempotency_conflict, rate_limited, internal. requestId is echoed for support/correlation. On 401 and 403 (insufficient_scope) the response also carries a WWW-Authenticate challenge (RFC 6750/9728). See errors-rate-limits.md for the full registry and Idempotency-Key / Retry-After semantics. New (protocol endpoints) - RFC 6749 bodies. The OAuth protocol endpoints (/register, /authorize, /token, /revoke, client management) do not use the resource envelope. They return RFC 6749-style { "error", "error_description" } bodies with codes like invalid_grant, invalid_client, invalid_scope, unsupported_grant_type, subscription_required:

4. Scope mapping

Where a legacy API key reached an entire /api/v1 endpoint group with no permission boundary, the OAuth token must carry the specific scope(s) below. Scopes are defined in the oauth2 security scheme of docs/api/oauth/openapi.yaml. Notes:
  • Register the refresh_token grant type to receive a refresh token; offline_access may still be requested as an optional OIDC signal. Without the registered grant type, /token returns no refresh_token.
  • A few endpoints map across groups: campaign/program invite links live under creators:write on OAuth (POST /campaigns/{id}/invites, POST /invites, DELETE /invites/{id}), and listing invites uses creators:read. Creating and revoking invites is flagged sensitive under settings:write/creators:write.
  • payouts:write, invoices:write, and messages:write are sensitive - they move money or fire external side effects. Sandbox/test-mode grants are rejected (403 forbidden) and payouts:write requires an Idempotency-Key.
  • A request whose token lacks the needed scope returns 403 insufficient_scope with a WWW-Authenticate challenge naming the required scope.

5. Rate limits

Legacy. 100 requests/minute per API key; allowlisted partner accounts get 400/minute. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; exceeding the limit returns 429. New. Resource endpoints are limited to 600 requests/minute per (client, company) pair - i.e. the budget is scoped to the client and the tenant the token is bound to, not to a single key. Responses carry X-RateLimit-* headers and 429 carries Retry-After. The protocol endpoints (/register, /authorize, /token, /revoke) have their own, separate per-IP / per-client limits and return RFC 6749 429 bodies. See errors-rate-limits.md for header details and back-off guidance.

What remains available on /api/v1 during migration

  • The legacy /api/v1 surface remains reachable for backward compatibility, but it is deprecated and unmaintained. Existing API-key integrations should migrate to OAuth; do not build new integrations against this frozen contract.
  • The restricted endpoints - Jobs (/jobs), Applicants (/applicants), and payout execution (/payouts/execute, /payouts/quick-pay) - stay API-key + partner-allowlist only and are documented in the Full Access spec (docs/api/sideshift-api-private.yaml). A non-allowlisted key calling them gets 403. (Payout execution and Quick Pay also exist on OAuth under payouts:write, but the legacy restricted variants are unchanged.)
  • API-key management (creating/rotating keys, digest automations) stays in the dashboard with a Firebase session - it is not part of the API-key contract and has no OAuth equivalent.
  • For new integrations, use OAuth: you get least-privilege scoping and guaranteed single-tenant binding instead of a single all-powerful per-company key.

7. Endpoint parity notes

New / OAuth-only (no /api/v1 equivalent):
  • Outbound webhook subscriptions - full CRUD (/webhooks), signed test delivery (POST /webhooks/{id}/test), and a delivery log (GET /webhooks/{id}/deliveries). The legacy API has no push/webhook surface at all (it is pull-only).
  • Idempotency-Key on mutations - every OAuth POST/PATCH/PUT honors an Idempotency-Key (required on payouts:write and campaign analytics-history imports); replaying the same key + body returns the original response, a different body is 409. The legacy surface has no idempotency mechanism.
  • First-class campaign write operations - create/update/archive/duplicate campaigns, set payment structures, create/cancel contracts, review applications, create collections - most of which the read-leaning /api/v1 surface does not expose as writes.
Without a direct OAuth equivalent:
  • Analytics recruitment - /analytics/recruitment has no direct OAuth equivalent. OAuth does expose accounts, kpis, overview, time-series, and videos under the analytics:read scope.