Skip to main content

How to Build a Document Signing Flow with DocuSign API

·APIScout Team
docusigne-signaturedocument signingtutorialapi integration

How to Build a Document Signing Flow with DocuSign API

DocuSign handles legally-binding electronic signatures. Upload a document, define signing fields, send to signers, and track completion. This guide covers embedded signing (in-app), remote signing (via email), templates, and webhook notifications.

What You'll Build

  • Envelope creation with signing fields
  • Embedded signing (sign in your app)
  • Remote signing (sign via email)
  • Reusable templates
  • Webhook notifications for completion

Prerequisites: DocuSign Developer account (free sandbox), Node.js 18+.

1. Setup

Create Developer Account

  1. Go to developers.docusign.com
  2. Create a free developer account
  3. Go to Settings → Apps and Keys
  4. Note your Integration Key (Client ID) and Account ID
  5. Generate an RSA Key Pair (for JWT auth)

Install SDK

npm install docusign-esign

Authentication (JWT Grant)

// lib/docusign.ts
import docusign from 'docusign-esign';

const INTEGRATION_KEY = process.env.DOCUSIGN_INTEGRATION_KEY!;
const USER_ID = process.env.DOCUSIGN_USER_ID!;
const ACCOUNT_ID = process.env.DOCUSIGN_ACCOUNT_ID!;
const PRIVATE_KEY = process.env.DOCUSIGN_PRIVATE_KEY!;
const BASE_PATH = 'https://demo.docusign.net/restapi'; // Use 'https://www.docusign.net/restapi' for production

let accessToken: string | null = null;
let tokenExpiry = 0;

export async function getApiClient(): Promise<docusign.ApiClient> {
  const apiClient = new docusign.ApiClient();
  apiClient.setBasePath(BASE_PATH);

  // Get or refresh token
  if (!accessToken || Date.now() > tokenExpiry) {
    const results = await apiClient.requestJWTUserToken(
      INTEGRATION_KEY,
      USER_ID,
      ['signature', 'impersonation'],
      Buffer.from(PRIVATE_KEY, 'utf-8'),
      3600 // 1 hour
    );

    accessToken = results.body.access_token;
    tokenExpiry = Date.now() + (results.body.expires_in - 300) * 1000;
  }

  apiClient.addDefaultHeader('Authorization', `Bearer ${accessToken}`);
  return apiClient;
}

export async function getEnvelopesApi(): Promise<docusign.EnvelopesApi> {
  const apiClient = await getApiClient();
  return new docusign.EnvelopesApi(apiClient);
}

2. Create and Send Envelope

Basic Envelope (Remote Signing)

// lib/send-envelope.ts
import docusign from 'docusign-esign';
import { getEnvelopesApi } from './docusign';
import { readFileSync } from 'fs';

const ACCOUNT_ID = process.env.DOCUSIGN_ACCOUNT_ID!;

export async function sendForSignature(options: {
  documentPath: string;
  documentName: string;
  signerEmail: string;
  signerName: string;
  subject: string;
}): Promise<string> {
  const envelopesApi = await getEnvelopesApi();

  // Read document
  const documentBytes = readFileSync(options.documentPath);
  const documentBase64 = documentBytes.toString('base64');

  // Create envelope definition
  const envelopeDefinition = new docusign.EnvelopeDefinition();
  envelopeDefinition.emailSubject = options.subject;

  // Add document
  const document = new docusign.Document();
  document.documentBase64 = documentBase64;
  document.name = options.documentName;
  document.fileExtension = 'pdf';
  document.documentId = '1';
  envelopeDefinition.documents = [document];

  // Add signer
  const signer = new docusign.Signer();
  signer.email = options.signerEmail;
  signer.name = options.signerName;
  signer.recipientId = '1';
  signer.routingOrder = '1';

  // Add signature tab (where to sign)
  const signHere = new docusign.SignHere();
  signHere.anchorString = '/sig1/'; // Looks for this text in the document
  signHere.anchorUnits = 'pixels';
  signHere.anchorYOffset = '10';
  signHere.anchorXOffset = '20';

  // Add date tab
  const dateSigned = new docusign.DateSigned();
  dateSigned.anchorString = '/date1/';
  dateSigned.anchorUnits = 'pixels';

  const tabs = new docusign.Tabs();
  tabs.signHereTabs = [signHere];
  tabs.dateSignedTabs = [dateSigned];
  signer.tabs = tabs;

  const recipients = new docusign.Recipients();
  recipients.signers = [signer];
  envelopeDefinition.recipients = recipients;
  envelopeDefinition.status = 'sent'; // 'created' for draft

  // Send
  const result = await envelopesApi.createEnvelope(ACCOUNT_ID, {
    envelopeDefinition,
  });

  return result.envelopeId!;
}

Fixed Position Tabs (No Anchor Text)

// Place signature at exact coordinates
const signHere = new docusign.SignHere();
signHere.documentId = '1';
signHere.pageNumber = '1';
signHere.xPosition = '200';
signHere.yPosition = '700';
signHere.recipientId = '1';

3. Embedded Signing (In-App)

// lib/embedded-signing.ts
import docusign from 'docusign-esign';
import { getEnvelopesApi } from './docusign';

const ACCOUNT_ID = process.env.DOCUSIGN_ACCOUNT_ID!;

export async function getSigningUrl(options: {
  envelopeId: string;
  signerEmail: string;
  signerName: string;
  returnUrl: string;
}): Promise<string> {
  const envelopesApi = await getEnvelopesApi();

  const viewRequest = new docusign.RecipientViewRequest();
  viewRequest.returnUrl = options.returnUrl;
  viewRequest.authenticationMethod = 'none';
  viewRequest.email = options.signerEmail;
  viewRequest.userName = options.signerName;
  viewRequest.recipientId = '1';

  const result = await envelopesApi.createRecipientView(
    ACCOUNT_ID,
    options.envelopeId,
    { recipientViewRequest: viewRequest }
  );

  return result.url!; // Redirect user to this URL
}

API Route for Embedded Signing

// app/api/sign/route.ts
import { NextResponse } from 'next/server';
import { sendForSignature } from '@/lib/send-envelope';
import { getSigningUrl } from '@/lib/embedded-signing';

export async function POST(req: Request) {
  const { documentPath, signerEmail, signerName } = await req.json();

  // 1. Create envelope
  const envelopeId = await sendForSignature({
    documentPath,
    documentName: 'Agreement.pdf',
    signerEmail,
    signerName,
    subject: 'Please sign this agreement',
  });

  // 2. Get embedded signing URL
  const signingUrl = await getSigningUrl({
    envelopeId,
    signerEmail,
    signerName,
    returnUrl: `${process.env.NEXT_PUBLIC_URL}/signing-complete?envelopeId=${envelopeId}`,
  });

  return NextResponse.json({ signingUrl, envelopeId });
}

Signing Component

// components/SignDocument.tsx
'use client';
import { useState } from 'react';

export function SignDocument({ documentId }: { documentId: string }) {
  const [loading, setLoading] = useState(false);

  const handleSign = async () => {
    setLoading(true);

    const res = await fetch('/api/sign', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        documentPath: `/documents/${documentId}.pdf`,
        signerEmail: 'user@example.com',
        signerName: 'John Doe',
      }),
    });

    const { signingUrl } = await res.json();

    // Redirect to DocuSign signing ceremony
    window.location.href = signingUrl;
  };

  return (
    <button onClick={handleSign} disabled={loading}>
      {loading ? 'Preparing document...' : 'Sign Document'}
    </button>
  );
}

4. Multiple Signers (Routing)

export async function sendMultiSignerEnvelope(options: {
  documentBase64: string;
  signers: { email: string; name: string; routingOrder: number }[];
}) {
  const envelopesApi = await getEnvelopesApi();

  const envelopeDefinition = new docusign.EnvelopeDefinition();
  envelopeDefinition.emailSubject = 'Please sign this document';

  // Document
  const document = new docusign.Document();
  document.documentBase64 = options.documentBase64;
  document.name = 'Contract.pdf';
  document.documentId = '1';
  envelopeDefinition.documents = [document];

  // Create signers with routing order
  const signers = options.signers.map((s, i) => {
    const signer = new docusign.Signer();
    signer.email = s.email;
    signer.name = s.name;
    signer.recipientId = String(i + 1);
    signer.routingOrder = String(s.routingOrder);

    const signHere = new docusign.SignHere();
    signHere.anchorString = `/sig${i + 1}/`;
    signHere.anchorUnits = 'pixels';

    const tabs = new docusign.Tabs();
    tabs.signHereTabs = [signHere];
    signer.tabs = tabs;

    return signer;
  });

  const recipients = new docusign.Recipients();
  recipients.signers = signers;
  envelopeDefinition.recipients = recipients;
  envelopeDefinition.status = 'sent';

  const result = await envelopesApi.createEnvelope(ACCOUNT_ID, {
    envelopeDefinition,
  });

  return result.envelopeId!;
}

Routing order determines signing sequence:

  • Same routing order = signers can sign in parallel
  • Different routing order = sequential (order 1 signs first, then order 2)

5. Templates

Create Template (Once)

export async function createTemplate() {
  const apiClient = await getApiClient();
  const templatesApi = new docusign.TemplatesApi(apiClient);

  const template = new docusign.EnvelopeTemplate();
  template.name = 'NDA Agreement';
  template.description = 'Standard NDA template';
  template.emailSubject = 'NDA for your signature';

  // Add role (filled in when sending)
  const signer = new docusign.Signer();
  signer.roleName = 'Signer';
  signer.recipientId = '1';
  signer.routingOrder = '1';

  const recipients = new docusign.Recipients();
  recipients.signers = [signer];
  template.recipients = recipients;

  const result = await templatesApi.createTemplate(ACCOUNT_ID, {
    envelopeTemplate: template,
  });

  return result.templateId;
}

Send from Template

export async function sendFromTemplate(options: {
  templateId: string;
  signerEmail: string;
  signerName: string;
}) {
  const envelopesApi = await getEnvelopesApi();

  const envelopeDefinition = new docusign.EnvelopeDefinition();
  envelopeDefinition.templateId = options.templateId;

  // Fill in template role
  const templateRole = new docusign.TemplateRole();
  templateRole.email = options.signerEmail;
  templateRole.name = options.signerName;
  templateRole.roleName = 'Signer'; // Must match template role name

  envelopeDefinition.templateRoles = [templateRole];
  envelopeDefinition.status = 'sent';

  const result = await envelopesApi.createEnvelope(ACCOUNT_ID, {
    envelopeDefinition,
  });

  return result.envelopeId!;
}

6. Webhooks (Connect)

Configure Webhook

In DocuSign Admin → Connect → Add Configuration:

  • URL: https://your-app.com/api/webhooks/docusign
  • Events: Envelope Sent, Delivered, Completed, Declined, Voided

Handle Events

// app/api/webhooks/docusign/route.ts
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const body = await req.text();

  // DocuSign sends XML by default
  // Parse the envelope status
  const envelopeId = extractFromXml(body, 'EnvelopeID');
  const status = extractFromXml(body, 'Status');

  switch (status) {
    case 'completed':
      // All signers have signed
      await handleCompleted(envelopeId);
      break;

    case 'declined':
      // A signer declined
      await handleDeclined(envelopeId);
      break;

    case 'voided':
      // Sender voided the envelope
      await handleVoided(envelopeId);
      break;
  }

  return NextResponse.json({ received: true });
}

async function handleCompleted(envelopeId: string) {
  // Download signed document
  const envelopesApi = await getEnvelopesApi();
  const document = await envelopesApi.getDocument(
    ACCOUNT_ID,
    envelopeId,
    'combined' // All documents in one PDF
  );

  // Save to storage
  // Update database record
}

function extractFromXml(xml: string, tag: string): string {
  const match = xml.match(new RegExp(`<${tag}>(.*?)</${tag}>`));
  return match ? match[1] : '';
}

7. Download Signed Documents

export async function downloadSignedDocument(envelopeId: string): Promise<Buffer> {
  const envelopesApi = await getEnvelopesApi();

  const document = await envelopesApi.getDocument(
    ACCOUNT_ID,
    envelopeId,
    'combined' // 'combined' for all docs, or document ID for specific
  );

  return Buffer.from(document as any);
}

// Check envelope status
export async function getEnvelopeStatus(envelopeId: string) {
  const envelopesApi = await getEnvelopesApi();
  const envelope = await envelopesApi.getEnvelope(ACCOUNT_ID, envelopeId);

  return {
    status: envelope.status, // 'sent', 'delivered', 'completed', 'declined'
    sentDateTime: envelope.sentDateTime,
    completedDateTime: envelope.completedDateTime,
  };
}

Pricing

PlanPriceEnvelopes
Personal$10/month5/month
Standard$25/user/monthUnlimited
Business Pro$40/user/monthUnlimited + advanced features
API PlansCustomVolume-based
Developer SandboxFreeUnlimited (test only)

Common Mistakes

MistakeImpactFix
Using demo base path in productionAPI calls failSwitch to www.docusign.net for production
Not handling token refresh401 errors after 1 hourCheck expiry before each request
Missing consent grantJWT auth failsUser must grant consent via OAuth URL once
Anchor strings not in documentTabs don't appearUse fixed position tabs or verify anchors
Not checking envelope status before signingErrors on completed envelopesVerify status is 'sent' before creating view

Need e-signatures? Compare DocuSign vs HelloSign vs PandaDoc on APIScout — signing APIs, pricing, and integration complexity.

Comments