Skip to main content

DocuSign API: Document Signing Integration 2026

·APIScout Team
Share:

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

Electronic signatures created via DocuSign are legally binding under the ESIGN Act (Electronic Signatures in Global and National Commerce Act, US 2000), UETA (Uniform Electronic Transactions Act, adopted by 47 US states), and eIDAS (EU Regulation No 910/2014). These laws establish that electronic signatures carry the same legal weight as wet signatures for the vast majority of commercial agreements.

Three types of electronic signature under eIDAS matter for European contracts. Simple Electronic Signature (SES) is what DocuSign's standard workflow produces — any digital acceptance of a document. Advanced Electronic Signature (AES) requires the signature to be uniquely linked to the signer and capable of identifying them — DocuSign's identity verification features (SMS authentication, ID verification) achieve this level. Qualified Electronic Signature (QES) requires a qualified certificate from a trust service provider and special hardware — DocuSign's QES product supports this but requires a separate agreement and is mainly used for healthcare and government documents in EU member states. For most commercial contracts (SaaS agreements, NDAs, employment contracts), SES is legally sufficient in both the US and EU.

DocuSign's audit trail is critical for enforceability. Every envelope generates an automatically maintained certificate of completion — a tamper-evident document that records the IP address, geolocation, device type, and timestamp of every action (viewed, signed, declined) by every recipient. Courts have accepted DocuSign audit trails as evidence in disputes. Store the envelope ID in your database — you can retrieve the audit trail certificate at any time via envelopesApi.getDocument(ACCOUNT_ID, envelopeId, 'certificate').

Documents that cannot be e-signed: some document types require wet signatures by law even with ESIGN Act coverage — wills, testamentary trusts, adoption documents, divorce decrees, and certain government forms. Consumer loan disclosures under the Federal Truth in Lending Act have specific ESIGN opt-in requirements. Verify your specific use case's legal requirements before deploying; DocuSign's legal team publishes a jurisdiction guide for their enterprise customers.


DocuSign Alternatives

DocuSign is the enterprise standard, but the API pricing and integration complexity are high for smaller teams. Three alternatives are worth considering:

HelloSign (Dropbox Sign): acquired by Dropbox in 2019, now deeply integrated with the Dropbox product suite. HelloSign's API is simpler than DocuSign's — the SDK has fewer concepts (no separate "recipients API" vs "envelopes API" distinction), and the embedded signing UX is cleaner. Pricing is substantially lower: $25/month for 5 users with unlimited signatures, versus DocuSign Standard at $25/user/month. The trade-off is fewer enterprise features: no advanced SSO, limited workflow automation, and Dropbox Sign lacks DocuSign's compliance certifications (FDA 21 CFR Part 11, FedRAMP) for regulated industries.

PandaDoc: focuses on the full document workflow — creation, negotiation, and signature in one platform. Unlike DocuSign, PandaDoc includes document creation (you build templates in their editor, not your own PDF), and the pricing model bundles unlimited signatures with document storage. PandaDoc's API covers document creation and sending, but the template system is tightly coupled to PandaDoc's editor rather than your own PDF documents. Better choice for sales teams creating custom proposals; worse choice for developers building document-agnostic signing infrastructure.

Adobe Acrobat Sign (formerly Adobe Sign / EchoSign): strong for organizations already in the Adobe ecosystem. Adobe Sign has excellent PDF form field recognition (automatically detects form fields in uploaded PDFs and converts them to signing fields) and deep Microsoft Office integration. The API is less developer-friendly than DocuSign's — the REST API is well-documented but the SDK ecosystem is thinner and community resources are sparser.

The practical decision for most SaaS applications: HelloSign for simplicity and cost if DocuSign's compliance certifications aren't required; DocuSign if you're targeting enterprise buyers in regulated industries (healthcare, finance, government) where DocuSign is already their standard.


Production vs. Sandbox Configuration

DocuSign developer accounts use a separate base URL from production. The most common production launch failure is leaving the sandbox URL in production configuration:

// Sandbox (development):
const BASE_PATH = 'https://demo.docusign.net/restapi';

// Production:
const BASE_PATH = 'https://www.docusign.net/restapi';

// Use environment variable to switch:
const BASE_PATH = process.env.NODE_ENV === 'production'
  ? 'https://www.docusign.net/restapi'
  : 'https://demo.docusign.net/restapi';

Go-live requires separate DocuSign approval when moving from sandbox to production. You must submit your application through DocuSign's go-live process, which includes agreeing to API terms of service and, for higher-volume integrations, completing a technical review. The sandbox account ID and production account ID are different — update your DOCUSIGN_ACCOUNT_ID environment variable when moving to production.

JWT consent must be granted by the signing user (or admin for impersonation) before JWT tokens work. In the sandbox, visit https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id={INTEGRATION_KEY}&redirect_uri={REDIRECT_URI} to grant consent. In production, use account.docusign.com. Document this consent step clearly in your deployment runbook — it's manual and must be repeated when rotating integration keys.


Methodology

DocuSign webhook configuration for high availability: the Connect webhook system delivers events at-least-once but does not guarantee ordering. If your webhook endpoint is down when an event fires, DocuSign retries with exponential backoff for up to 72 hours. To handle this reliably, always fetch the current envelope status from the API when you receive a webhook event rather than trusting the event payload alone — use the event as a trigger to call envelopesApi.getEnvelope() and store the authoritative status. This prevents stale-state bugs if webhooks arrive out of order (which happens when retries occur) and makes your webhook handler idempotent by design.

DocuSign sandbox vs production environment differences: the sandbox (demo.docusign.net) does not send real emails — all email notifications go to the sender's email address regardless of recipient configuration, which is by design to prevent accidental spam to real recipients during development. This means you cannot fully test the recipient email experience in sandbox; use the embedded signing flow for sandbox testing and verify email delivery separately in a staging environment that points to a production DocuSign account with test recipients under your control.

DocuSign eSignature REST API v2.1 documentation sourced as of March 2026; specific API method names and SDK structures follow the docusign-esign npm package v6.x. ESIGN Act legal summary is informational — verify applicability for your specific jurisdiction and document type with qualified legal counsel before relying on e-signatures in high-stakes or regulated contexts. eIDAS regulation guidance based on the official EU 910/2014 regulation text and ENISA guidance documentation. HelloSign/Dropbox Sign pricing from published pricing page March 2026. PandaDoc pricing from published pricing page March 2026. JWT authentication scope requirements (signature, impersonation) verified from DocuSign OAuth documentation. The impersonation scope allows your application to act on behalf of a DocuSign user without an interactive OAuth flow; this scope requires explicit approval from DocuSign during the go-live review process and from the user whose account you're impersonating during initial consent grant. Production go-live requirements described per DocuSign's developer center go-live documentation as of March 2026.


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

Related: How to Build an AI Chatbot with the Anthropic API, How to Build an API Abstraction Layer in Your App, How to Build an API SDK That Developers Actually Use

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.