# Errors

Errors come back with an HTTP status and a JSON body with a stable `code`, a human `message` and, for some codes, `details`. Branch on `code`, not on `message`.

```json title=Error body
{
  "call_id": "call_...",
  "error": {
    "code": "invalid_input",
    "message": "Input failed validation",
    "details": [{ "path": ["siteUrl"], "message": "Invalid input: expected string, received undefined" }]
  }
}
```

Over MCP the same code and message are returned as a tool error.

## Codes

| Code | HTTP | What happened | What to do |
| --- | --- | --- | --- |
| `unauthenticated` | 401 | The API key is missing, malformed or revoked. | Send `Authorization: Bearer oa_…` with a current key. |
| `invalid_json` | 400 | The request body isn't JSON. | Send a JSON body with `Content-Type: application/json`. |
| `invalid_input` | 400 | The body or the endpoint's `input` failed validation. `details` lists each issue. | Fix the fields named in `details`. The input schema is at `GET /v1/providers/:id`. |
| `provider_not_found` | 404 | No provider with that id. | Use an id from `GET /v1/providers`. |
| `endpoint_not_found` | 404 | The provider has no raw endpoint with that id. | Use an id from `GET /v1/providers/:id`. |
| `tool_not_found` | 404 | The provider has no smart tool with that name. | Use a name from `GET /v1/providers/:id`. |
| `connection_not_found` | 404 | No active connection for this provider that you can see, or the `connection_id` doesn't exist or is outside your clients. | Connect an account, or check `GET /v1/connections`. |
| `connection_ambiguous` | 400 | Several active connections match. `details.connection_ids` lists them. | Pass `connection_id`. |
| `connection_inactive` | 409 | The connection you named is expired or revoked. | Reconnect it in the dashboard or send a new connect link. |
| `read_only` | 403 | A read-only key or grant tried an endpoint or tool that writes. | Use a read-write key, or approve the agent with read-write access. |
| `credentials_expired` | 401 | The account owner revoked access, or Google expired it. The connection is marked **Token expired**. | Reconnect the account. For a client, send a new connect link. |
| `credentials_rejected` | 502 | Google refused the stored credentials, for example a service account without access. | Check the account's access to the property, then reconnect. |
| `upstream_error` | provider's status | The provider answered with an error. `details` holds its response body. | Read `details`; often a wrong property id or a date range with no data. |
| `upstream_unreachable` | 502 | The provider couldn't be reached. | Retry with backoff. |
| `internal_error` | 500 | Something failed on our side. | Retry; if it persists, send us the `call_id`. |

Every failed call is logged. Look it up by `call_id` in **Call logs** or with `GET /v1/logs/:call_id`.
