← back to blog
    Berner SetterwallOctober 2, 20266 min read

    Webhooks: Cogny Events Push Themselves Now

    Cogny already does the work unattended. Scheduled audits run overnight. Alerts watch budget pacing and metric targets every hour. Loops pick up growth work over MCP, raise a budget, pause a losing ad set, publish a post, and measure what it did.

    The results, though, sat still and waited to be collected. You opened the app, or you asked the chat what happened since Friday. For a solo operator that's fine — it's one workspace and you're in it daily. For an agency running fifteen clients, "go look" doesn't scale, and neither does "ask the AI for an update," because you have to know to ask.

    Webhooks invert it. You pick the events you care about, hand Cogny an https URL, and each event arrives as a signed JSON POST the moment it happens.


    The eight events

    EventFires when
    report.completedA scheduled report or audit finishes. Carries the title, the executive summary, the section list, and a link to the full report.
    alert.triggeredA budget-pacing or metric-target alert crosses its threshold. Carries current value, target, and deviation.
    alert.resolvedThat alert returns to normal and closes.
    ticket.createdCogny files a new recommendation — an audit finding, an experiment idea, a bug.
    ticket.status_changedA recommendation moves on the board, with the previous and new status.
    campaign.updatedAn agent writes a change to a connected platform: a budget, bid, status, creative, or audience update. One event per write.
    loop_run.completedA loop run has been evaluated. Carries the outcome (success, partial or failed), the summary, the measured metrics and the estimated dollar impact when there is one.
    task.shippedAn agent published something on a connected platform — a campaign, an ad, a blog post, a page, a site. Carries what shipped, where, and a link to it.

    alert.resolved is there on purpose. An alert that fires into a Slack channel and never visibly closes trains people to ignore the channel.

    loop_run.completed is the one to wire into a results channel: it arrives once the run's impact has been measured, so the message says what the loop actually achieved, not just that it ran.

    campaign.updated is the one agencies tend to reach for first. Every write-class MCP call an agent makes — on any connected platform — produces one event, which makes it a live feed of what changed in the ad accounts, separate from whatever the agent said it was doing.

    What a payload looks like

    Every event uses the same envelope:

    {
      "id": "5f8b1c2e-...",
      "event": "alert.triggered",
      "created_at": "2026-09-03T08:14:22.104Z",
      "warehouse_id": "9c1e...",
      "text": "🚨 Alert: Google Ads pacing 42% over plan. → https://app.cogny.com/warehouse/9c1e.../alerts?incident=b21f...",
      "data": {
        "alert": {
          "incident_id": "b21f...",
          "type": "budget_pacing",
          "severity": "critical",
          "title": "Google Ads pacing 42% over plan",
          "description": "Spend is tracking to $14,200 against a $10,000 monthly budget.",
          "url": "https://app.cogny.com/warehouse/9c1e.../alerts?incident=b21f..."
        },
        "measurement": {
          "current_value": 14200,
          "target_value": 10000,
          "deviation_percent": 42,
          "platform": "google_ads"
        },
        "resolution": null,
        "workspace": { "id": "9c1e...", "name": "Northwind", "url": "https://app.cogny.com/warehouse/9c1e..." }
      }
    }
    

    Two deliberate choices in there. Every payload carries the human-readable label and the id, so a Slack message can be composed straight from the body with no follow-up API call. And every payload carries a deep link, because the point of a notification is that someone clicks it.

    Verifying that it came from Cogny

    Your endpoint is a public URL. Anyone who finds it can POST to it, so verify before you act.

    Each delivery carries:

    X-Cogny-Signature: t=1800000000,v1=<hex hmac-sha256>
    X-Cogny-Event: alert.triggered
    X-Cogny-Delivery: 5f8b1c2e-...
    X-Cogny-Webhook-Id: 7a3d...
    

    The signed message is `${t}.${rawBody}` and the key is your endpoint's signing secret. It's the same scheme Stripe and GitHub use, which means most webhook libraries already verify it and you can write the check in ten lines:

    import { createHmac, timingSafeEqual } from 'crypto';
    
    function verify(rawBody, header, secret) {
      const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
      const timestamp = Number(parts.t);
    
      // Reject replays: the timestamp is inside the signed material.
      if (!Number.isFinite(timestamp)) return false;
      if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
    
      const expected = createHmac('sha256', secret)
        .update(`${timestamp}.${rawBody}`)
        .digest('hex');
    
      const a = Buffer.from(expected);
      const b = Buffer.from(parts.v1 ?? '');
      return a.length === b.length && timingSafeEqual(a, b);
    }
    

    Verify against the raw body, before any JSON parsing — re-serialising changes the bytes and the signature won't match.

    What happens when your endpoint is down

    It will be, at some point. A deploy, a rate limit, an expired certificate.

    Cogny attempts each delivery immediately, and a failure is queued and retried at roughly one minute, five minutes, thirty minutes, two hours, six hours, and twelve hours — seven attempts spanning about twenty hours, then abandoned. So a receiver that breaks on a Friday evening is still being retried on Saturday morning.

    Two responses mean something specific:

    • Any 2xx is success. Return it as soon as you've accepted the body; don't hold the connection open while you do work.
    • 410 Gone disables the endpoint. It's the conventional "stop sending" and Cogny treats it that way.

    Twenty consecutive failed deliveries also disables the endpoint, with a reason you can read in Settings. Re-enabling clears the failure count.

    Every attempt is in the delivery log next to the endpoint — event, status, attempt count, the response code, and the error string your endpoint returned. When Slack didn't get something, that's where the answer is.

    A note on URLs we won't call

    Webhook targets have to be public https. Cogny rejects plain http, embedded credentials, private and reserved IP ranges, link-local addresses, and internal hostnames — at save time and again immediately before every single POST, because DNS can be re-pointed after you save.

    If your receiver lives inside a private network, put a public relay in front of it.

    Which plans include webhooks

    Webhooks are included on Cogny Cloud and on AppSumo Tier 3 and Tier 4. On other plans the Webhooks tab isn't shown, and the API answers with the plan that would include it.

    What webhooks don't do (yet)

    A webhook is a signed POST to a URL — that's the whole integration. There are no built-in email or CRM connectors in this first version. Slack works directly: paste a Slack incoming-webhook URL and each event appears as a one-line message, because every event carries a ready-made text field with a link. For HubSpot, Salesforce, Klaviyo or email, point the endpoint at a Zapier or Make catch hook and map the fields there.

    Getting started

    Settings → Webhooks → Add endpoint. You need the owner or admin role on the workspace — choosing where a workspace's report content gets sent is a data-egress decision, so it isn't open to every seat.

    Point it at a Slack incoming-webhook URL, a Zapier or Make catch hook, or your own handler. Pick two or three events you'd genuinely act on rather than all eight — a channel that gets everything gets muted. Then hit Test: it sends a sample event through the identical signed path a real one takes, and tells you what your endpoint said back.