<- All posts

Usero Journal

Feedback API: Collect Feedback From Any Platform With One HTTP POST

Will Smith··7 min read

A feedback API is an HTTP endpoint you POST to in order to record user feedback, instead of relying on a JavaScript widget. It is what you reach for when the feedback comes from somewhere a browser widget cannot run: a native iOS or Android app, a Python backend, a CLI, or a desktop app.

Most feedback tools ship a JS snippet, drop it in your HTML, done. That snippet is useless the moment your product is not a web page. There is no DOM in a Swift view, no browser in a backend job, no widget mount point in a terminal. If you want to capture feedback from those surfaces, you need a plain HTTP endpoint you can call from any language. This guide covers what a good feedback API looks like, with copy-pasteable examples in curl, Swift, Python, and JavaScript.

For context: I am Will, I build Usero. The endpoint below is the real one Usero exposes, and it is the same endpoint our widget uses under the hood, so nothing here is a special degraded mode.

Why a JavaScript Widget Is Not Enough

A widget is the right answer for a web app. It mounts in the page, captures the URL and a screenshot, and your users never leave. The problem is everything that is not a web page in a browser.

  • Native apps. An iOS app in Swift or an Android app in Kotlin has no DOM. You cannot embed a web widget in a native view without an ugly webview hack, and even then it will not feel native. A direct HTTP call from your settings screen is the clean path.
  • Server-side and backend jobs. A failed import, a flaky third-party call, a batch job that produced a weird result, these are worth capturing as feedback, and there is no user sitting in a browser to click anything. Your code posts the report itself.
  • CLIs and desktop apps. A command-line tool can prompt for a quick rating after a run and POST it. An Electron or native desktop app can do the same from its own UI. No web page involved.

In every one of these cases the missing piece is the same: a plain HTTP endpoint that takes a JSON body. Once you have that, the surface the feedback came from stops mattering.

What a Good Feedback API Looks Like

Not all collection endpoints are equal. Here is what makes one pleasant to use from any platform, and what Usero’s endpoint does.

  • One POST, JSON body. No SDK to install, no client library to keep up to date. If your language can make an HTTPS request, it can send feedback.
  • CORS wide open. The endpoint returns Access-Control-Allow-Origin: *, so a browser fetch, a mobile request, and a server call all behave the same.
  • Client id, not a secret key. The clientId in the body identifies your project. It is safe to ship inside a mobile binary, so you skip the API-key rotation dance.
  • Rating and/or comment. Send a numeric rating, a free-text comment, or both. The only hard rule is that at least one of them is present.
  • Attach context with metadata. An arbitrary JSON metadata object (10KB max serialized) carries app version, OS, build number, or feature flags, so every report arrives with what you need to reproduce it.

A Minimal Request

The smallest valid call is a clientId and one of rating or comment. The rating scale is 1 to 4: 1 Terrible, 2 Bad, 3 Good, 4 Amazing.

curl -X POST https://usero.io/api/feedback \
  -H "Content-Type: application/json" \
  -d '{"clientId": "YOUR_CLIENT_ID", "rating": 3}'

A successful response looks like this:

{ "success": true, "feedbackId": "abc123" }

A Full Request With Context

In practice you want more than a bare rating. Add the comment, the user’s email so you can reply, the page or screen they were on, and a metadata object with the version and platform details that make a bug reproducible.

curl -X POST https://usero.io/api/feedback \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "YOUR_CLIENT_ID",
    "rating": 2,
    "comment": "Export to CSV times out on large projects",
    "userEmail": "jamie@example.com",
    "pageUrl": "https://yourapp.com/projects/42/export",
    "pageTitle": "Export project",
    "metadata": {
      "appVersion": "2.4.1",
      "os": "macOS 15.2",
      "build": 1842
    }
  }'

The optional environment field is worth a note: omit it unless you separate environments like production and staging. Feedback without it lands in your default inbox, which is what you want most of the time.

The Same Call From Swift, Python, and JS

Because it is one POST with a JSON body, the call translates directly into any language. Here is the in-app feedback request from a native iOS app in Swift, where metadata pulls the build and OS version straight off the device.

var request = URLRequest(url: URL(string: "https://usero.io/api/feedback")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: [
    "clientId": "YOUR_CLIENT_ID",
    "rating": 2,
    "comment": "Crash on opening the export sheet",
    "metadata": [
        "appVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "unknown",
        "os": ProcessInfo.processInfo.operatingSystemVersionString,
    ],
])
let (data, _) = try await URLSession.shared.data(for: request)

Server-side from Python, for a backend job posting feedback about itself:

import requests

requests.post(
    "https://usero.io/api/feedback",
    json={
        "clientId": "YOUR_CLIENT_ID",
        "comment": "Nightly sync skipped 12 records with no error",
        "metadata": {"job": "nightly-sync", "build": 1842},
    },
)

And from JavaScript, where you can pull the page details off the browser:

await fetch('https://usero.io/api/feedback', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    clientId: 'YOUR_CLIENT_ID',
    rating: 4,
    comment: 'Love the new dashboard',
    userEmail: 'jamie@example.com',
    pageUrl: window.location.href,
    pageTitle: document.title,
    metadata: { appVersion: '2.4.1', os: navigator.platform },
  }),
})

Handling Errors

Three error responses are worth knowing about so your client code can react instead of swallowing failures silently.

  • 400, invalid data. Validation failed. The body carries an issues object naming each field that failed. The usual culprit is sending neither a rating nor a non-empty comment, since one of them is required.
  • 400, metadata too large. Your serialized metadata exceeds 10KB. Trim it down, the metadata is for version and platform context, not for dumping full logs.
  • 403, domain not allowed. Your client has a domain allowlist and this request’s origin is not on it. Add the origin in Settings, or clear the allowlist if you do not need it. New clients have no allowlist, so you will not hit this until you add one.

A 500 means something broke on our side and is safe to retry. For everything else, read the error string in the JSON body, it tells you what to fix.

Where API Feedback Goes Next

A POST endpoint is table stakes. The part that matters is what happens after the feedback lands. In Usero, feedback that arrives via the API is treated like feedback from anywhere else. It clusters with your widget feedback, your Slack feedback, and your GitHub feedback, so ten reports of the same bug from three different surfaces become one item with clear demand behind it.

The endpoint is the boring part. The point is that a backend job and a real user can flag the same problem, and you see it as one thing instead of two.

From there a clustered item can become a GitHub pull request, a first pass at the fix, opened against your repo for you to review and merge. You read the diff, you decide. Nothing ships on its own, and nothing merges automatically. The API is where the signal comes in; the PR is where it turns into a change you can actually evaluate.

How To Start

  1. Find your clientId. Create a client in Usero and grab the id that starts with client_. That is the only credential you need.
  2. POST a test request. Run the minimal curl above with your real clientId. A 200 with success: true means you are wired up.
  3. Check the inbox. Open your Usero inbox and the test feedback is there, alongside whatever the widget and your other sources have collected.

Related Reading

If You Want To Try Usero

Usero gives you one endpoint to collect feedback from any surface, a widget for the web, and an HTTP POST for everything the widget cannot reach. It all lands in one inbox, clusters automatically, and a clustered request can open a GitHub pull request you review and merge. Try it free and send your first API feedback in a few minutes.

Frequently Asked Questions

What is a feedback API?

A feedback API is an HTTP endpoint you POST to in order to record a piece of user feedback, instead of relying on a JavaScript widget embedded in a web page. You send a JSON body with a rating or a comment (or both) and the feedback shows up in your inbox. Because it is a plain HTTP call, it works from a native iOS or Android app, a Python backend, a CLI, a desktop app, or a cron job, anywhere a browser widget cannot run.

How do I collect feedback via a REST API?

With Usero you POST to https://usero.io/api/feedback with Content-Type application/json. The body must include your clientId (it starts with client_) and at least one of rating (an integer 1 to 4) or a non-empty comment. There is no API key and no auth header to manage; the clientId in the body identifies your project. A successful call returns {"success": true, "feedbackId": "..."}.

Do I need an API key to send feedback?

No. The clientId in the request body is what identifies your project, and it is not a secret in the way an API key is, so it is safe to ship inside a mobile binary or client-side code. If you want to lock down who can submit, add a domain allowlist in Settings; requests from origins that are not on the list get a 403. New clients have no allowlist, so every request is accepted until you add one.

Can I send in-app feedback from a native mobile app?

Yes. The endpoint sets Access-Control-Allow-Origin: * and accepts a plain JSON POST, so a Swift URLSession call or a Kotlin OkHttp call works the same as a browser fetch. Put the app version, OS version, and build number in the metadata object (10KB max serialized) so every report from your iOS or Android users arrives with the context you need to reproduce it.

What happens to feedback I send through the API?

It lands in the same inbox as feedback from the widget, Slack, GitHub, or a CSV import, and it clusters together with all of it. Ten reports of the same bug from your API, your widget, and Slack group into one item, so you see real demand instead of a pile of near-duplicates. A clustered item can then become a GitHub pull request, a first pass at the fix that you review and merge. Nothing merges on its own.

Why am I getting a 400 error when I POST feedback?

The most common cause is sending neither a rating nor a non-empty comment; one of them is required. The 400 response includes an issues object that names each failing field, so check it to see exactly what was rejected. The other 400 you can hit is "Metadata too large (max 10KB)", telling you the serialized metadata object is over the limit and needs trimming.

Build a feedback loop your team actually uses

Usero collects, clusters, and turns user feedback into shipped fixes.

Get started free