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

# Éditeur de modèles intégrable

> Intégrez l'éditeur de modèles de Firma dans votre produit avec une authentification JWT sécurisée.

Intégrez l'éditeur de modèles de Firma directement dans votre application grâce à une authentification JWT pour un accès sécurisé et limité dans le temps. C'est idéal pour les intégrations en marque blanche et les applications multi-tenant.

## Cas d'usage

* **Édition de modèles en marque blanche** : laissez vos utilisateurs éditer des modèles sous votre propre marque
* **Applications multi-tenant** : accès sécurisé aux modèles par utilisateur sans exposer de clés API
* **Workflows intégrés** : création de modèles fluide au sein de votre produit
* **Accès limité dans le temps** : les jetons expirent automatiquement pour plus de sécurité

## Comment ça fonctionne

1. Votre serveur demande un jeton JWT à l'API de Firma en utilisant votre clé API
2. Firma renvoie un jeton JWT de courte durée contenant l'ID du modèle
3. Votre frontend intègre l'éditeur avec le jeton JWT
4. Le jeton expire automatiquement (expiration configurable)

<Note>
  **Limite de débit** : les endpoints JWT prennent en charge **120 requêtes par minute par clé API** pour les applications à fort volume.
</Note>

***

## Authentification JWT

### Générer un jeton JWT

Générez un jeton JWT pour un modèle spécifique via l'endpoint `/generate-template-token`.

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

**Corps de la requête** :

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

**Réponse (201 Created)** :

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

**En-têtes de limite de débit** :

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

***

## Guide d'implémentation

<Warning>
  **Sécurité** : n'exposez jamais votre clé API dans du code côté client. Générez toujours les jetons JWT depuis votre backend sécurisé.
</Warning>

### Backend : générer un jeton JWT

Appelez l'API de Firma depuis votre backend pour générer un jeton JWT. Votre endpoint backend doit accepter un ID de modèle et renvoyer le JWT à votre frontend.

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

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

### Implémentation frontend — HTML / JavaScript natif

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

### Implémentation 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;
```

### Options de configuration

| Option            | Type                  | Description                                                                                                                         |
| ----------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `container`       | HTMLElement \| string | Élément DOM ou sélecteur CSS où monter l'éditeur (requis)                                                                           |
| `jwt`             | string                | Jeton d'authentification fourni par votre backend (requis)                                                                          |
| `templateId`      | string                | Identifiant du modèle (requis)                                                                                                      |
| `theme`           | `'dark'` \| `'light'` | Thème de l'éditeur (par défaut : `'dark'`)                                                                                          |
| `readOnly`        | boolean               | Active le mode lecture seule (par défaut : `false`)                                                                                 |
| `height`          | string                | Hauteur du conteneur (par défaut : `'100vh'`)                                                                                       |
| `width`           | string                | Largeur du conteneur (par défaut : `'100%'`)                                                                                        |
| `showCloseButton` | boolean               | Affiche un bouton de fermeture dans l'en-tête de l'éditeur (par défaut : `false`)                                                   |
| `onSave`          | function              | Callback appelé lorsque le modèle est enregistré                                                                                    |
| `onClose`         | function              | Callback appelé quand le bouton de fermeture est cliqué ou que `triggerClose()` est appelé. Reçoit `{ hasUnsavedChanges: boolean }` |
| `onError`         | function              | Callback pour la gestion des erreurs                                                                                                |
| `onLoad`          | function              | Callback appelé lorsque l'éditeur se charge avec les données du modèle                                                              |

### Méthodes d'instance

| Méthode          | Description                                                                                                                                          |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `triggerClose()` | Déclenche le flux de fermeture par programmation. Vérifie les changements non enregistrés et invoque `onClose` avec `{ hasUnsavedChanges: boolean }` |

Utilisez `triggerClose()` lorsque votre application hôte doit fermer l'éditeur depuis sa propre interface, par exemple lors d'un événement de navigation parent ou d'un bouton « retour » :

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

## Événements postMessage (éditeur → hôte)

L'éditeur de Firma émet des événements postMessage pour les actions importantes du cycle de vie. Voici un schéma d'événements minimal recommandé que vous pouvez implémenter pour réagir aux enregistrements et publications dans l'éditeur. Si vous disposez d'un schéma canonique dans votre plateforme, remplacez ces noms par vos noms d'événements officiels.

<Tip>
  Utilisez ces événements pour suivre l'activité de l'éditeur sans effectuer d'appels API supplémentaires, afin de rester dans les limites de débit.
</Tip>

Enveloppe de l'événement (payload 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
	}
}
```

### Exemple d'écouteur côté client (JS simple)

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

***

## Gestion du cycle de vie du jeton

Les jetons JWT sont générés avec une durée d'expiration suffisante pour des sessions d'édition classiques. L'éditeur gère automatiquement l'expiration du jeton.

### Expiration automatique

Les jetons JWT expirent automatiquement en fonction de l'horodatage `expires_at`. Après expiration :

* L'éditeur intégré rejette le jeton
* Les utilisateurs doivent demander un nouveau jeton pour continuer
* Aucun appel API n'est nécessaire, les jetons expirent de manière passive

***

## Limite de débit

Consultez le guide sur les [Limites de débit](rate-limits).

***

## Bonnes pratiques de sécurité

<Warning>
  **N'exposez jamais votre clé API dans du code côté client.** Générez toujours les jetons JWT depuis un endpoint serveur sécurisé.
</Warning>

### ✅ À faire

* ✅ Générez les jetons depuis votre serveur backend
* ✅ Surveillez les limites de débit
* ✅ Utilisez HTTPS pour toutes les requêtes API

### ❌ À ne pas faire

* ❌ N'exposez pas les clés API dans le code frontend
* ❌ Ne réutilisez pas les jetons entre différents utilisateurs
* ❌ Ne journalisez pas les jetons JWT (risque de sécurité)
* ❌ Ne partagez pas les jetons entre différents modèles

***

## Dépannage

### Erreur de jeton expiré

**Symptôme** : l'éditeur affiche « Token expired » ou une erreur d'authentification

**Solution** :

* Implémentez le renouvellement du jeton avant son expiration
* Générez un nouveau jeton et rechargez l'iframe
* Vérifiez la synchronisation de l'horloge système

### 401 Unauthorized

**Symptôme** : la génération du JWT échoue avec un 401

**Causes possibles** :

* Clé API invalide ou manquante
* La clé API ne dispose pas des permissions requises
* La clé API est désactivée

**Solution** : vérifiez la clé API dans le tableau de bord et contrôlez les permissions

### 404 Not Found

**Symptôme** : modèle introuvable lors de la génération du JWT

**Causes possibles** :

* L'ID du modèle n'existe pas
* Le modèle appartient à un autre espace de travail
* Le modèle a été supprimé

**Solution** : vérifiez l'ID du modèle et l'accès à l'espace de travail

### Limite de débit dépassée

**Symptôme** : 429 Too Many Requests

**Solution** :

* Implémentez la mise en cache des jetons
* Augmentez la durée d'expiration du jeton
* Attendez la réinitialisation de la limite de débit (vérifiez l'en-tête `X-RateLimit-Reset`)
* Implémentez une logique de nouvelle tentative avec backoff

***

## Étapes suivantes

* [Intégrer l'interface de signature](embeddable-signing) pour une signature en marque blanche (100 req/min)
* [Envoyer des demandes de signature](sending-signing-request) avec des modèles personnalisés (100 req/min)
* [Configurer des webhooks](webhooks) pour suivre les événements liés aux modèles (60 req/min)
