Skip to main content

Basic Signing Workflow

A complete, end-to-end example of sending a document for signature and handling the response.

Overview

This example demonstrates:

  1. Creating an envelope with a PDF document
  2. Adding a signer with signature fields
  3. Sending for signature via email
  4. Tracking status via webhook
  5. Downloading the signed document

Prerequisites

  • Propper API credentials (client ID and secret)
  • A PDF document to send for signing
  • A webhook endpoint to receive events

Complete Code Example

// sign-workflow.js
import fs from 'fs/promises';
import express from 'express';
import crypto from 'crypto';

const BASE_URL = 'https://api.propper.ai/restapi/v2.1';
const AUTH_URL = 'https://auth.propper.ai';
const ACCOUNT_ID = process.env.PROPPER_ACCOUNT_ID;

// Token management
let tokenCache = null;

async function getAccessToken() {
if (tokenCache && tokenCache.expiresAt > Date.now()) {
return tokenCache.token;
}

const response = await fetch(`${AUTH_URL}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.PROPPER_CLIENT_ID,
client_secret: process.env.PROPPER_CLIENT_SECRET,
scope: 'sign:read sign:write',
}),
});

if (!response.ok) {
throw new Error('Failed to get access token');
}

const { access_token, expires_in } = await response.json();

tokenCache = {
token: access_token,
expiresAt: Date.now() + (expires_in * 1000) - 60000, // 1 min buffer
};

return access_token;
}

// API helper
async function apiRequest(path, options = {}) {
const token = await getAccessToken();

const response = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});

if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.errorCode} - ${error.message}`);
}

// Handle 204 No Content
if (response.status === 204) {
return null;
}

return response.json();
}

// Step 1: Create and send envelope
async function sendForSignature(pdfPath, signerEmail, signerName) {
// Read PDF and convert to base64
const pdfBuffer = await fs.readFile(pdfPath);
const documentBase64 = pdfBuffer.toString('base64');

const envelope = await apiRequest(`/accounts/${ACCOUNT_ID}/envelopes`, {
method: 'POST',
body: JSON.stringify({
emailSubject: 'Please sign: Service Agreement',
emailBlurb: 'Please review and sign this agreement at your earliest convenience.',
status: 'sent', // 'created' for draft, 'sent' to send immediately
documents: [
{
documentId: '1',
name: 'Service Agreement.pdf',
documentBase64,
order: '1',
},
],
recipients: {
signers: [
{
email: signerEmail,
name: signerName,
recipientId: '1',
routingOrder: '1',
tabs: {
signHereTabs: [
{
documentId: '1',
pageNumber: '1',
xPosition: '100',
yPosition: '600',
anchorString: '/sig1/', // Alternative: use anchor text
},
],
dateSignedTabs: [
{
documentId: '1',
pageNumber: '1',
xPosition: '300',
yPosition: '600',
},
],
},
},
],
},
}),
});

console.log(`Envelope created and sent: ${envelope.envelopeId}`);
console.log(`Status: ${envelope.status}`);

return envelope;
}

// Step 2: Check envelope status
async function getEnvelopeStatus(envelopeId) {
const envelope = await apiRequest(
`/accounts/${ACCOUNT_ID}/envelopes/${envelopeId}?include=recipients`
);

console.log(`Envelope ${envelopeId}:`);
console.log(` Status: ${envelope.status}`);
console.log(` Recipients:`);

if (envelope.recipients?.signers) {
for (const signer of envelope.recipients.signers) {
console.log(` - ${signer.name} (${signer.email}): ${signer.status}`);
}
}

return envelope;
}

// Step 3: Download signed document
async function downloadSignedDocument(envelopeId, outputPath) {
const token = await getAccessToken();

const response = await fetch(
`${BASE_URL}/accounts/${ACCOUNT_ID}/envelopes/${envelopeId}/documents/combined`,
{
headers: { 'Authorization': `Bearer ${token}` },
}
);

if (!response.ok) {
throw new Error('Failed to download document');
}

const buffer = Buffer.from(await response.arrayBuffer());
await fs.writeFile(outputPath, buffer);

console.log(`Signed document saved to: ${outputPath}`);
}

// Step 4: Webhook handler
function createWebhookServer(port = 3000) {
const app = express();

app.post('/webhooks/sign', express.raw({ type: 'application/json' }), async (req, res) => {
// Verify webhook signature
const signature = req.headers['x-propper-signature'];
const payload = req.body.toString();

const expectedSignature = `sha256=${crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex')}`;

if (signature !== expectedSignature) {
console.warn('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}

const event = JSON.parse(payload);
console.log(`Webhook received: ${event.type}`);

switch (event.type) {
case 'document.sent':
console.log(`Document ${event.data.document.id} sent to recipients`);
break;

case 'document.viewed':
console.log(`Document ${event.data.document.id} viewed by ${event.data.signer?.email}`);
break;

case 'document.signed':
console.log(`Document ${event.data.document.id} signed by ${event.data.signer?.email}`);
break;

case 'document.completed':
console.log(`Document ${event.data.document.id} completed!`);
// Download the signed document
await downloadSignedDocument(
event.data.document.id,
`./signed-${event.data.document.id}.pdf`
);
break;

case 'document.declined':
console.log(`Document ${event.data.document.id} declined: ${event.data.reason}`);
break;
}

res.status(200).send('OK');
});

app.listen(port, () => {
console.log(`Webhook server listening on port ${port}`);
});

return app;
}

// Main execution
async function main() {
try {
// Start webhook server
createWebhookServer(3000);

// Send document for signature
const envelope = await sendForSignature(
'./contract.pdf',
'client@example.com',
'John Smith'
);

// Check status after a delay (or use webhooks instead)
setTimeout(async () => {
await getEnvelopeStatus(envelope.envelopeId);
}, 5000);

} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}

main();

Environment Setup

Create a .env file:

PROPPER_ACCOUNT_ID=your-organization-uuid
PROPPER_CLIENT_ID=your-client-id
PROPPER_CLIENT_SECRET=your-client-secret
WEBHOOK_SECRET=your-webhook-secret

Running the Example

# Install dependencies
npm install express

# Run
node sign-workflow.js

Expected Output

Webhook server listening on port 3000
Envelope created and sent: abc123-def456-ghi789
Status: sent

Envelope abc123-def456-ghi789:
Status: sent
Recipients:
- John Smith (client@example.com): sent

# When signer opens email:
Webhook received: document.viewed
Document abc123-def456-ghi789 viewed by client@example.com

# When signer completes:
Webhook received: document.signed
Document abc123-def456-ghi789 signed by client@example.com

Webhook received: document.completed
Document abc123-def456-ghi789 completed!
Signed document saved to: ./signed-abc123-def456-ghi789.pdf

Next Steps