/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(anddocs/api/sideshift-api-private.yamlfor the restricted/Full-Access endpoints) - OAuth:
docs/api/oauth/openapi.yaml
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 thex-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.
campaigns:read) and is bound to exactly one company tenant (company_id),
so a single token can never reach across companies.
getting-started.md):
- Register a client once via Dynamic Client Registration (
POST /register, RFC 7591) to obtain aclient_id. - Authorization code + PKCE (
GET /authorize→ consent →POST /token) is the standard flow for user-facing apps. PKCE withS256is 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 therefresh_tokengrant.offline_accessis 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+jwtand expire after 3600s. The company’s subscription is checked when the token calls a protected resource (402 subscription_requiredif 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 takepage (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.
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.
/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_tokengrant type to receive a refresh token;offline_accessmay still be requested as an optional OIDC signal. Without the registered grant type,/tokenreturns norefresh_token. - A few endpoints map across groups: campaign/program invite links live under
creators:writeon OAuth (POST /campaigns/{id}/invites,POST /invites,DELETE /invites/{id}), and listing invites usescreators:read. Creating and revoking invites is flagged sensitive undersettings:write/creators:write. payouts:write,invoices:write, andmessages:writeare sensitive - they move money or fire external side effects. Sandbox/test-mode grants are rejected (403 forbidden) andpayouts:writerequires anIdempotency-Key.- A request whose token lacks the needed scope returns
403 insufficient_scopewith aWWW-Authenticatechallenge naming the required scope.
5. Rate limits
Legacy. 100 requests/minute per API key; allowlisted partner accounts get 400/minute. Every response carriesX-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/v1surface 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 gets403. (Payout execution and Quick Pay also exist on OAuth underpayouts: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-Keyon mutations - every OAuthPOST/PATCH/PUThonors anIdempotency-Key(required onpayouts:writeand campaign analytics-history imports); replaying the same key + body returns the original response, a different body is409. 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/v1surface does not expose as writes.
- Analytics recruitment -
/analytics/recruitmenthas no direct OAuth equivalent. OAuth does exposeaccounts,kpis,overview,time-series, andvideosunder theanalytics:readscope.