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 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 a secret that belongs to the rule, so your endpoint can check that the request came from Ads Lighthouse and was not changed.
- Up to three attempts, 15 and 60 minutes apart, when your endpoint times out or answers 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. Account owners and administrators can then open Webhook signing secrets on the same page and copy the rule's secret into your endpoint's configuration.
- Check the x-ads-lighthouse-signature header of each request, then store the event id you receive and ignore any id you have already processed.
Verify the signature
Compute the HMAC over the raw request body before parsing it, compare in constant time and refuse timestamps older than 5 minutes. Find the rule's secret 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 refused.
- Other refusals from your endpoint, such as 400 or 403, are not retried.
- Rotating a secret takes effect on the next delivery, with no overlap. Update your endpoint right after rotating: a request it refuses with a 4xx status is not sent again.
- Payloads contain only allowlisted observation fields, never internal identifiers or raw collection responses.