# Quick answer: what changes vs Twitch Helix
Kick's public API uses OAuth 2.1 with PKCE on every authorization-code exchange — yes, even confidential server clients. OAuth host: id.kick.com. Honestly — resource API host: api.kick.com/public/v1. Access tokens expire after 3600 seconds by default. Refresh tokens rotate on every exchange. Push delivery is webhooks-only as of April 2026. Porting from Twitch Helix? Three changes bite first: PKCE is required, refresh rotation is strict, scope names sit closer to GitHub than Twitch. (New to the platform itself? Start with our explainer on what Kick is in 2026, then come back here for the integration mechanics.)
- OAuth host: id.kick.com (separate from the resource host)
- Resource host: api.kick.com/public/v1
- Grants supported: authorization_code (with PKCE), refresh_token, client_credentials
- PKCE: code_challenge_method must be S256. Plain is rejected
- Push delivery: webhooks only. WebSocket transport is on the roadmap
- Token endpoints: /oauth/authorize, /oauth/token, /oauth/revoke, /oauth/token/introspect
# Why PKCE is non-optional on Kick
PKCE on Kick is not advisory. The authorization server rejects any /oauth/authorize request that lacks code_challenge — even when the client also presents a secret. That mirrors the wider OAuth 2.1 consolidation. Per the OAuth 2.1 spec page on oauth.net: "PKCE is required for all OAuth clients using the authorization code flow." The same page notes that "the Implicit grant (response_type=token) is omitted from this specification" and "the Resource Owner Password Credentials grant is omitted from this specification" — two flows older Twitch tutorials still reference.
RFC 7636 Section 1.1 spells out the protection mechanism: "An attacker who intercepts the authorization code at (B) is unable to redeem it for an access token, as they aren't in possession of the 'code_verifier' secret." Translation from the spec language: even if your client_secret leaks through a log file or a misconfigured CDN, an attacker who steals an authorization code from a redirect URL still can't complete the token exchange without the verifier you stored server-side. Pin code_challenge_method to S256: RFC 7636 §4.2 requires it from any client capable of computing it — plain method is still allowed in spec but Kick rejects it on the wire. Belt and suspenders.
A common porting regression: an auth URL builder written for Twitch does not append code_challenge, so id.kick.com answers with invalid_request. Recognising the cause takes longer than fixing it. Update your Twitch code paths before pointing them at Kick.
# Step 1. Register your application
Open kick.com/settings/developer signed in to the account that will own the app. Kick's Help Center page "KICK Dev" describes the same path: "head to your account settings page and choose the 'developer' tab, then use the form to create an app and receive your Client ID and Client Secret." Fill in:
- Application name. Shown to users on the consent screen. Stable over time. Renaming after consent does not invalidate live tokens.
- Redirect URIs. One or more exact-match URIs your OAuth callback lives at. HTTPS required in production. Localhost with explicit port is allowed for development.
- Description. One or two sentences that surface on the consent screen.
- Category. Pick the closest fit. Today it is informational, with Kick reserving the right to rate-limit by category later.
Save and you receive a client_id (public, safe to embed in front-end bundles) and a client_secret (server-side only). Quick note — treat the secret like a database password. Public single-page apps that authenticate users through PKCE alone may operate without a secret. Server-side clients should always present both.
Tightest gotcha: redirect_uri mismatch. Kick performs exact string comparison. https://example.com/oauth/callback does not match https://example.com/oauth/callback/ — trailing slash matters. Capitalisation matters. Ports matter. Subdomains matter. Staging vs prod URL must match byte-for-byte. The OAuth 2.1 spec backs the strictness: "Redirect URIs must be compared using exact string matching." Keep one URI per environment. Avoid wildcard tricks. They will not work.
# Step 2. Generate the PKCE pair
Before redirecting the user, create a fresh code_verifier and code_challenge pair per session. RFC 7636 Section 4.1 defines the verifier verbatim: "code_verifier = high-entropy cryptographic random STRING using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" from Section 2.3 of [RFC3986], with a minimum length of 43 characters and a maximum length of 128 characters." The challenge is derived per Section 4.2: "S256: code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))". In code, each fits on one line.
Reference implementation in Node.js (works on Node 18+ where base64url is built into Buffer):
const crypto = require('crypto'); const verifier = crypto.randomBytes(64).toString('base64url'); const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
Same pair in Python:
import secrets, hashlib, base64; verifier = secrets.token_urlsafe(64); challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode('ascii')).digest()).rstrip(b'=').decode('ascii');
Code_verifier survival across redirect is the second-tightest gotcha after redirect_uri match. Store it somewhere the redirect handler can retrieve during token exchange. A server-side session keyed by the state parameter is the cleanest option for a multi-instance backend. Encrypted cookie works for stateless setups. localStorage works for SPAs. The verifier must survive the round trip to id.kick.com and back. If your storage layer drops it, the token exchange fails with invalid_grant and the user has to start over. Classic trap: in-memory store, multi-instance backend, no sticky sessions.
- Never log the verifier in plaintext. Treat it as a short-lived secret
- Generate a new verifier for every authorization attempt. Reusing verifiers defeats PKCE
- Send code_challenge_method=S256 in the authorization request. Plain method is documented in RFC 7636 but Kick rejects it
- Use 64 random bytes encoded base64url (~86 chars) as a safe default. Comfortably exceeds the 43-char minimum without hitting the 128-char ceiling
# Step 3. Build the authorization request
Redirect the user's browser to https://id.kick.com/oauth/authorize with the parameters below. Kick's developer documentation lists the same set on the "Generating Tokens (OAuth 2.1 Flow)" page on docs.kick.com, mirrored under KickEngineering/KickDevDocs on GitHub. Keep the auth URL builder in a single helper file — fewer edge cases that way.
- response_type=code. Always code for the 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 the Scopes section below)
- state. Opaque value echoed back unchanged. Use it to prevent CSRF and to key your verifier lookup
- code_challenge. The PKCE challenge from step 2
- code_challenge_method=S256
A complete URL looks like this:
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
User sees Kick's consent screen. Reviews the scopes. Approves or denies. On approval Kick redirects to your URI with ?code=AUTH_CODE&state=abc123. On denial you receive ?error=access_denied&state=abc123. Verify state before doing anything else. An attacker who steals a code from a referrer log or a leaky proxy can try to feed it into your callback. State validation is your defence. Use authorization_code grant, not implicit. Skip refresh_token rotation at your peril.
One development gotcha worth flagging. Real talk: Kick allows 127.0.0.1 redirect URIs for local testing. Quick note — if your dev server runs on Next.js, the framework rewrites 127.0.0.1 to localhost in some cases. The KickEngineering docs note this explicitly and recommend a sacrificial query parameter that sits before redirect_uri, or registering localhost directly. Registering localhost means fewer moving parts.
# Step 4. Exchange the code for tokens
Your redirect handler receives the authorization code and must POST to https://id.kick.com/oauth/token with a form-urlencoded body. Codes are single-use and short-lived. Exchange immediately. Do not stash for later.
- 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. Only for confidential clients
- code_verifier. The PKCE verifier you stored in step 2
Set Content-Type to application/x-www-form-urlencoded. A 200 response returns JSON with access_token, refresh_token, token_type (always Bearer), expires_in (seconds, typically 3600 for one hour), and scope (space-separated list of scopes actually granted — may be a subset of what you asked for if the user deselected boxes on the consent screen).
Sample successful response, redacted:
{ "access_token": "eyJhbGciOi...", "token_type": "Bearer", "refresh_token": "def456...", "expires_in": 3600, "scope": "user:read channel:read chat:write" }
Store both tokens server-side encrypted at rest. The access_token is short-lived (one hour by default). Here is the thing — the refresh_token is long-lived and rotated on use. Public SPAs should not hold either directly. Standard pattern is backend-for-frontend — browser holds a session cookie, server hop forwards API calls with the user's token attached.
Calls to api.kick.com/public/v1 use the access_token in the Authorization header: Authorization: Bearer eyJhbGciOi.... Honestly — the Endpoints catalogue in the Kick Dev docs covers Users, Channels, Livestreams, Categories, Chat and Moderation. Same surface area Twitch exposes through Helix, with subtly different request shapes.
# Step 5. Refresh token rotation
Kick issues a new refresh_token on every refresh exchange and immediately invalidates the previous one. This is refresh-token rotation — the OAuth 2.1 default for public clients per the spec note: "Refresh tokens for public clients must either be sender-constrained or one-time use." Kick applies the same rule to confidential clients. Old refresh tokens stop working the moment a new pair is issued. No exceptions.
Sharp edge here: concurrent refreshes. Two API calls fire at the same moment. Both detect an expired access_token. Both POST refresh_token=current to /oauth/token. One gets back a fresh pair. The other gets invalid_grant — because the first call already burned the token. From the user's seat the integration appears to randomly log them out. Fix is per-user serialization.
- Check whether the access_token is still valid (expires_at > now + 60s buffer)
- If yes, proceed with the API call
- If no, acquire a per-user mutex (Redis, Postgres advisory lock, or in-process if single-instance)
- Re-check after acquiring. Another concurrent request may have already refreshed
- If still stale, POST grant_type=refresh_token to /oauth/token, atomically update both tokens in storage, release the mutex
- Retry the original API call with the new access_token
The refresh body is short. grant_type=refresh_token, refresh_token=<current>, client_id, plus client_secret if you registered as confidential. No code_verifier. PKCE belongs to the authorization-code grant only. The response shape is identical to the original token exchange. Persist the new refresh_token before returning success. If you crash between receiving the new pair and storing it, the user is locked out and must re-authorize. Wrap that block in a database transaction.
# Available scopes and what they unlock
Kick's scope grammar uses colons rather than Twitch's dotted style. The list is finer-grained than Twitch's was at launch. Request only what you need. Over-requesting raises consent-decline rates measurably (industry pattern, not Kick-specific). Scope changes after initial authorization require a fresh consent. You cannot silently expand on an existing token.
- user:read. Read the authenticated user's profile (id, username, email-verification state)
- channel:read. Read the user's channel metadata (name, description, category, follower count)
- channel:write. Update channel metadata (category, title, tags)
- chat:write. Send chat messages on behalf of the authenticated user or as a bot
- chat:read. Subscribe to chat events (now folded under events:subscribe in most flows)
- streamkey:read. Read the user's stream URL and stream key for broadcasting
- events:subscribe. Create webhook subscriptions for chat, follows, subscriptions, livestream-status events
- moderation:ban. Issue or lift bans and timeouts on a channel where the user is moderator
- moderation:chat_message:manage. Delete chat messages
- channel:rewards:read. Read the channel-points rewards configuration
- channel:rewards:write. Create, update or delete channel-points rewards
If your integration evolves, prompt the user to re-authorize with the updated scope list. Consent screen will show the delta. Do not chain multiple narrow consents in one session. Conversion drops with every extra screen.
# Webhook subscriptions and event types
Webhooks are the only push transport on Kick today. WebSocket-based events sit on the public roadmap as Issue #20 in KickEngineering/KickDevDocs but are not shipping as of April 2026. Subscribe and your access token must carry events:subscribe. Account must have a public webhook URL configured in developer settings.
Subscriptions live at https://api.kick.com/public/v1/events/subscriptions. POST to create, GET to list, DELETE to remove. Documented event types include:
- chat.message.sent. A chat message is posted in a channel you subscribed to
- channel.followed. A viewer follows the channel
- channel.subscription.new. A new paid subscription starts
- channel.subscription.renewal. An existing subscription renews
- channel.subscription.gifts. One or more gift subs are sent
- livestream.status.updated. A stream goes live or offline
- livestream.metadata. Title, category or tags change mid-stream
- kicks.gifted. Kicks (the platform's tip currency) are gifted
- moderation.banned. A viewer is banned or timed out
Verify the HMAC signature on incoming webhook requests. Kick signs payloads with your app's signing secret and includes the signature in a header. The verification function is exactly what you would implement against Stripe or GitHub webhooks. Skipping the check is a security bug, not an optimization.
Delivery semantics matter. Kick docs note that "Kick will retry sending the webhook 3 times, until 200 response is made by your server. If your server is unreachable or non-OK response is returned, Kick automatically unsubscribe the event." Respond 2xx fast — within a couple of seconds — and process the payload asynchronously. Push to a queue, ACK right away, and let downstream workers do the heavy lifting. A slow handler that occasionally times out will get its subscription torn down and you will lose events you did not know you were missing.
# Rate limits and backoff strategy
Rate limit numbers are the one section of Kick's API surface that is poorly documented as of April 2026. Platform returns 429 Too Many Requests with a Retry-After header in seconds. Community libraries such as @nekiro/kick-api expose a KickRateLimitError with a retryAfter property. Hard numbers — per-client_id ceiling, per-user-token ceiling — are not published officially. Budget conservatively until Kick documents them or your integration produces empirical data.
Common failure mode: fan-out. One cron iterates 50 user channels in parallel. Each spawns a refresh and three reads. The aggregate trips the per-client limit even though no single user is hot. Mitigation is the same recipe used against Twitch Helix — token-bucket gating at both the client_id level and the per-user level.
- Always honour Retry-After. Do not retry on 429 before the header expires
- Use a token-bucket per client_id and per user_id as gating layers
- Prefer webhooks over polling for high-volume use cases. Subscribe to channel.followed and chat.message.sent instead of polling a follower count or chat history
- Cache read-only metadata (channel metadata, follower counts, category info) with short TTLs of 30 to 60 seconds
- When you hit 429, log the Retry-After value and the endpoint. The pattern reveals which surface area is undersized for your traffic profile
# Common errors and how to debug them
The errors you are most likely to hit:
- invalid_grant on token exchange. The code_verifier does not match the code_challenge, the code has been used already (single-use), or the redirect_uri does not match the one used in step 3. Inspect your PKCE storage layer first. This is where most regressions hide.
- invalid_client. client_id wrong, client_secret wrong, or client_secret sent for a public app that does not have one. Verify your Developer Portal values. Confirm whether the app was registered as confidential or public.
- invalid_request. A required parameter is missing. Usually state, code_challenge, or code_challenge_method in the authorization request.
- unauthorized_client. Your app's registered grant types do not include authorization_code. Check the Developer Portal grants tab.
- invalid_scope. You requested a scope that does not exist or that requires a higher app tier. Kick's experimental scopes occasionally rename. Verify against the current docs.
- access_denied. The user declined consent. Log it and move on. Not a bug.
- expired_token. Your access_token expired. Refresh and retry.
- invalid_grant on refresh. You sent an old, already-rotated refresh_token. Look for a concurrent refresh you forgot to serialize.
Debugging principle: Kick's error responses are OAuth-standard and helpful, but logging the request body plus response body server-side (with code, code_verifier, and tokens redacted) saves hours on intermittent issues. Always correlate logs with the state parameter so multi-user debugging stays scoped to one session at a time.
# Differences from Twitch Helix OAuth
Porting from Twitch Helix? Conceptual model is the same — authorization code flow with refresh tokens, scope-gated REST endpoints, signed webhooks. Mechanics diverge enough to require a per-provider configuration. We unpacked the broader platform comparison in our Kick vs Twitch 2026 piece. The API-layer differences land like this:
- PKCE is mandatory on Kick for every client type. Twitch keeps it optional for confidential clients
- Kick rotates refresh tokens on every exchange. Twitch refresh tokens are reusable until revoked
- Kick scope names use colons (channel:read). Twitch uses dotted paths (user:read:email)
- Kick's authorization endpoint is id.kick.com. Twitch's is id.twitch.tv
- Kick's resource API base is api.kick.com/public/v1. Twitch's is api.twitch.tv/helix
- Kick does not issue ID tokens (pure OAuth 2.0 authorization code, no OIDC). Twitch supports OIDC-enabled flows for the OpenID claims
- Kick offers webhooks only. Twitch EventSub supports webhook callbacks plus WebSocket transport
- Webhook signature verification differs. Kick uses HMAC over the raw body. Twitch EventSub bundles HMAC plus a challenge-response handshake
A shared OAuth library configured for Twitch is a reasonable starting point. Needs per-provider adjustments for the PKCE mandate and the refresh-rotation semantics. On Node.js, openid-client works with a Kick-specific client configuration and explicit refresh-serialization middleware. The same library, with different config, can talk to Twitch in the same codebase. Run Python? kickcom.py and authlib both handle the PKCE shape correctly out of the box.
Worth noting: Kick is younger than Twitch as a programmable surface. Platform launched its public API in 2024 with a $100K developer bounty program. Docs evolve in real time on github.com/KickEngineering/KickDevDocs. Pin your dependencies. Watch the changelog. Expect occasional schema tweaks on experimental endpoints. The OAuth core has been stable through 2025 and into 2026. Moderation and channel-points endpoints have moved more. If your integration touches creator monetization metadata, our breakdown of the Kick monetization path (Creator, Verification, Partner) covers what each tier exposes through the API.
# Частые вопросы
Can I use the implicit grant or password grant on Kick?
No. OAuth 2.1 deprecates both. Kick supports authorization_code with PKCE, refresh_token, and client_credentials for app-only endpoints. The OAuth 2.1 spec page on oauth.net puts it plainly: "the Implicit grant is omitted from this specification" and "the Resource Owner Password Credentials grant is omitted from this specification."
Do I need PKCE if I have a client_secret?
Yes. Kick's authorization server rejects authorization-code requests without code_challenge and code_challenge_method even when a client_secret is provided. PKCE became universal in OAuth 2.1, regardless of client confidentiality.
How long are access tokens and refresh tokens valid?
Access tokens carry an expires_in value in the token response, typically 3600 seconds (one hour). Refresh tokens last longer and rotate on every exchange. The spec deliberately does not communicate refresh-token expiry to the client. Treat them as long-lived but always handle a re-authorize fallback when they fail.
What is the API base URL for resource endpoints?
https://api.kick.com/public/v1. Calls use the access_token in an Authorization: Bearer header. Users, Channels, Livestreams, Categories, Chat and Moderation endpoints all live under that prefix. The OAuth host (id.kick.com) is intentionally separated from the resource host.
How do I subscribe to chat or follow events?
Request the events:subscribe scope, configure a public webhook URL in your Developer Portal settings, and POST to https://api.kick.com/public/v1/events/subscriptions with the event name (chat.message.sent, channel.followed, etc.) and the broadcaster_user_id you want to listen on. Verify the HMAC signature on incoming payloads. Always.
Why does my refresh occasionally return invalid_grant?
Concurrent refreshes. Kick rotates refresh_token on use, so two parallel refresh attempts race and one of them sends a freshly-invalidated token. Serialize refresh exchanges per user with a mutex (Redis, Postgres advisory lock, or in-process for single-instance backends). Have concurrent API calls wait for the in-flight refresh to finish before retrying.
What is the best way to test the flow in development?
Register a development-only app with localhost or 127.0.0.1 redirect URIs and a dev-only client_id. Postman's OAuth 2.1 helper supports PKCE correctly. Never reuse a production client_id for dev testing. Kick's rate limits and scope approvals are scoped per client. A noisy dev session can degrade your production limits.
Does Kick support WebSocket events like Twitch EventSub?
Not yet. As of April 2026 webhooks are the only push transport. WebSocket transport is tracked publicly as Issue #20 in the KickEngineering/KickDevDocs repository. Until it ships, desktop apps that cannot host a public webhook URL will need a server-side relay.