What it does
- Alerts for a new advertiser, an advertiser observed again after at least 30 days, a brand mention in ad copy, or one of your affiliate IDs.
- A scheduled summary, daily or weekly at the hour you choose, even when nothing changed: checks saved, new and returning advertisers, brand mentions, affiliate IDs, policy cases and the top advertisers.
- A JSON body with the event id, type, project brand, time, a link to the evidence and the observation details.
- Headers that name the event type and carry a stable delivery id.
- A signature on every request, made with the rule's own secret, so your endpoint can confirm the request came from Ads Lighthouse unchanged.
- Up to three attempts, 15 and 60 minutes apart, when your endpoint times out or returns 408, 429 or a 5xx status.
Set it up
- Prepare an HTTPS endpoint on your system that accepts POST requests with a JSON body.
- In Ads Lighthouse, open Alerts and select Rules.
- Choose the project and event type, choose Webhook as the channel and enter your URL.
- Select Add rule. An account owner or admin can then open Webhook signing secrets on the same page and copy the rule's secret into your endpoint's settings.
- Verify the x-ads-lighthouse-signature header on each request. Store each event id and ignore any id you have already processed.
Verify the signature
Compute the HMAC over the raw request body before you parse it. Compare signatures in constant time, and reject timestamps older than 5 minutes. The rule's secret is under Alerts > Rules > Webhook signing secrets.
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verifyAdsLighthouseSignature(
secret: string,
header: string,
rawBody: string | Buffer,
nowSeconds = Math.floor(Date.now() / 1000),
): boolean {
let timestamp = Number.NaN;
const signatures: string[] = [];
for (const part of header.split(",")) {
const [key, value = ""] = part.split("=", 2);
if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
else if (key === "v1") signatures.push(value);
}
if (!Number.isSafeInteger(timestamp) || Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS)
return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
return signatures.some((signature) => {
const received = Buffer.from(signature, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
});
}
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_signature(secret: str, header: str, raw_body: bytes) -> bool:
timestamp = None
signatures = []
for part in header.split(","):
key, _, value = part.partition("=")
if key == "t" and value.isdigit():
timestamp = int(value)
elif key == "v1":
signatures.append(value)
if timestamp is None or abs(time.time() - timestamp) > TOLERANCE_SECONDS:
return False
signed = str(timestamp).encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, signature) for signature in signatures)
Limits to know
- Delivery is at least once: a retried request repeats the same event id, so your endpoint should deduplicate.
- The URL must use HTTPS and resolve to a public address. Private and local network addresses are rejected.
- Other refusals from your endpoint, such as 400 or 403, are not retried.
- A new secret applies from the next delivery, with no overlap. Update your endpoint right after you rotate it: a request it rejects with a 4xx status is not sent again.
- Payloads contain only a fixed set of observation fields, never internal identifiers or raw collection data.