Webhooks: Send Link Views and NDA Acceptances to Your Systems
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.
Last updated November 1, 2026
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 Deal Team+.
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 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
- Open Settings → Webhooks and click Add webhook.
- Enter an Endpoint name of up to 100 characters.
- Choose Generic webhook or Slack incoming webhook. You can't change this later.
- Enter the Destination URL.
- Select at least one event. Link viewed starts selected and NDA accepted doesn't.
- Click Add webhook. For a Generic endpoint, click Copy secret now: the signing secret is shown once and can't be retrieved later.
- 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.
{
"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_requiredsays 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.acceptedfires 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.
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,429and any5xx. - Not retried: other
3xxand4xxresponses. 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 on429and503when 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.
- Turn the endpoint off.
- Click Rotate signing secret, then Rotate secret.
- Copy the new secret, which is shown once, and update your receiver.
- 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: create links, manage access rules and send agreements from your own code.
- NDA gates: set up the NDA that NDA accepted reports.
- Plans and pricing: see which features are included on each tier.
