Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kick (kick)

Kick is a live-streaming platform. Rustiz posts a chat message to the broadcaster’s channel through the Kick public API, using OAuth 2.1 with PKCE. Threads become chat replies (reply_to_message_id). There is no stream management, clip creation or media upload.

  • Rust source: crates/rustiz-providers/src/kick.rs
  • Upstream port: kick.provider.ts
  • Editor: Normal
  • Connection mode: oauth (authorization code + PKCE)
  • No sandbox and no app review for ordinary chat apps.

Overview

PropertyValue
Identifierkick
Display nameKick
Connection modeoauth (authorization code grant with PKCE S256)
Editornormal
Channel kindsocial
Comments / threadsAdvertised as no: supports_comments is left false, even though comment() is implemented. The engine truncates extra thread entries and MCP validation reports “does not support thread replies”.
Pending / async publishingNo. supports_pending = false; post() sends and returns Posted/Success
AnalyticsNo
Post analyticsNo
MentionsNo
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job3
Character limit (max_length)500 characters, hard-coded
OAuth scopeschat:write, user:read, channel:read
Redirect URI{FRONTEND_URL}/integrations/social/kick

Server Environment Variables

required_env() returns exactly:

#![allow(unused)]
fn main() {
fn required_env(&self) -> &'static [&'static str] {
    &["KICK_CLIENT_ID", "KICK_SECRET"]
}
}

required_env_alternatives() is not overridden.

VariableRequiredDefaultPurpose
KICK_CLIENT_IDYes—Client id from the Kick developer settings
KICK_SECRETYes—Client secret. The name is KICK_SECRET, not KICK_CLIENT_SECRET.
KICK_ID_BASE_URLNohttps://id.kick.comOAuth origin (/oauth/authorize, /oauth/token). Read through Config::base_url("KICK_ID", …).
KICK_BASE_URLNohttps://api.kick.comPublic API origin (/public/v1/...). Read through Config::base_url("KICK", …).
FRONTEND_URLYes in practicehttp://localhost:5000Base of the redirect URI
JWT_SECRETNo direct useemptyNot read by the Kick flow. The OAuth state row stores the PKCE verifier and state without encryption for this provider. The rest of the server uses it for other secrets.

A very common configuration error is setting KICK_CLIENT_SECRET. The code only reads KICK_SECRET; rustiz providers --status will keep reporting kick as missing KICK_SECRET until the variable is renamed.

Step-by-Step Developer App Setup

1. Prepare the Kick account

  1. Sign in at https://kick.com.
  2. Enable two-factor authentication. Kick requires 2FA to access the developer tab.
  3. Review Kick’s Developer Terms and access policy at https://dev.kick.com (or the links in the developer settings).

2. Create the app

  1. Open Account Settings → Developer: https://kick.com/settings/developer.
  2. Create an app. Kick asks for:
    • an app name,
    • a Redirect URL (one URL per app in the current UI),
    • acceptance of the developer terms.
  3. Kick returns a Client ID and Client Secret. Copy both immediately.

The app is usable as soon as it is created. There is no separate “publish” or “submit for review” step for the chat API, and no product/API subscription to enable; the granted scopes are the only permission gate.

3. Set the environment and restart

KICK_CLIENT_ID="…"
KICK_SECRET="…"

Restart every Rustiz process. Check:

rustiz providers --status

4. Redirect URI (exact)

Kick::redirect_uri() is:

{FRONTEND_URL}/integrations/social/kick
  • FRONTEND_URL is trimmed of trailing slashes; register the URI without a trailing slash.
  • There is no redirectmeto.com wrapper for Kick.
  • Local example: http://localhost:5000/integrations/social/kick.
  • Kick’s docs note a 127.0.0.1 workaround for Next.js apps (where the framework rewrites the first 127.0.0.1 in a URL). Rustiz is not Next.js, so the workaround is not needed; 127.0.0.1 works if that is what you registered.
  • The same URI must be used in the authorize request and the token exchange.

There is a refresh quirk: authenticate() appends ?refresh=<internal-id> to the redirect URI when the connection was started with a refresh query parameter, while generate_auth_url() always sends the URI without it. Kick matches redirect URLs exactly, so a reconnect/refresh flow can fail with an invalid redirect_uri. Normal CLI/MCP connections do not set refresh.

5. Scopes requested and checked

Kick::new() sets:

#![allow(unused)]
fn main() {
info.scopes = vec!["chat:write", "user:read", "channel:read"];
}
ScopeUsed byWhy
chat:writePOST /public/v1/chatSend chat messages
user:readGET /public/v1/usersRead the channel’s user id, name and avatar during connect
channel:readNothing in this providerRead channel metadata; Rustiz never calls the channels API. It is an extra scope.

The authorize URL URL-encodes the space-joined list:

https://id.kick.com/oauth/authorize?response_type=code
  &client_id=…
  &redirect_uri=…
  &scope=chat%3Awrite%20user%3Aread%20channel%3Aread
  &state=…
  &code_challenge=…
  &code_challenge_method=S256

check_scopes() is never called. The token response’s scope is discarded; Rustiz never compares granted scopes with info.scopes. A user who denies chat:write still gets a “connected” channel; the first post fails.

channel:read is not used by the provider. Remove it from info.scopes if you want the minimal consent screen.

6. PKCE

Kick requires PKCE on every authorization-code exchange, even for confidential server clients.

  • Verifier: 64 random bytes from OsRng, base64url without padding (86 characters).
  • Challenge: SHA-256 of the verifier’s ASCII bytes, base64url without padding.
  • Method: S256.
  • The verifier is stored in the OAuth state row (codeVerifier, plaintext) and sent as code_verifier during the token exchange.
  • A new verifier/challenge is generated for every connect attempt. There is no reuse.

7. Sandbox / testing vs production

  • No sandbox. Kick has no test-mode app or separate test API. The app works against the production API immediately.
  • No app review for ordinary chat apps. App verification is optional: it adds a verified badge to the bot account and raises chat subscription limits from 1,000 to 10,000. To request it, email developers@kick.com with the client id, app name, whether the bot also needs verification, the reason and supporting evidence. It is not required to post chat messages.
  • Kick’s API docs have a “Test it” console on each endpoint. To use it with a user token:
    1. Set the app’s Redirect URL to https://docs.kick.com.
    2. Enter the client id and secret.
    3. Choose PKCE SHA-256.
    4. Select the scope the endpoint needs.
    5. Authorize and send the request. This is a docs playground, not a separate environment: requests hit production.
  • For tests, use a test Kick account/channel. Chat messages are visible to real viewers if the channel is live.
  • Production uses the same app and credentials. There is no review queue.

8. Token lifetime and refresh

  • The token response includes access_token, refresh_token, expires_in and scope. Rustiz stores the access token, refresh token and expires_in.
  • refresh_token() performs the refresh grant and calls /public/v1/users again to refresh the id/name/avatar.
  • Kick access tokens are short-lived; the exact lifetime is in expires_in. The engine refreshes when the stored expiry is near.
  • A 401 that is not handled by the provider is classified as a refresh error by the generic HTTP layer.

Connecting a Channel (CLI & MCP)

CLI

rustiz channels connect kick --open
  • Prints the Kick consent URL; --open opens it.
  • With a local FRONTEND_URL, the CLI serves the callback and waits up to --timeout (default 300 s).
  • With a public HTTPS FRONTEND_URL, the CLI prints the URL and exits; the server handles the callback. Poll rustiz channels connect-status <id>.
  • No --external-url and no --fields: Kick is a normal OAuth provider. customFields takes precedence in connect.rs and would be treated as an authorization code, which fails.

MCP connectChannelTool

{ "provider": "kick" }

Returns:

{
  "connected": false,
  "pending": true,
  "requiresHuman": true,
  "connectionId": "…",
  "url": "https://id.kick.com/oauth/authorize?...",
  "message": "A human must open this URL and log in. Then poll with connectionId or check integrationList."
}

Poll with:

{ "connectionId": "…" }

integrationSchema reports:

{ "type": "oauth", "headless": false, "requiresHuman": true, "requiresExternalUrl": false }

There is no triggerTool / channelSetupTool follow-up: the broadcaster is the authenticated user, so is_between_steps is false.

What gets stored

GET /public/v1/users returns a data value that may be an array or an object. user_fields() takes the first array element (or the object) and reads:

  • user_id, falling back to id → channel id (used as broadcaster_user_id)
  • name → name and username
  • profile_picture → picture

If the response is {} because the status was an error, all fields are empty but the access token is still saved. That channel fails at publish time with a null broadcaster id.

Post Settings & Media Validation

settings_schema()

Kick has no provider-specific settings:

{ "type": "object", "properties": {}, "additionalProperties": true }

__type is added by the engine. There is no message type, color, reply mode or channel selection.

validate_settings()

Not overridden; every settings object is accepted.

check_validity()

Not overridden. There are no pre-flight media or length checks beyond max_length.

Media requirements

Media is ignored. Kick chat cannot attach images or video. post() and comment() never read post.media; a post with images publishes as text only, with no warning. This is silent data loss.

How a post is built

POST {KICK_BASE_URL}/public/v1/chat
Authorization: Bearer <access token>
Content-Type: application/json

{
  "type": "user",
  "content": "<truncated text>",
  "broadcaster_user_id": <numeric user id>
}
  • type is always "user". Kick also supports "bot", but Rustiz does not use it.
  • broadcaster_user_id is id.trim().parse::<i64>(); a non-numeric channel id becomes JSON null, which Kick rejects with 400.
  • content is truncated to 500 Rust chars (Unicode scalar values) before sending.
  • Kick documents the limit as 500 user-perceived characters (grapheme clusters) and no more than 2,048 UTF-8 bytes. Rust’s chars().take(500) is neither: it can split a combining sequence, counts a family emoji as several characters, and can still emit more than 2,048 bytes for 500 four-byte characters.
  • The response is read as { data: { message_id, is_sent } } or a top-level { message_id, is_sent }. Missing message_id → random 10-char id.
  • is_sent == false → PostStatus::Success (the Rust enum has no error state), so a message dropped by Kick looks successful. The API’s message error text is discarded.

How a comment is built

Same endpoint, plus:

{ "reply_to_message_id": "<last_comment_id or post_id>" }
  • The reply target is last_comment_id when non-empty, otherwise the root post_id.
  • Kick replies are flat (reply_to_message_id), not a nested thread.
  • supports_comments = false, so the engine truncates threads before this code runs. Manual provider calls would work; MCP validation rejects thread replies.

Plugs

No @Plug / @PostPlug methods. run_plug() and run_post_plug() return Unsupported.

Implementation Notes & Gotchas

  • KICK_SECRET, not KICK_CLIENT_SECRET. This is the most common setup failure. Check required_env() in the Rust source; the variable is KICK_SECRET.
  • channel:read is unused. The provider only calls /public/v1/users and /public/v1/chat. Requesting channel metadata is unnecessary and expands the consent screen.
  • PKCE verifier length. 64 random bytes → 86 base64url characters, within RFC 7636’s 43–128 range. The challenge is S256, not plain.
  • Refresh redirect mismatch. A connection started with ?refresh= sends a token-exchange redirect_uri with the query string, while the authorize request used the bare URI. Kick’s exact matching can reject it. Normal connections are unaffected.
  • get_user_info() ignores the HTTP status. It reads the response body and parses it even on 401/403. A JSON error body becomes {}, so the channel can be saved with empty id/name and only fail at publish time. If a Kick channel has a blank name, check the token and the /public/v1/users response.
  • No PKCE on refresh. The refresh grant sends only grant_type, client_id, client_secret and refresh_token; Kick’s docs do not require a verifier or redirect URI for refresh.
  • No scope check. Granted scopes are never compared with the requested list. A denied chat:write is discovered only when posting fails.
  • No error classification. handle_errors() returns None; Kick’s { message } error bodies pass through the generic HTTP layer. A 401 becomes a refresh error, a 429 is retried up to three times.
  • is_sent: false is hidden. A message rejected by Kick (slow mode, banned words, channel restrictions, rate limit) returns 200 with is_sent: false and is reported as Success. Check the channel if a post claims success but does not appear.
  • The message id fallback is random. If Kick omits message_id, Rustiz generates a random id. That id cannot be used for replies or deletion. The same applies to post_id for a dropped message.
  • Media is silently dropped. No pre-flight warning. A post with images publishes as text.
  • 500-character limit mismatch. Kick’s current docs specify grapheme clusters plus a 2,048-byte ceiling. The Rust implementation uses chars().take(500) and the engine uses chars().count(). Expect edge cases with emoji, flags, skin tones and combining marks. For reliable results, keep posts well under 500 ASCII characters and avoid dense emoji.
  • broadcaster_user_id must be numeric. The internal id comes from get_user_info()’s user_id/id. If Kick returns a string UUID-like id, parse::<i64>() fails and the API receives null. Kick currently uses numeric user ids; this is a compatibility risk if that changes.
  • Release URL is the channel, not the message. https://kick.com/{profile} (or https://kick.com/channel if profile is empty). Kick chat messages have no stable public permalink.
  • No channel:read calls. There is no channel lookup, no “go live” and no stream metadata. The provider cannot verify that the channel exists or is live before posting.
  • Rate limits. Kick returns 429 with a message; the docs do not publish a single numeric chat limit. max_concurrent_job = 3 is a conservative cap. The generic HTTP layer retries 429/5xx up to three times.
  • No analytics or mentions. Both flags are false; there are no overrides.
  • No app verification needed. Verification only adds a badge and raises chat subscription limits. It is not an app review and does not gate posting.

References