---
title: Droplet
kind: resource
slug: droplet
summary: "A single-file HTML or Markdown app served on Drop. Versioned content, access policy, optional Space membership."
---

# Droplet

A single-file HTML or Markdown app served on Drop. Versioned content, access policy, optional Space membership.

The **Droplet** is Drop's core unit. Every Droplet has a stable `slug` that lives in its URL (`/view/{slug}`), a current published version, an access policy (PRIVATE / SPACE / SPECIFIC / EVERYONE), and optional membership in one Space. Skills (`appType: SKILL`) and Drops (`appType: DROP`) share this same shape; the only difference is rendering and feature gates.

## The Droplet object

### Attributes

- **`slug`** `string` — URL-friendly identifier. Stable for the lifetime of the Droplet — appears in `/view/{slug}` and every API path. Minimum 2 characters, lowercase with hyphens.
- **`name`** `string` — Human-readable display name. Shown on the dashboard and in every share context.
- **`description`** `string · nullable` — One-line description shown alongside the name on the dashboard.
- **`appType`** `enum` (enum: `DROPLET` / `DROP` / `SKILL`) — Default `DROPLET` (static HTML/Markdown). `DROP` is feature-flagged Node.js apps with databases; `SKILL` is a Claude Code skill recipe rendered with markdown preview.
- **`internalAccess`** `enum` (enum: `PRIVATE` / `SPACE` / `SPECIFIC` / `EVERYONE`) — Who at Hi Marley can view this Droplet. `SPACE` is live-bound to the Droplet's owning Space membership. `SPECIFIC` uses a named-people grant list. Set via the [Set access](#set-access) method.
- **`accessMode`** `enum` (enum: `LINK` / `PASSWORD` / `SSO`) — Anonymous-share mode for external share links. Combines with `internalAccess` via the share-link layer.
- **`currentVersion`** `integer` — The latest version number. Incremented on every deploy and rollback.
- **`publishedVersion`** `integer · nullable` — The version currently published at the live URL. May lag `currentVersion` if the latest version is a draft.
- **`spaceId`** `string · nullable` — If the Droplet lives in a Space, the Space's id. Set via [Move into a Space](#move). `null` for personal-workspace Droplets.
- **`tags`** `string[]` — Free-form tags. Used by search (`tag:retro`) and Space featured-tags curation. Lowercase-normalized server-side.
- **`archivedAt`** `timestamp · nullable` — Set when the Droplet is non-destructively archived (hidden from default lists, still recoverable). `null` when active.
- **`id`** `string · less-common` — CUID primary key. Stable but rarely surfaced; use `slug` in API paths.
- **`s3Key`** `string · less-common` — S3 path to the live HTML/Markdown file. `{slug}/index.html`. Internal — rarely needed by API consumers.
- **`sourceFormat`** `enum · less-common` (enum: `HTML` / `MARKDOWN`) — Detected at deploy time from file extension. Markdown is rendered server-side with a Hi Marley stylesheet.
- **`fileSize`** `integer · less-common` — Size of the live file in bytes.
- **`searchText`** `string · less-common` — Extracted text content used for full-text search. Regenerated on every deploy.
- **`ownerId`** `string · less-common` — The User who created the Droplet. Carries owner-only permissions (transfer, delete, change access mode).
- **`status`** `enum · less-common` (enum: `ACTIVE` / `ARCHIVED` / `DELETED`) — Hard-delete state. `ARCHIVED` here means *destructively* removed (S3 + edge cleanup done) — distinct from the non-destructive `archivedAt`.
- **`commentsEnabled`** `boolean · less-common` — Whether `/view/{slug}` shows the comments side panel. Default `true`.
- **`allowEmbedding`** `boolean · less-common` — Whether the Droplet can be embedded in an iframe on a non-Drop origin. Default `false`.
- **`inlineEditingEnabled`** `boolean · less-common` — Owner-only flag: enables the in-browser pencil editor on `/view/{slug}`. Default `false`.
- **`createdAt`** `timestamp · less-common` — ISO 8601 timestamp of creation.
- **`updatedAt`** `timestamp · less-common` — ISO 8601 timestamp of last metadata or content update.

### JSON example

```json
{
  "id": "clx1abc...",
  "slug": "q2-retro-board",
  "name": "Q2 retro",
  "description": "Quarterly retrospective board for the platform team",
  "appType": "DROPLET",
  "internalAccess": "SPACE",
  "accessMode": "LINK",
  "currentVersion": 3,
  "publishedVersion": 3,
  "s3Key": "q2-retro-board/index.html",
  "fileSize": 45200,
  "sourceFormat": "HTML",
  "searchText": "Q2 platform retro board with cards for went well…",
  "tags": ["retro", "platform"],
  "spaceId": "spc_platform_team",
  "ownerId": "usr_alex",
  "status": "ACTIVE",
  "archivedAt": null,
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": false,
  "createdAt": "2026-04-12T14:22:00.000Z",
  "updatedAt": "2026-05-31T09:15:00.000Z"
}
```

## Methods

### Create a Droplet

`POST /api/cli/deploy`

Deploy a new Droplet from a single HTML or Markdown file. Auto-creates the slug if not provided. Pass `?draft=true` to land in your private workspace instead of publishing. Two-step: this returns a presigned S3 upload URL; PUT the file to that URL, then POST to confirm.

**CLI**

```bash
drop create app.html \
  --slug q2-retro-board \
  --name "Q2 retro" \
  --description "Quarterly retrospective board"

# Draft (lands in your private workspace)
drop create app.html --slug q2-retro-board --draft
```

**MCP**

```json
{
  "tool": "drop_create",
  "arguments": {
    "name": "Q2 retro",
    "slug": "q2-retro-board",
    "description": "Quarterly retrospective board",
    "content": "<!DOCTYPE html>..."
  }
}
```

**HTTP**

```bash
# Step 1 — request presigned URL
curl -X POST https://drop.himarley.ai/api/cli/deploy \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug":"q2-retro-board","name":"Q2 retro"}'

# Step 2 — upload file
curl -X PUT "$UPLOAD_URL" --data-binary @app.html

# Step 3 — confirm
curl -X POST https://drop.himarley.ai/api/apps/q2-retro-board/confirm \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (201)

```json
{
  "app": {
    "id": "clx1abc...",
    "slug": "q2-retro-board",
    "name": "Q2 retro",
    "description": "Quarterly retrospective board",
    "appType": "DROPLET",
    "accessMode": "LINK",
    "sourceFormat": "HTML",
    "status": "ACTIVE",
    "createdAt": "2026-04-12T14:22:00.000Z"
  },
  "uploadUrl": "https://drop-apps.s3.amazonaws.com/q2-retro-board/v1/index.html?X-Amz-...",
  "isNew": true
}
```

### List pipeline stages

`GET /api/apps/{slug}/pipeline`

Return the latest-attempt PipelineStage row for each of the droplet's 7 background stages (upload, text-extract, summary, KB ingest, edge sync, CDN invalidation, vector indexing). Stages that have never run are absent. Caller must be able to read the droplet.

**CLI**

```bash
drop pipeline q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_pipeline_list",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/apps/q2-retro-board/pipeline' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "stages": [
    {
      "stage": "UPLOAD",
      "status": "SUCCEEDED",
      "attempt": 1,
      "startedAt": "2026-06-07T15:00:00.000Z",
      "endedAt": "2026-06-07T15:00:01.230Z",
      "lastError": null,
      "metadata": {
        "version": 3,
        "fileSize": 84321
      }
    }
  ]
}
```

### Retrieve a Droplet

`GET /api/apps/{slug}`

Return the full Droplet object by slug. Includes Space context (live member count when `internalAccess` is `SPACE`) and the effective audience (computed from access mode + Space policy). 404 to anyone who can't see it (privacy by indistinguishability).

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet's URL slug.

**CLI**

```bash
drop get q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_get",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/apps/q2-retro-board' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "id": "clx1abc",
  "slug": "q2-retro-board",
  "name": "Q2 retro",
  "description": "Quarterly retrospective board for the platform team",
  "appType": "DROPLET",
  "internalAccess": "SPACE",
  "accessMode": "LINK",
  "currentVersion": 3,
  "publishedVersion": 3,
  "spaceId": "spc_platform_team",
  "tags": [
    "retro",
    "platform"
  ],
  "archivedAt": null,
  "ownerId": "usr_alex",
  "status": "ACTIVE",
  "s3Key": "q2-retro-board/index.html",
  "fileSize": 45200,
  "searchText": "Q2 platform retro…",
  "sourceFormat": "HTML",
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": false,
  "createdAt": "2026-04-12T14:22:00.000Z",
  "updatedAt": "2026-05-31T09:15:00.000Z"
}
```

### Update a Droplet

`PATCH /api/apps/{slug}`

Patch Droplet metadata: `name`, `description`, `tags`, `allowEmbedding`, `commentsEnabled`, `inlineEditingEnabled`, `accessMode`, `password`, `publicName`, `spaceSlug`. Use the dedicated [Set access](#set-access) endpoint to change `internalAccess` (audience expansions need an explicit confirm). Tags are server-side normalized: trim → lowercase → drop empty → cap at 50 chars each → dedup → cap at 20 total. Only the owner can change `accessMode`, `password`, `publicName`, `spaceSlug`, and `inlineEditingEnabled`; editors can change the other metadata.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`name`** _(string · body)_ — New display name.
- **`description`** _(string · body)_ — New description.
- **`tags`** _(string[] · body)_ — Replace tags (server normalizes: lowercase, dedup, ≤ 20 entries, ≤ 50 chars each).
- **`commentsEnabled`** _(boolean · body)_ — Toggle the comments panel on /view/{slug}.
- **`allowEmbedding`** _(boolean · body)_ — Allow iframe embedding from non-Drop origins.
- **`inlineEditingEnabled`** _(boolean · body)_ — Owner-only. Enable the in-browser pencil editor.
- **`accessMode`** _(enum · body)_ — Owner-only. LINK | PASSWORD | SSO.
- **`password`** _(string · body)_ — Required when accessMode is PASSWORD.
- **`publicName`** _(string · body)_ — Owner-only. Custom URL path.
- **`spaceSlug`** _(string | null · body)_ — Owner-only. Move into/between Spaces (or null to evict).

**CLI**

```bash
drop set q2-retro-board --name "Q2 platform retro" --tags retro,platform,2026q2
```

**MCP**

```json
{
  "tool": "drop_patch",
  "arguments": {
    "slug": "q2-retro-board",
    "name": "Q2 platform retro",
    "tags": [
      "retro",
      "platform",
      "2026q2"
    ]
  }
}
```

**HTTP**

```bash
curl -X PATCH 'https://drop.himarley.ai/api/apps/q2-retro-board' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Q2 platform retro","tags":["retro","platform","2026q2"]}'
```

**Response** (200)

```json
{
  "id": "clx1abc",
  "slug": "q2-retro-board",
  "name": "Q2 platform retro",
  "description": "Quarterly retrospective board for the platform team",
  "appType": "DROPLET",
  "internalAccess": "SPACE",
  "accessMode": "LINK",
  "currentVersion": 3,
  "publishedVersion": 3,
  "spaceId": "spc_platform_team",
  "tags": [
    "retro",
    "platform",
    "2026q2"
  ],
  "archivedAt": null,
  "ownerId": "usr_alex",
  "status": "ACTIVE",
  "s3Key": "q2-retro-board/index.html",
  "fileSize": 45200,
  "searchText": "Q2 platform retro…",
  "sourceFormat": "HTML",
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": false,
  "createdAt": "2026-04-12T14:22:00.000Z",
  "updatedAt": "2026-05-31T09:15:00.000Z"
}
```

### Delete

`DELETE /api/apps/{slug}`

Irreversible hard-delete (029-freshness FR-028). Gated by the clean-slate rule (caller is sole owner AND zero comments AND zero view events AND no `AppAccess` grants). On gate failure returns `409 NOT_CLEAN_SLATE` with `{ code, reason, message }`. On success: removes the App row (Prisma cascade-deletes versions, comments, share links, access rows, stars, view events), removes the S3 prefix `{slug}/`, removes the DynamoDB edge record, and triggers a CloudFront invalidation for `/{slug}/*`. Use [Archive](#archive) for recoverable removal.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop delete q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_delete",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/apps/q2-retro-board' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "success": true,
  "hardDeleted": true
}
```

### Set access

`PUT /api/apps/{slug}/access`

Change `internalAccess` (PRIVATE / SPACE / SPECIFIC / EVERYONE). Audience expansions return `409 CONFIRMATION_REQUIRED` until `{ confirmed: true }` is included; reductions are immediate. See `POST /api/apps/{slug}/access/preview` for the audience diff. The full request body supports `people` (email grant list) and `grants` (mixed audience: email + User Group grants); see the route source for the complete schema.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`internalAccess`** _(enum · body)_ — PRIVATE | SPACE | SPECIFIC | EVERYONE.
- **`confirmed`** _(boolean · body)_ — Apply an audience-expanding change past the 409 gate.

**CLI**

```bash
drop set-access q2-retro-board --internal-access EVERYONE --confirmed
```

**MCP**

```json
{
  "tool": "drop_set_access",
  "arguments": {
    "slug": "q2-retro-board",
    "internalAccess": "EVERYONE",
    "confirmed": true
  }
}
```

**HTTP**

```bash
curl -X PUT 'https://drop.himarley.ai/api/apps/q2-retro-board/access' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"internalAccess":"EVERYONE","confirmed":true}'
```

**Response** (200)

```json
{
  "internalAccess": "EVERYONE",
  "changed": [
    "mode"
  ]
}
```

### Move into a Space

`POST /api/apps/{slug}/move`

Move a Droplet into / between / out of a Space. Flips `internalAccess` to `SPACE` on move-in; resets to `PRIVATE` on move-out (FR-015). Preserves prior `SPECIFIC` grants as supplemental rows on move-in (FR-013) so a later move-out can recover them. Owner-only. Returns `409 CONFIRMATION_REQUIRED` with the access preview for audience-expanding moves; never for reductions.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`toSpaceId`** _(string | null · body)_ — Target Space id, or null to evict to personal workspace.
- **`confirmed`** _(boolean · body)_ — Apply an audience-expanding move past the 409 gate.

**CLI**

```bash
drop move q2-retro-board --to-space-id spc_platform_team --confirmed
```

**MCP**

```json
{
  "tool": "drop_app_move",
  "arguments": {
    "slug": "q2-retro-board",
    "toSpaceId": "spc_platform_team",
    "confirmed": true
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/move' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"toSpaceId":"spc_platform_team","confirmed":true}'
```

**Response** (200)

```json
{
  "app": {
    "slug": "q2-retro-board",
    "spaceId": "spc_platform_team",
    "internalAccess": "SPACE"
  },
  "changed": [
    "space",
    "visibility",
    "preserved_specific"
  ],
  "preservedSpecificCount": 2
}
```

### Rollback to a version

`POST /api/apps/{slug}/versions/{version}/rollback`

Roll back to a previous version. The target version's content is copied to the live path and the CDN cache is invalidated. The previous live version stays in history as a separate row.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`version`** _(number · path · required)_ — Target version number.

**CLI**

```bash
drop rollback q2-retro-board 5
```

**MCP**

```json
{
  "tool": "drop_rollback",
  "arguments": {
    "slug": "q2-retro-board",
    "version": 5
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/versions/5/rollback' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "success": true,
  "version": {
    "id": "clxver5",
    "version": 5,
    "fileSize": 45200,
    "deployedBy": {
      "name": "Alex Chen",
      "email": "alex@himarley.com"
    },
    "notes": null,
    "createdAt": "2026-04-12T14:22:00.000Z"
  }
}
```

### List versions

`GET /api/apps/{slug}/versions`

Return the full version history for a Droplet — newest first. Each row includes the deployer, the file size, optional notes, the conversion status (for Markdown Droplets), and whether the version is the current published one.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop versions q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_versions",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/apps/q2-retro-board/versions' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "versions": [
    {
      "id": "clxver3",
      "version": 3,
      "fileSize": 45200,
      "deployedBy": {
        "name": "Alex Chen",
        "email": "alex@himarley.com"
      },
      "notes": "Fixed chart colors",
      "isCurrent": true,
      "createdAt": "2026-05-31T09:15:00.000Z",
      "sourceFormat": "HTML"
    }
  ],
  "currentVersion": 3,
  "publishedVersion": 3
}
```

### Redeploy

`POST /api/apps/{slug}/redeploy`

Provision the next published version of a Droplet. Returns a presigned S3 upload URL — PUT the new HTML/Markdown content to that URL, and the version is live as soon as the upload completes. Owner/editor only; `baseVersion` provides optimistic concurrency.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`fileSize`** _(number · body)_ — Expected upload size in bytes.
- **`notes`** _(string · body)_ — Version notes.
- **`contentType`** _(string · body)_ — text/html (default) | text/markdown.
- **`baseVersion`** _(number · body)_ — Optimistic concurrency baseline.

**CLI**

```bash
drop redeploy q2-retro-board --notes "Updated team list" --file-size 45200
```

**MCP**

```json
{
  "tool": "drop_redeploy",
  "arguments": {
    "slug": "q2-retro-board",
    "notes": "Updated team list",
    "fileSize": 45200
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/redeploy' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"notes":"Updated team list","fileSize":45200}'
```

**Response** (200)

```json
{
  "uploadUrl": "https://drop-apps.s3.amazonaws.com/q2-retro-board/v4/index.html?X-Amz-...",
  "version": 4,
  "app": {
    "id": "clx1abc",
    "slug": "q2-retro-board",
    "name": "Q2 retro",
    "deployCount": 4,
    "lastDeployedAt": "2026-05-31T09:15:00.000Z"
  }
}
```

### Duplicate

`POST /api/apps/{slug}/duplicate`

Create a new Droplet seeded from this one's latest version. The new Droplet starts in `PRIVATE` mode under the caller's ownership; pass an optional `name` and `newSlug`. Returns the new Droplet record. Slug collisions auto-resolve by appending `-2`, `-3`, etc.

**Parameters**

- **`slug`** _(string · path · required)_ — The source Droplet slug.
- **`name`** _(string · body)_ — Name for the duplicate.
- **`newSlug`** _(string · body)_ — Slug for the duplicate (auto-resolves collisions).

**CLI**

```bash
drop duplicate q2-retro-board --name "Q3 retro template"
```

**MCP**

```json
{
  "tool": "drop_duplicate",
  "arguments": {
    "slug": "q2-retro-board",
    "name": "Q3 retro template"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/duplicate' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Q3 retro template"}'
```

**Response** (201)

```json
{
  "id": "clxnew",
  "slug": "q2-retro-board-copy",
  "name": "Q3 retro template",
  "description": "Quarterly retrospective board for the platform team",
  "appType": "DROPLET",
  "internalAccess": "PRIVATE",
  "accessMode": "LINK",
  "currentVersion": 1,
  "publishedVersion": 1,
  "spaceId": null,
  "tags": [],
  "archivedAt": null,
  "ownerId": "usr_alex",
  "status": "ACTIVE",
  "s3Key": "q2-retro-board-copy/index.html",
  "fileSize": 45200,
  "searchText": null,
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": false,
  "createdAt": "2026-05-31T09:15:00.000Z",
  "updatedAt": "2026-05-31T09:15:00.000Z"
}
```

### Star a Droplet

`POST /api/apps/{slug}/star`

Mark a Droplet as starred for the calling user. Stars are per-user — they pin the Droplet to the top of the dashboard and surface it in `drop list --starred`. Idempotent: re-starring returns the original `starredAt` timestamp.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop star q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_star",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/star' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "starredAt": "2026-05-31T09:15:00.000Z"
}
```

### Unstar a Droplet

`DELETE /api/apps/{slug}/star`

Remove the caller's star on a Droplet. Idempotent: returns `removed: false` if there was nothing to remove.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop unstar q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_unstar",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/apps/q2-retro-board/star' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "removed": true
}
```

### Pin to Space

`POST /api/apps/{slug}/pin`

Pin a Droplet to the top of its Space. Only valid for Droplets that belong to a Space. Pinning is per-Droplet (not per-user — every Space member sees the same pinned set). Owner/editor only.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop pin q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_pin",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/pin' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "pinnedAt": "2026-05-31T09:15:00.000Z"
}
```

### Unpin from Space

`DELETE /api/apps/{slug}/pin`

Remove a Droplet's pin within its Space. Owner/editor only. Idempotent.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop unpin q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_unpin",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/apps/q2-retro-board/pin' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "pinnedAt": null
}
```

### Publish a draft

`POST /api/apps/{slug}/publish`

Promote one of the caller's drafts to a new published version. Squashes any sibling drafts in the workspace (only the chosen draft survives, the rest are discarded). With no `draftId`, publishes the latest draft. If the Droplet was previously archived, publish clears `archivedAt` and the response includes `wokeFromArchive: true` (029-freshness FR-018 wake-on-publish). Owner/editor only.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`draftId`** _(string · body)_ — Draft id or shortId — defaults to latest.
- **`notes`** _(string · body)_ — Override the draft's notes.

**CLI**

```bash
drop publish q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_publish",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/publish' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "version": 4,
  "publishedAt": "2026-05-31T09:15:00.000Z",
  "siblingsDiscarded": 2
}
```

### Unpublish

`POST /api/apps/{slug}/unpublish`

Take a published Droplet offline. Sets `publishedVersion = null` (preserves `currentVersion` so republish can restore the same version). Viewers see an "unavailable" placeholder. Drafts are unaffected. Owner/editor only.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop unpublish q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_unpublish",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/unpublish' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "ok": true
}
```

### Republish

`POST /api/apps/{slug}/republish`

Restore a previously-unpublished Droplet by setting `publishedVersion = currentVersion`. Idempotent on already-published Droplets. Owner/editor only.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop republish q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_republish",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/republish' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "version": 4
}
```

### List drafts

`GET /api/apps/{slug}/drafts`

List the calling user's drafts on this Droplet. Drafts are private per-caller — co-editors cannot see them. Returns 404 (not 403) when the caller lacks edit rights (privacy by indistinguishability).

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop drafts q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_list_drafts",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/apps/q2-retro-board/drafts' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "drafts": [
    {
      "id": "clxdr1",
      "shortId": 1,
      "fileSize": 46123,
      "sourceFormat": "HTML",
      "notes": "WIP: refactor cards",
      "createdAt": "2026-05-31T09:15:00.000Z",
      "commentCount": 0
    }
  ]
}
```

### Retrieve a draft

`GET /api/apps/{slug}/drafts/{draftId}`

Get metadata for one of the caller's drafts on a Droplet. `draftId` may be the CUID or the workspace-local short ID (e.g. `1`, `2`). Returns 404 when not owned by the caller (privacy by indistinguishability).

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`draftId`** _(string · path · required)_ — Draft CUID or shortId.

**CLI**

```bash
drop draft get q2-retro-board 1
```

**MCP**

```json
{
  "tool": "drop_get_draft",
  "arguments": {
    "slug": "q2-retro-board",
    "draftId": "1"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/apps/q2-retro-board/drafts/1' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "id": "clxdr1",
  "shortId": 1,
  "fileSize": 46123,
  "sourceFormat": "HTML",
  "notes": "WIP: refactor cards",
  "createdAt": "2026-05-31T09:15:00.000Z",
  "commentCount": 0
}
```

### Discard a draft

`DELETE /api/apps/{slug}/drafts/{draftId}`

Discard a single draft from the caller's workspace. Returns 204 on success. 404 when the draft is gone or never existed (the CLI / MCP layer treats 404 as idempotent success).

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`draftId`** _(string · path · required)_ — Draft CUID or shortId.

**CLI**

```bash
drop draft discard q2-retro-board 1
```

**MCP**

```json
{
  "tool": "drop_discard_draft",
  "arguments": {
    "slug": "q2-retro-board",
    "draftId": "1"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/apps/q2-retro-board/drafts/1' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (204)

```json
{
  "discarded": true
}
```

### Discard all drafts

`DELETE /api/apps/{slug}/drafts`

Discard every draft in the caller's workspace on this Droplet. Always returns 204 (idempotent — re-running on an empty workspace is a no-op).

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop drafts discard-all q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_discard_all_drafts",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/apps/q2-retro-board/drafts' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (204)

```json
{
  "discarded": true
}
```

### Set Droplet Type

`PUT /api/apps/{slug}/type`

Set the Droplet's Type slot (`App.typeId`) by resolving a Type by name. Resolution honors scope (`USER` | `SPACE` | `GLOBAL`) when provided, otherwise falls back to most-specific-wins from the viewer's context (user → space → global). NON-DESTRUCTIVE: existing `typeFields` are PRESERVED across a Type change, and any sign-off — votes and audit log — survives it. Values the new Type does not declare simply do not render, and are still there if you switch back. A topic-shaped Type seeds its draft status for keys that are absent, so a returning decision keeps the status it left with. To genuinely discard a decision, use `droplet.clearType`, which is the explicit act and does purge. Owner/editor only.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`typeName`** _(string · body · required)_ — Name of the Type to attach.
- **`scope`** _(enum · body)_ — USER | SPACE | GLOBAL — narrows Type resolution.
- **`spaceSlug`** _(string · body)_ — Disambiguate when scope=SPACE and the viewer belongs to multiple Spaces with a Type of this name.

**CLI**

```bash
drop droplets set-type q2-retro-board --type-name recommendation --scope SPACE --space-slug platform-team
```

**MCP**

```json
{
  "tool": "drop_droplets_set_type",
  "arguments": {
    "slug": "q2-retro-board",
    "typeName": "recommendation",
    "scope": "SPACE",
    "spaceSlug": "platform-team"
  }
}
```

**HTTP**

```bash
curl -X PUT 'https://drop.himarley.ai/api/apps/q2-retro-board/type' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"typeName":"recommendation","scope":"SPACE","spaceSlug":"platform-team"}'
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "typeId": "typ_recommendation_platform",
  "typeName": "recommendation",
  "scope": "SPACE",
  "typeFields": {}
}
```

### Clear Droplet Type

`DELETE /api/apps/{slug}/type`

Clear the Droplet's Type slot — sets `App.typeId = null` and resets `App.typeFields = {}`. On a decision this is the un-decision path: the sign-offs and the recorded call go with the Type, reported as `purgedSignoff`. Idempotent: returns `cleared: false` when there was nothing to clear. Owner/editor only.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.

**CLI**

```bash
drop droplets clear-type q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_droplets_clear_type",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/apps/q2-retro-board/type' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "cleared": true,
  "purgedSignoff": {
    "responses": 3,
    "hadVerdict": true
  }
}
```

### Set a typed field on a Droplet

`PUT /api/apps/{slug}/fields/{fieldKey}`

Write a single value into `App.typeFields[fieldKey]`. The Droplet must have a Type slot set (`App.typeId != null`); the server validates `fieldKey` against the Type's schema and coerces / rejects `value` per the field definition. Per-field errors return `400 TYPE_FIELD_*` with `{ field }` extras. Archived Droplets reject writes with `409 APP_ARCHIVED`. Owner/editor only.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`fieldKey`** _(string · path · required)_ — The field key (defined by the Droplet's current Type).
- **`value`** _(string | number | boolean | null | array · body · required)_ — The new value — scalar, null (clear), or audience-ref array. Server validates against the Type schema.

**CLI**

```bash
drop droplets set-field q2-retro-board status --value in-flight
```

**MCP**

```json
{
  "tool": "drop_droplets_set_field",
  "arguments": {
    "slug": "q2-retro-board",
    "fieldKey": "status",
    "value": "in-flight"
  }
}
```

**HTTP**

```bash
curl -X PUT 'https://drop.himarley.ai/api/apps/q2-retro-board/fields/status' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"value":"in-flight"}'
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "fieldKey": "status",
  "value": "in-flight",
  "typeFields": {
    "status": "in-flight",
    "priority": "high"
  }
}
```

### Clear a typed field on a Droplet

`DELETE /api/apps/{slug}/fields/{fieldKey}`

Remove a single key from `App.typeFields`. Idempotent: returns `cleared: false` when the key was already absent. Owner/editor only.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`fieldKey`** _(string · path · required)_ — The field key to remove.

**CLI**

```bash
drop droplets clear-field q2-retro-board priority
```

**MCP**

```json
{
  "tool": "drop_droplets_clear_field",
  "arguments": {
    "slug": "q2-retro-board",
    "fieldKey": "priority"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/apps/q2-retro-board/fields/priority' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "fieldKey": "priority",
  "cleared": true,
  "typeFields": {
    "status": "in-flight"
  }
}
```

### Transfer ownership

`POST /api/apps/{slug}/transfer`

Hand a Droplet to another person. Callable by the current owner **or** a site admin — the admin path exists because the common case (the owner is on PTO or has left) is exactly when the owner cannot act. The prior owner keeps an `EDITOR` grant unless `keepPriorOwnerAsEditor` is false.

Ownership is not an audience change: the recipient must already be able to see the Droplet, or the call returns `409 RECIPIENT_NO_ACCESS`. Grant them access first, then transfer. `internalAccess`, `accessMode`, Space membership, and every draft workspace are left alone — drafts are keyed per-user and stay with their author.

Transferring to the current owner is a no-op (`204`). Archived Droplets are rejected.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`to`** _(string · body · required)_ — Email of the new owner. Must already have access to the Droplet.
- **`keepPriorOwnerAsEditor`** _(boolean · body)_ — Default true. Leaves the prior owner an EDITOR grant so they keep write access. Set false when offboarding.

**CLI**

```bash
drop transfer forum-video-review --to jackie.booth@himarley.com --keep-prior-owner-as-editor
```

**MCP**

```json
{
  "tool": "drop_transfer_ownership",
  "arguments": {
    "slug": "forum-video-review",
    "to": "jackie.booth@himarley.com",
    "keepPriorOwnerAsEditor": true
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/forum-video-review/transfer' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to":"jackie.booth@himarley.com","keepPriorOwnerAsEditor":true}'
```

**Response** (200)

```json
{
  "id": "clx1abc",
  "slug": "forum-video-review",
  "name": "Forum Video Review",
  "description": null,
  "appType": "DROPLET",
  "internalAccess": "EVERYONE",
  "accessMode": "LINK",
  "currentVersion": 42,
  "publishedVersion": 42,
  "spaceId": null,
  "tags": [
    "forum-video"
  ],
  "archivedAt": null,
  "ownerId": "usr_jackie",
  "status": "ACTIVE",
  "s3Key": "forum-video-review/index.html",
  "fileSize": 107111,
  "searchText": "Forum video review…",
  "sourceFormat": "HTML",
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": true,
  "createdAt": "2026-08-12T16:18:33.352Z",
  "updatedAt": "2026-09-11T10:02:00.000Z"
}
```

### Transfer everything one person owns

`POST /api/admin/ownership/transfer-by-owner`

Move every Droplet a person owns to someone else — the offboarding path. Restricted to the `@drop-owner-transfer` role; being a site admin is not sufficient.

Deliberately returns a **count**, never a list: `total` plus a breakdown by access mode. That is enough to judge whether you named the right person (fourteen is plausible, four hundred is a typo) without displaying titles. Archived Droplets are excluded.

The count is **recipient-aware**: `total` is what will actually move, and `blocked` is what the recipient cannot read (a Space they are not in, a SPECIFIC list they are not on). Transfer never widens an audience, so those stay put.

One call moves at most 50, chosen from the **movable** set rather than the most recent, so batch slots are never spent on Droplets the recipient cannot read. `remaining` counts movable leftovers only — it reaches zero, where a raw owned-count would stall forever on blocked rows. Counting stops at 500; past that `truncated` is set and the numbers are a floor. Re-running is safe: a Droplet already owned by the recipient is a no-op.

Call with `dryRun: true` first — the UI does. Partial success is the contract: each Droplet goes through the same per-Droplet rules, and anything that could not move comes back in `skipped` with a code.

Bulk emits ONE notification per party rather than one per Droplet, while still writing a per-Droplet audit row.

**Parameters**

- **`from`** _(string · body · required)_ — Email of the departing owner.
- **`to`** _(string · body · required)_ — Email of the person receiving them.
- **`reason`** _(string · body · required)_ — Who asked and why. Required.
- **`keepPriorOwnerAsEditor`** _(boolean · body)_ — Default false for this mode.
- **`dryRun`** _(boolean · body)_ — Return counts without moving anything.

**CLI**

```bash
drop transfer-all --from allison.paraskevas@himarley.com --to jackie.booth@himarley.com --reason "Allison on PTO through 9/22; Jackie running the forum review." --dry-run
```

**MCP**

```json
{
  "tool": "drop_transfer_by_owner",
  "arguments": {
    "from": "allison.paraskevas@himarley.com",
    "to": "jackie.booth@himarley.com",
    "reason": "Allison on PTO through 9/22; Jackie running the forum review.",
    "keepPriorOwnerAsEditor": false,
    "dryRun": true
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/admin/ownership/transfer-by-owner' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"from":"allison.paraskevas@himarley.com","to":"jackie.booth@himarley.com","reason":"Allison on PTO through 9/22; Jackie running the forum review.","keepPriorOwnerAsEditor":false,"dryRun":true}'
```

**Response** (200)

```json
{
  "dryRun": true,
  "total": 12,
  "byAccess": {
    "EVERYONE": 9,
    "PRIVATE": 3
  },
  "archived": 1,
  "blocked": 2,
  "truncated": false
}
```

### Archive

`POST /api/apps/{slug}/archive`

Non-destructively archive a Droplet — sets `archivedAt`, hides it from default lists, but preserves all content and audit history. Restore with the unarchive endpoint. Idempotent: a second call returns 204 with no body.

**CLI**

```bash
drop archive q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_archive",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/archive' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "archivedAt": "2026-05-31T09:15:00.000Z"
}
```

### Unarchive

`POST /api/apps/{slug}/unarchive`

Clear `archivedAt` and return the Droplet to the active lane. Idempotent: a second call returns 204 with no body.

**CLI**

```bash
drop unarchive q2-retro-board
```

**MCP**

```json
{
  "tool": "drop_unarchive",
  "arguments": {
    "slug": "q2-retro-board"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/unarchive' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "archivedAt": null
}
```

### Retry pipeline stage

`POST /api/apps/{slug}/pipeline/{stage}/retry`

Re-run a single pipeline stage on this droplet. Creates a new PipelineStage row at attempt = (prior + 1) and returns the terminal status. Synchronous — the caller waits for the stage to complete. Caller must be droplet owner or sys admin.

**CLI**

```bash
drop pipeline retry q2-retro-board KB_INGEST
```

**MCP**

```json
{
  "tool": "drop_pipeline_retry",
  "arguments": {
    "slug": "q2-retro-board",
    "stage": "KB_INGEST"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q2-retro-board/pipeline/KB_INGEST/retry' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "slug": "q2-retro-board",
  "stage": "KB_INGEST",
  "status": "SUCCEEDED",
  "attempt": 2,
  "lastError": null
}
```
