# Troubleshooting

> Find the next step for an offline browser, a pending approval, or an API request that didn't complete.

When a request can't complete, the API returns an HTTP status and a short JSON message:

```json
{ "error": "extension offline" }
```

## Common responses

| Response | Next step |
|---|---|
| `503 extension offline` | Open the connected browser. [Check its status](/docs/connect/#a-users-browsers) before trying again. |
| `503 extension outdated` | Apigami in that browser must be updated before it can connect again. Chrome installs the update on its own; if it hasn't reconnected within a few minutes, ask the user to restart Chrome. |
| `403 not paired` | This user isn't connected to this app. Send them through the [connect page](/docs/connect/). |
| `403 account disabled` | Apigami disabled this account. Skip this person: your other users still answer, and this one may come back. |
| `403 app is private` | This person left your private app's team. They pass again if they come back. |
| `404 unknown user_ref` | The reference isn't one of your team's. Check you saved the `user_ref` from [the code exchange](/docs/connect/#3-exchange-the-code) and use a key of the same team. |
| `403 consent requested` | The approval window is waiting for an answer. Let the user respond, then try again. |
| `403 no grant` | The user declined or removed access. They can change this from Apigami's menu in their browser. |
| `401 bad client credentials` | Check the API key. It may be missing, incorrect or revoked. |

An unanswered approval also includes `"consentPending": true`. Don't open a new connection
flow for this: the browser is connected and waiting for the user's decision.

## Handle an error in your code

```js
const data = await response.json()

if (!response.ok) {
  if (data.consentPending) {
    console.log('Please answer the approval window in your browser.')
  } else {
    console.error(data.error)
  }
} else {
  console.log(data.result)
}
```

## Other responses

<details>
<summary>Account access and verification</summary>

| Response | Next step |
|---|---|
| `403 client disabled` | No member of the app's team has an active account. [Contact us](mailto:contact@apigami.com) if you think this is a mistake. |
| `403 client not verified` | This app has reached ten connected users. [Contact us for verification](/docs/authentication/#verification). |
| Email not confirmed | Creating an app and using [the MCP server](/docs/mcp/) require a confirmed address. The dashboard and the MCP server send the confirmation email; open its link, then try again. |
| `400 clientId does not match the token` | Remove `appId` or `clientId` from the body. The API key already identifies your app. |

</details>

<details>
<summary>Connecting a user</summary>

| Response | Next step |
|---|---|
| `400 invalid_grant` from `POST /connect/token` | The code is wrong, expired, already used or issued to another app, or `code_verifier` or `redirect_uri` doesn't match. [Start a new connection](/docs/connect/#1-start-the-connection-on-your-server). |
| `400 redirect_uri is not registered for this app` | Register the URL in the dashboard, path included. Only the query and the fragment can vary. |
| `400 code_challenge (S256) is required` | Send a base64url SHA-256 challenge and `code_challenge_method: "S256"`. |
| `error=access_denied` on your redirect URL | The user cancelled. Offer the connect button again. |
| `error=expired` on your redirect URL | The user took more than 30 minutes. Start a new connection. |

</details>

<details>
<summary>Choosing the user's browser</summary>

| Response | Next step |
|---|---|
| `403 install_ref is not a browser of this user` | Use an `install_ref` listed by [`GET /v1/users/{user_ref}/installs`](/docs/connect/#a-users-browsers), or leave it out. |
| `409 no browser is attached to this user` | Ask the user to open your connect link in the Chrome they use and attach it. |
| `409 several browsers of this user are online…` | The site was never asked about and more than one browser could ask. Send `install_ref`. |
| `409 no browser of this user holds the permission for …` | None of the user's browsers can reach that site. Ask the user to open that browser, or send `install_ref`. |

</details>

<details>
<summary>Invalid request parameters</summary>

| Message | Next step |
|---|---|
| `user_ref is required` | Send the user's `user_ref` in the body. It replaces `extensionId`: get one by [connecting the user](/docs/connect/). |
| `user_ref must be a string` or `install_ref must be a string` | Send the references as strings, exactly as Apigami returned them. |
| `op is required` | Include the name of the [operation](/docs/operations/). |
| `params.domain is required` | Include a hostname such as `example.com` in `params.domain`. |
| `targetDomain is required` | Include the hostname in the approval request. |

These responses have HTTP status `400`.

`403 <domain> is Apigami's own domain` answers a call or an approval request on `apigami.com`,
`apigami.tech` or a host under them, such as your application's address, `{slug}.apigami.tech`: no
operation may run there and no approval window opens for it.

</details>

<details>
<summary>Emails and AI</summary>

`emails.send` and `ai.generate` are answered by Apigami, never a browser, and refuse with their own
messages, each with what to do: [Emails to your users](/docs/emails/#refusals) and
[AI in your application](/docs/ai/#refusals).

</details>

<details>
<summary>Event and list query parameters</summary>

| Message | Next step |
|---|---|
| `since must be an ISO timestamp` | Send an ISO timestamp such as `2026-09-10T00:00:00Z`. |
| `bad cursor` | Pass back the previous response's `next` value without changing it. |
| `limit must be a number` | Send an integer. The default is 100 for `GET /events` and 500 for `GET /v1/users` and `GET /v1/installs`; the maximum is 1000. |

These responses have HTTP status `400`. See [Reading events](/docs/webhooks/#reading-the-journal-get-events) and
[Everyone connected to your app](/docs/connect/#everyone-connected-to-your-app).

</details>

<details>
<summary>An operation failed or timed out</summary>

| Response | Next step |
|---|---|
| `502 extension disconnected` | The browser disconnected during the call. Check whether the action completed before retrying. |
| `502 operation has timed out` | The browser did not answer within 35 seconds, or stopped answering for 20 seconds during the call. Reduce the work and check for a result before retrying. |
| `502 operation failed` or another browser message | Check the URL, tab, selector and approved domain for the operation. |
| `tab <id> was closed` or `tab <id> is not on the granted domain <domain>` | Pass the `tabId` `tabs.open` returned, or one from `tabs.list`. A tab id belongs to one browser until it restarts. |
| `the tab opened on <domain> is still loading` | The tab has not reached the site yet. Try again in a moment. |
| `request.fetch` answers `status: 403` with `x-datadome`, `cf-mitigated: challenge`, or a body naming `captcha-delivery` or `challenge-platform` | Anti-bot protection, not a sign-in problem. Open a tab of the site with `tabs.open`, then retry. Don't read or copy the anti-bot cookie. |
| `403 <host> serves another application's pages, on its author's own domain…` | Another application, of your group or not, serves its pages at that name, its author's [own address](/docs/app-domain/#your-own-address-a-custom-hostname), and they hold their token there: no operation of yours runs on that name or under it, and no approval window opens for it. Its parent domain and the names beside it stay reachable. If the name is yours, [contact us](mailto:contact@apigami.com). |
| `500 internal error` | An Apigami error occurred. [Contact us](mailto:contact@apigami.com) if it persists. |
| Your app's address, `https://{slug}.apigami.tech`, answers `404` *No application answers at this address.* | No version answers at that host: check the slug, deploy a version with `app_deploy` and publish it with `app_publish`. `{slug}--staging` serves the version your latest deployment put there, and answers this when there is none. A slug you renamed away from answers this too. |
| A page of your app's address answers `503` *This page took too long* or *This page was stopped* | That request used more than its 300 seconds of computing time, or the 128 MB of memory its instance shares with the requests it serves at once: look for a loop that does not end, or move the work to an action. `app_logs` says which ([Limits](/docs/app-server/#limits)). |
| A page of your app's address answers `502` *This page failed* | Your code threw while answering it: `app_logs` has what it printed. |
| An action or `GET /v1/me` answers `401 bad run token` to a page's token | The token expired or was renewed, or the request comes from another origin than its page's. Use the latest token; `apigami.js` keeps it for you. Full page at your app's address, the person's session renews the token in silence until 30 days go by without a visit; on your own site, `apigami.js` renews it for twelve hours after the person went through your connect link. A `401` there means the session or those twelve hours ended, or the renewal was refused because the account, its connection or its browser no longer passes, or the person signed out of Apigami's dashboard. `apigami.js` then sends the person to be connected again at the next action that needs one, and they come back to the page. |
| An action's answer has no `X-Apigami-*` header your code set | Apigami removes them all: they are its own. `X-Apigami-Settings` is read before, to [ask the person to set up or reconnect](/docs/app-sign-in/#ask-a-person-to-set-up-or-reconnect). |

A `502` whose body carries `"outcome": "unknown"` means the browser received the call and its answer
was lost: the request may have reached the site. Before repeating a write, such as sending a message,
read the site to see whether the first one went through.

Messages such as `invalid url`, `no open tab for example.com` or `no element matches` explain
what the browser couldn't do. The [operation guide](/docs/operations/) lists the expected parameters.

</details>

API calls aren't queued or retried automatically. Before retrying an action that changes something
on a site, check whether it already completed. This avoids sending a message or submitting a form twice.