Plugin Endpoints
REST endpoints that become available when the corresponding plugin is installed — auth, workflow, automation, views, realtime, notifications, AI, i18n, and file storage.
These REST endpoints are only available when the corresponding plugin is installed. Check the discovery manifest services map before calling them; all paths are relative to the base URL (defaults to /api/v1).
This page is a catalog of routes the platform serves for you — it is not about authoring an endpoint of your own. To publish your own URL as metadata, see Declarative Endpoints.
Authentication (Plugin Required)
These endpoints are only available when an auth plugin is installed. Check discovery.services.auth.enabled first.
POST /auth/sign-in/email
Authenticate with email and password (better-auth's email sign-in route, mounted under the auth plugin's /auth prefix) and receive a session token. There is no /auth/login route.
Body: { email: "admin@example.com", password: "secret" }
Response: { token: "jwt...", user: { ... } }
Plugin-Provided Service Endpoints
The following endpoints become available when the corresponding plugin is installed and registered with the kernel. Use the discovery services map to check availability.
Workflow (/workflow) — removed in v17
There is no workflow endpoint, and there is no workflow service slot. The
three routes documented here were declared in the API protocol and served by
nothing — no dispatcher handler, no plugin — so they 404'd for the whole life
of the declaration. The slot, the WorkflowProtocol methods behind it and the
discovery fields that reported it were all retired in v17 (#4451).
Use the live mechanisms instead: an object validation rule of type
state_machine for lifecycle transitions, an approval flow node for human
approval pauses (ADR-0019), and lifecycle hooks / record_change flows for
record-triggered automation.
Approve/reject were never workflow routes (ADR-0019): approval is a flow node, and decisions are recorded on the approvals runtime via POST /approvals/requests/:id/approve and POST /approvals/requests/:id/reject.
Automation (/automation) — Plugin Required
| Method | Endpoint | Description |
|---|---|---|
| POST | /automation/:name/trigger | Trigger an automation flow by name (legacy alias: /automation/trigger/:name). Failures answer real status codes, not a 200 wrapping an inner failure: 404 unknown flow, 409 FLOW_DISABLED, 422 FLOW_NO_START_NODE, 400 FLOW_FAILED for a run that ran and was rejected — see Run a flow via API |
The automation dispatcher also exposes flow CRUD (GET/POST /automation, GET/PUT/DELETE /automation/:name) and run observability/resume routes — see Durable pause & resume.
Views (/ui) — Plugin Required
Not yet mounted. The /ui/views CRUD routes are declared in the API protocol but no handler is registered for them, so they return 404 today (and the /api/v1/ui prefix is itself deprecated in favour of /api/v1/meta/view). The always-available view resolver is a separate route — see the note below.
| Method | Endpoint | Description |
|---|---|---|
| GET | /ui/views/:object | List views for an object |
| GET | /ui/views/:object/:viewId | Get a view definition |
| POST | /ui/views/:object | Create a new view |
| PATCH | /ui/views/:object/:viewId | Update a view |
| DELETE | /ui/views/:object/:viewId | Delete a view |
The auto-generated (non-CRUD) view resolver GET /ui/view/:object/:type is always available and provided by the kernel — see Metadata API. It is a separate route from the /ui/views CRUD above.
Realtime (/realtime) — Plugin Required
Not implemented. There is no realtime HTTP or WebSocket surface: @objectstack/service-realtime is an in-process pub/sub bus consumed via kernel.getService('realtime'), not over HTTP. The dispatcher has no /realtime branch, discovery deliberately omits the route (features.websockets is false), and the routes below would return 404 (ADR-0076 D12). They document the planned protocol shape only.
| Method | Endpoint | Description |
|---|---|---|
| POST | /realtime/connect | Establish WebSocket/SSE connection |
| POST | /realtime/disconnect | Close connection |
| POST | /realtime/subscribe | Subscribe to channel |
| POST | /realtime/unsubscribe | Unsubscribe |
| PUT | /realtime/presence/:channel | Set presence |
| GET | /realtime/presence/:channel | Get channel presence |
Notifications (/notifications) — Plugin Required
| Method | Endpoint | Description |
|---|---|---|
| GET | /notifications | List notifications (inbox) |
| POST | /notifications/read | Mark notifications as read (body: { ids: string[] }) |
| POST | /notifications/read/all | Mark all as read |
The core dispatcher implements only the list / read / read-all routes above. Device-registration and preference endpoints are provided by specific notification plugins, if any.
AI (/ai) — Plugin Required
These are the routes service-ai mounts, and the SDK method that reaches each:
| Method | Endpoint | SDK | Description |
|---|---|---|---|
| POST | /ai/chat | ai.chat / ai.chatStream | Chat completion — JSON with stream: false, otherwise the Vercel UI Message Stream |
| POST | /ai/chat/stream | — | Generic-SSE twin of /ai/chat: the same completion without the tool loop or persistence |
| POST | /ai/complete | ai.complete | Text completion |
| GET | /ai/models | ai.models | Models this environment offers (ADR-0028) |
| GET | /ai/status | — | Active adapter provenance (console diagnostics) |
| GET | /ai/effective-model | — | Resolved model ids and their source (console diagnostics) |
| POST / GET | /ai/conversations | ai.conversations.create / .list | Create / list conversations |
| GET / PATCH / DELETE | /ai/conversations/:id | ai.conversations.get / .update / .delete | Read / update / delete |
| POST | /ai/conversations/:id/messages | ai.conversations.addMessage | Append a message |
This table used to be inverted (#3718). It listed /ai/nlq,
/ai/suggest and /ai/insights — none of which any plugin has ever mounted —
and its callout stated "there is no /ai/chat route", which was wrong.
The three phantom routes were declared in DEFAULT_AI_ROUTES and called by
client.ai.nlq/suggest/insights; every call 404ed. v17 removed that SDK
namespace rather than build endpoints for it, and the same issue then gave
the SDK the surface that does exist — the ai.* column above.
Reviewed dispositions for all 12 routes live in the cloud repo,
packages/service-ai/src/ai-route-ledger.ts, whose conformance test reads
buildAIRoutes() and drives the SDK against it. The three rows with no SDK
method are deliberate: /status and /effective-model are operator
diagnostics, and /chat/stream is superseded by /chat's streaming mode.
For a React chat UI prefer useChat() (@ai-sdk/react) — it speaks the same
protocol ai.chatStream parses and owns message state for you.
i18n (/i18n) — Plugin Required
| Method | Endpoint | Description |
|---|---|---|
| GET | /i18n/locales | List available locales |
| GET | /i18n/translations/:locale | Get translation bundle |
| GET | /i18n/labels/:object/:locale | Get field labels |
File Storage (/storage) — Plugin Required
Provided by @objectstack/service-storage, which registers these routes on the
host HTTP server. Uploads are a three-step protocol — ask for a presigned
target, send the bytes straight to it, then commit — so the bytes never proxy
through the API server, and the same shape works for S3, local disk, or any
other adapter.
| Method | Endpoint | SDK | Description |
|---|---|---|---|
| POST | /storage/upload/presigned | storage.getPresignedUrl | Step 1 — mint an upload target for a new sys_file row |
| POST | /storage/upload/complete | storage.upload | Step 3 — commit the uploaded bytes |
| POST | /storage/upload/chunked | storage.initChunkedUpload | Open a chunked / resumable session |
| PUT | /storage/upload/chunked/:uploadId/chunk/:chunkIndex | storage.uploadPart | Send one chunk |
| POST | /storage/upload/chunked/:uploadId/complete | storage.completeChunkedUpload | Assemble the parts |
| GET | /storage/upload/chunked/:uploadId/progress | storage.resumeUpload | Read session progress (first step of a resume) |
| GET | /storage/files/:fileId/url | storage.getDownloadUrl | Resolve a short-lived signed download URL |
| GET | /storage/files/:fileId | — | Stable browser URL; 302s to the same signed URL (what file fields carry) |
storage.upload(file) runs steps 1–3 for you, including the direct-to-storage
PUT in step 2.
This table used to list two routes that never worked (#4087). POST /storage/upload and GET /storage/file/:id were a dispatcher-side bridge to
the file-storage service, written against a service shape that does not
exist: it called upload(key, data, options?) as upload(file, { request }) —
a TypeError against every implementation in the repo — and read the Buffer
that download(key) resolves as if it were a { url | stream | mimeType }
descriptor. They stayed mounted and reachable — nothing shadowed them — so
anyone who followed this table got a 500; everyone who followed the SDK used
the protocol above and never touched them.
Both routes are retired. /storage is service-storage's surface — install that
package to get it. Discovery advertises the route only when the occupant of the
storage slot (spelled file-storage before #9683; that spelling stays a
deprecated v17 alias) actually mounts HTTP handlers, so an in-memory dev
implementation now reports handlerReady: false and no routes.storage
instead of pointing at a path with nothing behind it.
See also
- API Overview — discovery, error handling, and protocol types
- Client SDK — service-aware TypeScript client that adapts to installed plugins
- Declarative Endpoints — declaring an endpoint of your own (
apis:), the other half of this page's name