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

VK (vk)

VK (VKontakte) is a social network. Rustiz connects a VK user through VK ID OAuth 2.1 + PKCE and publishes to that user’s own wall, with optional photos/videos and comments. It does not publish to communities.

Important — reported VK ID/API limitation. The OAuth flow in this provider obtains a VK ID token (the vk2.a.* format). There are reports (Postiz issue #1408, April 2026, tested against the live API) that such tokens are intended for authentication and do not authorize VK API methods such as wall.post: the call fails with error_code: 1051 — "Method is not available for this profile type". The Rust port does check the VK error object and will mark the post as an error instead of silently succeeding, but it cannot change the token type. If VK still enforces this, VK channels can connect but cannot publish with this flow. See Implementation Notes.

  • Rust source: crates/rustiz-providers/src/vk.rs
  • Upstream port: vk.provider.ts
  • Editor: Normal (plain text)
  • Connection mode: oauth

Overview

PropertyValue
Identifiervk
Display nameVK
Connection modeoauth (VK ID authorization code + PKCE S256)
Editornormal
Channel kindsocial
Comments / threadsYes. supports_comments = true; comments are created with wall.createComment
Pending / async publishingNo. supports_pending = false; post() returns PostStatus::Completed
AnalyticsNo
Post analyticsNo
MentionsNo
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job5 (the code comments that VK limits are per user token)
Character limit (max_length)2048
VK ID hosthttps://id.vk.ru (VK_ID_BASE_URL)
VK API hosthttps://api.vk.ru (VK_BASE_URL)
API version5.251 (hard-coded)

Server Environment Variables

required_env() returns exactly one variable:

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

required_env_alternatives() is not overridden. VK_ID is the VK ID app client id, not a URL or a base URL.

VariableRequiredDefaultPurpose
VK_IDYes""VK ID application id (client_id). An empty value is rejected before the login URL is shown.
VK_ID_BASE_URLNohttps://id.vk.ruOverrides the VK ID authorization/token/userinfo host. https://id.vk.com currently redirects to id.vk.ru; set this only for tests or a proxy.
VK_BASE_URLNohttps://api.vk.ruOverrides the VK API host used for photos.*, video.save, wall.post, wall.createComment. Both api.vk.ru and api.vk.com respond; the default is the .ru host.
FRONTEND_URLNohttp://localhost:5000Public origin used to build the redirect URI. Trailing slashes are stripped.
JWT_SECRETNo""Not used by this OAuth provider (no encrypted client info/custom fields). Set it for the rest of the deployment.

There is no VK_CLIENT_SECRET, VK_SERVICE_TOKEN, or VK_SECRET variable. The VK ID app’s “protected key” and “service key” are not read by this provider.

rustiz providers --status reports VK_ID (never its value) and marks VK configured when it is non-empty.

Step-by-Step Developer App Setup

1. Create a VK ID app

Portal: https://id.vk.ru/about/business/go/. Since December 2025 the VK ID authorization service is entered through VK Business ID (https://bid.vk.com/). You need a business profile; verify it to unlock settings, keys and extended access. An unverified profile can have its apps blocked after 60 days.

  1. Sign in to the VK ID service with VK Business ID.
  2. Create/verify the business profile.
  3. Open Мои приложения → Добавить приложение.
  4. Choose the Web platform.
  5. Set the base domain (for example social.example.com).
  6. Set the trusted redirect URL (see below).
  7. Upload an icon and create the app.
  8. Copy the application ID into VK_ID.

The app page also shows a protected key (client secret) and a service key. Rustiz does not use either.

2. Add the exact redirect URI

redirect_uri() is deliberately different from most providers:

#![allow(unused)]
fn main() {
let frontend = self.ctx.config.frontend_url();
if frontend.contains("https") {
    format!("{frontend}/integrations/social/vk")
} else {
    format!("https://redirectmeto.com/{frontend}/integrations/social/vk")
}
}

So:

FRONTEND_URLRedirect URI sent to VK
https://social.example.comhttps://social.example.com/integrations/social/vk
http://localhost:5000https://redirectmeto.com/http://localhost:5000/integrations/social/vk
http://192.168.1.10:5000https://redirectmeto.com/http://192.168.1.10:5000/integrations/social/vk

There is no trailing slash. The check is a substring check (frontend.contains("https")), not a URL-scheme check; an http:// URL whose host or path contains the letters https is treated as HTTPS and is not wrapped.

Rustiz sends this authorize URL (redirect and scope are URL-encoded):

{VK_ID_BASE_URL}/authorize
  ?response_type=code
  &client_id={VK_ID}
  &code_challenge_method=S256
  &code_challenge={BASE64URL(SHA256(code_verifier))}
  &redirect_uri={urlencode(redirect URI above)}
  &state={32-char state}
  &scope={urlencode(vkid.personal_info email wall status docs photos video)}

VK’s redirect rules make the redirectmeto.com fallback impractical. VK requires the trusted redirect URL’s domain to exactly match the app’s base domain (or a .domain.com wildcard base domain), and the redirect URL is case-sensitive. You cannot own redirectmeto.com, so a local http:// FRONTEND_URL cannot be registered as-is. VK’s localhost exception is also narrow:

  • base domain localhost;
  • trusted redirect http://localhost (port 80) or https://localhost (port 443);
  • other ports are not supported.

Use a real HTTPS deployment or an HTTPS tunnel hostname as FRONTEND_URL, and register https://<host>/integrations/social/vk as both the base domain and the trusted redirect URL. Avoid the redirectmeto.com path for VK.

3. Choose the app privacy type: Public

VK ID apps are public or confidential:

TypeToken exchangeRequirement
PublicFrom the client/backend without a service key; 15,000 token requests/day per IPRustiz’s authenticate() does not send service_token
ConfidentialBackend only, IP allowlist, service_token mandatory in every token exchange/refresh; 120,000 requests/day per appNot supported by this provider

Rustiz must use a public app. A confidential app will reject the token exchange because auth_form() never includes service_token, and the backend IP would also have to be allowlisted.

4. Enable access scopes

Rustiz requests exactly this space-separated scope string:

vkid.personal_info email wall status docs photos video
ScopeUsed for
vkid.personal_infoBasic profile: first/last name, avatar, user id
emailEmail on oauth2/user_info (not read by Rustiz, but requested)
wallwall.post and wall.createComment
photosphotos.getWallUploadServer / photos.saveWallPhoto
videovideo.save
statusRequested but not used by this provider
docsRequested but not used by this provider

In the app’s Доступы section, enable these. VK distinguishes:

  • Basic access — vkid.personal_info (always), and email (optional).
  • Extended access — wall, photos, video, status, docs, and others. Extended access requires a verified business profile. Some permissions (notably wall) are described by VK as granted “in exceptional cases” through a support request to devsupport@corp.vk.com.

This is the real production gate. Without the wall permission, the authorize flow may still complete (or grant a subset), but wall.post fails. Requesting status and docs when they are not used only adds review surface; consider whether your app needs them before submitting.

check_scopes() is not called in vk.rs. Rustiz never verifies the scope set that VK actually granted; the first API method call reveals a missing permission.

5. Sandbox / testing mode

VK has no separate sandbox environment for VK ID. There is a “mark as test app” flag in the app settings, but VK documents it as a visual marker that does not change behavior.

Practical testing:

  • Use your own VK account and a test app.
  • Use a real HTTPS FRONTEND_URL; the localhost/redirectmeto.com path is not registrable under VK’s domain rules.
  • Expect the first publish to be the real test: VK ID login can succeed while wall access is still missing.
  • VK API quotas apply: 10,000 calls/month for new/unverified users and 100,000,000 calls/month after business verification (effective 7 September 2026). VK ID authorization calls are not counted against the VK API quota.
  • The VK ID public-app token endpoint allows 15,000 requests/day per IP; a confidential app allows 120,000/day per app.

6. Production app review

There is no VK App Store-style review for a VK ID app. Once enabled, any VK user can authorize it. The gates are:

  1. verified VK Business ID profile;
  2. extended access approval for wall/photos/video (and support approval for wall if VK requires it);
  3. VK API quota.

An app can be blocked if the business profile is not verified, or for policy violations. Check the app’s status in Мои приложения; a blocked app shows the reason.

Connecting a Channel (CLI & MCP)

CLI

rustiz channels connect vk

Optional:

rustiz channels connect vk --open
rustiz channels connect vk --timeout 600

Do not pass --fields or --external-url:

  • --fields uses the customFields path and base64-encodes JSON as the OAuth code; VK’s authenticate() expects a real authorization code plus device id.
  • --external-url calls external_url(), which VK does not implement.

The device_id callback problem

VK’s redirect returns three query parameters:

https://social.example.com/integrations/social/vk?code=<code>&device_id=<device_id>&state=<state>

The generic Rustiz callback (crates/rustiz-server/src/oauth.rs) reads code, state and error from the query:

#![allow(unused)]
fn main() {
struct CallbackQuery {
    code: Option<String>,
    state: Option<String>,
    error: Option<String>,
}
}

vk.rs expects the device id to be packed into the code as <code>&&&&<device_id>:

#![allow(unused)]
fn main() {
fn split_code(value: &str) -> (String, String) {
    match value.split_once("&&&&") {
        Some((code, device)) => (code.to_string(), device.to_string()),
        None => (value.to_string(), String::new()),
    }
}
}

On a real VK redirect the device id is a separate query parameter, so authenticate() sends an empty device_id to POST /oauth2/auth. VK’s token exchange requires device_id, so the connection fails even though the user approved the app.

Workaround: rewrite the callback URL before reloading, packing the device id into the code value with percent-encoded ampersands:

https://social.example.com/integrations/social/vk?code=<code>%26%26%26%26<device_id>&state=<state>

The callback decodes this to code&&&&device_id, split_code() recovers both values, and the token exchange can proceed. The code is valid for 10 minutes; do this immediately.

A durable fix is to teach the server callback to accept device_id and pass it to the provider (for example as part of AuthParams.code).

MCP

Start:

{ "provider": "vk" }

Poll:

{ "connectionId": "Ab12Cd" }

connectChannelTool returns the authorize URL and connectionId. A human must open the URL, approve the app, and — with the current callback — you must apply the device_id rewrite before polling. Then poll the same connectionId and call integrationList.

No customFields, externalUrl, name or description for VK. custom_fields() is not overridden, so integrationSchema.customFields is an empty array: there are no keys or validation regexes.

No setup step

is_between_steps = false; there is no channelSetupTool step and no triggerTool function. There is no page/community picker: the channel is the authorized user and publishes to that user’s wall.

Post Settings & Media Validation

settings_schema()

The schema is empty:

#![allow(unused)]
fn main() {
json!({ "type": "object", "properties": {}, "additionalProperties": true })
}

validate_settings() is the trait default (accepts anything). VK has no provider-specific settings; the target is implied by the token (wall.post without owner_id posts on the authorized user’s own wall).

Example:

{ "__type": "vk" }

The engine adds __type automatically when missing. rustiz posts create --json post.json works; provider settings are optional.

Publish behavior

post() uses only the first PostDetails:

  1. Uploads every media item (see below), concurrently.

  2. Builds attachments as a comma-separated list of {type}{user_id}_{media_id} values, e.g. photo123_456,video123_789.

  3. Calls:

    POST {VK_BASE_URL}/method/wall.post?v=5.251&access_token=...&client_id=...
    

    with form fields:

    { "message": "<post message>", "attachments": "photo..." }
    

    The code always sends message, even when it is empty. attachments is omitted when there is no media.

  4. Reads response.post_id, and returns PostResponse::done(...): status = PostStatus::Completed, release_url = https://vk.com/feed?w=wall{user_id}_{post_id}.

No owner_id and no from_group are sent, so this is a personal wall post, not a community post. The client_id query parameter is sent although wall.post does not require it.

Comments / threads

comment():

  • uploads the comment’s media with the same upload_media() helper;
  • calls wall.createComment with message, post_id, and optional attachments;
  • reads response.comment_id;
  • returns release_url for the original post;
  • ignores last_comment_id completely, so every comment is a top-level comment, not a reply to the previous one.

post_id is the numeric wall post id (the provider’s post_id is a single id, not a comma-separated list). Because posts go to the user’s own wall, wall.createComment can resolve the post without owner_id.

Media requirements

  • No check_validity() override: any type/count is accepted by Rustiz.

  • Media is classified by filename only:

    #![allow(unused)]
    fn main() {
    let video = has_extension(&media.path, "mp4");
    }

    has_extension lowercases the path and checks for the substring .mp4. Therefore:

    • .mp4, .MP4, or .mp4?x=1 → video path;
    • .mov, .webm, .mkv, .avi, .m4v, .gif, .pdf, etc. → photo path, even though mime_for() knows some video MIME types.
  • Photos:

    1. GET /method/photos.getWallUploadServer?owner_id={user_id}&access_token=...&v=5.251
    2. POST the bytes to the returned upload_url as multipart field photo;
    3. POST /method/photos.saveWallPhoto?access_token=...&v=5.251 with photo, server, hash;
    4. collect response[0].id.
  • Videos:

    1. GET /method/video.save?access_token=...&v=5.251;
    2. POST the bytes to the returned upload_url as multipart field photo (the code uses the same field name for both types);
    3. use video.save’s response.video_id as the attachment id.
  • Likely video bug: VK’s video.save upload server normally expects the file under the multipart field video_file (the upstream issue reports video_file). The Rust port sends photo, so video uploads may not attach.

  • Uploads run concurrently via futures::future::try_join_all; one failure aborts the whole post. max_concurrent_job = 5 limits post jobs, not this per-post fan-out.

  • VK’s platform limit is 10 media per wall post. Rustiz does not enforce it; VK rejects the 11th.

  • mime_for() maps jpg/jpeg, png, gif, webp, mp4, mov, avi, mkv, webm; anything else becomes application/octet-stream.

  • No alt text, thumbnails or convert_to_jpeg support.

Plugs

No @Plug or @PostPlug methods. run_plug() and run_post_plug() use the trait defaults and return ProviderError::Unsupported. No analytics, mentions, pending posts, resolve_release_id or missing() implementation.

Implementation Notes & Gotchas

  • VK ID tokens may not be accepted by VK API methods. The flow obtains a vk2.a.* VK ID token. Postiz issue #1408 (April 2026, live API, v=5.199) reports wall.post → error_code: 1051 — "Method is not available for this profile type", while read-only methods such as users.get work. Apps created on id.vk.com/id.vk.ru cannot fall back to the legacy oauth.vk.com token endpoint (it returns 401). The Rust port correctly checks the error object (check_api_error) and will fail the post with a readable message, but it cannot make a VK ID token API-capable. If this is still enforced, VK is connectable but not publishable with this provider. Workarounds reported upstream require a legacy vk1.a.* user token (oauth.vk.com implicit flow) or a community token, neither of which this provider implements.
  • wall is an exceptional permission. VK’s wall.post documentation says the wall access right is granted in exceptional cases through devsupport@corp.vk.com. A working login is not proof that publishing is allowed.
  • Token exchange is multipart, not form-urlencoded. VK’s reference says POST /oauth2/auth must use Content-Type: application/x-www-form-urlencoded. auth_form() uses reqwest::multipart::Form, which sends multipart/form-data; the oauth2/user_info call uses the same multipart helper. If VK strictly enforces the content type, both code exchange and refresh (and userinfo) fail.
  • Token errors are not inspected. authenticate() reads access_token directly. If VK returns an {"error": ...} body with HTTP 200, the empty access token is passed to userinfo and the connection fails with the generic “The platform did not return an access token” message instead of VK’s error_description.
  • Token exchange omits state. VK lists state as a required parameter for grant_type=authorization_code; authenticate() does not send it. The authorize request does send a 32-character state, and Rustiz verifies it before calling the provider, but VK may still reject the exchange.
  • Confidential apps fail. No service_token is sent on exchange or refresh, and no backend IP allowlist is configured. Use a public app.
  • device_id is not captured by the generic callback. See the rewrite workaround above. This is the most likely cause of a “login succeeded but token exchange failed” report.
  • redirectmeto.com conflicts with VK’s base-domain rule. VK requires the redirect domain to exactly match the app’s base domain. A local http://localhost:5000 becomes a redirectmeto.com URL, which cannot be your base domain. Use HTTPS and a real host.
  • Refresh tokens rotate and invalidate the old pair. VK returns a new access + refresh token; the old pair is invalid immediately. Rustiz stores refresh_token as {refresh}&&&&{device_id} and re-splits it on refresh. If a refresh response omits refresh_token, details() stores an empty string and the next refresh fails.
  • No proactive refresh. refresh_cron is the default false; the engine refreshes reactively when check_api_error returns error 5 or the shared HTTP layer sees a 401. Access tokens last one hour.
  • Refresh request omits service_token too. Public apps are fine; confidential apps are not.
  • VK returns HTTP 200 on API errors. check_api_error() inspects all["error"] after every API call:
    • error_code 5 → ProviderError::refresh_token (re-auth/refresh);
    • 6 (too many requests per second), 9 (flood control), 29 (rate limit) → ProviderError::Other (retryable);
    • everything else → bad_body with VK’s error_msg. Because VK uses 200, the shared 401/429/500 classification does not see these errors. HTTP-level failures still go through handle_errors, which VK does not override (so a plain 401 is refreshed by the shared layer; 429/500 are retried up to 3 times / 4 attempts).
  • Only wall.post and wall.createComment are implemented. There is no wall.edit, wall.delete, community posting, scheduled post (publish_date), poll, or from_group.
  • Personal wall only. post() does not send owner_id, so it cannot post to a community even if the user is an admin. release_url assumes the post is on the user’s wall.
  • max_length is 2048, much lower than VK’s own wall post limit. Longer text is rejected by Rustiz’s validation before VK sees it.
  • Attachments assume user_id owns every medium. The code builds photo{user_id}_{id} and video{user_id}_{id}. For personal-wall photos and videos this is normally correct, but if VK returns a different owner (for example a community), the attachment string is wrong. The upstream issue flags this as a general bug.
  • username is not a handle. details() sets username = first_name.to_lowercase(), so the channel profile is a lowercased first name, not VK’s screen name. name is "{first} {last}" (a trailing space if the last name is empty).
  • The authorize URL has no prompt, provider, lang_id, or scheme. Users always see the default VK ID flow; there is no way to force consent or bypass interaction from Rustiz.
  • PKCE verifier uses rand::thread_rng, not OsRng. It is a CSPRNG-backed generator in practice, but the code does not use OsRng like the Whop provider. The verifier is 86 base64url characters and the state is 32 alphanumeric characters, both within VK’s requirements.
  • No scope check. check_scopes() is never called; a granted subset is discovered only when a method fails.
  • VK API version is pinned to 5.251. 5.251, 5.199, 5.207 and 5.300 currently respond on api.vk.ru, but VK changes method behavior between versions. A future removal of 5.251 requires a code change.

References