# DYSTIL for AI agents

> API version 1.0.0. Use DYSTIL on a user's behalf over MCP or REST with their personal API key.

DYSTIL is a reading and writing desk. You act as the user whose API key you hold: everything you do shows up in their dashboard exactly as if they had done it, and every dashboard button has a tool that does the same thing.

The vocabulary:
- Bookmark (a "spark"): an item in the user's Spark feed, synced from X, Nostr or Instagram, a pasted link, or a forwarded email. Each carries its `source` platform, author, tags, text, and (in get_bookmark) the DYSTIL agents' takes, a fact-check, quoted posts and who else saved it. The inbox is bookmarks not yet dystilled, filed or archived.
- Dystillation: a post the user writes from one or more sources. Drafts are private; published ones appear on the user's public curator page. Each has a human body and a separate AI draft (aiBody).
- Project: a workspace with a description, goals, sources (bookmarks and dystillations), a member discussion, private notes, and collaborative dystillations.
- Project dystillation: a post the project's members write together (get_project lists them). Use the same dystillation tools; get_dystillation's `project` shows the co-authors and who has the editor open. Its sidebar discussion is the project's thread (list_project_messages, post_project_message). Don't rewrite the body while someone holds the edit lock: you get 409 being_edited.

The Spark feed, as the user sees it:
- list_bookmarks is the feed with its filters (source, status, tag, interest, search); get_bookmark is the card in full. sync_bookmarks is the Sync button.
- The card's "What next?" menu: Chat is chat with bookmarkId; New dystillation is dystil_bookmark kind "standard"; Repost is dystil_bookmark kind "repost".
- Archive with update_bookmark; add_bookmark is "Add from URL".

Watching the feed and filing new bookmarks into projects:
1. list_projects once to learn what each project is about (name, description, goals). Refresh it now and then.
2. list_new_bookmarks with since=<now> the first time; store the returned cursor and pass it back on every later call. You only ever receive each bookmark once.
3. For each new bookmark, get_bookmark if you need more than the summary, then decide which project(s) it serves. File it with add_project_sources (this marks it processed, so it leaves the inbox). Leave it in the inbox when nothing fits, and archive it only if the user asked you to triage.
4. Optionally note why with add_project_note (private to the user).

Ground rules:
- Publishing (publish_dystillation), making a project public, posting to a project discussion, and adding or inviting members are visible to other people. Do them only when the user asked.
- Tools marked destructive (deleting, locking, removing) can't be undone or reach other people; use them only on an explicit request. Prefer archiving to deleting.
- chat, ask_dystillation_agent, dystil_bookmark (standard), create_dystillation with a url, generate_article, generate_hero_image, run_agent_research, add_enrichment, retry_dystillation_step (research) and create_project_dystillation spend the user's AI credit.
- Background jobs (content fetch, research, AI drafts, images) finish after the call returns. Poll the get_* tool for their status fields rather than assuming they are done.
- Limit: 120 requests per minute per key.

## Connecting

1. The user creates a key at https://u760jbxm88k6vlvcwrqqcqpz.altmachina.io/dashboard/settings (API keys card), choosing full access or read only. Keys start with `dystil_` and are shown once.
2. Send it on every request as `Authorization: Bearer <key>` (`X-API-Key: <key>` also works).

### MCP

Streamable HTTP endpoint: `https://u760jbxm88k6vlvcwrqqcqpz.altmachina.io/api/mcp` (stateless, JSON responses). Every operation below is a tool of the same name.

Claude Code:

```
claude mcp add --transport http dystil https://u760jbxm88k6vlvcwrqqcqpz.altmachina.io/api/mcp --header "Authorization: Bearer <key>"
```

Clients configured with JSON (Claude Desktop via mcp-remote, Cursor, and others):

```json
{
  "mcpServers": {
    "dystil": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://u760jbxm88k6vlvcwrqqcqpz.altmachina.io/api/mcp", "--header", "Authorization: Bearer ${DYSTIL_API_KEY}"],
      "env": { "DYSTIL_API_KEY": "<key>" }
    }
  }
}
```

### REST

Base URL `https://u760jbxm88k6vlvcwrqqcqpz.altmachina.io/api/v1`. GET and DELETE take parameters in the query string; POST and PATCH take a JSON body. Path parameters are shown as `:name`. `GET https://u760jbxm88k6vlvcwrqqcqpz.altmachina.io/api/v1` lists every endpoint with its JSON Schema.

```
curl -H "Authorization: Bearer $DYSTIL_API_KEY" "https://u760jbxm88k6vlvcwrqqcqpz.altmachina.io/api/v1/bookmarks/new?since=2026-01-01T00:00:00Z"
```

Errors come back as `{ "error": { "code", "message" } }` with a matching HTTP status: 400 invalid input, 401 bad key, 403 not allowed (read-only key or project role), 404 not found or not yours, 429 rate limited (see `Retry-After`).

## Reference

### get_me

`GET /api/v1/me` · read-only keys allowed

The DYSTIL account this API key acts as: name, public curator page, connected bookmark sources, and the key's access level. Call this first to orient yourself.

### list_tags

`GET /api/v1/tags` · read-only keys allowed

The user's AI-assigned topic tags with how many items carry each. Use a tag name as the `tag` filter on list_bookmarks.

### list_sections

`GET /api/v1/sections` · read-only keys allowed

The sections (categories) of the user's public newspaper, in display order.

### get_sync_status

`GET /api/v1/sync` · read-only keys allowed

For each connected account (a user may link several per source, e.g. a personal and a business X): its source (x, nostr, instagram), handle, whether a sync is running, when it last finished, how many items it brought in, and the last error.

### sync_bookmarks

`POST /api/v1/sync` · needs a full-access key

Start a bookmark sync from the user's connected platforms right now (the Spark feed's Sync button): pulls new X bookmarks, Nostr bookmarks and Instagram posts from every linked account. Returns at once; poll get_sync_status until each account is idle, then list_new_bookmarks to pick up what arrived. Sync also runs on the user's own schedule.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `source` | "x" | "nostr" | "instagram" | no | One source to sync. Omit to sync every connected source. |

### list_bookmarks

`GET /api/v1/bookmarks` · read-only keys allowed

Page through the user's Spark feed (bookmarks synced from X, Nostr and Instagram, pasted links, forwarded emails), newest first, with the same filters as the feed's tabs and dropdowns. Each item carries its source platform, author, tags and text. Defaults to the inbox: items not yet dystilled, filed into a project, or archived. `counts` gives the tab badges (per source and per status). Use list_new_bookmarks instead when you are watching for new arrivals.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `status` | "undistilled" | "distilled" | "processed" | "archived" | "all" | no | undistilled = the inbox; distilled = turned into a dystillation; processed = filed into a project; archived = dismissed; all = every non-archived, non-processed item. Default "undistilled". |
| `source` | "x" | "nostr" | "email" | "youtube" | "instagram" | "facebook" | "website" | no | Only items from this platform. |
| `query` | string | no | Words that must all appear in the title, text or author. |
| `tag` | string | no | Only items carrying this tag name (see list_tags). |
| `interest` | string | no | Only items under this interest (see list_interests). |
| `sort` | "added" | "published" | no | added = when it reached DYSTIL; published = when it was originally posted. Default "added". |
| `page` | integer | no | 1-based page number. Default 1. |
| `limit` | integer | no | Items per page (max 100). Default 25. |

### list_new_bookmarks

`GET /api/v1/bookmarks/new` · read-only keys allowed

The stream of bookmarks as they arrive, oldest first, in every status. Built for agents that watch the feed: pass back the returned `cursor` on the next call to get only what came in since. Start with `since` (e.g. the current time) to skip the backlog, or with neither to walk the whole history. Items whose content is still being fetched are held back until it lands, so everything you receive is complete.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `cursor` | string | no | The `cursor` from your previous call. |
| `since` | string | no | ISO 8601 time to start from when you have no cursor yet. |
| `limit` | integer | no | Max items to return (max 100). Default 50. |

### get_bookmark

`GET /api/v1/bookmarks/:id` · read-only keys allowed

One bookmark in full: complete text, source platform and author, tags and interests, quoted Nostr notes, what each DYSTIL agent made of it (the card's flip side), the web fact-check, other curators who saved it, and which projects and dystillation it already belongs to. Read this before deciding what to do with an item.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Bookmark id (uuid). |

### add_bookmark

`POST /api/v1/bookmarks` · needs a full-access key

Save a URL to the user's feed (a website, YouTube video, Instagram or Facebook post). The page content is fetched in the background; the item reports `processing: true` until it lands. X and Nostr posts arrive through sync instead. Saving the same URL twice returns the existing bookmark.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `url` | string | yes | The link to save. |

### update_bookmark

`PATCH /api/v1/bookmarks/:id` · needs a full-access key

Archive a bookmark (dismiss it from the inbox; reversible) or flip its distilled flag. To file a bookmark into a project use add_project_sources, which also marks it processed.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Bookmark id (uuid). |
| `archived` | boolean | no | true to archive, false to restore. |
| `distilled` | boolean | no | Manually mark as (un)distilled. |

### dystil_bookmark

`POST /api/v1/bookmarks/:id/dystil` · needs a full-access key

The feed card's 'What next?' menu. kind 'standard' is New dystillation: distill the item into the user's own post, with AI research of the topic, an AI article draft and a hero image (spends the user's AI credit). kind 'repost' is Repost: share it as it is in the user's edition, with no research, article or image. Either way you get a draft dystillation (publish it with publish_dystillation). Idempotent: a bookmark already dystilled returns its existing dystillation.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Bookmark id (uuid). |
| `kind` | "standard" | "repost" | no | standard = New dystillation; repost = Repost. Default "standard". |

### list_interests

`GET /api/v1/interests` · read-only keys allowed

The themes DYSTIL has inferred from the user's tags (each clusters several tags). Pass an id as `interest` to list_bookmarks for everything under that theme.

### delete_bookmark

`DELETE /api/v1/bookmarks/:id` · needs a full-access key · destructive

Permanently delete a pasted link, video, Instagram/Facebook post or forwarded email from the feed. X and Nostr items can't be deleted here (they would come back on the next sync; unbookmark them on the platform), and locked items can't be deleted at all. Dystillations already made from it keep their own copy. Prefer update_bookmark with archived: true unless the user asked for deletion.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Bookmark id (uuid). |

### lock_bookmark

`POST /api/v1/bookmarks/:id/lock` · needs a full-access key · destructive

Take a permanent, immutable snapshot of a bookmark: its text and media are frozen (media re-hosted), and the snapshot's sha256 is notarized publicly on Nostr. One-way: a locked item can never be edited, overwritten by sync, unlocked or deleted. Only on the user's explicit request.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Bookmark id (uuid). |

### chat

`POST /api/v1/chat` · needs a full-access key

The feed card's Chat ('Ask questions about this item. Nothing is published.') and the dashboard's Chat page. Pass `bookmarkId` to ask about one feed item (continues that item's existing thread, like the button); pass `conversationId` to continue any thread; pass neither for a new library-wide question. Answers cite the user's saved items (`sources`). Waits for the full answer. Spends AI credit; nothing is published.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `question` | string | yes |  |
| `bookmarkId` | string | no | Ask about this feed item. |
| `conversationId` | string | no | Continue this thread (from a previous answer or list_chats). |

### list_chats

`GET /api/v1/chats` · read-only keys allowed

The user's chat threads, most recently active first. `bookmarkId` is set on a thread opened from a feed item.

### get_chat

`GET /api/v1/chats/:id` · read-only keys allowed

Every message in one chat thread, oldest first, with the sources each answer cited.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Conversation id (uuid). |

### list_projects

`GET /api/v1/projects` · read-only keys allowed

The user's projects (owned and shared), most recently active first, each with its description and goals so you can judge what belongs in it. This is the context to read before filing bookmarks into projects.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `query` | string | no | Text to find in the project name or description. |
| `archived` | boolean | no | true lists archived projects instead. Default false. |
| `page` | integer | no | 1-based page number. Default 1. |
| `limit` | integer | no | Items per page (max 100). Default 25. |

### get_project

`GET /api/v1/projects/:id` · read-only keys allowed

One project in full: description, goals, members, every source (bookmarks and dystillations) and its collaborative dystillations.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Project id (uuid). |

### create_project

`POST /api/v1/projects` · needs a full-access key

Create a private project owned by the user, optionally seeded with goals and with bookmarks/dystillations as sources (bookmarks filed this way leave the inbox as 'processed').

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `name` | string | yes |  |
| `description` | string | no |  |
| `goals` | string[] | no | Goal titles, in order. Default []. |
| `bookmarkIds` | string[] | no | Default []. |
| `dystillationIds` | string[] | no | Default []. |

### update_project

`PATCH /api/v1/projects/:id` · needs a full-access key

Rename a project, rewrite its description, change its visibility, or archive it. Name/description/visibility need owner or admin; archiving is owner-only. Making a project public exposes its source board at a public URL, so only do that when asked.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Project id (uuid). |
| `name` | string | no |  |
| `description` | string | no |  |
| `visibility` | "private" | "public" | no |  |
| `archived` | boolean | no |  |

### add_project_sources

`POST /api/v1/projects/:projectId/sources` · needs a full-access key

The project page's 'Add source': add bookmarks and/or dystillations to a project as sources (list_project_source_candidates shows what can be added). This is how you file a new bookmark into the project it relates to: the bookmark is marked 'processed' and leaves the inbox. Already-added items are left as they are. Returns which ids were added and which were refused (not yours, or not found).

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `bookmarkIds` | string[] | no | Default []. |
| `dystillationIds` | string[] | no | Default []. |

### remove_project_source

`DELETE /api/v1/projects/:projectId/sources/:sourceId` · needs a full-access key · destructive

Take a source off a project (by the `sourceId` from get_project). The bookmark or dystillation itself is kept; its copy is also removed from the project's dystillations.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `sourceId` | string | yes | Project source id (uuid). |

### add_project_goal

`POST /api/v1/projects/:projectId/goals` · needs a full-access key

Add a goal (task/outcome) to a project's checklist.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `title` | string | yes |  |

### update_project_goal

`PATCH /api/v1/projects/:projectId/goals/:goalId` · needs a full-access key

Tick a goal off (or reopen it), or assign it to a project member (`assigneeId: null` clears).

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `goalId` | string | yes | Goal id (uuid). |
| `completed` | boolean | no |  |
| `assigneeId` | string | null | no | A member's user id, or null to unassign. |

### list_project_messages

`GET /api/v1/projects/:projectId/messages` · read-only keys allowed

The project's member discussion thread, oldest first (the most recent page).

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |

### post_project_message

`POST /api/v1/projects/:projectId/messages` · needs a full-access key

Post a message to the project's discussion as the user. Other members see it as from them, and @curatorSlug mentions notify those members by email or Nostr DM, so write as the user would and only mention people deliberately.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `content` | string | yes |  |
| `replyToId` | string | no | A message id to reply to. |

### list_project_notes

`GET /api/v1/projects/:projectId/notes` · read-only keys allowed

The user's private notes on a project (no other member can see these), newest first.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |

### add_project_note

`POST /api/v1/projects/:projectId/notes` · needs a full-access key

Add a private note to a project, visible only to the user. A good place to record why you filed something or what you noticed.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `content` | string | yes |  |

### create_project_dystillation

`POST /api/v1/projects/:projectId/dystillations` · needs a full-access key

The project page's 'New dystillation': create a collaborative post inside a project, seeded from the project's sources (research is queued when there are sources to research, which spends AI credit). Optionally write it in the same call with `title` and `markdown` or `html`. Returns the full dystillation; keep editing it with update_dystillation, ask agents with ask_dystillation_agent, and publish with publish_dystillation.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `title` | string | no | The post's title. |
| `markdown` | string | no | The post body as Markdown (converted to the editor's HTML). |
| `html` | string | no | The post body as HTML (sanitized to the editor's allowed tags). Send markdown or html, not both. |

### list_project_source_candidates

`GET /api/v1/projects/:projectId/source-candidates` · read-only keys allowed

The project page's 'Add source' picker: the user's feed items (Feed tab) and dystillations (Dystillations tab) that aren't on this project's board yet, newest first. `query` narrows both by words in the title, text or author. File the ones you pick with add_project_sources; for a link that isn't saved yet use add_project_link.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `query` | string | no |  |
| `limit` | integer | no | Max items per tab. Default 50. |

### add_project_link

`POST /api/v1/projects/:projectId/links` · needs a full-access key

The project's 'Add from URL': save any link (website, YouTube, X or Nostr post, Instagram or Facebook post) as a feed bookmark and add it to the project as a source in one step. The content is fetched in the background; get_bookmark shows `processing: true` until it lands. Unlike add_bookmark, X and Nostr links are saved directly.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `url` | string | yes |  |

### remove_project_goal

`DELETE /api/v1/projects/:projectId/goals/:goalId` · needs a full-access key · destructive

Delete a goal from a project's checklist.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `goalId` | string | yes | Goal id (uuid). |

### edit_project_message

`PATCH /api/v1/projects/:projectId/messages/:messageId` · needs a full-access key

Edit the text of one of the user's own discussion messages, or delete it (`delete: true`; the thread keeps a 'message deleted' caption). Only the author's messages can be changed.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `messageId` | string | yes | Message id (uuid). |
| `content` | string | no |  |
| `delete` | boolean | no |  |

### react_to_project_message

`POST /api/v1/projects/:projectId/messages/:messageId/reactions` · needs a full-access key

Toggle the user's emoji reaction on a discussion message (sending the same emoji again removes it). Allowed: 👍 ❤️ 🎉 👀 🚀 😄.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `messageId` | string | yes | Message id (uuid). |
| `emoji` | "👍" | "❤️" | "🎉" | "👀" | "🚀" | "😄" | yes |  |

### update_project_note

`PATCH /api/v1/projects/:projectId/notes/:noteId` · needs a full-access key

Rewrite one of the user's private project notes, or remove it (`delete: true`).

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `noteId` | string | yes | Note id (uuid). |
| `content` | string | no |  |
| `delete` | boolean | no |  |

### list_project_people

`GET /api/v1/projects/:projectId/people` · read-only keys allowed

Who is on a project (members with their roles), the experts it was composed from (authors of its sources, with whether each can be invited), and pending requests to join. Experts and join requests are visible to the owner and admins only.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |

### manage_project_member

`POST /api/v1/projects/:projectId/members` · needs a full-access key · destructive

Owner/admin only. action 'add' adds a DYSTIL curator by `curatorSlug`; 'invite_experts' invites experts by id from list_project_people (on DYSTIL they get a notification; Nostr-only authors get a DM invite); 'set_role' changes a member's role; 'remove' takes them off. Adding or inviting people notifies them, so only do it when the user asked.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `action` | "add" | "invite_experts" | "set_role" | "remove" | yes |  |
| `curatorSlug` | string | no | For 'add'. |
| `expertIds` | string[] | no | For 'invite_experts'. |
| `userId` | string | no | The member, for 'set_role' and 'remove'. |
| `role` | "viewer" | "editor" | "admin" | no | For 'add', 'invite_experts' and 'set_role'. Default "editor". |

### resolve_join_request

`POST /api/v1/projects/:projectId/join-requests/:requestId` · needs a full-access key

Owner/admin only. Approve (they join as a viewer) or decline someone's request to join (ids from list_project_people).

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |
| `requestId` | string | yes | Join request id (uuid). |
| `approve` | boolean | yes |  |

### lock_project_sources

`POST /api/v1/projects/:projectId/lock` · needs a full-access key · destructive

Lock every feed source in the project that the user owns (see lock_bookmark: a permanent, publicly notarized snapshot that can never be undone). Only on the user's explicit request.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string | yes | Project id (uuid). |

### delete_project

`DELETE /api/v1/projects/:id` · needs a full-access key · destructive

Owner only. Permanently delete a project with its board, goals, discussion and notes. Its bookmarks and standalone dystillations are kept. Can't be undone: prefer update_project with archived: true unless the user asked for deletion.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Project id (uuid). |

### list_dystillations

`GET /api/v1/dystillations` · read-only keys allowed

The user's own dystillations (their posts: drafts and published), most recently edited first. Project dystillations are listed on get_project instead.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `query` | string | no | Text to find in the title, body or author. |
| `archived` | boolean | no | true lists the archived ones instead. Default false. |
| `page` | integer | no | 1-based page number. Default 1. |
| `limit` | integer | no | Items per page (max 100). Default 25. |

### get_dystillation

`GET /api/v1/dystillations/:id` · read-only keys allowed

One dystillation in full: the human body (HTML and plain text), the AI draft, research, sources, sections and tags, enrichments, each agent's research, the page toggles, plus the status of any background jobs. A project dystillation (written collaboratively inside a project) also carries `project`: the project, its co-author byline, and who has the editor open. Project dystillations are open to members of that project only, with your project role.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |

### create_dystillation

`POST /api/v1/dystillations` · needs a full-access key

Start a new draft dystillation. With `url` it ingests that page, X post, YouTube video or Nostr note and researches it in the background (spends AI credit). Without one it creates a blank draft you can fill with title + markdown. To dystil something already in the feed, use dystil_bookmark.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `url` | string | no | A link to build the dystillation from. |
| `title` | string | no |  |
| `markdown` | string | no | The post body as Markdown (converted to the editor's HTML). |
| `html` | string | no | The post body as HTML (sanitized to the editor's allowed tags). Send markdown or html, not both. |

### update_dystillation

`PATCH /api/v1/dystillations/:id` · needs a full-access key

Everything in the dystillation editor's post and sidebar: title, body, slug, which body the public page shows, newspaper sections, featured, publish date, the show-original / show-enrichments / show-hero / comments toggles, and archiving. A body save queues the same follow-up jobs as saving in the editor (hero image, sections, tags). Fields you leave out are unchanged. On a project dystillation, a title/body/slug change is refused with 409 being_edited while someone has the collaborative editor open with the edit lock.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `title` | string | no |  |
| `markdown` | string | no | The post body as Markdown (converted to the editor's HTML). |
| `html` | string | no | The post body as HTML (sanitized to the editor's allowed tags). Send markdown or html, not both. |
| `slug` | string | no | URL slug; normalized and made unique. |
| `publishSource` | "human" | "ai" | no | Which body the public page renders: your body or the AI draft. |
| `sections` | string[] | no | The newspaper sections to file it under, by slug or label (see list_sections). A label that doesn't exist yet creates the section. Replaces the current set. |
| `featured` | boolean | no | Feature it at the top of the public page. |
| `publishedAt` | string | no | Backdate or move the publish date (published dystillations only). |
| `showOriginal` | boolean | no | Show the original post(s) on the public page. |
| `showEnrichments` | boolean | no | Show the enrichments on the public page. |
| `showHero` | boolean | no | Show the hero image on the public page. |
| `commentsEnabled` | boolean | no | Let signed-in readers comment. |
| `archived` | boolean | no | Archive (hide from the public page) or restore. Owner only. |

### publish_dystillation

`POST /api/v1/dystillations/:id/publish` · needs a full-access key

Publish a dystillation to the user's public curator page (or take it back to draft). Publishing is public and immediate: only do it when the user has asked you to.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `published` | boolean | no | false unpublishes. Default true. |

### generate_article

`POST /api/v1/dystillations/:id/article` · needs a full-access key

Queue (or re-run) the AI article draft for a dystillation, written in the user's chosen agent voice from its sources and research. Lands in `aiBody`, never over the user's own body. Spends AI credit; poll get_dystillation for `articleStatus`.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |

### add_dystillation_source

`POST /api/v1/dystillations/:id/sources` · needs a full-access key

Attach another original post to a dystillation, from the user's feed (`bookmarkId`) or a fresh link (`url`, fetched in the background). Sources feed the research and the AI draft.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `bookmarkId` | string | no | A feed bookmark to attach. |
| `url` | string | no | A link to attach. |

### manage_dystillation_source

`POST /api/v1/dystillations/:id/sources/:sourceId/:action` · needs a full-access key · destructive

Act on one of a dystillation's original posts (the `sources[].id` from get_dystillation): archive it (kept but left out of the article and public page), unarchive it, remove it, or retry a fetch that failed.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `sourceId` | string | yes | Dystillation source id (uuid). |
| `action` | "archive" | "unarchive" | "remove" | "retry" | yes |  |

### reorder_dystillation_sources

`POST /api/v1/dystillations/:id/sources/order` · needs a full-access key

Set the order of a dystillation's original posts. The first non-archived one is the primary source shown on feed cards.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `sourceIds` | string[] | yes | Every source id, in the new order. |

### retry_dystillation_step

`POST /api/v1/dystillations/:id/retry` · needs a full-access key

Re-run a background step that failed or that you want fresh: 'ingest' re-fetches the URL a dystillation was created from; 'research' re-runs the web research (spends AI credit). For the AI article use generate_article.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `step` | "ingest" | "research" | yes |  |

### generate_hero_image

`POST /api/v1/dystillations/:id/hero-image` · needs a full-access key

Queue a new AI hero image for a dystillation (replaces the current one when it lands). Spends AI credit.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |

### list_agents

`GET /api/v1/agents` · read-only keys allowed

The DYSTIL agent personas (their names and what each brings). Use an id with run_agent_research or add_enrichment.

### run_agent_research

`POST /api/v1/dystillations/:id/agent-research` · needs a full-access key

Have one DYSTIL agent research the dystillation's topic in its own voice; results appear in get_dystillation's `agentResearch` (the editor's Research tab). Runs once per agent per dystillation, and never for a repost. Spends AI credit.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `agentId` | string | yes | Agent (see list_agents) id (uuid). |

### add_enrichment

`POST /api/v1/dystillations/:id/enrichments` · needs a full-access key

Queue an enrichment on a dystillation: an agent's take (`agentId`) or the answer to your own `prompt`. Results appear in get_dystillation's `enrichments`, and on the public page when showEnrichments is on. Spends AI credit.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `agentId` | string | no | An agent from list_agents. |
| `prompt` | string | no | A question or instruction, when not using an agent. |

### delete_dystillation

`DELETE /api/v1/dystillations/:id` · needs a full-access key · destructive

Permanently delete a dystillation with its enrichments, chat and images. Its source bookmarks return to undistilled. Can't be undone: prefer update_dystillation with archived: true unless the user asked for deletion.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |

### list_dystillation_agent_chat

`GET /api/v1/dystillations/:id/agent-chat` · read-only keys allowed

The editor workspace's chat with DYSTIL agents about this dystillation, oldest first. Each agent has its own thread (`agentId`); pass one to read just that thread. Separate from the project discussion between people.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `agentId` | string | no | Only this agent's thread. |

### ask_dystillation_agent

`POST /api/v1/dystillations/:id/agent-chat` · needs a full-access key

The editor workspace's chat: ask one DYSTIL agent (see list_agents) about this dystillation. It answers in its own voice from the post, its enrichments and your earlier messages to it; the first message also starts that agent's research. Waits for the full answer. Spends AI credit; nothing is published.

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `id` | string | yes | Dystillation id (uuid). |
| `agentId` | string | yes | Agent (see list_agents) id (uuid). |
| `message` | string | yes |  |
