# Errors

> Every error code the Poacher API and MCP tools return, what it means and what to do.

Source: https://app.poacher.io/developers/errors · API version 1.10.1

Every error has the same shape. `code` is stable; `message` is written for a person; `docs` links to the row below.

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This request requires alerts:read. Ask the account owner to reconnect with that permission.",
    "required_scope": "alerts:read",
    "docs": "https://app.poacher.io/developers/errors#insufficient_scope"
  }
}
```

MCP tools return the same object inside a result with `isError: true`. On `/mcp`, a missing sign-in is an HTTP 401 and a missing permission an HTTP 403 with a `WWW-Authenticate` header, so the client can ask the person to sign in or add the permission ([authorization](https://app.poacher.io/developers/authorization#step-up)).

## Sign-in and permissions {#access}

Fix the credentials or ask the person for more access. Retrying the same request will not help.

| Code | HTTP | What it means | What to do |
| --- | --- | --- | --- |
| `unauthorized` | 401 | The bearer key or token is missing, invalid, expired or revoked. Browser cookies never count. | Send `Authorization: Bearer …` with a current key, or sign in again through OAuth. |
| `invalid_token` | 401 | On `/mcp`: the token was rejected. The `WWW-Authenticate` header says where to sign in. | Start OAuth again from the `resource_metadata` URL in the header. |
| `insufficient_scope` | 403 | The key works but lacks the permission this request needs. `required_scope` names it. | Ask the person to reconnect with that permission, or create a key that has it. Do not retry. |
| `invalid_origin` | 403 | A browser on another site sent an account request. Account requests are server-to-server. | Call from a server, script or desktop agent. Public documents allow any origin. |
| `invalid_host` | 403 | The request reached Poacher through an unexpected host name. | Use `https://app.poacher.io`. |
| `public_interface` | 400 | A credential was sent to the public-only MCP address `/api/mcp`. | Connect to `/mcp` instead; it accepts sign-in. |

## The request itself {#request}

Correct the request and send it again.

| Code | HTTP | What it means | What to do |
| --- | --- | --- | --- |
| `invalid_id` | 400 | An ID in the path is not a positive whole number in range. | Use IDs exactly as the API returned them. |
| `invalid_parameters` | 400 | `limit` or `before` is out of range. | Use `limit` 1–100 and a `before` cursor from `next_before`. |
| `invalid_json` | 400 | The body is not valid JSON. | Send one JSON object. |
| `invalid_content_type` | 415 | The body is not sent as JSON. | Set `Content-Type: application/json`. |
| `request_too_large` | 413 | The body is over the limit: 2 KiB for edits, 16 KiB for paid-work requests. | Send fewer sources or shorter text. |
| `invalid_request` | 400 | A field is missing or malformed. The message names it. | Fix the field the message names. |
| `idempotency_required` | 400 | A quote or edit was sent without an `Idempotency-Key`. | Send a new random key of 16–100 letters, digits, `-` or `_`. |
| `idempotency_conflict` | 409 | That idempotency key was already used for a different request. | Use a new key for a new request; reuse a key only to retry the identical request. |
| `revision_required` | 428 | An edit was sent without `If-Match`. | Read the alert and send its `edit_revision` as `If-Match: "…"`. |
| `method_not_allowed` | 405 | This address does not accept that HTTP method. | Use the method shown in the reference. |
| `not_found` | 404 | The endpoint does not exist, or the record belongs to another account. The two look the same on purpose. | Check the ID came from this account. |
| `invalid_name` | 400 | The alert name is empty or too long. | Send 1–120 characters. |
| `invalid_edit` | 400 | The edit asks for something the API cannot change. | Only `name` and `status: "paused"` can be edited here. |

## Quotes and approvals {#quotes}

Paid work is quoted first and approved with the quote’s exact `request_hash`. These errors keep an approval from applying to anything other than what was quoted.

| Code | HTTP | What it means | What to do |
| --- | --- | --- | --- |
| `quote_expired` | 409 | The quote is older than 15 minutes. | Make a new quote and ask the person again. |
| `quote_changed` | 409 | The quote no longer matches its `request_hash`, or a different key tried to approve it. | Approve with the original key and the exact hash, or make a new quote. |
| `work_cancelled` | 409 | The request was cancelled. | Make a new quote to try again. |
| `api_approval` | 409 | This check or preview can no longer be approved. | Make a new quote. |
| `invalid_quote` | 400 | The quote request is malformed. | Fix the field the message names. |
| `invalid_preview` | 400 | The preview request is malformed: for example both `url` and `sources`, or a bad interval. | Send `url` or `sources`, not both; put intervals on each source. |
| `invalid_edit_preview` | 400 | The changes for a saved-search preview are not supported. | Change only the settings listed for `quote_edit_preview`. |
| `invalid_check` | 400 | The check request is malformed. | Send `alert_id`, `maximum_charge_micros` and an idempotency key. |
| `invalid_schedule` | 400 | A schedule’s minute offset or time zone is invalid. | Use 0–1439 minutes and an IANA time zone. |
| `invalid_source` | 400 | A source URL is not a public web address Poacher can read. | Use a public `https://` search or listing URL. |
| `invalid_url` | 400 | The URL could not be read as a web address. | Send a full `https://` URL. |
| `duplicate_source` | 400 | The same source URL appears twice. | Send each URL once. |
| `incompatible_filters` | 400 | The URLs import different exact price filters. Nothing has started. | Use separate previews or alerts for them. |
| `source_settings_required` | 400 | A source needs settings that only the app can set. | Set up this source in the app. |
| `source_settings_required` | 409 | A saved source needs settings reviewed in the app first. | Open the alert in the app. |
| `check_in_progress` | 409 | An approved check for this alert is already queued or running. | Wait for it with `get_check`, then quote again if needed. |
| `api_daily_limit` | 409 | The person’s daily API allowance cannot cover this ceiling. | Ask the person to raise the allowance in Developer access, or lower the ceiling. |
| `balance` | 409 | The prepaid balance cannot cover the ceiling or minimum reserve. | Ask the person to add funds in the app. |
| `pricing_unavailable` | 409 | Paid work is not available on this deployment yet. | Check `get_capabilities` later. |

## Alerts and saved searches {#alerts}

The alert is in a state where this cannot run. Most of these need the person to act in the app.

| Code | HTTP | What it means | What to do |
| --- | --- | --- | --- |
| `stale_alert` | 409 | The alert changed since you read it. | Read it again and use the new `edit_revision`. |
| `alert_changed` | 409 | The alert changed after the quote. | Make a new quote. |
| `configuration_changed` | 409 | The alert’s settings changed after you read `configuration_revision`. | Read the configuration again and review the change. |
| `configuration_too_large` | 409 | The alert has too many sources to return at once. Nothing partial was returned. | Open the alert in the app. |
| `configuration_unavailable` | 409 | A source on this alert needs review in the app. | Open the alert in the app. |
| `alert_ended` | 409 | The alert’s end date has passed. | Ask the person to extend or recreate it in the app. |
| `alert_held` | 409 | The alert is paused for repair, billing or a result limit. | Ask the person to resolve the hold in the app. |
| `account_held` | 409 | The account has a funding or billing hold. | Ask the person to resolve it in the app. |
| `source_not_ready` | 409 | The alert needs its own active sources before a check can run. | Ask the person to refresh the alert in the app. |
| `source_unavailable` | 409 | A source is not available right now. | Try later, or check the source in the app. |
| `brief_needs_review` | 409 | The saved matching brief needs review before it can be reused. | Send complete replacement `criteria`, or review the brief in the app. |
| `edit_target_required` | 409 | This preview can only replace the alert it was made from. | Use the original alert, or start a new preview. |
| `draft_unavailable` | 409 | This preview was already used, or is a replacement draft. | Start a new preview. |

## Creating alerts and monitoring {#monitoring}

Only for keys with `monitoring:manage`, and only while finite monitoring is switched on.

| Code | HTTP | What it means | What to do |
| --- | --- | --- | --- |
| `monitoring_unavailable` | 503 | Finite monitoring is switched off on this deployment. | Use `start_alert_in_app` and let the person finish in Poacher. |
| `invalid_creation` | 400 | The creation request is malformed. | Fix the field the message names. |
| `invalid_update` | 400 | The update request is malformed. | Fix the field the message names. |
| `invalid_monitoring` | 400 | The monitoring request is malformed: for example an expiry not in exact UTC milliseconds. | Use an expiry like `2026-12-01T00:00:00.000Z`, within 30 days. |
| `key_expiry` | 400 | Monitoring would outlast the key. | End monitoring before the key expires. |
| `preview_not_ready` | 409 | The preview has not finished. | Wait for its final receipt with `get_preview`. |
| `preview_not_verified` | 409 | The preview is not a finished, settled, successful preview. | Run a new preview and wait for its final receipt. |
| `price_not_verified` | 409 | The preview lacks a complete running-price estimate. | Run a new preview. |
| `preview_changed` | 409 | The preview changed after the quote. | Make a new quote. |
| `connection_changed` | 409 | A selected channel is no longer enabled or no longer belongs to this account. | List channels again and choose enabled ones. |
| `minimum_not_accepted` | 409 | The alert’s monthly minimum has not been accepted. | Ask the person to accept it in the app. |
| `authorization_ended` | 409 | This monitoring permission has ended. | Quote new monitoring if the person wants it. |

## Channels {#channels}

Every new channel stays off until the person enters the code it received.

| Code | HTTP | What it means | What to do |
| --- | --- | --- | --- |
| `invalid_type` | 400 | The channel type is not one the API can add. | Use `slack`, `discord`, `ntfy` or `sms`. Email and browser notifications are set up in the app. |
| `invalid_destination` | 400 | The webhook, topic or phone number is not valid for that type. | Check the destination format in the reference. |
| `acknowledgement_required` | 400 | An ntfy topic was added without confirming it is private. | Send `ntfy_topic_is_private: true` once the person confirms. |
| `consent_required` | 400 | A phone number was added without the person’s agreement to text messages. | Show the disclosure from `get_capabilities` (`channels.sms`) and send `sms_consent` only after they agree. |
| `disclosure_changed` | 409 | The text-message disclosure changed since the person agreed. | Show the current disclosure again and send its version. |
| `sms_unavailable` | 503 | Text messages are not switched on yet. | Check `channels.sms.available` in `get_capabilities`. |
| `invalid_code` | 400 | The code is not six digits. | Ask the person for the 6-digit code. |
| `code_rejected` | 409 | The code is wrong or expired, or too many wrong codes were sent. | Ask the person to check the code, or send a new one with `resend_channel_code`. |
| `channel_refused` | 409 | The channel cannot be used: for example a number that replied STOP. | Ask the person to choose another channel. |

## Limits and outages {#limits}

Wait, then retry the identical request.

| Code | HTTP | What it means | What to do |
| --- | --- | --- | --- |
| `rate_limited` | 429 | A request limit was reached. `Retry-After` says how many seconds to wait. | Wait that long, then retry. Do not retry sooner. |
| `temporarily_unavailable` | 503 | Poacher could not complete the request. | Retry with backoff. Reuse the same idempotency key so nothing runs twice. |
