Skip to main content
Blog

Docusign eSignature Integration 101: Implementing JWT authentication and sending your first envelope

Author Abhay Kumar Nelaturu
Abhay Kumar NelaturuPartner Solution Architect
Summary5 min read

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.

Table of contents

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:

  1. Log in using Docusign JWT (JSON Web Token) Authentication.

  2. 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:

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-esign

2. 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.

  1. Generate an RSA Keypair in your Docusign Dashboard.

  2. Copy the private key (including the -----BEGIN... and -----END... lines).

  3. Create a file named private.key in the root Blog-Tenant-App folder 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:

  1. Request a Token: Use the requestJWTUserToken method from the SDK.

  2. Handle Consent: If this is the first time the user is logging in, Docusign will return a consent_required error. We must catch this and redirect the user to a permission screen.

  3. 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:

  1. Documents: The files to be signed (PDF, HTML, DOCX).

  2. Recipients: Who needs to sign them.

  3. 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:

  1. Click Login using JWT.

  2. If this is your first time, you will be redirected to Docusign to click Accept.

  3. 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

Author Abhay Kumar Nelaturu
Abhay Kumar NelaturuPartner Solution Architect

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.

More posts from this author

Related posts

  • Developers

    Docusign Developers 2025 wrapped: The year of innovation

    Author Julian Macagno
    Julian Macagno
    Docusign Developers 2025 wrapped: The year of innovation

Docusign IAM is the agreement platform your business needs

Start for FreeExplore Docusign IAM
Person smiling while presenting