Actions

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 POST request 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

  1. Go to Actions → Browse Catalog.

  2. Open Send Notification and click Set Up.

  3. Give it a Name.

  4. Under Run When, pick the events it listens for. See Events.

  5. Under How Often, pick how often it may send:

    OptionSends
    Every timeOnce for each event
    At most once every 15 minutesOnce per quarter hour, for each event and project
    At most once an hourOnce per hour, for each event and project
    At most once a dayOnce per day (UTC), for each event and project
    At most once a weekOnce 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.

  6. Under Send To, click Add Destination, pick where it goes and fill it in. Add as many as you need.

  7. 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.

FieldFills 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

HeaderValue
content-typeapplication/json
user-agentConcord-Webhooks/1.0
x-concord-event-typeThe event, such as compliance_requests.received
x-concord-delivery-idThe delivery's ID, the same as id in the body
x-concord-timestampWhen this attempt was signed, in seconds since 1970
x-concord-signaturev1= followed by the signature. See Verify The Signature
x-concord-testtrue, 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"
  }
}
FieldMeans
idThe delivery's ID. A retry keeps it, so use it to drop duplicates
typeThe event. See Events
createdAtWhen Concord created the message. A retry keeps it
testtrue on a Send Test, and left out otherwise
data.eventNameThe event's name, as shown in Concord
data.titleThe event's headline
data.projectId, data.projectNameThe project, when the event has one
data.entityIdWhat the event is about, such as the privacy request's ID
data.propertiesThe event's details, which differ by event
data.linkWhere 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:

  1. Take the raw body exactly as it arrived, before parsing the JSON. Most frameworks give you a parsed copy that won't match.
  2. Join the x-concord-timestamp header, a ., and the raw body.
  3. Compute an HMAC-SHA256 of that with your signing secret, as lowercase hex.
  4. Compare it with the part of x-concord-signature after v1=, using a constant-time comparison.
  5. 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

  1. Edit the notification and click Rotate Secret on the webhook.
  2. Copy the new secret it shows.
  3. 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 data above, with no id, type or createdAt around it.
  • x-concord-signature is the HMAC-SHA256 of the raw body alone, as hex, with no v1= and no x-concord-timestamp.

To move one to v1, remove the webhook destination and add it again with the same address, then update your system.

Events

EventtypeDetails in properties
Privacy request receivedcompliance_requests.receivedrequest: change, delete, view or do_not_sell. Also complianceRequestId
Daily privacy request summarycompliance_requests.dailyreceived.count, resolved.count, dueSoon.count, overdue.count
Weekly privacy request summarycompliance_requests.weeklyreceived.count, resolved.count, dueSoon.count, overdue.count
Scan found issuesconfiguration.issuesdomain
Task assignedtasks.assigneddueAt, when the task has a due date
Balance running lowbilling.low_balancepool: action for Actions, credit for Data Credits, or entitlement for plan capacity
Billing updatebilling.updatesNone
Self-learning skippedbilling.self_learning_skippedNone
Data Credits auto-purchasedbilling.data_credits_auto_purchasedNone

The billing events are free to send. See What It Costs.

Troubleshooting

ProblemWhat to do
A destination type shows a plan instead of its settingsYour 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 matchCheck 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 messageAdd 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 expectedCheck How Often. Repeats inside its time block are skipped, and a notification sends at most 100 times a day.