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 aswall.post: the call fails witherror_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
| Property | Value |
|---|---|
| Identifier | vk |
| Display name | VK |
| Connection mode | oauth (VK ID authorization code + PKCE S256) |
| Editor | normal |
| Channel kind | social |
| Comments / threads | Yes. supports_comments = true; comments are created with wall.createComment |
| Pending / async publishing | No. supports_pending = false; post() returns PostStatus::Completed |
| Analytics | No |
| Post analytics | No |
| Mentions | No |
is_between_steps | false |
convert_to_jpeg | false |
max_concurrent_job | 5 (the code comments that VK limits are per user token) |
Character limit (max_length) | 2048 |
| VK ID host | https://id.vk.ru (VK_ID_BASE_URL) |
| VK API host | https://api.vk.ru (VK_BASE_URL) |
| API version | 5.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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
VK_ID | Yes | "" | VK ID application id (client_id). An empty value is rejected before the login URL is shown. |
VK_ID_BASE_URL | No | https://id.vk.ru | Overrides 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_URL | No | https://api.vk.ru | Overrides 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_URL | No | http://localhost:5000 | Public origin used to build the redirect URI. Trailing slashes are stripped. |
JWT_SECRET | No | "" | 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.
- Sign in to the VK ID service with VK Business ID.
- Create/verify the business profile.
- Open Мои приложения → Добавить приложение.
- Choose the Web platform.
- Set the base domain (for example
social.example.com). - Set the trusted redirect URL (see below).
- Upload an icon and create the app.
- 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_URL | Redirect URI sent to VK |
|---|---|
https://social.example.com | https://social.example.com/integrations/social/vk |
http://localhost:5000 | https://redirectmeto.com/http://localhost:5000/integrations/social/vk |
http://192.168.1.10:5000 | https://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) orhttps://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:
| Type | Token exchange | Requirement |
|---|---|---|
| Public | From the client/backend without a service key; 15,000 token requests/day per IP | Rustiz’s authenticate() does not send service_token |
| Confidential | Backend only, IP allowlist, service_token mandatory in every token exchange/refresh; 120,000 requests/day per app | Not 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
| Scope | Used for |
|---|---|
vkid.personal_info | Basic profile: first/last name, avatar, user id |
email | Email on oauth2/user_info (not read by Rustiz, but requested) |
wall | wall.post and wall.createComment |
photos | photos.getWallUploadServer / photos.saveWallPhoto |
video | video.save |
status | Requested but not used by this provider |
docs | Requested but not used by this provider |
In the app’s Доступы section, enable these. VK distinguishes:
- Basic access —
vkid.personal_info(always), andemail(optional). - Extended access —
wall,photos,video,status,docs, and others. Extended access requires a verified business profile. Some permissions (notablywall) are described by VK as granted “in exceptional cases” through a support request todevsupport@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.compath is not registrable under VK’s domain rules. - Expect the first publish to be the real test: VK ID login can succeed while
wallaccess 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:
- verified VK Business ID profile;
- extended access approval for
wall/photos/video(and support approval forwallif VK requires it); - 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:
--fieldsuses thecustomFieldspath and base64-encodes JSON as the OAuthcode; VK’sauthenticate()expects a real authorization code plus device id.--external-urlcallsexternal_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:
-
Uploads every media item (see below), concurrently.
-
Builds
attachmentsas a comma-separated list of{type}{user_id}_{media_id}values, e.g.photo123_456,video123_789. -
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.attachmentsis omitted when there is no media. -
Reads
response.post_id, and returnsPostResponse::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.createCommentwithmessage,post_id, and optionalattachments; - reads
response.comment_id; - returns
release_urlfor the original post; - ignores
last_comment_idcompletely, 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_extensionlowercases 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 thoughmime_for()knows some video MIME types.
-
Photos:
GET /method/photos.getWallUploadServer?owner_id={user_id}&access_token=...&v=5.251POSTthe bytes to the returnedupload_urlas multipart fieldphoto;POST /method/photos.saveWallPhoto?access_token=...&v=5.251withphoto,server,hash;- collect
response[0].id.
-
Videos:
GET /method/video.save?access_token=...&v=5.251;POSTthe bytes to the returnedupload_urlas multipart fieldphoto(the code uses the same field name for both types);- use
video.save’sresponse.video_idas the attachment id.
-
Likely video bug: VK’s
video.saveupload server normally expects the file under the multipart fieldvideo_file(the upstream issue reportsvideo_file). The Rust port sendsphoto, so video uploads may not attach. -
Uploads run concurrently via
futures::future::try_join_all; one failure aborts the whole post.max_concurrent_job = 5limits 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()mapsjpg/jpeg,png,gif,webp,mp4,mov,avi,mkv,webm; anything else becomesapplication/octet-stream. -
No alt text, thumbnails or
convert_to_jpegsupport.
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) reportswall.post→error_code: 1051 — "Method is not available for this profile type", while read-only methods such asusers.getwork. Apps created onid.vk.com/id.vk.rucannot fall back to the legacyoauth.vk.comtoken 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 legacyvk1.a.*user token (oauth.vk.comimplicit flow) or a community token, neither of which this provider implements. wallis an exceptional permission. VK’swall.postdocumentation says thewallaccess right is granted in exceptional cases throughdevsupport@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/authmust useContent-Type: application/x-www-form-urlencoded.auth_form()usesreqwest::multipart::Form, which sendsmultipart/form-data; theoauth2/user_infocall 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()readsaccess_tokendirectly. If VK returns an{"error": ...}body with HTTP 200, the empty access token is passed touserinfoand the connection fails with the generic “The platform did not return an access token” message instead of VK’serror_description. - Token exchange omits
state. VK listsstateas a required parameter forgrant_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_tokenis sent on exchange or refresh, and no backend IP allowlist is configured. Use a public app. device_idis 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.comconflicts with VK’s base-domain rule. VK requires the redirect domain to exactly match the app’s base domain. A localhttp://localhost:5000becomes aredirectmeto.comURL, 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_tokenas{refresh}&&&&{device_id}and re-splits it on refresh. If a refresh response omitsrefresh_token,details()stores an empty string and the next refresh fails. - No proactive refresh.
refresh_cronis the defaultfalse; the engine refreshes reactively whencheck_api_errorreturns error 5 or the shared HTTP layer sees a 401. Access tokens last one hour. - Refresh request omits
service_tokentoo. Public apps are fine; confidential apps are not. - VK returns HTTP 200 on API errors.
check_api_error()inspectsall["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_bodywith VK’serror_msg. Because VK uses 200, the shared 401/429/500 classification does not see these errors. HTTP-level failures still go throughhandle_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.postandwall.createCommentare implemented. There is nowall.edit,wall.delete, community posting, scheduled post (publish_date), poll, orfrom_group. - Personal wall only.
post()does not sendowner_id, so it cannot post to a community even if the user is an admin.release_urlassumes the post is on the user’s wall. max_lengthis 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_idowns every medium. The code buildsphoto{user_id}_{id}andvideo{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. usernameis not a handle.details()setsusername = first_name.to_lowercase(), so the channel profile is a lowercased first name, not VK’s screen name.nameis"{first} {last}"(a trailing space if the last name is empty).- The authorize URL has no
prompt,provider,lang_id, orscheme. 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, notOsRng. It is a CSPRNG-backed generator in practice, but the code does not useOsRnglike 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.207and5.300currently respond onapi.vk.ru, but VK changes method behavior between versions. A future removal of5.251requires a code change.
References
- VK ID app creation and settings (Russian): https://id.vk.ru/about/business/go/docs/ru/vkid/latest/vk-id/connection/create-application
- VK ID API reference (authorize, token, refresh, userinfo): https://id.vk.ru/about/business/go/docs/ru/vkid/latest/vk-id/connection/api-description
- VK ID OAuth 2.1 implementation: https://id.vk.ru/about/business/go/docs/ru/vkid/latest/vk-id/connection/realization
wall.post: https://dev.vk.com/ru/method/wall.postwall.createComment: https://dev.vk.com/ru/method/wall.createCommentphotos.getWallUploadServer: https://dev.vk.com/ru/method/photos.getWallUploadServerphotos.saveWallPhoto: https://dev.vk.com/ru/method/photos.saveWallPhotovideo.save: https://dev.vk.com/ru/method/video.save- VK access rights: https://dev.vk.com/ru/reference/access-rights
- VK API errors: https://dev.vk.com/ru/reference/errors
- Postiz issue #1408 (VK ID token /
wall.post1051, video field): https://github.com/gitroomhq/postiz-app/issues/1408 - Postiz VK settings: https://docs.postiz.com/public-api/providers/vk