Notifications
fdbck closes three loops. Channels keep you in the loop — new feedback, opened PRs, merged fixes, and failed runs pushed to Slack, Discord, Telegram, or your own webhook. Reporter email closes the loop with the person who reported the issue — one email when their fix ships, nothing else, ever. And board subscribers — anyone who upvoted a public item and asked to be told — get one email when that request ships (see below).
Channels
Channels live in Account → Notifications. You can have up to 10; each one is a destination (Slack, Discord, Telegram, or a generic webhook), a set of per-event toggles, and an optional project scope — by default a channel receives events from all your projects, or you can pin it to a single one. Every channel has a Send test button that fires a sample event so you can verify the wiring end to end.
Slack
Slack channels use an incoming webhook. In Slack, go to api.slack.com/apps → your app (create one if needed) → Incoming Webhooks → activate them → Add New Webhook to Workspace, and pick the channel messages should land in. Copy the URL and paste it into a new Slack channel in Account → Notifications.
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
Only hooks.slack.com URLs are accepted — the URL itself is the credential, so treat it like a secret.
Discord
In Discord, open the target channel's settings (the gear next to its name) → Integrations → Webhooks → New Webhook, then Copy Webhook URL and paste it into a new Discord channel in Account → Notifications.
https://discord.com/api/webhooks/000000000000000000/XXXXXXXXXXXXXXXXXXXXXXXX
Only discord.com/api/webhooks (or the legacy discordapp.com/api/webhooks) URLs are accepted. Same deal: the URL is the credential.
Telegram
Telegram needs two values: a bot token and a chat id.
- Bot token. Message
@BotFather, send/newbot, and follow the prompts. You get a token shaped like123456:ABC-DEF…. - Chat id. Message
@userinfobotto get your own numeric id. For a group, add your bot to the group and use the group's (negative) numeric id instead.
One Telegram gotcha: bots can't message anyone first. Open a chat with your bot and hit Start (or add it to the group) before you hit Send test — otherwise deliveries fail with a “chat not found” error.
Generic webhook
The escape hatch for everything else — Zapier, n8n, a custom service. fdbck sends a JSON POST to any https URL you give it, one request per event. Add an optional signing secret and every request is HMAC-signed so you can verify it really came from fdbck.
Payload
{
"event": "pr.opened",
"project": "acme/webapp",
"item": {
"id": "b1f0c3…",
"summary": "Export button on the reports page does nothing",
"url": "https://fdbck.app/acme/webapp/runs/b1f0c3…"
},
"detail": "https://github.com/acme/webapp/pull/128",
"sentAt": "2026-07-03T17:24:08.301Z"
}detail is the optional second line (a PR URL, an error headline) and is null when there isn't one. item.url deep-links to the item in your dashboard.
Headers
| Header | Sent | Value |
|---|---|---|
| Content-Type | always | application/json |
| User-Agent | always | fdbck-notify/1 |
| X-Fdbck-Event | always | The event type, same as the body's event field — route on it without parsing. |
| X-Fdbck-Delivery | always | A unique UUID per delivery. |
| X-Fdbck-Signature | secret set | sha256=<hex> — HMAC-SHA256 of the exact raw request body, keyed with your signing secret. |
Verify the signature
When a secret is set, X-Fdbck-Signature is sha256= followed by the hex HMAC-SHA256 of the exact raw request body, keyed with your secret. Compute the same HMAC over the bytes you received — before any JSON parsing or re-serialization — and compare with a timing-safe equality check.
import { createHmac, timingSafeEqual } from "node:crypto";
function isValidFdbckSignature(rawBody, signature, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signature ?? "");
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
// Express example — capture the raw body, verify, then parse:
app.post("/hooks/fdbck", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.get("X-Fdbck-Signature");
if (!isValidFdbckSignature(req.body, sig, process.env.FDBCK_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
res.sendStatus(200); // respond fast — fdbck times out after 6s
const event = JSON.parse(req.body);
// …handle event
});Events
Five events, toggled per channel. Each maps one-to-one to a moment in the fdbck loop.
| Event | Label | When it fires |
|---|---|---|
| feedback.created | New feedback | A submission landed in your inbox — from the widget or the API. The message shown is the PII-scrubbed copy. |
| fix.parked | Fix parked for review | With “Review before opening” on, the worker parked a fix for your approval instead of opening a PR. It's blocked on you — approve or discard it from the dashboard. |
| pr.opened | Fix PR opened | Your worker opened a fix PR for an item. Includes the PR link. |
| item.fixed | Fixed & merged | The fix PR was merged. Fires once per item — and also triggers the reporter email below. |
| run.failed | Agent run failed | A worker run ended in an error. Includes the first line of the error. |
Reporter email: “we'll tell you when it's fixed”
The widget's form includes an optional email field — “Email — optional, we'll tell you when it's fixed.” If a reporter fills it in, fdbck sends them exactly one email when the fix for their item is merged: a “Your feedback shipped” note with a scrubbed echo of what they reported and a link to their private status page. No drip, no follow-ups, no marketing.
That email also carries a one-click mute link. Clicking it immediately deletes the stored address for that one item — no account, no unsubscribe center — and lands the reporter back on their status page.
The address stays private. It's stored separately from the feedback content, never appears on any public surface (the board, the status page), and is never included in what the AI agent sees. It's deleted when the reporter mutes or when the item is deleted. A malformed address is silently dropped at submission — it never blocks the report.
Posting directly to the API? Pass the same email field in the body — see the Feedback API. Shipped emails go out only when the fdbck server has a mail provider configured (fdbck.app does).
Board subscribers: “email me when this ships”
On a public board, anyone who upvotes an item can also ask to be told when it ships. When the fix for that item is merged, fdbck sends each subscriber exactly one email — “A request you upvoted just shipped” — with the item's public title, a scrubbed summary, and a link to the PR. Every email carries a one-click unsubscribe link, and the same at-most-once, mailer-gated rules as the reporter email apply.
Public-safe by construction. The board is anonymous — a subscriber's address is stored only for the unsubscribe link and the ship notification, never shown on the board, and the email contains only the public title and scrubbed summary, never a reporter's raw text. Building your own board? The subscribe/unsubscribe endpoints are in the Board API.
Delivery semantics
- Best-effort, never blocking. Notifications are fire-and-forget. A delivery failure never fails the thing that triggered it — a feedback submission never errors because Slack was down.
- 6-second timeout per destination. Webhook consumers should respond with a 2xx quickly and do their work after.
- No retries (v1). A missed delivery is missed — treat channels as a convenience layer, not a system of record. The dashboard always has the full state.
- Health is visible. Each channel row in Account → Notifications shows when it last delivered and the last error if one occurred. Use Send test whenever you're unsure.