Embed SignStack into Your App

This guide is the practical companion to Embedding Components. You'll wire up a working signing flow inside your own app — server-side embed-token issuance, browser-side mounting of the web component, and event handling.

The same pattern applies to all three components (<signstack-participant>, <signstack-builder>, <signstack-workflow>); the differences are which intent you mint a token for and which props each component takes.

Sample Apps

Prefer to start from working code? Each sample is a minimal end-to-end integration — a small Node backend that mints embed tokens plus a frontend that mounts all three components:

The Pattern

  1. Your server holds the long-lived API key. When a logged-in user wants to access an embedded surface (e.g. sign a document), your server calls SignStack's POST /v1/orgs/{orgId}/namespaces/{namespaceKey}/auth/embed to mint a short-lived embed token scoped to that user + intent.
  2. Your server returns the token to the browser.
  3. The browser mounts the SignStack web component and passes the token in.
  4. The component uses the token for its own API calls; you listen for events (signed, declined, sessionExpired).

Step 1: Load the Web Components Bundle

Drop the SignStack embed bundle (script + stylesheet) into your app's HTML. Once loaded, the three custom elements register themselves and you can use them anywhere in your DOM.

Track the latest v1 (auto-updates within major version 1, so you get fixes without changing your HTML):

<link rel="stylesheet" href="https://cdn.signstack.ai/embed/v1/signstack.css" />
<script type="module" src="https://cdn.signstack.ai/embed/v1/signstack.js"></script>

Each major version (v1, v2, …) is a stable track — new majors ship breaking changes behind a new path, so your HTML keeps working until you choose to move.

Step 2: Mint an Embed Token (Server-Side)

For an embedded signing flow:

curl -X POST https://api.signstack.ai/v1/orgs/{orgId}/namespaces/{namespaceKey}/auth/embed \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": "participant",
    "workflowId": "b6f3a8d2-1c4e-4b9a-a7f3-2e8c1d5a9b4f",
    "stepKey": "consultant_signs",
    "expiresIn": 900,
    "allowedOrigins": ["https://yourapp.com"],
    "context": {
      "recipientEmail": "jane@example.com",
      "recipientName": "Jane Doe"
    }
  }'

Response:

{
  "accessToken": "eyJhbGciOi…",
  "orgId": "f1e2d3c4-b5a6-4789-a012-345678901234",
  "tokenType": "Bearer",
  "expiresIn": 900,
  "expiresAt": "2026-04-19T16:30:00.000Z",
  "scopes": ["..."],
  "subject": { "type": "embed", "id": "8a9b0c1d-2e3f-4a5b-6c7d-8e9f0a1b2c3d", "orgId": "f1e2d3c4-b5a6-4789-a012-345678901234" },
  "resources": [{ "type": "workflow", "id": "b6f3a8d2-1c4e-4b9a-a7f3-2e8c1d5a9b4f", "actions": ["sign"] }]
}

accessToken is what the browser receives. Pass allowedOrigins to scope the token to your domain (browser CORS protection); set expiresIn to the shortest reasonable window for your UX.

The full intent catalog and security rationale lives in Securely Embedding Components.

Step 3: Mount the Component (Browser-Side)

<signstack-participant embed-token="eyJhbGciOi…"></signstack-participant>

Everything the component needs (org_id, namespace_key, workflow_id, step_key) is carried as claims inside the token — no separate attributes are required.

In React (custom elements work natively):

function SigningPage({ embedToken }) {
  const ref = useRef(null);

  useEffect(() => {
    const el = ref.current;
    const onSigned   = (e) => console.log('signed', e.detail);
    const onDeclined = (e) => console.log('declined', e.detail);
    const onExpired  = () => refetchEmbedToken();
    el.addEventListener('signed', onSigned);
    el.addEventListener('declined', onDeclined);
    el.addEventListener('sessionExpired', onExpired);
    return () => {
      el.removeEventListener('signed', onSigned);
      el.removeEventListener('declined', onDeclined);
      el.removeEventListener('sessionExpired', onExpired);
    };
  }, []);

  return <signstack-participant ref={ref} embed-token={embedToken} />;
}

The component picks up your app's CSS for typography, colors, and surrounding chrome — no theme-override config to manage.

Step 4: Handle Token Expiry

Embed tokens are deliberately short-lived. When one expires, the participant component fires a sessionExpired event. Your handler should call your server to mint a fresh token and update the component's embed-token attribute. The component will reconnect with the new token. (The builder surfaces an expired/invalid token through its error event; the workflow component emits no lifecycle events, so for long-lived monitor views track the token's expiresAt and remint before it lapses.)

Component Reference

All three components take embed-token as their only required attribute — every other detail (org, namespace, workflow ID, step key, resource info) is carried as JWT claims inside the token. The component decodes them on mount.

Component Intent (token mint) Required body fields Key events (camelCase)
<signstack-participant> participant workflowId, stepKey signed, declined, signingError, sessionExpired, closed
<signstack-workflow> workflow workflowId (none — persistence is server-side; track expiresAt to remint)
<signstack-builder> builder resourceKey, resourceKind (+ optional resourceVersion) saved, published, ran, deleted, error

intent itself can also be omitted in the mint request — the server infers it from the body shape (workflowId + stepKey → participant; workflowId alone → workflow; resourceKey + resourceKind → builder).

For <signstack-workflow> specifically, the token's scopes are adapted to whatever the calling API key holds, and the component fetches the workflow's live status when it mounts:

  • Pending workflow + key has workflow:update → token grants update; component renders the editor
  • Pending workflow + read-only key → token is read-only; component renders the monitor
  • In-flight workflow + key has workflow:update → token grants update; component renders the monitor with operator actions (reminders, cancel) inline
  • In-flight workflow + read-only key → token is read-only; component renders the monitor (view-only)
  • Key without workflow:read → 403 at mint time

Status is fetched at mount, not stamped into the token, so transitions between mint and mount don't strand the user in a stale view. No mode attribute on the element, no client-side branching.

For <signstack-builder>, the token's scopes are likewise adapted to whatever the calling API key holds, so the editor's toolbar actions light up only when the key can back them (Edit/Delete, scenario authoring, Publish-to-Library, and Run). The builder runs inside your page with no router of its own, so navigation-style actions don't redirect — they emit events for your app to act on instead:

  • Run creates the workflow and emits ran ({ workflowId }). Listen for it and mount a <signstack-workflow> with a freshly-minted workflow token to show the new run.
  • Delete removes the resource and emits deleted ({ key, version }). The edited resource no longer exists, so unmount the builder (and refresh your own list) in response.

(The participant component runs the actual signature capture in a sandboxed context for security; the others render inline within your DOM.)

Common Pitfalls

  • Forgetting allowedOrigins — Tokens are usable from any browser without it. Always pass your app's exact origin(s).
  • Reusing tokens across users — Each token is scoped to one user/intent. Mint per-session; don't cache server-side.
  • Long expiresIn — Default to 15 minutes. Long-lived embed tokens defeat the security model.
  • Treating events as authoritative — Always confirm signing completion via webhooks or workflow status; the browser event is a UX signal, not a source of truth.