Test DocuSign Connect webhooks locally
Receive DocuSign Connect events on localhost with a Horizon tunnel, and verify the HMAC signature in a Next.js route handler.
Receive DocuSign Connect events on your laptop while you build, with a public HTTPS URL DocuSign can reach.
Horizon has no DocuSign integration. Connect sends webhooks to a public URL, and Horizon provides that URL.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - A DocuSign developer (demo) account with administrator access to Connect
- A Next.js app that uses the App Router and runs on port 3000
Start your app
When you turn on HMAC for a Connect configuration, DocuSign computes an HMAC-SHA256 (a keyed hash) of the raw request body with your secret key. It Base64-encodes the result and sends it in X-DocuSign-Signature-1. If you have several active keys, DocuSign sends one header per key: X-DocuSign-Signature-2, and so on. A delivery is valid when any one of them matches your key.
Create a route handler. Read the raw body with request.text(). Don't parse it first, because the signature covers the exact bytes.
import { createHmac, timingSafeEqual } from "node:crypto";
const SIGNATURE_HEADER_PREFIX = "x-docusign-signature-";
export async function POST(request: Request) {
const secret = process.env.DOCUSIGN_CONNECT_HMAC_KEY;
if (!secret) {
return new Response("Missing DOCUSIGN_CONNECT_HMAC_KEY", { status: 500 });
}
const body = await request.text();
const expected = createHmac("sha256", secret).update(body).digest();
const signatures = [...request.headers]
.filter(([name]) => name.startsWith(SIGNATURE_HEADER_PREFIX))
.map(([, value]) => Buffer.from(value, "base64"));
const isValid = signatures.some(
(signature) =>
signature.length === expected.length && timingSafeEqual(signature, expected),
);
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}
const payload = JSON.parse(body);
console.log(`Received DocuSign event: ${payload.event}`);
return new Response("ok", { status: 200 });
}Reply with 200 quickly. DocuSign logs a failure for any other status.
You get the key in a later step. Add it to .env.local once you have it:
DOCUSIGN_CONNECT_HMAC_KEY=replace-with-your-connect-keyStart the app:
npm run devStart a tunnel
In a second terminal, open a tunnel to port 3000 on a subdomain you reserved, with -s:
hrzn tunnel http://localhost:3000 -s my-docusign-appHORIZON: Tunnel connected
URL https://my-docusign-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-docusign-appYour public URL is https://my-docusign-app.hrzn.run. Without -s the subdomain is random and changes on every run, so your Connect configuration would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
Create a Connect key
- Sign in to DocuSign and open Settings.
- Under Integrations, select Connect.
- Select Connect Keys.
- Select Add Secret Key.
- Copy the key into
.env.localasDOCUSIGN_CONNECT_HMAC_KEY, then restartnpm run dev.
DocuSign shows the key value only when you create it. Copy it now.
Add the Connect configuration
Your endpoint URL is the tunnel URL plus the route path: https://my-docusign-app.hrzn.run/api/webhooks/docusign.
- On the Connect page, select Add Configuration, then Custom.
- Enter a name for the configuration.
- Enter the endpoint URL as URL to Publish.
- Select Include HMAC Signature.
- Select the envelope events you want, and choose JSON as the data format. The handler parses JSON.
- Save the configuration.
Trigger an event
Send an envelope from your demo account and complete it. DocuSign then posts the matching event to your URL.
To resend an event, open Settings, Connect, then Publish. DocuSign lists envelopes with recent events there, and an admin can republish them. This works for deliveries that failed, or for envelopes you want to send again.
Check it works
After the envelope event fires, the Horizon terminal prints one line:
POST 200 /api/webhooks/docusignYour app terminal prints:
Received DocuSign event: envelope-completedTo see DocuSign's side, open Settings, Connect, then Logs. It shows the most recent 100 logs.
Troubleshooting
The signature doesn't match
The Horizon line shows [401]. Check these in order:
DOCUSIGN_CONNECT_HMAC_KEYmatches the key you created under Connect Keys. Restartnpm run devafter you edit.env.local.- The handler hashes the raw body. DocuSign says to treat the payload as bytes. Reading it as a parsed object or re-serializing it makes the hash differ.
- The signature is Base64, not hex. Compare decoded bytes, as the handler does.
- Include HMAC Signature is selected on the configuration. Without it, DocuSign sends no signature headers.
Only one of two keys works
When you have several active keys, DocuSign sends one signature header per key. The handler above checks every X-DocuSign-Signature-N header. A handler that reads only X-DocuSign-Signature-1 fails when you rotate keys.
DocuSign reports failures and nothing arrives
- Check that the Horizon terminal and
npm run devare both running. - Check that URL to Publish ends with
/api/webhooks/docusign. - Open Settings, Connect, then Logs to read the error DocuSign recorded.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-docusign-app and update URL to Publish if it differs.
Next steps
- Read DocuSign's guide to HMAC security for Connect.
- Reserve a subdomain so your URL never changes: see pricing.
Test Contentful webhooks locally
Receive Contentful webhook events on localhost with a Horizon tunnel, and verify the signed request with verifyRequest.
Test Dropbox webhooks locally
Receive Dropbox webhook notifications on localhost with a Horizon tunnel, answer the challenge request, and verify X-Dropbox-Signature.