Skip to content

API

REST reference for ingestion, widget configuration, and reading feedback back out — with keys, errors, and rate limits.

Base URL: https://feedex.rianfernando.com

Two endpoints, two key types. Both answer with the same envelope.


Response envelope

Success:

{ "data": {} }

Failure:

{
  "error": {
    "code": "validation_error",
    "message": "Invalid feedback payload.",
    "details": { "description": ["Description must be at least 5 characters."] }
  }
}

code is stable and machine-readable. message is safe to show a user. details appears only for validation failures.

CodeStatusMeaning
unauthorized401Missing or unrecognised key
forbidden403Key is valid but not permitted here
not_found404No such resource
conflict409Already exists
validation_error422Payload failed validation
rate_limited429Slow down
internal_error500Our fault

Keys

TypePrefixWhere it livesCan
Publicpk_fdx_Client-side, in a <script> tagCreate feedback for one project
Secretsk_fdx_Your server onlyRead that project's feedback

Public keys are stored verbatim — they are published in snippets and are not secrets. Secret keys are stored only as HMAC digests and shown once at creation. Both rotate from the dashboard.


POST /api/v1/feedback

Create a feedback item. Authenticated by a public key in the body. This is what the widget calls.

CORS is open (Access-Control-Allow-Origin: *) because the widget runs on origins Feedex cannot enumerate. It is safe because the endpoint accepts no credentials, the key grants only "create feedback for this project", and it is rate limited per IP and per project.

Request

{
  "publicKey": "pk_fdx_...",
  "category": "bug",
  "title": "Optional — derived from the description if omitted",
  "description": "The export button does nothing on the reports page.",
  "email": "user@example.com",
  "name": "Ada Lovelace",
  "context": {
    "url": "https://example.com/reports",
    "path": "/reports",
    "browser": "Chrome",
    "browserVersion": "131.0.6778",
    "os": "macOS 15.2",
    "device": "desktop",
    "viewport": { "width": 1512, "height": 858 },
    "language": "en-US",
    "timezone": "America/New_York",
    "custom": { "plan": "pro" }
  }
}
FieldTypeRequiredNotes
publicKeystringyes8–128 chars
descriptionstringyes5–5000 chars
categoryenumnobug, feature, ui, performance, content, question, other. Defaults to other
titlestringno≤200 chars; derived from the description if omitted
emailstringnoValid email, or empty
namestringno≤120 chars
contextobjectnoAll fields optional; unknown keys are dropped
attachmentsarraynoUp to 3 files. See below

Each entry in attachments is:

FieldTypeNotes
namestring≤255 chars
typestringimage/png, image/jpeg, image/webp, image/gif, text/plain, application/json, application/pdf
datastringBase64, without a data: prefix. ≤512 KB decoded

One report may carry at most 3 files totalling 1 MB decoded. A file of any other type, or over the cap, fails the whole request with validation_error. If the project has attachments turned off, files are dropped and the report is still accepted — a cached page running an older configuration should not start failing.


GET /api/v1/widget-config

Returns a project's widget appearance settings. Authenticated by a public key in the query string. This is what lets the dashboard restyle every embed without a snippet edit.

GET /api/v1/widget-config?key=pk_fdx_...

Everything returned is public by construction — it is read with a publishable key and ends up in the widget's DOM on the host page regardless. No domain, secret key, feedback content, or workspace identity is exposed.

Cached at the edge for five minutes (stale-while-revalidate for a day), so this is not a database round trip per visitor.

Response — 200

{
  "data": {
    "project": { "name": "Portfolio" },
    "widget": {
      "position": "bottom-right",
      "accentColor": "#B58BF9",
      "buttonLabel": "Feedback",
      "launcherIcon": "chat",
      "title": "Send feedback",
      "description": "Found a bug or have an idea? Let us know.",
      "successMessage": "Thanks — your feedback has been received.",
      "requireEmail": false,
      "categories": ["bug", "ui", "feature", "other"],
      "theme": "auto",
      "attachments": {
        "enabled": true,
        "maxCount": 3,
        "maxBytes": 524288,
        "maxTotalBytes": 1048576,
        "accept": "image/png,image/jpeg,image/webp,image/gif,.txt,.log,.json,.pdf"
      }
    }
  }
}

Response — 201

{
  "data": {
    "id": "fbk_m4x9k2c1_a83jf0zq",
    "reference": "#42",
    "status": "open",
    "createdAt": "2026-08-01T17:04:11.221Z"
  }
}

Example

curl -X POST https://feedex.rianfernando.com/api/v1/feedback \
  -H "Content-Type: application/json" \
  -d '{
    "publicKey": "pk_fdx_...",
    "category": "bug",
    "description": "The export button does nothing on the reports page.",
    "email": "user@example.com"
  }'

Rate limits

ScopeLimit
Per IP20 / minute
Per project240 / minute

GET /api/v1/issues

List a project's feedback. Authenticated by a secret key as a bearer token.

Scoped to the key's own project. A projectId query parameter is ignored — the key decides.

Query parameters

NameTypeDefaultNotes
statusenumopen, in_progress, testing, resolved, closed
priorityenumlow, medium, high, critical
categoryenumAs above
qstringSubstring match on title, description, reporter email
sortenumnewestnewest, oldest, priority
pageint11–1000
perPageint251–100

Response — 200

{
  "data": {
    "items": [
      {
        "id": "fbk_m4x9k2c1_a83jf0zq",
        "reference": 42,
        "title": "Export button does nothing",
        "description": "Clicking Export CSV does nothing at all.",
        "category": "bug",
        "status": "open",
        "priority": "critical",
        "tags": [],
        "reporter": { "email": "user@example.com", "name": null },
        "context": {
          "url": "https://example.com/reports",
          "browser": "Chrome",
          "viewport": { "width": 1512, "height": 858 }
        },
        "createdAt": "2026-08-01T17:04:11.221Z",
        "updatedAt": "2026-08-01T17:04:11.221Z",
        "resolvedAt": null
      }
    ],
    "pagination": { "page": 1, "perPage": 25, "total": 1, "totalPages": 1 }
  }
}

Internal notes are never returned.

Example

curl https://feedex.rianfernando.com/api/v1/issues \
  -H "Authorization: Bearer sk_fdx_..." \
  -G -d status=open -d priority=critical -d perPage=50

Rate limits

120 requests per minute per key. Every response carries:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 2026-08-01T17:05:00.000Z

GET /api/health

Unauthenticated liveness probe. Executes a real query, so it fails when the process is up but the database is not.

{
  "status": "ok",
  "driver": "postgres",
  "latencyMs": 3,
  "timestamp": "2026-08-01T17:04:11.221Z"
}

Returns 503 with "status": "error" when the database is unreachable.


Versioning

The path carries the version. v1 is stable: fields will be added, never removed or repurposed. A breaking change would ship as v2 alongside it.

Not yet available

Writes through the API (updating status, adding notes), webhooks, and listing projects. All are on the roadmap.