Webhooks
Receive signed real-time Loop events, verify the Loop-Signature HMAC SHA-256 header, handle feedback.created and feedback.resolved, and use retries.
Overview
Webhooks let your systems react to feedback the moment it changes. When an event occurs, Loop sends an HTTP POST to the endpoint you configure, with a JSON body and a signature header. Delivery is exactly-once under normal conditions, with automatic retries and exponential backoff if your endpoint is unavailable.
Configure your endpoint URL and copy its signing secret from the Loop dashboard. The secret is used to verify every payload.
Event types
Each delivery carries an event type describing what happened:
| Event | Fires when |
|---|---|
feedback.created |
A new feedback item is created |
feedback.resolved |
An item's status changes to resolved |
Payload shape
The request body is a JSON envelope. The data field contains the full feedback item, identical to the API response shape.
{
"id": "evt_5b8e",
"type": "feedback.created",
"createdAt": "2026-06-19T14:02:11Z",
"data": {
"id": "fb_3a91",
"user": { "id": "u_8f2c" },
"message": "The CSV export timed out on large datasets.",
"sentiment": "negative",
"source": "in-app-widget",
"tag": "exports",
"status": "open",
"createdAt": "2026-06-19T14:02:11Z"
}
}
Every request also includes signature headers:
| Header | Description |
|---|---|
Loop-Signature |
HMAC SHA-256 of the raw request body, hex-encoded |
Loop-Timestamp |
Unix timestamp (seconds) of when the event was sent |
Loop-Event-Id |
Stable event id, useful for idempotency |
Verifying the signature
The Loop-Signature header is an HMAC SHA-256 of the raw, unparsed request body using your endpoint's signing secret. Always verify it before trusting a payload, and always compute the HMAC over the raw bytes — re-serializing parsed JSON will change the bytes and break verification.
const crypto = require('crypto');
function verifyLoopSignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader || '');
// Constant-time comparison to avoid timing attacks.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Wire it into an Express handler. Note the use of the raw body parser so the bytes are unchanged:
const express = require('express');
const app = express();
app.post(
'/webhooks/loop',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.header('Loop-Signature');
const secret = process.env.LOOP_WEBHOOK_SECRET;
if (!verifyLoopSignature(req.body, signature, secret)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
switch (event.type) {
case 'feedback.created':
// route the new item to triage
break;
case 'feedback.resolved':
// notify the original reporter
break;
}
// Acknowledge quickly with a 2xx.
res.status(200).send('ok');
}
);
Responding and retries
Return a 2xx status code as soon as you have accepted the event — do the heavy work asynchronously. Any non-2xx response, a timeout, or a connection failure marks the delivery as failed and schedules a retry.
Retries use exponential backoff, spreading attempts over an increasing interval:
| Attempt | Approximate delay after the previous try |
|---|---|
| 1 | immediate |
| 2 | ~1 minute |
| 3 | ~5 minutes |
| 4 | ~30 minutes |
| 5 | ~2 hours |
| 6 | ~6 hours |
After the final attempt, the delivery is marked failed and surfaced in the dashboard, where you can replay it manually.
Idempotency and exactly-once delivery
Loop targets exactly-once delivery, but retries after an ambiguous failure can occasionally deliver the same event twice. Make your handler idempotent by de-duplicating on Loop-Event-Id:
if (await alreadyProcessed(req.header('Loop-Event-Id'))) {
return res.status(200).send('duplicate ignored');
}
Next steps
You can now react to feedback in real time. Pair webhooks with the Feedback API to enrich, tag, and resolve items, or revisit Authentication to manage the keys behind your integration.