Project webhooks
Send new feedback to your agent.
Each project can send signed feedback.created events to one HTTPS endpoint. Delivery is asynchronous and uses a durable retry queue.
Receiver safety
Feedback content and submitter email are customer data. Protect them in transit, in logs, and at rest.
At-least-once delivery
Store each delivery ID before processing. A retry uses the same ID, so your receiver can ignore duplicates.
Payload
Optional values are present as null. Attachments are represented only by attachmentCount.
feedback.created
{
"id": "event_cuid",
"type": "feedback.created",
"createdAt": "2026-08-18T12:34:56.000Z",
"project": { "id": "project_cuid", "name": "Example" },
"feedback": {
"id": "feedback_cuid",
"content": "The save button does not work.",
"email": "person@example.com",
"status": "OPEN",
"category": "BUG",
"sentiment": "NEGATIVE",
"summary": "The save action fails.",
"priority": 82,
"pageUrl": "https://example.com/settings",
"browser": "Chrome",
"os": "Windows",
"device": "desktop",
"attachmentCount": 1,
"createdAt": "2026-08-18T12:34:55.000Z"
},
"analysis": { "status": "complete" }
}Headers and signature
Sign the exact raw body. Do not parse and serialize it before verification.
Node.js receiver
import { createHmac, timingSafeEqual } from "node:crypto";
const timestamp = request.headers.get("x-feedbackbasket-timestamp");
const received = request.headers.get("x-feedbackbasket-signature");
const rawBody = await request.text();
const expected = "v1=" + createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(timestamp + "." + rawBody, "utf8")
.digest("hex");
const valid = received !== null &&
Buffer.byteLength(received) === Buffer.byteLength(expected) &&
timingSafeEqual(Buffer.from(received), Buffer.from(expected));| X-FeedbackBasket-Event | feedback.created or webhook.test |
|---|---|
| X-FeedbackBasket-Delivery | Stable delivery ID |
| X-FeedbackBasket-Timestamp | Unix seconds |
| X-FeedbackBasket-Signature | v1=<lowercase HMAC hex> |
Delivery rules
- Requests stop after 10 seconds and never follow redirects.
- Private, local, link-local, multicast, and unspecified targets are blocked.
- HTTP 408, 429, and 5xx responses retry up to five total attempts.
- Retries wait about 1, 5, 25, and 125 minutes. Valid 429 Retry-After values are capped at 24 hours.
- HTTP 410 stops retries and pauses the endpoint.
- A rotated secret takes effect immediately. Update the receiver before you send more events.