Skip to main content

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: Let users manage signing requests under your brand
  • Multi-tenant applications: Secure per-user signing request 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
Rate limit: JWT endpoints support 100 requests per minute per API key for high-volume applications.

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:
{
  "companies_workspaces_signing_requests_id": "123e4567-e89b-12d3-a456-426614174000"
}
Response (200 OK):
{
  "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

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 signing request ID and return the JWT to your frontend. Node.js example:
// 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:
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

<!-- Load the Firma Signing Request Editor library -->
<script src="https://app.firma.dev/embed/signing-request-editor.js"></script>

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

<script>
  async function initializeEditor(signingRequestId) {
    try {
      // Get JWT from your backend endpoint
      const jwt = await fetch('/your-backend-endpoint/generate-token?signingRequestId=' + signingRequestId)
        .then(res => res.json())
        .then(data => data.jwt);
      
      // Initialize the signing request editor
      const editor = new FirmaSigningRequestEditor({
        container: document.getElementById('signing-request-editor-container'),
        jwt: jwt,
        signingRequestId: signingRequestId,
        theme: 'dark', // or 'light'
        onSave: (data) => {
          console.log('Signing request saved:', data);
        },
        onSend: (data) => {
          console.log('Signing request sent:', data);
        },
        onError: (error) => {
          console.error('Editor error:', error);
        },
        onLoad: (signingRequest) => {
          console.log('Editor loaded successfully:', signingRequest);
        }
      });
    } catch (error) {
      console.error('Failed to initialize editor:', error);
    }
  }
  
  // Usage
  initializeEditor('08f72e3b-79a8-4eac-a268-b3f9efaf6573');
</script>

Frontend implementation — React

import { useEffect, useRef, useState } from 'react';

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

  // Load the Firma Signing Request Editor library
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://app.firma.dev/embed/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);
      }
    };
  }, []);

  // Fetch JWT token from your backend
  useEffect(() => {
    async function fetchToken() {
      try {
        const response = await fetch(`/your-backend-endpoint/generate-token?signingRequestId=${signingRequestId}`);
        const data = await response.json();
        setJwt(data.jwt);
      } catch (error) {
        console.error('Failed to fetch JWT:', error);
      }
    }
    
    fetchToken();
  }, [signingRequestId]);

  // 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,
      theme: 'dark',
      onSave: (data) => console.log('Saved:', data),
      onSend: (data) => console.log('Sent:', data),
      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

OptionTypeDescription
containerHTMLElementDOM element to mount the editor (required)
jwtstringAuthentication token from your backend (required)
signingRequestIdstringSigning request 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 signing request is saved
onSendfunctionCallback when signing request is sent
onErrorfunctionCallback for error handling
onLoadfunctionCallback when editor loads with signing request data

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.
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": "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)

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
  • Users must request a new token 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:
// 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

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 (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 users
  • ❌ 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