commit 05c83b36bfe40eba783b690f9353c89cd69cc305
parent 24a699fd7e3c497b882b21fb0091238e10c802b1
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 26 Aug 2026 04:27:22 -0700
Document the TUI API contract
Diffstat:
| M | README.md | | | 5 | +++-- |
| A | doc/api.md | | | 259 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 262 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
@@ -68,8 +68,9 @@ Environment overrides are `TEXTLOG_URL`, `TEXTLOG_TOKEN`,
`TEXTLOG_THEME`, and `NO_COLOR`. Session and UI settings are stored in the
platform user configuration directory.
-See [doc/usage.md](doc/usage.md) for the complete controls, configuration, and
-API coverage.
+See [doc/usage.md](doc/usage.md) for the complete controls and configuration.
+See [doc/api.md](doc/api.md) for the HTTP API contract used by the terminal
+client.
## Development
diff --git a/doc/api.md b/doc/api.md
@@ -0,0 +1,259 @@
+# Textlog API used by the terminal client
+
+This document describes the HTTP API contract used by this version of
+`gotextlog`. It is an integration guide for compatible Textlog instances,
+not a description of the complete server API. The upstream instance publishes
+its full, machine-readable contract at
+[`/api/openapi.json`](https://textlog.cc/api/openapi.json).
+
+## Transport and instance selection
+
+The client treats the configured URL as an instance origin and sends API
+requests below that origin:
+
+```text
+<instance origin>/api/v1/<endpoint>
+```
+
+For example, `textlog --url https://notes.example/path` uses
+`https://notes.example/api/v1`; any path, query, or fragment in the configured
+URL is discarded. The configuration validator accepts HTTP and HTTPS URLs;
+use HTTPS for instances reached over a network.
+
+Ordinary requests use:
+
+```http
+Accept: application/json
+Authorization: Bearer <token> # when a token is configured
+Content-Type: application/json # when the request has a JSON body
+```
+
+The client sends its bearer token on public as well as authenticated endpoints.
+The live endpoint instead requests `text/event-stream`. Request cancellation is
+propagated through the Go context.
+
+## Authentication
+
+Textlog sign-in is an email-code exchange. Account creation is not available
+through the API, so a user must already have an account on the selected
+instance.
+
+1. `POST /auth/request` asks the instance to email a code.
+2. `POST /auth/verify` exchanges the email and six-digit code for a session.
+3. The client saves `data.token` and sends it as a bearer token.
+4. At startup, `GET /me` restores the signed-in account.
+5. `DELETE /auth/session` revokes the current session during sign-out.
+
+Changing the configured instance also changes which server receives the saved
+bearer token. Credentials and accounts are local to an instance; this client
+does not implement federation or cross-instance identity.
+
+In the endpoint tables, **optional** means the endpoint does not require a
+session but receives the bearer token when one is available. **Required** means
+the TUI invokes it for a signed-in operation. **None** identifies sign-in
+endpoints normally called before a token exists.
+
+## Endpoint inventory
+
+All paths in this section are relative to `/api/v1`.
+
+### Feeds, search, and live data
+
+| Method and path | Auth | Request | TUI use and response |
+| --- | --- | --- | --- |
+| `GET /feeds/latest` | Optional | `limit`, optional `cursor` | Latest feed. Reads a paginated post collection. |
+| `GET /feeds/hot` | Optional | `limit`, optional `cursor` | Hot feed. Reads a paginated post collection. |
+| `GET /activities/for-you` | Required | `limit`, optional `cursor` | Personalized activity feed. Reads an activity collection. |
+| `GET /activities/to-me` | Required | `limit`, optional `cursor` | Replies, mentions, and follows directed to the user. Reads an activity collection. |
+| `GET /search` | Optional | Required `q`; `limit`; optional `cursor` | Searches public posts and reads a paginated post collection. |
+| `GET /firehose` | Optional | No parameters | Live feed over server-sent events (SSE). |
+
+Feed and search screens request 20 records at a time. Their Next and Previous
+commands retain the opaque cursors returned by the server. The client decodes
+`has_unread` from activity responses but does not currently display it or mark
+feed/activity records as read.
+
+The firehose returns server-sent events in this form:
+
+```text
+event: post
+data: {"id":123,"body":"hello",...}
+
+```
+
+The `data` value can be either a post object or `{"data": <post>}`. The parser
+supports multiline SSE data, comments, CRLF, and a final event without a blank
+line. It ignores event types other than `post` and skips malformed post events.
+A post event must include `id`, `body`, `created_at`, and
+`author.handle`. When an active stream ends or fails, the TUI reconnects after
+a short delay; pausing or leaving the live screen cancels it. The protocol does
+not replay posts missed between connections.
+
+### Posts and threads
+
+| Method and path | Auth | Request | TUI use and response |
+| --- | --- | --- | --- |
+| `GET /posts/{id}` | Optional | Positive integer `id` | Opens a post or obtains thread ancestry. Reads a post envelope. |
+| `GET /posts/{id}/replies` | Optional | `depth`, `limit`, optional `cursor` | Builds the recursive reply tree. Reads a paginated reply collection. |
+| `POST /posts` | Required | `{"body": string}` or `{"body": string, "parent_id": integer}` | Creates a top-level note or reply. Reads the created post envelope. |
+| `PATCH /posts/{id}` | Required | `{"body": string}` | Edits a post owned by the signed-in user. Reads the updated post envelope. |
+| `DELETE /posts/{id}` | Required | No body | Deletes a post owned by the signed-in user. The client accepts `data.deleted`. |
+| `POST /posts/{id}/report` | Required | `{"reason":"harassment"|"spam"|"impersonation"|"other"}` | Reports another user's post. The client accepts `data.reported`. |
+
+To render a thread, the TUI first gets the selected post. If it is a reply, it
+uses `top_id` when available, otherwise follows `parent_id`, fetching
+ancestors until it reaches the root. It then requests every reply page for the
+root with `depth=5` and `limit=100`. Repeated pagination cursors are treated
+as an error. Replies deeper than five levels are not fetched by this client.
+
+The compose editor currently limits submitted bodies to 280 runes and ten
+lines, even though the Textlog service permits notes up to 500 characters.
+The server remains responsible for authoritative validation and authorization.
+
+### Profiles and relationships
+
+| Method and path | Auth | Request | TUI use and response |
+| --- | --- | --- | --- |
+| `GET /users/{handle}` | Optional | URL-escaped `handle` | Loads public profile details in a data envelope. |
+| `GET /users/{handle}/notes` | Optional | `limit`, optional `cursor` | Loads the profile's top-level notes. |
+| `GET /users/{handle}/replies` | Optional | `limit`, optional `cursor` | Loads the profile's replies. |
+| `GET /users/{handle}/following/users` | Optional | `limit`, optional `cursor` | Lists accounts followed by the profile. |
+| `GET /users/{handle}/following/tags` | Optional | `limit`, optional `cursor` | Lists hashtags followed by the profile. |
+| `GET /users/{handle}/followers` | Optional | `limit`, optional `cursor` | Lists accounts following the profile. |
+| `GET /users/{handle}/blocks` | Required | `limit`, optional `cursor` | Lists blocks when viewing the signed-in user's profile. |
+| `POST /users/{handle}/follow` | Required | No body | Follows an account; accepts `data.following`. |
+| `DELETE /users/{handle}/follow` | Required | No body | Unfollows an account; accepts `data.following`. |
+| `POST /users/{handle}/block` | Required | No body | Blocks an account; accepts `data.blocked`. |
+| `DELETE /users/{handle}/block` | Required | No body | Unblocks an account; accepts `data.blocked`. |
+
+Opening a profile is a composite operation: the TUI requests the profile,
+notes, replies, followed users, followed tags, and followers sequentially. It
+also requests blocks when the profile belongs to the signed-in user. Each list
+is requested once with `limit=100`; profile tabs do not currently paginate
+past that first page. A failure in a required profile request fails the whole
+profile load. Failure to load the optional blocks tab is ignored.
+
+The API client still contains support for the deprecated
+`GET /users/{handle}/posts` alias, but the TUI uses `/notes` and never calls
+the alias.
+
+### Hashtags
+
+| Method and path | Auth | Request | TUI use and response |
+| --- | --- | --- | --- |
+| `GET /tags/{tag}` | Optional | URL-escaped `tag`, without `#` | Loads hashtag details in a data envelope. |
+| `GET /tags/{tag}/posts` | Optional | `limit`, optional `cursor` | Loads posts carrying the hashtag. |
+| `GET /tags/{tag}/followers` | Optional | `limit`, optional `cursor` | Lists accounts following the hashtag. |
+
+Opening a hashtag requests all three endpoints sequentially with `limit=100`
+for the two collections. The TUI displays the first page and does not currently
+provide hashtag follow/block actions.
+
+### Session and account
+
+| Method and path | Auth | Request | TUI use and response |
+| --- | --- | --- | --- |
+| `POST /auth/request` | None | `{"email": string}` | Emails a sign-in code. The client accepts `data.sent`. |
+| `POST /auth/verify` | None | `{"email": string, "code": string}` | Returns a session envelope containing a token, expiry, and current user. |
+| `DELETE /auth/session` | Required | No body | Revokes the active token; the client accepts `data.revoked`. |
+| `GET /me` | Required | No parameters | Loads the current account in a data envelope. |
+| `PATCH /me` | Required | `{"bio": string}` | Updates the current account bio and returns the account envelope. |
+
+The TUI validates that sign-in codes contain six digits before sending them.
+It stores the verified session token in its platform configuration directory
+and clears it after successful revocation. The bio editor limits input to 1,000
+runes; the instance performs final validation.
+
+## Response shapes consumed by the client
+
+Most single-resource and mutation responses use an envelope:
+
+```json
+{"data": {}}
+```
+
+Collections use cursor pagination:
+
+```json
+{
+ "data": [],
+ "pagination": {"next_cursor": null}
+}
+```
+
+`next_cursor` is either an opaque string or `null`. The client sends a
+nonempty value back unchanged as the next request's `cursor`.
+
+### Post and reply
+
+The client consumes these post fields:
+
+| Field | Type | Use |
+| --- | --- | --- |
+| `id` | integer | Post identity, navigation, and mutation target. |
+| `top_id` | integer or null | Root lookup for a reply thread. |
+| `parent_id` | integer or null | Reply relationship and parent navigation. |
+| `body` | string | Rendered note text. |
+| `created_at` | RFC 3339 timestamp | Ordering/display and live-event validation. |
+| `reply_count` | integer | Reply count metadata. |
+| `tags` | array of strings | Hashtag rendering and navigation. |
+| `mentions` | array of strings | Mention rendering and navigation. |
+| `url` | URL string | Open-in-browser action. |
+| `api_url` | URL string | Decoded for API compatibility; not used for routing. |
+| `author` | user reference | Author rendering, profile navigation, follow/block actions. |
+| `parent` | post or null | Quoted parent context in feeds. |
+
+A reply contains the post fields plus integer `depth`, used to indent and fold
+the thread tree.
+
+### User, profile, and hashtag
+
+A user reference contains `handle`, `url`, and `api_url`. The full profile
+adds `bio`, `created_at`, `post_count`, `replies_count`,
+`follower_count`, `following_user_count`, `following_tag_count`,
+`following_count`, and optional `blocked_user_count` and
+`blocked_tag_count`.
+
+A hashtag reference contains `tag`, `post_count`, `follower_count`, `url`,
+and `api_url`.
+
+The current-user response contains `handle`, `email`, `bio`,
+`email_verified`, and `can_post`. A verified session adds `token` and
+`expires_at` around that user object.
+
+### Activity
+
+An activity collection adds `has_unread` and returns objects with `id`,
+`type`, `created_at`, `unread`, and a type-dependent `payload`. The TUI
+understands these activity types:
+
+- `post`, `reply`, and `mention`: payload decodes as a post.
+- `user_follow`, `tag_follow`, and `signup`: payload supplies an `actor`
+ user reference and, when applicable, a `target.handle` or `target.tag`.
+
+Unknown or malformed activity payloads are skipped.
+
+## Errors and compatibility requirements
+
+Textlog errors are expected to have this form:
+
+```json
+{"error":{"code":"rate_limited","message":"Slow down"}}
+```
+
+For a non-2xx response, the client records the HTTP status plus `error.code`
+and `error.message`. If the body is absent or malformed, it uses
+`http_error` and `HTTP <status>` (`stream_error` for the firehose). A
+numeric `Retry-After` header is displayed to the user in seconds. Transport
+failures use `network_error`; cancellation remains a context cancellation.
+
+A compatible instance must implement the routes and JSON fields used by the
+screens it wants to support. JSON may include additional fields: Go's decoder
+ignores them. Successful JSON responses must have valid field types and RFC
+3339 timestamps where timestamps are consumed.
+
+The TUI does not currently call several upstream API families, including
+drafts, polls and voting, tag follow/block mutations, activity read markers,
+latest-feed read markers, explore, post unpublish, feed keys, archives, embeds,
+or RSS/Atom feeds. Consult the selected instance's OpenAPI document for those
+server capabilities.