
Docusign eSignature Integration 101: Implementing JWT authentication and sending your first envelope
Learn how to implement JWT authentication and send your first envelope with a Docusign Node.js integration. This step-by-step guide turns a basic app skeleton into a working prototype that can dispatch real agreements.

For many developers, the biggest hurdle in adopting a new API isn't understanding the concepts—it's crossing the gap between reading the documentation and writing that first successful line of code.
You have your environment ready and your use cases defined, but how do you actually turn those plans into a functioning application?
Welcome back to the Docusign eSignature Integration 101 series. In Part 1: Setting the Foundation, we established our development environment and cloned the application skeleton. In Part 2: Planning Your Integration, we mapped out our strategy, identifying the specific features and data flows our app requires.
Now, we are ready for the main event: implementation.
In this post, we will turn our empty React skeleton into a fully functional application that can:
Log in using Docusign JWT (JSON Web Token) Authentication.
Create an agreement in draft status.
We will take our static React skeleton and transform it into a fully interactive prototype. By the end of this tutorial, you will have tackled two of the most critical integration challenges: implementing secure JWT Authentication and writing the logic to send your first envelope via the Docusign eSignature API.
Let’s get building. You can follow along with the complete source code in the Blog-Tenant-App GitHub repository.
You'll need:
A free Docusign developer account; create one if you don't already have one.
Your Integration Key and RSA Private Key.
Step 1: Install dependencies and configure secrets
Before we write logic, we need to give our app the tools it needs to talk to Docusign.
1. Install the SDK
Navigate to your server directory and install the official Docusign Node.js SDK. This library handles all the heavy lifting for API calls.
cd server
npm install docusign-esign2. Configure your environment variables
In Part 1, you saw a .env_example file. Now we need to populate it with real credentials. Rename .env_example to .env and fill in your integration details from the Docusign Developer Dashboard:
DS_CLIENT_ID: Your Integration Key.
DS_IMPERSONATED_USER_GUID: Your User ID (found in "Apps and Keys" under My Account Information).
DS_TARGET_ACCOUNT_ID: Your API Account ID.
DS_AUTH_SERVER:
account-d.docusign.com(for developer sandbox).SESSION_SECRET: A random string to secure your sessions.
DS_SIGNER_EMAIL / NAME: (Optional) Hardcode a signer for testing purposes.
3. The private key
To use JWT Authentication, your app needs to sign requests with an RSA Private Key.
Generate an RSA Keypair in your Docusign Dashboard.
Copy the private key (including the
-----BEGIN...and-----END...lines).Create a file named
private.keyin the rootBlog-Tenant-Appfolder and paste the key there.
Tip: Ensure your private.key has proper line breaks! Pasting it as a single long line is a common cause of "PEM routines" errors.
Step 2: Implementing JWT authentication
Authentication is often the hardest part of an integration. We are using the JWT Grant flow, which is perfect for "system integrations" where the app acts on behalf of a user (impersonation).
We need to edit server/controllers/jwtController.js. Here is the logic we are implementing:
Request a Token: Use the
requestJWTUserTokenmethod from the SDK.Handle Consent: If this is the first time the user is logging in, Docusign will return a
consent_requirederror. We must catch this and redirect the user to a permission screen.Start a Session: Once we have the token, we save it in the user's session so we don't have to log in again for every request.
Here is the implementation:
JavaScript
// server/controllers/jwtController.js
const docusign = require('docusign-esign');
//const fs = require('fs');
//const path = require('path');
const login = async (req, res, next) => {
try {
const dsApi = new docusign.ApiClient();
// Set the Auth Server (account-d.docusign.com for Sandbox)
dsApi.setOAuthBasePath(process.env.DS_AUTH_SERVER);
// Read the private key file
// We go up two folders (../../) because this file is in /server/controllers
const rsaKey = fs.readFileSync(path.resolve(__dirname, '../../private.key'));
const jwtLifeSec = 10 * 60; // Token valid for 10 minutes
const scopes = 'signature impersonation';
// 1. Request the JWT Token
const results = await dsApi.requestJWTUserToken(
process.env.DS_CLIENT_ID,
process.env.DS_IMPERSONATED_USER_GUID,
scopes,
rsaKey,
jwtLifeSec
);
const accessToken = results.body.access_token;
// 2. Get User Info to find their Account ID
const userInfo = await dsApi.getUserInfo(accessToken);
// Find the account the user wants to use (or default to the first one)
const accountInfo = userInfo.accounts.find(account =>
account.accountId === process.env.DS_TARGET_ACCOUNT_ID) || userInfo.accounts[0];
// 3. Save session data so the user stays logged in
req.session.accessToken = accessToken;
req.session.accountId = accountInfo.accountId;
req.session.basePath = accountInfo.baseUri + "/restapi";
console.log("Login successful. Token received.");
// Send success status to the client
res.status(200).send("Login successful");
} catch (error) {
// 4. Handle "Consent Required" error
// If DocuSign returns this error, we must tell the Client to redirect the user
if (error.response && error.response.body && error.response.body.error === 'consent_required') {
console.log("Consent required. Redirecting client.");
const consentScopes = 'signature%20impersonation';
const redirectUri = 'http://localhost:3000';
// Construct the Consent URL
const consentUrl = `https://${process.env.DS_AUTH_SERVER}/oauth/auth?response_type=code&scope=${consentScopes}&client_id=${process.env.DS_CLIENT_ID}&redirect_uri=${redirectUri}`;
// Send special status 210 to Client (as seen in your Login.js)
res.status(210).send(consentUrl);
} else {
// Log other errors (like invalid key, wrong user ID, etc.)
console.log("Login Error:", error);
next(error);
}
}
};
/**
* Logs the user out by destroying the session.
*/
const logout = (req, res) => {
req.session = null;
console.log('Successfully logged out!');
res.status(200).send('Success: you have logged out');
};
/**
* Sends back "true" if the user is logged in, false otherwise.
*/
const isLoggedIn = (req, res) => {
let isLoggedIn;
if (req.session.isLoggedIn === undefined) {
isLoggedIn = false;
} else {
isLoggedIn = req.session.isLoggedIn;
}
res.status(200).send(isLoggedIn);
};
module.exports = {
checkToken,
login,
logout,
isLoggedIn,
};
Step 3: Creating an envelope
Now that we are authenticated, let's send a document! We will update server/controllers/sendAgreementsController.js.
In the Docusign world, an Envelope is a container that holds:
Documents: The files to be signed (PDF, HTML, DOCX).
Recipients: Who needs to sign them.
Tabs: Where they need to sign.
For tabs placement please refer this blog.
We will create a simple function that creates an envelope and saves it as draft. The signer for the envelope is assigned to the signer defined in your .env file.
JavaScript
const docusign = require('docusign-esign');
const sendAgreement = async (req, res) => {
try {
// 1. Validate Session
if (!req.session.accessToken || !req.session.accountId) {
return res.status(401).send('User is not logged in. Please login first.');
}
// 2. Define the Envelope Args
const args = {
accessToken: req.session.accessToken,
basePath: req.session.basePath,
accountId: req.session.accountId,
status: 'created',
signerEmail: process.env.DS_SIGNER_EMAIL,
signerName: process.env.DS_SIGNER_NAME,
// The public URL for the PDF
pdfUrl: "https://raw.githubusercontent.com/docusign/developer-blog-samples/refs/heads/main/tab-placement-strategy/simple_doc.pdf"
};
// 3. Create the API Client
const dsApi = new docusign.ApiClient();
dsApi.setBasePath(args.basePath);
dsApi.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
const envelopesApi = new docusign.EnvelopesApi(dsApi);
// 4. Create the Envelope Definition (Using remoteUrl)
const envelopeDefinition = makeEnvelope(args);
// 5. Call DocuSign API
const results = await envelopesApi.createEnvelope(args.accountId, {
envelopeDefinition: envelopeDefinition,
});
console.log(`Envelope created. ID: ${results.envelopeId}`);
res.status(200).json({
msg: "Agreement sent successfully!",
envelopeId: results.envelopeId
});
} catch (error) {
console.log('Error sending envelope:', error);
res.status(500).send(error.message || "Error sending agreement");
}
};
/**
* Helper function to create the envelope JSON structure
*/
function makeEnvelope(args) {
// Create the Document object
// IMPORTANT: We use 'remoteUrl' instead of 'documentBase64'
const doc1 = new docusign.Document();
doc1.remoteUrl = args.pdfUrl; // <--- The SDK accepts this property
doc1.name = 'Simple Remote Doc';
doc1.fileExtension = 'pdf'; // Still required so DocuSign knows how to render it
doc1.documentId = '1';
// Create the signer recipient
const signer1 = docusign.Signer.constructFromObject({
email: args.signerEmail,
name: args.signerName,
recipientId: '1',
routingOrder: '1',
});
// Create a SignHere tab
// (We use x/y coordinates to be safe since we are using a remote doc)
const signHere1 = docusign.SignHere.constructFromObject({
documentId: '1',
pageNumber: '1',
xPosition: '100',
yPosition: '150'
});
// Add the tab to the signer
const signer1Tabs = docusign.Tabs.constructFromObject({
signHereTabs: [signHere1],
});
signer1.tabs = signer1Tabs;
// Add the recipients to the envelope object
const recipients = docusign.Recipients.constructFromObject({
signers: [signer1],
});
// Create the final EnvelopeDefinition
return docusign.EnvelopeDefinition.constructFromObject({
emailSubject: 'Please sign this remote document',
documents: [doc1],
recipients: recipients,
status: args.status,
});
}
module.exports = { sendAgreement };
Step 4: Connecting the frontend
Finally, we update our React frontend to trigger these actions. In client/src/pages/Home.js, we add a button that calls our new /api/sendAgreement endpoint.
JavaScript
// client/src/pages/Home.js snippet
const handleSendAgreement = async () => {
setLoading(true);
setMessage('');
try {
// Calls the backend API to send the envelope
const response = await axios.post('/api/sendAgreement');
if (response.status === 200) {
setMessage(`Success! Envelope ID: ${response.data.envelopeId}`);
}
} catch (error) {
console.error(error);
if (error.response && error.response.status === 401) {
setMessage("Session expired. Please log in again.");
setTimeout(() => navigate('/'), 2000);
} else {
setMessage("Error sending agreement. Check console for details.");
}
} finally {
setLoading(false);
}
};
Step 4: Connecting the frontend
Finally, we update our React frontend to trigger these actions. In client/src/pages/Home.js, we add a button that calls our new /api/sendAgreement endpoint.
// client/src/pages/Home.js snippet
const handleSendAgreement = async () => {
setLoading(true);
setMessage('');
try {
// Calls the backend API to send the envelope
const response = await axios.post('/api/sendAgreement');
if (response.status === 200) {
setMessage(`Success! Envelope ID: ${response.data.envelopeId}`);
}
} catch (error) {
console.error(error);
if (error.response && error.response.status === 401) {
setMessage("Session expired. Please log in again.");
setTimeout(() => navigate('/'), 2000);
} else {
setMessage("Error sending agreement. Check console for details.");
}
} finally {
setLoading(false);
}
};
The result
Run npm run dev and navigate to localhost:3000.
You should be able see the UI as shown below:

Click Login using JWT.
If this is your first time, you will be redirected to Docusign to click Accept.
Once logged in, you will see your new Home screen.

4. Click Create Agreement.
Check the drafts folder in Docusign UI. You should be able to see your first envelope.
Congratulations! You have successfully crossed the most challenging phase of your integration journey.
By completing this tutorial, you have transformed a static code skeleton into a living, breathing application. You now have a secure authentication layer using JWT Grant, and a robust backend capability to dispatch agreements using the eSignature REST API.
We now have a working integration that can authenticate and send documents. In the next post, we will explore Webhooks (Docusign Connect) to learn how to update our app's UI automatically when the user signs the document, rather than waiting for us to refresh the page.
Resources

Abhay Kumar Nelaturu is an experienced technical engineer with a background in Computer Science Engineering, specializing in designing and implementing end-to-end integrations. Proficient in the pre-sales and deployment lifecycle, his expertise includes technical discovery, devising solution architecture, and customer on-boarding. His career is built on a strong foundation as a Salesforce developer, including extensive experience building compelling platform solutions.
Related posts
Docusign IAM is the agreement platform your business needs



