Feedback API
The <script> widget is the fastest way to collect feedback, but you don't have to use it. If you're building your own submission UI — a settings page, a support modal, a CLI — post directly to the ingestion endpoint. It's the same API the widget calls.
Submit feedback
A single POST. Your widget key authenticates the request — it's a public embed key, safe to ship in client code. It routes feedback to your project; it is not your worker token.
POST https://fdbck.app/api/v1/feedback Content-Type: application/json X-Widget-Key: wk_your_key_here X-Idempotency-Key: 8f3b… (optional)
Headers
X-Widget-Key is required. Send Content-Type: application/json too — the body is parsed as JSON regardless, but it's the correct content type. X-Idempotency-Key is optional: pass any UUID and a repeat with the same key within 24 hours returns the original submission instead of creating a duplicate. Mint one key per composed message and reuse it across retries — a fresh key per attempt defeats the point.
Body fields
Only message is required. Everything else is optional context that helps triage.
| Field | Type | Description |
|---|---|---|
| message | string · required | The feedback itself, 1–2000 characters. |
| type | "bug" | "feature" | "other" | A hint only. Triage assigns the authoritative type — this just seeds it. |
| url | string · optional | The page the feedback is about. Absolute URL, ≤ 2000 chars. |
| user_id | string · ≤ 200 | Your own identifier for the submitter. Opaque to fdbck. |
| string · ≤ 320 | Submitter email for the one-time fixed notification. Malformed addresses are silently dropped — never a rejection. | |
| pageContext | object · optional | Structured page context (route, title, headings). The auto-captured fields are re-scrubbed server-side; host-declared fields (data-context, source root, component, version) are trusted and passed through as-is — keep secrets out of them. |
| capture | object · optional | A repro bundle: { screenshot?, diagnostics? }. Only stored when the project has opted in (Settings → Repro capture). screenshot = { dataUrl, w, h } — a jpeg/webp base64 data URL ≤ 420 KB (≤ 300 KB decoded); diagnostics = { console[], errors[], network[] } (bounded, re-scrubbed server-side). A rejected screenshot never fails the request. |
| metadata | object · ≤ 30 keys | String→string map. Values ≤ 500 chars, keys ≤ 80. For your own tags (build, plan, session). |
Example
curl https://fdbck.app/api/v1/feedback \
-H "Content-Type: application/json" \
-H "X-Widget-Key: wk_your_key_here" \
-H "X-Idempotency-Key: $(uuidgen)" \
-d '{
"message": "The export button on the reports page does nothing.",
"type": "bug",
"url": "https://app.example.com/reports",
"user_id": "usr_1042",
"email": "sam@example.com",
"metadata": { "plan": "pro", "build": "1.4.2" }
}'Response
A fresh submission returns 201 with the item id and a status link you can show the submitter so they can follow their feedback.
{
"id": "b1f0…",
"statusUrl": "https://fdbck.app/feedback/status/…"
}When an X-Idempotency-Key matches a submission from the last 24 hours, the endpoint returns 200 with the existing id (and no statusUrl) — the earlier item stands, nothing new is created.
Errors
Every error is JSON with a machine-readable error code and a hint describing what to do.
| Status | Code | When |
|---|---|---|
| 400 | invalid_payload | Body isn't valid JSON, or a field failed validation (e.g. message missing or over 2000 chars). |
| 401 | missing_widget_key | No X-Widget-Key header. Set it to your project's widget key. |
| 401 | invalid_widget_key | The widget key isn't recognized. Check the value. |
| 413 | payload_too_large | Body exceeds the 512 KB cap (sized to fit an optional screenshot; a plain report is tiny). |
| 429 | rate_limited | Over 10 requests per minute for this widget key. Back off and retry. |
Fetch widget config
If you're rendering your own submission UI, you can pull the owner's presentation choices — the confirmation copy, footer links, accent, redirect — from the same config the embedded widget reads. It's keyed by the public widget key, read-only, and cached at the edge for about 60 seconds. Rate limit is 60 requests per minute per key.
GET https://fdbck.app/api/v1/widget/config?key=wk_your_key_here
| Field | Type | Description |
|---|---|---|
| accent | string | null | The owner's accent color (hex), or null to use the default amber. |
| theme | "auto" | "light" | "dark" | Color scheme preference. |
| success | { title, body } | The post-submit confirmation copy. |
| footerLinks | { label, url }[] | Up to 3 owner links shown in the widget footer. |
| showBranding | boolean | Whether the "Powered by fdbck" line is shown. |
| redirectUrl | string | null | An absolute http(s) URL to send the user to after a successful submit, or null. |
| capture | { screenshot, diagnostics } | Booleans — whether the project opted into screenshot attachments and technical diagnostics. The widget only offers/collects capture when these are true. |
Owners set all of this in Settings → Widget messaging — no code change on your side; re-fetch to pick up their edits.
Error webhook
Beyond human feedback, you can pipe runtime errors straight into the same inbox — from an error tracker, a logging pipeline, or your own catch block. Unlike the public widget key, this endpoint is signed with your project's webhook secret (Settings → General), so only your servers can post errors.
BODY='{"error":"TypeError: cannot read x of undefined","file":"checkout.ts"}'
TS=$(($(date +%s) * 1000)) # epoch MILLISECONDS
# Sign "{timestamp}.{body}" (or just the body if you omit X-Timestamp).
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$FDBCK_WEBHOOK_SECRET" | awk '{print $2}')
curl -X POST https://fdbck.app/api/v1/webhooks/error \
-H "Content-Type: application/json" \
-H "X-Project-Id: your-project-id" \
-H "X-Webhook-Secret: $SIG" \
-H "X-Timestamp: $TS" \
-d "$BODY"| Field | Type | Description |
|---|---|---|
| error | string · required | The error message or summary. |
| stacktrace | string · optional | The stack trace, if you have it — helps the agent locate the fault. |
| file | string · optional | The file the error points at, to seed the agent's search. |
| user_id | string · optional | Your own identifier for the affected user. Opaque to fdbck. |
Compute an HMAC-SHA256 with your webhook secret and send the hex digest as X-Webhook-Secret. If you include X-Timestamp (epoch milliseconds), sign {timestamp}.{body} and it enables replay protection (±5-minute window); otherwise sign the raw body alone. Rate limit: 60 requests per minute per project.
PII & safety
The server re-scrubs the page context and diagnostics your submitters send (the client scrub is bypassable since the widget key is public), and computes a PII-scrubbed copy of the message that is the only version ever rendered on public surfaces — the board and the status page. Fields you declare yourself (data-context, source root, component, version) are trusted and pass through as-is, so keep secrets out of them. Your raw message stays intact for you and the triage agent. And when it reaches the agent, the message is handled as untrusted data, never as instructions — a submitter can't steer the agent by what they write.