# Start here: the Zen Social Community API

You are an AI assistant working for an owner or admin of one community on Zen Social. This API lets you set up that
community for them and move its posts, events, comments and direct messages over from another platform. Read this
whole page before you call anything, then the pages it links to. The admin is usually not a developer: explain what you
are about to do in plain words, and ask before anything that cannot be undone.

## What you need

The admin's message gives you two things:

- the **API address**, for example `https://zensocial.app/api/community/v1`;
- the **key**, a secret that starts with `zsc_`.

If you do not have both, ask the admin to make a key in Zen Social and paste you the message it shows. The
button is **New key**, on the tab **Import & AI access** of their community's **Manage** page.

In every example, `$API` is the API address and `$KEY` is the key:

```sh
API="https://zensocial.app/api/community/v1"
KEY="zsc_…"
curl "$API/organization" -H "Authorization: Bearer $KEY"
```

Every request sends the key in the `Authorization: Bearer …` header (see [Keys and access](keys.md)). Send request
bodies as flat JSON with `Content-Type: application/json`: the fields go at the top level, with no wrapping key
(`{"name": "Book Club"}`, not `{"circle": {"name": "Book Club"}}`). Where a photo can be uploaded, a multipart form
works too.

## What this version can do

- Read and change the community itself: its name, description, colours and photo. See [The organization](organization.md).
- List, create, change and delete the community's circles. See [Circles](circles.md).
- Read the member list, to match people from the old platform. See [Members](members.md).
- Read, import, change and delete posts, with their original dates, authors, links and pictures. See [Posts](posts.md).
- Read, import, change and delete events, with their original dates, organisers and pictures. See
  [Events](events.md).
- Read, import, change and delete the comments on posts and events, replies included, with their original dates and
  authors. See [Comments](comments.md).
- Move members' old private conversations in, with their original dates and authors, and undo the move of one. This
  only works with a key made with the permission **May import direct messages**, and it is write-only. See
  [Direct messages](direct-messages.md).
- Hand imported posts, events and comments to the people who wrote or organised them, once they have joined, and undo
  that. See [Authors and claims](authors.md).

Not yet in this version: the rota arrives in a later version of this API. Who said they are going to an event (RSVPs)
is not part of the API at all, and neither are pictures on comments.

## What it cannot do

- Invite, add, approve or remove members. People join in the app, invited by a person.
- Send messages or emails to members. Moving old conversations in is not sending: nobody is told.
- Read, list, search or export direct messages. There is no way to see a conversation through the API, not even one you
  moved in.
- Take content out of Zen Social (export).
- Create a new community. The community already exists; the key belongs to it.
- Anything the admin could not do in the app. The key acts as the admin who made it, never as more. There are two
  exceptions. One is the posts, events and comments you import: you can still correct, delete and claim them after their
  author has changed (see [Who may change which post](posts.md#who-may-change-which-post),
  [Who may change which event](events.md#who-may-change-which-event) and
  [Who may change which comment](comments.md#who-may-change-which-comment)). The other is moving direct messages in,
  which an admin cannot do in the app and which needs its own key permission (see
  [Direct messages](direct-messages.md)).

## How a move goes

1. The admin makes a key and pastes its message to you. If you are reading this, that is done.
2. **Look before you change anything.** Read `GET /organization`, `GET /circles` and `GET /members`, tell the admin
   what you found, and agree with them what you will do.
3. **Set up the community:** name, short description, colours, photo (`PATCH /organization`).
4. **Create the circles** (`POST /circles`), each with an `external_id`. The admin becomes the owner of every circle
   you create. A circle that already existed in Zen Social is different: the admin must join it in the app first,
   and be its owner or admin there before you can change it. You cannot join it for them.
5. **Import the posts** (`POST /posts`), each with an `external_id`, and with its author: a member's id when the
   person has already joined, otherwise their name and your id for them. Start with a small sample. See
   [Posts](posts.md).
6. **Import the events** (`POST /events`), the same way: each with an `external_id` and its organiser, and with its
   `time_zone`. Start with a small sample here too. See [Events](events.md).
7. **Import the comments** (`POST /comments`), after the posts and events they are on, and each reply after the comment
   it replies to. Each has an `external_id` and its author. Start with a small sample. See
   [Comments](comments.md).
8. **The admin invites the members**, and people sign up in the app. Only the admin can do this; you cannot.
9. **Match people.** When members have joined, read `GET /members` and match them to the people of the old platform.
   With the admin's yes for each match, hand their imported posts, events and comments to them with a claim. See
   [Authors and claims](authors.md).
10. **Move the direct messages** (`POST /conversations`), after the comments and once the people in the conversations
    have joined and been matched. This needs a key made with the permission **May import direct messages**, and **the
    community must have told its members first**: ask the admin to confirm it, and to confirm your match of every
    person to a member. Start with a small sample. See
    [Direct messages](direct-messages.md).
11. **The rota** comes last, once its people are members. Not in this version.
12. **Finish:** tell the admin to revoke the key.

## Silent by default

Nothing you create or change through this API notifies members: no bell, no push message, no email, no line in
anyone's daily summary. That holds for imports, changes, deletes and claims. A conversation you move in is silent too,
and shows as read. A comment you add to an old post or event does not move that post or event up in the circle's
activity.

The one exception is a post, event or comment create that sends `"notify": true`: it tells people about it the way the
app would when a member posts, makes an event or comments. It needs a member as the author or organiser, an event also
needs to be still to come, and the rules below say when it is allowed. Changes and deletes are always silent.

An imported event that has already happened sends no reminder, ever. An imported event still to come sends its normal
reminder: 24 hours before a timed event, and at 18:00 the evening before an all-day event, to the people who answered
going or maybe (not the organiser). An import has no such people, since RSVPs are not part of the API, and an event
imported less than a day before it starts gets no reminder.

One notice goes to the admin, not to members: when a circle is deleted, the admin whose key it is gets the app's
usual "circle deleted" notice once the deletion has finished.

## Re-runs and `external_id`

Send an `external_id` with every create: your own id for the thing on the old platform, as a string (for example
`"fb-group-1234"` or `"old-site-circle-7"`). Zen Social remembers it for this community, so the same create can be
sent again without making a copy. A crashed or interrupted import can simply start again from the beginning. An
`external_id` may not start with `fb:`: those are kept for the posts members bring from their own Facebook download.

The answer to a create tells you what happened:

| Answer | Meaning | What to do |
|---|---|---|
| `201 Created` | New. Nothing had this `external_id` yet. | Carry on. |
| `200 OK` | Already imported. It was updated with what you sent and keeps its id. | Carry on. |
| `409 {"error":"record_deleted"}` | It was imported once and someone has deleted it since (for a comment, its post or event counts too). It is never brought back. | Skip it, and report it to the admin. |
| `409 {"error":"changed_since_import"}` | Someone edited it in the app after your last write. It was not overwritten. | Skip it, and report it to the admin. |
| `409 {"error":"in_edition"}` | 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. |
| `409 {"error":"conflict"}` | Two creates with the same `external_id` arrived at the same moment. | Send that one create again, once, after the other has answered. |
| `404 {"error":"not_found"}` | A comment the admin can no longer see in the app, a post or event no longer in any of this community's circles, or an event that no longer has a date. Nothing was changed. | Skip it, and report it to the admin. |
| `403 {"error":"forbidden"}` | The admin may no longer change it: for a circle, they are no longer its owner or admin; for a post or event, they can no longer post in one of its circles, or can no longer see it in the app; for a comment, they can no longer comment in one of the circles of its post or event. | Skip it, and report it to the admin. |

The "changed in the app" mark stays: a later `PATCH` through the API still changes the record, but the next re-run
answers `409 changed_since_import` for it again. That is on purpose. A re-run must never undo what people did in the app.

Without an `external_id` every create makes a new record, so running the same import twice makes two of everything.
A post, event or comment without one can also never be handed to its author later, and a comment without one often
cannot be changed through the API either (see [Authors and claims](authors.md) and
[Comments](comments.md#who-may-change-which-comment)).

A re-run of a post changes only its text and link. A re-run of an event changes only its title, times, place, link,
description and limits. Circles, author, creation date and pictures stay as first imported, and the re-run does not
check them again. A re-run of a comment changes only its text. See [Posts](posts.md#re-runs),
[Events](events.md#re-runs) and [Comments](comments.md#re-runs). An event someone changed or cancelled in the app, or a
comment someone edited, answers `409 changed_since_import`.

## Rules for assistants

Follow these every time, whatever else you are asked:

1. **Confirm with the admin before any DELETE.** Say exactly what will be deleted, including everything inside it, and
   wait for a clear yes. Confirm before every claim too, and before undoing one: show the admin the name match you
   intend to make (the name on the old platform, how many posts, events and comments, and the member) and wait for their
   yes. Never guess between two members with the same or a similar name. The same holds for undoing the move of a
   conversation (rule 7).
2. **Show the admin the change before a PATCH of a post, event or comment that may be someone's own words:** one whose
   author is now a member (its `author` has an `id`), or whose `edited_at` is not `null` (someone edited it in the
   app). Show it as it is and as it will be, and wait for their yes.
3. **Never send `"notify": true` unless the admin asked for people to be told.** Moving content in must not ping
   anyone.
4. **Import a small sample first** (two or three items), and ask the admin to check it in the app before you run the
   rest.
5. **Always send `external_id`** on a create (for a post, event or comment by someone who has not joined, theirs in
   `author` too), and treat a `409` answer as "skip and report", never as a reason to delete something and try again.
   (The one exception is `409 conflict`: send that create again, once.)
6. **Never guess a time zone for an event.** If the old platform's export does not say, ask the admin where the event
   was held. For an all-day event, `ends_at` is the last day itself: a calendar's end date is usually the day after, so
   subtract one day (see [Events](events.md#the-times-starts_at-ends_at-and-time_zone)).
7. **Direct messages are different from everything else here.** Use them only with a key that has the permission
   **May import direct messages**, and never claim, to the admin or anyone, that you can read, list or export a
   conversation: you cannot, and the API never shows you one. Before the first real import, ask the admin to confirm
   that the members have been told that their old conversations are moving; without that yes, do not import, not even
   a sample of real people's conversations. Also show the admin the full mapping from each person in the old
   conversations to the Zen Social member you will use, and wait for an explicit yes. Never guess between similar names:
   a person you cannot match with certainty means that conversation is not imported. A wrong match shows a private
   conversation to the wrong person, and an undo cannot take back that it was seen. Move conversations only as they
   are in the old platform's export, between the people who were in them: never compose, reword, add or leave out a
   message, and never make a conversation that did not exist. Then start with a small sample the admin has chosen. If
   a conversation was moved wrongly, undo that one conversation (`DELETE /conversations/:external_id`) after the
   admin's yes, instead of anything broader. A `409 cannot_undo` or `409 cannot_continue` means people have used the conversation: leave it
   and tell the admin. The rule "confirm before any DELETE" holds for this undo too.
8. **Tell the admin to revoke the key when the work is done:** **Manage**, tab **Import & AI access**, **Revoke**.

Also: when an answer surprises you (a `401`, a run of `403`s, a `500`), stop and tell the admin instead of retrying in
a loop. Never show the key in anything you write for other people.

## Requests and answers

- **Lists** answer `{"data": […], "next_page": 2}`, 100 items per page. Ask for the next one with `?page=2`;
  `next_page` is `null` on the last page.
- **Ids:** circles, posts, events, comments and the community have numeric ids. Members have text ids such as
  `"EveABqLyto9L"`; they are not database numbers, and stay the same. An author's `external_id` in an address is
  percent-encoded (see [Authors and claims](authors.md#the-author-id-in-the-address)).
- **Times** are ISO 8601, with their offset from UTC. An event's `starts_at` and `ends_at` are shown in the event's own
  `time_zone`, and you send them with an offset too (see [Events](events.md#the-times-starts_at-ends_at-and-time_zone)).
- **Errors** answer `{"error": "<code>"}`, sometimes with `"details"`. Every code is in [Errors](errors.md).
- **Pace:** at most 300 requests a minute per key. Go one request at a time; on `429`, wait the seconds in the
  `Retry-After` header. See [Keys and access](keys.md).

## Example: set up a community moved from a Facebook group

The admin wants the group's three sub-groups as circles. You have read `GET /organization`, `GET /circles` and
`GET /members`, told the admin what is there, and agreed the plan with them.

Set the community's description and colour:

```sh
curl -X PATCH "$API/organization" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"short_description": "Neighbours on Elm Street", "brand_color": "#1D4ED8"}'
```

Create one circle as a sample, with the sub-group's id from Facebook as its `external_id`:

```sh
curl "$API/circles" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"external_id": "fb-group-1234", "name": "Garden Swap", "short_description": "Seeds, plants and tools", "accessibility": "guarded"}'
```

It answers `201 Created` with the new circle. Ask the admin to look at it in the app. When they are happy, create the
other two the same way. If the run stops halfway, send all three again: the circles already made answer `200 OK`
instead of being made twice.

Then remind the admin to invite the members in the app. Moving the group's posts and events in comes next: see the
example in [Authors and claims](authors.md#example-three-posts-and-an-event-from-two-facebook-authors). Its comments
follow the posts and events they are on: see [Comments](comments.md). When everything is done, remind the admin to
revoke the key.

## All pages

- [Keys and access](keys.md): sending the key, expiry, what the key may do, limits.
- [The organization](organization.md): the community itself.
- [Circles](circles.md): the community's circles, including deleting one.
- [Members](members.md): the member list and one circle's members.
- [Posts](posts.md): reading, importing, changing and deleting posts.
- [Events](events.md): reading, importing, changing and deleting events.
- [Comments](comments.md): reading, importing, changing and deleting the comments on posts and events.
- [Direct messages](direct-messages.md): moving members' old private conversations in, and undoing one. Write-only.
- [Authors and claims](authors.md): handing imported posts, events and comments to the members who wrote or organised
  them, and undoing it.
- [Errors](errors.md): every error code and what to do about it.
