Keeping content current
Keep your artwork data up to date.
Artwork can be updated or withdrawn after an editor selects it. Use Sync to refresh your local cache and stored references. Add webhooks when you have a delivery process in place and need faster updates.
Choose the right mechanism
Incremental Sync
Pull changes in order
Start here for a scheduled refresh or local search index. A saved cursor lets your job resume after a restart.
GET /v1/sync
Webhooks
Process an event delivery
Use delivered events to trigger a refresh. Verify the signature and make repeated deliveries safe to process.
/v1/webhooks/*
You can use both: delivered webhooks trigger a refresh, while Sync catches changes missed during downtime.
Incremental Sync
On the first request, omit cursor. For later requests, use the last cursor you successfully committed. Apply the page of changes and save its next_cursor in the same database transaction so a crash cannot skip unapplied events.
curl --get "https://collection-test.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
}
Continue while has_more is true. Once caught up, keep the committed cursor and poll again on your next scheduled run. If processing fails, retry the same page before advancing.
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
Choose an HTTPS route on your backend that can read the raw request body. Register it below, then store the returned signing secret in your secret manager. You’ll use that secret to verify requests.
curl --request POST "https://collection-test.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
Verify the signature before parsing or processing the event. The signature is a base64 HMAC-SHA256 of <timestamp>.<raw-request-body>. The examples accept timestamps within five minutes and compare signatures in constant time.
Pass the original request bytes and a header map with lowercase names to these helpers. Configure your framework’s raw-body support before JSON middleware; serialising parsed JSON again will change the bytes and fail verification.
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-test.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.