Mite developer docs
SDK, REST API, authentication, triage, errors, rate limits, and webhooks
Mite is in-app feedback and release notes for React Native and Expo apps. Your
app sends bug reports, feature requests, and user identities to the Mite API.
Mite triages each report as it arrives, you act on it in the Mite dashboard, and
you publish release notes back to the same app.
This page is the developer index: SDK, REST API, authentication, triage, errors,
rate limits, webhooks, and the machine-readable files that describe all of it.
Quickstart
- Create an app in the Mite dashboard and generate
an API key under Settings → Keys. - Install the SDK in your Expo or React Native app.
- Submit your first bug report.
npm install @usemite/mite-sdk
import { Mite, MiteProvider, useBugReport } from '@usemite/mite-sdk'
const mite = new Mite({ apiKey: 'mite_ak_...' })
mite.init()
// Wrap your app once:
<MiteProvider miteInstance={mite}>{/* Your app */}</MiteProvider>
// Then submit bugs from anywhere:
const { submitBugReport } = useBugReport()
await submitBugReport({
title: 'Something broke',
description: 'What the user saw',
})
SDK
The React Native and Expo SDK is published as@usemite/mite-sdk. It wraps every
endpoint below, collects device and app context, and retries safely. Use it
unless you have a reason to call the API directly.
Authentication
Every endpoint except /api/v1/health needs a Mite API key, sent as a bearer
token:
Authorization: Bearer mite_ak_...
Mite API keys are publishable, SDK-style tokens, like a Sentry DSN or a PostHog
project key. They ship inside your mobile binary and anyone can extract them
from a shipped APK or IPA, so they are not secrets. Mite handles abuse with
per-key rate limits and instant revocation instead of secrecy. Never use a Mite
key as a substitute for your own user authentication.
Each key carries scopes:
| Scope | Grants |
|---|---|
read |
List releases, announcements, feature requests |
write |
Submit bug reports, feature requests, votes, identities |
A request with the wrong scope is refused with 403.
Base URL
https://usemite.com
API endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET |
/api/v1/health |
none | Liveness probe |
POST |
/api/v1/bug-reports |
write |
Submit a bug report |
POST |
/api/v1/errors |
write |
Send a batch of JS error occurrences |
POST |
/api/v1/upload-url |
write |
Create a single-use attachment upload URL |
POST |
/api/v1/identify |
write |
Create or update an end-user profile |
GET |
/api/v1/releases |
read |
List published releases |
GET |
/api/v1/announcements |
read |
List active announcements |
GET |
/api/v1/feature-requests |
read |
List feature requests |
POST |
/api/v1/feature-requests |
write |
Submit a feature request |
POST |
/api/v1/feature-requests/vote |
write |
Toggle a vote |
GET |
/api/v1/feature-requests/votes |
read |
List one voter's votes |
GET |
/getImage |
read |
Fetch a stored attachment |
Every path also answers OPTIONS for CORS preflight.
The full request and response schema for each endpoint is published as OpenAPI
3.1 at usemite.com/openapi.json.
Submit a bug report
curl -X POST https://usemite.com/api/v1/bug-reports \
-H "Authorization: Bearer mite_ak_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Crash when opening the Profile tab",
"description": "The app closes immediately after tapping Profile.",
"app_version": "1.4.2",
"device_info": { "model": "iPhone 15", "os": "iOS 18.2" }
}'
{ "id": "j57...", "status": "NEEDS_TRIAGE" }
The call returns as soon as the report is stored. Triage runs after the
response, so the status is NEEDS_TRIAGE here even for a report that is
triaged a moment later.
Send JS errors
The SDK batches the JS errors it catches and sends them here. Every event joins
the group of the same error: the same stack groups together even when the
message differs, and an error with too thin a stack groups by its message with
ids and numbers masked out. Errors do not count against the report quota.
curl -X POST https://usemite.com/api/v1/errors \
-H "Authorization: Bearer mite_ak_..." \
-H "Content-Type: application/json" \
-d '{
"events": [{
"name": "TypeError",
"message": "Cannot read property \'id\' of undefined",
"stack": "TypeError: ...\n at renderRow (index.bundle:1:48211)",
"is_fatal": false,
"app_version": "1.4.2"
}]
}'
{ "accepted": 1, "groups": ["erg_..."] }
On paid plans, triage runs once when a group is first seen and again when a
resolved group comes back. It labels the group:
| Verdict | Meaning | What Mite does |
|---|---|---|
duplicate |
The same fault as an open bug report or another error | Links the error to that bug |
recurring |
Keeps happening, or came back after it was resolved | Opens a bug, or reopens the closed one |
real_issue |
A fault in the app's code | Opens a bug report for it |
non_issue |
An expected condition, like a cancelled action or no network | Nothing |
noise |
Library warnings, development messages, platform errors | Nothing |
An error is only written off as non_issue or noise when the model is at
least 70% sure; anything less stays a real issue.
Triage
On paid plans, every accepted bug report is sent to one model call that answers
these questions about it:
| Question | Answer |
|---|---|
actionable |
Probability that there is something to act on |
spam |
Probability that the report is spam |
reproducible |
Probability that the description is reproducible |
category |
One of crash, ui, performance, data, auth, network, and six more |
severity |
cosmetic, minor, major, or blocking |
frustration |
calm, frustrated, or angry |
area |
One of the feature areas you saved for the app, or other |
duplicate_of |
The id of one of the 25 newest open reports, or none |
actionable, spam, and reproducible are stored as probabilities on their
own. The rest carry a confidence, which the report page shows for category and
severity. The area question is asked only when the app has saved areas, andduplicate_of only when the app has an open report to compare against.
Code reads the answers and decides what to do with them:
- Priority is set from severity, reproducibility, and frustration, and only
when severity carries at least0.85confidence and no one has set a
priority on the report. - A duplicate is shown as a hint at
0.6confidence or higher, with a Mark
as duplicate button. Nothing is merged for you. - A report is treated as noise at
0.9or higher and moves behind a toggle on
the bugs list. Nothing is closed. - With an issue provider set for the app, a bug that is not noise and whose
severity rounds toblockingat0.85confidence or higher opens a linked
GitHub or Linear issue.
A feature request is triaged too, with a shorter question set: spam,category over feature, improvement, bug, question, and other, plus area andduplicate_of under the same conditions. It gets no severity, so no priority
rule applies to it.
Triage needs OPENROUTER_API_KEY. Without it, or when the call fails, the
report is stored untouched and the rest of intake continues.
Triage is not exposed over the API today. Re-run it on a single bug report from
the Triage card on the report page.
Errors
Every failure returns JSON, never HTML. Branch on code, not on error.
{
"error": "API key is missing the required 'write' scope",
"code": "forbidden",
"hint": "Create a key with the write scope in Settings → Keys, or use an existing key that has it."
}
| Status | code |
Meaning |
|---|---|---|
400 |
invalid_request |
Malformed JSON, or a field failed validation |
401 |
unauthorized |
Missing, malformed, or unknown API key |
402 |
REPORT_QUOTA_EXCEEDED |
The plan's monthly report allowance is used up |
402 |
STORAGE_QUOTA_EXCEEDED |
The plan's attachment storage is used up |
403 |
forbidden |
The key lacks the scope the endpoint needs |
404 |
not_found |
No such resource for this application |
413 |
payload_too_large |
The request body is larger than 256 KB |
429 |
rate_limited |
Per-key rate limit exceeded |
500 |
internal_error |
Unexpected server error |
A quota refusal is 402, never 429. A 429 tells a client to retry; a
monthly quota cannot improve by retrying, only by changing plan. Quota
responses carry a quota object with limit, used, and resets_at.
Rate limits
Limits are per API key, as a token bucket refilled over one minute. They are
scoped to the key rather than to the application, so one misbehaving key cannot
exhaust the budget of a sibling key on the same app.
| Endpoint | Policy | Requests per minute |
|---|---|---|
/api/v1/bug-reports |
bug-reports |
60 |
/api/v1/errors |
errors |
60 |
/api/v1/upload-url |
upload-url |
30 |
/api/v1/identify |
identify |
120 |
/api/v1/releases |
releases |
120 |
/api/v1/announcements |
announcements |
120 |
/api/v1/feature-requests (GET) |
feature-requests-list |
120 |
/api/v1/feature-requests (POST) |
feature-requests-create |
30 |
/api/v1/feature-requests/vote |
feature-requests-vote |
60 |
/api/v1/feature-requests/votes |
feature-requests-votes |
120 |
/getImage |
images |
300 |
/api/v1/health is unauthenticated and unmetered.
Rate limit headers
Every metered response carries your remaining quota, so you can throttle before
you are refused rather than discovering the limit by hitting it. The fields
follow the IETF RateLimit header fields draft.
RateLimit-Policy: "releases";q=120;w=60
RateLimit: "releases";r=118;t=1
RateLimit-Limit: 120
RateLimit-Remaining: 118
RateLimit-Reset: 1
| Field | Meaning |
|---|---|
RateLimit-Policy |
The quota in force: q requests per w seconds |
RateLimit |
What is left for you: r requests, t seconds until the quota is whole |
RateLimit-Limit |
Superseded plain-integer form of q |
RateLimit-Remaining |
Superseded plain-integer form of r |
RateLimit-Reset |
Superseded plain-integer form of t |
The three plain-integer fields are from an earlier draft and are no longer part
of the specification. Mite still sends them because most deployed HTTP clients
read them; prefer RateLimit and RateLimit-Policy in new code.
All of them are named in Access-Control-Expose-Headers, so a browser can read
them cross-origin.
A refused request returns 429 with Retry-After in seconds, alongsideRateLimit reporting r=0. When both are present, Retry-After is
authoritative.
HTTP/1.1 429 Too Many Requests
Retry-After: 1
RateLimit-Policy: "bug-reports";q=60;w=60
RateLimit: "bug-reports";r=0;t=60
Rate limits on usemite.com
The machine-readable documents on usemite.com carry RateLimit-Policy and are
metered per client at 600 requests per minute. Those documents are/openapi.json, /api/v1, /.well-known/api-catalog, /agents.md,/llms-full.txt, and the JSON errors under /api/.
Those responses carry no live RateLimit field. They are publicly cacheable,
and a shared cache would otherwise hand one client's remaining quota to the
next. A refusal is no-store, so it carries the full set.
Webhooks
Mite posts outgoing notifications to Discord, Slack, and a signed endpoint of
your own (see Trigger an agent). Add a URL under
Settings → Webhooks in the dashboard and choose which events fire:
- a new bug report arrives
- a bug report changes status
- a new feature request arrives
With Notify only on actionable reports on for an app, the arrival events
wait for triage and are skipped for a report triage calls noise. The same gate
holds the owner's email and in-app notification. If triage cannot run, all three
are sent.
Discord URLs must start with https://discord.com/api/webhooks/ and Slack URLs
with https://hooks.slack.com/services/. Mite does not accept inbound webhooks.
MCP server
Mite serves a Model Context Protocol server
at https://usemite.com/mcp. Connect Claude Code, Cursor, or a scheduled job to
it, and the agent can work through your bug queue, move the roadmap, and ship
release notes.
The MCP server is included on paid plans. An agent works only while the plan
of the app owner includes agent automations. When the plan loses it, every
request from the agent is refused with 402 and code FEATURE_LOCKED. The
agent works again when the plan is upgraded.
An agent connects in one of two ways. A connection made with OAuth acts on your
account. An agent key acts on one app.
Connect with OAuth
Claude, ChatGPT, and Cursor sign in with your Mite account. Addhttps://usemite.com/mcp as a custom connector. The client opens a browser, you
sign in to Mite and approve the client, and no key is copied. Claude Code does
the same when you add the server without a header:
claude mcp add --transport http mite https://usemite.com/mcp
A connection acts on every app you own, with read and write access. Thelist_apps tool lists those apps, and every other tool takes one of them asapp, by slug or id. An app you created but that now has another owner is
refused.
The connection is listed under Account connections in Settings → Agents
of any app. Revoke there blocks the client for your account. Mite refuses
every token of that client, new or old, with 403 and the codeconnection_blocked, even after you sign in again. Click Allow again on
the connection to unblock it. Revoke does not change your other clients.
A request without a token gets 401 with aWWW-Authenticate: Bearer resource_metadata="https://usemite.com/.well-known/oauth-protected-resource/mcp"
challenge. That document names the authorization server the client signs in
with. A token without the mcp scope gets 403 with insufficient_scope.
Connect with an agent key
A scheduled job or CI run has nobody to sign in, so it uses an agent key,
not an API key. Create one under Settings → Agents in the dashboard. An
agent key differs from an API key in three ways:
- It is a secret. Mite shows it once and stores only its hash. Keep it out of
your app binary and your repository. - It is bound to one app. Every tool acts on that app, so no tool takes an app
id, and a request that names another app's bug is refused withFORBIDDEN. - It is
readorwrite. Areadkey sees only the read tools.
Add the server to Claude Code:
claude mcp add --transport http mite https://usemite.com/mcp --header "Authorization: Bearer mite_agent_..."
Or commit a .mcp.json that reads the key from the environment:
{
"mcpServers": {
"mite": {
"type": "http",
"url": "https://usemite.com/mcp",
"headers": { "Authorization": "Bearer ${MITE_AGENT_KEY}" }
}
}
}
Tools
| Tool | Access | What it does |
|---|---|---|
get_app |
read |
The app, and how many bug reports sit in each status |
list_bugs |
read |
Bug reports, newest first, filtered by status, priority, and date |
get_bug |
read |
One bug report with its comments, attachments, and linked requests |
search_bugs |
read |
Bug reports like a text query or another bug, and what fixed them |
list_changes |
read |
New reports, status changes, comments, and triage since a cursor |
list_feature_requests |
read |
Feature requests, most voted first |
list_crash_groups |
read |
Crash signatures shared by two or more bug reports |
list_releases |
read |
Releases, highest version first |
get_release |
read |
One release and the bugs it fixes |
update_bug |
write |
Change a bug's status, priority, or title |
claim_bug |
write |
Claim a bug for a while so other agents leave it alone |
release_bug_claim |
write |
Drop your claim on a bug |
assign_bug |
write |
Assign a bug to the app owner, or unassign it |
comment_on_bug |
write |
Add a comment to a bug report |
retriage_bug |
write |
Run triage on a bug report again |
set_feature_request_status |
write |
Move a feature request on the roadmap |
merge_feature_requests |
write |
Merge duplicate feature requests and their votes |
link_bug_to_release |
write |
Mark a bug fixed in a release, and resolve it if it is still open |
upsert_release |
write |
Create a release, or update the one with the same version |
draft_release_notes |
write |
Draft notes from what shipped, resolving duplicates of fixed bugs |
create_announcement |
write |
Draft an announcement, or return the unarchived one with its title |
update_announcement |
write |
Edit, publish, or archive an announcement |
list_bugs and list_changes page with a cursor. Pass the next_cursor of one page as thecursor of the next, until next_cursor is null.
An OAuth connection also has list_apps, and each tool above takes an app
input.
A tool that fails returns an error result whose text starts with a code:FORBIDDEN, NOT_FOUND, INVALID, or FEATURE_LOCKED when the plan does not
include the feature. A missing, unknown, revoked, or expired key or token is
refused with 401 before any tool runs. So is a key whose app has changed
owner since the key was created. The new owner creates their own keys.
The server is metered per agent key or OAuth connection under the mcp policy
at 120 requests per minute, with the same RateLimit headers as the REST API.
Prompts
The server also has one prompt, fix_bug, which needs read access. It takes
a bug_id (and app on an OAuth connection) and returns a brief for a coding
agent: what the user reported, the device, OS, and app version, the navigation
trail, crash frames, attachments, Mite triage, linked feature requests, and
related reports with the release that fixed them. The brief ends by asking the
agent to link its fix back with comment_on_bug and link_bug_to_release.
Issues that Mite opens on GitHub or Linear use the same brief as their body. Afix_bug call that fails returns a JSON-RPC error whose message starts with
the same codes as a failed tool.
Trigger an agent
An agent can poll the queue on a schedule, or Mite can wake it when something
happens. Add an endpoint URL under Settings → Webhooks → Signed webhook.
Mite shows a signing secret (whsec_...) once. Rotate secret replaces it.
The URL must use https and point to a public host. Mite posts each event with
one attempt and a 10 second timeout, and the settings page shows the status
code of the last delivery. Send test event posts a webhook.test event.
| Event | Fires when | data |
|---|---|---|
bug.created |
a bug report arrives | application_id, id |
bug.status_changed |
a bug report changes status | application_id, id, status, previous_status |
feature_request.created |
a feature request arrives | application_id, id |
bug.triaged |
triage finishes on a new bug | the triage verdict, see below |
feature_request.triaged |
triage finishes on a new request | the triage verdict, see below |
The event toggles under Settings → Webhooks apply to the endpoint as well.bug.triaged follows the new bug toggle and feature_request.triaged follows
the new feature request toggle. Discord and Slack do not get the triaged
events. A bulk status change sends one bug.status_changed per bug. The other
events hold ids only:
{
"type": "bug.created",
"timestamp": "2026-09-25T18:00:00.000Z",
"data": { "application_id": "app_...", "id": "bug_..." }
}
Your agent reads the details with get_bug or list_feature_requests.
The triaged events carry the verdict, so an agent can act without a follow-up
call. They fire only when triage runs, and they fire even for a
report triaged as noise while Notify only on actionable reports is on.
Check is_noise to skip those.
{
"type": "bug.triaged",
"timestamp": "2026-09-25T18:00:05.000Z",
"data": {
"application_id": "app_...",
"id": "bug_...",
"title": "Crash on launch",
"category": "crash",
"severity": "blocking",
"suggested_priority": "CRITICAL",
"area": "Checkout",
"duplicate_of": "bug_...",
"is_noise": false,
"confidence": { "category": 0.93, "severity": 0.88 },
"url": "https://usemite.com/my-app/bugs/bug_..."
}
}
severity is cosmetic, minor, major, or blocking. area andduplicate_of are null when triage has no confident answer.feature_request.triaged holds id, title, category, demand (niche,few, many, or most), demand_score (0 to 100), area, duplicate_of,bug_id (the bug the request was linked to, or null), is_noise,confidence (category, demand), and url.
Mite signs with Standard Webhooks, so any
Standard Webhooks library verifies the webhook-id, webhook-timestamp, andwebhook-signature headers. Verify against the raw body, before you parse it.
This Cloudflare Worker verifies an event and starts a GitHub Actions workflow
through repository_dispatch:
import { Webhook } from 'svix'
type Env = { MITE_WEBHOOK_SECRET: string; GITHUB_TOKEN: string }
export default {
async fetch(request: Request, env: Env) {
const body = await request.text()
try {
new Webhook(env.MITE_WEBHOOK_SECRET).verify(
body,
Object.fromEntries(request.headers),
)
} catch {
return new Response('Invalid signature', { status: 401 })
}
const event: { type: string; data: { id: string } } = JSON.parse(body)
if (event.type !== 'bug.created') return new Response(null, { status: 204 })
await fetch('https://api.github.com/repos/acme/app/dispatches', {
method: 'POST',
headers: {
Authorization: `Bearer ${env.GITHUB_TOKEN}`,
Accept: 'application/vnd.github+json',
'User-Agent': 'mite-relay',
},
body: JSON.stringify({
event_type: 'mite-bug',
client_payload: { bug_id: event.data.id },
}),
})
return new Response(null, { status: 202 })
},
}
The workflow runs your agent with the MCP server connected, and the agent callsget_bug with client_payload.bug_id. Delivery is at most once. Keep a
scheduled run too, so a missed event still gets picked up.
Machine-readable resources
| Resource | What it is |
|---|---|
| /openapi.json | OpenAPI 3.1 description of the Mite API |
| /api/v1 | Mite API discovery document |
| /.well-known/api-catalog | Mite API catalog, as an RFC 9727 linkset |
| /agents.md | When to use Mite, and how to make the first call |
| /llms.txt | Index of Mite's pages for agents |
| /llms-full.txt | Every Mite page as one Markdown document |
| /sitemap.xml | Every public page |
| /docs.md | This page as Markdown |
Every public page also answers Accept: text/markdown with a Markdown
rendition of itself, and advertises it with a Link: rel="alternate" header.
Support
Open an issue on github.com/usemite/mite-sdk
for SDK problems, or use the in-app feedback widget in the Mite dashboard.