Skip to content
Feedi

Docs

Feedi docs

Install the headless SDK, submit feedback, and wire signed webhook automation without extra platform surface.

Platform

Set up the SDK

Install

Add Feedi with your platform package manager in the app target that owns feedback UI.

Swift Package Manager · Package.swift
dependencies: [
    .package(url: "https://github.com/getfeedi/feedi-ios-sdk", from: "1.0.1")
]

In Xcode, choose File > Add Package Dependencies and paste the repository URL, or add the .package(...) line to Package.swift.

Configure

Create or select a project in the dashboard, add an app, and configure the SDK with that app's public write key.

Swift
import Feedi

Feedi.configure(apiKey: "feedi_eu_pk_...")

Copy the app write key from the setup panel after app creation. Public write keys are safe to embed in an app binary because the server stores only key hashes and metadata.

Submit feedback

Submit feedback from your own UI. The SDK sends the request to POST /v1/feedback and returns a receipt.

Swift
// submitFeedback is async and throws — call it from a Task or any async
// context; failures are thrown as FeediError.
let receipt = try await Feedi.submitFeedback(
    message: "The settings screen is confusing.",
    userEmail: "user@example.com",
    customMetadata: [
        "screen": "settings"
    ]
)

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/json
  • X-Feedi-Event — the event name, currently always feedback.created.
  • X-Feedi-Timestamp — the Unix time (seconds) the delivery was signed.
  • X-Feedi-Signature — a hex-encoded HMAC-SHA256 signature (see below).
feedback.created · HTTP
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 always feedback.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, otherwise null.
  • feedback.metadata.builtin — optional SDK-provided fields such as appVersion, buildNumber, iOSVersion / androidVersion, and locale.
  • 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.

Verify a delivery · Node.js
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.

Read your feedback

REST API

Read your feedback back out over a small, read-only REST API — from scripts, dashboards, or agentic coding tools like Claude Code and Codex. It's available on the Indie plan and authenticated with an account-wide secret key. Everything here is also described by an OpenAPI 3.1 spec your tools can read directly.

Base URL & authentication

The API is served at https://api.feedi.dev and is read-only. Authenticate every request with an account-wide secret key in the Authorization header:

List recent feedback · curl
# Create a key at /account/api-keys, then:
export FEEDI_API_KEY="feedi_eu_sk_…"

curl -H "Authorization: Bearer $FEEDI_API_KEY" \
  "https://api.feedi.dev/v1/feedback?status=new&limit=25"

Create and revoke keys under Account → API keys (Indie plan). A key looks like feedi_eu_sk_… (or feedi_us_sk_… for US accounts) and reads across all of your projects and apps. Treat it like a password: it’s a server-side credential, so keep it out of browsers, mobile apps, and public repos. A missing or invalid key returns 401; a key on a plan without API access returns 403.

Endpoints

  • GET /v1/feedback — list feedback, newest first. Query params: status (new default, or archived), app, project, tag, limit (1–100, default 25), and cursor.
  • GET /v1/feedback/{id} — one item by its short id, including built-in and custom metadata and per-channel delivery status.
  • GET /v1/projects — your projects.
  • GET /v1/apps — your apps, optionally scoped with ?project=.
  • GET /v1/tags — distinct tags with feedback counts, optionally scoped with ?app= or ?project=.

Pagination

Lists are keyset-paginated. Each response carries a next_cursor; pass it back as the cursor query param to fetch the next page, and stop when next_cursor is null. limit caps at 100.

Filter and paginate · curl
# Filter by app, then walk pages with the cursor from the previous response.
curl -H "Authorization: Bearer $FEEDI_API_KEY" \
  "https://api.feedi.dev/v1/feedback?app=APP_ID&cursor=NEXT_CURSOR"

# Discover the app, project, and tag ids you can filter by.
curl -H "Authorization: Bearer $FEEDI_API_KEY" "https://api.feedi.dev/v1/apps"

Response shape

List endpoints return { "data": [...], "next_cursor": … }; single items and discovery endpoints return { "data": … }. Field names are snake_case. A feedback item:

GET /v1/feedback · response
{
  "data": [
    {
      "id": "fbk_a1b2c3",
      "message": "I couldn't finish onboarding.",
      "reporter_email": "user@example.com",
      "status": "new",
      "received_at": "2026-06-19T21:58:30.000Z",
      "tags": ["onboarding", "bug"],
      "app": { "id": "8f3a…", "name": "Dogfood App", "platform": "ios" },
      "project": { "id": "2a17…", "name": "Dogfood" }
    }
  ],
  "next_cursor": "eyJyIjoxNz…"
}

The detail endpoint adds a feedback item’s metadata and delivery status:

GET /v1/feedback/{id} · response
{
  "data": {
    "id": "fbk_a1b2c3",
    "message": "I couldn't finish onboarding.",
    "reporter_email": "user@example.com",
    "status": "new",
    "received_at": "2026-06-19T21:58:30.000Z",
    "tags": ["onboarding", "bug"],
    "app": { "id": "8f3a…", "name": "Dogfood App", "platform": "ios" },
    "project": { "id": "2a17…", "name": "Dogfood" },
    "metadata": {
      "built_in": { "appVersion": "2.4.0", "buildNumber": "1180", "iOSVersion": "18.5", "locale": "en-US" },
      "custom": { "screen": "onboarding" }
    },
    "delivery": [
      { "channel": "email",  "status": "sent", "external_url": null },
      { "channel": "github", "status": "sent", "external_url": "https://github.com/acme/app/issues/42" }
    ]
  }
}
  • id — the submission’s short id, the same one shown in your inbox.
  • reporter_email — the reporter’s email if your app collected one, else null.
  • received_at — ISO-8601 timestamp of when Feedi accepted the submission.
  • metadata.built_in / metadata.custom — the SDK-provided and custom fields attached at submit time (detail only).
  • delivery — one entry per channel (email, webhook, github) with its status and an external_url when there is one, such as a created GitHub issue (detail only).

Rate limits

Each key is limited per minute and per day. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix seconds). When you exceed the limit the API returns 429 with a Retry-After header — wait for the reset and retry.

Errors

Errors are JSON: { "error": "code", "message"?: "…" }. Common codes: 400 invalid_query / invalid_cursor, 401 unauthorized, 403 api_access_requires_indie, 404 not_found, and 429 rate_limited.

Use it from Claude Code, Codex & agents

The API is described by an OpenAPI 3.1 spec at api.feedi.dev/v1/openapi.json, with a short summary at api.feedi.dev/llms.txt. Point any agent, the Claude Agent SDK, or Codex at the spec and it can call the endpoints directly. For Claude Code, install the Feedi plugin with /plugin marketplace add getfeedi/feedi-claude-plugin — it ships a skill and a /feedback command. Export your key as FEEDI_API_KEY first.