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
- 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 |
Need e-signatures? Compare DocuSign vs HelloSign vs PandaDoc on APIScout — signing APIs, pricing, and integration complexity.