Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Feedback Endpoint

Send one feedback submission to a feedback channel

POST https://ingest.modem.dev/feedback accepts one feedback submission for a feedback channel. The feedback widget and the feedback MCP server both call it. Call it yourself from a backend, a CLI, a mobile app, or anything else that can make an HTTPS request.

Authentication

Send the channel’s feedback token as a bearer token:

Authorization: Bearer feedback_<32 hex characters>

A missing or malformed header gets 401. A token that is unknown, revoked, expired after a rotation, or whose channel is disabled also gets 401, with the same body, so a caller cannot tell those cases apart. Check the channel in Settings → Feedback Channels.

This is the only ingest route that accepts browser requests. If the channel has allowed origins, the request’s Origin header must match one exactly or the response is 403. Requests without an Origin header always pass.

Request

Send either JSON or multipart form data. Any other content type gets 415.

JSON

POST /feedback HTTP/1.1
Host: ingest.modem.dev
Authorization: Bearer feedback_…
Content-Type: application/json

{
    "submission_id": "0192e2f0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "message": "Exports time out for workspaces with more than 10k topics.",
    "name": "Ada Lovelace",
    "email": "ada@example.com",
    "url": "https://app.example.com/exports",
    "source": "api",
    "tags": { "category": "user_feedback", "plan": "pro" }
}

Multipart

Use multipart/form-data to attach screenshots or to name the reporter from your backend. Only three part names are allowed; any other part gets 400.

Part Count Content
payload 1 The JSON body above.
screenshot 0 to 2 An image file. PNG, JPEG, GIF, or WebP, at most 5 MB each. Modem checks the bytes, not the declared type.
author 0 or 1 JSON naming the signed-in user of your app. See The author part.

The whole request must stay under 12 MB.

curl https://ingest.modem.dev/feedback \
    -H "Authorization: Bearer $MODEM_FEEDBACK_TOKEN" \
    -F 'payload={"submission_id":"0192e2f0-1a2b-7c3d-8e4f-5a6b7c8d9e0f","message":"The export never finished.","source":"api"};type=application/json' \
    -F 'screenshot=@export-timeout.png' \
    -F 'author={"user_id":"usr_8213","name":"Ada Lovelace","email":"ada@example.com"};type=application/json'

Payload fields

Unknown fields are rejected.

Field Type Required Description
submission_id string Yes A UUID version 7 you generate. It is the idempotency key; see Retries.
message string Yes The reporter’s words, 1 to 4,096 characters after trimming.
name string No Self-reported display name, up to 256 characters.
email string No Self-reported email, up to 320 characters. Recorded as a claim, not verified. A malformed address does not reject the submission but is not used to link a person.
url string No The page the reporter was on. http or https only, up to 2,048 characters.
source string No One of widget, crash, tui, agent, api. Defaults to widget.
associated_event_id string No The error-tracking event a crash report is about, 1 to 128 characters.
replay_id string No A session replay ID, 1 to 128 characters.
trace_id string No A trace ID, 1 to 128 characters.
release string No Your app’s version, 1 to 200 characters.
environment string No For example production, 1 to 64 characters.
tags object No Up to 20 string labels. Keys 1 to 32 characters, values up to 200.
context object No Free-form JSON about what the reporter was doing. At most 8,192 characters when serialized.

identity is reserved for a future verified-identity feature and is rejected if sent.

The category tag

tags.category says what kind of report this is. It is a convention on tags, not a separate field:

Value Meaning
user_feedback The reporter’s own words. The default when the tag is absent.
agent_friction An AI agent’s own report of a failing tool, a missing capability, or an unclear error, sent with consent.

The author part

When your backend already knows who is signed in, send an author part instead of asking the reporter to type their name and email:

Field Type Required Description
user_id string Yes Your app’s ID for the user, 1 to 128 characters.
name string No Up to 256 characters.
email string No A malformed address is dropped, not refused.

Modem records the author as a claimed identity keyed by your user ID and this channel. It is not treated as a verified identity, because nothing proves what an app says about its own users.

Responses

Status Meaning
202 Accepted. The body is { "id": "…" }, the ID of the feedback event. Accepted means queued for processing, not stored.
400 Invalid JSON, a payload that fails validation (details lists the fields), or an unexpected multipart part.
401 Missing, malformed, or inactive token.
403 The request’s origin is not in the channel’s allowed origins.
409 A request with the same submission_id is still being accepted. Wait for Retry-After (one second) and retry.
413 A screenshot is over 5 MB, or the request is over 12 MB.
415 Unsupported content type, or a screenshot that is not a PNG, JPEG, GIF, or WebP.
429 Rate limited. Wait for Retry-After (60 seconds).
500 Modem could not record the submission. Retry with the same submission_id.
503 Screenshots are temporarily unavailable. Retry without them, or later.

Browsers can read Retry-After on 409 and 429; it is the only response header exposed to them.

Retries

Generate submission_id once per submission and send the same one on every retry of that submission. Modem dedupes on the channel and submission_id: once the first request is accepted, every later request with that ID gets 202 with the first submission’s id and nothing is stored twice. If the reporter edits the draft, that is a new submission with a new ID.

Retry a failed connection, a 409, a 429, or a 5xx. A 4xx other than those will fail the same way again.

Rate limits

Each token allows 60 submissions per minute, and each client IP allows 20 per minute. Over either limit the response is 429 with Retry-After: 60.

Last updated on

Was this page helpful?