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

# Embeddable template editor

> Embed the template editor in your product with secure JWT authentication.

Embed Firma's template editor inside your application using JWT authentication for secure, time-limited access. This is ideal for white-label integrations and multi-tenant applications.

## Use cases

* **White-label template editing**: Let users edit templates under your brand
* **Multi-tenant applications**: Secure per-user template access without exposing API keys
* **Embedded workflows**: Seamless template creation within your product
* **Time-limited access**: Tokens expire automatically for security

## How it works

1. Your server requests a JWT token from Firma's API using your API key
2. Firma returns a short-lived JWT token with the template ID
3. Your frontend embeds the editor with the JWT token
4. The token expires automatically (configurable expiration)

<Note>
  **Rate limit**: JWT endpoints support **120 requests per minute per API key** for high-volume applications.
</Note>

***

## JWT Authentication

### Generate JWT token

Generate a JWT token for a specific template using the `/generate-template-token` endpoint.

**Endpoint**: `POST /generate-template-token`

**Request body**:

```json theme={null}
{
  "companies_workspaces_templates_id": "123e4567-e89b-12d3-a456-426614174000"
}
```

**Response (201 Created)**:

```json theme={null}
{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_at": "2024-04-20T10:00:00Z",
  "jwt_record_id": "jwt123-e89b-12d3-a456-426614174000"
}
```

**Rate limit headers**:

```
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 1729180800
```

***

## Implementation guide

<Warning>
  **Security**: Never expose your API key in client-side code. Always generate JWT tokens from your secure backend.
</Warning>

### Backend: Generate JWT token

Call the Firma API from your backend to generate a JWT token. Your backend endpoint should accept a template ID and return the JWT to your frontend.

**Node.js example**:

```js theme={null}
// Example: Simple backend function to generate JWT
async function generateTemplateToken(templateId) {
  const response = await fetch('https://api.firma.dev/functions/v1/signing-request-api/generate-template-token', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FIRMA_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      companies_workspaces_templates_id: templateId
    })
  })
  
  const data = await response.json()
  return data.token
}
```

**Python example**:

```py theme={null}
import os
import requests

def generate_template_token(template_id):
    response = requests.post(
        'https://api.firma.dev/functions/v1/signing-request-api/generate-template-token',
        headers={
            'Authorization': f'Bearer {os.getenv("FIRMA_API_KEY")}',
            'Content-Type': 'application/json'
        },
        json={
            'companies_workspaces_templates_id': template_id
        }
    )
    data = response.json()
    return data['token']
```

### Frontend implementation — HTML / Vanilla JavaScript

```html theme={null}
<!-- Load the Firma Template Editor library -->
<script src="https://api.firma.dev/functions/v1/embed-proxy/template-editor.js"></script>

<!-- Create a container for the editor -->
<div id="template-editor-container"></div>

<script>
  // Assuming you've already fetched the JWT from your backend
  const jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
  const templateId = '08f72e3b-79a8-4eac-a268-b3f9efaf6573';
  
  // Initialize the template editor
  const editor = new FirmaTemplateEditor({
    container: '#template-editor-container',
    jwt: jwt,
    templateId: templateId,
    
    // Optional configuration
    width: '100%',        // Custom width (default: '100%')
    height: '100vh',      // Custom height (default: '100vh')
    showCloseButton: true, // Show close button in header
    
    // Optional callbacks
    onSave: (data) => {
      console.log('Template saved:', data);
    },
    onClose: (data) => {
      if (data.hasUnsavedChanges) {
        // Prompt user to save before leaving
      }
      console.log('Editor closed, unsaved changes:', data.hasUnsavedChanges);
    },
    onError: (error) => {
      console.error('Editor error:', error);
    },
    onLoad: (template) => {
      console.log('Editor loaded successfully:', template);
    }
  });
</script>
```

### Frontend implementation — React

```jsx theme={null}
import { useEffect, useRef, useState } from 'react';

function TemplateEditorComponent({ templateId, jwt }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<any>(null);
  const [isLoaded, setIsLoaded] = useState(false);

  // Load the Firma Template Editor library
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://api.firma.dev/functions/v1/embed-proxy/template-editor.js';
    script.async = true;
    
    script.onload = () => setIsLoaded(true);
    document.body.appendChild(script);
    
    return () => {
      if (document.body.contains(script)) {
        document.body.removeChild(script);
      }
    };
  }, []);

  // Initialize the editor when library and JWT are ready
  useEffect(() => {
    if (!isLoaded || !containerRef.current || !jwt) return;

    editorRef.current = new window.FirmaTemplateEditor({
      container: containerRef.current,
      jwt: jwt,
      templateId: templateId,
      
      // Optional configuration
      theme: 'dark',        // 'light' | 'dark' (default: 'dark')
      readOnly: false,      // Disable editing (default: false)
      width: '100%',        // Custom width (default: '100%')
      height: '100vh',      // Custom height (default: '100vh')
      showCloseButton: true, // Show close button in header
      
      // Optional callbacks
      onSave: (data) => console.log('Saved:', data),
      onClose: (data) => {
        if (data.hasUnsavedChanges) {
          // Prompt user to save before leaving
        }
      },
      onError: (error) => console.error('Error:', error),
      onLoad: (template) => console.log('Loaded:', template)
    });

    return () => {
      if (editorRef.current?.destroy) {
        editorRef.current.destroy();
      }
    };
  }, [isLoaded, jwt, templateId]);

  return <div ref={containerRef} className="w-full h-full" />;
}

export default TemplateEditorComponent;
```

### Configuration Options

| Option            | Type                  | Description                                                                                                    |
| ----------------- | --------------------- | -------------------------------------------------------------------------------------------------------------- |
| `container`       | HTMLElement \| string | DOM element or CSS selector to mount the editor (required)                                                     |
| `jwt`             | string                | Authentication token from your backend (required)                                                              |
| `templateId`      | string                | Template identifier (required)                                                                                 |
| `theme`           | `'dark'` \| `'light'` | Editor theme (default: `'dark'`)                                                                               |
| `readOnly`        | boolean               | Enable read-only mode (default: `false`)                                                                       |
| `height`          | string                | Container height (default: `'100vh'`)                                                                          |
| `width`           | string                | Container width (default: `'100%'`)                                                                            |
| `showCloseButton` | boolean               | Show a close button in the editor header (default: `false`)                                                    |
| `onSave`          | function              | Callback when template is saved                                                                                |
| `onClose`         | function              | Callback when close button is clicked or `triggerClose()` is called. Receives `{ hasUnsavedChanges: boolean }` |
| `onError`         | function              | Callback for error handling                                                                                    |
| `onLoad`          | function              | Callback when editor loads with template data                                                                  |

### Instance Methods

| Method           | Description                                                                                                                      |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `triggerClose()` | Programmatically triggers the close flow. Checks for unsaved changes and invokes `onClose` with `{ hasUnsavedChanges: boolean }` |

Use `triggerClose()` when your host application needs to close the editor from its own UI — for example, a parent navigation event or a "back" button:

```js theme={null}
// Close the editor from your app's UI
document.getElementById('my-back-button').addEventListener('click', () => {
  editor.triggerClose();
});
```

## postMessage events (editor → host)

Firma's editor will emit postMessage events for important lifecycle actions. Below is a recommended, minimal event schema you can implement for reacting to editor saves and publishes. If you have a canonical schema in your platform, replace these with your official event names.

<Tip>
  Use these events to track editor activity without making additional API calls, helping you stay within rate limits.
</Tip>

Event envelope (window\.postMessage payload):

```json theme={null}
{
	"type": "editor.event",
	"event": "editor.saved", // or editor.published, editor.closed
	"payload": {
		"template_id": "tmpl_123",
		"updated_at": "2025-09-04T12:34:56Z",
		"draft": false
	}
}
```

### Client listener example (plain JS)

```js theme={null}
window.addEventListener('message', (ev) => {
	// 1) Validate origin
	if (ev.origin !== 'https://app.firma.dev') return
	// 2) Validate shape
	const data = ev.data || {}
	if (data.type !== 'editor.event') return

	switch (data.event) {
		case 'editor.saved':
			console.log('Template saved', data.payload)
			// Optionally fetch the latest template via API
			break
		case 'editor.published':
			console.log('Template published', data.payload)
			break
		case 'editor.closed':
			console.log('Editor closed')
			break
		default:
			break
	}
})
```

***

## Token lifecycle management

JWT tokens are generated with sufficient expiration time for typical editing sessions. The editor will automatically handle token expiration.

### Automatic expiration

JWT tokens expire automatically based on the `expires_at` timestamp. After expiration:

* The embedded editor will reject the token
* Users must request a new token to continue
* No API call needed — tokens expire passively

***

## Rate limiting

See the guide on [Rate Limits](/guides/rate-limits).

***

## Security best practices

<Warning>
  **Never expose your API key in client-side code.** Always generate JWT tokens from a secure server endpoint.
</Warning>

### ✅ Do's

* ✅ Generate tokens from your backend server
* ✅ Monitor rate limits
* ✅ Use HTTPS for all API requests

### ❌ Don'ts

* ❌ Don't expose API keys in frontend code
* ❌ Don't reuse tokens across users
* ❌ Don't log JWT tokens (security risk)
* ❌ Don't share tokens between different templates

***

## Troubleshooting

### Token expired error

**Symptom**: Editor shows "Token expired" or authentication error

**Solution**:

* Implement token refresh before expiration
* Generate a new token and reload the iframe
* Check system clock synchronization

### 401 Unauthorized

**Symptom**: JWT generation fails with 401

**Possible causes**:

* Invalid or missing API key
* API key doesn't have required permissions
* API key is disabled

**Solution**: Verify API key in dashboard and check permissions

### 404 Not Found

**Symptom**: Template not found when generating JWT

**Possible causes**:

* Template ID doesn't exist
* Template belongs to different workspace
* Template was deleted

**Solution**: Verify template ID and workspace access

### Rate limit exceeded

**Symptom**: 429 Too Many Requests

**Solution**:

* Implement token caching
* Increase token expiration time
* Wait for rate limit reset (check `X-RateLimit-Reset` header)
* Implement retry logic with backoff

***

## Next steps

* [Embed signing interface](/guides/embeddable-signing) for white-label signing (100 req/min)
* [Send signing requests](/guides/sending-signing-request) with custom templates (100 req/min)
* [Set up webhooks](/guides/webhooks) to track template events (60 req/min)
