Back to Play 5 Resources
Play 5: Client Onboarding

E-Signature Webhook Setup Guide (Adobe Sign)

Same for Adobe Sign.

E-Signature Webhook Setup Guide (Adobe Sign)

Adobe Sign webhooks

push real-time notifications to your systems when clients sign documents, complete agreements, or trigger other signature events. This eliminates polling, reduces manual status checks, and keeps your CRM
and practice management system synchronized automatically.

This guide walks you through the complete technical setup, from generating webhook

credentials to handling signature verification in production code.

What You Need Before Starting

Adobe Sign Enterprise or Business Account
You need admin access to create webhooks

. Personal accounts don't support webhook
configuration. Log in at https://secure.na1.adobesign.com/public/admin and verify you can access Account > Webhooks
.

Public HTTPS Endpoint
Adobe Sign requires a publicly accessible URL with valid SSL. Local development URLs won't work. Use ngrok for testing or deploy to a staging server. Your endpoint must respond within 10 seconds or Adobe Sign will retry.

Webhook

Secret Generator
Generate a 32-character random string for signature verification. Use this command:

openssl rand -hex 32

Store this secret in your environment variables. Never commit it to version control.

Backend Framework
This guide provides Node.js/Express examples, but the webhook

structure works with Python (Flask/FastAPI), Ruby (Rails/Sinatra), or any HTTP server framework.

Step 1: Create the Webhook
in Adobe Sign

Log in to Adobe Sign and navigate to Account > Webhooks

.

  1. Click "Create a webhook

    " in the top right.

  2. Webhook

    name: Use a descriptive identifier like production-client-onboarding or staging-engagement-letters.

  3. Webhook

    URL: Enter your public endpoint. Format: https://yourdomain.com/webhooks/adobe-sign. Adobe Sign will POST JSON payloads to this URL.

  4. Webhook

    notification URL: Leave blank unless you need a separate failure notification endpoint.

  5. Events: Select these critical events for client onboarding:

    • AGREEMENT_CREATED - Agreement sent to signers
    • AGREEMENT_ACTION_COMPLETED - Individual signer finished
    • AGREEMENT_WORKFLOW_COMPLETED - All signers completed
    • AGREEMENT_RECALLED - Agreement cancelled
    • AGREEMENT_EXPIRED - Agreement passed deadline
  6. Webhook

    scope: Choose "Account" to receive events for all agreements, or "Group" to limit to specific user groups.

  7. Conditional parameters: Leave empty for now. Use this later to filter by agreement name patterns or custom fields.

  8. Click "Save" and copy the Webhook

    ID from the confirmation screen. You'll need this for troubleshooting.

Step 2: Configure Webhook
Authentication

Adobe Sign uses HMAC-SHA256 signatures to verify webhook

authenticity.

Navigate to Account > Webhooks

, click your webhook
name, then scroll to "Webhook
signature key".

  1. Click "Generate new key" if this is your first setup.

  2. Copy the signature key. This is different from your API integration key.

  3. Store it as an environment variable:

export ADOBE_SIGN_WEBHOOK_SECRET="your_signature_key_here"

Adobe Sign sends this signature in the X-AdobeSign-ClientId header (despite the confusing name). Your code must verify every incoming request matches this signature.

Step 3: Build the Webhook
Receiver

Create an endpoint that validates signatures, parses events, and updates your systems.

Node.js/Express Implementation:

const express = require('express');
const crypto = require('crypto');
const app = express();

// Use raw body for signature verification
app.use('/webhooks/adobe-sign', express.raw({ type: 'application/json' }));

app.post('/webhooks/adobe-sign', async (req, res) => {
  const signature = req.headers['x-adobesign-clientid'];
  const webhookSecret = process.env.ADOBE_SIGN_WEBHOOK_SECRET;
  
  // Verify signature
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(req.body)
    .digest('base64');
  
  if (signature !== expectedSignature) {
    console.error('Webhook signature mismatch', {
      received: signature,
      expected: expectedSignature
    });
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Parse the verified payload
  const payload = JSON.parse(req.body.toString());
  const event = payload.event;
  const agreementId = payload.agreement?.id;
  
  try {
    switch (event) {
      case 'AGREEMENT_CREATED':
        await handleAgreementCreated(agreementId, payload);
        break;
        
      case 'AGREEMENT_ACTION_COMPLETED':
        await handleSignerCompleted(agreementId, payload);
        break;
        
      case 'AGREEMENT_WORKFLOW_COMPLETED':
        await handleAgreementCompleted(agreementId, payload);
        break;
        
      case 'AGREEMENT_RECALLED':
        await handleAgreementCancelled(agreementId, payload);
        break;
        
      case 'AGREEMENT_EXPIRED':
        await handleAgreementExpired(agreementId, payload);
        break;
        
      default:
        console.log(`Unhandled event: ${event}`);
    }
    
    // Always return 200 within 10 seconds
    res.status(200).json({ received: true });
    
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Still return 200 to prevent retries
    res.status(200).json({ received: true, error: error.message });
  }
});

async function handleAgreementCreated(agreementId, payload) {
  // Update CRM: Set opportunity status to "Awaiting Signature"
  await updateCRM({
    agreementId: agreementId,
    status: 'sent',
    sentDate: payload.agreement.createdDate,
    signers: payload.agreement.participantSets.map(p => p.memberInfos[0].email)
  });
}

async function handleSignerCompleted(agreementId, payload) {
  const completedSigner = payload.participantSet.memberInfos[0].email;
  
  // Log individual signer completion
  await logSignerActivity({
    agreementId: agreementId,
    signerEmail: completedSigner,
    completedAt: payload.actingUser.actingUserDate,
    ipAddress: payload.actingUser.ipAddress
  });
  
  // Send internal notification
  await notifyTeam(`${completedSigner} signed agreement ${agreementId}`);
}

async function handleAgreementCompleted(agreementId, payload) {
  // Download signed PDF
  const signedPdf = await downloadSignedDocument(agreementId);
  
  // Store in document management system
  await storeFinalDocument({
    agreementId: agreementId,
    fileName: payload.agreement.name,
    pdfBuffer: signedPdf,
    completedDate: payload.agreement.completedDate
  });
  
  // Update CRM: Move opportunity to "Closed Won"
  await updateCRM({
    agreementId: agreementId,
    status: 'fully_executed',
    completedDate: payload.agreement.completedDate
  });
  
  // Trigger onboarding workflow
  await startClientOnboarding(agreementId);
}

async function handleAgreementCancelled(agreementId, payload) {
  await updateCRM({
    agreementId: agreementId,
    status: 'cancelled',
    cancelledBy: payload.actingUser.email,
    cancelledDate: payload.actingUser.actingUserDate
  });
}

async function handleAgreementExpired(agreementId, payload) {
  await updateCRM({
    agreementId: agreementId,
    status: 'expired',
    expirationDate: payload.agreement.expirationDate
  });
  
  // Alert account manager
  await notifyAccountManager(agreementId, 'Agreement expired without completion');
}

app.listen(3000);

Critical Implementation Notes:

Use express.raw() instead of express.json() for the webhook

route. You need the raw body buffer to verify the signature correctly.

Always return HTTP 200, even if your internal processing fails. Returning 4xx or 5xx triggers Adobe Sign retries, which can flood your system. Log errors internally and handle them asynchronously.

Set a 9-second timeout on all database and API

calls inside webhook
handlers. Adobe Sign expects responses within 10 seconds.

Step 4: Test the Integration

Local Testing with ngrok:

ngrok http 3000

Copy the HTTPS URL (e.g., https://abc123.ngrok.io) and update your Adobe Sign webhook

URL to https://abc123.ngrok.io/webhooks/adobe-sign.

Send a Test Webhook

:

In Adobe Sign, go to your webhook

settings and click "Test webhook
". This sends a sample AGREEMENT_CREATED event. Check your server logs for the incoming payload.

End-to-End Test:

  1. Create a test agreement in Adobe Sign with your email as the signer.
  2. Sign the document.
  3. Verify your webhook
    received AGREEMENT_ACTION_COMPLETED and AGREEMENT_WORKFLOW_COMPLETED events.
  4. Check that your CRM
    or database updated correctly.

Common Issues:

Signature verification fails: Ensure you're using the raw request body, not the parsed JSON. The signature is calculated on the exact bytes Adobe Sign sent.

Webhook

times out: Adobe Sign retries failed webhooks
up to 10 times with exponential backoff. If your endpoint is slow, process events asynchronously using a job queue.

Missing events: Check your webhook

event selections in Adobe Sign. Some events (like AGREEMENT_MODIFIED) aren't enabled by default.

Step 5: Handle Webhook
Retries

Adobe Sign retries failed webhooks

(non-200 responses) with this schedule:

  • Immediate retry
  • 5 minutes
  • 15 minutes
  • 1 hour
  • 6 hours
  • 24 hours

Implement idempotency to handle duplicate events. Use the webhookNotificationId field as a unique identifier:

const processedWebhooks = new Set();

app.post('/webhooks/adobe-sign', async (req, res) => {
  const payload = JSON.parse(req.body.toString());
  const notificationId = payload.webhookNotificationId;
  
  if (processedWebhooks.has(notificationId)) {
    console.log(`Duplicate webhook ${notificationId}, skipping`);
    return res.status(200).json({ received: true, duplicate: true });
  }
  
  processedWebhooks.add(notificationId);
  // Process event...
});

For production systems, store processed notification IDs in Redis or your database with a 7-day TTL.

Step 6: Monitor Webhook
Health

Adobe Sign provides webhook

delivery logs at Account > Webhooks
> [Your Webhook
] > Activity.

Check this weekly for:

  • Failed deliveries (4xx/5xx responses)
  • Slow response times (>5 seconds)
  • Disabled webhooks
    (Adobe Sign auto-disables after 100 consecutive failures)

Set up internal monitoring:

const webhookMetrics = {
  received: 0,
  processed: 0,
  failed: 0,
  avgProcessingTime: 0
};

// Log metrics every hour
setInterval(() => {
  console.log('Webhook metrics:', webhookMetrics);
  // Send to your monitoring service (Datadog, CloudWatch, etc.)
}, 3600000);

Production Checklist

Before going live:

  • [ ] Webhook
    secret stored in environment variables, not code
  • [ ] HTTPS endpoint with valid SSL certificate
  • [ ] Signature verification implemented correctly
  • [ ] Idempotency handling for duplicate events
  • [ ] Response time under 9 seconds for all code paths
  • [ ] Error logging to track processing failures
  • [ ] Monitoring alerts for webhook
    delivery failures
  • [ ] Tested with all selected event types
  • [ ] Documented webhook
    URL and event mappings for your team

Adobe Sign webhooks

eliminate the need for polling and provide instant updates when clients interact with your agreements. Proper implementation ensures your systems stay synchronized without manual intervention.

Revenue Institute

Reviewed by Revenue Institute

This guide is actively maintained and reviewed by the implementation experts at Revenue Institute. As the creators of The AI Workforce Playbook, we test and deploy these exact frameworks for professional services firms scaling without new headcount.

Revenue Institute

Need help turning this guide into reality? Revenue Institute builds and implements the AI workforce for professional services firms.

RevenueInstitute.com