Gallery Platform API

Webhooks

Register one endpoint per gallery and receive a signed JSON event whenever the gallery's content changes.

Webhooks are the push counterpart to the REST API. A gallery registers one endpoint and the platform POSTs a signed JSON event to it whenever the gallery's content changes — so you don't have to poll /api/v1.

Subscribing

Register your endpoint in the gallery sidebar's Developer → Webhooks section in the app and select the events you want. That page is also where the signing secret is revealed — copy it; you'll need it to verify deliveries. There is one endpoint per gallery; webhook subscriptions are managed in-app, not through an API key.

Event types

Events are named resource.action. Currently emitted:

ResourceEvents
Artworksartwork.created, artwork.updated, artwork.deleted, artwork.interest_added
Gallerygallery.updated, gallery.member_added, gallery.member_removed, gallery.member_role_changed
Contactscontact.created, contact.updated, contact.deleted, contact.erased, interaction.logged
Offersoffer.sent, offer.opened, offer.artwork_viewed
Invoices & paymentsinvoice.issued, invoice.paid, payment.recorded, payment.refunded
Payablespayable.created, payable.paid
Publicationspublication.updated

The in-app Webhooks page always shows the live event catalog — new event types are added as the platform grows, so ignore event types you don't recognize rather than failing on them.

The delivery request

POST <your endpoint>
Content-Type: application/json
User-Agent: GalleryPlatform-Webhooks/1
x-gallery-webhook-id: 8f0c…
x-gallery-webhook-event: artwork.created
x-gallery-webhook-signature: t=1750507200,v1=<hex>
{
  "id": "8f0c…",
  "type": "artwork.created",
  "created_at": "2026-06-21T12:00:00.000Z",
  "gallery_id": "…",
  "data": { }
}

data carries a snapshot of the affected resource, taken at emit time.

Verifying the signature

The x-gallery-webhook-signature header is t=<unix timestamp>,v1=<hex>, where v1 is HMAC-SHA256(secret, "<t>.<rawBody>") — it signs the timestamp and the raw request body, so both are tamper-evident.

To verify:

  1. Read t and v1 from the header.
  2. Compute HMAC-SHA256(secret, t + "." + rawRequestBody) (use the raw body bytes, before JSON parsing).
  3. Compare against v1 with a constant-time comparison.
  4. Reject deliveries whose t is too old to bound replay.
import crypto from "node:crypto";

const TOLERANCE_SECONDS = 5 * 60;

function verify(secret: string, rawBody: string, header: string): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  if (!parts.t || !parts.v1) return false;

  // Bound replay: reject signatures older than the tolerance.
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const received = Buffer.from(parts.v1);
  // timingSafeEqual throws on length mismatch — check length first.
  return (
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), received)
  );
}

Delivery, retries & idempotency

  • Delivery is attempted immediately after the change, then retried with backoff (1m → 5m → 30m → 2h) up to 5 attempts before the delivery is marked failed.
  • Because of retries, an event can be delivered more than once. Deduplicate on x-gallery-webhook-id (also the payload id) — it's stable per event.
  • Respond 2xx promptly to acknowledge; do slow work asynchronously.

On this page