> ## 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 signing request editor

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

# Embeddable signing request editor

Embed Firma's signing request 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 signing workflows**: Manage signing requests under your brand
* **Multi-tenant applications**: Secure per-signer access without exposing API keys
* **Embedded document workflows**: Seamless signing request management within your product
* **Time-limited access**: Tokens expire automatically for security (7-day expiration)

## 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 signing request ID
3. Your frontend embeds the editor with the JWT token
4. The token expires automatically after 7 days

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

***

## JWT Authentication

### Generate JWT token

Generate a JWT token for a specific signing request using the `/jwt/generate-signing-request` endpoint.

**Endpoint**: `POST /jwt/generate-signing-request`

**Request body**:

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

**Response (200 OK)**:

```json theme={null}
{
  "jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "jwt_id": "jwt123-e89b-12d3-a456-426614174000",
  "expires_at": "2024-04-27T10:00:00Z",
  "signing_request_id": "123e4567-e89b-12d3-a456-426614174000",
  "created_at": "2024-04-20T10:00:00Z"
}
```

**Rate limit headers**:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
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 signing request ID and return the JWT to your frontend.

**Node.js example**:

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

**Python example**:

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

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

### Frontend implementation — HTML / Vanilla JavaScript

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

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

<script>
  // Assuming you've already fetched the JWT from your backend
  const jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
  const signingRequestId = '08f72e3b-79a8-4eac-a268-b3f9efaf6573';
  
  // Initialize the signing request editor
  const editor = new FirmaSigningRequestEditor({
    container: '#signing-request-editor-container',
    jwt: jwt,
    signingRequestId: signingRequestId,
    showCloseButton: true, // Show close button in header
    onSave: (data) => {
      console.log('Signing request saved:', data);
    },
    onSend: (data) => {
      console.log('Signing request sent:', 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: (signingRequest) => {
      console.log('Editor loaded successfully:', signingRequest);
    }
  });
</script>
```

### Frontend implementation — React

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

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

  // Load the Firma Signing Request Editor library
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://api.firma.dev/functions/v1/embed-proxy/signing-request-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.FirmaSigningRequestEditor({
      container: containerRef.current,
      jwt: jwt,
      signingRequestId: signingRequestId,
      showCloseButton: true, // Show close button in header
      onSave: (data) => console.log('Saved:', data),
      onSend: (data) => console.log('Sent:', data),
      onClose: (data) => {
        if (data.hasUnsavedChanges) {
          // Prompt user to save before leaving
        }
      },
      onError: (error) => console.error('Error:', error),
      onLoad: (signingRequest) => console.log('Loaded:', signingRequest)
    });

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

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

export default SigningRequestEditorComponent;
```

### Configuration Options

| Option             | Type                  | Description                                                                                                    |
| ------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------- |
| `container`        | HTMLElement           | DOM element to mount the editor (required)                                                                     |
| `jwt`              | string                | Authentication token from your backend (required)                                                              |
| `signingRequestId` | string                | Signing request 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 signing request is saved                                                                         |
| `onSend`           | function              | Callback when signing request is sent                                                                          |
| `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 signing request 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 signing request 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 sends.

<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": "signing_request.saved", // or signing_request.sent, signing_request.closed
	"payload": {
		"signing_request_id": "sr_123",
		"updated_at": "2025-09-04T12:34:56Z",
		"status": "draft" // or "sent"
	}
}
```

### 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 'signing_request.saved':
			console.log('Signing request saved', data.payload)
			// Optionally fetch the latest signing request via API
			break
		case 'signing_request.sent':
			console.log('Signing request sent', data.payload)
			// Trigger webhook or notification
			break
		case 'signing_request.closed':
			console.log('Editor closed')
			break
		default:
			break
	}
})
```

***

## Token lifecycle management

JWT tokens are generated with a 7-day expiration time for typical editing workflows. The editor will automatically handle token expiration.

### Automatic expiration

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

* The embedded editor will reject the token
* A new token must be requested to continue
* No API call needed — tokens expire passively

### Token refresh (optional)

For long-running editing sessions, you can refresh the token before expiration using the `updateJWT()` method:

```js theme={null}
// Generate a new JWT from your backend
const newJwt = await fetch('/your-backend/generate-token').then(r => r.json());

// Update the editor with the new token
editor.updateJWT(newJwt.jwt);
```

***

## Rate limiting

JWT endpoints support **100 requests per minute per API key**.

**Rate limit headers**:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1729180800
```

If you exceed the rate limit (429 response):

* Cache tokens and reuse them until expiration (7 days)
* Implement exponential backoff for retries
* Monitor `X-RateLimit-Remaining` header
* Generate tokens on-demand, not preemptively

***

## 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 (100 requests/minute)
* ✅ Use HTTPS for all API requests
* ✅ Use `readOnly: true` for view-only access

### ❌ Don'ts

* ❌ Don't expose API keys in frontend code
* ❌ Don't reuse tokens across sessions or signers
* ❌ Don't log JWT tokens (security risk)
* ❌ Don't share tokens between different signing requests

***

## Troubleshooting

### Token expired error

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

**Solution**:

* Implement token refresh before expiration (7-day window)
* Generate a new token and call `editor.updateJWT(newToken)`
* 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**: Signing request not found when generating JWT

**Possible causes**:

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

**Solution**: Verify signing request ID and workspace access

### Rate limit exceeded

**Symptom**: 429 Too Many Requests

**Solution**:

* Implement token caching (7-day expiration window is generous)
* Wait for rate limit reset (check `X-RateLimit-Reset` header)
* Implement retry logic with exponential backoff

### Editor not loading

**Symptom**: Container is empty or shows loading spinner indefinitely

**Possible causes**:

* Script not loaded (check `script.onload` event)
* Invalid JWT or signing request ID
* CORS issues (check browser console)

**Solution**:

* Verify script is loaded from `https://app.firma.dev/embed/signing-request-editor.js`
* Check JWT is valid and not expired
* Ensure backend returns correct CORS headers

***

## Next steps

* [Embed template editor](/guides/embeddable-template-editor) for white-label template creation (120 req/min)
* [Send signing requests](/guides/sending-signing-request) programmatically (100 req/min)
* [Set up webhooks](/guides/webhooks) to track signing request events (60 req/min)
* [Configure workspace settings](/guides/workspace-settings) for multi-tenant apps (100-200 req/min)
