# Errors

Every API error has an HTTP status and a JSON body with a stable, machine-readable `code`, a
human-readable `message` and, sometimes, `details`:

```json
{
  "error": {
    "code": "missing_files",
    "message": "Upload every file before finalizing",
    "details": { "missingFiles": ["index.html"] }
  }
}
```

Match on `code`, not on `message`: messages can be reworded. The CLI prints the same code and
message to stderr (for example `missing_files: Upload every file before finalizing`) and exits
with status 1.

## Requests and manifests

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `invalid_json` | 400 | The body is not a JSON object. | Send a JSON object with `Content-Type: application/json`. |
| `body_too_large` | 413 | The request body is too large. | Send a smaller manifest. |
| `invalid_manifest` | 400 | `files` is missing, empty, has too many entries, or an entry is not an object. A ZIP can also expand past the file limit. | Send 1 to 500 files. |
| `invalid_path` | 400 | A file path is hidden, traverses folders, is too long, is not NFC-normalised, starts with the reserved `__edge` segment or has a forbidden character. | Use relative, NFC-normalised paths with no segments starting with `.` (which also rules out `..`), and no `%`, `?`, `#`, backslashes or control characters. |
| `duplicate_path` | 400 | Two files have the same path, possibly after a ZIP was unpacked. | Make every path unique. |
| `invalid_hash` | 400 | A file hash is not a lower-case SHA-256 hex string. | Send the SHA-256 of each file's exact bytes. |
| `invalid_content_type` | 400 | A content type is malformed. | Use a plain type such as `text/html; charset=utf-8`. |
| `invalid_spa_mode` | 400 | `spaMode` is not `true` or `false`. | Send a boolean. |
| `missing_index` | 400 | Single-page app mode is on but there is no root `index.html`. | Add `index.html`, or set `spaMode` to `false`. |
| `invalid_title` | 400 | The title is not a string of up to 120 characters. | Shorten the title. |
| `invalid_slug` | 400 | The name is not 3 to 48 lower-case letters, numbers and inner hyphens, or it is reserved. | Choose another name, or omit it for a random one. |
| `invalid_password` | 400 | A visitor password is not 8 to 256 characters. | Choose a longer or shorter password. |
| `use_metadata` | 400 | An update tried to change the password. | Use `PATCH /publish/{slug}/metadata`. |
| `empty_patch` | 400 | A metadata change had nothing to change. | Send `title`, `password` or `feedbackEnabled`. |
| `version_required` | 400 | A finalise call was sent without a `versionId`. | Send the `versionId` from the create or update response. |

## Access

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `unauthorized` | 401 | The key is missing, malformed, revoked or removed. | Check the key. If it worked before, it has probably been removed: ask the account owner, do not retry. |
| `anonymous_disabled` | 401 | This service only accepts publishing with a key. | Send a key. |
| `forbidden` | 403 | The key does not own this link. | Use the owner's key. |
| `claim_required` | 403 | The link is an unclaimed guest link and no valid claim token was sent. | Send its claim token, or claim it first. |
| `feedback_owner_only` | 403 | Only the owner can turn feedback on or off. | Use the owner's key, not a claim token. |
| `cross_origin` | 403 | A browser request came from another website. | Call the API from a server, CLI or agent. |
| `wrong_host` | 421 | The request was sent to the wrong host. | Use `https://publish.dropzoom.link`. |
| `challenge_required` | 400 | A request without a key tried to create a guest link, which needs a human check. | Send a key. Guest links are for people in a browser. |
| `challenge_failed` | 403 | The human check for a guest link did not pass. | Complete the check again in the browser. |
| `challenge_expired` | 403 | The human check expired or was already used. | Complete a new check in the browser. |

## Uploads and finalising

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `invalid_upload_token` | 403 | The upload URL's token is wrong or was replaced by a refresh. | Use the latest upload URLs. |
| `length_required` | 411 | An upload had no `Content-Length`. | Send `Content-Length` equal to the manifest size. |
| `size_mismatch` | 400 | The uploaded bytes differ in size from the manifest. | Upload the exact file you described. |
| `checksum_mismatch` | 400 | The uploaded bytes do not match the manifest hash. | Upload the exact file you described. |
| `upload_expired` | 410 | Upload URLs last one hour, and a staged version can only be refreshed for 24 hours after it was created. | Within 24 hours, call `uploads/refresh` for fresh upload URLs, then upload and finalise. After 24 hours, stage a new version by publishing again. |
| `missing_files` | 400 | Finalising was attempted before every file was uploaded. `details.missingFiles` lists them. | Upload the listed files, then finalise again. |
| `invalid_zip` | 400 | A ZIP file is damaged or uses a feature DropZoom does not support. | Rebuild the ZIP with standard settings. |
| `invalid_zip_path` | 400 | A path inside a ZIP is unsafe, such as an absolute path or one that traverses folders. | Rebuild the ZIP with relative paths. |
| `encrypted_zip` | 400 | The ZIP is password-protected. | Upload an unencrypted ZIP. |
| `empty_zip` | 400 | The ZIP has no publishable files. | Check what the ZIP contains. |
| `nested_archive` | 400 | A ZIP contains another ZIP, 7z or tar archive. | Unpack inner archives before zipping. |
| `zip_changed` | 409 | The ZIP changed while it was being unpacked. | Publish again. |
| `secret_detected` | 422 | The content looks like it contains a secret, such as a private key or access token. `details.files` names the files. | Remove the secret, rotate it if it was real, then publish again. |
| `malware_detected` | 422 | A guest publication was refused by malware scanning. | Do not publish that file. |
| `finalize_conflict` | 409 | The version changed while it was being finalised. | Retry finalising. |

## Conflicts and state

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `not_found` | 404 | The link, version, upload or route does not exist. | Check the name and path. |
| `method_not_allowed` | 405 | The route exists but not with that HTTP method. | Use a method listed for the route in the [HTTP API](/docs/api/). |
| `expired` | 410 | The guest link has expired. | Publish again. Expired guest links cannot be claimed. |
| `slug_taken` | 409 | The name is already in use, or was used before. Names are never reused. | Choose another name. |
| `anonymous_slug` | 400 | A guest publication tried to choose its name. | Omit `slug`; guest links get random names. |
| `version_conflict` | 409 | Someone else published first. `details.currentVersionId` shows the live version when known. | Read the link, then publish again from its current version. |
| `cannot_prune` | 409 | The version is live or does not exist. | Prune a version that is not live. |
| `already_claimed` | 409 | Another account owns this guest link now. | Nothing to do. |
| `claim_conflict` | 409 | The link changed while it was being claimed. | Retry the claim. |

## Limits

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `file_too_large` | 413 | A file is over the per-file limit. | See [Limits](/docs/limits/). |
| `site_too_large` | 413 | The publication is over the per-publication limit, possibly after unpacking a ZIP. | Publish less, or split it across links. |
| `guest_site_too_large` | 413 | A guest publication is over the guest limit. | Register to publish larger work. |
| `storage_limit` | 413 | Your account's total storage is full. Only the live and unfinished (staged) versions of your links count. | Delete links you no longer need, prune unfinished versions, or publish less. Pruning retired versions does not free space, because they do not count. |
| `guest_capacity` | 503 | Guest publishing is temporarily full. | Register to publish now. |
| `site_limit` | 409 | You have reached the number of links an account can have. | Delete a link you no longer need. |
| `version_limit` | 409 | This link has reached the number of versions it can keep, including pruned versions that have not been cleaned up yet. | Prune old versions, then retry after cleanup has removed them, which takes about 2 hours. |
| `rate_limited` | 429 | Too many requests from your address. | Wait a minute, then retry. |

## Feedback

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `invalid_state` | 400 | `state` is not `open`, `resolved` or `all`. | Use one of those values. |
| `invalid_limit` | 400 | `limit` is not a whole number from 1 to 200. | Use a smaller whole number. |
| `invalid_cursor` | 400 | The pagination cursor is malformed. | Use the `nextCursor` value exactly as returned. |
| `invalid_feedback_enabled` | 400 | `feedbackEnabled` is not `true` or `false`. | Send a boolean. |
| `invalid_request` | 400 | Resolve needs `ids` or `all`, not both, and IDs must be 32-character feedback IDs. | Send one or the other. |
| `too_many_ids` | 400 | More than 90 IDs in one resolve call. | Split the IDs across calls. The CLI does this for you. |
| `feedback_not_found` | 404 | One or more IDs are not feedback on this link. `details.ids` lists them. | Check the IDs. |

## Temporary problems

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `content_scan_unavailable` | 503 | Secret scanning could not finish. Nothing was published. | Retry finalising later. |
| `malware_scan_unavailable` | 503 | Malware scanning for a guest publication could not finish. Nothing was published. | Retry later. |
| `zip_unpack_unavailable` | 503 | A ZIP could not be unpacked just now. | Retry finalising later. |
| `challenge_unavailable` | 503 | The human check could not be completed. | Retry later. |
| `publish_outcome_unknown` | 503 | The service could not confirm whether the link was created. | List your links before retrying, so you do not create a duplicate. |
| `internal_error` | 500 | Something unexpected went wrong. | Retry later. If it keeps happening, use the [contact form](https://publish.dropzoom.link/contact). |
