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
- In the ShowroomPro dashboard, open Settings → Integrations → Webhooks.
- Enter an Endpoint URL. It must be a publicly reachable HTTPS URL that accepts
POST. - Copy the Signing Secret. You need it to verify signatures and to call the session API.
- Choose the events to send and save.
- Click Send test to receive a
test.ping. The Recent deliveries list shows the status code of every attempt.
#Events
| Event | When | Includes |
|---|---|---|
lead.created | A shopper entered their details in the quiz | Contact details only |
design.generated | A session's first AI designs were generated | Contact details, every design choice, image URLs |
quote.requested | The shopper approved a design and asked for a quote | Same as design.generated, plus customerNotes; imageUrls holds only the approved image |
test.ping | You clicked Send test | A 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
{
"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
{
"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
POSTwithContent-Type: application/jsonandUser-Agent: RingBuilder-Webhook/1.0. - Any
2xxresponse 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(+iterationNumberfor 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)
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)
// 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)
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 "", 200Common 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:
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.
| Status | Meaning |
|---|---|
| 401 | Missing or wrong Bearer token |
| 403 | The session belongs to another account |
| 404 | No 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.