Webhooks

Webhooks send leads and designs from the ring builder to your own server, CRM or automation tool as they happen. They are available once the ring builder is claimed. Unclaimed sandboxes never send webhooks.

#Set up a webhook

  1. In the ShowroomPro dashboard, open Settings → Integrations → Webhooks.
  2. Enter an Endpoint URL. It must be a publicly reachable HTTPS URL that accepts POST.
  3. Copy the Signing Secret. You need it to verify signatures and to call the session API.
  4. Choose the events to send and save.
  5. Click Send test to receive a test.ping. The Recent deliveries list shows the status code of every attempt.

#Events

EventWhenIncludes
lead.createdA shopper entered their details in the quizContact details only
design.generatedA session's first AI designs were generatedContact details, every design choice, image URLs
quote.requestedThe shopper approved a design and asked for a quoteSame as design.generated, plus customerNotes; imageUrls holds only the approved image
test.pingYou clicked Send testA test message

design.approved appears in the event list but is reserved and not sent today. Use quote.requested.

Refinements after the first generation don't re-send design.generated. To get every image, read the session with the session API.

#lead.created

json
{
  "event": "lead.created",
  "timestamp": "2026-10-16T14:02:11.204Z",
  "sessionId": "8d1f0c3e-…",
  "sessionUrl": "https://www.showroompro.ai/api/webhooks/sessions/8d1f0c3e-…",
  "shareUrl": "https://www.showroompro.ai/design/…",
  "customer": { "name": "Maya Chen", "email": "maya@example.com", "phone": "+15555550123", "country": "US" }
}

#design.generated

json
{
  "event": "design.generated",
  "timestamp": "2026-10-16T14:04:40.019Z",
  "sessionId": "8d1f0c3e-…",
  "sessionType": "engagement-ring",
  "sessionUrl": "https://www.showroompro.ai/api/webhooks/sessions/8d1f0c3e-…",
  "shareUrl": "https://www.showroompro.ai/design/…",
  "customer": { "name": "Maya Chen", "email": "maya@example.com", "phone": "+15555550123", "country": "US" },
  "iterationNumber": 1,
  "imageUrls": ["https://…/design-1.png", "https://…/design-2.png"],
  "preferences": {
    "stoneType": "lab-diamond",
    "stoneShape": "oval",
    "stoneCarat": "1.5",
    "settingStyle": "halo",
    "metalType": "platinum",
    "bandStyle": "pave",
    "bandWidth": "2mm",
    "settingHeight": "low",
    "finishingTouches": ["hidden-halo"],
    "budgetRange": "5k-10k",
    "ringSize": "6",
    "timeline": "3-to-6-months",
    "purpose": null,
    "additionalNotes": null,
    "engraving": null,
    "engravingPlacement": null,
    "sideStoneCount": null,
    "sideStoneShape": null
  }
}

Field values are slugs (for example lab-diamond, oval, white-gold). Budget, timeline and band width can also be custom labels if the jeweler customized those quiz options. Wedding band sessions send weddingBandPreferences (metal, width, profile, finish, stones and so on) instead of preferences, and sessionType is the band type. Any field can be null when the shopper skipped it.

#Delivery and retries

  • Each delivery is a POST with Content-Type: application/json and User-Agent: RingBuilder-Webhook/1.0.
  • Any 2xx response counts as delivered. Respond within 10 seconds; do slow work after responding.
  • Failed deliveries are retried 3 times: after 5 seconds, 30 seconds and 2 minutes.
  • Retries can cause duplicates. Deduplicate on event + sessionId (+ iterationNumber for designs).

#Verify the signature

Every delivery has an X-RingBuilder-Signature header: the hex-encoded HMAC-SHA256 of the raw request body, keyed with your signing secret. Compute it over the exact bytes you received, before any JSON parsing, and compare in constant time.

#Node.js (Express)

js
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.SHOWROOMPRO_WEBHOOK_SECRET;

app.post("/webhooks/showroompro", express.raw({ type: "application/json" }), (req, res) => {
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const received = req.get("X-RingBuilder-Signature") || "";
  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!valid) return res.status(401).send("bad signature");

  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200); // acknowledge first, then do the work
  handleEvent(event);
});

#Next.js (App Router)

ts
// app/api/showroompro-webhook/route.ts
import crypto from "node:crypto";

export async function POST(req: Request) {
  const raw = await req.text();
  const expected = crypto.createHmac("sha256", process.env.SHOWROOMPRO_WEBHOOK_SECRET!).update(raw).digest("hex");
  const received = req.headers.get("x-ringbuilder-signature") ?? "";
  if (received.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
    return new Response("bad signature", { status: 401 });
  }
  const event = JSON.parse(raw);
  // ...store the lead, notify the team, etc.
  return new Response("ok");
}

#Python (Flask)

python
import hashlib, hmac, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["SHOWROOMPRO_WEBHOOK_SECRET"].encode()

@app.post("/webhooks/showroompro")
def showroompro_webhook():
    raw = request.get_data()  # raw bytes, before JSON parsing
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    received = request.headers.get("X-RingBuilder-Signature", "")
    if not hmac.compare_digest(expected, received):
        abort(401)
    event = request.get_json()
    return "", 200

Common mistakes: verifying a re-serialized JSON.stringify(req.body) instead of the raw body, and comparing with == instead of a constant-time function.

#Session API

GET /api/webhooks/sessions/{sessionId} returns the full session, including every iteration's images. Authenticate with your signing secret as a Bearer token:

bash
curl -s https://www.showroompro.ai/api/webhooks/sessions/SESSION_ID \
  -H "Authorization: Bearer $SHOWROOMPRO_WEBHOOK_SECRET"

The response includes sessionId, sessionType, status, approvedAt, customer, preferences or weddingBandPreferences, latestImages, allIterationImages ([{ iteration, images, isApproved }]), allImageUrls, customerMessage, sessionUrl and shareUrl. Only sessions from your own account can be read.

StatusMeaning
401Missing or wrong Bearer token
403The session belongs to another account
404No session with that ID

Call the session API on https://www.showroompro.ai. If you follow a sessionUrl that redirects, make sure your HTTP client keeps the Authorization header (many clients drop it on redirects), or build the URL from sessionId yourself.

The full schema for every event and the session API is in the OpenAPI spec.