Skip to main content
Direct signing links allow you to embed Documenso’s signing interface in your application using a simple iframe. This is the recommended approach for most integrations.

Overview

Direct links provide recipient-specific URLs that can be embedded directly in your application. Each recipient gets a unique token that grants access to sign the document.

Getting Recipient Tokens

Via API v1

When you create a document via the API, signing links are automatically generated for each recipient:
const response = await fetch('https://app.documenso.com/api/v1/documents', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_TOKEN',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'Service Agreement',
    recipients: [
      {
        email: 'customer@example.com',
        name: 'John Doe',
        role: 'SIGNER'
      }
    ],
    // Don't send emails - you'll embed the signing page
    meta: {
      sendEmail: false
    }
  })
});

const data = await response.json();
// Each recipient has a unique token
const recipientToken = data.recipients[0].token;
For reusable templates, you can create a direct link that allows anyone with the link to sign:
// Create a direct link for a template (via tRPC)
const directLink = await trpc.template.createTemplateDirectLink.mutate({
  id: { secondaryId: 'template-abc123' },
  userId: 1,
  teamId: 1
});

// Direct link URL structure:
// https://app.documenso.com/d/{token}
const directUrl = `https://app.documenso.com/d/${directLink.token}`;
Direct Template Features:
  • No pre-defined recipients required
  • Anyone with the link can sign
  • Signer provides their own name/email
  • Perfect for public forms and agreements

Basic Iframe Embedding

Simple Embed

<!DOCTYPE html>
<html>
<head>
  <title>Sign Document</title>
  <style>
    #documenso-embed {
      border: none;
      width: 100%;
      height: 100vh;
    }
  </style>
</head>
<body>
  <iframe
    id="documenso-embed"
    src="https://app.documenso.com/embed/sign/abc123xyz"
    allow="clipboard-write"
  ></iframe>
</body>
</html>

Responsive Embed

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe
    src="https://app.documenso.com/embed/sign/abc123xyz"
    style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;"
    allow="clipboard-write"
  ></iframe>
</div>

Pre-filling Recipient Data

You can pre-fill and lock recipient information by passing data in the URL hash:
function createEmbedUrl(token: string, options: {
  email?: string;
  lockEmail?: boolean;
  name?: string;
  lockName?: boolean;
  allowDocumentRejection?: boolean;
  showOtherRecipientsCompletedFields?: boolean;
  darkModeDisabled?: boolean;
}) {
  const embedData = {
    email: options.email,
    lockEmail: options.lockEmail ?? false,
    name: options.name,
    lockName: options.lockName ?? false,
    allowDocumentRejection: options.allowDocumentRejection,
    showOtherRecipientsCompletedFields: options.showOtherRecipientsCompletedFields,
    darkModeDisabled: options.darkModeDisabled ?? false
  };

  // Encode as base64 URL hash
  const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
  return `https://app.documenso.com/embed/sign/${token}#${hash}`;
}

// Usage
const embedUrl = createEmbedUrl('abc123xyz', {
  email: 'customer@example.com',
  lockEmail: true,  // Prevent user from changing email
  name: 'John Doe',
  lockName: true,   // Prevent user from changing name
  allowDocumentRejection: true
});

Embed Data Schema

FieldTypeDescription
emailstringPre-fill recipient email (templates only)
lockEmailbooleanPrevent email editing
namestringPre-fill recipient name
lockNamebooleanPrevent name editing
allowDocumentRejectionbooleanAllow recipient to reject document
showOtherRecipientsCompletedFieldsbooleanShow fields completed by other recipients
darkModeDisabledbooleanDisable dark mode
cssstringCustom CSS (Enterprise only)
cssVarsobjectCSS variables (Enterprise only)

Listening to Events

The embedded iframe sends events to your application via postMessage:
type EmbedEvent = 
  | { action: 'document-ready'; data: null }
  | { action: 'document-completed'; data: {
      token: string;
      documentId: number;
      envelopeId: string;
      recipientId: number;
    }}
  | { action: 'document-rejected'; data: {
      token: string;
      documentId: number;
      envelopeId: string;
      recipientId: number;
      reason?: string;
    }}
  | { action: 'document-error'; data: null }
  | { action: 'field-signed'; data: {
      fieldId?: number;
      value?: string;
      isBase64?: boolean;
    }}
  | { action: 'field-unsigned'; data: { fieldId?: number } };

window.addEventListener('message', (event: MessageEvent<EmbedEvent>) => {
  // Verify origin for security
  if (event.origin !== 'https://app.documenso.com') {
    return;
  }

  switch (event.data.action) {
    case 'document-ready':
      console.log('✓ Document loaded and ready for signing');
      break;

    case 'document-completed':
      console.log('✓ Document signed successfully!');
      // Redirect to success page or show confirmation
      window.location.href = '/documents/success';
      break;

    case 'document-rejected':
      console.log('✗ Document rejected by recipient');
      const { reason } = event.data.data;
      alert(`Document rejected: ${reason || 'No reason provided'}`);
      break;

    case 'field-signed':
      console.log('Field signed:', event.data.data);
      // Track progress
      break;

    case 'document-error':
      console.error('Error in embedded document');
      // Show error message to user
      break;
  }
});

Direct Template Embedding

Direct templates allow anyone to sign without pre-creating recipients:
<!-- Embed direct template link -->
<iframe
  src="https://app.documenso.com/d/direct-template-token#embedData"
  width="100%"
  height="800px"
  allow="clipboard-write"
></iframe>

Pre-filling Direct Template Data

function createDirectTemplateEmbedUrl(
  directToken: string,
  options: {
    email?: string;
    lockEmail?: boolean;
    name?: string;
    lockName?: boolean;
    externalId?: string;
  }
) {
  const embedData = {
    email: options.email,
    lockEmail: options.lockEmail ?? false,
    name: options.name,
    lockName: options.lockName ?? false
  };

  const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
  
  // Add externalId as query parameter if provided
  const queryParams = options.externalId 
    ? `?externalId=${encodeURIComponent(options.externalId)}`
    : '';

  return `https://app.documenso.com/d/${directToken}${queryParams}#${hash}`;
}

// Usage: Customer initiates signing from your app
const embedUrl = createDirectTemplateEmbedUrl('direct-token-xyz', {
  email: 'customer@example.com',
  lockEmail: true,
  name: 'Jane Smith',
  lockName: false,
  externalId: 'order-12345' // Your internal reference
});

Full Example: React Component

import { useEffect, useState } from 'react';

interface DocumensoEmbedProps {
  token: string;
  recipientName?: string;
  recipientEmail?: string;
  onComplete?: (data: any) => void;
  onReject?: (reason?: string) => void;
  onError?: () => void;
}

export function DocumensoEmbed({
  token,
  recipientName,
  recipientEmail,
  onComplete,
  onReject,
  onError
}: DocumensoEmbedProps) {
  const [embedUrl, setEmbedUrl] = useState<string>('');
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    // Build embed URL with pre-filled data
    const embedData = {
      name: recipientName,
      lockName: !!recipientName,
      email: recipientEmail,
      lockEmail: !!recipientEmail,
      allowDocumentRejection: true
    };

    const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
    const url = `https://app.documenso.com/embed/sign/${token}#${hash}`;
    setEmbedUrl(url);
  }, [token, recipientName, recipientEmail]);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.origin !== 'https://app.documenso.com') return;

      switch (event.data.action) {
        case 'document-ready':
          setIsReady(true);
          break;
        case 'document-completed':
          onComplete?.(event.data.data);
          break;
        case 'document-rejected':
          onReject?.(event.data.data.reason);
          break;
        case 'document-error':
          onError?.();
          break;
      }
    };

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

  return (
    <div className="relative w-full h-screen">
      {!isReady && (
        <div className="absolute inset-0 flex items-center justify-center bg-gray-50">
          <div className="text-gray-600">Loading document...</div>
        </div>
      )}
      <iframe
        src={embedUrl}
        className="w-full h-full border-0"
        allow="clipboard-write"
      />
    </div>
  );
}

Security Considerations

Origin Verification

Always verify the message origin:
window.addEventListener('message', (event) => {
  // For self-hosted instances
  const allowedOrigins = [
    'https://app.documenso.com',
    'https://your-domain.com'
  ];
  
  if (!allowedOrigins.includes(event.origin)) {
    console.warn('Ignored message from untrusted origin:', event.origin);
    return;
  }
  
  // Process event
});

Token Security

  • Never expose recipient tokens in client-side code
  • Generate tokens server-side only
  • Use presign tokens for additional security
  • Tokens are automatically expired when documents are completed

iframe Sandbox

For maximum security, consider using iframe sandbox attributes:
<iframe
  src="https://app.documenso.com/embed/sign/token"
  sandbox="allow-scripts allow-same-origin allow-forms"
  allow="clipboard-write"
></iframe>

Troubleshooting

Document Not Loading

  1. Check token validity: Tokens expire after document completion
  2. Verify recipient access: Ensure it’s the recipient’s turn to sign
  3. Check authentication: Some documents require account login
  4. Review Enterprise plan: Embedding requires appropriate subscription

Events Not Received

  1. Verify origin: Check that your origin validation allows the embed domain
  2. Check browser console: Look for CORS or security errors
  3. Test in incognito: Rule out browser extension interference

Styling Issues

  1. Set iframe dimensions: Ensure adequate width/height
  2. Check z-index: Verify iframe isn’t hidden behind other elements
  3. Allow clipboard: Include allow="clipboard-write" for signature features

Next Steps

CSS Customization

Customize the embedded interface appearance

React Component

Build a reusable React wrapper