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 human grant retains only the scopes the user consented to (e.g. campaigns:read), while each token is further capped by the user’s live team role and permissions (see Scopes and token security). A saved permissions.scopes array is the exact grant - an empty array grants nothing. Existing memberships without a scope array keep their prior behavior, including the legacy reporting-only Employee ceiling, until an authorized manager saves an explicit selection. This ceiling is re-applied against the subject’s current team membership and permissions on every verification, so a token can lose access it once had but can never gain access beyond what was consented. The token is bound to one connected company tenant (company_id). An agency-parent grant can also consent to all direct sub-accounts (including ones created later) or an explicit current subset. Send X-Act-As-Company with a direct child’s company id to make that child the effective tenant for a single request - the child must be both consented at grant time and currently permitted, or the request fails; omit the header to use the parent. This does not create an agency-wide data union. Parameters such as companyId, company_id, client, and subaccountId are not general tenant selectors when an endpoint does not explicitly document them.
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. The live ceiling is re-applied on refresh against the retained consented set, so an authorized role or permission change can take effect without granting anything that was not originally consented.
  • Client credentials (grant_type=client_credentials) is available for company-authorized machine clients registered in the dashboard. Public registration alone does not authorize this flow. See Company automations.
  • 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, oauth_required, oauth_needs_reauthentication, rate_limited, upstream_error, 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):
  • 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.
Still pull-only: Neither surface pushes. The Platform API has no webhook or subscription endpoints, so keep polling as you do today, and keep your existing backoff. Webhooks on SideShift belong to Connect, which is a separate product with its own credentials rather than a feature of this API.