VCP 1 Documentation›API Reference
Delta Sync
The optional changes feed that lets clients re-sync a large imported library without re-crawling it.
Advertise the changes keyword in your manifest'scapabilities to mount this route. Without it, clients re-sync by re-listing everything and comparing updated_at — which works, but costs far more requests on both sides.
GET/changes?since={cursor}
# At first import: no "since" param -> empty list + the current cursor
curl "https://api.example.com/vcp/v1/changes" \
-H "VCP-Service-Token: vcps_a81f42..."
# Every re-sync after that: pass the stored cursor
curl "https://api.example.com/vcp/v1/changes?since=chg_8842&limit=200" \
-H "VCP-Service-Token: vcps_a81f42..."import requests
API = "https://api.example.com/vcp/v1"
HEADERS = {"VCP-Service-Token": "vcps_a81f42..."}
# At first import: store the current cursor (the change list is empty)
since = requests.get(f"{API}/changes", headers=HEADERS).json()["next_since"]
# Every re-sync after that: drain everything since the stored cursor
while True:
resp = requests.get(f"{API}/changes",
params={"since": since, "limit": 200}, headers=HEADERS)
if resp.status_code == 410:
full_recrawl() # cursor expired -> re-list everything
break
resp.raise_for_status()
data = resp.json()
for change in data["changes"]:
apply_change(change) # e.g. refetch vid_x, or delete col_y locally
if not data["changes"]:
break
since = data["next_since"] # persist for the next syncrequire "rest-client"
require "json"
API = "https://api.example.com/vcp/v1"
HEADERS = { "VCP-Service-Token" => "vcps_a81f42..." }
# At first import: store the current cursor (the change list is empty)
since = JSON.parse(RestClient.get("#{API}/changes", HEADERS).body)["next_since"]
# Every re-sync after that: drain everything since the stored cursor
loop do
begin
resp = RestClient.get("#{API}/changes",
HEADERS.merge(params: { since: since, limit: 200 }))
rescue RestClient::Gone
full_recrawl # cursor expired -> re-list everything
break
end
data = JSON.parse(resp.body)
data["changes"].each { |change| apply_change(change) }
break if data["changes"].empty?
since = data["next_since"] # persist for the next sync
endconst API = 'https://api.example.com/vcp/v1'
const HEADERS = { 'VCP-Service-Token': 'vcps_a81f42...' }
// At first import: store the current cursor (the change list is empty)
let since = (await (await fetch(API + '/changes', { headers: HEADERS })).json()).next_since
// Every re-sync after that: drain everything since the stored cursor
while (true) {
const resp = await fetch(API + '/changes?since=' + since + '&limit=200', { headers: HEADERS })
if (resp.status === 410) {
await fullRecrawl() // cursor expired -> re-list everything
break
}
if (!resp.ok) throw new Error('VCP request failed: ' + resp.status)
const data = await resp.json()
for (const change of data.changes) await applyChange(change)
if (!data.changes.length) break
since = data.next_since // persist for the next sync
}{
"changes": [
{ "type": "video.updated", "id": "vid_88213", "updated_at": "2026-07-15T02:00:00Z" },
{ "type": "video.deleted", "id": "vid_11209" },
{ "type": "collection.updated", "id": "col_summer24" },
{ "type": "collection.deleted", "id": "col_old" }
],
"next_since": "chg_9016"
}How it works
- Bootstrap — at first import the client calls
GET /changeswith nosince. You return an empty list plus the currentnext_sincecursor, which the client stores. - Re-sync — the client passes its stored cursor and applies each entry:
*.updatedmeans refetch that object,*.deletedmeans remove it locally. It then stores the newnext_since. - Entry types follow the pattern
<object>.<created|updated|deleted>—video.updated,collection.deleted, and so on.
Cursor lifetime
since cursors must stay valid for at least 30 days. If you can't honor a cursor (it's too old, your change log was truncated), return 410 gone — that signals the client to do a full re-crawl and bootstrap a new cursor.