> ## 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.

# Editor de plantillas incrustable

> Incrusta el editor de plantillas de Firma en tu producto con autenticación JWT segura.

Incrusta el Editor de Plantillas de Firma dentro de tu aplicación usando autenticación JWT para un acceso seguro y de tiempo limitado. Esto es ideal para integraciones de marca blanca y aplicaciones multiinquilino.

## Casos de uso

* **Edición de plantillas de marca blanca**: Permite que los usuarios editen plantillas bajo tu marca
* **Aplicaciones multiinquilino**: Acceso seguro a plantillas por usuario sin exponer claves API
* **Flujos de trabajo incrustados**: Creación de plantillas sin fisuras dentro de tu producto
* **Acceso de tiempo limitado**: Los tokens expiran automáticamente por seguridad

## Cómo funciona

1. Tu servidor solicita un token JWT a la API de Firma usando tu clave API
2. Firma devuelve un token JWT de corta duración con el ID de la plantilla
3. Tu frontend incrusta el editor con el token JWT
4. El token expira automáticamente (expiración configurable)

<Note>
  **Límite de tasa**: Los endpoints JWT admiten **120 solicitudes por minuto por clave API** para aplicaciones de alto volumen.
</Note>

***

## Autenticación JWT

### Generar token JWT

Genera un token JWT para una plantilla específica usando el endpoint `/generate-template-token`.

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

**Cuerpo de la solicitud**:

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

**Respuesta (201 Created)**:

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

**Encabezados de límite de tasa**:

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

***

## Guía de implementación

<Warning>
  **Seguridad**: Nunca expongas tu clave API en código del lado del cliente. Genera siempre los tokens JWT desde tu backend seguro.
</Warning>

### Backend: Generar token JWT

Llama a la API de Firma desde tu backend para generar un token JWT. El endpoint de tu backend debe aceptar un ID de plantilla y devolver el JWT a tu frontend.

**Ejemplo en Node.js**:

```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
}
```

**Ejemplo en Python**:

```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']
```

### Implementación en frontend — HTML / JavaScript nativo

```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>
```

### Implementación en frontend — 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;
```

### Opciones de configuración

| Opción            | Tipo                  | Descripción                                                                                                               |
| ----------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `container`       | HTMLElement \| string | Elemento del DOM o selector CSS donde montar el editor (obligatorio)                                                      |
| `jwt`             | string                | Token de autenticación de tu backend (obligatorio)                                                                        |
| `templateId`      | string                | Identificador de la plantilla (obligatorio)                                                                               |
| `theme`           | `'dark'` \| `'light'` | Tema del editor (predeterminado: `'dark'`)                                                                                |
| `readOnly`        | boolean               | Habilita el modo de solo lectura (predeterminado: `false`)                                                                |
| `height`          | string                | Alto del contenedor (predeterminado: `'100vh'`)                                                                           |
| `width`           | string                | Ancho del contenedor (predeterminado: `'100%'`)                                                                           |
| `showCloseButton` | boolean               | Muestra un botón de cierre en el encabezado del editor (predeterminado: `false`)                                          |
| `onSave`          | function              | Callback cuando se guarda la plantilla                                                                                    |
| `onClose`         | function              | Callback cuando se hace clic en el botón de cierre o se llama a `triggerClose()`. Recibe `{ hasUnsavedChanges: boolean }` |
| `onError`         | function              | Callback para el manejo de errores                                                                                        |
| `onLoad`          | function              | Callback cuando el editor carga con los datos de la plantilla                                                             |

### Métodos de instancia

| Método           | Descripción                                                                                                                                   |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `triggerClose()` | Activa el flujo de cierre de forma programática. Comprueba si hay cambios sin guardar e invoca `onClose` con `{ hasUnsavedChanges: boolean }` |

Usa `triggerClose()` cuando tu aplicación anfitriona necesite cerrar el editor desde su propia interfaz, por ejemplo, ante un evento de navegación del padre o un botón "atrás":

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

## Eventos postMessage (editor → anfitrión)

El editor de Firma emitirá eventos postMessage para acciones importantes del ciclo de vida. A continuación encontrarás un esquema de eventos recomendado y mínimo que puedes implementar para reaccionar a los guardados y publicaciones del editor. Si tienes un esquema canónico en tu plataforma, reemplaza estos por los nombres de evento oficiales de tu sistema.

<Tip>
  Usa estos eventos para hacer seguimiento de la actividad del editor sin realizar llamadas API adicionales, lo que te ayuda a mantenerte dentro de los límites de tasa.
</Tip>

Envoltorio del evento (payload de window\.postMessage):

```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
	}
}
```

### Ejemplo de listener en el cliente (JS puro)

```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
	}
})
```

***

## Gestión del ciclo de vida del token

Los tokens JWT se generan con tiempo de expiración suficiente para sesiones de edición típicas. El editor gestionará automáticamente la expiración del token.

### Expiración automática

Los tokens JWT expiran automáticamente según la marca de tiempo `expires_at`. Tras la expiración:

* El editor incrustado rechazará el token
* Los usuarios deben solicitar un nuevo token para continuar
* No se necesita ninguna llamada API: los tokens expiran de forma pasiva

***

## Límite de tasa

Consulta la guía sobre [Límites de Tasa](rate-limits).

***

## Prácticas recomendadas de seguridad

<Warning>
  **Nunca expongas tu clave API en código del lado del cliente.** Genera siempre los tokens JWT desde un endpoint de servidor seguro.
</Warning>

### ✅ Qué hacer

* ✅ Genera los tokens desde tu servidor backend
* ✅ Supervisa los límites de tasa
* ✅ Usa HTTPS para todas las solicitudes API

### ❌ Qué no hacer

* ❌ No expongas claves API en código de frontend
* ❌ No reutilices tokens entre usuarios
* ❌ No registres tokens JWT en logs (riesgo de seguridad)
* ❌ No compartas tokens entre distintas plantillas

***

## Solución de problemas

### Error de token expirado

**Síntoma**: El editor muestra "Token expired" o un error de autenticación

**Solución**:

* Implementa la renovación del token antes de que expire
* Genera un nuevo token y recarga el iframe
* Comprueba la sincronización del reloj del sistema

### 401 Unauthorized

**Síntoma**: La generación del JWT falla con 401

**Posibles causas**:

* Clave API inválida o ausente
* La clave API no tiene los permisos necesarios
* La clave API está deshabilitada

**Solución**: Verifica la clave API en el panel y comprueba los permisos

### 404 Not Found

**Síntoma**: Plantilla no encontrada al generar el JWT

**Posibles causas**:

* El ID de la plantilla no existe
* La plantilla pertenece a otro espacio de trabajo
* La plantilla fue eliminada

**Solución**: Verifica el ID de la plantilla y el acceso al espacio de trabajo

### Límite de tasa excedido

**Síntoma**: 429 Too Many Requests

**Solución**:

* Implementa el almacenamiento en caché de tokens
* Aumenta el tiempo de expiración del token
* Espera a que se reinicie el límite de tasa (comprueba el encabezado `X-RateLimit-Reset`)
* Implementa lógica de reintento con backoff

***

## Próximos pasos

* [Incrustar la interfaz de firma](embeddable-signing) para firma de marca blanca (100 req/min)
* [Enviar solicitudes de firma](sending-signing-request) con plantillas personalizadas (100 req/min)
* [Configurar webhooks](webhooks) para hacer seguimiento de eventos de plantillas (60 req/min)
