This guide explains how to set up a backend that powers in-app DocuSign signing using the react-native-docusign package. The mobile app handles the native signing UI; your backend handles authentication, envelope creation, and credential management.
The mobile app does not handle DocuSign credentials directly. Your backend mints a short-lived access token, creates the envelope, and returns a session payload. The app feeds this payload to the native DocuSign SDK, which opens a fully native PDF viewer + signature capture screen (no WebView).
Mobile App Your Backend DocuSign
| | |
| 1. POST /signing-session | |
| { your business params } | |
|----------------------------->| |
| | 2. JWT Bearer Grant |
| | (RSA key, scope=signature |
| | impersonation) |
| |----------------------------->|
| | 3. { access_token } |
| |<-----------------------------|
| | |
| | 4. GET /oauth/userinfo |
| |----------------------------->|
| | 5. { sub, accounts[] } |
| |<-----------------------------|
| | |
| | 6. POST /envelopes |
| | (template + embedded |
| | signer with clientUserId) |
| |----------------------------->|
| | 7. { envelopeId } |
| |<-----------------------------|
| | |
| 8. Session payload (13 fields)| |
|<-----------------------------| |
| | |
| 9. SDK: initialize() | |
| 10. SDK: login(token) | |
| 11. SDK: presentCaptive | |
| Signing(envelope) | |
| | |
| 12. Native signing UI opens | |
|----------------------------------------------------->|
| | |
| User signs | |
| | |
| 13. onSigningComplete | |
| (UX confirmation only) | |
| | 14. DocuSign Connect webhook |
| |<-----------------------------|
| | 15. Update your DB |
- A DocuSign developer account (free)
- An Integration Key (client_id) created in DocuSign Admin > Apps and Keys
- An RSA keypair generated and the public key uploaded to your integration
- User consent granted for
signature impersonationscope (one-time consent URL) - Mobile SDK access enabled for your integration key (may require contacting DocuSign support to register your iOS bundle ID and Android package name)
- The
react-native-docusignpackage installed in your mobile app
Your backend endpoint must return this exact JSON shape. The mobile app validates it against a Zod schema at runtime.
{
"integratorKey": "your-integration-key-guid",
"environment": "demo",
"accessToken": "eyJ0eXAiOiJNVCIs...",
"expiresIn": 3600,
"host": "https://demo.docusign.net/restapi",
"accountId": "your-docusign-account-guid",
"userId": "impersonated-user-guid",
"userName": "Service Account",
"email": "service@example.com",
"envelopeId": "created-envelope-guid",
"clientUserId": "YOUR_APP_USER_123",
"recipientName": "John Doe",
"recipientEmail": "john@example.com"
}| Field | Type | Description |
|---|---|---|
integratorKey |
string | Your DocuSign Integration Key (client_id). Used by the SDK for initialize(). |
environment |
"demo" or "production" |
Determines which DocuSign environment the SDK connects to. |
accessToken |
string | Short-lived DocuSign access token minted via JWT Bearer Grant. |
expiresIn |
number | Token lifetime in seconds. Keep short (recommended ≤ 900). |
host |
string (URL) | DocuSign REST API base URL (e.g. https://demo.docusign.net/restapi). |
accountId |
string | DocuSign account GUID. |
userId |
string | DocuSign user GUID (the impersonated user). |
userName |
string | Display name of the impersonated user. |
email |
string | Email of the impersonated user. |
envelopeId |
string | The envelope GUID created by your backend. |
clientUserId |
string | Identifies the signer as "embedded." MUST match the value set on the envelope recipient. |
recipientName |
string | Signer's name. MUST exactly match the recipient on the envelope. |
recipientEmail |
string | Signer's email. MUST exactly match the recipient on the envelope. |
Store these in a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.). Never commit them to source control.
- Integration Key (client_id): GUID from DocuSign Admin
- RSA private key: the private half of the keypair you uploaded to DocuSign
- Impersonated User ID: the DocuSign user GUID your app acts on behalf of
Build a JWT assertion, sign it with your RSA private key, and exchange it for an access token.
import { ApiClient } from 'docusign-esign';
import fs from 'fs';
const INTEGRATION_KEY = process.env.DOCUSIGN_INTEGRATION_KEY;
const USER_ID = process.env.DOCUSIGN_USER_ID;
const RSA_PRIVATE_KEY = fs.readFileSync(process.env.DOCUSIGN_RSA_KEY_PATH);
const apiClient = new ApiClient();
apiClient.setOAuthBasePath('account-d.docusign.com'); // or 'account.docusign.com' for production
const tokenResponse = await apiClient.requestJWTUserToken(
INTEGRATION_KEY,
USER_ID,
['signature', 'impersonation'],
RSA_PRIVATE_KEY,
3600 // token lifetime in seconds
);
const accessToken = tokenResponse.body.access_token;
const expiresIn = tokenResponse.body.expires_in;Manual approach (without the SDK, using jsonwebtoken + axios):
import jwt from 'jsonwebtoken';
import axios from 'axios';
const now = Math.floor(Date.now() / 1000);
const assertion = jwt.sign(
{
iss: INTEGRATION_KEY,
sub: USER_ID,
aud: 'account-d.docusign.com',
iat: now,
exp: now + 3600,
scope: 'signature impersonation',
},
RSA_PRIVATE_KEY,
{ algorithm: 'RS256' }
);
const { data } = await axios.post('https://account-d.docusign.com/oauth/token', null, {
params: {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion,
},
});
const accessToken = data.access_token;
const expiresIn = data.expires_in;Cache the token until it expires. Do not mint a new one per request.
Call /oauth/userinfo to get the account ID, base URI, and user details.
const userInfoResponse = await apiClient.getUserInfo(accessToken);
const account = userInfoResponse.accounts.find(a => a.isDefault) ?? userInfoResponse.accounts[0];
const basePath = `${account.baseUri}/restapi`;Cache this per account. The baseUri varies by region (e.g. na2.docusign.net, eu.docusign.net, au.docusign.net). Never hardcode it.
The signer must have clientUserId set. This marks them as "embedded" (captive). Without it, DocuSign treats the signer as "remote" and sends them an email/SMS link instead. The SDK cannot open remote-signer envelopes.
import { EnvelopesApi } from 'docusign-esign';
apiClient.setBasePath(basePath);
apiClient.addDefaultHeader('Authorization', `Bearer ${accessToken}`);
const envelopesApi = new EnvelopesApi(apiClient);
const envelopeResult = await envelopesApi.createEnvelope(account.accountId, {
envelopeDefinition: {
emailSubject: 'Please sign this document',
status: 'sent',
documents: [{
documentBase64: documentBase64Content,
name: 'document.pdf',
fileExtension: 'pdf',
documentId: '1',
}],
recipients: {
signers: [{
email: recipientEmail,
name: recipientName,
recipientId: '1',
routingOrder: '1',
clientUserId: clientUserId, // required for embedded signing
tabs: {
signHereTabs: [{
documentId: '1',
pageNumber: '1',
xPosition: '100',
yPosition: '600',
}],
},
}],
},
},
});
const envelopeId = envelopeResult.envelopeId;You can also use DocuSign templates instead of inline documents:
const envelopeResult = await envelopesApi.createEnvelope(account.accountId, {
envelopeDefinition: {
emailSubject: 'Please sign this document',
status: 'sent',
templateId: 'your-template-guid',
templateRoles: [{
name: recipientName,
email: recipientEmail,
roleName: 'Signer', // must match the role name in the template
clientUserId: clientUserId,
}],
},
});return {
integratorKey: INTEGRATION_KEY,
environment: 'demo', // or 'production'
accessToken,
expiresIn: Math.min(expiresIn, 900),
host: basePath,
accountId: account.accountId,
userId: userInfoResponse.sub,
userName: userInfoResponse.name,
email: userInfoResponse.email,
envelopeId,
clientUserId,
recipientName,
recipientEmail,
};The mobile app's onSigningComplete event is a UX hint only, not an authoritative completion signal. Set up a DocuSign Connect webhook to receive envelope status events on your backend.
Events to listen for: sent, delivered, completed, declined, voided.
// POST /webhooks/docusign (your webhook endpoint)
app.post('/webhooks/docusign', (req, res) => {
const event = req.body;
const envelopeId = event.envelopeId;
const status = event.status; // 'completed', 'declined', 'voided', etc.
// Update your database
await db.envelopes.updateStatus(envelopeId, status);
// Optionally send push notification to the user
if (status === 'completed') {
await pushNotifications.send(userId, 'Your document has been signed.');
}
res.sendStatus(200);
});The first time your integration key impersonates a user, DocuSign requires the user to grant consent. Open this URL in a browser (once per user):
https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id=YOUR_INTEGRATION_KEY&redirect_uri=YOUR_REDIRECT_URI
For production, replace account-d with account.
After the user grants consent, JWT Bearer Grant works for that user without further interaction.
import express from 'express';
import { ApiClient, EnvelopesApi } from 'docusign-esign';
import fs from 'fs';
const app = express();
app.use(express.json());
const INTEGRATION_KEY = process.env.DOCUSIGN_INTEGRATION_KEY!;
const USER_ID = process.env.DOCUSIGN_USER_ID!;
const RSA_KEY = fs.readFileSync(process.env.DOCUSIGN_RSA_KEY_PATH!);
const ENVIRONMENT = (process.env.DOCUSIGN_ENV as 'demo' | 'production') ?? 'demo';
const AUTH_SERVER = ENVIRONMENT === 'demo' ? 'account-d.docusign.com' : 'account.docusign.com';
let cachedToken: { token: string; expiresAt: number } | null = null;
let cachedAccount: { accountId: string; basePath: string; userId: string; userName: string; email: string } | null = null;
async function getAccessToken(): Promise<{ accessToken: string; expiresIn: number }> {
if (cachedToken && Date.now() < cachedToken.expiresAt) {
const remaining = Math.floor((cachedToken.expiresAt - Date.now()) / 1000);
return { accessToken: cachedToken.token, expiresIn: remaining };
}
const apiClient = new ApiClient();
apiClient.setOAuthBasePath(AUTH_SERVER);
const response = await apiClient.requestJWTUserToken(
INTEGRATION_KEY, USER_ID, ['signature', 'impersonation'], RSA_KEY, 3600
);
const accessToken = response.body.access_token;
const expiresIn = response.body.expires_in;
cachedToken = { token: accessToken, expiresAt: Date.now() + (expiresIn - 60) * 1000 };
return { accessToken, expiresIn };
}
async function getAccountInfo(accessToken: string) {
if (cachedAccount) return cachedAccount;
const apiClient = new ApiClient();
apiClient.setOAuthBasePath(AUTH_SERVER);
const userInfo = await apiClient.getUserInfo(accessToken);
const account = userInfo.accounts.find(a => a.isDefault) ?? userInfo.accounts[0];
cachedAccount = {
accountId: account.accountId,
basePath: `${account.baseUri}/restapi`,
userId: userInfo.sub,
userName: userInfo.name,
email: userInfo.email,
};
return cachedAccount;
}
app.post('/api/docusign/signing-session', async (req, res) => {
try {
const { documentBase64, recipientName, recipientEmail, clientUserId, emailSubject } = req.body;
// 1. Get access token
const { accessToken, expiresIn } = await getAccessToken();
// 2. Resolve account
const account = await getAccountInfo(accessToken);
// 3. Create envelope with embedded signer
const apiClient = new ApiClient();
apiClient.setBasePath(account.basePath);
apiClient.addDefaultHeader('Authorization', `Bearer ${accessToken}`);
const envelopesApi = new EnvelopesApi(apiClient);
const envelopeResult = await envelopesApi.createEnvelope(account.accountId, {
envelopeDefinition: {
emailSubject: emailSubject ?? 'Please sign this document',
status: 'sent',
documents: [{
documentBase64,
name: 'document.pdf',
fileExtension: 'pdf',
documentId: '1',
}],
recipients: {
signers: [{
email: recipientEmail,
name: recipientName,
recipientId: '1',
routingOrder: '1',
clientUserId,
tabs: {
signHereTabs: [{ documentId: '1', pageNumber: '1', xPosition: '100', yPosition: '600' }],
},
}],
},
},
});
// 4. Return session payload
res.json({
integratorKey: INTEGRATION_KEY,
environment: ENVIRONMENT,
accessToken,
expiresIn: Math.min(expiresIn, 900),
host: account.basePath,
accountId: account.accountId,
userId: account.userId,
userName: account.userName,
email: account.email,
envelopeId: envelopeResult.envelopeId,
clientUserId,
recipientName,
recipientEmail,
});
} catch (error) {
console.error('Signing session error:', error);
res.status(502).json({ error: 'Failed to create signing session' });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));Once your backend returns the session payload, the mobile app uses it like this:
import { initialize, loginWithAccessToken, presentCaptiveSigning } from 'react-native-docusign';
// 1. Fetch session from your backend
const session = await api.post('/api/docusign/signing-session', {
documentBase64,
recipientName: 'John Doe',
recipientEmail: 'john@example.com',
clientUserId: 'USER_123',
});
// 2. Initialize SDK (once per cold start)
await initialize({
integratorKey: session.integratorKey,
environment: session.environment,
});
// 3. Login with backend-minted token
await loginWithAccessToken({
accessToken: session.accessToken,
accountId: session.accountId,
userId: session.userId,
userName: session.userName,
email: session.email,
host: session.host,
expiresIn: session.expiresIn,
});
// 4. Open native signing UI
const result = await presentCaptiveSigning({
envelopeId: session.envelopeId,
recipientUserName: session.recipientName,
recipientEmail: session.recipientEmail,
recipientClientUserId: session.clientUserId,
});
// result.status: 'completed' | 'cancelled' | 'error'- RSA private key stored in secrets manager, never in source control
- Access tokens short-lived (≤ 15 min recommended)
- Backend authorizes every request (user owns the envelope / resource)
-
clientUserId,recipientName,recipientEmailresolved server-side, not trusted from client - DocuSign Connect webhook set up for authoritative envelope completion
- Rate limiting on the signing-session endpoint
| Environment | Auth Server | REST Base |
|---|---|---|
| Demo / Development | account-d.docusign.com |
https://demo.docusign.net/restapi |
| Production | account.docusign.com |
per-account base_uri from /oauth/userinfo + /restapi |
Not all DocuSign tab types render in the mobile SDK. Plan your templates accordingly.
Supported: Signature, Initials, First Name, Last Name, Text, Full Name, Company, Title, Date Signed, Checkbox, List, Radio.
Unsupported: Email, Approve, Decline, Date, Formula, Envelope Id, Note, Notarize, Number, Signer Attachment, SSN, View, Zip, Email address.
| Error | Cause | Fix |
|---|---|---|
login_failed: unauthorized (token valid via /oauth/userinfo) |
Mobile SDK not enabled for integration key, or bundle ID / package name not registered | Contact DocuSign support to enable Mobile SDK access |
login_failed: unauthorized (token rejected by /oauth/userinfo) |
Token expired or minted with wrong scope | Re-mint with scope=signature impersonation |
consent_required on JWT grant |
User has not granted impersonation consent | Open consent URL in browser |
RECIPIENT_NOT_IN_ENVELOPE |
clientUserId / recipientName / recipientEmail mismatch |
Ensure values exactly match the envelope signer |
ENVELOPE_IS_NOT_IN_CORRECT_STATE |
Envelope status is not sent |
Ensure status: 'sent' when creating the envelope |