# 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.
