# Dropbox simulator

A stateful Dropbox API v2 simulator — one account's files and folders, listings and search, shared links, file requests, shared folders and file members, upload sessions, batch jobs and tags.

`dropbox-twin` is a local, stateful simulator of the Dropbox API v2 — one account's files and folders, listings and search with their cursors, shared links, file requests, shared folders and file members, upload sessions, batch jobs and `save_url`, and tags — serving both `api.dropboxapi.com`'s RPC routes and `content.dropboxapi.com`'s upload and download routes on one port. It never calls Dropbox: no OAuth, no email is sent and no real account changes.

## Quickstart

You need an API key from [Account → API keys](https://www.rystic.ai/account/api-keys) ([how](https://www.rystic.ai/docs/licensing.md#get-an-api-key)).

```bash
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…                      # your API key
rystic run dropbox-twin -d --port 0 --scenario dropbox_personal_archive   # prints the URL it serves on
curl -s http://localhost:<port>/_rystic/version
```

Expect `"product":"dropbox-twin"`. The examples below use `localhost:8080`; use the port `rystic run` printed.

On Enterprise, `rystic login ./rystic-license.json` takes the place of the key ([license files](https://www.rystic.ai/docs/licensing.md#enterprise-license-files)).

Read the account and its tree, then upload a file and download it back:

```bash
curl -s localhost:8080/2/users/get_current_account -H 'Authorization: Bearer sl.placeholder' \
  -H 'Content-Type: application/json' -d null
# -> {"account_id":"dbid:AAApersonalarchive00000000000000001","account_type":{".tag":"pro"},...,"email":"ada.sealy@example.test",...}

curl -s localhost:8080/2/files/list_folder -H 'Authorization: Bearer sl.placeholder' \
  -H 'Content-Type: application/json' -d '{"path":""}'
# -> {"cursor":"…","entries":[{".tag":"folder",...,"path_display":"/Apps",...},{...,"path_display":"/Documents",...},{".tag":"file",...,"path_display":"/notes.md",...,"size":1794},{...,"path_display":"/Photos",...}],"has_more":false}

curl -s localhost:8080/2/files/upload -H 'Authorization: Bearer sl.placeholder' \
  -H 'Content-Type: application/octet-stream' \
  -H 'Dropbox-API-Arg: {"path":"/hello.txt","mode":"add"}' --data-binary 'hello'
# -> {"client_modified":"2026-02-02T00:00:00Z",...,"name":"hello.txt","path_display":"/hello.txt",...,"size":5}

curl -s localhost:8080/2/files/download -H 'Authorization: Bearer sl.placeholder' \
  -H 'Dropbox-API-Arg: {"path":"/hello.txt"}'
# -> hello   (the metadata is in the Dropbox-API-Result response header)
```

Point the official JavaScript SDK at the twin through a `fetch` that rewrites its two hosts:

```ts
import { Dropbox } from "dropbox";
const twin = "http://localhost:8080";
const dbx = new Dropbox({
  accessToken: "sl.placeholder",
  fetch: (url, init) => fetch(url.replace(/^https:\/\/(api|content)\.dropboxapi\.com/, twin), init),
});
const me = await dbx.usersGetCurrentAccount();   // me.result.email === "ada.sealy@example.test"
```

## Plans

`dropbox-twin` is on Pro and Enterprise, not Free ([plans](https://www.rystic.ai/docs/licensing.md#plans)); on a Free key, `rystic pull dropbox-twin` and a start are refused with `product_not_in_plan`. Both plans serve the whole Dropbox surface and the whole control plane below, custom latency and fault injection included. Without `--scenario` the twin boots one Basic account with **no files**, and `POST /_rystic/reset` wipes it back to that.

**Enterprise** runs on a license file instead of an API key.

## Auth

Dropbox reads an access token as `Authorization: Bearer sl.…`. The twin accepts any bearer value and checks nothing else; no Dropbox app, OAuth flow or token is involved.

1. A request with no `Authorization` header, or with another scheme, answers 400 with a JSON string body, `Error in call to API function "…": request is malformed`, on `users/get_current_account`, and 401 `invalid_access_token` on the files and sharing routes. Dropbox answers 400 `text/plain` there (RYS-2663).
2. In `dropbox_team_shared` the token picks the member: `sl.rosa-desktop` is Rosa, `sl.toni-laptop` is Toni, and any other value is Rosa, the world's default caller. Every other world has one account, which every token reaches.

## Coverage

<!-- BEGIN generated: served-operations (go run ./ingest/cmd/twindocs dropbox; do not edit) -->

<!-- coverage-claim -->
93 routes across 17 resources, counted from the sim's literal registrations.

| Resource | Operations |
|---|---|
| Asynchronous job (7) | `POST /2/files/copy_batch/check_v2`; `POST /2/files/create_folder_batch/check`; `POST /2/files/delete_batch`; `POST /2/files/delete_batch/check`; `POST /2/files/move_batch/check_v2`; `POST /2/files/save_url`; `POST /2/files/save_url/check_job_status` |
| File request (7) | `POST /2/file_requests/count`; `POST /2/file_requests/create`; `POST /2/file_requests/delete`; `POST /2/file_requests/delete_all_closed`; `POST /2/file_requests/get`; `POST /2/file_requests/list`; `POST /2/file_requests/update` |
| File revision (content) (1) | `POST /2/files/restore` |
| File-request list cursor (2) | `POST /2/file_requests/list/continue`; `POST /2/file_requests/list_v2` |
| File/folder entry (23) | `POST /2/files/copy_batch_v2`; `POST /2/files/copy_v2`; `POST /2/files/create_folder_batch`; `POST /2/files/create_folder_v2`; `POST /2/files/delete_v2`; `GET, POST /2/files/download`; `GET, POST /2/files/download_zip`; `POST /2/files/get_metadata`; `GET, POST /2/files/get_preview`; `POST /2/files/get_temporary_link`; `GET, POST /2/files/get_thumbnail_v2`; `POST /2/files/list_revisions`; `POST /2/files/move_batch_v2`; `POST /2/files/move_v2`; `POST /2/files/tags/add`; `POST /2/files/tags/get`; `POST /2/files/tags/remove`; `POST /2/files/upload`; `POST /2/files/upload_session/finish`; `POST /2/sharing/get_file_metadata`; `POST /2/sharing/get_file_metadata/batch`; `POST /2/sharing/list_file_members/batch`; `POST /2/sharing/list_shared_links` |
| list_folder cursor (3) | `POST /2/files/list_folder`; `POST /2/files/list_folder/continue`; `POST /2/files/list_folder/get_latest_cursor` |
| Per-file sharing record (1) | `POST /2/sharing/update_file_policy` |
| Search cursor (2) | `POST /2/files/search/continue_v2`; `POST /2/files/search_v2` |
| Shared folder (3) | `POST /2/sharing/get_folder_metadata`; `POST /2/sharing/share_folder`; `POST /2/sharing/update_folder_policy` |
| Shared link (5) | `POST /2/sharing/create_shared_link_with_settings`; `GET, POST /2/sharing/get_shared_link_file`; `POST /2/sharing/get_shared_link_metadata`; `POST /2/sharing/modify_shared_link_settings`; `POST /2/sharing/revoke_shared_link` |
| Shared-file membership (5) | `POST /2/sharing/add_file_member`; `POST /2/sharing/relinquish_file_membership`; `POST /2/sharing/remove_file_member_2`; `POST /2/sharing/unshare_file`; `POST /2/sharing/update_file_member` |
| Shared-folder membership (5) | `POST /2/sharing/add_folder_member`; `POST /2/sharing/mount_folder`; `POST /2/sharing/relinquish_folder_membership`; `POST /2/sharing/unmount_folder`; `POST /2/sharing/update_folder_member` |
| Sharing asynchronous job (5) | `POST /2/sharing/check_job_status`; `POST /2/sharing/check_remove_member_job_status`; `POST /2/sharing/check_share_job_status`; `POST /2/sharing/remove_folder_member`; `POST /2/sharing/unshare_folder` |
| Sharing listing cursor (10) | `POST /2/sharing/list_file_members`; `POST /2/sharing/list_file_members/continue`; `POST /2/sharing/list_folder_members`; `POST /2/sharing/list_folder_members/continue`; `POST /2/sharing/list_folders`; `POST /2/sharing/list_folders/continue`; `POST /2/sharing/list_mountable_folders`; `POST /2/sharing/list_mountable_folders/continue`; `POST /2/sharing/list_received_files`; `POST /2/sharing/list_received_files/continue` |
| Space usage (1) | `POST /2/users/get_space_usage` |
| Upload session (5) | `POST /2/files/upload_session/append_batch`; `POST /2/files/upload_session/append_v2`; `POST /2/files/upload_session/finish_batch_v2`; `POST /2/files/upload_session/start`; `POST /2/files/upload_session/start_batch` |
| User account (3) | `POST /2/users/get_account`; `POST /2/users/get_account_batch`; `POST /2/users/get_current_account` |

<!-- END generated: served-operations -->

1. RPC routes take a JSON body under `Content-Type: application/json`; `users/get_current_account`, `users/get_space_usage`, `file_requests/list`, `count` and `delete_all_closed` take the literal `null`. The content routes — `files/upload`, `download`, `download_zip`, `get_thumbnail_v2`, `get_preview`, `upload_session/start`, `append_v2` and `finish`, and `sharing/get_shared_link_file` — take their argument in the `Dropbox-API-Arg` header beside the bytes and answer their result in the `Dropbox-API-Result` header. The download routes also take it as the `arg` query parameter (`GET /2/files/download?arg=<json>`), and `download` and `get_shared_link_file` answer a byte `Range` with 206 and `Content-Range`. `GET /_rystic/version` names both hosts in `service_hosts`.
2. Paths are matched case-insensitively and answered as `path_lower` beside the `path_display` you gave. A path that holds nothing answers 409 `path/not_found`; a create, or an add-mode upload, over an existing entry answers 409 `path/conflict`, and `autorename: true` lands the upload as `name (1)`. Every error carries Dropbox's `error` object and `error_summary`.
3. `delete_v2` is a soft delete: the entry no longer reads back by path, and `files/download` of it answers 409 `path/not_found`. `list_folder` with `include_deleted: true` lists its tombstone, and `get_metadata` with `include_deleted: true` answers it as `DeletedMetadata` (`.tag`, `name`, `path_lower`, `path_display`). A deleted file is still a file to `files/list_revisions`, which answers its revisions with `is_deleted: true` and `server_deleted`, and any of them restores through `files/restore`. Every upload, session finish, restore and move of a file records its own `rev`; `list_revisions` in `id` mode lists a file's revisions across renames, and `mode` takes the bare tag (`"id"`) as well as `{".tag": "id"}`.
4. `files/list_folder` pages by `limit` and the cursor `list_folder/continue` walks; `files/search_v2` matches the words of a name, case-insensitively, scoped by `options.path`, and pages through `search/continue_v2`. A cursor the twin never handed out answers 400.
5. `save_url`, `copy_batch_v2`, `move_batch_v2` and `delete_batch` answer an `async_job_id`; the check answers `in_progress` on the first read and `complete` on the next, or once the clock has moved two seconds past the launch, as Dropbox's do. Each entry of a copy, move or delete batch is copied, moved or deleted as `copy_v2`, `move_v2` and `delete_v2` do it: a copied file downloads its bytes under its own `rev`, a folder goes with everything beneath it, and a file's members and shared links follow it or go with it. `create_folder_batch` answers `complete` inline, and a completed `save_url` job has written its file at the job's path. An `upload_session/append_v2` whose `cursor.offset` differs from the bytes held answers 409 `incorrect_offset` with `correct_offset`; a second `finish` at the held offset commits the bytes again, and an append after finish answers 409 `closed`.
6. Each account holds its own files: in `dropbox_team_shared`, Toni's `/Drafts` answers `path/not_found` to Rosa, and what reaches her is the file Toni shared, through `sharing/list_received_files` and `sharing/get_file_metadata`.
7. The account that shares a folder is its owner: after `share_folder`, `list_folders` lists the share, `get_folder_metadata` and `list_folder_members` answer it with `access_type` owner, and `unshare_folder` launches its job.

Not modelled, by decision: the `team`, `paper`, `file_properties`, `contacts`, `account`, `auth`, `check` and `openid` families, longpoll and webhooks, the deprecated routes whose successors are served, and the Business-only `permanently_delete`. An unserved route answers 404 `{"message":"No route in twin for …"}`, not Dropbox's error.

## Control plane

Every route under `/_rystic/` is listed in [routes by plan](https://www.rystic.ai/docs/control-plane.md#routes-by-plan). The twin serves no longpoll and sends no webhook.

## Scenarios

Five shipped worlds, each on `2026-02-02T00:00:00Z`.

| Scenario | What it holds |
|---|---|
| `dropbox_personal_archive` | Ada Sealy's verified Pro account and a nine-entry tree with revs, hashes and sizes: `/Apps`, `/Documents`, `/notes.md`, `/Photos/2025` |
| `dropbox_fresh_signup` | a just-created Basic account, email unverified, holding only the auto-created `/Apps` |
| `dropbox_deleted_backlog` | four deleted files and a deleted folder across `/Archive` and `/Drafts`, listed as tombstones, beside the one file that survived; each deleted file lists its revision and restores |
| `dropbox_paired_identity` | a personal Basic account paired with a Northwind work account, both `is_paired`, each with its own namespace and tree |
| `dropbox_team_shared` | two Acme members with their own tokens (`sl.rosa-desktop`, `sl.toni-laptop`), a shared team folder tree beside one private file, and a timeline the clock plays: Toni creates `/Drafts`, uploads a sketch and shares it with Rosa, then Rosa files her scratch note and deletes `/Scratch` |

## Seeding state

`POST /_rystic/seed` takes a register map — `state_account` (keyed `account_id`), `state_entry` (keyed `path_lower`), `state_revision`, `state_shared_link`, `state_file_request`, `state_shared_folder`, `state_shared_folder_member`, `state_file_member` and the rest `GET /_rystic/registers` lists with their fields — and lands it directly. `--seed <file>` or `RYSTIC_SEED=<file>` boots one.

## Determinism & reset

Ids, revs, cursors, job ids and link URLs come from sequences the reset rewinds, so the first folder you create after a reload has the same `id:` every time. The clock starts at `2026-01-01T00:00:00Z` in a bare boot and at `2026-02-02T00:00:00Z` in every world, and stays there — never moving by wall time — until `POST /_rystic/clock` sets or advances it, so two identical runs from the same reset give byte-identical entries, timestamps included. `POST /_rystic/reset` wipes back to one Basic account with no files; `POST /_rystic/scenario` loads a world again without a restart.

## Egress

The twin calls nothing out: `save_url` fetches nothing (its completed job writes a placeholder derived from the URL), a shared link, a file request or a member added to a file sends no email, and there is no webhook. `GET /_rystic/egress` answers an empty log.

## Limitations

<!-- BEGIN generated: known-gaps (go run ./ingest/cmd/twindocs dropbox; do not edit) -->

**Known issues.**

- The scenario pack and one repair record describe calls the twin does not serve (RYS-2230)
- A request with no `Authorization` header answers a 400 or 401 JSON body where Dropbox answers 400 text/plain (RYS-2663)
- The twin's 500 answers, such as the one for a malformed upload-session id, are never compared against Dropbox's (RYS-2664)
- Dropbox's text/plain error bodies, such as the 400 for a malformed argument, are answered as JSON (RYS-2665)
- `files/tags/get` on a folder addressed by the `id` that `create_folder_v2` answered is unmeasured: the probe sends the id where Dropbox keys by path (RYS-2685)
- An upload or move into a missing parent folder creates no intermediate folders, where Dropbox creates them; create the folder with `create_folder_v2` first (RYS-2697)
- `list_folder/continue` is a positional window over the current listing, not Dropbox's change feed: a delete, a move's source, an overwrite and an entry that sorts earlier never reach a cursor, and already-listed entries are re-sent (RYS-2772)
- No file over 1 MiB can be stored: `files/upload` above 1 MiB answers 413 with a non-Dropbox body and an upload session's second MiB answers 409 `too_large`, where Dropbox accepts 150 MB (RYS-2773)
- A shared folder is not one namespace: a member's mount lists nothing and each member's writes stay invisible to the other; the worlds' seeded `shared_folder_id`s answer `invalid_id` and `list_folders` is empty; a shared folder and its children never carry `sharing_info` (RYS-2774)
- `move_v2` writes under a file, moves a tombstone and moves a folder onto `/` (it vanishes), refuses a move onto a deleted path with `to/conflict/other` and a case-only rename with `duplicated_or_nested_paths`; an upload with a trailing slash stores `name ""` (RYS-2776)
- `sharing/get_file_metadata/batch` and `list_file_members/batch` answer 500 when any entry is not a file, and a malformed cursor answers 500 on `file_requests/list/continue` and `list_received_files/continue` and is ignored by `list_shared_links`, where Dropbox answers per-entry errors and 409 (RYS-2777)
- `files/upload` ignores `strict_conflict`, mints a new `rev` for an identical-bytes overwrite where Dropbox keeps the rev, and accepts an unknown `mode` (RYS-2778)
- A request with a wrong `Content-Type` (form-urlencoded or text/plain on a JSON route, anything but octet-stream on an upload) is accepted where Dropbox answers 400, so a client that works on the twin is refused by Dropbox (RYS-2791)
- `create_shared_link_with_settings` with `requested_visibility: team_only` on an account with no team answers 200 with a public link; what Dropbox answers there is unmeasured (RYS-2806)
- A content `GET` that carries its token only in the `authorization` query parameter answers 401, and `Dropbox-API-Path-Root` and `ns:` paths are ignored, so every call reads the personal tree (RYS-2807)
- Two accounts' files at the same path share one set of file members and share records, so one owner's sharing shows on the other's file (RYS-2817)
- `add_folder_member` with no `access_level` adds an editor, where Dropbox adds a viewer; pass `access_level` explicitly (RYS-2818)
- `check_remove_member_job_status` words its warning differently from Dropbox, and relinquishing a mounted folder completes where Dropbox documents a `mounted` error (RYS-2819)
- In the `dropbox_personal_archive` world, seeded folders carry the file-only fields `is_downloadable`, `rev` and `size` (RYS-2820)

**Not served.** 195 of the 283 operations in the published spec are not served.

- files, 37 of 67 served: `POST /2/files/alpha/get_metadata`, `POST /2/files/alpha/upload`, `POST /2/files/copy`, `POST /2/files/copy_batch`, `POST /2/files/copy_batch/check`, `POST /2/files/copy_reference/get`, `POST /2/files/copy_reference/save`, `POST /2/files/create_folder`, … and 22 more
- sharing, 38 of 44 served: `POST /2/sharing/create_shared_link`, `POST /2/sharing/get_shared_links`, `POST /2/sharing/relinquish_access`, `POST /2/sharing/remove_file_member`, `POST /2/sharing/set_access_inheritance`, `POST /2/sharing/transfer_folder`
- users, 4 of 5 served: `POST /2/users/features/get_values`
- Not served at all: account (3), auth (2), check (2), contacts (2), file_properties (16), openid (1), paper (18), riviera (15), team (97), team_log (2)

**Not measured.** The fidelity suite runs 252 probes, and 466 documented branches have none. 149 need a credential the measuring account does not hold (401 ×80, 403 ×68, 409 ×1). The other 317:

| Operation | Status | Why |
|---|---:|---|
| `GET, POST /2/files/download` | 200 | nothing the twin serves creates the record it needs |
| `GET, POST /2/files/download` | 206 | nothing the twin serves creates the record it needs |
| `GET, POST /2/files/download` | 409 | the model describes no input that reaches it (2 branches) |
| `GET, POST /2/files/download` | 416 | the model describes no input that reaches it |
| `GET, POST /2/files/download` | 429 | no request a client sends provokes it |
| `GET, POST /2/files/download` | 500 | no request a client sends provokes it |
| `GET, POST /2/files/download_zip` | 200 | nothing the twin serves creates the record it needs |
| `GET, POST /2/files/download_zip` | 409 | the model describes no input that reaches it (3 branches) |
| `GET, POST /2/files/download_zip` | 429 | no request a client sends provokes it |
| `GET, POST /2/files/download_zip` | 500 | no request a client sends provokes it |
| `GET, POST /2/files/get_preview` | 200 | nothing the twin serves creates the record it needs |
| `GET, POST /2/files/get_preview` | 409 | a well-formed request does not provoke it |
| `GET, POST /2/files/get_preview` | 409 | the model describes no input that reaches it (4 branches) |
| `GET, POST /2/files/get_preview` | 429 | no request a client sends provokes it |
| `GET, POST /2/files/get_preview` | 500 | no request a client sends provokes it |
| `GET, POST /2/files/get_thumbnail_v2` | 200 | nothing the twin serves creates the record it needs |
| `GET, POST /2/files/get_thumbnail_v2` | 409 | nothing the twin serves creates the record it needs |
| `GET, POST /2/files/get_thumbnail_v2` | 409 | the model describes no input that reaches it (8 branches) |
| `GET, POST /2/files/get_thumbnail_v2` | 429 | no request a client sends provokes it |
| `GET, POST /2/files/get_thumbnail_v2` | 500 | no request a client sends provokes it |
| … and 225 more | | |

**Not modeled.** What the model's operations state they leave out:

- The 400 body is text/plain, not JSON; its exact wording is undocumented, so the branch is modelled shape-only and the response encoding is left as the default (a plain-text body on either side may read as an encoding gap rather than a value diff). (`GET, POST /2/files/download`)
- The 409 LookupError tags restricted_content, unsupported_content_type and locked, and DownloadError.other, are documented but have no derivable recipe on a fixture account; they are not given their own branches. (`GET, POST /2/files/download`)
- The deprecated rev argument field is declared in the header shape but no branch distinguishes its use from a rev: path. (`GET, POST /2/files/download`)
- This route is served from https://content.dropboxapi.com, not the api.dropboxapi.com host the state model records for the RPC routes; the host difference is recorded here but not expressed in the vector. (`GET, POST /2/files/download`)
- state_entry's documented producers in this model include create_folder_v2 (a folder) and copy/move; a success probe needs a FILE at the referenced path, which only files/upload creates. No prior-step recipe is declared because files/upload takes its own argument in a JSON-valued header with an opaque payload body, which x_requires_steps cannot express as a body here. (`GET, POST /2/files/download`)
- The 400 plain-text message for the sealed malformed-header recipe ('{not json') was measured live (round 20261010T112419Z) and sealed as the branch's example_body; messages for other malformed-header variants (absent header, wrong-typed path, pattern violation) follow the same 'Error in call to API function "files/download_zip": <decoder error>' format but their exact texts are unmeasured. (`GET, POST /2/files/download_zip`)
- The 500 InternalServerError branch has no documented body and no trigger condition, so it carries no example, no field policies and no probe recipe. (`GET, POST /2/files/download_zip`)
- The route also accepts GET with the same Dropbox-API-Arg header (x-rystic-accepted-methods); only POST is modeled. (`GET, POST /2/files/download_zip`)
- too_large and too_many_files carry no executable recipe: provisioning a 20 GB folder or 10,000 entries on the live account is out of reach. (`GET, POST /2/files/download_zip`)
- LookupError members malformed_path, restricted_content, unsupported_content_type and locked are reachable under PreviewError.path but are not modeled as separate branches: no evidence names a request that provokes them on this route. (`GET, POST /2/files/get_preview`)
- No fixture binding exists for an existing previewable file: the success example's Dropbox-API-Arg path is a ${path:json} placeholder resolved through the state_entry reference, which needs a producer that uploads a file with a supported extension (files/upload carries its own argument in a header and an opaque byte body, which x_requires_steps cannot express today). (`GET, POST /2/files/get_preview`)
- The deprecated PreviewArg.rev field is declared but carries no reference or guard: the spec directs callers to put the revision in `path` instead. (`GET, POST /2/files/get_preview`)
- The route also accepts GET (x-rystic-accepted-methods); only the POST form is modeled. (`GET, POST /2/files/get_preview`)
- path/not_file has no recipe: it needs an existing folder path, and no fixture-seeded folder is declared. (`GET, POST /2/files/get_preview`)
- unsupported_extension, unsupported_content and in_progress carry no executable recipe: each needs a file of a particular kind (or a preview mid-generation) staged in the live account, and no evidence names a value that provokes them against the fixture. (`GET, POST /2/files/get_preview`)
- No recipe for the 200 branch: reaching it needs a source file whose bytes are a real image of a supported extension, and the only producer on this surface (files/upload) takes its payload as opaque bytes in a request body with the path in a JSON-valued header, which x_requires_steps bodies cannot express. (`GET, POST /2/files/get_thumbnail_v2`)
- The 200 payload is a server-generated thumbnail image: a hermetic twin cannot reproduce the exact bytes, so the declared bytes comparison (length + SHA-256) will differ from the oracle's even on an otherwise correct answer. Recorded as a known fidelity gap rather than a weaker success claim. (`GET, POST /2/files/get_thumbnail_v2`)
- The 20MB photo limit is documented in prose only; which of unsupported_image / conversion_error it produces is not stated. (`GET, POST /2/files/get_thumbnail_v2`)
- The 400 body is text/plain (the response_encoding vocabulary has no text form); the branch is declared with no example_body, so only its status is modelled. (`GET, POST /2/files/get_thumbnail_v2`)
- The route also accepts GET (x-rystic-accepted-methods); only the POST form is modelled. (`GET, POST /2/files/get_thumbnail_v2`)
- … and 325 more

<!-- END generated: known-gaps -->

Sharing members are measured only as far as Dropbox's daily invite cap allows: Dropbox refuses `add_file_member` and `add_folder_member` with 409 `rate_limit` or `banned_member` once an account has sent its day's invites, so each round scores 19 member probes inconclusive rather than comparing them. Search matches names only, never a file's content; content matching is declared unmodeled. Thumbnails and previews are placeholder bytes answered as `application/octet-stream`, and a shared link, a temporary link or a file request's URL is a minted value its URL does not serve: `sharing/get_shared_link_file` answers a shared link's bytes. A shipped world's seeded files download their seeded bytes, and a file you upload downloads back byte for byte. The twin never answers 429.

Fidelity is scored differentially against a live Dropbox account, per response field: the last full-suite round matched **233 of 252 probes on 2026-10-13** (`convergence_log.json` `20261013T132744Z`), with 13 inconclusive — 12 sharing-member probes the daily invite cap leaves rate-limited and one search page Dropbox's index had not yet reached — against a target of 80%. Each release states its own number.

Release notes: [Changelog](https://www.rystic.ai/docs/changelog.md#dropbox)

---

Source: https://www.rystic.ai/docs/simulators/dropbox · Markdown: https://www.rystic.ai/docs/simulators/dropbox.md
