Errors and limits
The error envelope, every error code, rate limits per plan and size limits for the REST API.
Error response
Every error is JSON with a code and a message.
{
"error": {
"code": "invalid_session",
"message": "Session tokens are 8 to 128 characters."
}
}
Branch on code. The message is written for a person, names the fix, and can change.
Status codes
| Status | Meaning |
|---|---|
400 |
The request is invalid, or it was refused by a rule with no status of its own |
401 |
The credential is missing or matches nothing |
403 |
The token’s person does not hold the role this needs |
404 |
Not found, or not in your organization |
409 |
It already exists, or it was already settled |
429 |
Rate limited. Wait the seconds in Retry-After. |
500 |
internal_error. Retry, and contact support if it continues. |
Error codes
Credentials and requests
| Code | Status | When |
|---|---|---|
missing_api_key |
401 | No Authorization: Bearer header |
invalid_api_key |
401 | The key matches no organization or was revoked, or an OAuth token was sent to a messaging route |
publishable_key |
401 | A median_pk_ key was sent. The API takes MEDIAN_KEY. |
invalid_token |
401 | The OAuth token is unknown or expired, or its person left the organization |
no_organization |
403 | The OAuth token has no organization bound, or its organization is gone |
token_required |
401 | A key was sent to an account route |
forbidden |
403 | The token’s person lacks the role |
needs_a_person |
400 | A key tried to invite or change members |
legacy_api_key |
400 | A key from before Median keys was sent to PUT /v1/tool-endpoints |
invalid_json |
400 | The body is not a JSON object |
invalid_request |
400 | A field is missing or has the wrong type, or user, context or page holds a field that does not exist. In that last case the message lists the allowed fields. Unknown top-level fields are ignored. |
not_found |
404 | No such route under /v1 |
rate_limited |
429 | See rate limits |
internal_error |
500 | Median failed to handle the request |
Messages and uploads
| Code | Status | When |
|---|---|---|
invalid_session |
400 | The session is not 8 to 128 characters after trimming |
empty_message |
400 | No body and no attachments |
message_too_long |
400 | The body is over 4,000 characters |
too_many_attachments |
400 | More than 6 attachments |
attachment_missing |
400 | An attachment id is not a file from POST /v1/uploads |
attachment_wrong_type |
400 | The file is HTML, XHTML, SVG or XSLT |
empty_file |
400 | The upload body is empty |
file_too_big |
400 | The upload is over 20 MB |
Keys, webhooks and tool endpoints
| Code | Status | When |
|---|---|---|
too_many_keys |
400 | The organization holds 10 Median keys |
invalid_name |
400 | A key name is over 40 characters, or an agent name is not 2 to 40 characters |
missing_median_key |
400 | Adding a webhook or tool endpoint before any Median key exists |
invalid_url |
400 | The URL breaks the endpoint rules. The message says which. |
no_events |
400 | A webhook endpoint with no events |
too_many_endpoints |
400 | The organization holds 10 webhook endpoints |
too_many_tool_endpoints |
400 | The organization holds 10 tool endpoints |
no_endpoint |
400 | A sync with no url and no tool endpoint connected |
endpoint_required |
400 | A sync with no url while several endpoints are connected |
endpoint_not_found |
404 | A sync url that matches no connected endpoint |
invalid_input |
400 | A tool’s input does not match its schema |
too_many_running |
400 | Three tools are already running in the conversation |
conversation_archived |
400 | Running a tool in an archived conversation |
Organization and members
| Code | Status | When |
|---|---|---|
invalid_slug |
400 | The slug is not 3 to 32 lowercase letters, numbers and single dashes, or it is reserved |
slug_taken |
400 | Another organization has the slug |
too_many_organizations |
400 | You are in 50 organizations |
invalid_email |
400 | The invitation address is not an email address |
too_many_invites |
400 | 100 invitations are pending |
cannot_grant |
400 | Only an owner can invite or make an owner |
cannot_manage_self |
400 | Changing your own role or removing yourself |
outranked |
400 | The member ranks at or above you and you are not an owner |
last_owner |
400 | Demoting or removing the only owner |
member_not_found |
404 | The user is not in the organization |
Inbox, knowledge, signals and tasks
| Code | Status | When |
|---|---|---|
bad_snooze_time |
400 | The snooze time is invalid, under a minute away, or over a year away |
not_archived |
400 | Deleting a conversation that is not archived |
customer_unreachable |
400 | Reaching out to a customer who never opened the widget and has no email address |
invalid_feedback |
400 | A feedback note over 2,000 characters |
title_missing |
400 | A knowledge document with an empty title |
body_missing |
400 | A knowledge document with an empty body |
body_too_large |
400 | A knowledge document over 900,000 bytes |
doc_is_synced |
400 | Editing a document synced from a repository or imported from Notion |
folder_name_missing |
400 | A folder with an empty name |
folder_too_deep |
400 | A fourth folder level |
folder_cycle |
400 | Moving a folder into itself |
order_invalid |
400 | The move destination no longer exists |
bad_url |
400 | The crawl address is not a site address |
upgrade_required |
400 | Starting a crawl without a paid plan |
too_many_crawls |
400 | Three crawls are running |
signal_empty |
400 | A signal with an empty title |
title_required |
400 | A task with an empty title |
key_prefix_invalid |
400 | A task key prefix that is not up to 8 letters or numbers starting with a letter |
not_a_member |
400 | A task assignee who is not in the organization, or switching to an organization you are not in |
invalid_query |
400 | An explorer query names a field or measure its dataset lacks |
invalid_personality |
400 | Agent personality over 2,000 characters |
Integrations
| Code | Status | When |
|---|---|---|
slack_not_connected, discord_not_connected, linear_not_connected, github_not_connected |
400 | Connect the app in the dashboard first. With several GitHub accounts connected, github_not_connected also means none of them owns the repository. |
channel_not_found, role_not_found, team_not_found |
404 | No Slack or Discord channel, Discord role, or Linear team by that name. The message lists what exists. |
mirror_bot_offline, mirror_bot_timeout, mirror_act_failed, discord_bot_offline, discord_bot_timeout, discord_act_failed |
400 | The bot could not look up Slack or Discord channels or roles. Try again. |
repo_name_invalid |
400 | The repository is not written as owner/repo |
repo_already_connected |
400 | The repository is already connected |
bad_docs_url |
400 | The published address of a repository is not a valid URL |
address_private |
400 | The published address is not on the public internet |
repo_not_connected |
400 | The repository is not connected |
commit_repo_not_found |
404 | The repository’s commits are not read |
issue_repo_missing |
400 | Changing the issue repository’s settings without naming a repository when none is set |
address_taken |
400 | The support email address belongs to another organization |
linear_failed |
400 | Median could not read your Linear teams. The message says why. |
Not found
The 404 codes are conversation_not_found, customer_not_found, fact_not_found, knowledge_doc_not_found, knowledge_folder_not_found, suggestion_not_found, signal_not_found, commit_not_found, approval_not_found, invite_not_found, endpoint_not_found, tool_not_found, key_not_found, org_not_found, task_not_found and log_not_found, and any other code ending in _not_found, such as member_not_found, channel_not_found, role_not_found, team_not_found, repo_not_found and commit_repo_not_found. An id from another organization reads as not found.
Conflicts
| Code | Status | When |
|---|---|---|
already_a_member |
409 | The invited address is already in the organization |
already_invited |
409 | An invitation to that address is pending |
approval_settled |
409 | The tool call was already decided |
approval_expired |
409 | The tool call expired, or its conversation ended |
suggestion_settled |
409 | The suggestion was already approved or dismissed |
suggestion_stale |
409 | The document changed after the suggestion was written |
suggestion_gone |
409 | The suggestion’s document was deleted |
signal_exists |
409 | A signal with the same title or alias exists |
crawl_running |
409 | That site is already being crawled |
already_a_task |
409 | The signal is already on the task board |
not_a_request |
409 | Declining a card that is not in Requests |
Rate limits
Limits apply per organization. Every key, OAuth token, MCP session and assistant action in the organization draws from one allowance. Each class is a token bucket with a sustained rate per minute and a burst that can be spent at once.
Tier (tier) |
Plan | Reads a minute | Writes a minute | Uploads a minute |
|---|---|---|---|---|
explore |
Explore | 120, burst 20 | 30, burst 10 | 5, burst 2 |
standard |
Standard | 6,000, burst 1,000 | 1,200, burst 300 | 300, burst 100 |
higher |
Pro, or Standard with the API limits add-on | 12,000, burst 2,000 | 2,400, burst 600 | 600, burst 200 |
highest |
Pro with the API limits add-on | 24,000, burst 4,000 | 4,800, burst 1,200 | 1,200, burst 400 |
unlimited |
Unlimited API add-on | No fixed limit. Fair use applies. |
Plan and add-on prices are in Plans.
What counts
| Class | Requests |
|---|---|
| Read | Every GET, plus POST /v1/analytics/explore and POST /v1/analytics/records |
| Upload | POST /v1/uploads |
| Write | Every other POST, PUT, PATCH and DELETE |
- Each
median.*call inside an MCPmedian_runcounts as one request of its class. - The assistant’s actions count the same way.
- A request is charged before it runs, so a request that fails afterwards still counts.
POST /v1/organizationsandPOST /v1/me/organizationalways draw from a per-person allowance.GET /v1/medoes too while the token has no organization, or its person has left it. The allowance is 600 reads a minute with a burst of 100, and 120 writes a minute with a burst of 30.
When you hit a limit
HTTP/1.1 429 Too Many Requests
retry-after: 3
content-type: application/json
{"error":{"code":"rate_limited","message":"Your organization's API allowance is temporarily full. Please retry shortly."}}
Retry-Afteris in whole seconds, at least 1.- Median can restrict an organization under fair use. The response is the same
429 rate_limited, with"reason": "fair_use"inerrorand “API access is temporarily limited to protect shared capacity. Please retry shortly.” or “API access is temporarily limited because unusual automated traffic was detected. Contact support for review.” - Each refusal of the organization’s allowance is recorded in Logs as API limit reached.
Read your limits
curl https://api.median.sh/v1/billing/limits \
-H "Authorization: Bearer $MEDIAN_KEY"
{
"tier": "standard",
"version": "2026-09-22",
"limits": {
"read": { "perMinute": 6000, "burst": 1000 },
"write": { "perMinute": 1200, "burst": 300 },
"upload": { "perMinute": 300, "burst": 100 }
}
}
limits is null on unlimited. Any member can call it. The CLI is median billing limits. The dashboard shows the same numbers in Settings → Billing on the API limits row.
Other limits
These are separate buckets. A REST call that meets one is still charged to the tier as well.
| What | Limit | Counted per |
|---|---|---|
Manifest syncs from POST /v1/tool-endpoints/sync, the Sync button, and one per endpoint from POST /v1/tools/sync |
60 an hour, burst 10 | Organization |
| Invitation emails, sent or resent | 30 an hour, burst 10 | Organization |
MCP client registration, POST /oauth/register |
300 an hour, burst 50 | Caller IP address |
Device login start, POST /oauth/device_authorization |
300 an hour, burst 50 | Caller IP address |
- Adding a tool endpoint or changing its URL is not counted as a sync.
- Registration and device login also share a ceiling of 6,000 an hour across all callers.
- The REST buckets refuse with
429 rate_limitedand “Too many requests. Wait a moment and try again.” The OAuth routes refuse with429and the OAuth errorslow_down.
Size limits
| Limit | Value | Past it |
|---|---|---|
| Message body | 4,000 characters after trimming | message_too_long |
| Attachments | 6 per message | too_many_attachments |
| Upload | 20 MB per file | file_too_big |
| Upload file name | 200 characters | Cut to fit |
| Attachment types | text/html, application/xhtml+xml, image/svg+xml, text/xsl and application/xslt+xml upload, but are refused at send |
attachment_wrong_type |
| Session token | 8 to 128 characters after trimming | invalid_session |
user.name |
80 characters | Cut to fit |
user.email |
320 characters | Cut to fit |
user.avatarUrl |
An https URL, up to 512 characters | Dropped |
user.metadata |
16 entries. Keys and values up to 200 characters. | Extra entries dropped, text cut |
| Thread history | 200 messages across the session’s 10 most recently started conversations. The most recently active fill it first. | Older messages left out |
| Knowledge document | 900,000 bytes | body_too_large |
| Feedback note | 2,000 characters | invalid_feedback |
| Median keys | 10 per organization | too_many_keys |
| Webhook endpoints | 10 per organization, URLs up to 512 characters | too_many_endpoints |
| Tool endpoints | 10 per organization, URLs up to 512 characters | too_many_tool_endpoints |
| Pending invitations | 100 per organization | too_many_invites |
| Organizations | 50 per person | too_many_organizations |
| Crawls running | 3 per organization | too_many_crawls |
| Tools running | 3 per conversation | too_many_running |
| Knowledge folders | 3 levels deep | folder_too_deep |