Skip to content
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.

Account → Slack, Teams & Webhooks → Outbound webhooksNeeds a subscription or trial5 min read

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

  1. In the Outbound webhooks section, click New webhook.
  2. Give it a Name and paste the endpoint's URL. It must be public HTTPS: plain http, localhost, names ending in .local or .internal, hosts without a dot, and private or link-local IP addresses are refused.
  3. Copy the signing secret shown after you save. It is displayed only this once.
  4. Click Send test to post a test event, then attach scheduled reports or alerts to the webhook.

What a webhook request looks like

Every message is a POST with these headers:

HeaderValue
Content-Typeapplication/json
User-AgentAutoPSEO-Webhook/1.0
X-AutoPSEO-EventThe event type, for example traffic_drop
X-AutoPSEO-Signaturesha256= 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:

FieldWhat it holds
idA random 32-character hex string, new for every request
event_typeWhat happened (see the next section)
severitycritical, high, medium or low for alerts; info for reports, summaries and tests
site_idAutoPSEO's id for the property, or null for tests and the demo property
site_urlThe Search Console property, such as sc-domain:example.com or https://www.example.com/, or null for tests
occurred_atWhen the message was built, as an ISO 8601 timestamp in UTC
title, textThe headline and the summary sentence that Slack and Teams show
urlThe report in AutoPSEO, or null for tests
payload.fieldsThe labelled values Slack shows as fields and Teams as facts
payload.tablecolumns and rows for reports and summaries, otherwise null
sourceAlways 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_typeSent whenExtra keys in payload
traffic_drop, traffic_spike, position_drop, ctr_drop, new_lost_rankings, anomaly, index_flip, rank_drop, audit_regressionAn alert rule fires, on schedule or through Test ruledetector, window, range (the dates, or null for index_flip, rank_drop and audit_regression), result
scheduled_reportA scheduled report goes out, on schedule or through Send nowreportType (summary, queries, pages or rankings), range (dates plus a label), totals, previous
report_summarySomeone uses Send to Slack on a report pageNone: the figures are in fields and table
testSomeone clicks Send test on the webhooksample, 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:

  1. Read the raw request body before any JSON parsing. Parsing and serialising again can change the bytes.
  2. Compute HMAC-SHA256 over those bytes, using the secret as text: the 64-character string exactly as shown, not decoded from hex.
  3. Prefix sha256= and compare the result with the X-AutoPSEO-Signature header in constant time.
  4. 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.
  • id is created per request, so it identifies a delivery rather than an event. To de-duplicate alerts, combine event_type, site_url and the date of occurred_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.

Put your SEO content on autopilot

Connect Search Console and your site. AutoPSEO scans it and plans the first topics right away. No credit card needed.