VCP 1 Documentation›API Reference
Error Handling
One error envelope, a fixed vocabulary of codes, and unambiguous consumer behavior for each — implement error handling once and it works for every endpoint.
The envelope
Every non-2xx response carries the same JSON shape:
{ "error": { "code": "rate_limited", "message": "Too many requests." } }code is machine-readable and drives consumer behavior; messageis for logs and debugging (clients render provider text as plain text, never markup).
Error codes
| HTTP | error.code | Meaning / consumer behavior |
|---|---|---|
| 400 | invalid_request | Malformed request. |
| 400 | authorization_pending | Pairing not yet approved — keep polling. |
| 400 | slow_down | Increase the poll interval by 5 seconds. |
| 400 | pairing_expired | Restart the pairing flow. |
| 401 | invalid_service_token | Service token missing/invalid — the gateway alerts platform ops; users see the server as temporarily unavailable. |
| 401 | expired_token | Refresh the access token, retry once. |
| 401 | unauthorized | Session token invalid or revoked — re-authenticate. |
| 401 | invalid_user_token | User token invalid/expired — keep the library, prompt for a new token. Never a silent downgrade — see user tokens. |
| 403 | forbidden | Authenticated but not allowed: missing scope, or an object owned by another user. |
| 404 | not_found | Object deleted — remove it locally. |
| 409 | conflict | Write conflicts with current state (e.g. duplicate create) — refetch, then reapply or drop. |
| 410 | gone | Provider discontinued VCP — the client removes the server. Also used for expired delta cursors. |
| 412 | precondition_failed | If-Match version mismatch — refetch the object and reapply the change. See optimistic concurrency. |
| 429 | rate_limited | Back off per Retry-After, exponential + jitter. |
| 5xx | server_error | Retry with backoff, max 3 attempts. |
Two additional client-facing codes never come from your server: the gateway reportsunregistered_publisher (origin not in the registry) andunsupported_version (no shared protocol version) to the app itself.
A retry-aware request helper
The retryable cases — 429 with Retry-After, and transient5xx — are worth wrapping once so every call site gets the behavior for free:
# -i shows the status line and headers; the envelope is in the body
curl -i "https://api.example.com/vcp/v1/collections" \
-H "VCP-Service-Token: vcps_wrong..."
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{ "error": { "code": "invalid_service_token", "message": "Invalid service token." } }import time
import random
import requests
API = "https://api.example.com/vcp/v1"
HEADERS = {"VCP-Service-Token": "vcps_a81f42..."}
def vcp_get(path, **params):
for attempt in range(4):
resp = requests.get(API + path, params=params, headers=HEADERS)
if resp.status_code == 429:
wait = float(resp.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait + random.random())
continue
if resp.status_code >= 500:
time.sleep(2 ** attempt + random.random())
continue
if resp.ok:
return resp.json()
error = resp.json()["error"]
raise RuntimeError(f"{resp.status_code} {error['code']}: {error['message']}")
raise RuntimeError("gave up after retries")
print(len(vcp_get("/collections", limit=50)["collections"]))require "rest-client"
require "json"
API = "https://api.example.com/vcp/v1"
HEADERS = { "VCP-Service-Token" => "vcps_a81f42..." }
def vcp_get(path, params = {})
4.times do |attempt|
begin
resp = RestClient.get("#{API}#{path}", HEADERS.merge(params: params))
return JSON.parse(resp.body)
rescue RestClient::TooManyRequests => error
wait = (error.response.headers[:retry_after] || 2**attempt).to_f
sleep(wait + rand)
rescue RestClient::ServerBrokeConnection, RestClient::InternalServerError,
RestClient::BadGateway, RestClient::ServiceUnavailable
sleep(2**attempt + rand)
rescue RestClient::ExceptionWithResponse => error
body = JSON.parse(error.response.body)
raise "#{error.http_code} #{body.dig("error", "code")}: #{body.dig("error", "message")}"
end
end
raise "gave up after retries"
end
puts vcp_get("/collections", limit: 50)["collections"].lengthconst API = 'https://api.example.com/vcp/v1'
const HEADERS = { 'VCP-Service-Token': 'vcps_a81f42...' }
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000))
async function vcpGet(path) {
for (let attempt = 0; attempt < 4; attempt++) {
const resp = await fetch(API + path, { headers: HEADERS })
if (resp.status === 429) {
const wait = Number(resp.headers.get('Retry-After')) || 2 ** attempt
await sleep(wait + Math.random())
continue
}
if (resp.status >= 500) {
await sleep(2 ** attempt + Math.random())
continue
}
const body = await resp.json()
if (resp.ok) return body
throw new Error(resp.status + ' ' + body.error.code + ': ' + body.error.message)
}
throw new Error('gave up after retries')
}
const data = await vcpGet('/collections?limit=50')
console.log(data.collections.length)