# Droppebble

> Droppebble shares files and shortens URLs. Every drop (a file or a short
> link) lives under a site hostname, gets a permanent slug and URL, and can
> have a readable alias. Update a file or a link's destination without
> changing the link you already shared.

Droppebble is multi-tenant. One deployment serves many sites, each on its
own hostname. The API lives on the app host only (https://droppebble.com); a request to
a customer hostname never reaches it. Every drop route is scoped to a site
hostname in the path, not the host you call it on.

## Auth

Create an API key at https://droppebble.com/app, account menu, API keys. Send it as a
bearer token.

```
Authorization: Bearer <key>
```

A session cookie also works for browser-driven calls, but a key is what an
external client should use. `GET /api/sites` lists the hostnames a key can
use. Every other drop route needs one of those hostnames in its path.

## List your sites

```sh
curl https://droppebble.com/api/sites -H "Authorization: Bearer $DROP_KEY"
```

Returns `{"sites":[{"id","hostname","brandName","status","orgId","role"}]}`.
`role` is `viewer`, `editor`, or `admin`. Uploading and managing drops
needs `editor` or higher.

## Upload a file

```sh
curl -X POST https://droppebble.com/api/sites/example.com/drops \
  -H "Authorization: Bearer $DROP_KEY" \
  -H "Content-Type: application/pdf" \
  -H "X-Drop-Filename: report.pdf" \
  --data-binary @report.pdf
```

The body is the raw file, streamed through, never multipart form data.
Required: `X-Drop-Filename` (percent-encoded, since HTTP headers are
Latin-1; `?filename=` also works as a query param). Set `Content-Type`
to the file's type; it defaults to `application/octet-stream`.
Optional: `X-Drop-Title` (percent-encoded, or `?title=`) and
`Content-Length` as a size hint only. There is no field to request a
specific slug; it is always generated. Set a readable alias afterward with
`PATCH .../drops/:slug` (below), or create a short link instead if you
want to choose the readable path up front.

Response: `{"slug","url"}`. `slug` is the permanent generated id; `url`
is its canonical share link.

## Publish a new version

Same upload, to an existing slug's `/version` path. There is no title
field here; publishing a version keeps the drop's existing title.

```sh
curl -X POST https://droppebble.com/api/sites/example.com/drops/x7k2p9a/version \
  -H "Authorization: Bearer $DROP_KEY" \
  -H "Content-Type: application/pdf" \
  -H "X-Drop-Filename: report-v2.pdf" \
  --data-binary @report-v2.pdf
```

The drop's URL does not change; it now serves the new version. Previous
versions stay in history, see `GET .../drops/:slug` below.

## Create a short link

```sh
curl -X POST https://droppebble.com/api/sites/example.com/drops/links \
  -H "Authorization: Bearer $DROP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"destinationUrl":"https://example.com/book","alias":"demo","title":"Book a demo"}'
```

`destinationUrl` is required, a complete `http://` or `https://` URL
with no embedded credentials. `alias`, `title`, and `expiresAt` (a Unix
timestamp in milliseconds) are optional; omit them or send `null` for
none. A taken alias returns `409`. Response: the drop's JSON, see "Drop
fields" below, with `kind: "link"`.

## List and get drops

```sh
curl "https://droppebble.com/api/sites/example.com/drops?limit=50&offset=0" \
  -H "Authorization: Bearer $DROP_KEY"

curl https://droppebble.com/api/sites/example.com/drops/x7k2p9a \
  -H "Authorization: Bearer $DROP_KEY"
```

`limit`/`offset` are optional on the list. It returns `{"drops":[...]}`;
one drop's route returns the same shape plus `versions`, an array of
`{version, filename, contentType, size, createdAt, createdVia, current}`.

### Drop fields

`kind` (`"file"` or `"link"`), `slug`, `alias`, `title`, `enabled`,
`createdAt`, `updatedAt`, `expiresAt`, `presenceEnabled`,
`recordSessions`,
`editEnabledUntil`, `canonicalUrl`, `url`. Files add `filename`,
`contentType`, `size`, `version`. Links add `destinationUrl`,
`previewDescription`. `canonicalUrl` always uses the permanent slug;
`url` uses the alias when one is set, otherwise it matches `canonicalUrl`.

## Update settings

```sh
curl -X PATCH https://droppebble.com/api/sites/example.com/drops/x7k2p9a \
  -H "Authorization: Bearer $DROP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Q3 report (final)","alias":"q3-report","expiresAt":null}'
```

Send only the fields you want to change. `alias: null` or
`expiresAt: null` clears that setting; `title: ""` clears the title.
`destinationUrl` and `previewDescription` are only valid on link drops.
`presenceEnabled` and `recordSessions` (booleans) are only valid on file
drops. A conflicting alias returns `409`.

## Toggle, revert, delete

```sh
curl -X POST https://droppebble.com/api/sites/example.com/drops/x7k2p9a/toggle \
  -H "Authorization: Bearer $DROP_KEY"

curl -X POST "https://droppebble.com/api/sites/example.com/drops/x7k2p9a/revert?version=1" \
  -H "Authorization: Bearer $DROP_KEY"

curl -X DELETE https://droppebble.com/api/sites/example.com/drops/x7k2p9a \
  -H "Authorization: Bearer $DROP_KEY"
```

Toggle flips enabled/disabled and returns `{"enabled": bool}`; a disabled
or expired drop serves an unavailable page instead of its content. Revert
is file drops only; `version` can also be sent as a JSON body
`{"version":1}`, and it does not delete later versions, only changes which
one is served. Delete removes the drop and every version permanently and
returns `{"ok":true}`.

## Move a drop to another site

```sh
curl -X POST https://droppebble.com/api/sites/example.com/drops/x7k2p9a/move \
  -H "Authorization: Bearer $DROP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"drop.example.org"}'
```

The drop moves from the site in the path to the site named in
`hostname`. It keeps its slug, alias, versions, and analytics. The response
has the same fields as `GET .../drops/:slug`, with `url` and
`canonicalUrl` on the new hostname. The old hostname answers requests for the
slug or the old alias with a `301` to the new URL, and keeps the query string.
Moving the drop back to its former site removes that redirect.

The caller needs `admin` access on both sites. A target site the caller
cannot see returns `404`. The target must be active and different from the
source, or the request returns `400` or `409`. An alias or slug that already
exists on the target returns `409` and nothing changes.

## Reading content

Drops are served publicly, unauthenticated, on the site's own hostname (or
on https://droppebble.com for sites hosted on the app domain itself):

```
GET https://example.com/x7k2p9a
GET https://example.com/q3-report
```

Both serve the same drop, one via the permanent slug, one via its alias. A
file drop returns its raw bytes with the stored `Content-Type`; a link
drop redirects (`302`) to its destination. Markdown files (`.md`,
`.markdown`, or `text/markdown`) render as a formatted document for
browsers by default; add `?raw=1` for the plain-text source or
`?download=1` to save the original file. A request without
`Accept: text/html`, such as curl, gets the original bytes either way.

## Errors

Every error response is JSON, `{"error": "..."}`, sometimes with extra
fields (import previews, rate-limit detail). Status codes:

- `400`: invalid input, such as a bad hostname, a bad alias, malformed
  JSON, or a missing required field.
- `401`: missing or invalid `Authorization`, or an unauthenticated
  session. The body also carries `"docs"`, a link back to this file.
- `403`: a cross-site request authenticated by cookie (CSRF guard), or an
  authenticated caller without enough access to a site they can otherwise
  see.
- `404`: unknown or inaccessible site or drop. An inaccessible site looks
  exactly like an unknown one, so access cannot be probed by diffing
  responses.
- `409`: alias or short-link path already in use.

## MCP server

`https://droppebble.com/mcp` is a remote MCP server (stateless streamable HTTP) with
the same API key as a bearer token. Tools:
`drop_push` (without `slug` it creates a new link; with `slug` it
publishes a new version at the same URL), `drop_cat`, `drop_info`,
`drop_ls`, `drop_rename`, `drop_revert`, `drop_toggle`. There is no
delete tool.

## API keys

```sh
curl -X POST https://droppebble.com/api/keys \
  -H "Authorization: Bearer $DROP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci"}'
```

Returns the plaintext key once, as `key`, alongside `id`, `name`,
`prefix`, `start`, and `createdAt`. It is never shown again; store it
somewhere safe. `GET /api/keys` lists your keys without the plaintext.
`DELETE /api/keys/:id` revokes one.
