---
title: "Webhooks: Send Link Views and NDA Acceptances to Your Systems"
lang: en
canonical_url: https://www.peony.ink/docs/features/webhooks
description: "Peony posts each link view and NDA acceptance to your HTTPS endpoint or Slack on Deal Team and Enterprise. HTTPS requests are signed; retries run up to 24 hours."
---

Webhooks post each **link view** and **NDA acceptance** to your own HTTPS endpoint or to a Slack channel, so your CRM or workflow tool can react without polling <Plan>Deal Team+</Plan>.

Peony is a virtual data room for fundraising, M&A and due diligence, and webhooks let your deal tools react as soon as a counterparty opens a link or accepts an NDA.

You don't need [API access](https://www.peony.ink/docs/apis-sdks) or an API key. Any admin can open **Settings** → **Webhooks** on any plan to see what's configured, but adding or changing an endpoint needs Deal Team or Enterprise. Trials don't qualify. Turning an endpoint off and deleting it stay available on every plan. If the workspace leaves Deal Team, delivery stops and your endpoints stay stored.

## Add an Endpoint

1. Open **Settings** → **Webhooks** and click **Add webhook**.
2. Enter an **Endpoint name** of up to 100 characters.
3. Choose **Generic webhook** or **Slack incoming webhook**. You can't change this later.
4. Enter the **Destination URL**.
5. Select at least one event. **Link viewed** starts selected and **NDA accepted** doesn't.
6. Click **Add webhook**. For a Generic endpoint, click **Copy secret** now: the signing secret is shown once and can't be retrieved later.
7. Turn the endpoint on. New endpoints start **Disabled**.

A workspace can have up to 10 endpoints. Enabling applies only to future events. There is no backfill, replay or test button, so verify with a real link view and check the endpoint's latest delivery, which shows **Queued**, **Processing**, **Retrying**, **Delivered**, **Failed** or **Cancelled**.

**Destination rules.** A Generic URL must use HTTPS and reach a public address. A Slack URL must be an Incoming Webhook URL in the form `https://hooks.slack.com/services/...`, created in your Slack workspace. Peony shows only the hostname after saving, so to change a destination, enter the complete URL under **Replace destination URL**.

## Events

Each request body is one JSON object. Read `type` and `version` from the body, since no header carries them.

```json
{
  "id": "0b8e7d6c-5a4f-4e3d-9c2b-1a0f9e8d7c6b",
  "type": "nda.accepted",
  "version": 1,
  "occurred_at": "2026-11-01T09:00:00.000Z",
  "data": {
    "acceptance_id": "40c1ac46-f506-4204-8177-4aa70dcc1868",
    "accepted_at": "2026-11-01T09:00:00.000Z",
    "link": { "id": "6f1c2b3a-4d5e-4f60-8a7b-9c0d1e2f3a4b", "name": "Investor link" },
    "nda": { "id": "7ddf62a7-e122-4f5e-9671-a1b687e9ccab", "name": "Room NDA", "version": 2, "source_type": "file" },
    "viewer": { "id": null, "name": "Alex Example", "email": "alex@example.com" }
  }
}
```

| UI label | `type` | Sent when | `data` fields |
| --- | --- | --- | --- |
| **Link viewed** | `link.viewed` | A view is captured | `content_name`, `link_name`, `viewer` (`name`, `email`), `device_type`, `city`, `country`, `analytics_url`, `nda_required` |
| **NDA accepted** | `nda.accepted` | An acceptance completes | `acceptance_id`, `accepted_at`, `link`, `nda` (`id`, `name`, `version`, `source_type`), `viewer` (`id`, `name`, `email`) |

- **`nda_required`** says whether the link required an NDA when the view was captured. It doesn't prove the viewer accepted one, and if it's missing the value is unknown, not false.
- **`nda.accepted`** fires for an accepted uploaded NDA (`file`) or a finalized template NDA (`template`). A view, an unsigned agreement or reuse of an earlier acceptance sends nothing. Accepting a new NDA version is a new event.
- **Version 1 can gain fields**, so ignore any you don't recognize. It won't remove or rename one.

## Verify the Signature

Generic requests carry five headers. Slack messages are plain text with no Peony signature, since Slack's URL carries the credential.

| Header | Value |
| --- | --- |
| `Peony-Webhook-Signature` | `v1=` followed by 64 lowercase hex characters |
| `Peony-Webhook-Timestamp` | Unix seconds |
| `Peony-Event-Id` | Event UUID, the same across retries and endpoints |
| `Peony-Delivery-Id` | Delivery UUID for this endpoint, the same across retries |
| `Peony-Delivery-Attempt` | Attempt number, starting at 1 |

Compute HMAC-SHA256 over the timestamp, a period, and the raw body bytes. The key is the full secret as UTF-8, including its `pny_whsec_` prefix. Read the raw body before parsing any JSON, compare in constant time, and reject timestamps more than five minutes old.

```js
const crypto = require("node:crypto")

function verifyPeonyWebhook(rawBody, headers, secret) {
  const timestamp = headers["peony-webhook-timestamp"]
  const match = /^v1=([0-9a-f]{64})$/.exec(headers["peony-webhook-signature"] ?? "")
  if (!match || !/^\d+$/.test(timestamp ?? "")) return false
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest()
  return crypto.timingSafeEqual(expected, Buffer.from(match[1], "hex"))
}
```

## Retries and Duplicates

Respond with any 2xx within 10 seconds. Peony ignores the response body.

- **Retried:** timeouts, DNS and network failures, `408`, `425`, `429` and any `5xx`.
- **Not retried:** other `3xx` and `4xx` responses. Peony doesn't follow redirects.
- **Schedule:** up to 8 attempts within 24 hours of the delivery being created, waiting about 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours and 12 hours.
- **`Retry-After`:** honored on `429` and `503` when it asks for a later time than the schedule. If it points past the 24 hours, the delivery ends as **Failed**.

Delivery is at least once with no ordering guarantee. To drop a repeated delivery, key on `Peony-Delivery-Id`. To run one business action once across several endpoints, key on your consumer name plus `Peony-Event-Id`. Retries keep both IDs and the same body, and send a new timestamp, signature and attempt number.

Disabling, deleting, re-pointing or unsubscribing an endpoint cancels its queued work, though an attempt already in flight may finish. If too many deliveries fail in a row, Peony turns the endpoint off and sends a **Webhook endpoint automatically disabled** email. Fix the receiver, then turn the endpoint back on under **Settings** → **Webhooks**.

## Rotate a Signing Secret

**Rotate signing secret** stops the current secret immediately, so requests fail verification until your receiver has the new one.

1. Turn the endpoint off.
2. Click **Rotate signing secret**, then **Rotate secret**.
3. Copy the new secret, which is shown once, and update your receiver.
4. Turn the endpoint back on.

## Common Questions

#### Why did I receive the same event twice?
Peony retries when it doesn't get a 2xx in time, including when your receiver did the work but responded late. Deduplicate with `Peony-Delivery-Id`.

#### What does the Slack message contain?
An NDA acceptance names the viewer, link, NDA, version and acceptance time as plain text, with no link.

#### Can I get events from before I enabled the endpoint?
No. An endpoint receives only events that occur after it's enabled, and there's no replay.

#### Does a failed delivery affect the viewer?
No. A failed delivery never undoes the view or acceptance it reports.

## Next Steps

- [Peony API](https://www.peony.ink/docs/apis-sdks): create links, manage access rules and send agreements from your own code.
- [NDA gates](https://www.peony.ink/docs/features/security/nda-gates): set up the NDA that **NDA accepted** reports.
- [Plans and pricing](https://www.peony.ink/docs/account-billing/plans-and-pricing): see which features are included on each tier.

---

_Markdown version of [https://www.peony.ink/docs/features/webhooks](https://www.peony.ink/docs/features/webhooks.md) for AI agents and LLMs; canonical HTML: https://www.peony.ink/docs/features/webhooks. Every page on peony.ink is available as markdown — append `.md` to the URL or request it with `Accept: text/markdown`. More: [llms.txt](https://www.peony.ink/llms.txt)._
