Documentation
Outbound webhooks: payload and signature
Receive AutoPSEO alerts and reports as signed JSON at any HTTPS endpoint: the request headers, body fields, event types and how to check the signature.
An outbound webhook sends AutoPSEO's alerts, scheduled reports, test messages and report summaries to an endpoint you run, as one JSON POST per message signed with HMAC-SHA256. Use it to feed Zapier, Make, n8n, an incident tool or your own code; no Slack or Teams account is involved. Looking for the webhook that delivers articles to your site? That one belongs to the content engine: see content delivery.
Add a webhook
- In the Outbound webhooks section, click New webhook.
- Give it a Name and paste the endpoint's URL. It must be public HTTPS: plain
http,localhost, names ending in.localor.internal, hosts without a dot, and private or link-local IP addresses are refused. - Copy the signing secret shown after you save. It is displayed only this once.
- Click Send test to post a
testevent, then attach scheduled reports or alerts to the webhook.
What a webhook request looks like
Every message is a POST with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | AutoPSEO-Webhook/1.0 |
X-AutoPSEO-Event | The event type, for example traffic_drop |
X-AutoPSEO-Signature | sha256= followed by 64 lowercase hex characters |
Here is a traffic-drop alert for a domain property from the 06:00 UTC run on 12 October 2026, formatted for reading (on the wire it is compact JSON, and the id and site_id values are made up):
{
"id": "9b2e61f04c3a4d7e8f15a0b6c2d39e47",
"event_type": "traffic_drop",
"severity": "high",
"site_id": "4f0c1e9a2b7d4c58",
"site_url": "sc-domain:example.com",
"occurred_at": "2026-10-12T06:00:04.183Z",
"title": "Traffic drop: example.com",
"text": "Clicks 1,500 vs 2,000 in the previous window (-25.0%).",
"url": "https://app.autopseo.com/property/sc-domain%3Aexample.com",
"payload": {
"fields": [
{ "label": "Metric", "value": "clicks" },
{ "label": "Current", "value": "1,500" },
{ "label": "Baseline", "value": "2,000" },
{ "label": "Window", "value": "7 days (2026-10-05 → 2026-10-11 vs 2026-09-28 → 2026-10-04)" },
{ "label": "Change", "value": "-25.0%" }
],
"table": null,
"detector": "traffic_drop",
"window": 7,
"range": { "start": "2026-10-05", "end": "2026-10-11", "compStart": "2026-09-28", "compEnd": "2026-10-04" },
"result": {
"fired": true,
"metric": "clicks",
"value": 1500,
"baseline": 2000,
"change": -0.25,
"severity": "high",
"summary": "Clicks 1,500 vs 2,000 in the previous window (-25.0%)."
}
},
"source": "autopseo"
}
The top-level fields are the same for every event:
| Field | What it holds |
|---|---|
id | A random 32-character hex string, new for every request |
event_type | What happened (see the next section) |
severity | critical, high, medium or low for alerts; info for reports, summaries and tests |
site_id | AutoPSEO's id for the property, or null for tests and the demo property |
site_url | The Search Console property, such as sc-domain:example.com or https://www.example.com/, or null for tests |
occurred_at | When the message was built, as an ISO 8601 timestamp in UTC |
title, text | The headline and the summary sentence that Slack and Teams show |
url | The report in AutoPSEO, or null for tests |
payload.fields | The labelled values Slack shows as fields and Teams as facts |
payload.table | columns and rows for reports and summaries, otherwise null |
source | Always autopseo |
Ratios are fractions: a change of -0.25 is a fall of 25%, and a ctr of 0.034 is 3.4%.
Event types
event_type | Sent when | Extra keys in payload |
|---|---|---|
traffic_drop, traffic_spike, position_drop, ctr_drop, new_lost_rankings, anomaly, index_flip, rank_drop, audit_regression | An alert rule fires, on schedule or through Test rule | detector, window, range (the dates, or null for index_flip, rank_drop and audit_regression), result |
scheduled_report | A scheduled report goes out, on schedule or through Send now | reportType (summary, queries, pages or rankings), range (dates plus a label), totals, previous |
report_summary | Someone uses Send to Slack on a report page | None: the figures are in fields and table |
test | Someone clicks Send test on the webhook | sample, always true |
result repeats what the detector measured: fired, metric, value, baseline, change (null when there is nothing to compare with), severity and summary, plus z, the modified z-score, for anomaly. totals and previous hold clicks, impressions, ctr and position for a report's two windows. What each detector measures is described under Alerts.
How to verify the signature
The signature is an HMAC-SHA256 of the exact bytes AutoPSEO sent, keyed with the webhook's signing secret and written as sha256= plus lowercase hex. To check it:
- Read the raw request body before any JSON parsing. Parsing and serialising again can change the bytes.
- Compute HMAC-SHA256 over those bytes, using the secret as text: the 64-character string exactly as shown, not decoded from hex.
- Prefix
sha256=and compare the result with theX-AutoPSEO-Signatureheader in constant time. - Reject the request when they differ. Otherwise answer with a 2xx status straight away and do the slow work afterwards.
A receiver for Node.js with no dependencies:
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
const secret = process.env.AUTOPSEO_WEBHOOK_SECRET;
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const raw = Buffer.concat(chunks);
const expected = Buffer.from('sha256=' + createHmac('sha256', secret).update(raw).digest('hex'));
const received = Buffer.from(String(req.headers['x-autopseo-signature'] || ''));
if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
res.writeHead(401).end();
return;
}
res.writeHead(204).end();
const event = JSON.parse(raw.toString('utf8'));
console.log(event.event_type, event.severity, event.site_url);
});
}).listen(8080);
The length check comes first because Node's timingSafeEqual throws when the two buffers differ in length.
Delivery rules
- Each message gets one attempt with a 10-second timeout. Any answer other than 2xx counts as a failure and is kept in Delivery history with the status code and the start of your response body.
- Redirects are not followed. A 3xx answer fails with a note to use the final URL.
- Nothing is retried on a timer. A failed alert is not marked as sent, so its next check evaluates it again and posts if it still fires: the next morning for most detectors, the next hour for index flips. A failed scheduled report is not resent in the same period, so use Send now after fixing the endpoint.
idis created per request, so it identifies a delivery rather than an event. To de-duplicate alerts, combineevent_type,site_urland the date ofoccurred_at: a rule posts at most once per property per UTC day, unless someone runs Test rule.
The signing secret
AutoPSEO shows the signing secret once, when you save the webhook, and the list never shows it again. Store it with your receiver's configuration straight away, alongside the endpoint it protects.
The URL itself is stored encrypted, and the list shows only its host and last characters. Deleting a webhook also deletes every rule that posts to it. For a readable card in a Teams channel rather than JSON, use Microsoft Teams.