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
| Property | Value |
|---|---|
| Identifier | kick |
| Display name | Kick |
| Connection mode | oauth (authorization code grant with PKCE S256) |
| Editor | normal |
| Channel kind | social |
| Comments / threads | Advertised 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 publishing | No. supports_pending = false; post() sends and returns Posted/Success |
| Analytics | No |
| Post analytics | No |
| Mentions | No |
is_between_steps | false |
convert_to_jpeg | false |
max_concurrent_job | 3 |
Character limit (max_length) | 500 characters, hard-coded |
| OAuth scopes | chat: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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
KICK_CLIENT_ID | Yes | — | Client id from the Kick developer settings |
KICK_SECRET | Yes | — | Client secret. The name is KICK_SECRET, not KICK_CLIENT_SECRET. |
KICK_ID_BASE_URL | No | https://id.kick.com | OAuth origin (/oauth/authorize, /oauth/token). Read through Config::base_url("KICK_ID", …). |
KICK_BASE_URL | No | https://api.kick.com | Public API origin (/public/v1/...). Read through Config::base_url("KICK", …). |
FRONTEND_URL | Yes in practice | http://localhost:5000 | Base of the redirect URI |
JWT_SECRET | No direct use | empty | Not 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
- Sign in at https://kick.com.
- Enable two-factor authentication. Kick requires 2FA to access the developer tab.
- Review Kick’s Developer Terms and access policy at https://dev.kick.com (or the links in the developer settings).
2. Create the app
- Open Account Settings → Developer: https://kick.com/settings/developer.
- 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.
- 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_URLis trimmed of trailing slashes; register the URI without a trailing slash.- There is no
redirectmeto.comwrapper 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.1in a URL). Rustiz is not Next.js, so the workaround is not needed;127.0.0.1works 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"];
}
| Scope | Used by | Why |
|---|---|---|
chat:write | POST /public/v1/chat | Send chat messages |
user:read | GET /public/v1/users | Read the channel’s user id, name and avatar during connect |
channel:read | Nothing in this provider | Read 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 ascode_verifierduring 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:
- Set the app’s Redirect URL to
https://docs.kick.com. - Enter the client id and secret.
- Choose PKCE
SHA-256. - Select the scope the endpoint needs.
- Authorize and send the request. This is a docs playground, not a separate environment: requests hit production.
- Set the app’s Redirect URL to
- 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_inandscope. Rustiz stores the access token, refresh token andexpires_in. refresh_token()performs the refresh grant and calls/public/v1/usersagain 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;
--openopens 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. Pollrustiz channels connect-status <id>. - No
--external-urland no--fields: Kick is a normal OAuth provider.customFieldstakes precedence inconnect.rsand 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 toid→ channelid(used asbroadcaster_user_id)name→nameandusernameprofile_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>
}
typeis always"user". Kick also supports"bot", but Rustiz does not use it.broadcaster_user_idisid.trim().parse::<i64>(); a non-numeric channel id becomes JSONnull, which Kick rejects with400.contentis truncated to 500 Rustchars (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 }. Missingmessage_id→ random 10-char id. is_sent == false→PostStatus::Success(the Rust enum has noerrorstate), so a message dropped by Kick looks successful. The API’smessageerror 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_idwhen non-empty, otherwise the rootpost_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, notKICK_CLIENT_SECRET. This is the most common setup failure. Checkrequired_env()in the Rust source; the variable isKICK_SECRET.channel:readis unused. The provider only calls/public/v1/usersand/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-exchangeredirect_uriwith 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/usersresponse.- No PKCE on refresh. The refresh grant sends only
grant_type,client_id,client_secretandrefresh_token; Kick’s docs do not require a verifier or redirect URI for refresh. - No
scopecheck. Granted scopes are never compared with the requested list. A deniedchat:writeis discovered only when posting fails. - No error classification.
handle_errors()returnsNone; 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: falseis hidden. A message rejected by Kick (slow mode, banned words, channel restrictions, rate limit) returns 200 withis_sent: falseand is reported asSuccess. 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 topost_idfor 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 useschars().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_idmust be numeric. The internal id comes fromget_user_info()’suser_id/id. If Kick returns a string UUID-like id,parse::<i64>()fails and the API receivesnull. 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}(orhttps://kick.com/channelifprofileis empty). Kick chat messages have no stable public permalink. - No
channel:readcalls. 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
429with a message; the docs do not publish a single numeric chat limit.max_concurrent_job = 3is a conservative cap. The generic HTTP layer retries429/5xxup 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
- Kick developer landing: https://dev.kick.com
- Kick app setup: https://docs.kick.com/getting-started/kick-apps-setup
- OAuth 2.1 flow: https://docs.kick.com/getting-started/generating-tokens-oauth2-flow
- Scopes: https://docs.kick.com/getting-started/scopes
- Chat API (500 graphemes / 2,048 bytes): https://docs.kick.com/apis/chat
- Users API: https://docs.kick.com/apis/users
- FAQs (app verification): https://docs.kick.com/apis/faqs
- Postiz Kick settings: https://docs.postiz.com/public-api/providers/kick