# Send Docusign Envelope-Completed Alerts to Your Team's Phones

> Verify one Docusign Connect completion event, keep agreement details out of the alert, and send one fixed signal to a shared PingRoom Room.

- Published: 2026-10-30
- Author: PingRoom
- Category: Developers
- Topics: Docusign envelope completed notification, Docusign Connect webhook phone alert, envelope-completed webhook
- Canonical: https://pingroom.io/blog/docusign-envelope-completed-push-notifications
- Cover image: https://pingroom.io/blog/docusign-envelope-completed-push-notifications-cover.webp
- Cover description: Pingo and two PingRoom avatars watch an unlettered clay envelope pass through a brass verification ring into a completion tray.

To send a Docusign envelope-completed alert to a team's phones, configure a Docusign Connect JSON SIM webhook for `envelope-completed`, verify its HMAC over the exact request body, accept only the intended Connect configuration and account, queue a derived envelope event ID, then send fixed copy to a PingRoom incoming webhook.

That gives a shared Room one small completion signal. Docusign remains the place to inspect the envelope, recipients, signatures, documents, and Connect delivery history.

If Docusign's own email or mobile notifications already reach the right person, use them. Add this relay when a bounded group needs the same completion signal in one Room, without forwarding agreement details onto lock screens.

## Decide what the Ping is allowed to say

An envelope-completed event can carry more context than a phone alert needs. This recipe deliberately sends only:

- **Title:** Agreement completed
- **Message:** Open Docusign to review the completed envelope

It does not forward the document title, email subject, sender, recipient, signer, custom fields, or document contents. It also does not download the completed document.

The alert means the selected Docusign account emitted `envelope-completed`. It is not legal advice, proof of enforceability, proof that a human reviewed the final document, authorization to act, or a guarantee that every phone displayed the notification.

## What you need

- Docusign API and Connect access for the account you will monitor. Availability varies by Docusign account and plan, so confirm it in your account.
- A public HTTPS receiver and a durable queue.
- A private PingRoom Room whose members should receive the signal.
- PingRoom Pro. A Room can have [up to four incoming webhooks](/features/incoming-webhook).

Create the PingRoom webhook from the Room's settings and save its secret URL in your server-side secret store. The URL is a credential: do not put it in browser code, source control, screenshots, or Docusign event data. The [webhook reference](/documents/webhooks) documents the current request fields, cooldown, idempotency, and error behavior.

## Configure one narrow Docusign event

In Docusign Connect, choose the JSON SIM event model, select only `envelope-completed`, and enable HMAC signing. Leave optional envelope summary data off unless another consumer truly needs it. This relay uses only the account ID, configuration ID, and envelope ID.

Docusign documents account-level, envelope-level, and recipient-level ways to request event notifications. Use the narrowest configuration that matches your workflow. Record these values as server-side configuration:

```text
DOCUSIGN_CONNECT_CONFIGURATION_ID=24680
DOCUSIGN_ACCOUNT_ID=12345678
DOCUSIGN_HMAC_SECRETS=current-key,previous-key-during-rotation
PINGROOM_WEBHOOK_URL=https://api.pingroom.io/api/webhooks/...
```

Do not treat the example IDs as real credentials. During HMAC key rotation, accept the current and immediately previous keys only for the planned overlap, then remove the old key.

## Verify, filter, and queue before acknowledging

Docusign computes HMAC-SHA256 over the unmodified request body and sends the Base64 digest in a numbered `X-Docusign-Signature-*` header. Verify those bytes before parsing JSON. Reformatting the body first will change the digest.

The receiver below keeps the durable job to one derived string. Your queue needs an atomic `putIfAbsent(key, value)` operation backed by durable storage, not an in-process `Map` in production.

```js
import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_BODY_BYTES = 128 * 1024;

function headerValues(headers, prefix) {
  const expected = prefix.toLowerCase();
  return Object.entries(headers)
    .filter(([name]) => name.toLowerCase().startsWith(expected))
    .map(([, value]) => String(value));
}

function signatureMatches(rawBody, signature, secret) {
  const received = Buffer.from(signature, "base64");
  const expected = createHmac("sha256", secret).update(rawBody).digest();
  return (
    received.length === expected.length && timingSafeEqual(received, expected)
  );
}

export async function acceptDocusignWebhook({
  rawBody,
  headers,
  hmacSecrets,
  expectedConfigurationId,
  expectedAccountId,
  queue,
}) {
  const bytes = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody ?? "");
  if (bytes.length > MAX_BODY_BYTES)
    return { status: 413, code: "payload_too_large" };

  const signatures = headerValues(headers, "x-docusign-signature-");
  const valid = signatures.some((signature) =>
    hmacSecrets.some((secret) => signatureMatches(bytes, signature, secret)),
  );
  if (!valid) return { status: 401, code: "invalid_signature" };

  let event;
  try {
    event = JSON.parse(bytes.toString("utf8"));
  } catch {
    return { status: 400, code: "invalid_json" };
  }

  if (event?.event !== "envelope-completed")
    return { status: 200, code: "ignored_event" };
  if (String(event?.configurationId ?? "") !== expectedConfigurationId) {
    return { status: 200, code: "ignored_configuration" };
  }
  if (String(event?.data?.accountId ?? "") !== expectedAccountId) {
    return { status: 200, code: "ignored_account" };
  }

  const envelopeId = String(event?.data?.envelopeId ?? "").toLowerCase();
  const uuid =
    /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
  if (!uuid.test(envelopeId))
    return { status: 400, code: "invalid_envelope_id" };

  const eventKey = `docusign:envelope-completed:${expectedAccountId}:${envelopeId}`;
  try {
    const inserted = await queue.putIfAbsent(eventKey, { eventKey });
    return { status: 200, code: inserted ? "queued" : "duplicate", eventKey };
  } catch {
    return { status: 503, code: "queue_unavailable" };
  }
}
```

Return HTTP 200 only after the durable insert succeeds, including for an already-stored duplicate. Docusign asks listeners to respond quickly and recommends asynchronous processing. A queue failure should return a retryable failure rather than acknowledge an event you did not retain.

![Pingo closes a small cabinet while two PingRoom avatars place one unlettered completed-envelope token in a shallow queue tray.](/blog/docusign-envelope-completed-push-notifications-inline.webp)

Pingo, PingRoom's fictional CMO, keeps the rule admirably short here: the shared tray gets a completion token; the detailed papers stay in Docusign. He has no access to anyone's Room or envelopes—the human team owns the configuration and claims.

## Let a worker send fixed copy

The worker reads the tiny queued job, not the original Docusign payload. Reuse the same event key as both PingRoom's `Idempotency-Key` and `correlation_id`.

```js
export async function deliverCompletionSignal({
  job,
  pingroomWebhookUrl,
  fetchImpl = fetch,
}) {
  const destination = new URL(pingroomWebhookUrl);
  if (destination.protocol !== "https:") throw new Error("HTTPS required");

  const result = await fetchImpl(destination, {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      "Idempotency-Key": job.eventKey,
    },
    body: JSON.stringify({
      title: "Agreement completed",
      message: "Open Docusign to review the completed envelope",
      correlation_id: job.eventKey,
    }),
  });

  if (result.status === 429 || result.status >= 500)
    return { outcome: "retry" };
  if (!result.ok) return { outcome: "permanent_failure" };

  const payload = await result.json();
  return payload?.success === true
    ? { outcome: "delivered" }
    : { outcome: "retry" };
}
```

Retry network failures, HTTP 429, server errors, and malformed success responses with capped backoff. Treat other 4xx responses as configuration failures that need human review. PingRoom's idempotency key makes a downstream retry safe when the same key and body are reused; it does not make Docusign or the network exactly-once.

## Test the boundaries before using a real envelope

The production test fixture for this guide checks:

1. A valid current or previous HMAC key accepts the exact raw body.
2. An invalid signature, malformed JSON, or oversized body is rejected.
3. Another event, Connect configuration, or account is ignored.
4. A malformed envelope ID is rejected.
5. A repeated envelope-completed event produces one durable job.
6. The queued job and PingRoom request contain none of the fixture's subject, sender, recipient, signer, or email values.
7. Queue failure returns a retryable response; downstream 429, server, and network failures stay retryable.

These are deterministic mocks, not a live Docusign or phone-delivery test. Before production, send a Docusign test event, inspect the Connect log, confirm exactly one queue record, then fire a low-stakes test envelope to a private Room. Check Room history as well as a locked test phone: operating-system policy, connectivity, Room mutes, and notification settings can delay or suppress device display.

## Know which system answers which question

| Question                                                          | Source of truth                                     |
| ----------------------------------------------------------------- | --------------------------------------------------- |
| Did the selected Connect configuration emit `envelope-completed`? | Docusign Connect log                                |
| Who signed, what changed, and what is in the document?            | Docusign envelope and audit history                 |
| Did the relay durably accept this envelope event?                 | Your queue and relay logs                           |
| Did PingRoom accept the fixed Room Ping?                          | PingRoom webhook response and Room history          |
| Did every phone display it?                                       | Not guaranteed; check device and notification state |

The boundary matters. A completion alert can prompt the team to review the authoritative record; it should not silently authorize the next business action.

## Sources

- Docusign: [Connect overview](https://developers.docusign.com/platform/webhooks/connect/)
- Docusign: [Implementing Connect](https://developers.docusign.com/platform/webhooks/connect/implement/)
- Docusign: [JSON SIM event model](https://developers.docusign.com/platform/webhooks/connect/json-sim-event-model/)
- Docusign: [Manually authenticating HMAC signatures](https://www.docusign.com/blog/developers/manually-authenticating-hmac-signatures-docusign-connect-webhook-configurations)
- PingRoom: [Turn a webhook into a push notification](/blog/webhook-to-push-notification)

## Send one careful completion signal

[Create or open a private Room](/app/rooms), add an incoming webhook, and test the fixed alert with non-sensitive data. Keep Docusign authoritative, keep the Room audience small, and make the next human step explicit outside the notification.
