# 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

| 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](#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.

| 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](https://www.showroompro.ai/openapi.json).

---

Source: https://www.showroompro.ai/docs/webhooks · Last updated 2026-10-16 · All docs: https://www.showroompro.ai/llms.txt
