Skip to main content
While Documenso doesn’t provide an official React SDK, you can easily build reusable React components to wrap the embedding functionality. This guide shows you how to create production-ready React components for document signing.

Installation

No additional dependencies are required beyond React. For TypeScript support, ensure you have type definitions:
npm install react react-dom
npm install -D @types/react @types/react-dom

Basic Signing Component

DocumensoEmbed Component

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

export interface DocumensoEmbedProps {
  /** Recipient signing token */
  token: string;
  
  /** Pre-fill recipient name */
  name?: string;
  
  /** Lock name field (prevent editing) */
  lockName?: boolean;
  
  /** Pre-fill recipient email */
  email?: string;
  
  /** Lock email field (prevent editing) */
  lockEmail?: boolean;
  
  /** Allow recipient to reject document */
  allowDocumentRejection?: boolean;
  
  /** Show fields completed by other recipients */
  showOtherRecipientsCompletedFields?: boolean;
  
  /** Disable dark mode */
  darkModeDisabled?: boolean;
  
  /** Custom CSS (Enterprise only) */
  css?: string;
  
  /** CSS variables for theming (Enterprise only) */
  cssVars?: Record<string, string>;
  
  /** Documenso instance URL */
  baseUrl?: string;
  
  /** Loading state callback */
  onReady?: () => void;
  
  /** Document completed callback */
  onComplete?: (data: {
    token: string;
    documentId: number;
    envelopeId: string;
    recipientId: number;
  }) => void;
  
  /** Document rejected callback */
  onReject?: (data: {
    token: string;
    documentId: number;
    envelopeId: string;
    recipientId: number;
    reason?: string;
  }) => void;
  
  /** Error callback */
  onError?: () => void;
  
  /** Field signed callback */
  onFieldSigned?: (data: {
    fieldId?: number;
    value?: string;
    isBase64?: boolean;
  }) => void;
  
  /** Field unsigned callback */
  onFieldUnsigned?: (data: { fieldId?: number }) => void;
  
  /** iframe className */
  className?: string;
  
  /** iframe style */
  style?: React.CSSProperties;
}

export function DocumensoEmbed({
  token,
  name,
  lockName = false,
  email,
  lockEmail = false,
  allowDocumentRejection = false,
  showOtherRecipientsCompletedFields = false,
  darkModeDisabled = false,
  css,
  cssVars,
  baseUrl = 'https://app.documenso.com',
  onReady,
  onComplete,
  onReject,
  onError,
  onFieldSigned,
  onFieldUnsigned,
  className = 'w-full h-screen border-0',
  style
}: DocumensoEmbedProps) {
  const [embedUrl, setEmbedUrl] = useState<string>('');

  useEffect(() => {
    // Build embed data
    const embedData: Record<string, any> = {
      darkModeDisabled
    };

    if (name) embedData.name = name;
    if (lockName) embedData.lockName = lockName;
    if (email) embedData.email = email;
    if (lockEmail) embedData.lockEmail = lockEmail;
    if (allowDocumentRejection) embedData.allowDocumentRejection = allowDocumentRejection;
    if (showOtherRecipientsCompletedFields) {
      embedData.showOtherRecipientsCompletedFields = showOtherRecipientsCompletedFields;
    }
    if (css) embedData.css = css;
    if (cssVars) embedData.cssVars = cssVars;

    // Encode as URL hash
    const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
    const url = `${baseUrl}/embed/sign/${token}#${hash}`;
    setEmbedUrl(url);
  }, [
    token,
    name,
    lockName,
    email,
    lockEmail,
    allowDocumentRejection,
    showOtherRecipientsCompletedFields,
    darkModeDisabled,
    css,
    cssVars,
    baseUrl
  ]);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      // Verify origin for security
      const allowedOrigin = new URL(baseUrl).origin;
      if (event.origin !== allowedOrigin) {
        return;
      }

      switch (event.data.action) {
        case 'document-ready':
          onReady?.();
          break;
        case 'document-completed':
          onComplete?.(event.data.data);
          break;
        case 'document-rejected':
          onReject?.(event.data.data);
          break;
        case 'document-error':
          onError?.();
          break;
        case 'field-signed':
          onFieldSigned?.(event.data.data);
          break;
        case 'field-unsigned':
          onFieldUnsigned?.(event.data.data);
          break;
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [baseUrl, onReady, onComplete, onReject, onError, onFieldSigned, onFieldUnsigned]);

  return (
    <iframe
      src={embedUrl}
      className={className}
      style={style}
      allow="clipboard-write"
      title="Documenso Document Signing"
    />
  );
}

Usage Examples

Basic Usage

import { DocumensoEmbed } from './components/DocumensoEmbed';

function SigningPage() {
  return (
    <div className="container mx-auto p-4">
      <h1>Sign Your Contract</h1>
      <DocumensoEmbed
        token="recipient-token-xyz"
        onComplete={(data) => {
          console.log('Document signed!', data);
          // Redirect to success page
          window.location.href = '/success';
        }}
      />
    </div>
  );
}

Pre-filled and Locked Fields

function CustomerSigningPage({ customerEmail, customerName }: {
  customerEmail: string;
  customerName: string;
}) {
  return (
    <DocumensoEmbed
      token="token-123"
      email={customerEmail}
      lockEmail={true}  // Prevent customer from changing email
      name={customerName}
      lockName={false}  // Allow name editing
      allowDocumentRejection={true}
      onComplete={async (data) => {
        // Save to your database
        await fetch('/api/documents/completed', {
          method: 'POST',
          body: JSON.stringify(data)
        });
      }}
    />
  );
}

Custom Styling

function BrandedSigningPage({ token }: { token: string }) {
  return (
    <DocumensoEmbed
      token={token}
      cssVars={{
        primary: '#3b82f6',
        background: '#ffffff',
        foreground: '#0f172a',
        radius: '0.75rem'
      }}
      css={`
        .embed--Root {
          font-family: 'Inter', sans-serif;
        }
        button {
          font-weight: 600;
        }
      `}
    />
  );
}

Advanced Component with Loading State

import { useState } from 'react';
import { DocumensoEmbed } from './DocumensoEmbed';

export function SigningPageWithLoader({ token }: { token: string }) {
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  return (
    <div className="relative w-full h-screen">
      {isLoading && (
        <div className="absolute inset-0 flex items-center justify-center bg-gray-50 z-10">
          <div className="text-center">
            <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
            <p className="text-gray-600">Loading document...</p>
          </div>
        </div>
      )}
      
      {error && (
        <div className="absolute inset-0 flex items-center justify-center bg-red-50 z-10">
          <div className="text-center">
            <p className="text-red-600 text-lg font-semibold">Error loading document</p>
            <p className="text-red-500 mt-2">{error}</p>
            <button
              onClick={() => window.location.reload()}
              className="mt-4 px-4 py-2 bg-red-600 text-white rounded"
            >
              Retry
            </button>
          </div>
        </div>
      )}
      
      <DocumensoEmbed
        token={token}
        onReady={() => setIsLoading(false)}
        onError={() => {
          setIsLoading(false);
          setError('Failed to load the document. Please try again.');
        }}
        onComplete={(data) => {
          console.log('Completed:', data);
          window.location.href = '/success';
        }}
      />
    </div>
  );
}

Direct Template Component

import { useEffect, useState } from 'react';

export interface DirectTemplateEmbedProps {
  /** Direct template token */
  directToken: string;
  
  /** Pre-fill recipient email */
  email?: string;
  
  /** Lock email field */
  lockEmail?: boolean;
  
  /** Pre-fill recipient name */
  name?: string;
  
  /** Lock name field */
  lockName?: boolean;
  
  /** External reference ID */
  externalId?: string;
  
  /** CSS variables */
  cssVars?: Record<string, string>;
  
  /** Custom CSS */
  css?: string;
  
  /** Base URL */
  baseUrl?: string;
  
  /** Callbacks */
  onReady?: () => void;
  onComplete?: (data: { token: string; documentId: number; recipientId: number }) => void;
  onError?: (error: string) => void;
  
  /** Styling */
  className?: string;
}

export function DirectTemplateEmbed({
  directToken,
  email,
  lockEmail = false,
  name,
  lockName = false,
  externalId,
  cssVars,
  css,
  baseUrl = 'https://app.documenso.com',
  onReady,
  onComplete,
  onError,
  className = 'w-full h-screen border-0'
}: DirectTemplateEmbedProps) {
  const [embedUrl, setEmbedUrl] = useState<string>('');

  useEffect(() => {
    const embedData: Record<string, any> = {};
    
    if (email) embedData.email = email;
    if (lockEmail) embedData.lockEmail = lockEmail;
    if (name) embedData.name = name;
    if (lockName) embedData.lockName = lockName;
    if (css) embedData.css = css;
    if (cssVars) embedData.cssVars = cssVars;

    const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
    const queryParams = externalId ? `?externalId=${encodeURIComponent(externalId)}` : '';
    const url = `${baseUrl}/d/${directToken}${queryParams}#${hash}`;
    
    setEmbedUrl(url);
  }, [directToken, email, lockEmail, name, lockName, externalId, css, cssVars, baseUrl]);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      const allowedOrigin = new URL(baseUrl).origin;
      if (event.origin !== allowedOrigin) return;

      switch (event.data.action) {
        case 'document-ready':
          onReady?.();
          break;
        case 'document-completed':
          onComplete?.(event.data.data);
          break;
        case 'document-error':
          onError?.(event.data.data);
          break;
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [baseUrl, onReady, onComplete, onError]);

  return (
    <iframe
      src={embedUrl}
      className={className}
      allow="clipboard-write"
      title="Sign Document"
    />
  );
}

Direct Template Usage

function PublicSigningForm() {
  return (
    <DirectTemplateEmbed
      directToken="direct-template-xyz"
      externalId="order-12345"
      onComplete={(data) => {
        // Track completion
        analytics.track('Document Signed', data);
        // Show success
        toast.success('Document signed successfully!');
      }}
    />
  );
}

Custom Hook

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

interface UseDocumensoEmbedOptions {
  token: string;
  baseUrl?: string;
}

export function useDocumensoEmbed({ token, baseUrl = 'https://app.documenso.com' }: UseDocumensoEmbedOptions) {
  const [isReady, setIsReady] = useState(false);
  const [isCompleted, setIsCompleted] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [completionData, setCompletionData] = useState<any>(null);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      const allowedOrigin = new URL(baseUrl).origin;
      if (event.origin !== allowedOrigin) return;

      switch (event.data.action) {
        case 'document-ready':
          setIsReady(true);
          break;
        case 'document-completed':
          setIsCompleted(true);
          setCompletionData(event.data.data);
          break;
        case 'document-error':
          setError('An error occurred while loading the document');
          break;
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [baseUrl]);

  const reset = useCallback(() => {
    setIsReady(false);
    setIsCompleted(false);
    setError(null);
    setCompletionData(null);
  }, []);

  return {
    isReady,
    isCompleted,
    error,
    completionData,
    reset
  };
}

// Usage
function SigningPage({ token }: { token: string }) {
  const { isReady, isCompleted, error, completionData } = useDocumensoEmbed({ token });

  if (error) {
    return <div>Error: {error}</div>;
  }

  if (isCompleted) {
    return (
      <div>
        <h2>Document Signed!</h2>
        <p>Document ID: {completionData.documentId}</p>
      </div>
    );
  }

  return (
    <div>
      {!isReady && <div>Loading...</div>}
      <DocumensoEmbed token={token} />
    </div>
  );
}

Next.js Integration

App Router (Next.js 13+)

'use client';

import { DocumensoEmbed } from '@/components/DocumensoEmbed';
import { useRouter } from 'next/navigation';

export default function SignDocumentPage({ params }: { params: { token: string } }) {
  const router = useRouter();

  return (
    <div className="h-screen">
      <DocumensoEmbed
        token={params.token}
        onComplete={async (data) => {
          // Save to database via API route
          await fetch('/api/documents/complete', {
            method: 'POST',
            body: JSON.stringify(data)
          });
          
          // Redirect to success page
          router.push('/documents/success');
        }}
        onError={() => {
          router.push('/documents/error');
        }}
      />
    </div>
  );
}

Pages Router

import { useRouter } from 'next/router';
import { DocumensoEmbed } from '@/components/DocumensoEmbed';

export default function SignPage() {
  const router = useRouter();
  const { token } = router.query;

  if (!token || typeof token !== 'string') {
    return <div>Invalid token</div>;
  }

  return (
    <DocumensoEmbed
      token={token}
      onComplete={() => router.push('/success')}
    />
  );
}

TypeScript Types

// types/documenso.ts

export interface EmbedEvent {
  action: 'document-ready' | 'document-completed' | 'document-rejected' | 'document-error' | 'field-signed' | 'field-unsigned';
  data: any;
}

export interface DocumentCompletedData {
  token: string;
  documentId: number;
  envelopeId: string;
  recipientId: number;
}

export interface DocumentRejectedData extends DocumentCompletedData {
  reason?: string;
}

export interface FieldSignedData {
  fieldId?: number;
  value?: string;
  isBase64?: boolean;
}

export interface FieldUnsignedData {
  fieldId?: number;
}

export interface CssVariables {
  background?: string;
  foreground?: string;
  primary?: string;
  primaryForeground?: string;
  secondary?: string;
  secondaryForeground?: string;
  border?: string;
  radius?: string;
  // ... add more as needed
}

export interface EmbedData {
  email?: string;
  lockEmail?: boolean;
  name?: string;
  lockName?: boolean;
  allowDocumentRejection?: boolean;
  showOtherRecipientsCompletedFields?: boolean;
  darkModeDisabled?: boolean;
  css?: string;
  cssVars?: CssVariables;
}

Testing

import { render, screen } from '@testing-library/react';
import { DocumensoEmbed } from './DocumensoEmbed';

describe('DocumensoEmbed', () => {
  it('renders iframe with correct src', () => {
    render(<DocumensoEmbed token="test-token" />);
    
    const iframe = screen.getByTitle('Documenso Document Signing');
    expect(iframe).toBeInTheDocument();
    expect(iframe).toHaveAttribute('src');
  });

  it('calls onReady when document is ready', () => {
    const onReady = jest.fn();
    render(<DocumensoEmbed token="test-token" onReady={onReady} />);

    // Simulate postMessage from iframe
    window.postMessage({ action: 'document-ready', data: null }, window.location.origin);
    
    expect(onReady).toHaveBeenCalled();
  });
});

Best Practices

  1. Always validate origin in message event listeners
  2. Use TypeScript for type safety
  3. Handle errors gracefully with user-friendly messages
  4. Show loading states for better UX
  5. Clean up event listeners in useEffect cleanup
  6. Memoize URLs to avoid unnecessary re-renders
  7. Test iframe integration in different browsers

Next Steps

Angular SDK

Build Angular wrapper components

Vue SDK

Build Vue wrapper components