> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spitshake.io/llms.txt
> Use this file to discover all available pages before exploring further.

# JWT Tokens

> Generate and validate JWT tokens for secure embedding of signing forms and template builders in your application.

# JWT Tokens

DocuTrust uses JSON Web Tokens (JWT) to authenticate embedded signing forms and template builders. Tokens are generated server-side, scoped to a specific resource (submitter or template), and passed to the frontend embed components.

## Overview

| Property               | Value                                                           |
| ---------------------- | --------------------------------------------------------------- |
| **Algorithm**          | HS256 (HMAC-SHA256)                                             |
| **Default expiration** | 1 hour (general), 4 hours (form), 4 hours (builder)             |
| **Signing key**        | Account JWT secret (falls back to `SECRET_KEY_BASE` if not set) |
| **Token endpoint**     | `POST /api/embed/token`                                         |

## Token types

DocuTrust issues two types of embed tokens, each scoped by the `aud` (audience) claim.

### Form tokens

Form tokens authenticate a signing session for a specific submitter. The `sub` claim contains the submitter ID, and the `sid` claim contains the submission ID.

**Full JWT claims:**

```json theme={null}
{
  "sub": "142",
  "iss": "docutrust",
  "aud": "form",
  "sid": "89",
  "aid": "1",
  "iat": 1744195920,
  "exp": 1744210320,
  "opts": {
    "allow_to_resubmit": false,
    "with_title": true,
    "with_send_copy_button": true,
    "with_download_button": true
  }
}
```

**Claim reference:**

| Claim                        | Type    | Description                                                                      |
| ---------------------------- | ------- | -------------------------------------------------------------------------------- |
| `sub`                        | string  | Submitter ID. Identifies which signer this token is for.                         |
| `iss`                        | string  | Issuer. Always `"docutrust"`.                                                    |
| `aud`                        | string  | Audience. `"form"` for signing form tokens.                                      |
| `sid`                        | string  | Submission ID. The parent submission this submitter belongs to.                  |
| `aid`                        | string  | Account ID. The DocuTrust account that owns this submission.                     |
| `iat`                        | integer | Issued at. Unix timestamp when the token was generated.                          |
| `exp`                        | integer | Expiration. Unix timestamp when the token expires. Default: 4 hours after `iat`. |
| `opts.allow_to_resubmit`     | boolean | Whether the signer can resubmit after completing.                                |
| `opts.with_title`            | boolean | Whether to show the template title in the form.                                  |
| `opts.with_send_copy_button` | boolean | Whether to show the "Send a copy" button.                                        |
| `opts.with_download_button`  | boolean | Whether to show the download button on the completion screen.                    |

### Builder tokens

Builder tokens authenticate access to the template editor for a specific template. The `sub` claim contains the template ID.

**Full JWT claims:**

```json theme={null}
{
  "sub": "15",
  "iss": "docutrust",
  "aud": "builder",
  "aid": "1",
  "iat": 1744195920,
  "exp": 1744210320,
  "opts": {
    "autosave": true,
    "preview": true,
    "fields": []
  }
}
```

**Claim reference:**

| Claim           | Type    | Description                                                                      |
| --------------- | ------- | -------------------------------------------------------------------------------- |
| `sub`           | string  | Template ID. Identifies which template the builder will load.                    |
| `iss`           | string  | Issuer. Always `"docutrust"`.                                                    |
| `aud`           | string  | Audience. `"builder"` for template builder tokens.                               |
| `aid`           | string  | Account ID. The DocuTrust account that owns this template.                       |
| `iat`           | integer | Issued at. Unix timestamp when the token was generated.                          |
| `exp`           | integer | Expiration. Unix timestamp when the token expires. Default: 4 hours after `iat`. |
| `opts.autosave` | boolean | Whether the builder auto-saves changes.                                          |
| `opts.preview`  | boolean | Whether to show the document preview panel.                                      |
| `opts.fields`   | array   | Pre-defined field definitions. Empty array means all field types are available.  |

When `opts.fields` is populated, it contains field definition objects:

```json theme={null}
{
  "sub": "15",
  "iss": "docutrust",
  "aud": "builder",
  "aid": "1",
  "iat": 1744195920,
  "exp": 1744210320,
  "opts": {
    "autosave": true,
    "preview": true,
    "fields": [
      {"name": "Full Name", "type": "text", "role": "Recipient", "required": true},
      {"name": "Signature", "type": "signature", "role": "Recipient", "required": true},
      {"name": "Date Signed", "type": "date", "role": "Recipient", "required": true},
      {"name": "Agreed to Terms", "type": "checkbox", "role": "Recipient", "required": true}
    ]
  }
}
```

## Expiration defaults

| Constant             | Duration | Description                     |
| -------------------- | -------- | ------------------------------- |
| `DEFAULT_EXPIRATION` | 1 hour   | General-purpose token lifetime  |
| `FORM_EXPIRATION`    | 4 hours  | Signing form token lifetime     |
| `BUILDER_EXPIRATION` | 4 hours  | Template builder token lifetime |

Tokens are validated on every request. An expired token returns a `401 Unauthorized` response with:

```json theme={null}
{
  "error": "token_expired",
  "message": "The embed token has expired. Generate a new token via POST /api/embed/token.",
  "expired_at": "2026-04-08T18:12:00.000Z"
}
```

## Generating tokens via the API

Use `POST /api/embed/token` to generate embed tokens. Authenticate with your API token in the `X-Auth-Token` header.

### Generate a form token

```bash theme={null}
curl -X POST https://your-app.com/api/embed/token \
  -H "X-Auth-Token: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "audience": "form",
    "submitter_id": 142,
    "submission_id": 89,
    "options": {
      "allow_to_resubmit": false,
      "with_title": true,
      "with_send_copy_button": true,
      "with_download_button": true
    }
  }'
```

**Response:**

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxNDIiLCJpc3MiOiJkb2N1dHJ1c3QiLCJhdWQiOiJmb3JtIiwic2lkIjoiODkiLCJhaWQiOiIxIiwiaWF0IjoxNzQ0MTk1OTIwLCJleHAiOjE3NDQyMTAzMjAsIm9wdHMiOnsiYWxsb3dfdG9fcmVzdWJtaXQiOmZhbHNlLCJ3aXRoX3RpdGxlIjp0cnVlLCJ3aXRoX3NlbmRfY29weV9idXR0b24iOnRydWUsIndpdGhfZG93bmxvYWRfYnV0dG9uIjp0cnVlfX0.Xq2nJ8vGhW4kP9mR",
  "expires_at": "2026-04-08T18:12:00.000Z",
  "audience": "form",
  "submitter_id": 142,
  "submission_id": 89
}
```

### Generate a builder token

```bash theme={null}
curl -X POST https://your-app.com/api/embed/token \
  -H "X-Auth-Token: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "audience": "builder",
    "template_id": 15,
    "options": {
      "autosave": true,
      "preview": true,
      "fields": [
        {"name": "Full Name", "type": "text", "role": "Recipient", "required": true},
        {"name": "Signature", "type": "signature", "role": "Recipient", "required": true},
        {"name": "Date Signed", "type": "date", "role": "Recipient", "required": true},
        {"name": "Agreed to Terms", "type": "checkbox", "role": "Recipient", "required": true}
      ]
    }
  }'
```

**Response:**

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxNSIsImlzcyI6ImRvY3V0cnVzdCIsImF1ZCI6ImJ1aWxkZXIiLCJhaWQiOiIxIiwiaWF0IjoxNzQ0MTk1OTIwLCJleHAiOjE3NDQyMTAzMjAsIm9wdHMiOnsiYXV0b3NhdmUiOnRydWUsInByZXZpZXciOnRydWUsImZpZWxkcyI6W3sibmFtZSI6IkZ1bGwgTmFtZSIsInR5cGUiOiJ0ZXh0Iiwicm9sZSI6IlJlY2lwaWVudCIsInJlcXVpcmVkIjp0cnVlfSx7Im5hbWUiOiJTaWduYXR1cmUiLCJ0eXBlIjoic2lnbmF0dXJlIiwicm9sZSI6IlJlY2lwaWVudCIsInJlcXVpcmVkIjp0cnVlfSx7Im5hbWUiOiJEYXRlIFNpZ25lZCIsInR5cGUiOiJkYXRlIiwicm9sZSI6IlJlY2lwaWVudCIsInJlcXVpcmVkIjp0cnVlfSx7Im5hbWUiOiJBZ3JlZWQgdG8gVGVybXMiLCJ0eXBlIjoiY2hlY2tib3giLCJyb2xlIjoiUmVjaXBpZW50IiwicmVxdWlyZWQiOnRydWV9XX19.abc123",
  "expires_at": "2026-04-08T18:12:00.000Z",
  "audience": "builder",
  "template_id": 15
}
```

## Server-side token generation

For tighter control or when you want to generate tokens without calling the API, you can sign JWTs directly using your account's JWT secret.

### Ruby

```ruby theme={null}
require 'jwt'

# Your account's JWT secret (or falls back to SECRET_KEY_BASE)
jwt_secret = ENV.fetch('DOCUTRUST_JWT_SECRET') { ENV.fetch('SECRET_KEY_BASE') }

# Generate a form token
form_payload = {
  sub: '142',
  iss: 'docutrust',
  aud: 'form',
  sid: '89',
  aid: '1',
  iat: Time.now.to_i,
  exp: Time.now.to_i + (4 * 3600),  # 4 hours
  opts: {
    allow_to_resubmit: false,
    with_title: true,
    with_send_copy_button: true,
    with_download_button: true
  }
}

form_token = JWT.encode(form_payload, jwt_secret, 'HS256')
puts "Form token: #{form_token}"

# Generate a builder token
builder_payload = {
  sub: '15',
  iss: 'docutrust',
  aud: 'builder',
  aid: '1',
  iat: Time.now.to_i,
  exp: Time.now.to_i + (4 * 3600),  # 4 hours
  opts: {
    autosave: true,
    preview: true,
    fields: [
      { name: 'Full Name', type: 'text', role: 'Recipient', required: true },
      { name: 'Signature', type: 'signature', role: 'Recipient', required: true },
      { name: 'Date Signed', type: 'date', role: 'Recipient', required: true },
      { name: 'Agreed to Terms', type: 'checkbox', role: 'Recipient', required: true }
    ]
  }
}

builder_token = JWT.encode(builder_payload, jwt_secret, 'HS256')
puts "Builder token: #{builder_token}"
```

### Node.js

```javascript theme={null}
const jwt = require('jsonwebtoken');

// Your account's JWT secret (or falls back to SECRET_KEY_BASE)
const jwtSecret = process.env.DOCUTRUST_JWT_SECRET || process.env.SECRET_KEY_BASE;

// Generate a form token
const formPayload = {
  sub: '142',
  iss: 'docutrust',
  aud: 'form',
  sid: '89',
  aid: '1',
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + (4 * 3600),  // 4 hours
  opts: {
    allow_to_resubmit: false,
    with_title: true,
    with_send_copy_button: true,
    with_download_button: true
  }
};

const formToken = jwt.sign(formPayload, jwtSecret, { algorithm: 'HS256' });
console.log('Form token:', formToken);

// Generate a builder token
const builderPayload = {
  sub: '15',
  iss: 'docutrust',
  aud: 'builder',
  aid: '1',
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + (4 * 3600),  // 4 hours
  opts: {
    autosave: true,
    preview: true,
    fields: [
      { name: 'Full Name', type: 'text', role: 'Recipient', required: true },
      { name: 'Signature', type: 'signature', role: 'Recipient', required: true },
      { name: 'Date Signed', type: 'date', role: 'Recipient', required: true },
      { name: 'Agreed to Terms', type: 'checkbox', role: 'Recipient', required: true }
    ]
  }
};

const builderToken = jwt.sign(builderPayload, jwtSecret, { algorithm: 'HS256' });
console.log('Builder token:', builderToken);
```

### Python

```python theme={null}
import jwt
import time
import os

# Your account's JWT secret (or falls back to SECRET_KEY_BASE)
jwt_secret = os.environ.get('DOCUTRUST_JWT_SECRET') or os.environ['SECRET_KEY_BASE']

now = int(time.time())

# Generate a form token
form_payload = {
    'sub': '142',
    'iss': 'docutrust',
    'aud': 'form',
    'sid': '89',
    'aid': '1',
    'iat': now,
    'exp': now + (4 * 3600),  # 4 hours
    'opts': {
        'allow_to_resubmit': False,
        'with_title': True,
        'with_send_copy_button': True,
        'with_download_button': True
    }
}

form_token = jwt.encode(form_payload, jwt_secret, algorithm='HS256')
print(f'Form token: {form_token}')

# Generate a builder token
builder_payload = {
    'sub': '15',
    'iss': 'docutrust',
    'aud': 'builder',
    'aid': '1',
    'iat': now,
    'exp': now + (4 * 3600),  # 4 hours
    'opts': {
        'autosave': True,
        'preview': True,
        'fields': [
            {'name': 'Full Name', 'type': 'text', 'role': 'Recipient', 'required': True},
            {'name': 'Signature', 'type': 'signature', 'role': 'Recipient', 'required': True},
            {'name': 'Date Signed', 'type': 'date', 'role': 'Recipient', 'required': True},
            {'name': 'Agreed to Terms', 'type': 'checkbox', 'role': 'Recipient', 'required': True}
        ]
    }
}

builder_token = jwt.encode(builder_payload, jwt_secret, algorithm='HS256')
print(f'Builder token: {builder_token}')
```

## Security best practices

<AccordionGroup>
  <Accordion title="Generate tokens server-side only" icon="server">
    Never expose your JWT secret to the browser. Tokens must always be generated on your backend and passed to the frontend. The JWT secret should only exist in environment variables on your server.

    ```javascript theme={null}
    // WRONG: Never do this in browser code
    const token = jwt.sign(payload, 'my-secret');

    // RIGHT: Fetch a token from your backend
    const response = await fetch('/api/internal/embed-token', { method: 'POST' });
    const { token } = await response.json();
    ```
  </Accordion>

  <Accordion title="Use short expiration times" icon="clock">
    The default 4-hour expiration is suitable for most use cases. For high-security environments, generate tokens with shorter lifetimes (15-60 minutes) and refresh them as needed.

    ```ruby theme={null}
    # High-security: 15-minute token
    payload = {
      sub: submitter_id.to_s,
      iss: 'docutrust',
      aud: 'form',
      sid: submission_id.to_s,
      aid: account_id.to_s,
      iat: Time.now.to_i,
      exp: Time.now.to_i + (15 * 60),  # 15 minutes
      opts: {
        allow_to_resubmit: false,
        with_title: true,
        with_send_copy_button: true,
        with_download_button: true
      }
    }
    ```
  </Accordion>

  <Accordion title="Scope tokens to specific resources" icon="lock">
    Each token should be scoped to exactly one submitter (form) or one template (builder). Never create tokens that grant access to multiple resources. The `sub` claim enforces this scoping.
  </Accordion>

  <Accordion title="Rotate your JWT secret" icon="rotate">
    If your JWT secret is compromised, rotate it immediately in your DocuTrust account settings. All existing tokens signed with the old secret will be invalidated. DocuTrust supports a previous key for graceful rotation -- tokens signed with the previous key remain valid during the rotation window.
  </Accordion>

  <Accordion title="Validate the audience claim" icon="shield-check">
    When verifying tokens on your backend (e.g., in webhook handlers), always validate the `aud` claim matches the expected audience. A `form` token should never be accepted where a `builder` token is expected, and vice versa.

    ```ruby theme={null}
    decoded = JWT.decode(token, jwt_secret, true, {
      algorithm: 'HS256',
      iss: 'docutrust',
      verify_iss: true,
      aud: 'form',
      verify_aud: true
    })
    ```
  </Accordion>

  <Accordion title="Do not embed tokens in URLs" icon="triangle-exclamation">
    Avoid passing JWT tokens as URL query parameters. URLs are logged in server access logs, browser history, and proxy logs. Instead, pass tokens via `data-token` attributes or JavaScript configuration objects.

    ```html theme={null}
    <!-- WRONG: Token in URL (logged everywhere) -->
    <iframe src="https://your-app.com/embed?token=eyJ..."></iframe>

    <!-- RIGHT: Token in data attribute -->
    <docuseal-builder data-token="eyJ..."></docuseal-builder>
    ```
  </Accordion>
</AccordionGroup>

## Token flow diagram

The typical token flow for embedding:

<Steps>
  <Step title="Backend generates token">
    Your server calls `POST /api/embed/token` (or signs a JWT directly) with the submitter/template ID and desired options.
  </Step>

  <Step title="Token sent to frontend">
    Your backend returns the token to the browser, either as part of an HTML page render or via an AJAX response.
  </Step>

  <Step title="Frontend passes token to embed">
    The token is set as a `data-token` attribute on the web component, or passed to `DocuTrust.mount()` / `DocuTrust.mountBuilder()`.
  </Step>

  <Step title="Embed validates token">
    The DocuTrust iframe receives the token, validates the signature and expiration, and loads the appropriate resource (form or builder).
  </Step>

  <Step title="Token expires">
    After the configured expiration time, the token is no longer valid. If the user's session is still active, your backend should generate a fresh token.
  </Step>
</Steps>

## Troubleshooting

| Error                     | Cause                                             | Fix                                                                      |
| ------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------ |
| `401 token_expired`       | Token `exp` claim is in the past                  | Generate a new token with a fresh `exp`                                  |
| `401 invalid_signature`   | Token signed with wrong secret                    | Verify you are using the correct JWT secret for this account             |
| `401 invalid_audience`    | Token `aud` does not match endpoint               | Use `aud: "form"` for signing forms and `aud: "builder"` for the builder |
| `401 invalid_issuer`      | Token `iss` is not `"docutrust"`                  | Set `iss: "docutrust"` in the token payload                              |
| `403 account_mismatch`    | Token `aid` does not match the resource's account | Ensure the submitter/template belongs to the account in the token        |
| `404 submitter_not_found` | Token `sub` references a non-existent submitter   | Verify the submitter ID exists and has not been archived                 |
| `404 template_not_found`  | Token `sub` references a non-existent template    | Verify the template ID exists and has not been archived                  |
