SnagFrog
Compare Guides Docs Privacy Terms Sign in

Webhooks, Discord and Slack

Last updated 12 October 2026

Each app can send its events to a Discord or Slack channel, or to any HTTPS address as signed JSON. Set them up in the app's Integrations tab; each destination has a Test button that delivers a sample report.

Events

EventWhen
report.createdA new report arrives. Reports locked for being over the plan's limit are skipped.
report.reporter_repliedThe reporter replies by email or on the tracking page. The body has a "message".
report.status_changedSomeone, or Claude over MCP, changes the status. The body has a "change" with "from", "to" and the actor.
report.staff_messageA teammate replies or adds an internal note. Off by default.

Discord and Slack

Paste a channel webhook address. Messages show the report's number, type, priority and source with a link; a new report's message has its title too. Turn on "Include the description and reporter…" only for private channels.

JSON webhook

A POST to your HTTPS address with these headers:

  • X-Snag-Event: the event name.
  • X-Snag-Delivery: unique per delivery; a retry carries the same one.
  • X-Snag-Signature: t=<unix time>,v1=<hex>, an HMAC-SHA256 of t.body with the integration's secret.
{
  "id": "…",
  "event": "report.created",
  "created_at": "2026-10-12T09:30:00Z",
  "app": { "id": "…", "slug": "your-app", "name": "Your App" },
  "report": {
    "id": "…", "number": 42, "kind": "bug",
    "title": "Export crashes on Sonoma", "description": "…",
    "status": "open", "priority": "none", "labels": [], "source": "web",
    "url": "https://snagfrog.com/dashboard/reports/…",
    "reporter": { "name": "Jane", "email": "jane@example.com" },
    "diagnostics": { "version": "1.4.2", "os": "macOS 15.1" },
    "created_at": "2026-10-12T09:30:00Z"
  }
}

Checking the signature

Check it against the raw body, before parsing, and refuse anything older than a few minutes:

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

function verify(rawBody, header, secret, toleranceSeconds = 300) {
  try {
    const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
    if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
    const expected = Buffer.from(createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'));
    const given = Buffer.from(v1);
    return expected.length === given.length && timingSafeEqual(expected, given);
  } catch {
    return false;
  }
}

Retries

  • A delivery that fails with 429, a 5xx status, a network error or a timeout is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. A Retry-After header can lengthen a wait.
  • Other 4xx answers are final.
  • After 20 failures in a row the integration turns itself off and the workspace is emailed.

Issue trackers

Also on the Studio plan: connect GitHub or Linear and create an issue from a report with one click. That is a separate connection under Settings → Connections, not a webhook.

Questions

Which plan includes webhooks, Discord and Slack?

The Studio plan ($29 a month), and the 30-day trial, which includes Studio features. On the Indie plan, each report can be copied as Markdown.

Is the text of internal notes ever posted to a channel?

Not to Discord or Slack: the staff-message event is off by default, and when it is on, their messages never contain the text of an internal note. The JSON webhook is different: it goes to your own server, and its "message" does carry the note.

How do I ignore a delivery I already handled?

Every delivery has a unique X-Snag-Delivery header, which is also the "id" in the body. A retry carries the same one, so store the ids you have handled and skip repeats.

More reference pages

  • MCP server reference
  • Web widget reference
  • Errors API reference
  • Changelog reference

Start a free 30-day trial · All docs · Guides · Compare with other tools · Changelog

SnagFrog by Karakabakov Privacy · Terms · DPA