Proteside Docs

Webhooks

Receive alerts, new scripts and finished reports on your endpoint, verify the HMAC signature and handle retries.

With a webhook, Proteside sends a POST to your endpoint as soon as something happens: a new alert, an unknown script on the checkout, a finished PCI DSS report. You don't need to keep polling the API.

Webhooks, Slack, Microsoft Teams and email are all notification channels of the same organization. What you create through the API shows up in Settings → Notifications, and vice versa.

Create a webhook

Open Settings → Notifications and click Add channel. You need to be an owner or admin.

Choose the Webhook type and the scope: This store (only events from the selected store) or Organization (events from every store). Type and scope can't be changed later.

Fill in Name and URL (it must start with https://) and leave Format set to JSON (signed).

Fill in Signing secret with a random value of 16 to 128 characters and keep it safe: you'll need it to verify deliveries.

Choose the Minimum severity and the Events, keep Channel active on and click Save changes.

No secret, no signature

In the dashboard, the secret is optional. If you leave the field empty, deliveries go out without the X-Proteside-Signature header and the channel card shows "unsigned". Always fill in the secret on JSON webhooks.

Events

EventWhen it's sentseverityFiltered by min_severity
alert.createdA new alert is opened (first occurrence), including CSP violationsthe alert'syes
alert.resolvedAn SDK silent alert resolves itself because the SDK started sending events againmediumyes
integrity.changedAn authorized script's content changed (SCRIPT_INTEGRITY_MISMATCH alert)highyes
header.changedA security header on the payment page changed (HEADER_CHANGED alert)mediumyes
sdk.silentA store with traffic went 24 hours without sending SDK events (SDK_SILENT alert)mediumyes
script.detectedA new script appeared on the checkout and is awaiting reviewinfono
policy.appliedAn approval policy decided on a script (automatic mode or Apply)infono
report.readyThe scheduled PCI DSS report is ready (Monday, 09:00 UTC)infono
testYou sent a testinfoignores all filters

Catalog events that aren't sent today

script.authorized and script.blocked appear in the dashboard's event list and are accepted in the events field, but Proteside doesn't send them today. To track authorizations and blocks, query GET /scripts or the audit log.

alert.resolved is only sent when an SDK silent alert resolves itself. Resolving an alert in the dashboard or via PATCH /alerts/{id} doesn't generate an event. Reports generated by GET /reports/pci don't generate report.ready either: only the scheduled delivery does.

Channel filters

Each channel has two filters:

  • events: the list of accepted events. Empty or null means all of them.
  • min_severity: applies only to alert events (alert.created, alert.resolved, integrity.changed, header.changed and sdk.silent). script.detected, policy.applied and report.ready always go through.

The high default drops medium events

New channels start with min_severity: high (High and above). With that value, sdk.silent, header.changed and medium or low alerts (such as CSP violations) never arrive. If you want to know when the SDK stopped running, use min_severity: "medium" (Medium and above) or lower.

Delivery format

Proteside makes a POST with a JSON body and these headers:

HeaderContent
Content-Typeapplication/json
User-AgentProteside-Webhooks/1.0
X-Proteside-EventThe event type, for example alert.created
X-Proteside-DeliveryThe delivery ID (unique per channel and event, the same across all retries)
X-Proteside-Signaturet=<unix>,v1=<hex> (present when the channel has a secret)

Every event uses the same envelope:

Prop

Type

Payload examples

Verify the signature

When the channel has a secret, each delivery includes:

X-Proteside-Signature: t=1791277201,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is the send time in Unix seconds (recomputed on every attempt).
  • v1 is the HMAC-SHA256, in hexadecimal, of t + . + the raw body, using the entire secret as the key, including the whsec_ prefix.

To verify:

  1. Read the raw body, before any JSON parsing. Re-serializing the JSON changes the bytes and breaks the signature.
  2. Split out t and v1 and check their format: t an integer, v1 64 hexadecimal characters.
  3. Reject deliveries whose t is more than 300 seconds off from your clock. Proteside doesn't enforce this limit: protecting against malicious replays is the receiver's responsibility.
  4. Compute the expected HMAC and compare in constant time.
server.js
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const SECRET = process.env.PROTESIDE_WEBHOOK_SECRET; // "whsec_..."
const TOLERANCE_S = 300;

function verifySignature(rawBody, header, secret) {
  if (!header) return false;

  const parts = {};
  for (const item of header.split(',')) {
    const i = item.indexOf('=');
    if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
  }

  // 1. validate the format before any comparison
  if (!/^\d+$/.test(parts.t ?? '') || !/^[0-9a-f]{64}$/i.test(parts.v1 ?? '')) return false;

  // 2. time window against replays
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_S) return false;

  // 3. HMAC-SHA256(secret, "t." + raw body), compared in constant time
  const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
  const given = Buffer.from(parts.v1, 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}

const app = express();
const seen = new Set(); // in production, use Redis or your database

// express.raw keeps the body as a Buffer, unparsed
app.post('/hooks/proteside', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifySignature(req.body, req.get('X-Proteside-Signature'), SECRET)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  if (seen.has(event.id)) return res.sendStatus(200); // already processed
  seen.add(event.id);

  res.sendStatus(204); // respond quickly (under 8 s)...
  queueMicrotask(() => handleEvent(event)); // ...and process afterwards
});

function handleEvent(event) {
  console.log(event.type, event.data);
}

app.listen(3000);

Rotate the secret

Send a new secret in PATCH /webhooks/{id} (or edit the Signing secret in the dashboard). The change applies from the next delivery, with no overlap period: pending retries are also signed with the new secret. To avoid losing events, make your receiver accept both secrets for a few minutes, rotate it in Proteside, and then remove the old one.

Delivery and retries

  • The first attempt goes out when the event happens.
  • Your endpoint has 8 seconds to respond. Only 2xx responses count as success.
  • Redirects aren't followed: a 301 or 302 counts as a failure. Register the final URL.
  • On failure, Proteside retries until it completes 4 attempts: the immediate one plus three more, roughly 1 minute, 10 minutes and 60 minutes after the previous failure. Retries run in 5-minute cycles, so they may be delayed a few minutes beyond these intervals.
  • After the fourth failure, the delivery is marked Failed and isn't resent. The channel isn't deactivated automatically.
  • If the channel is deactivated when a retry is due, the delivery fails without being resent.
  • There's no ordering guarantee between events.

It may arrive more than once

If your endpoint processes the event but takes longer than 8 seconds to respond, Proteside counts it as a failure and sends it again. Deduplicate by the event id (evt_…), which stays the same across retries, or by the X-Proteside-Delivery header. Respond 2xx quickly and process in the background.

The history of the last 50 deliveries (event, status, attempts, HTTP code and error) is in Settings → Notifications, in the Delivery history card, for up to 90 days. The API doesn't expose the history or let you resend a delivery manually.

Test the webhook

In the dashboard, click Test connection on the channel's card. Through the API:

curl -s -X POST "https://app.proteside.com/api/v1/webhooks/a9b8c7d6-5555-4666-8777-888899990000/test" \
  -H "Authorization: Bearer $PROTESIDE_TOKEN"
{ "id": "a9b8c7d6-5555-4666-8777-888899990000", "delivered": false, "error": "http_404: Not Found" }

The test sends a signed test event, with a single attempt. It ignores the event and severity filters and is sent even if the channel is deactivated.

Check the delivered field

POST /webhooks/{id}/test responds 200 even when delivery fails. Check delivered and, if it's false, the reason in error: http_<status>: <start of the body>, timeout or a network message.

Slack, Microsoft Teams and email

If you only want notifications for people, you don't need your own endpoint:

  • Slack: create an Incoming Webhook in Slack and register its URL with "format": "slack" in the API, or with the Slack type in the dashboard. Proteside sends a formatted message with a summary of the event and a button to open it in the dashboard.
  • Microsoft Teams: the same with "format": "teams" or the Teams type. The message is an Adaptive Card.
  • Email: dashboard only (Email type, up to 5 recipients). Every store starts with an "E-mail (padrão)" channel (the default email channel) for the owner's email address.

Slack and Teams messages have fixed English text. Email channels only receive alert.created, report.ready and test, even if other events are checked: integrity.changed, header.changed and sdk.silent notifications don't arrive by email. For those, use a webhook, Slack or Teams.

Next steps

On this page