---
title: Webhooks
description: Receive feedback.post.created and feedback.post.status_changed events at your
  HTTPS endpoint, verify Standard Webhooks signatures, and handle retries.
---

Webhooks push feedback activity to your server as it happens. Register an HTTPS endpoint
in the Feeblo dashboard, pick the events you want, and your receiver gets a signed JSON
request each time something happens on a board. Use this page when you need to verify
signatures, deduplicate deliveries, or debug why an endpoint stopped receiving requests.

## Events

V1 emits two event types:

| Event                          | Fires when                                             |
| ------------------------------ | ------------------------------------------------------ |
| `feedback.post.created`        | Someone submits feedback on a board                     |
| `feedback.post.status_changed` | A moderator moves a post to a new status                |

You select these per route when you register the endpoint. The dashboard's "send test"
button delivers a `webhook.test` event instead. It travels through the same delivery and
signing path as real events, but you can't subscribe a route to it.

## Payload

Every request carries a versioned JSON envelope:

```json
{
  "id": "iev_example",
  "organizationId": "org_example",
  "type": "feedback.post.created",
  "version": 1,
  "occurredAt": "2026-08-11T00:00:00.000Z",
  "post": {
    "id": "pst_example",
    "title": "Example",
    "url": "https://app.feeblo.com/org_example/post/feedback/example"
  },
  "board": { "id": "brd_feedback", "name": "Feedback", "slug": "feedback" },
  "status": { "id": "pss_open", "type": "PENDING" },
  "actor": { "type": "end_user" }
}
```

The blocks break down like this:

- `post` holds the post ID, title, and absolute URL. `status.type` is one of `PENDING`,
  `REVIEW`, `PLANNED`, `IN_PROGRESS`, `COMPLETED`, or `CLOSED`.
- `board` identifies the board the post lives on.
- `previousStatus` appears on `feedback.post.status_changed` deliveries and names the
  status the post moved out of.
- `actor` classifies who caused the event: `end_user` for someone submitting through the
  widget or public board, `member` for a signed-in team member. Member events carry an
  optional `memberId` and `displayName`.

:::warning[What payloads leave out]
Payloads exclude post content, email addresses, credentials, and private organization
data. If you need the full post body, fetch it through your own admin access rather than
expecting it in the webhook. Treat every field as safe to log.
:::

## Verifying signatures

Each request carries Standard Webhooks headers plus one Feeblo-specific header:

| Header               | Meaning                                                       |
| -------------------- | ------------------------------------------------------------- |
| `webhook-id`         | Stable ID for this delivery. Survives retries.                 |
| `webhook-timestamp`  | Unix seconds for this attempt. Regenerated on every retry.     |
| `webhook-signature`  | HMAC signature for this attempt.                               |
| `x-feeblo-event`     | Event type, e.g. `feedback.post.created`.                      |

Requests arrive as `application/json` with `User-Agent: Feeblo-Webhooks/1`. Verify the
exact raw request bytes before you parse anything. A receiver that parses first and
verifies later accepts tampered bodies.

Install the [standardwebhooks](https://www.npmjs.com/package/standardwebhooks) library,
then verify with the secret Feeblo showed you at registration:

```package-install
standardwebhooks
```

```ts
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.FEEBLO_WEBHOOK_SECRET!);
const event = webhook.verify(rawBody, {
  "webhook-id": request.headers.get("webhook-id")!,
  "webhook-timestamp": request.headers.get("webhook-timestamp")!,
  "webhook-signature": request.headers.get("webhook-signature")!,
});
```

Secrets start with `whsec_`. Pass the whole string, prefix included.

## Rotating the signing secret

Rotate from the dashboard whenever someone leaves the team or a secret leaks. Feeblo
generates a new secret and shows it once. Store it immediately; nobody can retrieve it
afterward.

For 24 hours after rotation, Feeblo signs each delivery with both the new and the old
key, so in-flight deliveries stay verifiable while you deploy. During that window keep
both secrets configured and try each until verification succeeds. Once 24 hours pass,
drop the old secret.

## Retries and idempotency

Delivery is at-least-once. Your endpoint can receive the same delivery more than once, so
treat the `webhook-id` header as an idempotency key: skip processing when you've seen the
ID before, and return any 2xx status after successful handling.

A failed attempt schedules retries roughly 1 minute, 5 minutes, 30 minutes, 2 hours, 8
hours, and 24 hours later, with jitter of up to ±20% around each delay. That makes seven
attempts including the original request. Statuses 408, 409, 425, 429, and all 5xx codes
retry; other 3xx and 4xx codes end the delivery permanently. A valid `Retry-After` header
on a 429 response overrides the schedule, capped at 24 hours.

:::note[Respond within 10 seconds]
Feeblo abandons an attempt that hasn't finished after 10 seconds and treats it as a
transport failure. Do slow work after returning 2xx, or hand the job to a queue. Request
bodies stay under 256 KiB.
:::

## Endpoint requirements

Feeblo validates endpoints before saving them and again before every delivery:

- Production endpoints must use HTTPS. Development receivers may use plain HTTP.
- The URL can't contain credentials or a fragment.
- Hostnames must resolve to public addresses. Feeblo rejects localhost, private and
  reserved ranges, and cloud-metadata destinations.
- Feeblo resolves DNS once per delivery and pins the connection to those addresses, so a
  hostname can't swap to a private address between validation and connection.
- Feeblo doesn't follow redirects. Point the endpoint directly at your receiver.

Developing against a receiver on `localhost`? Set `INTEGRATION_ALLOW_PRIVATE_NETWORK=true`
in your Feeblo environment. The override only works when Feeblo runs in development mode
and defaults to false.

:::warning[Defense in depth]
Webhook validation complements infrastructure controls, never replaces them. Deny
private-network and cloud-metadata egress from your Feeblo host at the firewall or VPC
level, restrict receivers to traffic from expected sources, and verify signatures on
every request.
:::

## Next steps

<CardGroup cols={2}>
  <Card title="Triggers and events" href="/developers/triggers-and-events">
    The browser-side widget events webhooks complement.
  </Card>
  <Card title="Widget API" href="/developers/api">
    The HTTP endpoints behind the embedded widget.
  </Card>
  <Card title="Self-hosting environment" href="/self-hosting/environment">
    Every environment variable, including webhook egress settings.
  </Card>
</CardGroup>
