# API method changes — 2026-08-06 **For the mobile team.** Five endpoints changed from `POST` to `GET`. **One of them affects the mobile app**; the other four have no mobile caller. This is a breaking change with no compatibility shim — see *What breaks and when* below. --- ## Why Officer is growing a permission model. Members will get access to specific apps, and within an app the default is **read everything, write only your own data**. The rule that separates those two is the HTTP method: safe methods (`GET`, `HEAD`) are reads, everything else is a write. That only works if the method tells the truth. These five endpoints read something and returned it, while announcing themselves as writes — so under the new model a member would be denied a read they are entitled to, for no reason other than a habit in how the route was declared. Nothing else about them changed: same path, same response shape, same auth. --- ## The changes | # | Endpoint | Was | Now | Mobile affected | |---|----------|-----|-----|-----------------| | 1 | `/api/file-browser/video-info` | `POST` body `{url}` | `GET ?url=` | no | | 2 | `/api/file-browser/video-playlist` | `POST` body `{url}` | `GET ?url=` | no | | 3 | `/api/server-settings/ocr/models` | `POST` body `{url}` | `GET ?url=` | no | | 4 | `/api/transmission/_officer/port-test` | `POST` no body | `GET` | **YES** | | 5 | `/api/jellyfin/_config/:id/test` | `POST` body `{}` | `GET` | no | Note on #5: it already accepted `GET` as well as `POST`. It is now `GET` only, so that the method is a reliable signal rather than "whichever the caller felt like". --- ## What the mobile app has to change **One line.** `packages/core/src/services/transmission.ts:126` ```ts // before export const transmissionPortTest = () => request<{ open: boolean }>(`${T}/port-test`, { method: 'POST' }); // after export const transmissionPortTest = () => request<{ open: boolean }>(`${T}/port-test`); ``` The response is unchanged: `{ open: boolean }`. I searched `monorepo-mobile/packages` and `monorepo-mobile/apps` for callers of the other four and found none. If you know of one outside those trees, it needs the same treatment — path and response are identical, only the method and the location of `url` move. For the three that take a `url`, it moves from the JSON body to a query parameter and **must be percent-encoded**: ```ts `${base}/file-browser/video-info?url=${encodeURIComponent(url)}` ``` --- ## What breaks and when `port-test` returns **405** to a `POST` from the moment the platform is deployed. There is deliberately no transitional shim accepting both — the whole point of the change is that the method means something, and an endpoint answering to both methods means nothing. The blast radius is one button in the Transmission screen ("test peer port"). It does not affect torrents, downloads, or anything else in that app. If that is still unacceptable timing, a shim is a two-line change on the platform side — ask and it can go in, with a date for removal. --- ## What did NOT change, and will not Several endpoints look like the ones above but are staying `POST` **on purpose**. If you are tempted to "fix" them for consistency, please don't — both reasons below are deliberate. **They need a request body that `GET` cannot carry:** - `POST /api/chat/stt` — multipart audio upload - `POST /api/file-browser/tts`, `/tts-text`, `/ocr`, `/transcribe` — payloads to transform **They carry a credential, and a query string is the wrong place for one.** Query strings are written to access logs, shell history, proxy logs and `Referer` headers; request bodies are not: - `POST /api/server-settings/tts/voices` — takes an `apiKey` - `POST /api/server-settings/{smtp,tts,stt,ocr}/test` — take connection secrets - `POST /api/server-settings/local-providers/probe` — takes `{url, auth}` These are reads that must remain `POST`. The permission model handles them with an explicit annotation rather than by inferring from the method, which is why the method change stops at five endpoints instead of sweeping the whole API. Everything else that is `POST`/`PUT`/`PATCH`/`DELETE` genuinely mutates something and is unaffected. --- ## Verifying Against a running platform, with a valid token: ```bash # should be 200 curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" \ "$BASE/api/transmission/_officer/port-test" # should be 405 curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "Authorization: Bearer $TOKEN" \ "$BASE/api/transmission/_officer/port-test" ``` --- ## Platform-side changes, for reference Backend: - `src/servers/api/file-browser/router.ts` — `/video-info`, `/video-playlist` - `src/servers/api/server-settings/ocr.ts` — `/models` - `src/servers/sidecar/transmission/routes.ts` — `handlePortTest` - `src/servers/sidecar/jellyfin/config.ts` — the `test` action Web callers, already updated: - `src/workspaces/officerdev/src/hooks/useFilesAPI.ts` - `src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx` - `src/workspaces/officerdev/src/apps/Transmission/useTransmissionData.ts` - `src/workspaces/officerdev/src/apps/Jellyfin/useJellyfinData.ts` The sidecar contract comments at the top of `transmission/index.ts` and `jellyfin/index.ts` were updated to match.