Skip to Content
CEC Feedback Docs

Webhooks

Receive signed JSON events from CEC Feedback at your own endpoint, and verify them.

A webhook integration makes CEC Feedback POST a signed JSON body to a URL you run whenever one of the events you picked happens in the project.

Set it up

  1. Open the project's Integrations tab and turn on Webhook.
  2. Enter a Name and your endpoint's URL, and choose the Events.
  3. Save. CEC Feedback shows the signing secret (whsec_…) once: store it with your receiver. If you lose it, delete the integration and connect a new one.

Test in the integration's Settings sends a ping event, so you can check your endpoint.

Events

Eventtypedata
New feedbackfeedback.created{ feedbackId }
Feedback editedfeedback.updated{ feedbackId, changes }, with changes.<field> set to [before, after] for the title, description, visibility or tags
Status changedfeedback.status_changed{ feedbackId, from, to }, each open or resolved
Feedback archivedfeedback.archived{ feedbackId }
New commentcomment.created{ feedbackId, commentId }
Comment editedcomment.updated{ feedbackId, commentId }
Comment deletedcomment.deleted{ feedbackId, commentId }
Testping{}, and no feedback

Payload

{
  "id": "evt_01JC8Z6Q0M8T3V7R2K9W4X5Y6Z",
  "type": "feedback.created",
  "occurredAt": "2026-11-05T09:30:00.000Z",
  "projectId": "prj_01JC8Z5V1B2C3D4E5F6G7H8J9K",
  "data": { "feedbackId": "fb_01JC8Z6P9A1B2C3D4E5F6G7H8J" },
  "feedback": {
    "id": "fb_01JC8Z6P9A1B2C3D4E5F6G7H8J",
    "projectId": "prj_01JC8Z5V1B2C3D4E5F6G7H8J9K",
    "number": 142,
    "kind": "screenshot",
    "title": "Hero button overlaps the menu",
    "status": "open",
    "visibility": "everyone",
    "pageUrl": "https://example.com/pricing?utm_source=x",
    "pageUrlNormalized": "https://example.com/pricing",
    "anchor": null,
    "thumbnailAttachmentId": "att_01JC8Z6P7Q8R9S0T1V2W3X4Y5Z",
    "commentCount": 0,
    "reporter": { "id": "rep_01JC8Z6M2N3P4Q5R6S7T8V9W0X", "name": "Jane Doe", "email": null },
    "tagIds": [],
    "consoleErrorCount": 2,
    "screenshotStatus": "pending",
    "screenshotMethod": null,
    "createdAt": "2026-11-05T09:30:00.000Z",
    "updatedAt": "2026-11-05T09:30:00.000Z",
    "resolvedAt": null,
    "createdByUserId": null,
    "designId": null
  }
}

feedback is the item as it is when the delivery runs, not when the event happened. Fetch anything else through the REST API. To protect reporters and the sites they report on, a payload never includes reporter emails (reporter.email is always null), console or network logs, or file URLs: attachments are referenced by ID only.

Headers

HeaderValue
Content-Typeapplication/json
User-AgentCEC-Feedback-Webhooks/1
X-Feedback-EventThe event type, such as feedback.created
X-Feedback-Event-IdThe event id, the same on every retry
X-Feedback-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>

Verifying the signature

v1 is the lowercase hex HMAC-SHA256, keyed with your whsec_… secret, of the string "<t>.<raw body>". Check it before you trust anything in the body:

  1. Use the raw request body exactly as received. Parsing and re-serialising the JSON changes the bytes.
  2. Recompute the HMAC and compare it to v1 in constant time.
  3. Reject timestamps more than 5 minutes away from your clock, so a captured request can't be replayed later.

In Node.js:

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

function verifyFeedbackSignature(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {
  const parts = Object.fromEntries(header.split(',').map((part) => part.split('=', 2)));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || Math.abs(nowSeconds - timestamp) > 300) {
    return false;
  }
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? '', 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}

With Express, read the body raw (express.raw({ type: 'application/json' })), pass req.body.toString('utf8') as rawBody, and answer 401 when the check fails.

Responses and retries

Answer within 10 seconds. CEC Feedback doesn't read the response body.

Your responseWhat CEC Feedback does
2xxDelivered.
408, 429, 5xx, a timeout or a network errorRetries. A 429 with Retry-After: <seconds> is retried no sooner than that.
410 GoneStops, and pauses the integration. Turn it back on once the endpoint is back.
A 3xxFails without retrying. Redirects aren't followed, so give the final URL.
Any other 4xxFails without retrying. The integration shows the error.

Retries back off exponentially: 10 s, 20 s, 40 s, 80 s, 160 s, 320 s, 640 s, then 900 s, so the first attempt plus up to 8 retries over about 36 minutes. After that the delivery is marked failed, and Retry failed in the integration's Settings sends it again.

Duplicates and order

Delivery is at least once: a retry, or a delivery that worked but whose answer was lost, can bring the same event twice. Deduplicate on X-Feedback-Event-Id (the same as the body's id). Events can also arrive out of order, so use occurredAt, or fetch the current state from the API, when order matters.

Allowed URLs

The URL must use https: and point at a public host. CEC Feedback refuses localhost, *.localhost and *.internal hosts, and IP addresses in private, loopback or link-local ranges, both when you save the integration and before every send.

On this page