Skip to main content

Kick API OAuth 2.1 + PKCE — Step-by-Step Integration Guide

Kick's public API rolled out its OAuth 2.1 + PKCE authorization flow in late 2025, replacing the older undocumented internal endpoints most third-party tooling had been scraping against. The new flow is standards-compliant, documented, and rate-limited generously — but it diverges from Twitch's Helix OAuth in several specifics that trip first-time integrators. This guide walks through the full lifecycle from app registration to refresh-token rotation, with the error modes and scope tradeoffs we've hit while integrating Kick into Streamrise's reseller backend.

decordecor

Why PKCE is non-optional on Kick

PKCE (Proof Key for Code Exchange, RFC 7636) is mandatory on Kick's OAuth 2.1 implementation for every client type — public SPAs, native mobile apps, and confidential server-side clients. Kick's authorization server rejects any authorization request without a `code_challenge` parameter, regardless of whether the client also sends a secret. This differs from older OAuth 2.0 deployments where PKCE was optional for confidential clients, and it matches the 2024 OAuth 2.1 consolidation that made PKCE the single authorization-code-flow standard.

The practical consequence: you cannot skip the code_verifier / code_challenge generation even if you're running a server-side Node.js integration with a secret baked into the environment. Generating and validating the PKCE pair is the minimum viable glue. Kick's decision here is security-positive (PKCE closes the authorization-code-interception attack class entirely) but surprises integrators coming from pre-2022 OAuth libraries that made PKCE conditional.

Step 1 — Register your application

Open the Kick Developer Portal at kick.com/developer (log in with your Kick account). Create a new application and fill in:

  • Application name — shown to users on the consent screen. Stable over time; changing it after users have authorized breaks nothing but regenerates the consent scope audit log.
  • Redirect URIs — one or more exact-match URIs your OAuth callback lives at. Must be HTTPS in production; localhost with explicit port is allowed for development.
  • Description — 1-2 sentences surfaced on the consent screen.
  • Category — select the closest fit; affects nothing functionally but helps Kick rate-limit by category in the future.

After saving you get a `client_id` (public, safe to ship to the browser) and, for confidential apps, a `client_secret` (server-side only — never commit to version control, never send to the browser). The client_secret is optional for public SPAs relying on PKCE alone, but required for server-side apps that want the extra authentication layer.

Important: the redirect URI you register is matched exactly by string comparison, not by prefix. `https://example.com/oauth/callback` does not match `https://example.com/oauth/callback/`. Trailing slashes, case, and subdomains all matter — this is the most common misconfiguration on first integration.

Step 2 — Generate the PKCE pair

Before sending the authorization request, generate a fresh code_verifier + code_challenge pair. The code_verifier is a high-entropy random string 43-128 characters long from the set [A-Z / a-z / 0-9 / - . _ ~]. The code_challenge is the SHA-256 hash of the verifier, base64url-encoded (no padding).

Reference implementation in Node.js:

const crypto = require('crypto'); const verifier = crypto.randomBytes(64).toString('base64url'); const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');

Store the verifier somewhere you can retrieve it at token-exchange time — typically in a session keyed by the `state` parameter, or in an encrypted cookie. It must survive the redirect round-trip to Kick and back. If you lose the verifier, token exchange will fail with `invalid_grant`.

  • Never log the verifier in plaintext — treat it as a short-lived secret
  • Generate a new verifier for each authorization attempt; reusing verifiers defeats PKCE's purpose
  • The code_challenge_method must be `S256` in the authorization request — Kick rejects `plain` even though OAuth 2.0 allowed it

Step 3 — Build the authorization request

Redirect the user's browser to the authorization endpoint with the following query parameters:

  • response_type=code — always `code` for authorization code flow
  • client_id — from your Developer Portal registration
  • redirect_uri — exact match for one of your registered URIs
  • scope — space-separated list of scopes you need (see §Scopes below)
  • state — opaque value echoed back unchanged; use it to prevent CSRF + to key lookups
  • code_challenge — the PKCE challenge from step 2
  • code_challenge_method=S256

Example:

https://id.kick.com/oauth/authorize?response_type=code&client_id=YOUR_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&scope=channel%3Aread+chat%3Awrite&state=abc123&code_challenge=CHALLENGE&code_challenge_method=S256

The user lands on the Kick consent screen, reviews the scopes your app is requesting, and approves or denies. On approval, Kick redirects to your redirect_uri with `?code=&state=abc123`. On denial you get `?error=access_denied&state=abc123`. Always verify the state parameter matches the one you generated — an attacker who steals the code from URL logs can try to feed it into your callback, and state-validation is your defense.

Step 4 — Exchange the code for tokens

Your redirect handler receives the authorization code. Immediately POST to Kick's token endpoint with the following form-urlencoded body:

  • grant_type=authorization_code
  • code — the authorization code from the redirect
  • redirect_uri — exact match for the one used in step 3
  • client_id — from your registration
  • client_secret — if confidential client
  • code_verifier — the PKCE verifier you stored in step 2

Endpoint: `POST https://id.kick.com/oauth/token`. Content-Type must be `application/x-www-form-urlencoded`. A successful response returns JSON with `access_token`, `refresh_token`, `token_type` (always `Bearer`), `expires_in` (seconds, typically 3600), and `scope` (space-separated list of scopes actually granted — may be less than requested if the user deselected some).

Store the access_token and refresh_token in server-side storage encrypted at rest. The access_token is short-lived (1 hour default); the refresh_token is long-lived (90 days typical, rotated on each refresh). Never expose either to the browser — public SPAs should treat the token handling as a backend-for-frontend pattern where the browser holds a session cookie and the backend holds the OAuth tokens.

Step 5 — Refresh token rotation

Kick issues a NEW refresh_token on every refresh exchange. The previous refresh_token is invalidated immediately. This is refresh-token rotation, and it's strict — if your integration uses the old refresh_token after a refresh has happened, you get `invalid_grant` and the user has to re-authorize from scratch. This matters because a naive concurrent-request pattern (two API calls triggering two refresh attempts simultaneously) breaks one of them.

Practical mitigation: queue refresh exchanges so only one runs at a time per user, and have concurrent API calls wait for the refresh to complete before retrying. Pseudocode:

  • Check if access_token is valid (expires_at > now + 60s buffer)
  • If yes — proceed with API call
  • If no — acquire user-scoped refresh mutex
  • Double-check (another concurrent request may have already refreshed)
  • If still stale — POST to /oauth/token with grant_type=refresh_token, atomically update both tokens in storage, release mutex
  • Retry API call with the new access_token

Token refresh POST body: `grant_type=refresh_token`, `refresh_token=`, `client_id`, optionally `client_secret`. No code_verifier needed — PKCE applies only to the authorization-code grant.

Available scopes and what they unlock

Kick's scope model is finer-grained than Twitch's as of 2026. Request only the scopes you actually need — users see the list on the consent screen and over-requesting correlates with higher consent-decline rates (industry-standard pattern, not Kick-specific).

  • channel:read — read the authenticated user's channel metadata (name, description, category, follower count)
  • channel:write — update channel metadata (category, title, tags)
  • chat:read — subscribe to chat messages via the WebSocket gateway
  • chat:write — send messages as the authenticated user
  • stream:read — read live-stream metadata (start time, concurrent viewers, health)
  • user:read — read the authenticated user's profile (id, username, email verification state — note: not the email itself without a specific grant)
  • follows:read — read the user's following list
  • subscriptions:read — read the user's paid-subscriber list (requires Affiliate status on the channel being queried)
  • webhooks:manage — create, list, and delete webhook subscriptions on the user's account

Scope changes after initial authorization require a re-consent flow — you cannot silently expand scopes on an existing token. If your integration evolves, prompt the user to re-authorize with the updated scope list; the authorization server will show the delta.

Rate limits and backoff strategy

Kick's rate limits as of 2026-04 are documented generously but enforced strictly. The per-client-id limit is 600 requests per minute across all endpoints; the per-user-access-token limit is 180 requests per minute. Exceeded limits return `429 Too Many Requests` with a `Retry-After` header specifying seconds to wait.

The failure mode most integrators hit: spawning many per-user refreshes simultaneously around a burst of activity (e.g., reseller dashboard polls 50 users' channels every minute). Aggregate across users and you can blow the per-client limit even though per-user is fine. Mitigation is straightforward — queue + rate-limit at the client-id level with a token-bucket implementation.

  • Always honor Retry-After — do not retry on 429 before it expires
  • Use token-bucket per client_id (600/min) and per user_id (180/min) as gating layers
  • Webhooks > polling for high-volume use cases — subscribe to channel.update and chat.message via webhooks:manage rather than polling
  • Cache read-only data (channel metadata, follower counts) with short TTLs — 30s is usually acceptable

Common errors and how to debug them

  • `invalid_grant` on token exchange — the code_verifier doesn't match the code_challenge, the code has been used already (single-use), or the redirect_uri doesn't match exactly. Check your PKCE storage + redirect_uri match.
  • `invalid_client` — client_id wrong, client_secret wrong, or client_secret sent for a public app that doesn't have one. Verify your Developer Portal values.
  • `invalid_request` — missing required parameter. Usually state, code_challenge, or code_challenge_method in the authorization request.
  • `unauthorized_client` — your app's registered grant types don't include authorization_code. Check the Developer Portal grants tab.
  • `invalid_scope` — you requested a scope that doesn't exist or that requires a higher app tier. Kick's experimental scopes rotate names; verify against current docs.
  • `access_denied` — user declined consent. Log and move on; not a bug.
  • `expired_token` — your access_token expired. Refresh (see §5).

Debugging principle: Kick's error responses are OAuth-standard and helpful, but logging the request body + response body server-side (redacting the code, code_verifier, and tokens) saves hours on intermittent issues. Always correlate with the `state` parameter so multi-user debugging stays scoped.

Differences from Twitch Helix OAuth

For integrators porting from Twitch Helix, the conceptual model is the same — authorization code flow with refresh tokens — but the mechanical differences are real enough to require attention:

  • PKCE is mandatory on Kick; optional on Twitch for confidential clients
  • Kick rotates refresh tokens; Twitch does not rotate by default
  • Kick's per-scope rate limit is per-endpoint; Twitch groups endpoints into buckets
  • Kick scope names differ (channel:read vs user:read:email on Twitch)
  • Kick's authorization endpoint is at id.kick.com; Twitch's is at id.twitch.tv
  • Kick does not issue ID tokens (pure OAuth 2.0 authorization code, no OIDC); Twitch offers OIDC-enabled flows
  • Kick webhooks use a different verification scheme (HMAC signature in header) vs Twitch's EventSub (HMAC signature + challenge-response)

A shared OAuth library configured for Twitch is a reasonable starting point but will need per-provider adjustments for the PKCE-mandate and refresh-rotation semantics. We use `openid-client` (Node.js) with a Kick-specific client configuration and explicit refresh-serialization middleware for the Streamrise reseller backend.

FAQ

Can I use the implicit grant or password grant on Kick?

No. OAuth 2.1 deprecated both. Kick supports only authorization_code (with PKCE) + refresh_token + client_credentials (for app-only endpoints).

Do I need PKCE if I have a client_secret?

Yes. Kick's authorization server rejects requests without code_challenge + code_verifier even when a client_secret is provided. The OAuth 2.1 spec consolidated PKCE as universal.

How long are refresh tokens valid?

Up to 90 days of inactivity in typical deployments. Each refresh rotates the token and extends the window. If no refresh happens for 90 days, the refresh_token expires and the user must re-authorize.

Can I request offline_access like on Azure or Okta?

Kick does not use offline_access as a scope name; refresh tokens are issued by default on every authorization_code exchange. Include the scope you need normally; no special offline-access flag required.

What's the best way to test the flow in development?

Register a development-only app with localhost redirect URIs and a dev-only client_id. Use a browser-based tool like Postman's OAuth 2.1 flow or the Keycloak postman-style auth helper — both support PKCE correctly. Never reuse a production client_id for dev testing; Kick's rate limits and scope approvals are per-client.

Registration