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}/oauth2/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

// X-Propper-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
function verifySignature(payload, header, secret, toleranceSeconds = 300) {
const parts = (header || '').split(',').map((part) => part.split('='));
const timestamp = parts.find(([key]) => key === 't')?.[1];
const signatures = parts.filter(([key]) => key === 'v1').map(([, value]) => value);
const ts = Number(timestamp);
if (!timestamp || !Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) {
return false;
}

const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest();
return signatures.some((signature) => {
const received = Buffer.from(signature, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}

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('utf8');

if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
console.warn('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}

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

// agreementId is the same ID as envelopeId in the envelopes API
const { agreementId, recipient, reason } = event.data;

switch (event.type) {
case 'agreement.sent':
console.log(`Agreement ${agreementId} sent to recipients`);
break;

case 'agreement.viewed':
console.log(`Agreement ${agreementId} viewed by ${recipient?.email}`);
break;

case 'agreement.recipient_signed':
console.log(`Agreement ${agreementId} signed by ${recipient?.email}`);
break;

case 'agreement.completed':
console.log(`Agreement ${agreementId} completed!`);
// Download the signed document
await downloadSignedDocument(agreementId, `./signed-${agreementId}.pdf`);
break;

case 'agreement.declined':
console.log(`Agreement ${agreementId} declined: ${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 the signing page:
Webhook received: agreement.viewed
Agreement abc123-def456-ghi789 viewed by client@example.com

# When signer completes:
Webhook received: agreement.recipient_signed
Agreement abc123-def456-ghi789 signed by client@example.com

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

Next Steps​