---
title: Errors and retries
description: Handle HTTP errors, rate limits, and safe retries for OpenTester automation requests.
---

# Errors and retries {#errors-and-retries}

API failures return an HTTP status and JSON body:

```json
{
  "message": "Feedback not found.",
  "statusCode": 404
}
```

This example uses `Accept-Language: en`. `message` is display text and varies by language. Validation errors may include field paths such as `title: ...; issueIds: ...`. The REST API does not provide a separate stable string error code.

## HTTP status handling {#http-status-handling}

| Status | Typical cause | Action |
| --- | --- | --- |
| `400` | Invalid UUID, field, query, or PR URL; feedback from another app | Correct the input |
| `401` | Missing session, expired/revoked/invalid token, Bearer used for token management | Check authentication |
| `403` | Not the owner, wrong app token, missing scope, disallowed Origin | Check app, scope, and origin |
| `404` | Feedback, request, or token not found in the app | Recheck IDs and app |
| `409` | Same request ID with different content, or a different PR already linked | Inspect existing work |
| `413` | JSON body exceeds 64 KiB | Reduce the body |
| `429` | Too many requests | Wait for `Retry-After` seconds |
| `500` | Server failure | Retry with a bounded delay |

The first failed check determines the response. For example, a malformed Bearer mutation without Origin may return `403` before token validation.

## Rate limits {#rate-limits}

The default automation limit is 120 requests per minute per IP, shared by feedback, change requests, and token management. Changing apps or tokens does not reset that IP's allowance. Deployment and proxy settings may impose additional limits.

A `429` response provides the remaining wait in seconds:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 28
Content-Type: application/json
```

Space out polling and fetch only the pages you need. Avoid repeatedly sending identical requests in a tight loop.

## Missing responses {#missing-responses}

The server may have completed a request even when a connection drops or the client times out.

| Operation | Retry rule |
| --- | --- |
| Read feedback, requests, or tokens | Retry the same request |
| Create change request | Keep the same `clientRequestId` and body |
| Link PR | Keep the same request ID and PR URL |
| Revoke token | Retry the same token ID |
| Issue token | Do not retry automatically; each request issues another token |

Creation uses JSON `clientRequestId`, not an `Idempotency-Key` header. The same app/creator/key with different content returns `409`. Retain the ID together with its body until the outcome is known.

Honor `Retry-After` for `429`. Use bounded exponential backoff with jitter for temporary network/server errors. Resolve the cause before repeating `400`, `401`, `403`, `404`, or `409` requests.

## Common issues {#common-issues}

### 403 on another app owned by the same account {#403-on-another-app-owned-by-the-same-account}

Tokens belong to one app. Issue a token for the target app and match `OPENTESTER_APP_ID`.

### Writes work but reads fail {#writes-work-but-reads-fail}

Scopes are independent. Feedback reads need `feedback:read`; change request reads need `work-items:read`.

### Requests from the docs domain fail {#requests-from-the-docs-domain-fail}

The docs site is static. Use the API base URL for automation, and the signed-in console's Origin for web-session requests such as token management. See [authentication](/api/authentication).

### Feedback remains open after linking a PR {#feedback-remains-open-after-linking-a-pr}

This is expected. Linking stores the PR URL on the change request. Review and change feedback status separately in the console.
