Request Conventions
The headers, pagination, caching, and data rules shared by every VCP endpoint — learn them once, they apply everywhere.
All request and response bodies are application/json, UTF-8, over HTTPS (TLS 1.2+). There is no request-signing layer: TLS provides integrity and confidentiality, and the service token authenticates the gateway.
Headers
Every API request from the gateway to your server looks like this:
GET {api_base}/collections HTTP/1.1
VCP-Service-Token: vcps_a81f42...
Authorization: Bearer vcpa_71be02...
VCP-User-Token: vcpu_5f19ab...
VCP-Client: vctv-ios/2.4.1
VCP-Client-IP: 203.0.113.40
Accept: application/json| Header | Presence | Meaning |
|---|---|---|
VCP-Service-Token | Always | Gateway identification — see the service token. |
Authorization | With a session | End-user session credential: a pasted API key or a paired access token. |
VCP-User-Token | When supplied | Privileged-access user token. |
VCP-Client | Always | App name/version, for your analytics and debugging. Never key behavior on it. |
VCP-Client-IP | Always | The end-user device's public IP as seen by the gateway. If you geo-restrict or sign media URLs against an IP, use this — never the connecting (gateway) IP, because media is fetched by the device. |
Pagination
Every list endpoint is cursor-based:
- Request —
?limit=50&cursor=…, withlimitcapped at your manifest'slimits.max_page_size(default 100). - Response —
"next_cursor": "…", an opaque string that is absent ornullon the last page.
Cursors must remain valid for at least 10 minutes and must be treated as opaque — consumers never parse them. Draining a full list looks like this:
# First page
curl "https://api.example.com/vcp/v1/collections?limit=100" \
-H "VCP-Service-Token: vcps_a81f42..."
# Follow next_cursor from each response until it comes back null
curl "https://api.example.com/vcp/v1/collections?limit=100&cursor=eyJvIjoxMDB9" \
-H "VCP-Service-Token: vcps_a81f42..."import requests
API = "https://api.example.com/vcp/v1"
HEADERS = {"VCP-Service-Token": "vcps_a81f42..."}
def fetch_all(path, key):
items, cursor = [], None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
resp = requests.get(API + path, params=params, headers=HEADERS)
resp.raise_for_status()
data = resp.json()
items += data[key]
cursor = data.get("next_cursor")
if not cursor:
return items
collections = fetch_all("/collections", "collections")
print(len(collections), "collections")require "rest-client"
require "json"
API = "https://api.example.com/vcp/v1"
HEADERS = { "VCP-Service-Token" => "vcps_a81f42..." }
def fetch_all(path, key)
items = []
cursor = nil
loop do
params = { limit: 100 }
params[:cursor] = cursor if cursor
resp = RestClient.get("#{API}#{path}", HEADERS.merge(params: params))
data = JSON.parse(resp.body)
items.concat(data[key])
cursor = data["next_cursor"]
break if cursor.nil?
end
items
end
puts "#{fetch_all("/collections", "collections").length} collections"const API = 'https://api.example.com/vcp/v1'
const HEADERS = { 'VCP-Service-Token': 'vcps_a81f42...' }
async function fetchAll(path, key) {
const items = []
let cursor = null
do {
const url = new URL(API + path)
url.searchParams.set('limit', 100)
if (cursor) url.searchParams.set('cursor', cursor)
const resp = await fetch(url, { headers: HEADERS })
if (!resp.ok) throw new Error('VCP request failed: ' + resp.status)
const data = await resp.json()
items.push(...data[key])
cursor = data.next_cursor
} while (cursor)
return items
}
const collections = await fetchAll('/collections', 'collections')
console.log(collections.length + ' collections')Caching
List and detail responses should include an ETag; the gateway sendsIf-None-Match and handles 304 Not Modified:
# First fetch: note the ETag response header
curl -i "https://api.example.com/vcp/v1/collections" \
-H "VCP-Service-Token: vcps_a81f42..."
# HTTP/1.1 200 OK
# ETag: "col-list-v18"
# Later fetch: send it back — an unchanged list costs almost nothing
curl -i "https://api.example.com/vcp/v1/collections" \
-H "VCP-Service-Token: vcps_a81f42..." \
-H 'If-None-Match: "col-list-v18"'
# HTTP/1.1 304 Not ModifiedEvery object also carries updated_at (RFC 3339 UTC) — the basis fordelta sync, and the fallback comparison for providers without it.
Common data rules
| Rule | Value |
|---|---|
| IDs | Opaque strings, stable forever, ≤ 128 chars, [A-Za-z0-9_-]. |
| Timestamps | RFC 3339 UTC — 2026-07-16T09:30:00Z. |
| Durations / positions | Seconds, as a JSON number. |
| Language tags | BCP 47 — en, pt-BR. |
| Unknown JSON fields | Ignored by consumers; allowed for provider extensions (prefix custom fields x_). |
Rate limiting
You may rate-limit per end-user token and overall per publisher integration. If you do, respond 429 with a Retry-After header when exceeded — the gateway backs off with exponential delay plus jitter. SeeError Handling for the full retry rules.