Publisher Registration
Register your origin with the VC platform, prove you control it, and validate the service token that authenticates every request you'll ever receive.
A provider becomes a publisher by signing up on theVC publisher portal. Registration is the only gate in the system: the gateway will only ever contact origins recorded in the publisher registry, and users can only import from registered publishers.
What you register
| Registered datum | Notes |
|---|---|
| Origin | The https:// origin users will type (e.g. https://example.com). One registration per origin. |
| Manifest | Your discovery document, submitted through the portal. It must validate at submission time. |
| Contact | An operational/security contact for the integration. |
At registration the platform:
- Validates your manifest (schema,
https:URLs, size ≤ 64 KB). - Verifies you control the origin via a DNS TXT record, or a well-known file for hosts without DNS access (below).
- Issues your public
publisher_id(e.g.pub_3f92ka) and your service secret — shown once, retrievable only by rotation.
Manifest changes — new capabilities, endpoint moves, version bumps — are made by re-submitting the manifest through the portal. The registry copy is the single source of truth the gateway consults; your API never has to serve the manifest itself.
Origin verification (DNS TXT)
Registration must prove you control the origin's domain. The portal gives you a challenge token; you publish it on the host _vcp.<origin-host>:
# The portal issues a per-registration challenge token. Publish it as a
# TXT record on _vcp.<your-origin-host>:
#
# _vcp.example.com. TXT "vcp-verify=chal_7f20c1a9..."
#
# Then confirm it resolves before finishing registration:
dig TXT _vcp.example.com +short
"vcp-verify=chal_7f20c1a9..."Multiple vcp-verify records may coexist, which keeps re-registration overlap painless.
No DNS access? The HTTP fallback
If your site lives on a shared-platform subdomain — your-app.pages.dev, and the like — you can't create DNS records there. Serve the challenge from your site itself instead:
# No DNS control over your host (e.g. your-app.pages.dev)? Serve the
# challenge as a plain-text file instead, one challenge per line:
#
# https://your-app.pages.dev/.well-known/vcp-verify
#
# Confirm it's live before pressing verify:
curl -s https://your-app.pages.dev/.well-known/vcp-verify
vcp-verify=chal_7f20c1a9...The platform checks DNS first, then the file — you only need one of the two. Prefer the DNS record when you can: it proves control of the whole domain, while the file proves only control of the served content.
The service token
Your service secret is a shared secret between the VC platform and you alone. It has avcps_ prefix, carries at least 256 bits of entropy, and is unique per publisher — a leaked secret exposes only that one integration. It is never sent to devices, never embedded in the app, and never appears in a URL. The gateway sends it in a header on every request:
GET /vcp/v1/collections HTTP/1.1
Host: api.example.com
VCP-Service-Token: vcps_a81f42...Your obligations as a provider:
- Validate the token on every VCP endpoint with a constant-time comparison.
- Missing or invalid token →
401with error codeinvalid_service_token. No exceptions — no VCP resource is ever served without it. - Store the secret hashed or in a secret manager, like any API credential.
- Treat a valid token as the guarantee the request comes from the VC service — from infrastructure that has already applied app integrity checks, per-user rate limiting, and platform policy. You may expose privileged catalogs over VCP that your public website doesn't offer, on the strength of this header.
Validating the token
# A request without the token must be rejected — try it against your API:
curl -i "https://api.example.com/vcp/v1/collections"
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{ "error": { "code": "invalid_service_token", "message": "Missing or invalid service token." } }import hmac
# Two active secrets during a rotation window, one otherwise.
ACTIVE_SECRETS = ["vcps_new_secret...", "vcps_old_secret..."]
def valid_service_token(header_value):
if not header_value:
return False
return any(hmac.compare_digest(secret, header_value) for secret in ACTIVE_SECRETS)
# In your request handler:
# if not valid_service_token(request.headers.get("VCP-Service-Token")):
# return json_error(401, "invalid_service_token")require "openssl"
# Two active secrets during a rotation window, one otherwise.
ACTIVE_SECRETS = ["vcps_new_secret...", "vcps_old_secret..."]
def valid_service_token?(header_value)
return false if header_value.nil?
ACTIVE_SECRETS.any? { |secret| OpenSSL.secure_compare(secret, header_value) }
end
# In your request handler:
# halt_with_error(401, "invalid_service_token") unless
# valid_service_token?(request.env["HTTP_VCP_SERVICE_TOKEN"])import { timingSafeEqual } from 'node:crypto'
// Two active secrets during a rotation window, one otherwise.
const ACTIVE_SECRETS = ['vcps_new_secret...', 'vcps_old_secret...']
function validServiceToken(header_value) {
if (!header_value) return false
const candidate = Buffer.from(header_value)
return ACTIVE_SECRETS.some((secret) => {
const expected = Buffer.from(secret)
return expected.length === candidate.length && timingSafeEqual(expected, candidate)
})
}
// In your request handler:
// if (!validServiceToken(req.headers['vcp-service-token'])) {
// return jsonError(res, 401, 'invalid_service_token')
// }Rotation & revocation
- Rotation — you rotate the secret from the portal. During rotation the platform sends the new secret; accept both old and new for an overlap window (24 hours or less is recommended), then retire the old one. That's why the validation examples above check a list.
- Compromise — revoke the secret from the portal. The gateway stops using it immediately and a replacement is issued.
- Publisher revocation — the platform may revoke a publisher entirely (policy violation, abandoned integration). The gateway then refuses the origin and clients show the library as unavailable.