# Errors

This page lists every status the ScreenshotBuddy API answers with, what each one means, and which of them are worth sending again.

> Check the status code before you touch the body. A successful response is the rendered file itself, so parsing it as JSON fails on a working request.

## The shape of an error

Every error is JSON in the same shape, whatever went wrong.

| Field | Type | Description |
| --- | --- | --- |
| `code` | string | The reason the request was refused, as a short stable string. This is the field to branch on. Every code is listed under error codes below. |
| `message` | string | The same reason in words, written for a person to read. It can be reworded or made more specific at any time, so show it to a human rather than matching your code against it. |
| `errors` | object | Sent only when a particular parameter is at fault. It is keyed by the query parameter, with the messages for that parameter, and it is left out entirely otherwise. |
| `request_id` | string | Identifies that one answer, and it is in our logs against the request that produced it. Quote it when you ask us about a failure and we can look up exactly what happened. |

You get JSON whatever `Accept` header you send, so a client that asks for nothing in particular still gets an error it can parse rather than an HTML page.

The `request_id` is also sent as the `X-Request-Id` header, and the two are always the same value. A successful render answers with the file rather than JSON, so the header is the one place the id exists on every answer we send. Log it from there and you have it whether the request worked or not.

## Statuses

| Status | Worth retrying | Codes | Description |
| --- | --- | --- | --- |
| `200` | success |  | The rendered file. The body is the image or the PDF itself rather than JSON, so write it to a file instead of parsing it. The Content-Type names the format that was rendered. |
| `304` | success |  | The rendering is byte for byte the copy already held by the caller, named in the `If-None-Match` of the request, so no body is sent. What it costs depends on how that was established. An answer the cache could give is free: no credit, and counted against neither per-minute budget. One that had to render the page first, because the request sent `cache=0` or because the entry had lapsed, costs exactly what that render costs, a credit and a slot of the render budget, and what the caller saves is the download. `X-Cache: HIT` marks the free one; anything else was rendered, and carries the X-RateLimit headers to say so. |
| `401` | do not retry | `unauthenticated` | The request carried no bearer token, or a token that is unknown or revoked. |
| `402` | do not retry | `no_active_plan`, `credit_limit_reached` | The account has no active plan, or it has used every credit in the current period. The code says which of the two it is. |
| `403` | do not retry | `email_unverified`, `missing_ability` | The email address of the account has not been verified yet, or the token is not allowed to ask for the mode the request is in. The code says which of the two it is: a token carries a permission per mode, and one narrowed to screenshots cannot print a PDF. |
| `422` | do not retry | `validation_failed`, `invalid_url`, `blocked_host`, `target_unreachable` | A query parameter did not validate, or the target URL could not be reached or loaded ("The target URL could not be reached or loaded. Check that it is publicly available and try again."). The errors object names the parameter at fault. No credit is charged. |
| `429` | retry | `rate_limited` | The account went over one of its two per-minute limits: the renders its plan allows, or the separate and far larger number of answers it may be served out of the cache. The X-RateLimit headers on this answer describe the limit that refused it. Wait for the number of seconds in the Retry-After header before sending the request again. |
| `500` | do not retry | `render_failed`, `server_error` | The render failed for a reason we did not recognise. The credit is refunded. |
| `502` | retry | `upstream_error`, `upstream_quota_exceeded` | The rendering service failed to process the request. The credit is refunded and the request is worth sending again. |
| `503` | retry | `upstream_rate_limited` | The rendering service was momentarily busy and stayed busy across our own retries. The credit is refunded. Unlike the 502 this is not a failure of anything: wait the few seconds in the Retry-After header and the same request goes through. |
| `504` | retry | `render_timeout` | The render did not finish within its time budget. The credit is refunded and the request is worth sending again. |

The statuses marked `retry` (429, 502, 503 and 504) carry a `Retry-After` header with the number of seconds to wait before sending the same request again. Wait that long rather than retrying straight away: a retry that arrives sooner is answered the same way.

The others describe a request that would fail the same way every time, so change something before you send it again. A render that fails after the request was accepted refunds its credit, so a retry costs you nothing you were not going to spend.

## Error codes

Every code the API can answer with, and the status it comes back on. A status can carry more than one code, because the status alone does not always say what happened: both `402`s stop the request, but only one of them is fixed by buying more credits.

| Code | Status | Meaning |
| --- | --- | --- |
| `unauthenticated` | 401 | No bearer token was sent, or the token is unknown or revoked. |
| `invalid_signature` | 401 | A signed URL did not verify. Either the signature does not match the query, or the token it names is unknown, revoked or expired. Rebuild the URL from the canonical form on the signed URLs page, and check that nothing was appended to it after it was signed. |
| `signed_url_expired` | 401 | A signed URL is past the `expires` moment it was signed with. The signature itself was fine, so sign a new URL with a later expiry. |
| `email_unverified` | 403 | The account exists but its email address has not been verified yet. |
| `missing_ability` | 403 | The token is not allowed to ask for this mode. A token carries a permission per mode, `screenshot` and `pdf`, and both are granted unless the token was deliberately narrowed. Widen it in your dashboard, or send the request with a token that holds the permission. |
| `no_active_plan` | 402 | The account is on no plan, so it has no credits to spend. Pick a plan and the same request works. |
| `credit_limit_reached` | 402 | Every credit in the current period is spent. It resets at the end of the period. |
| `validation_failed` | 422 | A query parameter is missing, malformed, or not accepted in the mode the request is in. The errors object names it. |
| `invalid_url` | 422 | The url is missing or is not a URL the API will accept. Fix the string and send it again. |
| `blocked_host` | 422 | The url points at a private, loopback or internal address. Reformatting it will not help; only a publicly reachable host is rendered. |
| `target_unreachable` | 422 | The page itself could not be reached or loaded. Check that it is publicly available. |
| `rate_limited` | 429 | The account went over its renders per minute, or over the separate limit on the answers it may be served out of the cache. The X-RateLimit headers say which of the two. Wait for the Retry-After header and send the request again. |
| `render_failed` | 500 | The render failed for a reason we did not recognise. The credit is refunded. |
| `server_error` | 500 | Something failed that we did not expect at all. The catch-all, and always worth reporting with the request id. |
| `upstream_error` | 502 | The rendering service failed to process the request. The credit is refunded and a retry is worthwhile. |
| `upstream_quota_exceeded` | 502 | The rendering service has spent its own capacity for the period. Nothing is broken, so back off for longer than you would on an ordinary upstream failure. |
| `upstream_rate_limited` | 503 | The rendering service was busy and stayed busy while we retried. Nothing is broken and nothing about the request is wrong; wait the seconds in Retry-After and send it again. |
| `render_timeout` | 504 | The render ran past its time budget. The credit is refunded and the same request may well succeed later. |
| `not_found` | 404 | No endpoint exists at that path. |
| `method_not_allowed` | 405 | The endpoint exists but does not accept that HTTP method. The Allow header names the ones it does. |
| `api_moved` | 410 | The request went to the base URL the API used to be served on. Nothing about the request is wrong: send it to the base URL named in the message and the path after it, the parameters, the headers and the token all stay as they are. |

Three of them are not tied to taking a screenshot: `not_found`, `method_not_allowed` and `server_error` can answer any request under `https://api.screenshotbuddy.io/v1`, so handle them wherever you call us rather than only around a render.

A fourth, `api_moved`, arrives from nowhere on this host at all: it is what the base URL the API used to be served on answers now, and nothing but the base URL has to change to make it go away.

Codes are a published contract. We add new ones as the API grows, so treat one you do not recognise the way you would treat its status, and keep the `request_id` either way.

## Validation errors

A request that leaves out `url`, or sends a parameter the mode it is in does not accept, comes back as `422` with the parameter named in the `errors` object. The code says which kind of failure it was: `invalid_url` for a url the API will not take, `blocked_host` for one it will not visit, and `validation_failed` for any other parameter.

```json
{
    "code": "invalid_url",
    "message": "The url query parameter is required.",
    "errors": {
        "url": [
            "The url query parameter is required."
        ]
    },
    "request_id": "1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"
}
```

Which parameters a mode accepts is on the [taking screenshots](https://screenshotbuddy.io/documentation/taking-screenshots.md) and [creating PDFs](https://screenshotbuddy.io/documentation/creating-pdfs.md) pages. The `429` status has a page of its own under [rate limits](https://screenshotbuddy.io/documentation/rate-limits.md).
