Documentation contents

VCP 1 Documentation›Publishing

Authentication

The service token authenticates the gateway; this page is about the end user — three auth modes, plus an optional user token for privileged access.

Your manifest's auth.type declares one of three modes: none,api_key, or pairing. User credentials are held on the device, relayed per-request by the gateway, and never persisted by the VC service. Independently of the mode, you can also accept a user token that upgrades what the API returns.

ModeUser experienceBest for
noneNothing to doFully public catalogs.
api_keyPastes a key from your settings pageSites that already hand out personal keys.
pairingApproves the device in their own browserRecommended — no credentials ever typed into the player.

none — public library

No handshake. Requests carry only the service token.

manifest.json — auth
"auth": { "type": "none" }

api_key — pasted key

For sites that hand users a personal key on a settings page. The client shows yourkey_hint, optionally deep-links key_url, and the user pastes the key. It's relayed on every request as Authorization: Bearer vcpk_….

manifest.json — auth
"auth": {
  "type": "api_key",
  "key_hint": "Find your key at example.com/settings/vcp",
  "key_url": "https://example.com/settings/vcp"
}

Provider requirements:

  • Keys should carry a recognizable prefix (vcpk_) so leaked keys can be found by secret scanners.
  • Keys must be revocable per-device/per-key from the user's settings page.
  • Keys must carry at least 128 bits of entropy.

pairing — device authorization (recommended)

Modeled on the OAuth 2.0 Device Authorization Grant (RFC 8628). The user never types credentials into the player: the app shows a short code, and the user approves it in their own logged-in browser session on your site. All /pair and/token calls are made by the gateway (with the service token) on behalf of the device; only the approval leg is direct user↔provider.

Client ──▶ VC service                Provider                        User's browser
  │ POST /pair (via gateway)             │                               │
  │ ────────────────────────────────▶    │                               │
  │ ◀──────────────────────────────── 201: pairing_code, poll_token,     │
  │                                      verification_url, expires_in    │
  │                                       │                              │
  │  (shows code "XKCD-9204" and          │   user logs in, enters or    │
  │   opens verification_url)             │◀─ confirms the code ─────────│
  │                                       │                              │
  │ POST /token  (grant_type=pairing_poll, poll_token)  ... every 5 s    │
  │ ────────────────────────────────▶    │                               │
  │ ◀── 400 authorization_pending  (until approved)                      │
  │ ◀── 200: access_token, refresh_token, expires_in                     │

The flow in code

# Step 1 - create a pairing request
curl -X POST "https://api.example.com/vcp/v1/pair" \
  -H "VCP-Service-Token: vcps_a81f42..." \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "VClarity",
    "client_version": "2.4.1",
    "device_model": "iPhone16,2",
    "device_name": "Ash phone"
  }'

# Step 2 - poll until the user approves in their browser
# (400 authorization_pending until then)
curl -X POST "https://api.example.com/vcp/v1/token" \
  -H "VCP-Service-Token: vcps_a81f42..." \
  -H "Content-Type: application/json" \
  -d '{ "grant_type": "pairing_poll", "poll_token": "vcpp_9f31c6..." }'

The pairing request returns:

POST {auth.pairing_endpoint}
201 Created

{
  "pairing_code": "XKCD-9204",
  "poll_token": "vcpp_9f31c6...",
  "verification_url": "https://example.com/vcp/approve?code=XKCD-9204",
  "expires_in": 600,
  "poll_interval": 5
}

While pending, the token endpoint answers 400 authorization_pending — poll every poll_interval seconds, and add 5 seconds whenever you seeslow_down. After expires_in seconds it answers400 pairing_expired and the client restarts pairing. On approval:

POST {auth.token_endpoint}
200 OK

{
  "access_token": "vcpa_71be02...",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "vcpr_c04d11...",
  "scope": "library.read progress.write"
}

Provider requirements for pairing codes:

  • Codes must be single-use, expire in 10 minutes or less, and be unpredictable. As little as 20 bits shown to the user is acceptable only because approval happens in an authenticated browser session, attempts are rate-limited, and pairing can only be initiated through the gateway. The poll_token itself must carry at least 128 bits.
  • The approval page must display the requesting client_name and device_name, and require an authenticated session.
  • Code-entry and approval attempts must be rate-limited (e.g. 5 tries per code).

Refreshing tokens

Access tokens should be short-lived (24 hours or less). When one expires, the gateway exchanges the refresh token for a new pair:

curl -X POST "https://api.example.com/vcp/v1/token" \
  -H "VCP-Service-Token: vcps_a81f42..." \
  -H "Content-Type: application/json" \
  -d '{ "grant_type": "refresh_token", "refresh_token": "vcpr_c04d11..." }'

User tokens — privileged access

Independently of auth.type, you may accept a user token: a credential the user copies from their signed-in account on your website (a premium membership, a private-library grant) and pastes into the client next to the server URL. Presenting it upgrades what the API returns — typically unlocking privileged stream sources (higher resolutions, members-only items) that anonymous or merely-paired sessions don't get.

Advertise support in your manifest:

manifest.json — auth.user_token
"user_token": {
  "accepted": true,
  "hint": "Paste your access token from example.com/account/token",
  "url": "https://example.com/account/token"
}

The token travels in its own header on every API request — never inAuthorization — so it composes with any auth mode without collision. Effective access is the union of what the session credential and the user token each grant:

# The user token travels in its own header, alongside whatever session
# credential the request already carries:
curl "https://api.example.com/vcp/v1/videos/vid_88213" \
  -H "VCP-Service-Token: vcps_a81f42..." \
  -H "Authorization: Bearer vcpa_71be02..." \
  -H "VCP-User-Token: vcpu_5f19ab..."

Rules:

  • User tokens should carry the vcpu_ prefix, must have at least 128 bits of entropy, and must be revocable from the user's account page on your site.
  • An invalid or expired user token is a request failure —401 with code invalid_user_token — never a silent downgrade to public access. A silent downgrade would make the client cache public sources against a user who believes they have privileged access.
  • Clients respond to invalid_user_token by keeping the library, marking the server "token needs attention", and prompting for a new token. The stale token is not retried automatically.
  • The token lives in the device secure store; the gateway relays it per-request and never persists it.
  • Scope privileged behavior to content (which sources and items are visible), not protocol shape — responses stay structurally identical with and without a token.

Scopes

Scope names follow one convention: library.read covers every read endpoint, and each capability that writes provider-side state defines a<capability>.write scope. You may ignore scoping entirely and treat every credential as all-scopes.

ScopeGrants
library.readAll read endpoints.
progress.writePUT /videos/{id}/progress
reactions.writeReaction writes.
playlists.writePlaylist CRUD and reordering.

A future or vendor capability that introduces writes reuses the same pattern (x_acme_comments.write), so clients can reason about scopes without knowing the feature.

Coming soon!

We're putting the finishing touches on VClarity. It'll be on the App Store and Google Play very soon.