Docs
Feedi docs
Install the headless SDK, submit feedback, and wire signed webhook automation without extra platform surface.
Receive deliveries
Webhooks
Webhooks deliver each new submission to your own systems the moment it arrives. Every app has a default webhook that POSTs a Feedi-signed JSON payload to an HTTPS endpoint you control. On the Indie plan an app can add a second destination and post a ready-formatted message straight to Slack, Discord, or Telegram.
Request
Each default delivery is an HTTP POST to your endpoint with these headers:
Content-Type: application/jsonX-Feedi-Event— the event name, currently alwaysfeedback.created.X-Feedi-Timestamp— the Unix time (seconds) the delivery was signed.X-Feedi-Signature— a hex-encoded HMAC-SHA256 signature (see below).
POST https://example.com/feedi/webhook
Content-Type: application/json
X-Feedi-Event: feedback.created
X-Feedi-Timestamp: 1779746310
X-Feedi-Signature: 1b4f...raw_hex_digest
{
"event": "feedback.created",
"feedback": {
"id": "fbk_example123",
"appId": "app_123",
"appName": "Dogfood App",
"projectId": "proj_123",
"projectName": "Dogfood App",
"message": "I could not finish onboarding.",
"email": "user@example.com",
"metadata": {
"builtin": {
"appVersion": "2.4.0",
"buildNumber": "1180",
"iOSVersion": "18.5",
"locale": "en-US"
},
"custom": {
"screen": "onboarding"
}
},
"receivedAt": "2026-05-25T21:58:30.000Z"
}
}Payload fields
event— the event type, currently alwaysfeedback.created.feedback.id— the submission's short id, the same one shown in your inbox.feedback.appId/feedback.appName— the app the feedback came from.feedback.projectId/feedback.projectName— that app's project.feedback.message— the text your user submitted.feedback.email— the reporter's email if your app collected one, otherwisenull.feedback.metadata.builtin— optional SDK-provided fields such asappVersion,buildNumber,iOSVersion/androidVersion, andlocale.feedback.metadata.custom— the string key/values you attached at submit time.feedback.receivedAt— an ISO-8601 timestamp of when Feedi accepted the submission.
Verifying the signature
Feedi signs the string timestamp.raw_body — the X-Feedi-Timestamp value, a literal dot, then the exact request body — with your webhook's signing secret using HMAC-SHA256, hex-encoded. The secret is shown once when you create the webhook. Compute the same HMAC over the raw bytes you received (verify before parsing JSON), compare it to X-Feedi-Signature in constant time, and reject deliveries whose timestamp is too old to limit replay attacks.
import { createHmac, timingSafeEqual } from "node:crypto";
// Reject deliveries whose timestamp is too far off to blunt replay attacks.
const MAX_SKEW_SECONDS = 5 * 60;
// rawBody must be the exact bytes Feedi sent — read it before any JSON parsing.
function isValidFeediDelivery(rawBody, headers, signingSecret) {
const signature = headers["x-feedi-signature"];
const timestamp = headers["x-feedi-timestamp"];
if (!signature || !timestamp) return false;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (Number.isNaN(age) || age > MAX_SKEW_SECONDS) return false;
const expected = createHmac("sha256", signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time compare; lengths must match for timingSafeEqual.
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}Delivery & retries
Acknowledge a delivery by responding with any 2xx status; return quickly and do slower work asynchronously. On the Indie plan, Feedi retries transient failures — network errors and 408, 429, or 5xx responses — up to 5 attempts with backoff: immediate, 1 min, 5 min, 30 min, and 2 hr. Any other 4xx is treated as permanent and not retried. On the Starter plan the default webhook is delivered once with no retry. Because a delivery can arrive more than once, treat feedback.id as an idempotency key.
Slack, Discord & Telegram
On the Indie plan an app can add a second destination and choose Slack, Discord, or Telegram instead of the default type. Paste the platform's incoming webhook URL (Slack hooks.slack.com, Discord discord.com/api/webhooks/…) or, for Telegram, a bot token and chat id. Feedi posts a message already formatted for that platform rather than the signed JSON above, so there is nothing to build or verify on your side. The destination URL or bot token is the secret — keep it private and rotate it on the platform if it leaks.