Skip to main content
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)
Rate limit: JWT endpoints support 120 requests per minute per API key for high-volume applications.

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:
{
  "companies_workspaces_templates_id": "123e4567-e89b-12d3-a456-426614174000"
}
Response (201 Created):
{
  "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

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

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:
// 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:
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

<!-- 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')
    
    // Optional callbacks
    onSave: (data) => {
      console.log('Template saved:', data);
    },
    onError: (error) => {
      console.error('Editor error:', error);
    },
    onLoad: (template) => {
      console.log('Editor loaded successfully:', template);
    }
  });
</script>

Frontend implementation — React

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')
      
      // Optional callbacks
      onSave: (data) => console.log('Saved:', data),
      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

OptionTypeDescription
containerHTMLElement | stringDOM element or CSS selector to mount the editor (required)
jwtstringAuthentication token from your backend (required)
templateIdstringTemplate identifier (required)
theme'dark' | 'light'Editor theme (default: 'dark')
readOnlybooleanEnable read-only mode (default: false)
heightstringContainer height (default: '100vh')
widthstringContainer width (default: '100%')
onSavefunctionCallback when template is saved
onErrorfunctionCallback for error handling
onLoadfunctionCallback when editor loads with template data

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.
Use these events to track editor activity without making additional API calls, helping you stay within rate limits.
Event envelope (window.postMessage payload):
{
	"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)

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.

Security best practices

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

✅ 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