Keeping content current
React to catalogue changes.
Use cursor-based Sync as the reliable change feed for a local index. Use webhooks and delivery controls when your integration needs event-driven processing or delivery testing.
Choose the right mechanism
Incremental Sync
Pull changes in order
Best for scheduled jobs, recovery, local search indexes and deterministic progress through the complete change feed.
GET /v1/sync
Webhooks
Process an event delivery
Best for low-latency triggers and event-driven refresh. Consumers must verify signatures and deduplicate deliveries.
/v1/webhooks/*
Large integrations commonly use both: webhooks trigger fast refreshes and Sync closes any gap after downtime.
Incremental Sync
Start without a cursor. Persist next_cursor only after your CMS has committed the page successfully, then pass it unchanged on the next request.
curl --get "https://collection.vieunite.com/v1/sync" \
-H "Authorization: Bearer $VIEUNITE_TENANT_TOKEN" \
--data-urlencode "types=artwork,collection" \
--data-urlencode "limit=100" \
--data-urlencode "cursor=$LAST_COMMITTED_CURSOR"
{
"data": [
{
"event_id": "evt_123",
"type": "artwork",
"id": "art_456",
"action": "update",
"version": 7,
"updated_at": "2026-07-15T16:30:43Z",
"idempotency_key": "artwork:art_456:7",
"links": { "resource": "/v1/artworks/art_456" }
}
],
"next_cursor": "eyJvZmZzZXQiOjEwMH0",
"has_more": true
}
Apply change events
publishorupdateResolve or fetch the current resource and replace the cached version transactionally.deleteRemove it from discovery and mark stored references unavailable using the supplied reason.- Repeated eventDeduplicate with
idempotency_keyor compare the incoming resource version. - Processing failureDo not advance the committed cursor until the page can be replayed safely.
Register a webhook endpoint
Creation returns the signing secret once. Store it immediately in your secret manager.
curl --request POST "https://collection.vieunite.com/v1/webhooks/endpoints" \
-H "Authorization: Bearer $VIEUNITE_TENANT_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"url": "https://cms.example.com/webhooks/vieunite",
"event_types": [
"artwork.updated",
"artwork.unpublished",
"artwork.rights_updated",
"artwork.asset_updated",
"collection.updated"
]
}'
Verify webhook signatures
Compute a base64 HMAC-SHA256 over <timestamp>.<raw-request-body>. Use the unparsed request bytes, reject stale timestamps, and compare in constant time.
import crypto from "node:crypto";
const MAX_AGE_SECONDS = 300;
function verifyVieuniteWebhook({ rawBody, headers, secret }) {
if (!Buffer.isBuffer(rawBody)) {
throw new TypeError("rawBody must be the unparsed request Buffer");
}
const timestamp = headers["x-vieunite-timestamp"] || "";
const supplied = headers["x-vieunite-signature"] || "";
if (!/^\d+$/.test(timestamp)) return false;
const timestampSeconds = Number(timestamp);
const age = Math.abs(Math.floor(Date.now() / 1000) - timestampSeconds);
if (!Number.isSafeInteger(timestampSeconds) || age > MAX_AGE_SECONDS) {
return false;
}
const signedBytes = Buffer.concat([
Buffer.from(timestamp, "ascii"),
Buffer.from(".", "ascii"),
rawBody,
]);
const digest = crypto
.createHmac("sha256", secret)
.update(signedBytes)
.digest("base64");
const expected = Buffer.from(`v1=${digest}`, "ascii");
const received = Buffer.from(supplied, "ascii");
return expected.length === received.length &&
crypto.timingSafeEqual(expected, received);
}
import base64
import hashlib
import hmac
import time
MAX_AGE_SECONDS = 300
def verify_vieunite_webhook(*, raw_body: bytes, headers, secret: str) -> bool:
if not isinstance(raw_body, bytes):
raise TypeError("raw_body must be the unparsed request bytes")
timestamp = headers.get("x-vieunite-timestamp", "")
supplied = headers.get("x-vieunite-signature", "")
if not timestamp.isascii() or not timestamp.isdigit():
return False
age = abs(int(time.time()) - int(timestamp))
if age > MAX_AGE_SECONDS:
return False
signed_bytes = timestamp.encode("ascii") + b"." + raw_body
digest = base64.b64encode(
hmac.new(secret.encode("utf-8"), signed_bytes, hashlib.sha256).digest()
).decode("ascii")
return hmac.compare_digest(f"v1={digest}", supplied)
<?php
const MAX_AGE_SECONDS = 300;
function verifyVieuniteWebhook(
string $rawBody,
array $headers,
string $secret
): bool {
$timestamp = $headers["x-vieunite-timestamp"] ?? "";
$supplied = $headers["x-vieunite-signature"] ?? "";
if (!ctype_digit($timestamp)) {
return false;
}
$timestampSeconds = filter_var($timestamp, FILTER_VALIDATE_INT);
if ($timestampSeconds === false ||
abs(time() - $timestampSeconds) > MAX_AGE_SECONDS) {
return false;
}
// $rawBody must come directly from php://input. PHP strings are binary-safe.
$signedBytes = $timestamp . "." . $rawBody;
$digest = base64_encode(
hash_hmac("sha256", $signedBytes, $secret, true)
);
return hash_equals("v1=" . $digest, $supplied);
}
X-Vieunite-TimestampUnix timestamp included in the signed message.
X-Vieunite-Signaturev1=<base64-signature>.
Idempotency-KeyStable key for deduplicating the event.
Delivery controls
The API exposes event listing and explicit delivery preparation. A dry run returns the exact payload, signature headers and eligible target without sending an HTTP request.
curl --request POST "https://collection.vieunite.com/v1/webhooks/events/evt_123/deliver" \
-H "Authorization: Bearer $VIEUNITE_TENANT_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"endpoint_id": "wep_123",
"dry_run": true,
"timeout_seconds": 5
}'
Only set dry_run to false when intentionally requesting a real synchronous delivery. Treat this as an advanced testing or replay operation.