# Errors

An error answers with an HTTP status and a JSON body that names a stable code:

```json
{"error": "invalid", "details": {"name": ["has already been taken"]}}
```

`error` is always there; read it, not the wording. `details` appears only for some codes. Its messages are written for
people: show them to the admin, do not parse them.

## Every code

| Status | `error` | Meaning | What to do |
|---|---|---|---|
| `400` | `bad_request` | The body is not valid JSON, or a required parameter is missing (then `details.missing` names it). | Fix the request. Send `Content-Type: application/json` with a valid, flat JSON body. |
| `401` | `invalid_token` | The key is missing, mistyped, expired or revoked, or its admin is no longer an owner or admin of the community. The answer is the same for every cause, with the header `WWW-Authenticate: Bearer realm="Zen Social", error="invalid_token"`. | Check the `Authorization: Bearer …` header. If it is right, stop and ask the admin for a new key. See [Keys and access](keys.md). |
| `403` | `forbidden` | The key works, but its admin could not do this in the app, or (for `/conversations`) the key was not made with the permission **May import direct messages**. For example change a circle they are not an owner or admin of, post in a circle they are not a member of or may only read or comment in, comment in a circle they may only read, change a comment someone else wrote in the app, or read a post or event they could not see. | Ask the admin to join that circle in the app, with a role that may post there, or that may comment there for a comment (as its owner or admin, if it is to be changed). Do not look for a way around it. |
| `404` | `not_found` | It does not exist, it belongs to another community, or it is a deleted post or event. Also an event whose date is still being voted on (a "find a time" poll), a comment on a post or event the admin could not see, and a comment on a newsletter edition. A comment that has been deleted answers this to a `PATCH` and to a second `DELETE`, though `GET` still shows it as a deleted comment. For a claim: this community never imported a post, event or comment with that author `external_id`. For an undo of a conversation: this community never moved one in with that `external_id`, or it has been undone already. | Check the id. A key never reaches outside its community. |
| `409` | `record_deleted` | A create's `external_id` belongs to something that was imported and has been deleted since (or turned into an event in the app, for a post, or into a post, for an event; for a comment, its post or event was deleted). It is never brought back. | Skip it and report it to the admin. Do not delete or retry. |
| `409` | `changed_since_import` | A create's `external_id` belongs to something that someone changed in the app after your last write. It was not overwritten. | Skip it and report it to the admin. Do not delete or retry. |
| `409` | `in_edition` | A post change (a `PATCH`, or a create re-run with its `external_id`) for a post that is in a newsletter edition. Its text can no longer change. Nothing was changed. Events never answer this. | Skip it and report it to the admin. Do not delete or retry. |
| `409` | `cannot_continue` | A batch of direct messages for a conversation that people have used since it was moved (written, reacted, edited, deleted, reported or left), that has been removed, or that was sent with other `participant_ids`. Nothing was moved. | Skip that conversation and report it to the admin. See [Direct messages](direct-messages.md#batches-and-re-runs). |
| `409` | `cannot_undo` | An undo of a moved conversation that people have used since. It stays as it is. | Leave it and tell the admin. See [Direct messages](direct-messages.md#undo-one-delete-conversationsexternal_id). |
| `409` | `conflict` | Two requests made the same thing at the same moment, for example two creates with one `external_id`. | Send the request again, once, after the other one has answered. |
| `422` | `invalid` | A value is not allowed. `details` names each field and why, for example `{"accessibility": ["is not included in the list"]}` or `{"photo": ["could not be fetched as an image"]}`. Nothing was saved. | Fix the value. When it is a choice for the admin (a name that is taken), ask them. |
| `422` | `notify_needs_member_author` | A post, event or comment create with `"notify": true` and an `author` name from the old platform. Only a member can be the author of something that tells people. Nothing was made. | Leave `notify` out (imports should not notify anyone), or, if the admin wants people told, use the member's `author_id`. See [Posts](posts.md#telling-people-notify). The same code answers an event create with `"notify": true` and an `author` name: see [Events](events.md#telling-people-notify), and a comment: see [Comments](comments.md#telling-people-notify). |
| `422` | `notify_needs_future_event` | An event create with `"notify": true` for an event that has already happened. Only an event still to come can be announced. Nothing was made. | Leave `notify` out. Imports of past events should not notify anyone. See [Events](events.md#telling-people-notify). |
| `422` | `reply_to_another_thread` | A comment create with a `parent_id` of a comment that is on another post or event. Nothing was made. | Give the post or event the parent is on. See [Comments](comments.md#replies). |
| `422` | `reply_too_deep` | A comment create that replies to a comment already nested 9 levels deep. Nothing was made. | Attach the reply to the last comment that can take it, and tell the admin. See [Comments](comments.md#replies). |
| `422` | `reply_to_deleted` | A comment create that replies to a deleted comment. Nothing was made. | Skip it and report it to the admin, or attach it to the post or event itself if the admin agrees. See [Comments](comments.md#replies). |
| `422` | `not_accepting_comments` | A comment create on a cancelled event. (A deleted post or event is `404 not_found`.) Nothing was made. | Skip it and report it to the admin. See [Comments](comments.md#replies). |
| `422` | `participants_not_found` | A direct-message conversation names a person (in `participant_ids`) or an author (`author_id` of a message) who is not a member of the community. `details.participant_ids` and `details.author_ids` list the ids you sent that did not match. Nothing was moved. | Tell the admin who is missing. When they have joined, send the conversation again. See [Direct messages](direct-messages.md#everyone-must-be-a-member). |
| `422` | `not_allowed` | Rare: a person or an author in a direct-message conversation was banned from the community while the request ran. (A ban ends the membership, so otherwise the answer is `participants_not_found`. A block between them is never a refusal.) Nothing was moved. | Skip that conversation and report it to the admin. See [Direct messages](direct-messages.md#blocks). |
| `422` | `older_than_imported` | A later batch of direct messages that starts before the newest message already moved. Nothing was moved. | Send the batches in date order. See [Direct messages](direct-messages.md#batches-and-re-runs). |
| `422` | `confirmation_required` | A circle delete without `confirm`, or with a `confirm` that is not the circle's slug. Nothing was deleted. | Only after the admin's clear yes: send the circle's slug as `confirm`. See [Circles](circles.md). |
| `422` | `multiple_owners` | A circle delete for a circle that does not have exactly one owner. Nothing was deleted. | Tell the admin. The circle's owners sort it out in the app first. |
| `429` | (see below) | Too many requests. | Wait the seconds in `Retry-After`, then go on, one request at a time. |

## What `details` names for posts, events, comments and claims

A `422 invalid` answer names the field under `details`. For posts and claims these are:

| `details` key | Why |
|---|---|
| `circle_ids` | Missing, empty, or not a list of circle ids. |
| `content` | Missing or empty, not text, or (for a post sent with `notify`, or a change to a post not created through the API with an `external_id`) more words than the circle allows. It may be empty for a post with pictures sent without `notify`, and in a change to a post with pictures created through the API with an `external_id`. |
| `author_id` | Not a member's id, or the member is not in every circle the post goes into. |
| `author` | Not `{"external_id", "name"}` with an `external_id` of 1 to 200 characters (text or a number) and a name of 1 to 100, or sent together with `author_id`. |
| `created_at` | Not an ISO 8601 time; a date that does not exist (30 February) or an offset beyond `±14:00`; in the future, or before 1990. A time without an offset is read as UTC. |
| `link_url` | Not an absolute `http` or `https` address of at most 2000 characters. |
| `external_id` | Not text or a number, longer than 200 characters, or starting with `fb:` (kept for posts members bring from their own Facebook download). |
| `images` | Not a list of files, or more than 10 pictures in all. |
| `image_urls` | Not a list of addresses, or one that could not be fetched as a picture or is not JPEG, PNG or AVIF. The message names the address. |
| `attachments` | A picture that is not a JPEG, PNG or AVIF, is larger than 10 MB, or cannot be read. |
| `base` | The author has not completed their profile in the app (only for a post or comment sent with `notify`, or a change to a post or comment not created through the API with an `external_id`). |
| `member_id` | A claim without a `member_id`, or with one that is not a member of this community. |

Events use the same keys for `circle_ids`, `author_id`, `author`, `created_at`, `external_id`, `images`, `image_urls`
and `attachments`, and add these:

| `details` key | Why |
|---|---|
| `title` | Missing, empty, or not text. |
| `starts_at` | Missing; not an ISO 8601 time with an offset such as `+01:00` or `Z` (seconds are optional); a date that does not exist (30 February) or an offset beyond `±14:00`; a plain date for an event that is not all-day; before 1990, or more than 50 years ahead. A moved start of an event not created through the API with an `external_id` may not be before 2020. |
| `ends_at` | The same rules as `starts_at`, and not before the start. |
| `time_zone` | Missing, or not an IANA time zone such as `Europe/Copenhagen`. |
| `all_day`, `rsvp_enabled` | Not `true` or `false`. |
| `meeting_url` | Not an absolute `http` or `https` address without spaces, of at most 2048 characters. |
| `max_participants` | Not a whole number from 1 to 1,000,000, or `null`. |
| `description` | Not text, or (for an event sent with `notify`, or a change to an event not created through the API with an `external_id`) more words than the circle allows. Word-limit errors on events are under `description`, never `content`. |

Comments use `author_id`, `author`, `created_at` and `external_id` as posts do, and add these:

| `details` key | Why |
|---|---|
| `post_id`, `event_id` | Neither is given, or both, or the value is not an id. On a create the comment goes on exactly one of them, and on a list the same. |
| `parent_id` | Not an id of a comment, or the id of a comment the admin cannot read. Both answer the same, on purpose. |
| `content` | Missing, empty, or not text. Comments have no word limit. |
| `images`, `image_urls` | Sent at all: comments take no pictures. |

Direct-message conversations name a field of a message by its place in the list, counting from 0, for example
`messages[3].created_at` or `messages[0].image_urls`. Other keys are `external_id`, `participant_ids` (fewer than two
people, or the same person twice), `messages` (not 1 to 100 messages, more than 20 photos in all, or, on a first
import, not one message with text or a photo Zen Social can take) and the keys of a message: `external_id` (also when
two messages share one), `author_id`, `body` (over 4000 characters, or empty without photos), `created_at`, `images` and
`image_urls`. For a message, `image_urls` is refused only for an address that cannot be fetched, is over 5 MB or gives no image at
all: a photo of another type (a GIF, say) is left out and counted in `skipped_files`, not refused. The answer never repeats the text.

For every list (`GET /circles`, `/members`, `/posts`, `/events`, `/comments`), a `page` beyond the last possible page is
`422 invalid` under `details.page`. A page that is merely past the end of the data is an empty list, not an error. And a
null byte (the character `\u0000`) in any value of any request is `422 invalid`, under the field it was found in.

## Too many requests: 429

At most 300 requests a minute per key, and 600 a minute from one network address. Above that the answer has the header
`Retry-After: 60` and a body without a stable code:

```json
{"error":"Too many requests. Please try again later."}
```

Wait that many seconds before the next request.

The same answer comes when a key sends more than 1,000 direct messages in a minute (`POST /conversations`, counted by the
message, accepted or not). Wait, then send the same request again.

## An address that does not exist

A path the API does not have, such as `/rota` in this version, answers `404` with a web page, not JSON. That includes
every `GET` under `/conversations`: direct messages cannot be read. Check the
path against these pages.

## Server errors: 500

Something went wrong on Zen Social's side. Stop, tell the admin what you were doing, and try again later. If a create
fails like this, sending it again with the same `external_id` is safe.
