Registry API v1
Quick start
A REST API over JSON. Authenticate with an API key, read the public registry projection, publish your own works, synchronise changes and subscribe to signed webhooks.
API v1 · checking…Base URL and versioning
| Base URL | |
|---|---|
| Production | https://collect24.art/api/v1 |
| Stable alias | https://project--<project-id>.lovable.app/api/v1 |
The version is part of the path. Every response carries x-collect24-api-version: v1 and cache-control: no-store. Additive changes (new fields, new endpoints) can appear within v1; breaking changes get a new version prefix.
1 — Get an API key
Documentation is public; keys are issued after a short review. Request an API key and tell us whether you need read access, publishing access or both. Read keys can be issued quickly; publishing access is linked to a verified gallery, museum or collection profile.
Pass the key on every request. See Authentication for scopes and rate limits.
curl https://collect24.art/api/v1 \
-H "Authorization: Bearer c24_live_..."2 — Retrieve artworks
curl "https://collect24.art/api/v1/artworks?limit=25" \
-H "Authorization: Bearer c24_live_..."{
"object": "list",
"data": [
{
"object": "artwork",
"collect24_id": "C24-4711",
"id": "6f1b1f24-6c0c-4a5f-9a1a-6d2f1d2f0f11",
"title": "Untitled",
"artist": { "id": "b0d0…", "name": "Jane Doe", "slug": "jane-doe" },
"year_created": "2024",
"medium": "Oil on linen",
"dimensions_cm": { "height": 120, "width": 90, "depth": null },
"availability": "for_sale",
"price": { "amount": 8500, "currency": "EUR" },
"location": { "country": "BE" },
"current_collection": {
"slug": "gallery-example",
"name": "Gallery Example",
"type": "gallery",
"city": "Antwerp",
"country": "BE"
},
"images": {
"cover_url": "https://images.example/1.jpg",
"thumbnail_url": "/api/public/thumb/ab/cd/1.jpg"
},
"links": {
"self": "/api/v1/artworks/C24-4711",
"public_page": "https://collect24.art/artwork/6f1b1f24-…"
},
"created_at": "2026-02-01T09:12:44.120Z",
"updated_at": "2026-08-14T07:41:02.005Z"
}
],
"pagination": { "total": 1832, "limit": 25, "offset": 0, "has_more": true }
}3 — Retrieve one artwork by Collect24 Art ID
curl https://collect24.art/api/v1/artworks/C24-4711 \
-H "Authorization: Bearer c24_live_..."{id} accepts the permanent collect24_id or the record uuid. Store the collect24_id: it never changes.
4 — Search and filter
curl "https://collect24.art/api/v1/artworks?q=portrait&availability=for_sale&price_max=10000&limit=50" \
-H "Authorization: Bearer c24_live_..."Unknown query parameters are rejected with 400 invalid_query instead of being ignored, so typos in a sync client surface immediately. Maximum limit is 100. Full parameter list: Artworks.
5 — Create an artwork
curl -X POST https://collect24.art/api/v1/artworks \
-H "Authorization: Bearer c24_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: inv-2024-113-v1" \
-d '{
"title": "Untitled",
"artist_name": "Jane Doe",
"year_created": "2024",
"medium": "Oil on linen",
"height_cm": 120,
"width_cm": 90,
"availability": "for_sale",
"price": { "amount": 8500, "currency": "EUR" },
"cover_image_url": "https://gallery.example/img/1.jpg",
"external_id": "INV-2024-113",
"source_url": "https://gallery.example/works/untitled",
"inventory_ref": "INV-2024-113"
}'{
"object": "artwork_write_result",
"duplicate": false,
"matched_on": null,
"data": { "object": "artwork", "collect24_id": "C24-8123", "title": "Untitled", "...": "..." }
}Placement is derived server-side from your organisation profile: collection_id, gallery_id, owner_id and other ownership fields cannot be set and return 422 protected_field.
6 — Update an artwork
curl -X PATCH https://collect24.art/api/v1/artworks/C24-8123 \
-H "Authorization: Bearer c24_live_..." \
-H "Content-Type: application/json" \
-d '{ "availability": "private_collection" }'A PATCH only succeeds for artworks inside your own profile; anything else returns 403 not_permitted.
7 — Subscribe to webhooks
Webhook endpoints are configured by Collect24 for your organisation: send us the receiving URL and the events you want. You receive a signing secret once. Events, payload shape, retry behaviour and signature verification code are documented in Webhooks.
8 — Verify a webhook signature
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyCollect24Signature(rawBody, headers, signingSecret) {
const signature = headers["x-collect24-signature"] ?? "";
const timestamp = headers["x-collect24-timestamp"] ?? "";
// Reject anything older than five minutes.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected =
"sha256=" + createHmac("sha256", signingSecret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Pagination
limit defaults to 25 and is capped at 100; limit=101 returns 400 invalid_query. offset is capped at 100 000 — do not walk offsets for a full sync, use updated_since. Every list response carries pagination.total, limit, offset and has_more.
Incremental synchronisation
With updated_since the list is ordered ascending on (updated_at, id), which is deterministic even when many records share a timestamp, so paging a window neither skips nor duplicates records. Without it, results are newest-first for browsing.
cursor="2026-08-01T00:00:00Z"
offset=0
while :; do
curl -s "https://collect24.art/api/v1/artworks?updated_since=$cursor&limit=100&offset=$offset" \
-H "Authorization: Bearer c24_live_..." > page.json
# process page.json, then advance
[ "$(jq -r .pagination.has_more page.json)" = "true" ] || break
offset=$((offset + 100))
done
# store the highest updated_at seen and use it as the next cursorRe-processing the boundary record is safe: writes are idempotent on collect24_id.
Rate limits
Limits are per key, per minute, and enforced durably. Every response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (unix seconds). Over the limit you receive 429 rate_limited with a Retry-After header and retry_after_seconds in the body. The default ceiling is 120 requests per minute.
Idempotency
Send an Idempotency-Key header on POST /api/v1/artworks — recommended for retries and queue workers. The first request stores its result; a repeat with the same key and the same body replays that stored response (with idempotent-replay: true) instead of creating a second record. A repeat with the same key and a different body returns 409 idempotency_key_reused. A repeat while the first call is still running returns 409 idempotent_request_in_progress.
Deduplication
Independently of the header, every create is checked against the registry's identity keys in order of confidence: fingerprint (artist, title, year, dimensions), external_id, inventory_ref, source_url and image URL. A match returns 200 with duplicate: true, matched_on and the existing artwork — nothing is created.
{
"object": "artwork_write_result",
"duplicate": true,
"matched_on": "external_id",
"data": { "object": "artwork", "collect24_id": "C24-4711", "...": "..." }
}Withdrawn and private artworks
Reads go through the public registry projection. A work set to availability: "fully_private" leaves that projection: the PATCH responds 200 with data: null and public: false, and every later GET, search, filter and collection listing returns 404 artwork_not_found. The same applies to unpublished, withheld and removed works.
What is never exposed
Serializers are explicit allow-lists, so adding a column to the database never adds it to the API. The API never returns owner or user identifiers, private notes, valuations, acquisition prices, ownership records, documents, offers, transactions, private provenance, admin review fields or internal pipeline state. The holder's country appears only when it is set to be shown publicly.

