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
Productionhttps://collect24.art/api/v1
Stable aliashttps://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.

Verify your key
curl https://collect24.art/api/v1 \
  -H "Authorization: Bearer c24_live_..."

2 — Retrieve artworks

GET /api/v1/artworks
curl "https://collect24.art/api/v1/artworks?limit=25" \
  -H "Authorization: Bearer c24_live_..."
200 response
{
  "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

GET /api/v1/artworks/{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

Search
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

POST /api/v1/artworks
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"
  }'
201 response
{
  "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

PATCH /api/v1/artworks/{id}
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

Node.js
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.

Sync loop
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 cursor

Re-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.

200 duplicate response
{
  "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.