# retoor <retoor@molodetz.nl>

GROUP = {
        "slug": "conventions",
        "title": "Conventions & Errors",
        "intro": """
# Conventions & Errors

Shared rules that apply to every endpoint in this reference.

## Base URL

Every example uses your current host:

```
{{ base }}
```

## Authentication

Each endpoint is tagged **public**, **user**, or **admin**. Authenticate user and admin
endpoints with any of the four methods in [Authentication](/docs/authentication.html): the
`session` cookie, an `X-API-KEY` header, a `Bearer` token, or HTTP Basic credentials. The
interactive panels on this site pre-fill your own API key, so you can run user-level calls
immediately.

## Request bodies

POST endpoints accept `application/x-www-form-urlencoded` form fields (the same fields the
website submits). File uploads use `multipart/form-data`. A small number of endpoints accept
a JSON body; those are noted explicitly.

## HTML or JSON (content negotiation)

**Every** endpoint that renders a page or returns a redirect can also answer in JSON - the
website keeps working exactly as before, and automation gets structured data from the same
URLs. A request is served JSON when it sends either of:

- `Accept: application/json`
- `Content-Type: application/json`

A normal browser navigation (`Accept: text/html`) always receives HTML, so nothing existing
changes. Responses are defined by Pydantic models, so each page returns the same data the
template renders.

**Page reads** (GET) return the page payload as a JSON object (lists include a `next_cursor`
for pagination; detail pages embed author, comments, reactions, poll, and attachments).

**Actions** (the form POSTs: create / edit / delete / follow / send / mark-read …) return a
uniform envelope instead of a `302` redirect:

```json
{ "ok": true, "redirect": "/posts/abc-my-post", "data": { "uid": "…", "slug": "…", "url": "…" } }
```

`data` carries the created/affected resource where applicable, or `null`. Cookies (e.g. the
session set on login/signup) are still set on JSON responses.

**Errors** are JSON too when JSON is requested:

```json
{ "error": { "status": 404, "message": "Not found" } }
```

Validation failures return `422` with `{ "error": "validation", "fields": { "field": ["msg"] } }`.
Unauthenticated JSON requests to a protected endpoint return `401` (browsers are redirected to
the login page instead); non-admins calling an admin endpoint get `403`.

## Trying it here

Every endpoint below has a live panel. Pick the response format (**JSON** by default, or
**HTML** where the endpoint negotiates) and the panel sets the matching `Accept` header on the
request and the generated cURL/JavaScript/Python snippets. The **Expected** tab always shows the
modeled response shape; the **Live response** tab shows the real result after you press
**Send request**.

## AJAX responses (legacy shape)

The [Votes, Reactions, Bookmarks & Polls](/docs/social-actions.html) endpoints predate the
envelope and keep their original flat JSON shapes (e.g. `{ "saved": true }`). They return JSON
when the request carries an `X-Requested-With: fetch` header (a subset of the rule above);
without it they `302` redirect, mirroring the browser flow. The interactive panels send the
header for you.

## Pagination

Most list endpoints page with an opaque cursor. Pass the `before` query parameter set to the
`created_at` (or `synced_at`) value of the last item you received to fetch the next page; the
JSON payload returns a `next_cursor` to use as the next `before`. This covers the feed, the
post/project/gist/news lists, notifications, and saved bookmarks.

The follower and following lists are the exception: `GET /profile/{username}/followers` and
`GET /profile/{username}/following` use classic page-based pagination via the `page` query
parameter (25 per page), not a cursor.

## Identifiers

Posts, projects, gists, and news articles accept either their slug or their bare UUID in the
path. Slugs embed the first eight characters of the UUID.

## Dates

All dates rendered to users are `DD/MM/YYYY`. Timestamps in stored records are ISO-8601 UTC.

## Status codes

| Code | Meaning |
|------|---------|
| `200` | Success (JSON or HTML) |
| `201` | Resource created (uploads) |
| `302` | Redirect (browser-style success for form posts) |
| `400` | Invalid request body or parameters |
| `401` | Credentials supplied but invalid |
| `403` | Authenticated but not allowed |
| `404` | Resource not found |
| `413` | Upload exceeds the configured size limit (see [Uploads](/docs/uploads.html)) |
| `415` | Upload file type not allowed (see [Uploads](/docs/uploads.html)) |
| `422` | Form/body validation failed (JSON clients) |
| `429` | Rate limit exceeded (see Rate limiting) |
| `503` | Maintenance mode |

## Rate limiting

Mutating requests (`POST`/`PUT`/`DELETE`/`PATCH`) are rate limited per client IP over a
rolling window; reads are not limited. The limit and window are configurable by an
administrator (defaults: 60 requests per 60 seconds). When you exceed the limit you receive a
`429` whose `Retry-After` header gives the number of seconds to wait before retrying. The
OpenAI gateway (`/openai/...`) is exempt.

## Troubleshooting

**I get HTML back instead of JSON.** Send `Accept: application/json` (or
`Content-Type: application/json` on a body). A request is only served JSON when it asks for it
and does not also accept `text/html`; a normal browser navigation always gets HTML.
`X-Requested-With: fetch` is **not** a general JSON switch - it only applies to the legacy
engagement actions (votes, reactions, bookmarks, polls).

**An action returns `302` instead of the JSON envelope.** Same cause: the request did not ask
for JSON. Add the `Accept: application/json` header and the action returns
`{ "ok": true, "redirect": "...", "data": {...} }` instead of redirecting.

**A protected endpoint redirects me to the login page.** Browser-style (HTML) requests to a
`user`/`admin` endpoint without valid credentials are redirected to login; the same request
with `Accept: application/json` returns `401` instead. Non-admins calling an `admin` endpoint
get `403` (JSON) or a redirect to the feed (HTML).

**An upload is rejected with `413` or `415`.** `413` means the file exceeds the configured
size limit; `415` means the file type is not in the allowed list. Both limits are set by an
administrator.
""",
        "endpoints": [],
}
