> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Dropbox

> Client-mode REST API connectors for Dropbox: upload shift reports, batch records and alarm snapshots from MQTT as files, download recipes and configs, watch folders for new files with a cursor and longpoll, search, copy, move to archive, restore revisions, save URLs, tag files, and share them with temporary links, shared links and file requests

Technical Preview

## Overview

Client-mode connectors for the [Dropbox API v2](https://docs.dropboxapi.com/dropbox-api/docs/overview) that move plant files between the broker and Dropbox. A typical flow writes a shift report, batch record or alarm snapshot as a JSON or CSV file into a plant folder, reads a recipe or configuration file back when a line changes over, watches an inbox folder for files that people drop in (cursor plus longpoll, no polling loop), moves processed files to an archive folder, and hands out a temporary link or a shared link to a report. File requests let technicians and suppliers upload photos, certificates and PDFs into a plant folder without a Dropbox account.

Dropbox splits its API over three hosts with different request formats, so the template file `DropboxRestApi.lot` holds three connectors:

| Connector | Base address | Format | Events |
| - | - | - | - |
| `DropboxRestApi` | `https://api.dropboxapi.com` | RPC: `POST`, JSON argument in the body, JSON result. Also `/oauth2/token`. | 34 |
| `DropboxContentRestApi` | `https://content.dropboxapi.com` | Upload and download: JSON argument in the `Dropbox-API-Arg` header, file bytes in the body | 3 |
| `DropboxNotifyRestApi` | `https://notify.dropboxapi.com` | `files/list_folder/longpoll` only, without an `Authorization` header | 1 |

The connectors carry 38 events (36 POST, 2 GET; 13 writes, no delete). Topics use `storage/dropbox/<object>/<verb>`; each response is published on `<source topic>/response`.

> [!NOTE]
>
> The template was checked against the [Dropbox Stone API spec](https://github.com/dropbox/dropbox-api-spec) (host, request style, argument fields, required fields, deprecation and latest version of every function) and against a local mock of all three hosts. Without a token, Dropbox answered every packaged function with `400 Error in call to API function "<function>": Invalid authorization value`, which names the function, while the same path with a suffix answered `404`. With a dummy token every Bearer event answered `401 invalid_access_token`. `files/list_folder/longpoll` and `/oauth2/token` were also called live and rejected the dummy cursor and app credentials. Nothing has run with a real token yet, so the status stays blocked until an authenticated read and an upload pass.

## Setup

1. In the [App Console](https://www.dropbox.com/developers/apps), create a **Scoped access** app. Pick **App folder** to keep the broker inside `/Apps/<app name>/`, or **Full Dropbox** if it must reach existing team folders. Paths in payloads are relative to that root.
2. Under **Permissions**, enable the scopes the events need: `account_info.read`, `files.metadata.read`, `files.metadata.write`, `files.content.read`, `files.content.write`, `sharing.read`, `sharing.write`, `file_requests.read`, `file_requests.write`. Leave out the ones you do not use.
3. Authorise the app once with `token_access_type=offline` (for example `https://www.dropbox.com/oauth2/authorize?client_id=<APP_KEY>&token_access_type=offline&response_type=code`) and exchange the code at `/oauth2/token`. Keep the refresh token, app key and app secret in broker secrets (for example `DROPBOX_REFRESH_TOKEN`, `DROPBOX_APP_KEY`, `DROPBOX_APP_SECRET`). For a quick test, **Generate access token** on the app's settings page gives a short-lived token.
4. Keep the current access token in a broker secret (for example `DROPBOX_ACCESS_TOKEN`) and inject it in a LoT action with `GET SECRET`. Never retain topics that carry it.

## Authentication

Every event on `DropboxRestApi` and `DropboxContentRestApi` sends `Authorization: Bearer <token>` built from the `access_token` field of the payload:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "YOUR_DROPBOX_ACCESS_TOKEN"}
```

Dropbox access tokens are short-lived: `/oauth2/token` returns `expires_in` 14400 (4 hours). Refresh the token before it expires with `OAuthTokenRefresh` (`storage/dropbox/oauth/token/refresh`). That event sends no `Authorization` header; the app key and secret travel in the form:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"form": "grant_type=refresh_token&refresh_token=<DROPBOX_REFRESH_TOKEN>&client_id=<DROPBOX_APP_KEY>&client_secret=<DROPBOX_APP_SECRET>"}
```

> [!WARNING]
>
> The `OAuthTokenRefresh` response carries the new `access_token`, and the request carries the app secret. Restrict `storage/dropbox/oauth/#` with ACLs, never retain those topics, and store the new token in the secret instead of passing it around on open topics.

`FolderLongpoll` takes no token at all. Dropbox answers `400` if a longpoll request includes an `Authorization` header, so `DropboxNotifyRestApi` sends none.

## Payloads

| Connector | Fields |
| - | - |
| `DropboxRestApi` | `access_token` and `body` (JSON object, the function's argument). `AccountGetCurrent` and `SpaceUsageGet` take no argument (the connector sends JSON `null`). `OAuthTokenRefresh` takes `form` only. |
| `DropboxContentRestApi` | `access_token`, `arg` (JSON object sent in the `Dropbox-API-Arg` header) and, for `FileUpload`, `content` (the file body). |
| `DropboxNotifyRestApi` | `body` with `cursor` and `timeout` (30 to 480 seconds). |

Paths are relative to the app folder or the Dropbox root. The root is `""`; every other path starts with `/` and never ends with `/`. File ids (`id:...`) work wherever the reference accepts a path or id. The `Dropbox-API-Arg` header must be HTTP-header-safe: escape non-ASCII characters and `0x7F` as `\uXXXX` (a standard JSON encoder with ASCII output does this).

## Examples

Write a shift report as a JSON file (`storage/dropbox/files/upload`):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<token>", "arg": {"path": "/Coreflux/Porto/line-2/2026-10-05-shift-A.json", "mode": "add", "autorename": true, "mute": true}, "content": {"line": "2", "shift": "A", "oee": 0.87, "scrap": 14}}
```

Dropbox answers with the file metadata (`id`, `rev`, `size`, `content_hash`). Use `"mode": "overwrite"` to replace a file, or `{".tag": "update", "update": "<rev>"}` to replace it only if nobody changed it since that revision.

Read a recipe back on changeover (`storage/dropbox/files/download`):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<token>", "arg": {"path": "/Coreflux/Porto/line-2/recipe.json"}}
```

The response body is the file content. Dropbox sends the file metadata in the `Dropbox-API-Result` response header. Keep downloads to text and JSON files that fit an MQTT message.

Watch an inbox folder for new files without polling:

1. Get a cursor for future changes (`storage/dropbox/folders/latest-cursor/get`):

   ```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
   {"access_token": "<token>", "body": {"path": "/Coreflux/Porto/inbox", "recursive": true}}
   ```

2. Wait for a change (`storage/dropbox/folders/longpoll`, no token). The call blocks for up to `timeout` seconds plus up to 90 seconds of jitter, then returns `{"changes": true}` or `{"changes": false}`, sometimes with `backoff` (seconds to wait before the next longpoll):

   ```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
   {"body": {"cursor": "<cursor>", "timeout": 120}}
   ```

3. When `changes` is true, list them (`storage/dropbox/folders/list/continue`) and store the new cursor from the response:

   ```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
   {"access_token": "<token>", "body": {"cursor": "<cursor>"}}
   ```

A `409` with `reset` means the cursor expired. Start again with `FolderList` or `FolderLatestCursorGet`.

Archive a processed file instead of deleting it (`storage/dropbox/files/move`):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<token>", "body": {"from_path": "/Coreflux/Porto/inbox/report.csv", "to_path": "/Coreflux/Porto/archive/2026-10/report.csv", "autorename": true}}
```

Hand a firmware file to an edge device with a direct link that expires after four hours (`storage/dropbox/files/temporary-link/get`):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<token>", "body": {"path": "/Coreflux/firmware/gw-2.4.1.bin"}}
```

Ask a technician for photos of a failed bearing (`storage/dropbox/file-requests/create`):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<token>", "body": {"title": "Press 7 bearing photos", "destination": "/Coreflux/Porto/maintenance/press-7", "open": true, "description": "Work order WO-4711"}}
```

## Events

| Event | Connector | Method | Endpoint | Source topic | Write | Notes |
| - | - | - | - | - | - | - |
| `CheckUser` | API | `POST` | `/2/check/user` | `storage/dropbox/connection/check` | | Smoke: Dropbox echoes query back if the access token is valid (preview endpoint, no side effects). body: `{"query":"porto-line-2"}`. |
| `AccountGetCurrent` | API | `POST` | `/2/users/get_current_account` | `storage/dropbox/account/get-current` | | The account behind the token (account\_id, email, name, team, root\_info.root\_namespace\_id). No argument: the connector sends JSON null. |
| `SpaceUsageGet` | API | `POST` | `/2/users/get_space_usage` | `storage/dropbox/account/space-usage/get` | | Used and allocated space (individual or team allocation), e.g. to alarm before uploads start failing. No argument. |
| `AccountGet` | API | `POST` | `/2/users/get_account` | `storage/dropbox/accounts/get` | | Another account by id, e.g. who modified a file (sharing\_info.modified\_by). body: `{"account_id":"dbid:AAH4f99T0taONIb-OurWxbNQ6ywGRopQngc"}`. |
| `FolderList` | API | `POST` | `/2/files/list_folder` | `storage/dropbox/folders/list` | | Catalog 'POST files/list\_folder'. First page of a folder plus a cursor (keep it: it also returns later changes). body: `{"path":"/Coreflux/Porto/line-2","recursive":false,"limit":200}` ("" = root). |
| `FolderListContinue` | API | `POST` | `/2/files/list_folder/continue` | `storage/dropbox/folders/list/continue` | | Next page, or the changes since the cursor was issued (new / changed files, deleted entries). Always store the new cursor; 409 reset means start again with FolderList. body: `{"cursor":"AAH4f..."}`. |
| `FolderLatestCursorGet` | API | `POST` | `/2/files/list_folder/get_latest_cursor` | `storage/dropbox/folders/latest-cursor/get` | | A cursor for 'changes from now on' without listing the folder first (then FolderLongpoll + FolderListContinue). body: same as FolderList, e.g. `{"path":"/Coreflux/Porto/inbox","recursive":true}`. |
| `FolderCreate` | API | `POST` | `/2/files/create_folder_v2` | `storage/dropbox/folders/create` | Yes | Create a folder, e.g. one per line, batch or day. body: `{"path":"/Coreflux/Porto/line-2/2026-10-05","autorename":false}`. 409 path/conflict/folder if it exists. |
| `FolderCreateBatch` | API | `POST` | `/2/files/create_folder_batch` | `storage/dropbox/folders/create-batch` | Yes | Create up to 10000 folders in one call (e.g. a folder tree for a new plant). body: `{"paths":["/Coreflux/Porto/line-1","/Coreflux/Porto/line-2"],"autorename":false,"force_async":false}`. May return async\_job\_id; poll FolderCreateBatchCheck. |
| `FolderCreateBatchCheck` | API | `POST` | `/2/files/create_folder_batch/check` | `storage/dropbox/folders/create-batch/check` | | Status of an async folder batch. body: `{"async_job_id":"<id from FolderCreateBatch>"}`. |
| `MetadataGet` | API | `POST` | `/2/files/get_metadata` | `storage/dropbox/files/metadata/get` | | Metadata of one file or folder (id, rev, size, server\_modified, content\_hash). body: `{"path":"/Coreflux/Porto/line-2/recipe.json","include_deleted":false}` (path or "id:..."). |
| `Search` | API | `POST` | `/2/files/search_v2` | `storage/dropbox/files/search` | | Search names (and content on paid plans) under a folder. body: `{"query":"press-7","options":{"path":"/Coreflux/Porto","max_results":50,"file_extensions":["pdf","csv"],"file_status":"active"}}`. |
| `SearchContinue` | API | `POST` | `/2/files/search/continue_v2` | `storage/dropbox/files/search/continue` | | Next page of a search (has\_more true). body: `{"cursor":"<cursor from Search>"}`. |
| `FileCopy` | API | `POST` | `/2/files/copy_v2` | `storage/dropbox/files/copy` | Yes | Copy a file or folder, e.g. a recipe template into a new batch folder. body: `{"from_path":"/Coreflux/templates/batch.json","to_path":"/Coreflux/Porto/line-2/batch-1005-A.json","autorename":true}`. |
| `FileMove` | API | `POST` | `/2/files/move_v2` | `storage/dropbox/files/move` | Yes | Move or rename a file or folder; archive processed files instead of deleting them. body: `{"from_path":"/Coreflux/Porto/inbox/report.csv","to_path":"/Coreflux/Porto/archive/2026-10/report.csv","autorename":true}`. |
| `CopyBatch` | API | `POST` | `/2/files/copy_batch_v2` | `storage/dropbox/files/copy-batch` | Yes | Copy up to 1000 entries in one async job. body: `{"entries":[{"from_path":"/Coreflux/templates/a.json","to_path":"/Coreflux/Porto/line-2/a.json"}],"autorename":true}`. Poll CopyBatchCheck with the async\_job\_id. |
| `CopyBatchCheck` | API | `POST` | `/2/files/copy_batch/check_v2` | `storage/dropbox/files/copy-batch/check` | | Status of an async copy batch (in\_progress / complete with per-entry results). body: `{"async_job_id":"<id>"}`. |
| `MoveBatch` | API | `POST` | `/2/files/move_batch_v2` | `storage/dropbox/files/move-batch` | Yes | Move up to 1000 entries in one async job (e.g. archive a whole shift). body: `{"entries":[{"from_path":"/Coreflux/Porto/inbox/a.csv","to_path":"/Coreflux/Porto/archive/a.csv"}],"autorename":true}`. Poll MoveBatchCheck. |
| `MoveBatchCheck` | API | `POST` | `/2/files/move_batch/check_v2` | `storage/dropbox/files/move-batch/check` | | Status of an async move batch. body: `{"async_job_id":"<id>"}`. |
| `TemporaryLinkGet` | API | `POST` | `/2/files/get_temporary_link` | `storage/dropbox/files/temporary-link/get` | | A direct download link valid for 4 hours (e.g. hand a firmware or recipe file to an edge device over HTTP). body: `{"path":"/Coreflux/firmware/gw-2.4.1.bin"}`. |
| `TemporaryUploadLinkGet` | API | `POST` | `/2/files/get_temporary_upload_link` | `storage/dropbox/files/temporary-upload-link/get` | | A one-time upload link (60..14400 s) so a gateway or camera can POST a file directly (Content-Type application/octet-stream) without a token. body: `{"commit_info":{"path":"/Coreflux/Porto/cam-3/2026-10-05T14-20.jpg","mode":"add","autorename":true},"duration":3600}`. No file changes until the link is used. |
| `RevisionsList` | API | `POST` | `/2/files/list_revisions` | `storage/dropbox/files/revisions/list` | | Revisions of a file (who and when changed a recipe or config). body: `{"path":"/Coreflux/Porto/line-2/recipe.json","mode":"path","limit":10}`. |
| `FileRestore` | API | `POST` | `/2/files/restore` | `storage/dropbox/files/restore` | Yes | Restore a file to an earlier revision (roll back a bad recipe or config). body: `{"path":"/Coreflux/Porto/line-2/recipe.json","rev":"a1c10ce0dd78"}` (rev from RevisionsList). |
| `SaveUrl` | API | `POST` | `/2/files/save_url` | `storage/dropbox/files/save-url` | Yes | Dropbox downloads a public URL into a file (e.g. a nightly CSV export or a camera snapshot URL); the transfer must finish within 15 minutes. body: `{"path":"/Coreflux/Porto/exports/2026-10-05-oee.csv","url":"https://broker.example.com/exports/oee.csv"}`. Poll SaveUrlCheck with async\_job\_id. |
| `SaveUrlCheck` | API | `POST` | `/2/files/save_url/check_job_status` | `storage/dropbox/files/save-url/check` | | Status of a save\_url job (in\_progress / complete with metadata / failed). body: `{"async_job_id":"<id from SaveUrl>"}`. |
| `TagsAdd` | API | `POST` | `/2/files/tags/add` | `storage/dropbox/files/tags/add` | Yes | Tag a file or folder (preview endpoint; tag\_text is 1..32 word characters, lower-cased by Dropbox, at most 20 tags per item). body: `{"path":"/Coreflux/Porto/line-2/batch-1005-A.json","tag_text":"qa_released"}`. |
| `TagsGet` | API | `POST` | `/2/files/tags/get` | `storage/dropbox/files/tags/get` | | Tags on files or folders (preview endpoint). body: `{"paths":["/Coreflux/Porto/line-2/batch-1005-A.json"]}`. |
| `SharedLinkCreate` | API | `POST` | `/2/sharing/create_shared_link_with_settings` | `storage/dropbox/shared-links/create` | Yes | Create a shared link to a report or folder (409 shared\_link\_already\_exists returns the existing link's metadata). body: `{"path":"/Coreflux/Porto/reports/2026-W41.pdf","settings":{"audience":"team","access":"viewer","allow_download":true}}` (team audience needs a team account; basic users cannot set expires). |
| `SharedLinksList` | API | `POST` | `/2/sharing/list_shared_links` | `storage/dropbox/shared-links/list` | | Shared links of a path, or all links of the account (cursor paging). body: `{"path":"/Coreflux/Porto/reports/2026-W41.pdf","direct_only":true}` or `{}` or `{"cursor":"..."}`. |
| `SharedLinkMetadataGet` | API | `POST` | `/2/sharing/get_shared_link_metadata` | `storage/dropbox/shared-links/metadata/get` | | Metadata behind a shared link (name, size, link\_permissions, expires). body: `{"url":"https://www.dropbox.com/scl/fi/abc123/report.pdf?rlkey=xyz&dl=0"}`. |
| `SharedLinkSettingsUpdate` | API | `POST` | `/2/sharing/modify_shared_link_settings` | `storage/dropbox/shared-links/settings/update` | Yes | Change who can open a shared link or let it expire (instead of revoking it). body: `{"url":"https://www.dropbox.com/scl/fi/abc123/report.pdf?rlkey=xyz&dl=0","settings":{"audience":"no_one"},"remove_expiration":false}`. |
| `FileRequestCreate` | API | `POST` | `/2/file_requests/create` | `storage/dropbox/file-requests/create` | Yes | Open a file request so technicians or suppliers upload photos, certificates or PDFs into a plant folder without an account. body: `{"title":"Press 7 bearing photos","destination":"/Coreflux/Porto/maintenance/press-7","open":true,"description":"Work order WO-4711"}` (deadline only on Professional and Business accounts). |
| `FileRequestsList` | API | `POST` | `/2/file_requests/list_v2` | `storage/dropbox/file-requests/list` | | File requests of the account (id, url, destination, file\_count, is\_open). body: `{"limit":100}`. |
| `OAuthTokenRefresh` | API | `POST` | `/oauth2/token` | `storage/dropbox/oauth/token/refresh` | | Exchange the long-lived refresh token for a new short-lived access token (expires\_in 14400 s). No Authorization header; app key and secret go in the form. form: `grant_type=refresh_token&refresh_token=<DROPBOX_REFRESH_TOKEN>&client_id=<DROPBOX_APP_KEY>&client_secret=<DROPBOX_APP_SECRET>`. The response carries access\_token: never retain it, restrict storage/dropbox/oauth/# with ACLs. |
| `FileUpload` | Content | `POST` | `/2/files/upload` | `storage/dropbox/files/upload` | Yes | Catalog 'POST files/upload'. Write the MQTT payload's content as a file (JSON record, CSV, text, up to 150 MiB by spec; keep MQTT payloads small). arg: `{"path":"/Coreflux/Porto/line-2/2026-10-05-shift-A.json","mode":"add","autorename":true,"mute":true}` (or "mode":"overwrite", or `{".tag":"update","update":"<rev>"}` for safe edits). content: the file body. Returns FileMetadata (id, rev, size, content\_hash). |
| `FileDownload` | Content | `GET` | `/2/files/download` | `storage/dropbox/files/download` | | Read a file back (recipe, config, setpoint table). The response body is the file content; the metadata is in the Dropbox-API-Result response header. arg: `{"path":"/Coreflux/Porto/line-2/recipe.json"}` (optionally "rev"). Text and JSON files only through MQTT. |
| `SharedLinkFileGet` | Content | `GET` | `/2/sharing/get_shared_link_file` | `storage/dropbox/shared-links/file/get` | | Read a file through a shared link (e.g. a supplier's spec sheet shared with the plant). arg: `{"url":"https://www.dropbox.com/scl/fi/abc123/spec.json?rlkey=xyz&dl=0"}` (add "path" for a file inside a shared folder link). |
| `FolderLongpoll` | Notify | `POST` | `/2/files/list_folder/longpoll` | `storage/dropbox/folders/longpoll` | | Wait for changes under a cursor without polling: blocks up to timeout (30..480 s, plus up to 90 s jitter) and returns `{"changes":true\|false,"backoff":<s>}`; on true call FolderListContinue. No Authorization header (Dropbox answers 400 if one is sent). body: `{"cursor":"<cursor from FolderList / FolderLatestCursorGet>","timeout":120}`. Connector timeout is 600 s. |

## Limits

* Dropbox rate-limits per linked user (per team for team-linked apps) and does not publish the numbers. Above the limit it answers `429` with a `Retry-After` header. Wait that long, and batch work (`FolderCreateBatch`, `CopyBatch`, `MoveBatch`) instead of sending many single calls.
* `FileUpload` accepts up to 150 MiB per file, but keep MQTT payloads small. Larger files need upload sessions, which are not packaged.
* On Dropbox Business plans with a data transport limit, upload and download calls count against the team's monthly allowance. Over the limit Dropbox answers `403` `invalid_account_type/feature`.
* `FolderList` returns at most 2000 entries per page; follow `has_more` with `FolderListContinue`. `FolderLongpoll` accepts a `timeout` from 30 to 480 seconds, and the notify connector allows 600 seconds per request.
* Temporary links expire after four hours. Temporary upload links last 60 to 14400 seconds. `SaveUrl` transfers must finish within 15 minutes.

## Not packaged

* Delete-style functions on purpose: `files/delete_v2`, `delete_batch`, `permanently_delete`, `files/tags/remove`, shared link revoke and file request delete. Archive files with `FileMove` instead.
* Upload sessions (`upload_session/start`, `append_v2`, `finish`, batch variants) for files over 150 MiB, and binary downloads such as thumbnails, previews, `download_zip` and `export`.
* Team (Business) endpoints and the `Dropbox-API-Select-User`, `Dropbox-API-Select-Admin` and `Dropbox-API-Path-Root` headers for team spaces. Shared folder membership, Paper, file properties templates, file locking, contacts and account administration.
* Webhooks. Dropbox registers webhook URIs in the App Console, not through the API. A receiver would be a separate `ENABLE_SERVER` REST\_API connector that answers the `GET` verification by echoing `challenge` within ten seconds and checks `X-Dropbox-Signature` (HMAC-SHA256 of the body with the app secret). Notifications only carry account ids, so the connector would then call `FolderListContinue`. `FolderLongpoll` covers the same need without a public endpoint.

## Suggested branches

`feat/rest-api-route-dropbox` for the broker template and `feat/docs-rest-api-dropbox` for this page.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.