Securely Embedding Components (Session Tokens)

SignStack provides three embeddable web components for integrating signing, workflow editing/monitoring, and primitive editing directly into your application. To securely power these embedded experiences, SignStack uses Embed Tokens (also called Session Tokens).

This guide explains how embed tokens work and how to use them securely. For the component catalog and conceptual overview, see Embedding Components.

What Are Embed Tokens?

Embed tokens are short-lived, scoped JWT tokens designed specifically for frontend embedded components. Unlike API Keys (which are long-lived secrets for server-to-server communication), embed tokens are safe to use in browser environments because they:

  • Expire quickly (typically 15–60 minutes)
  • Are scoped to a specific intent (a single workflow + step for participant signing, a single workflow for editing/monitoring, a single primitive for builder — never namespace-wide write access)
  • Have limited permissions (only what's needed for that embed intent, further narrowed to what the calling API key actually holds)
  • Can be restricted by origin (CORS protection)

Embed Intents

SignStack supports three embed intents, one per embeddable web component:

Intent Component Identifying body fields
participant <signstack-participant> workflowId + stepKey
workflow <signstack-workflow> workflowId
builder <signstack-builder> resourceKey + resourceKind (+ resourceVersion?)

intent is optional in the request body — when omitted, the server infers it from the body shape using the table above. Pass it explicitly only if you want a hard 400 when the body doesn't match the intent you expect.

The JSON blocks below show the request body you POST to /v1/orgs/{orgId}/namespaces/{namespaceKey}/auth/embed for each intent. The full request/response cycle (with the curl envelope and the response shape) is in Generating Embed Tokens below.

1. Participant (participant)

Powers <signstack-participant>. Used when a participant needs to sign documents within your application.

Request body:

{
  "intent": "participant",
  "workflowId": "b6f3a8d2-1c4e-4b9a-a7f3-2e8c1d5a9b4f",
  "stepKey": "candidate_signs",
  "expiresIn": 900,
  "allowedOrigins": ["https://yourapp.com"],
  "context": {
    "recipientEmail": "signer@example.com",
    "recipientName": "John Doe"
  }
}

stepKey is required — the token is scoped to a single participant step.

2. Workflow (workflow)

Powers <signstack-workflow> — one intent for both the editor and monitor surfaces. The server adapts the token's scopes to what the calling API key holds (capability is fixed at mint). The component fetches the workflow's live status when it mounts and combines that with the token's scopes to decide what to render:

Workflow status (at mount) API key has workflow:update? Token grants Component renders
Pending yes read + update + file + file Editor
Pending no (read-only key) read + file Monitor (view-only)
In-flight / completed / failed / voided yes read + update + file + file Monitor with inline operator actions (reminders, cancel)
In-flight / completed / failed / voided no read + file Monitor (view-only)
(any) lacks workflow:read 403 at mint time

Status is fetched on mount (one extra round-trip to GET /workflows/{id}/status), not stamped into the JWT — so a workflow transitioning between mint and mount renders the right view rather than going stale until the next remint.

Request body:

{
  "intent": "workflow",
  "workflowId": "b6f3a8d2-1c4e-4b9a-a7f3-2e8c1d5a9b4f",
  "expiresIn": 3600,
  "allowedOrigins": ["https://yourapp.com"]
}

3. Builder (builder)

Powers <signstack-builder>. Used to let an end user edit a primitive (blueprint, template, etc.) inside your app — typically when you've handed someone a starter from the Library.

Request body:

{
  "intent": "builder",
  "resourceKind": "template",
  "resourceKey": "offer_letter",
  "resourceVersion": "1.2.0",
  "expiresIn": 3600,
  "allowedOrigins": ["https://yourapp.com"]
}

resourceVersion defaults to the most recent version of the resource if omitted.

Generating Embed Tokens

Embed tokens are generated server-side using your API access token, then passed to your frontend.

Step 1: Request an Embed Token (Server-Side)

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": "candidate_signs",
    "expiresIn": 900,
    "allowedOrigins": ["https://yourapp.com"],
    "context": {
      "recipientEmail": "signer@example.com"
    }
  }'

<ACCESS_TOKEN> is the JWT you get from POST /v1/auth/token — see A Full Guide to API Keys and JWTs.

Step 2: Response

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "orgId": "f1e2d3c4-b5a6-4789-a012-345678901234",
  "tokenType": "Bearer",
  "expiresIn": 900,
  "expiresAt": "2026-04-20T16:45:00.000Z",
  "scopes": ["workflow:read", "signing:sign"],
  "subject": {
    "type": "embed",
    "id": "8a9b0c1d-2e3f-4a5b-6c7d-8e9f0a1b2c3d",
    "orgId": "f1e2d3c4-b5a6-4789-a012-345678901234",
    "namespaceKey": "acme-prod",
    "mode": "live"
  },
  "resources": [
    {
      "type": "workflow",
      "id": "b6f3a8d2-1c4e-4b9a-a7f3-2e8c1d5a9b4f",
      "steps": ["candidate_signs"],
      "actions": ["sign", "view"]
    }
  ]
}

Step 3: Pass to Frontend

Return only the embed accessToken to your frontend. Never expose your API Key or your long-lived org access token to the browser.

// Your backend endpoint
app.post('/api/get-signing-token', async (req, res) => {
  const { workflowId, stepKey, signerEmail } = req.body;

  const resp = await fetch(`https://api.signstack.ai/v1/orgs/${ORG_ID}/namespaces/${NAMESPACE_KEY}/auth/embed`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${await getOrgAccessToken()}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      intent: 'participant',
      workflowId,
      stepKey,
      expiresIn: 900,
      allowedOrigins: ['https://yourapp.com'],
      context: { recipientEmail: signerEmail },
    }),
  });
  const { accessToken } = await resp.json();
  res.json({ token: accessToken });
});

Mounting the Component

Pass the embed token into the matching SignStack web component as the embed-token attribute — the JWT's claims carry org, namespace, workflow ID, step key, and resource info, so nothing else needs to be passed:

<signstack-participant embed-token="eyJhbGciOiJIUzI1NiIs..."></signstack-participant>

Listen for component-fired events (signed, declined, sessionExpired, etc. — event names are camelCase) to react to user actions. Full mounting walkthrough — including React and the per-component prop catalog — is in Embed SignStack into Your App.

Security Best Practices

1. Always Generate Tokens Server-Side

Never expose your API Key or long-lived access tokens to the frontend. Always have your backend mint embed tokens and pass only those to the client.

  User Browser                Your Backend                  SignStack API
  ────────────                ────────────                  ─────────────
                              holds API Key (long-lived)
                              + Access Token (1h, derived
                              from API Key) — neither ever
                              leaves the backend
       │                          │                              │
       │  (1) user action → ask backend for an embed token       │
       │ ───────────────────────► │                              │
       │                          │                              │
       │                          │  (2) POST /v1/orgs/.../auth/embed
       │                          │      Authorization: Bearer <Access Token>
       │                          │ ───────────────────────────► │
       │                          │                              │
       │                          │            Embed Token       │
       │                          │ ◄─────────────────────────── │
       │                          │                              │
       │  (3) backend returns only the Embed Token               │
       │ ◄─────────────────────── │                              │
       │                                                         │
       │  (4) <signstack-* embed-token="..."> talks to SignStack directly
       │ ──────────────────────────────────────────────────────► │

Only the per-session Embed Token ever reaches the browser. The API Key and the long-lived Access Token stay on your backend at all times.

2. Use Short Expiration Times

Set expiresIn to the minimum window your UX actually needs. Suggested starting points:

  • Participant signing (participant): 15–30 minutes
  • Workflow (workflow): 30–60 minutes (the editor side benefits from the longer end; the workflow component emits no expiry event, so long-running monitor views should track the token's expiresAt and remint before it lapses)
  • Builder (builder): 30–60 minutes

3. Restrict Allowed Origins

Always specify allowedOrigins to prevent your embed tokens from being used on unauthorized domains:

{
  "allowedOrigins": ["https://yourapp.com", "https://staging.yourapp.com"]
}

4. Authorize Before Minting

The SignStack API trusts that your server has already verified the requesting user is allowed to access the resource the token is being minted for. Run that check before calling /auth/embed:

// For a signing token: verify this user is the assigned signer
const workflow = await db.workflows.findById(workflowId);
if (workflow.orgId !== user.orgId) {
  throw new ForbiddenError('Access denied');
}
if (workflow.assignedSignerEmail !== user.email) {
  throw new ForbiddenError('Not your signing step');
}

Apply the equivalent check for each intent — e.g. for builder, confirm the user owns or has been granted edit rights on that primitive; for workflow, check namespace-level or workflow-level access.

5. Handle Token Expiration Gracefully

The participant component fires a sessionExpired event when its token expires. Listen for it, fetch a fresh token from your backend, and update the component's embed-token attribute (the builder reports an expired token via its error event; the workflow component emits nothing, so remint based on the token's expiresAt):

const el = document.querySelector('signstack-participant');
el.addEventListener('sessionExpired', async () => {
  const { token } = await fetch('/api/get-signing-token').then((r) => r.json());
  el.setAttribute('embed-token', token);
});

Token Payload Structure

Embed tokens contain specific claims that identify their purpose:

Claim Description
sub Embed session identifier
org_id Organization ID
namespace_key Namespace the token is scoped to
mode live or test
embed_type The embed intent (participant, workflow, builder)
workflow_id For participant / workflow: the specific workflow this token grants access to
step_key For participant: the signing step
resource_kind / resource_key / resource_version For builder: the primitive being edited (omit resource_version to default to the most recent)
allowed_origins CORS-allowed domains
scopes Permitted actions (e.g., workflow:read, workflow:update, signing:sign)
exp Expiration timestamp

Comparison: API Tokens vs. Embed Tokens

Aspect API Access Token Embed Token
Use Case Server-to-server API calls Frontend embedded components
Lifespan 1 hour Configurable, typically 15–60 minutes
Scope Namespace-wide (constrained by Scopes on the API key) A single intent: one workflow + step (participant), one workflow (workflow), or one primitive version (builder)
Generated From API Key (sk_ns_...) via POST /v1/auth/token Access Token via POST /v1/orgs/{orgId}/namespaces/{namespaceKey}/auth/embed
Safe for Frontend? No Yes
Origin Restrictions No Yes (allowedOrigins)

Common Use Cases

Embedded Signing in Your App

Let participants sign documents inline in your application — whether the document is an offer letter, an invoice, an NDA, or anything else:

  1. User initiates signing in your app
  2. Your backend creates a workflow and mints a participant embed token
  3. Frontend mounts <signstack-participant> with the token
  4. User signs, component fires signed
  5. Your backend receives a webhook notification (authoritative confirmation)

Self-Service Workflow Review

Let users review and edit a workflow's data and documents before launch (or between steps):

  1. User opens the workflow in your app
  2. Backend mints a workflow embed token for that workflow
  3. Frontend mounts <signstack-workflow>; because the workflow is pending and the API key holds workflow:update, the component renders the editor
  4. User makes changes, saves
  5. Changes are persisted to the workflow

Workflow Status Surface

Show a customer or internal team member the live status of one of their in-flight workflows — current step, participants, history — without sending them to Studio. The view is intentionally read-only:

  1. User opens a workflow detail view in your app
  2. Backend mints a workflow embed token for that specific workflow
  3. Frontend mounts <signstack-workflow>; because the workflow is in flight, the component renders the monitor. If the API key holds workflow:update, the monitor also exposes inline operator actions (send reminder, cancel); read-only keys see the view-only surface.

In-App Primitive Editing

Let a customer or an internal team member tweak a primitive that already lives in the namespace — for example, adjusting the wording or fields on one of their own templates — without sending them to Studio:

  1. User clicks "Edit template" in your app
  2. Backend mints a builder embed token for that primitive + version
  3. Frontend mounts <signstack-builder>; user edits in your shell, then saves a new draft or publishes a new version

Troubleshooting

Token Rejected (401)

  • Check that the token hasn't expired
  • Verify allowedOrigins includes your domain
  • Ensure the workflow / resource still exists and is accessible

CORS Errors

  • Add your domain to allowedOrigins when generating the token
  • Include both production and staging domains if needed

Component Not Loading

  • Verify the token was generated for the correct intent (each web component requires a matching intent)
  • Check browser console for JavaScript errors
  • Ensure the SignStack web components script is loaded — see Embed SignStack into Your App