Box HTTP API

Last updated 2026-09-21

Everything below is served by the box itself, at the box's own origin (https://pb-1234.plumbox.me, or a LAN address). There is no Plum server in the path.

This page covers what a third-party app needs. The machine-readable spec is at GET /openapi.json (no auth) — but note it currently documents only auth, drive, photos, shares, quota and storage; /api/apps/*, /api/oauth/*, /api/events and /apps/{id}/svc/* are described here and not yet in that document.

Authentication

Credential How Where it works
Session cookie session=…, set by the box's own login the web shell, panels
Personal access token Authorization: Bearer plum_pat_… /api/* and /apps/{id}/svc/*
Basic Authorization: Basic base64(<username>:plum_pat_…) /apps/{id}/svc/* (the username must own the token)

An OAuth exchange hands you a PAT, so "access token" and "personal access token" are the same thing on a box.

Scopes: files:read, files:write, user:profile, service:call:<app_id>, plus apps:install and apps:dev for developer tooling, and the legacy read / write / admin. Enforcement is per request: read covers GET/HEAD/OPTIONS and write covers the rest; files:write implies files:read; admin covers everything but is not obtainable through OAuth. A user:profile token opens exactly GET /api/auth/me; a service:call:<id> token opens that app's /svc/* and GET /api/apps/<id>/entitlement and nothing else; apps:install and apps:dev tokens are confined to /api/apps/.

Tokens

Method Path Notes
POST /api/auth/tokens session only. {"name","scopes":[…],"expiresInDays":null} → the token once, as token
GET /api/auth/tokens list (prefixes only)
DELETE /api/auth/tokens/{id} revoke
GET /api/auth/me the signed-in user

OAuth

Method Path Auth Purpose
GET /authorize session the consent screen (the portal redirects the user here)
POST /api/oauth/authorize session approve; returns {"code","redirect_uri","state"}
POST /api/oauth/token none exchange the code for a token — PKCE is the proof
GET /api/oauth/clients/{client_id} session look up a registered client

code_challenge_method=S256 is required, codes live 5 minutes and are single-use, the only grant_type is authorization_code, and there are no refresh tokens. Details and the client rules are in Companion apps.

Apps

Method Path What
GET /api/apps installed apps (id, name, version, permissions, launch URL, update_available)
GET /api/apps/overview the same with sizes, author, service state, publisherKid, countersignKind
GET /api/apps/{id}/status service state: starting/ready/crashed/give_up/start_timeout/stopped, restarts, limits, memory_current, oom_kills
POST /api/apps/{id}/restart restart the service
GET /api/apps/{id}/logs[?follow=1] log snapshot, or SSE; a token needs apps:dev
GET /api/apps/{id}/entitlement {"skus":[{sku,kind,expires_at,active}],"refreshed_at","stale"}
POST /api/apps/entitlements/refresh refetch receipts now → {"ok":true,"receipts":n}
GET /api/apps/{id}/permissions · POST what the user has enabled, and toggling it
POST /api/apps/install[?app_id=…] install a signed .plu (multipart plu, or a raw zip body); needs apps:install
POST /api/apps/store/install install from the store by {"store_app_id","version"}
GET /api/apps/store/listing[?q=] the store catalog, annotated with what is installed
GET /api/apps/store/compatible the "works with Plum Box" shelf (companion-only apps)
POST /api/apps/{id}/update · PATCH /api/apps/{id}/settings update now; {"auto_update":bool}
DELETE /api/apps/{id} · POST /api/apps/{id}/uninstall remove
GET /api/apps/trust · PUT the box's trust level (owner)
GET /api/apps/publishers trusted publisher keys and the embedded store key ids
POST /api/apps/publishers/pair · /pair/confirm developer pairing (no auth: the 6-digit code the owner reads off the box is the proof)

There is no per-app client endpoint — GET /api/oauth/clients/{client_id} is the only client lookup.

Events

GET /api/events?kinds=drive,photos,apps:dev.jan.notes
Accept: text/event-stream

Server-sent events. kinds is a comma-separated filter (empty = everything) and accepts <prefix>:* wildcards such as apps:*. Reconnect with the Last-Event-ID header (or ?last_id=); if too much was missed the box sends event: reset and you should re-read rather than patch. Frames are id: <n> / event: <kind> / data: {"id","kind","at","payload"}, with a : ping comment every 25 seconds and retry: 3000. At most 256 concurrent listeners per box.

Kinds published today: drive and photos (after any successful write under /api/drive/ or /api/photos/, payload {"path","method"}), notification, and apps:<app_id> — both from a service's control socket and from the box itself on install ({"type":"installed"}).

An app's own service

ANY /apps/{app_id}/svc/{path}

Proxied to the app's service with the caller's identity in X-Plum-* headers. A PAT needs the exact service:call:<app_id> scope. See Service apps for the headers and the failure codes.

Error shapes

Three, depending on the layer:

{"error": "forbidden", "message": "token lacks the apps:dev scope"}   // /api/apps/*
{"error": "invalid_scope", "error_description": "…"}                   // OAuth
{"error": "app backend unavailable"}                                   // the svc proxy

Developer-facing tools should read error and show message / error_description.