3D Jewelry Designer

Webhooks: events, payloads and signatures

Add an endpoint in Dashboard → API Keys → Webhooks, or over the API, and choose its events: lead.created, design.saved, cart.created, order.created, order.attributed and order.paid. Each delivery is a JSON POST signed with your endpoint's own secret. Check the x-rmj-signature header against an HMAC-SHA256 of the timestamp and the raw body before you act on it.

3D Jewelry Designer2 min read

Add an endpoint

Webhooks come with API access, from the Retailer plan up. In Dashboard → API Keys → Webhooks, paste an https:// address and tick the events you want. The signing secret (whsec_…) is shown once, right after you add the endpoint: store it where your server can read it. An account can have five endpoints, and you can pause, resume or remove each one.

The same is on the API, with the scopes webhooks:read and webhooks:write:

curl -X POST https://3djewelrydesigner.com/api/public/webhooks \
  -H "Authorization: Bearer rmj_live_…" -H "Content-Type: application/json" \
  -d '{ "url": "https://crm.example.com/3jd", "events": ["lead.created", "design.saved"] }'
# { "data": { "id": "…", "url": "…", "events": [ … ], "active": true, … }, "secret": "whsec_…" }

GET /api/public/webhooks lists them, PATCH /api/public/webhooks/{id} takes { "active": false } or a new url or events, and DELETE removes one. In GraphQL they are webhooks, createWebhook, updateWebhook and deleteWebhook.

The address must be public: local, private and cloud-metadata addresses are refused, both when you add it and again before every delivery. Redirects are not followed, so give the final address.

The events

EventSent when
lead.createdA shopper saves a design or sends an enquiry in a ring builder.
design.savedA shopper saves a ring design. It is a lead too, so lead.created is sent as well, with the same quoteId.
cart.createdA shopper adds a configured ring to the cart. Sent once per add, with the price the cart charges.
order.createdA Shopify order with a ring builder ring or a 3D product page in it.
order.attributedA Shopify order traced to the ring builder that made the sale, with the revenue its lines earned.
order.paidA shopper pays in the ring builder's own checkout.

lead.created, design.saved and cart.created carry the lead:

{
  "event": "design.saved",
  "createdAt": "2026-10-10T09:30:00.000Z",
  "data": {
    "quoteId": "5f0c…",
    "kind": "saved",
    "sku": "RB-RND-100-14Y",
    "priceCents": 248000,
    "currency": "USD",
    "selection": { "bandSlug": "classic-round", "headSelection": { "shape": "Round", "carat": 1 }, "metalPresetId": "yellow_gold_14k" },
    "customer": { "name": "Jane Doe", "email": "jane@example.com" },
    "embedId": "9b2e…",
    "embedSlug": "QthU0Dpfak8"
  }
}

A cart.created from the cart also has mode (how the cart line was written: match, mint, create, draft, hosted or event) and channel (embed, or api for a cart your own backend made). Prices are always the ones our server computed, never a number from the shopper's browser.

order.attributed names the sale's source:

{
  "event": "order.attributed",
  "createdAt": "2026-10-10T11:02:00.000Z",
  "data": {
    "orderNumber": "1042",
    "embedId": "9b2e…",
    "embedSlug": "QthU0Dpfak8",
    "sessionId": "s_8f…",
    "attributedCents": 248000,
    "orderTotalCents": 251500,
    "currency": "USD",
    "sku": "RB-RND-100-14Y",
    "channel": "shopify"
  }
}

order.created has sku, priceCents, currency, orderNumber and channel; order.paid has sku, priceCents, currency, embedId and channel. New keys can appear in any event at any time, so ignore the ones you don't know rather than reject the delivery. The public API's OpenAPI document lists every field under webhooks.

Check the signature

Every delivery has three headers:

  • x-rmj-event: the event name.
  • x-rmj-timestamp: when it was signed, in milliseconds since 1970.
  • x-rmj-signature: sha256= and the hex HMAC-SHA256 of <timestamp>.<raw body>, keyed with your endpoint's secret.

Sign the raw body exactly as it arrived, before any JSON parsing, and refuse a timestamp more than five minutes old so an old delivery cannot be replayed. A receiver in Node and Express:

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.RMJ_WEBHOOK_SECRET; // whsec_…
const app = express();

app.post("/3jd", express.raw({ type: "application/json" }), (req, res) => {
  const ts = req.get("x-rmj-timestamp") ?? "";
  const sig = req.get("x-rmj-signature") ?? "";
  const body = req.body.toString("utf8");

  const expected = "sha256=" + createHmac("sha256", SECRET).update(`${ts}.${body}`).digest("hex");
  const fresh = Math.abs(Date.now() - Number(ts)) < 5 * 60 * 1000;
  const same = sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!fresh || !same) return res.status(400).end();

  const { event, data } = JSON.parse(body);
  if (event === "design.saved") saveToCrm(data.quoteId, data.customer, data.selection);
  res.status(204).end(); // answer fast; do slow work after replying
});

The @3jd/sdk package has the same check as verifyWebhook(rawBody, { timestamp, signature }, secret).

Retries and repeats

We wait up to 5 seconds for an answer. A network error or a 5xx is retried once; any other answer outside 2xx is not. The endpoint's row in the dashboard shows the time and status of its last delivery (0 means we could not connect). A delivery can arrive twice, so treat a repeat quoteId or orderNumber as the same event. If your plan stops including API access, deliveries stop with it, the same way its API keys do.

Related

  • How an integration fits together

    From a shopper building a ring to the order in your systems: what runs where, which price to trust, and how products and 3D models stay in step.

  • The ring builder's JavaScript SDK

    Mount a ring builder on any page with one script, read every selection and add to cart, and size, theme and tear it down from your own code.

  • API versions and changes

    How the REST API, GraphQL, webhooks and the ring builder script change over time, and what you can rely on not to change under you.

Put your own design in the studio

Upload a file and see it rendered photoreal, ready to try on and to embed.