Developers
Embed the ClinReady Sign client portal in ClinReady, CareVeo, or any host application.
1 · Embed the login widget
Mount the portal in an iframe. On success it posts the session to your app.
<iframe
src="https://<portal-host>/embed/login?client=clinready&origin=https://app.clinready.com"
style="width:100%;max-width:440px;height:620px;border:0"
allow="publickey-credentials-get"
></iframe>
window.addEventListener("message", (e) => {
if (e.data?.source !== "clinready-portal") return;
if (e.data.type === "auth:success") {
// e.data.access_token, e.data.user, e.data.tenants, e.data.default_tenant
}
});2 · Redirect flow (no iframe)
Send users to /auth?redirect=/portal. Single-tenant users land directly in their workspace; multi-tenant users get a picker first.
3 · White-label embed theming
Pass logo, accent, brand, radius, and theme (light/dark) query params to any embed URL, or store defaults per organization. Logo URLs must be https; colors are validated CSS color values.
<iframe src="https://sign.clinready.com/embed/login?client=clinready&theme=light" style="width:100%;max-width:480px;height:640px;border:0" ></iframe>
4 · Embedded signing sessions
Mint a short-lived (~1 hour), single-use token server-to-server for one envelope recipient, then drop the returned URL in an iframe. Only allowed_origin ever receives postMessage events for that session.
// Server-to-server
const res = await fetch("https://<portal-host>/api/public/v1/signing-sessions", {
method: "POST",
headers: { "content-type": "application/json", "x-api-key": "<tenant-api-key>" },
body: JSON.stringify({
envelope_id: "<envelope-id>",
recipient_id: "<recipient-id>",
allowed_origin: "https://app.example.com",
redirect_url: "https://app.example.com/done",
ttl_seconds: 3600,
}),
});
const { url, token, expires_at } = await res.json();
// Client: mount url in an iframe
// <iframe src={url} style="width:100%;max-width:480px;height:720px;border:0"></iframe>
window.addEventListener("message", (e) => {
if (e.data?.source !== "clinready-portal") return;
// e.data.type: sign:loaded | sign:viewed | sign:signed | sign:declined | sign:completed | sign:error
});5 · Outbound webhooks
Register an HTTPS endpoint to receive signed envelope lifecycle events. Every request includes an X-ClinReady-Signature: t=<timestamp>,v1=<hex hmac> header computed as HMAC-SHA256(secret, `${timestamp}.${body}`). Verify it, reject stale timestamps, and compare digests in constant time:
// Node / Web Crypto verification
async function verify(secret, body, header) {
const [tPart, vPart] = header.split(",");
const timestamp = tPart.split("=")[1];
const expected = vPart.split("=")[1];
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);
const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${timestamp}.${body}`));
const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; // 5 min tolerance
return fresh && hex === expected;
}Sign in to an organization workspace to register webhook endpoints.
6 · REST endpoints
- GET /api/public/v1/me
- Bearer user access token. Returns the profile, every tenant membership with role, and the default tenant when there is exactly one.
- GET /api/public/v1/tenant
- Header
x-api-keywith a tenant API key. Returns the tenant and its members — server-to-server only, never from a browser. - POST /api/public/v1/signing-sessions
- Header
x-api-key. Body:envelope_id,recipient_id,allowed_origin, optionalredirect_url,ttl_seconds(default 3600) andtheme. Returns an/embed/signURL, the raw token, and its expiry. - GET /api/public/v1/embed-config?client=clinready
- Public. Returns the embed URL, allowed origins, and post-login path for a registered host application.
All endpoints send permissive CORS headers and answer OPTIONS preflight.
