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

Installation

No additional dependencies are required beyond Vue. This guide covers both Vue 3 (Composition API) and Vue 2 (Options API) implementations.
# Vue 3
npm install vue@^3

# Vue 2
npm install vue@^2

Vue 3 Component (Composition API)

DocumensoEmbed.vue

<template>
  <iframe
    :src="embedUrl"
    class="documenso-embed"
    allow="clipboard-write"
    title="Documenso Document Signing"
  />
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';

interface Props {
  token: string;
  name?: string;
  lockName?: boolean;
  email?: string;
  lockEmail?: boolean;
  allowDocumentRejection?: boolean;
  showOtherRecipientsCompletedFields?: boolean;
  darkModeDisabled?: boolean;
  css?: string;
  cssVars?: Record<string, string>;
  baseUrl?: string;
}

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

interface DocumentRejectedData extends DocumentCompletedData {
  reason?: string;
}

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

const props = withDefaults(defineProps<Props>(), {
  lockName: false,
  lockEmail: false,
  allowDocumentRejection: false,
  showOtherRecipientsCompletedFields: false,
  darkModeDisabled: false,
  baseUrl: 'https://app.documenso.com'
});

const emit = defineEmits<{
  ready: [];
  complete: [data: DocumentCompletedData];
  reject: [data: DocumentRejectedData];
  error: [];
  fieldSigned: [data: FieldSignedData];
  fieldUnsigned: [data: { fieldId?: number }];
}>();

const embedUrl = computed(() => {
  const embedData: Record<string, any> = {
    darkModeDisabled: props.darkModeDisabled
  };

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

  const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
  return `${props.baseUrl}/embed/sign/${props.token}#${hash}`;
});

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

  switch (event.data.action) {
    case 'document-ready':
      emit('ready');
      break;
    case 'document-completed':
      emit('complete', event.data.data);
      break;
    case 'document-rejected':
      emit('reject', event.data.data);
      break;
    case 'document-error':
      emit('error');
      break;
    case 'field-signed':
      emit('fieldSigned', event.data.data);
      break;
    case 'field-unsigned':
      emit('fieldUnsigned', event.data.data);
      break;
  }
};

onMounted(() => {
  window.addEventListener('message', handleMessage);
});

onUnmounted(() => {
  window.removeEventListener('message', handleMessage);
});
</script>

<style scoped>
.documenso-embed {
  width: 100%;
  height: 100%;
  border: none;
}
</style>

Vue 2 Component (Options API)

DocumensoEmbed.vue (Vue 2)

<template>
  <iframe
    :src="embedUrl"
    class="documenso-embed"
    allow="clipboard-write"
    title="Documenso Document Signing"
  />
</template>

<script>
export default {
  name: 'DocumensoEmbed',
  props: {
    token: {
      type: String,
      required: true
    },
    name: {
      type: String,
      default: undefined
    },
    lockName: {
      type: Boolean,
      default: false
    },
    email: {
      type: String,
      default: undefined
    },
    lockEmail: {
      type: Boolean,
      default: false
    },
    allowDocumentRejection: {
      type: Boolean,
      default: false
    },
    showOtherRecipientsCompletedFields: {
      type: Boolean,
      default: false
    },
    darkModeDisabled: {
      type: Boolean,
      default: false
    },
    css: {
      type: String,
      default: undefined
    },
    cssVars: {
      type: Object,
      default: undefined
    },
    baseUrl: {
      type: String,
      default: 'https://app.documenso.com'
    }
  },
  computed: {
    embedUrl() {
      const embedData = {
        darkModeDisabled: this.darkModeDisabled
      };

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

      const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
      return `${this.baseUrl}/embed/sign/${this.token}#${hash}`;
    }
  },
  mounted() {
    window.addEventListener('message', this.handleMessage);
  },
  beforeDestroy() {
    window.removeEventListener('message', this.handleMessage);
  },
  methods: {
    handleMessage(event) {
      const allowedOrigin = new URL(this.baseUrl).origin;
      if (event.origin !== allowedOrigin) {
        return;
      }

      switch (event.data.action) {
        case 'document-ready':
          this.$emit('ready');
          break;
        case 'document-completed':
          this.$emit('complete', event.data.data);
          break;
        case 'document-rejected':
          this.$emit('reject', event.data.data);
          break;
        case 'document-error':
          this.$emit('error');
          break;
        case 'field-signed':
          this.$emit('field-signed', event.data.data);
          break;
        case 'field-unsigned':
          this.$emit('field-unsigned', event.data.data);
          break;
      }
    }
  }
};
</script>

<style scoped>
.documenso-embed {
  width: 100%;
  height: 100%;
  border: none;
}
</style>

Usage Examples

Basic Usage (Vue 3)

<template>
  <div class="signing-page">
    <h1>Sign Your Contract</h1>
    <DocumensoEmbed
      :token="recipientToken"
      @complete="onComplete"
      @error="onError"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';

const router = useRouter();
const recipientToken = ref('token-xyz-123');

const onComplete = (data: any) => {
  console.log('Document signed!', data);
  router.push('/success');
};

const onError = () => {
  console.error('Error signing document');
  router.push('/error');
};
</script>

<style scoped>
.signing-page {
  height: 100vh;
  display: flex;
  flex-direction: column;
}
</style>

With Loading State

<template>
  <div class="signing-container">
    <div v-if="isLoading" class="loader-overlay">
      <div class="spinner" />
      <p>Loading document...</p>
    </div>

    <div v-if="error" class="error-overlay">
      <h2>Error</h2>
      <p>{{ error }}</p>
      <button @click="retry">Retry</button>
    </div>

    <DocumensoEmbed
      :token="token"
      @ready="isLoading = false"
      @complete="onComplete"
      @error="onError"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';

const props = defineProps<{
  token: string;
}>();

const isLoading = ref(true);
const error = ref<string | null>(null);

const onComplete = (data: any) => {
  console.log('Completed:', data);
};

const onError = () => {
  isLoading.value = false;
  error.value = 'Failed to load document. Please try again.';
};

const retry = () => {
  window.location.reload();
};
</script>

<style scoped>
.signing-container {
  position: relative;
  height: 100vh;
}

.loader-overlay,
.error-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.95);
  z-index: 10;
}

.spinner {
  width: 48px;
  height: 48px;
  border: 3px solid #f3f3f3;
  border-top: 3px solid #3b82f6;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>

Pre-filled Customer Data

<template>
  <DocumensoEmbed
    :token="token"
    :email="customerEmail"
    :lock-email="true"
    :name="customerName"
    :lock-name="false"
    :allow-document-rejection="true"
    @complete="handleComplete"
    @reject="handleReject"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';

const props = defineProps<{
  token: string;
  customerEmail: string;
  customerName: string;
}>();

const handleComplete = async (data: any) => {
  // Save to backend
  await fetch('/api/documents/complete', {
    method: 'POST',
    body: JSON.stringify(data)
  });
};

const handleReject = (data: any) => {
  console.log('Document rejected:', data);
};
</script>

Custom Styling

<template>
  <DocumensoEmbed
    :token="token"
    :css-vars="brandColors"
    :css="customCss"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';

const token = ref('token-123');

const brandColors = {
  primary: '#3b82f6',
  background: '#ffffff',
  foreground: '#0f172a',
  radius: '0.75rem'
};

const customCss = `
  .embed--Root {
    font-family: 'Inter', sans-serif;
  }
  button {
    font-weight: 600;
  }
`;
</script>

Direct Template Component

<template>
  <iframe
    :src="embedUrl"
    class="direct-template-embed"
    allow="clipboard-write"
    title="Sign Document"
  />
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';

interface Props {
  directToken: string;
  email?: string;
  lockEmail?: boolean;
  name?: string;
  lockName?: boolean;
  externalId?: string;
  cssVars?: Record<string, string>;
  css?: string;
  baseUrl?: string;
}

const props = withDefaults(defineProps<Props>(), {
  lockEmail: false,
  lockName: false,
  baseUrl: 'https://app.documenso.com'
});

const emit = defineEmits<{
  ready: [];
  complete: [data: any];
  error: [error: string];
}>();

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

  const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
  const queryParams = props.externalId 
    ? `?externalId=${encodeURIComponent(props.externalId)}` 
    : '';
  
  return `${props.baseUrl}/d/${props.directToken}${queryParams}#${hash}`;
});

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

  switch (event.data.action) {
    case 'document-ready':
      emit('ready');
      break;
    case 'document-completed':
      emit('complete', event.data.data);
      break;
    case 'document-error':
      emit('error', event.data.data);
      break;
  }
};

onMounted(() => {
  window.addEventListener('message', handleMessage);
});

onUnmounted(() => {
  window.removeEventListener('message', handleMessage);
});
</script>

<style scoped>
.direct-template-embed {
  width: 100%;
  height: 100%;
  border: none;
}
</style>

Composable for Embed State

// composables/useDocumensoEmbed.ts
import { ref, onMounted, onUnmounted } from 'vue';

export function useDocumensoEmbed(baseUrl = 'https://app.documenso.com') {
  const isReady = ref(false);
  const isCompleted = ref(false);
  const error = ref<string | null>(null);
  const completionData = ref<any>(null);

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

    switch (event.data.action) {
      case 'document-ready':
        isReady.value = true;
        break;
      case 'document-completed':
        isCompleted.value = true;
        completionData.value = event.data.data;
        break;
      case 'document-error':
        error.value = 'An error occurred while loading the document';
        break;
    }
  };

  onMounted(() => {
    window.addEventListener('message', handleMessage);
  });

  onUnmounted(() => {
    window.removeEventListener('message', handleMessage);
  });

  const reset = () => {
    isReady.value = false;
    isCompleted.value = false;
    error.value = null;
    completionData.value = null;
  };

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

Using the Composable

<template>
  <div>
    <div v-if="error">
      Error: {{ error }}
    </div>
    
    <div v-else-if="isCompleted">
      <h2>Document Signed!</h2>
      <p>Document ID: {{ completionData.documentId }}</p>
    </div>
    
    <div v-else>
      <div v-if="!isReady">Loading...</div>
      <DocumensoEmbed :token="token" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { useDocumensoEmbed } from '@/composables/useDocumensoEmbed';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';

const props = defineProps<{
  token: string;
}>();

const { isReady, isCompleted, error, completionData } = useDocumensoEmbed();
</script>

Nuxt 3 Integration

<!-- pages/sign/[token].vue -->
<template>
  <div class="container">
    <DocumensoEmbed
      v-if="token"
      :token="token"
      @complete="handleComplete"
    />
  </div>
</template>

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';

const route = useRoute();
const router = useRouter();
const token = computed(() => route.params.token as string);

const handleComplete = async (data: any) => {
  // Call API route
  await $fetch('/api/documents/complete', {
    method: 'POST',
    body: data
  });
  
  router.push('/success');
};
</script>

<style scoped>
.container {
  height: 100vh;
}
</style>

Pinia Store Integration

// stores/document.ts
import { defineStore } from 'pinia';

export const useDocumentStore = defineStore('document', {
  state: () => ({
    currentToken: null as string | null,
    isReady: false,
    isCompleted: false,
    completionData: null as any
  }),
  actions: {
    setToken(token: string) {
      this.currentToken = token;
    },
    markReady() {
      this.isReady = true;
    },
    markCompleted(data: any) {
      this.isCompleted = true;
      this.completionData = data;
    },
    reset() {
      this.isReady = false;
      this.isCompleted = false;
      this.completionData = null;
    }
  }
});

Using with Store

<template>
  <DocumensoEmbed
    v-if="documentStore.currentToken"
    :token="documentStore.currentToken"
    @ready="documentStore.markReady()"
    @complete="documentStore.markCompleted($event)"
  />
</template>

<script setup lang="ts">
import { useDocumentStore } from '@/stores/document';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';

const documentStore = useDocumentStore();
</script>

Testing

// DocumensoEmbed.spec.ts
import { mount } from '@vue/test-utils';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';

describe('DocumensoEmbed', () => {
  it('renders iframe with correct src', () => {
    const wrapper = mount(DocumensoEmbed, {
      props: {
        token: 'test-token'
      }
    });

    const iframe = wrapper.find('iframe');
    expect(iframe.exists()).toBe(true);
    expect(iframe.attributes('src')).toContain('test-token');
  });

  it('emits ready event when document is ready', async () => {
    const wrapper = mount(DocumensoEmbed, {
      props: {
        token: 'test-token'
      }
    });

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

    await wrapper.vm.$nextTick();
    expect(wrapper.emitted('ready')).toBeTruthy();
  });
});

Best Practices

  1. Always validate message origins for security
  2. Use TypeScript for type safety with Composition API
  3. Clean up event listeners in onUnmounted/beforeDestroy
  4. Use computed properties for reactive embed URLs
  5. Handle loading and error states gracefully
  6. Emit typed events for better developer experience
  7. Use composables for reusable embed logic

Next Steps

CSS Variables

Customize appearance with CSS variables

Direct Links

Learn about direct signing links