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
|
Editor |
| Pending | no (read-only key) | read + file
|
Monitor (view-only) |
| In-flight / completed / failed / voided | yes | read + update + 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'sexpiresAtand 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:
- User initiates signing in your app
- Your backend creates a workflow and mints a
participantembed token - Frontend mounts
<signstack-participant>with the token - User signs, component fires
signed - 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):
- User opens the workflow in your app
- Backend mints a
workflowembed token for that workflow - Frontend mounts
<signstack-workflow>; because the workflow is pending and the API key holdsworkflow:update, the component renders the editor - User makes changes, saves
- 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:
- User opens a workflow detail view in your app
- Backend mints a
workflowembed token for that specific workflow - Frontend mounts
<signstack-workflow>; because the workflow is in flight, the component renders the monitor. If the API key holdsworkflow: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:
- User clicks "Edit template" in your app
- Backend mints a
builderembed token for that primitive + version - 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
allowedOriginsincludes your domain - Ensure the workflow / resource still exists and is accessible
CORS Errors
- Add your domain to
allowedOriginswhen 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
