DocuSign API: Document Signing Integration 2026
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
- Go to developers.docusign.com
- Create a free developer account
- Go to Settings → Apps and Keys
- Note your Integration Key (Client ID) and Account ID
- 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
| Plan | Price | Envelopes |
|---|---|---|
| Personal | $10/month | 5/month |
| Standard | $25/user/month | Unlimited |
| Business Pro | $40/user/month | Unlimited + advanced features |
| API Plans | Custom | Volume-based |
| Developer Sandbox | Free | Unlimited (test only) |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Using demo base path in production | API calls fail | Switch to www.docusign.net for production |
| Not handling token refresh | 401 errors after 1 hour | Check expiry before each request |
| Missing consent grant | JWT auth fails | User must grant consent via OAuth URL once |
| Anchor strings not in document | Tabs don't appear | Use fixed position tabs or verify anchors |
| Not checking envelope status before signing | Errors on completed envelopes | Verify status is 'sent' before creating view |
Legal Validity and Compliance
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