punchline!

Integrations

Webhooks

Send each day's alert digest to your own systems: a CRM workflow, Zapier or Make, an internal service. The webhook gets the same digest as email and Slack, as structured JSON, signed so you can tell it came from Punchline.

Set one up

  1. Open Alerts → Where alerts go. You need to be an owner or admin of the site.
  2. Paste an https:// URL under Webhook (for your CRM) and save.
  3. Copy the signing secret shown after saving. It's shown once; store it with your receiver's configuration.
  4. Press Send a test to deliver a sample digest and check the result.

To change the URL, paste a new one: that also issues a new secret. To stop, tick Remove.

The URL must be https, use a public host name (not an IP address or an internal name like .local or .internal), and be at most 500 characters. Put any token your receiver needs in the URL's path or query string, not as user:password@. Before every delivery Punchline checks the host still resolves only to public addresses, and it connects to the address it checked.

When it's sent

Alerts are checked once a day, shortly after 07:00 UTC, for the previous UTC day. If any alert fired, one digest goes to every destination: email, Slack and the webhook. Quiet days send nothing.

Each digest is sent once. There's no automatic retry: if your endpoint is down or doesn't answer 2xx within 10 seconds, that day's webhook delivery is lost, though the alerts are still on the Alerts page and in the email and Slack digests. Respond quickly, do slow work after responding, and use Send a test after changing your receiver.

The request

POST /your/path HTTP/1.1
Content-Type: application/json
User-Agent: Punchline-Webhook/1.0
Punchline-Signature: t=1791273600,v1=5f2c…

Redirects aren't followed: point the webhook at the final URL.

The payload

{
  "type": "punchline.digest",
  "subject": "Punchline: 2 alerts for shop.example on Oct 6",
  "text": "Shop (shop.example), Oct 6 (UTC):\n\n• Agent share spike: Agent share: 46% on Oct 6, up 64% on the usual 28%.\n  https://app.punchlineanalytics.com/dashboard?site=25cbb06c-e534-4f14-a740-785079ddc69c&range=30d\n• A company is researching you: A company's agents are researching you (7 days to Oct 6): acme.com (strong): agents read pricing, security and comparison pages 9 times.\n  https://app.punchlineanalytics.com/dashboard/accounts?site=25cbb06c-e534-4f14-a740-785079ddc69c&range=30d\n\nManage alerts: https://app.punchlineanalytics.com/dashboard/alerts?site=25cbb06c-e534-4f14-a740-785079ddc69c\nOne digest a day, only when something fired.",
  "data": {
    "site": {
      "id": "25cbb06c-e534-4f14-a740-785079ddc69c",
      "name": "Shop",
      "domain": "shop.example"
    },
    "day": "2026-10-06",
    "alerts": [
      {
        "name": "Agent share spike",
        "kind": "anomaly",
        "summary": "Agent share: 46% on Oct 6, up 64% on the usual 28%.",
        "report_url": "https://app.punchlineanalytics.com/dashboard?site=25cbb06c-e534-4f14-a740-785079ddc69c&range=30d",
        "detail": {
          "change_pct": 64
        }
      },
      {
        "name": "A company is researching you",
        "kind": "account_signal",
        "summary": "A company's agents are researching you (7 days to Oct 6): acme.com (strong): agents read pricing, security and comparison pages 9 times.",
        "report_url": "https://app.punchlineanalytics.com/dashboard/accounts?site=25cbb06c-e534-4f14-a740-785079ddc69c&range=30d",
        "detail": {
          "accounts": [
            {
              "company_domain": "acme.com",
              "strength": "strong",
              "score": 23,
              "summary": "Agents read pricing, security and comparison pages 9 times",
              "agent_sessions": 4,
              "operators": [
                "openai",
                "anthropic"
              ],
              "intents": [
                {
                  "intent": "pricing",
                  "agent_pageviews": 4
                },
                {
                  "intent": "security",
                  "agent_pageviews": 3
                },
                {
                  "intent": "comparison",
                  "agent_pageviews": 2
                }
              ],
              "last_seen": "2026-10-06"
            }
          ]
        }
      }
    ]
  }
}
Field
type Always punchline.digest for now. Check it, and ignore types you don't handle.
subject, text The same subject and text as the email, for posting somewhere as is
data.site The site's ID, name and domain
data.day The UTC day the alerts are about
data.alerts[] One entry per alert that fired: its name, kind (threshold, anomaly, new_operator, account_signal), a one-line summary and a report_url to the report behind it
data.alerts[].detail Structured detail, when the kind has some. anomaly: change_pct, the change from the usual. new_operator: operators, the new operators and the agents they used. account_signal: accounts, each company's domain, strength, score, summary, agent sessions, operators, top intents and the last day seen.

New fields may be added; ignore ones you don't know.

Verify the signature

Punchline-Signature is t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw body> using your signing secret. Check it against the raw request body (before any JSON parsing), compare in constant time, and reject timestamps more than 5 minutes old so a captured request can't be replayed.

Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyPunchline(header, rawBody, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(String(header ?? "").split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = String(parts.v1 ?? "");
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

With Express, read the raw body for this route:

app.post("/hooks/punchline", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyPunchline(req.get("punchline-signature"), raw, process.env.PUNCHLINE_WEBHOOK_SECRET)) return res.sendStatus(401);
  res.sendStatus(204); // answer first, then do the work
  handleDigest(JSON.parse(raw));
});

Python:

import hashlib, hmac, time

def verify_punchline(header: str, raw_body: bytes, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Recipes

HubSpot or Salesforce activity per company

For CRM write-back without a webhook, connect HubSpot or Salesforce directly: Punchline adds a note or task to the matching company. Use the webhook when you want your own logic, such as routing by territory or creating a task for the account owner:

  1. Add the A company is researching you alert.
  2. In your workflow tool, receive the webhook, verify it, and loop over data.alerts[] where kind is account_signal.
  3. For each item in detail.accounts, look up the company by company_domain and create an activity with summary and the report_url.

Zapier or Make

Use a Catch Hook (Zapier) or Custom webhook (Make) trigger. These tools can't check signatures, so put a long random token in the webhook URL's path and treat the URL as a secret. Split data.alerts into items to handle each alert separately.

Post somewhere other than Slack

subject and text are ready to post as they are: send them to Microsoft Teams, Discord or an internal chat with a few lines of code.

Privacy

Digests carry totals and summaries for each alert's own condition, operator and agent names, and for account signals the company's domain. Never paths of individual visits, sessions or anything about one person. Text that comes from traffic is cleaned of control characters before it's sent.

Ask the docs

Answers come from these docs only, written by Claude (Anthropic). Your question is sent to answer it, and isn't stored.