API
Manage your Hermes engines and agents over HTTP — the same API the UI runs on.
API
Everything the Opereo AI UI does goes through this API — it is not a separate facade, so
what you can click you can curl. That is also what makes Opereo AI a control plane for
platforms that deploy and manage agents for their own users: one org-scoped key drives the
full lifecycle, and your users never need an Opereo AI account. Platform resources (projects, engines, templates, your
organization, API keys) have their own namespaces; engine operations are namespaced by
engine kind, and today that means Hermes:
/api/projects,/api/templates,/api/org,/api/keys, … — the platformGET /api/engines/{engineId}— what an engine is, whatever its kind/api/hermes/{engineId}/…— every Hermes operation
Authentication
Two credentials work on every endpoint:
- An API key — create one in Settings → API keys (org admins only). Keys are org-scoped: one key manages every project and engine the organization owns. The full key is shown once at creation and stored hashed.
- Your browser session — the app itself calls these endpoints with your Clerk session, so anything you can do signed in, the API does too.
curl -H "Authorization: Bearer fl_..." \
https://your-opereo.example/api/engines/$ENGINE_IDThe engine id is a UUID — it's in the URL (?engine=...) when you open an engine from the
sidebar.
Errors
Every failure is JSON: { "error": "message" } with a meaningful status —
400 invalid body or an engine kind that doesn't support the resource · 401 missing or
invalid credentials · 403 not yours to touch · 404 unknown resource (including ones
owned by another org) · 409 a name or address is already taken · 424 the engine or an
upstream service failed (box down, unknown profile, …).
Projects
A project groups engines. Creating one is instant and provisions nothing — add engines to
it with POST /api/hermes.
| method + path | body | returns |
|---|---|---|
GET /api/projects | — | the org's projects |
POST /api/projects | { name?, description? } — name defaults to a generated one | the project (with its slug) |
GET /api/projects/{projectId} | — | the project |
PATCH /api/projects/{projectId} | { name, slug, description? } | the updated project |
DELETE /api/projects/{projectId} | — | { ok } — tears down every engine in it |
Engines
| method + path | body | returns |
|---|---|---|
GET /api/projects/{projectId}/engines | — | [{ id, kind, name, isDefault, status, error, nodes }] |
GET /api/engines/{engineId} | — | the engine and its agents, for any kind |
POST /api/hermes | { projectId, name, type? } — type is hetzner (default), railway or upstash | { id } — returns immediately; provisioning continues in the background |
PATCH /api/engines/{engineId} | { name } | { ok } |
DELETE /api/engines/{engineId} | — | { ok } — tears down the box and every agent on it |
Engine creation is asynchronous. The row lands as status: "creating"; poll
GET /api/projects/{projectId}/engines until it is ready, or failed with the reason in
error. Hetzner takes ~2–3 min, Railway ~1 min, Upstash ~25 s.
Engine creation is kind-specific (POST /api/hermes); future engine kinds get their own
namespace root.
Agents
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles | — | [{ id, name, description }] |
POST /api/hermes/{e}/profiles | { name, description?, from? } — from clones an existing agent id | the created agent |
PATCH /api/hermes/{e}/profiles/{p} | { name, description } | { ok } |
DELETE /api/hermes/{e}/profiles/{p} | — | { ok } |
name is a free-text display name (1–64 characters), and description allows up to 500
characters. Creating or cloning generates a separate internal Hermes slug. Updating an
agent saves both fields together in profile.yaml; it does not rename its directory.
The returned id remains stable, including when the default profile's display name changes.
curl -X POST -H "Authorization: Bearer fl_..." -H "Content-Type: application/json" \
-d '{ "name": "researcher", "description": "Finds and summarizes sources" }' \
https://your-opereo.example/api/hermes/$E/profilesIdentity
The agent's SOUL — its standing instructions.
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles/{p}/identity | — | { instructions } |
PUT /api/hermes/{e}/profiles/{p}/identity | { instructions } | { ok } |
Chat & sessions
POST /api/hermes/{e}/chat?profile={p} streams a conversation turn (AI SDK UI-message
stream). Send { "messages": [...], "id": "<session id>" } — the id names the
engine-side session, created on first use and resumed forever after.
| method + path | returns |
|---|---|
GET /api/hermes/{e}/profiles/{p}/sessions | [{ id, title, updatedAt }] |
GET /api/hermes/{e}/profiles/{p}/sessions/{sessionId}/messages | the transcript as UI messages |
Models
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles/{p}/models | — | [{ id, name, provider, active }] |
PUT /api/hermes/{e}/profiles/{p}/models | { model, provider } | { ok } |
GET /api/hermes/{e}/profiles/{p}/reasoning | — | { effort } |
PUT /api/hermes/{e}/profiles/{p}/reasoning | { effort } | { ok } |
Providers
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles/{p}/providers | — | provider list with connection state |
PUT /api/hermes/{e}/profiles/{p}/providers/{slug} | { key } | { ok } — connect with an API key |
DELETE /api/hermes/{e}/profiles/{p}/providers/{slug} | — | { ok } |
POST /api/hermes/{e}/profiles/{p}/providers/{slug}/oauth | — | the oauth start (url + session) |
GET …/oauth?sessionId={s} | — | poll the pending oauth |
PUT …/oauth | { sessionId, code } | { ok } |
DELETE …/oauth | — | { ok } |
Environment
The agent's environment variables — the profile's .env on the box. Everything Hermes reads
from the environment lives here: provider keys, tool credentials, platform tokens.
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles/{p}/env | — | [{ name, isSet, redacted, description, category, isPassword, channelManaged, advanced }] |
PUT /api/hermes/{e}/profiles/{p}/env | { values: { KEY: "value", … } } | { ok } — writes every key, then restarts the agent's gateway once |
DELETE …/env/{key} | — | { ok } — restarts the gateway |
POST …/env/{key}/reveal | — | { key, value } — the plaintext |
Names are UPPER_SNAKE_CASE. The gateway reads its environment once at start, so writes and
deletes restart it — batch related keys into one PUT. channelManaged: true marks a
platform credential owned by a channel (TELEGRAM_BOT_TOKEN, API_SERVER_KEY, …); connect
those through Channels, which enables the platform alongside the value — a raw
env write sets the token but leaves the platform off.
Reveals are throttled and audit-logged by the box: five per 30 seconds per engine. A sixth
answers 424 with the box's message. Fine for a person, wrong for a loop.
curl -X PUT -H "Authorization: Bearer fl_..." -H "Content-Type: application/json" \
-d '{ "values": { "FIRECRAWL_API_KEY": "fc-…", "EXA_API_KEY": "…" } }' \
https://your-opereo.example/api/hermes/$E/profiles/$P/env
curl -X POST -H "Authorization: Bearer fl_..." \
https://your-opereo.example/api/hermes/$E/profiles/$P/env/FIRECRAWL_API_KEY/revealSkills
Skill names may contain slashes (category/name) and go in the path as-is.
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles/{p}/skills | — | installed skills with enabled, usage, source |
POST /api/hermes/{e}/profiles/{p}/skills | a template skill (below) | { ok } |
GET …/skills/{name} | — | { name, description, local, hub } |
PUT …/skills/{name} | { content } | { ok } |
PATCH …/skills/{name} | { enabled } | { ok } |
DELETE …/skills/{name} | — | { ok } |
Install body — local content or a hub reference:
{ "name": "greeting", "description": "Says hi properly",
"local": { "content": "---\nname: greeting\n---\n\nAlways greet warmly.", "files": [] },
"hub": null }The registry (search before you install):
| method + path | returns |
|---|---|
GET …/skill-registry?query={q} | registry search results |
GET …/skill-registry/preview?identifier={id} | the skill's full content |
GET …/skill-registry/scan?identifier={id} | the security scan verdict |
Channels
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles/{p}/channels | — | all platforms with connection state |
PUT …/channels/{channelId} | { values: { "ENV_VAR": "…" } } — the platform's credentials | { ok } |
DELETE …/channels/{channelId} | — | { ok } |
Automations
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles/{p}/automations | — | scheduled jobs |
POST …/automations | { id: null | "existing", name, schedule, prompt, deliver, skills } — create or update | { ok } |
PATCH …/automations/{id} | { enabled } | { ok } |
DELETE …/automations/{id} | — | { ok } |
Integrations
| method + path | returns |
|---|---|
GET /api/hermes/{e}/profiles/{p}/integrations | toolkits with connection state |
POST …/integrations/{toolkit} | { url } — an OAuth url to open, or null if connected immediately |
DELETE …/integrations/{toolkit} | { ok } |
Engine access
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/dashboard | — | { url, username, password } — the engine's own dashboard. As sensitive as your API key. |
POST /api/hermes/{e}/terminal | { profileId, attach? } | { url } — a one-shot websocket url for the agent's TUI |
POST /api/hermes/{e}/shell | the shell request the UI sends | streamed output |
Publishing & templates
An agent snapshots into a portable payload (identity, skills, automations, connected integrations and channels); publishing puts that payload on the marketplace; installing turns it back into a live agent.
| method + path | body | returns |
|---|---|---|
GET /api/hermes/{e}/profiles/{p}/snapshot | — | the agent's portable payload |
GET /api/hermes/{e}/profiles/{p}/listing | — | the agent's marketplace listing, or null |
PUT …/listing | { name, description, visibility, tags, payload } — visibility is public | org | private | { ok } — creates or versions the listing |
DELETE …/listing | — | { ok } — author only |
GET /api/templates?search=&tag=&page=&pageSize= | — | { items, pageCount, total } |
POST /api/templates/{templateId}/install | { projectId, engineId, name, description } | the created agent |
Automation templates
| method + path | returns |
|---|---|
GET /api/automation-templates | { templates, categories } — ready-made automation recipes |
Organization
Admin only — an API key counts as an admin.
| method + path | body | returns |
|---|---|---|
GET /api/org | — | the organization with members and invitations |
PATCH /api/org | { name, slug?, imageUrl? } — imageUrl: null removes the logo | { ok } |
PATCH /api/org/members/{userId} | { role } — admin | member | { ok } |
DELETE /api/org/members/{userId} | — | { ok } |
POST /api/org/invitations | { emailAddress, role } | { ok } |
DELETE /api/org/invitations/{invitationId} | — | { ok } |
API keys
Admin only. Only a hash is stored — the full key appears once, in the create response.
| method + path | body | returns |
|---|---|---|
GET /api/keys | — | the org's keys (prefix, name, last used — never the key) |
POST /api/keys | { name } | { key, keyPrefix } |
DELETE /api/keys/{apiKeyId} | — | { ok } |
Your profile
PATCH /api/me — { firstName, lastName, imageUrl? } → { ok }. Session only: an API
key is an organization credential, not a person, so this endpoint rejects it.