Authentication & signing
Every iframe URL carries a signature proving it came from you and telling Designa which of your users is loading the workspace. Pick whichever path matches your integration — both produce an equally valid signed URL, and everything downstream (the iframe, events, credits) is identical either way.
With a backend
Recommended — stronger trust. Compute the signature yourself, server-side. Never ship embed_secret to the browser.
const crypto = require("crypto");
function buildDesignaEmbedUrl({ userId, businessId, role, email }) {
const exp = String(Math.floor(Date.now() / 1000) + 300); // 5 min validity
// Field order is the contract — do not reorder:
// partnerId|partnerUserId|partnerBusinessId|role|email|exp
const message = [PARTNER_ID, userId, businessId, role, email, exp].join("|");
const sig = crypto.createHmac("sha256", EMBED_SECRET).update(message).digest("hex");
const params = new URLSearchParams({
partnerId: PARTNER_ID,
partnerUserId: userId, // your user's id in YOUR system
partnerBusinessId: businessId, // your business/team id — the credit pool
partnerRole: role, // "owner" | "admin" | "member"
email: email, // required — Designa provisions a real account for it
exp,
sig,
});
return `https://app.designa.ai/embed/remix?${params}`;
}Signature verification happens server-side at Designa, checked against your registered secret and origins. A copied, expired, or tampered URL never gets a session.
No business/team concept on your side?
partnerBusinessIdis how Designa groups users into one shared credit pool. If you have nothing to put there, pass the user's own id as the business id too — each user then gets their own solo pool instead of sharing one.Without a backend
For a plain frontend/SPA with no server of its own. Ask Designa to sign it for you — never send or store embed_secret in this path at all.
async function buildDesignaEmbedUrl({ userId, businessId, role, email }) {
const res = await fetch("https://api.designa.ai/ai/partners/sign-embed-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
partner_id: PARTNER_ID, // your public partner id — not sensitive
partner_user_id: userId,
partner_business_id: businessId, // optional — omit/undefined defaults to userId
role, // "owner" | "admin" | "member"
email,
}),
});
if (!res.ok) throw new Error("Designa rejected this embed request.");
const signed = await res.json(); // { partnerId, partnerUserId, partnerBusinessId, partnerRole, email, exp, sig }
const params = new URLSearchParams(signed);
return `https://app.designa.ai/embed/remix?${params}`;
}Trust here is anchored on the request's Origin header matching one of your registered allowed origins — not on you holding a secret at all. This is weaker than the backend path (an Origin header can be forged by a non-browser HTTP client, though not by a genuine in-browser fetch), but means your app never needs a backend or any secret storage.
403 — fall back to legacy unsigned mode rather than failing outright.The role field
Sent as partnerRole (or role in the sign-request body).
| Role | What they see |
|---|---|
owner | First user of a new partnerBusinessId becomes owner regardless of the role you send. Full access, including team usage and per-user spend caps (set from Designa's normal team page). |
admin | Buy Credits (Stripe top-up) and team usage view. |
member | Uses credits from the shared pool; can't buy or view team usage. |
Expiry & replay
expis Unix seconds. Expired URLs are rejected — render the URL at page load, not at build time.- Recommended validity window is 5 minutes; generate a fresh URL for every embed page load rather than caching one.
- A signature is tied to its exact field values — changing any field without re-signing invalidates it.