# Errors (https://upapi.io/docs/errors)



Every failure — bad input, a missing operation, a rate limit, a dead upstream —
comes back in the same shape:

```json
{
  "error": {
    "code": "INVALID_INPUT",
    "message": "language: String must contain at most 30 character(s)"
  }
}
```

`code` is a stable, machine-readable identifier: branch on it. `message` is
written for a human reading a log and may change at any time, so do not parse
it. For a marketplace operation the HTTP status matches the code, which means a
classified failure reaches you as the status it deserves rather than a blanket 502. [Hosted functions](https://upapi.io/docs/platform/hosted-functions) are the exception, in
both the status and the shape — see below.

Every response, successful or not, also carries an `x-request-id` header. Quote
it when reporting a problem — it is how we find your exact call.

## Codes [#codes]

| Code | HTTP | Meaning |
| --- | --- | --- |
| `CALLER_PROXY_AUTH_REJECTED` | 400 | The proxy you supplied (proxyUrl or x-proxy-url) refused our connection with HTTP 407. Check its credentials and whether your provider allow-lists the connecting IP. Distinct from PROXY_UNAVAILABLE, which is about our own pool. |
| `INVALID_INPUT` | 400 | The input failed the operation's schema validation. |
| `INVALID_JSON` | 400 | The request body was not parseable JSON. |
| `INVALID_PATH` | 400 | The request path is not a valid operation route. |
| `MEMORY_EXCEEDED` | 400 | A hosted function exceeded the memory allowed by your plan. Delivered as a 422, see below. |
| `MISSING_FUNCTION_ID` | 400 | The request did not identify an operation to run. |
| `AUTH_EXPIRED` | 401 | The supplied upstream credentials or session are no longer valid. |
| `AUTH_REQUIRED` | 401 | This operation requires credentials that were not supplied. |
| `UNAUTHORIZED` | 401 | The x-api-key header is missing, unknown, or disabled. |
| `FORBIDDEN` | 403 | Authenticated, but your plan or organization may not perform this action. |
| `NETWORK_DENIED` | 403 | An outbound request was blocked by the SSRF guard (private, link-local, or loopback address). |
| `NOT_FOUND` | 404 | No operation is published at this slug for your organization. |
| `CHECKPOINT_REQUIRED` | 409 | The upstream site stopped the operation at a security checkpoint a person must clear (an in-app approval, an authenticator prompt, or an identity check). Retrying will not help. |
| `CLIENT_REJECTED` | 409 | The upstream site rejected our client itself — a request signature, a device attestation, or an app-integrity check — rather than your input. Retrying, waiting, or routing through a different network will not change the answer. |
| `IDEMPOTENCY_IN_FLIGHT` | 409 | The first request with this Idempotency-Key is still running. Retry to receive its result. |
| `IDEMPOTENCY_KEY_REUSED` | 409 | This Idempotency-Key was already used for a different request. Use a new key. |
| `NO_MOBILE_EXIT` | 409 | You asked a proxy lease for a mobile-carrier exit and none of the exits probed was cellular. Retrying with the same inputs gives the same answer: try a different mobileCountry, or drop the requirement and accept a residential exit. |
| `PHONE_VERIFICATION_REQUIRED` | 409 | The upstream site stopped the operation and demanded a phone number. A person with a number is the only thing that clears it, so retrying will not help. |
| `QUOTA_EXCEEDED` | 429 | A budget is spent: the month's weighted units, a trial's unit allowance, or your plan's hosted-function compute seconds. |
| `RATE_LIMITED` | 429 | You exceeded the per-minute burst limit. It refills on its own within 60s. |
| `SERVICE_BUSY` | 429 | Execution capacity is momentarily saturated. Retry with backoff. |
| `INTERNAL_ERROR` | 500 | An unexpected failure on our side. Report it with the x-request-id. |
| `MISSING_HANDLER` | 500 | The operation is registered without a runnable handler. |
| `OPERATION_ERROR` | 500 | The operation raised an unclassified error while running. |
| `NOT_IMPLEMENTED` | 501 | The operation exists but this code path is not implemented. |
| `CAPTCHA_FAILED` | 502 | The upstream site presented a challenge that could not be solved. |
| `INVALID_OUTPUT` | 502 | The operation's result did not match its declared output schema. |
| `INVOCATION_FAILED` | 502 | The operation could not be dispatched to a worker. |
| `PROXY_UNAVAILABLE` | 502 | No healthy proxy was available for an operation that requires one. |
| `UPSTREAM_ERROR` | 502 | The third-party service returned an error or blocked the request. |
| `UPSTREAM_INVALID_RESPONSE` | 502 | The third-party service answered in a shape we could not parse. |
| `UPSTREAM_UNREACHABLE` | 502 | The third-party service could not be contacted at all. |
| `AUTH_UNAVAILABLE` | 503 | A session surface could not reach our authentication infrastructure. Keyed calls see SERVICE_UNAVAILABLE for the same outage — retry shortly. |
| `SERVICE_UNAVAILABLE` | 503 | A dependency of the platform is currently unavailable. |
| `OPERATION_TIMEOUT` | 504 | The operation exceeded its own timeout budget. |
| `UPSTREAM_TIMEOUT` | 504 | The third-party service the operation calls did not answer in time. |

## Semantics worth knowing [#semantics-worth-knowing]

These are the cases where the obvious reading of a status is wrong.

### 404 does not distinguish "missing" from "not yours" [#404-does-not-distinguish-missing-from-not-yours]

An unknown slug and an operation that exists but is not published to your
organization both return `404 NOT_FOUND`, with the same message. This is
deliberate: probing the gateway must not reveal which private operations exist.
If you expected an operation to work and get a 404, check the marketplace
listing rather than assuming a typo.

### Hosted-function failures arrive as a 4xx, whatever the code says [#hosted-function-failures-arrive-as-a-4xx-whatever-the-code-says]

Cloudflare replaces any origin 5xx body with its own opaque error page, which
would destroy the envelope. A hosted function's failures are attributable to
your code, so the gateway deliberately returns them with an edge-safe status
instead of the code's canonical one:

| Code                                                      | Table says | You receive |
| --------------------------------------------------------- | ---------- | ----------- |
| `OPERATION_TIMEOUT`                                       | 504        | 408         |
| `SERVICE_BUSY`                                            | 429        | 429         |
| `MEMORY_EXCEEDED`, `INVALID_OUTPUT`, `OPERATION_ERROR`, … | 400 / 502  | 422         |

The canonical status is preserved in an `X-Upapi-Execution-Status` header if you
need it. This remapping applies **only** to hosted functions; marketplace
operations return the status in the table above.

<Callout type="warn" title="Hosted functions also use a different envelope">
  A failure raised inside the isolate comes back as `{"error": "MEMORY_EXCEEDED", "message": "…"}` —
  `error` is a **string**, not the `{ code, message }` object. Read both shapes if you call hosted
  functions, exactly as the gateway itself does.
</Callout>

### `MEMORY_EXCEEDED` is a client error, and always a 422 [#memory_exceeded-is-a-client-error-and-always-a-422]

Exceeding the memory your plan allows is treated as a problem with the request —
you asked for more than you bought — so it is a client error rather than a 500.
Only a hosted function can raise it, so on the wire it is always a `422` by the
remapping above, never the `400` the code table lists. Retrying the same call
unchanged will fail the same way; either shrink the workload or move up a plan.

### `AUTH_UNAVAILABLE` is a 503, not a 401 — on session surfaces [#auth_unavailable-is-a-503-not-a-401--on-session-surfaces]

This code means our authentication infrastructure could not be reached, so your
credentials were never checked. It is an outage signal, not a credential signal.
Retry with backoff; do not rotate keys and do not surface "invalid credentials"
to your users.

It is emitted by the surfaces that authenticate a **session**: the dashboard,
the marketplace try-it runner, and the hosted MCP endpoint. A call authenticated
with an API key gets `503 SERVICE_UNAVAILABLE` for the same outage — a different
code, the same status and the same response. Handle both.

### 429 arrives with a reason [#429-arrives-with-a-reason]

Three codes share the 429 status, and the code — not the status, and not the
size of `Retry-After` — is what tells you whether waiting will help:

* `RATE_LIMITED` — you exceeded the **per-minute burst limit**. This is the one
  429 that clears itself: the window refills within 60 seconds and nothing has
  been spent. Wait and retry.
* `QUOTA_EXCEEDED` — a **budget is spent**. That is the month's weighted units,
  a trial's included units, or your plan's compute seconds for
  [hosted functions](https://upapi.io/docs/platform/hosted-functions). Retrying is pointless
  until the budget refills; `Retry-After`, where present, points at the moment it
  actually does — the next UTC month, or the end of your trial. Upgrading is the
  other fix.
* `SERVICE_BUSY` — execution capacity is momentarily saturated. Retry with
  backoff.

<Callout type="warn" title="This split changed">
  A monthly-quota or trial-cap rejection used to be `RATE_LIMITED` as well, leaving `Retry-After`'s
  magnitude as the only clue. If your client branches on `RATE_LIMITED` alone, it now retries only
  the burst window — which is what it should always have done. Handle `QUOTA_EXCEEDED` as a stop.
</Callout>

See [Rate limits](https://upapi.io/docs/rate-limits) for the headers that tell you which is
which before you hit them.

### The two 409s are about idempotency, and only one is your mistake [#the-two-409s-are-about-idempotency-and-only-one-is-your-mistake]

Both are raised by [`Idempotency-Key`](https://upapi.io/docs/api#idempotency) handling:

* `IDEMPOTENCY_KEY_REUSED` — you sent a key that a **different** request already
  used. Nothing ran. Two requests may not share a key; generate a new one.
* `IDEMPOTENCY_IN_FLIGHT` — you sent a key whose first request is **still
  running**. Nothing ran twice, either. Wait for `Retry-After` (one second) and
  send the same request again; you will get the first one's stored response.

### The two proxy codes name whose proxy failed [#the-two-proxy-codes-name-whose-proxy-failed]

Both mean a proxy refused to carry the request, and the fix is on opposite sides:

* `PROXY_UNAVAILABLE` (502) — **our** exit pool. No proxy was available, or the one
  we leased stopped working mid-call. Nothing about your request caused it and
  nothing in your request can fix it. Retry: a later call leases a different exit.
* `CALLER_PROXY_AUTH_REJECTED` (400) — the proxy **you** supplied, through an
  operation's `proxyUrl`, the `x-proxy-url` header, or your organization's
  configured proxy list, answered our connection with `HTTP 407`. Check its
  credentials, and whether your provider requires allow-listing the IP that
  connects to it. Retrying unchanged fails identically.

### 5xx codes name the layer that failed [#5xx-codes-name-the-layer-that-failed]

`UPSTREAM_*` codes mean the third-party service an operation wraps misbehaved —
the platform worked. `OPERATION_ERROR` and `INVALID_OUTPUT` point at the
operation itself. `INTERNAL_ERROR` is ours; report it with the `x-request-id`.

## Retrying [#retrying]

Retry `SERVICE_BUSY`, `SERVICE_UNAVAILABLE`, `AUTH_UNAVAILABLE`,
`IDEMPOTENCY_IN_FLIGHT`, the `UPSTREAM_*` family and `RATE_LIMITED` — with
exponential backoff, and always respecting `Retry-After` when it is present.

Do not retry `INVALID_INPUT`, `INVALID_JSON`, `INVALID_PATH`, `NOT_FOUND`,
`UNAUTHORIZED`, `FORBIDDEN`, `MEMORY_EXCEEDED`, `IDEMPOTENCY_KEY_REUSED`,
`CALLER_PROXY_AUTH_REJECTED` or `QUOTA_EXCEEDED` without changing something
first; the same request will fail identically.

Because an operation is a POST with real upstream side effects, a retry that is
*safe* is not automatically a retry that is *free* — send an
[`Idempotency-Key`](https://upapi.io/docs/api#idempotency) so a retried call replays the first
result instead of running and billing a second time.

The [TypeScript SDK](https://upapi.io/docs/sdk) implements exactly this policy, idempotency key
included.

<Callout title="One envelope, one exception">
  A request body over 5 MB is rejected at the edge, before it reaches the gateway, with `413` and
  the code `PAYLOAD_TOO_LARGE`. That code is not part of the operation vocabulary above — treat any
  oversized-body handling as a separate branch.
</Callout>
