Sending Notifications
Send an email or a signed webhook when something happens in Concord, such as a privacy request arriving, a scan finding issues, or your balance running low.
Overview
A Send Notification action listens for the events you choose, such as a privacy request arriving, a scan finding issues, or your balance running low. When one happens, it sends a message to every destination you added:
- Email: people in your organization.
- Webhook: a signed
POSTrequest to your own system.
One notification can listen for several events and send to several destinations, and you can set up as many notifications as you like.
Notifications never carry a requester's personal details, such as their name or email address. Each message links back to Concord, where you can see them.
Plans and Permissions
Send Notification and email destinations work on every plan. Webhook destinations are on Concord Privacy Pro and up, or any paid Concord Trust plan. A destination your plan doesn't include shows the plan it needs. If you change to a plan without it, that destination stops sending, and Activity says why.
Owners and Admins can set up notifications, which needs permission to edit actions.
What It Costs
- 1 Action each time a notification sends, however many destinations it sends to. It's charged only when at least one destination gets the message.
- Billing alerts are free: Balance running low, Billing update, Self-learning skipped and Data Credits auto-purchased.
- Send Test and retries are free.
- When your organization has no Actions left, nothing is sent, and Activity says "Not sent: this organization has no Actions left."
See Actions and Data Credits for how Actions are counted and topped up.
Set Up A Notification
-
Go to Actions → Browse Catalog.
-
Open Send Notification and click Set Up.
-
Give it a Name.
-
Under Run When, pick the events it listens for. See Events.
-
Under How Often, pick how often it may send:
Option Sends Every time Once for each event At most once every 15 minutes Once per quarter hour, for each event and project At most once an hour Once per hour, for each event and project At most once a day Once per day (UTC), for each event and project At most once a week Once per week, for each event and project The limits use fixed blocks of time, such as each hour on the clock, so two events either side of a boundary both send.
-
Under Send To, click Add Destination, pick where it goes and fill it in. Add as many as you need.
-
Leave Enabled on, then save.
Your notifications are listed under Actions → Your Actions, where you can edit, turn off or delete them.
Write Your Own Message
Each email destination has a message box. Leave it empty to send Concord's default message: the event's headline, its details and a link to it.
To write your own, type the message and click Insert Field to add details from the event. A field such as {{event.name}} is filled in when the notification sends.
| Field | Fills in |
|---|---|
{{event.name}} | What happened, for example "Privacy request received" |
{{event.title}} | The event's headline, which names the task or scan it's about |
{{event.properties}} | Every detail on the event, one per line |
{{event.properties.request}} | One detail, here the request type. Insert Field lists each event's details |
{{project.name}} | The project it happened in, when there is one |
{{organization.name}} | Your organization |
{{link}} | A link to it in Concord |
A few things to know:
- Fields go exactly where you put them. Add spaces or new lines between them.
{{event.name}}{{project.name}}sends "Privacy request receivedAcme Store". - A message with no fields gets the default details below it. Once your message uses a field, it's sent as you wrote it.
- A field shows in red when none of the events you picked has it.
- Email also has an Email Subject, which can use fields too. It's always one line.
Test And Check Delivery
Click Send Test on a destination to send it a sample message right away. A test is marked as one, isn't charged, and isn't retried.
To see what was sent, go to Actions → Your Actions → Activity. Each run shows every destination with its outcome: delivered, or the reason it wasn't.
Retries And Limits
When a destination can't take the message because of a problem that may pass, Concord tries again after 1 minute, 5 minutes, 30 minutes and 2 hours. Such problems include a timeout, a server error, or a webhook answering 429 or 5xx. When a webhook answers 429 with a Retry-After header, that can make the wait longer, up to 2 hours.
Each retry uses the destination as it's set up at the time. So if you fix a webhook address, the next retry uses the fix.
A problem that won't pass on its own isn't retried. That includes a webhook answering a 4xx other than 429, or a redirect.
A notification sends at most 100 times a day (UTC). Billing alerts don't count toward that.
Webhooks
A webhook destination sends each event to an address on your own system as an HTTPS POST with a JSON body, signed so you can check it came from Concord.
The Address
- It has to start with
https://and use a hostname, not an IP address. - It can't contain a username or password.
- It has to be reachable from the internet. Addresses on private networks are refused.
- Concord doesn't follow redirects, so use the final address.
Answer with any 2xx status within 10 seconds. Do any slow work after you've answered.
The Request
| Header | Value |
|---|---|
content-type | application/json |
user-agent | Concord-Webhooks/1.0 |
x-concord-event-type | The event, such as compliance_requests.received |
x-concord-delivery-id | The delivery's ID, the same as id in the body |
x-concord-timestamp | When this attempt was signed, in seconds since 1970 |
x-concord-signature | v1= followed by the signature. See Verify The Signature |
x-concord-test | true, on a Send Test only |
The body for a privacy request:
{
"id": "6f1d2c3b-4a5e-4f60-8b7a-9c0d1e2f3a4b",
"type": "compliance_requests.received",
"createdAt": "2026-10-19T18:44:28.425Z",
"data": {
"event": "compliance_requests.received",
"eventName": "Privacy request received",
"title": "Privacy request received",
"organizationId": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"organizationName": "Acme",
"projectId": "1b2c3d4e-5f6a-4b7c-9d8e-0f1a2b3c4d5e",
"projectName": "Acme Store",
"entityId": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"properties": {
"complianceRequestId": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
"request": "delete"
},
"link": "https://admin.concord.tech/acme/privacy-requests/request-log?complianceRequestId=2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f"
}
}| Field | Means |
|---|---|
id | The delivery's ID. A retry keeps it, so use it to drop duplicates |
type | The event. See Events |
createdAt | When Concord created the message. A retry keeps it |
test | true on a Send Test, and left out otherwise |
data.eventName | The event's name, as shown in Concord |
data.title | The event's headline |
data.projectId, data.projectName | The project, when the event has one |
data.entityId | What the event is about, such as the privacy request's ID |
data.properties | The event's details, which differ by event |
data.link | Where to see it in Concord |
Verify The Signature
When you add a webhook, Concord makes its signing secret, which starts with whsec_, and shows it once. Copy it before you save: it's never shown again.
To check a request:
- Take the raw body exactly as it arrived, before parsing the JSON. Most frameworks give you a parsed copy that won't match.
- Join the
x-concord-timestampheader, a., and the raw body. - Compute an HMAC-SHA256 of that with your signing secret, as lowercase hex.
- Compare it with the part of
x-concord-signatureafterv1=, using a constant-time comparison. - Refuse the request if the timestamp is more than 5 minutes old, so a copied request can't be replayed. A retry is signed again with a new timestamp, so it passes this check.
Node.js, with Express:
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.CONCORD_WEBHOOK_SECRET;
function isFromConcord(rawBody, headers) {
const timestamp = headers['x-concord-timestamp'];
const signature = headers['x-concord-signature'] ?? '';
if (!timestamp || !signature.startsWith('v1=')) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const received = Buffer.from(signature.slice(3));
return (
received.length === expected.length &&
crypto.timingSafeEqual(received, Buffer.from(expected))
);
}
const app = express();
app.post(
'/concord-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
if (!isFromConcord(req.body.toString('utf8'), req.headers)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
// Handle the event after answering.
},
);Python:
import hashlib
import hmac
import os
import time
SECRET = os.environ["CONCORD_WEBHOOK_SECRET"]
def is_from_concord(raw_body: bytes, headers) -> bool:
timestamp = headers.get("x-concord-timestamp")
signature = headers.get("x-concord-signature", "")
if not timestamp or not signature.startswith("v1="):
return False
try:
signed_at = int(timestamp)
except ValueError:
return False
if abs(time.time() - signed_at) > 300:
return False
expected = hmac.new(
SECRET.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature[3:].encode(), expected.encode())Duplicates
A retry carries the same id, the same x-concord-delivery-id and the same body as the first attempt. Only x-concord-timestamp and x-concord-signature change. Keep the IDs you've handled and skip any you've seen.
A Send Test's id starts with test-.
Rotate The Secret
- Edit the notification and click Rotate Secret on the webhook.
- Copy the new secret it shows.
- Save, and update your system with the new secret at the same time.
From the save on, requests are signed with the new secret only. A webhook with no secret shows Requests are not signed. Click Add Signing Secret to start signing it.
Webhooks Added Before Signing Version 1
A webhook added before Concord signed with v1 keeps its original format, so your system doesn't break:
- The body is the event's details alone: what sits under
dataabove, with noid,typeorcreatedAtaround it. x-concord-signatureis the HMAC-SHA256 of the raw body alone, as hex, with nov1=and nox-concord-timestamp.
To move one to v1, remove the webhook destination and add it again with the same address, then update your system.
Events
| Event | type | Details in properties |
|---|---|---|
| Privacy request received | compliance_requests.received | request: change, delete, view or do_not_sell. Also complianceRequestId |
| Daily privacy request summary | compliance_requests.daily | received.count, resolved.count, dueSoon.count, overdue.count |
| Weekly privacy request summary | compliance_requests.weekly | received.count, resolved.count, dueSoon.count, overdue.count |
| Scan found issues | configuration.issues | domain |
| Task assigned | tasks.assigned | dueAt, when the task has a due date |
| Balance running low | billing.low_balance | pool: action for Actions, credit for Data Credits, or entitlement for plan capacity |
| Billing update | billing.updates | None |
| Self-learning skipped | billing.self_learning_skipped | None |
| Data Credits auto-purchased | billing.data_credits_auto_purchased | None |
The billing events are free to send. See What It Costs.
Troubleshooting
| Problem | What to do |
|---|---|
| A destination type shows a plan instead of its settings | Your plan doesn't include it. See Plans and Permissions. |
| A webhook says "not a public host Concord can reach" | Use an https:// address with a hostname that's reachable from the internet. |
| A webhook says "Delivery failed with status 4xx" | Your system refused the request, so it isn't retried. Check the address and any authentication in front of it. |
| A webhook's signature doesn't match | Check against the raw body as it arrived, with the timestamp and a . in front, and the current secret. If you lost the secret, rotate it. |
| Fields run together in the message | Add spaces or new lines between them. |
| Activity says "Not sent: this organization has no Actions left" | Top up your Actions. Billing alerts still send. |
| A notification sent less often than expected | Check How Often. Repeats inside its time block are skipped, and a notification sends at most 100 times a day. |
Assessments
Run DPIAs, privacy impact assessments, California risk assessments, transfer impact assessments, and more from ready-made templates, with review, approval, and a permanent record.
Concord Support Commitment
This article outlines Concord's support commitment to our customers including support channels and response times available to customers across different plans.