Securely Embedding Components (Session Tokens)
SignStack provides four embeddable web components for integrating signing, workflow editing, workflow 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 signing, a single workflow for editing or monitoring, a single primitive version for resource editing — never namespace-wide write access)
- Have limited permissions (only what's needed for that embed intent)
- Can be restricted by origin (CORS protection)
Embed Intents
SignStack supports four embed intents, one per embeddable web component. The intent you choose determines which scopes the token carries and which component can consume it.
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. Signing Session (signing_session)
Powers <ss-signing-embed>. Used when a participant needs to sign documents within your application.
Request body:
{
"intent": "signing_session",
"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 Editing (workflow_editing)
Powers <ss-workflow-editor>. Used to view and edit a workflow's documents and input data — either before launch (filling in inputs, attaching documents) or while it's in flight (making allowed in-flight modifications between signing steps).
Request body:
{
"intent": "workflow_editing",
"workflowId": "b6f3a8d2-1c4e-4b9a-a7f3-2e8c1d5a9b4f",
"expiresIn": 3600,
"allowedOrigins": ["https://yourapp.com"]
}
3. Workflow Monitoring (workflow_monitoring)
Powers <ss-workflow-monitor>. Renders a live status surface for a single in-flight workflow — current step, participant statuses, history — and lets the viewer take limited operator actions: nudge participants with reminder emails and cancel the workflow.
Request body:
{
"intent": "workflow_monitoring",
"workflowId": "b6f3a8d2-1c4e-4b9a-a7f3-2e8c1d5a9b4f",
"expiresIn": 3600,
"allowedOrigins": ["https://yourapp.com"]
}
4. Resource Editing (resource_editing)
Powers <ss-resource-editor>. 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": "resource_editing",
"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": "signing_session",
"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: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: 'signing_session',
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:
<ss-signing-embed
workflow-id="b6f3a8d2-1c4e-4b9a-a7f3-2e8c1d5a9b4f"
embed-token="eyJhbGciOiJIUzI1NiIs..."
org-id="f1e2d3c4-b5a6-4789-a012-345678901234"
namespace-key="acme-prod"
></ss-signing-embed>
Listen for component-fired events (signed, declined, session-expired, etc.) 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) <ss-* 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:
- Signing sessions (
signing_session): 15–30 minutes - Workflow editing (
workflow_editing): 30–60 minutes - Workflow monitoring (
workflow_monitoring): 15–60 minutes (handlesession-expiredfor long-running views) - Resource editing (
resource_editing): 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 resource_editing, confirm the user owns or has been granted edit rights on that primitive; for workflow_monitoring, check namespace-level access.
5. Handle Token Expiration Gracefully
Each embed component fires a session-expired event when its token expires. Listen for it, fetch a fresh token from your backend, and update the component's embed-token attribute:
const el = document.querySelector('ss-signing-embed');
el.addEventListener('session-expired', 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 (signing_session, workflow_editing, workflow_monitoring, resource_editing) |
workflow_id |
For workflow intents: the specific workflow this token grants access to |
step_key |
For signing: the participant step |
resource_kind / resource_key / resource_version |
For resource_editing: the primitive being edited (omit resource_version to default to the most recent) |
allowed_origins |
CORS-allowed domains |
scopes |
Permitted actions (e.g., workflow:sign, workflow:edit) |
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 (signing), one workflow (editing or monitoring), or one primitive version (resource editing) |
| 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
signing_sessionembed token - Frontend mounts
<ss-signing-embed>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
workflow_editingembed token for that workflow - Frontend mounts
<ss-workflow-editor> - User makes changes, saves
- Changes are persisted to the workflow
Workflow Status Surface
Show a customer the live status of one of their in-flight workflows — current step, participants, history — without sending them to Studio. They can also nudge a stalled signer or cancel the workflow from the same surface:
- User opens a workflow detail view in your app
- Backend mints a
workflow_monitoringembed token for that specific workflow - Frontend mounts
<ss-workflow-monitor>; component shows status, signers, and timestamps in real time, and exposes "Send reminder" per participant and a "Cancel workflow" control
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
resource_editingembed token for that primitive + version - Frontend mounts
<ss-resource-editor>; 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
