API & webhooks
OtoDock speaks HTTP, and the reason to reach for it is webhooks — letting an outside system kick off your agents when something happens. This page covers how to authenticate and how to wire incoming events to your agents.
Authentication with API keys
External requests authenticate with an API key. Any user creates one under User Settings → API Keys; it fires that user's own triggers, and it's shown only once — copy it when you create it. Keys are scoped to firing triggers — they're the credential for your webhooks, not a general key to the dashboard. Send it as a bearer token:
Authorization: Bearer <your-api-key>
Revoke a key any time from the same page. For a shared agent's triggers, use a key scoped to that agent instead — an agent manager creates those on the same page.
Webhook endpoints live under /v1 at your install's address (for example
https://otodock.example.com/v1/...), and responses are JSON.
Incoming webhooks
The headline capability: give an external system a URL, and when it sends an event, your agent acts on it. Every trigger you create has its own webhook URL.
Your own webhooks
Wire up any system — an automation tool, a script, a device — to POST to a trigger's URL:
POST /v1/webhooks/user/{username}/{trigger} # a personal trigger
POST /v1/webhooks/agent/{agent}/{trigger} # a shared agent trigger
Send your event as a JSON body and include your API key. The data you send is handed to the agent — your task's instructions can reference the incoming fields, so the agent responds to the specifics of each event.
curl -X POST https://otodock.example.com/v1/webhooks/agent/support-bot/new-ticket \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"customer": "Acme", "priority": "high", "summary": "Login is down"}'
Connected-service webhooks
Events from an integration like GitHub arrive under the same /v1/webhooks/ path and are
verified automatically using the provider's signed-request mechanism — no API key needed.
You set these up by connecting the service and subscribing to its events; see
Integrations and Triggers.
Behind a login gateway
If you run OtoDock behind a reverse proxy that forces a login on every request (Authentik, Authelia, oauth2-proxy, Cloudflare Access), a few paths must skip that login so the clients that authenticate themselves can still connect:
^/v1/webhooks/ # incoming webhooks — OtoDock authenticates these itself (Bearer key / signed request)
^/v1/satellite # only if you use Remote Machines — the satellite authenticates with its own pairing credentials
^/v1/images/temp/ # only if an MCP mints short-lived public image URLs (e.g. reverse-image search) — the random URL token is the auth
^/v1/twilio/ # only if the phone service is connected to Twilio — every request carries Twilio's signature, validated fail-closed
^/s/ # external share links (1.7 and later) — the random token, a password by default, a bound cookie and an admin switch are the auth
^/v1/apps/[0-9a-f-]{36}/(api|ws|platform|client)(/|$) # apps with a server (1.7 and later) — the app's page calls its server without cookies; OtoDock's own viewer token is the auth
^/v1/apps/[0-9a-f-]{36}/inbound/ # an app's inbound webhooks (1.7 and later) — verified with the app's stored secret before anything runs
You can write the two app lines as one:
^/v1/apps/[0-9a-f-]{36}/(api|ws|platform|client|inbound)(/|$). Never widen them to
^/v1/apps/, though. The routes under that prefix that manage apps (approving, deploying,
pressing buttons, the app's page itself) must stay behind your gateway.
Everything else — the dashboard and all management endpoints — stays behind your gateway and works normally for a logged-in user. The endpoints that create triggers, keys, and subscriptions are not bypassed; they use your normal session.
One practical note for the live dashboard behind such a gateway: make sure your reverse
proxy forwards WebSocket upgrades for /ws/. With strict forced-login gateways it's
simplest to exempt /ws/ from the login redirect as well, because a WebSocket handshake
can't follow one. That's not an exposure, since OtoDock authenticates the socket itself
with your logged-in session. The tab icon needs no exemption: a signed-in browser sends
your gateway's cookie with it.
Where the exemptions go
| Gateway | Where |
|---|---|
| Authentik | Applications → Providers → your proxy provider → Advanced protocol settings → Unauthenticated Paths, one regex per line |
| Authelia | access_control.rules with policy: bypass for the domain plus resources regexes |
| oauth2-proxy | one --skip-auth-route per pattern |
| Pomerium | a separate route for each path (prefix or regex) with allow_public_unauthenticated_access: true |
nginx / openresty with auth_request | a location block for each path without the auth_request directive |
| Cloudflare Access | a Bypass policy on path-scoped applications |
Two things to know when the exemptions are nginx location blocks:
-
/s/carries a WebSocket (/s/<token>/ws), so give its block the same upgrade headers and long timeouts as your/ws/block, and write it aslocation ^~ /s/so no regex location overrides it. -
A regex
locationwhose pattern contains braces must be quoted, or nginx reads the{as the start of a block andnginx -tfails withunknown directive:location ~ "^/v1/apps/[0-9a-f-]{36}/(api|ws|platform|client|inbound)(/|$)" {proxy_pass http://otodock:8400;# ... your usual proxy headers, the WebSocket upgrade headers (the app's# socket lives under ws), and no auth_request}
Check it from outside
A gateway that blocks these paths doesn't show an error: links and apps just look
unstyled or stuck on "reconnecting". To check, run these from outside, signed out. If you
get a 302 or a login page back, the gateway is still answering instead of OtoDock:
# a share link: 404 from OtoDock for a made-up link is correct
curl -s -o /dev/null -w '%{http_code}\n' https://<public-host>/s/probe
# an app's server: 401 from OtoDock is correct
curl -s -o /dev/null -w '%{http_code}\n' \
https://<public-host>/v1/apps/00000000-0000-0000-0000-000000000000/api/x
# the dashboard itself must still ask you to sign in
curl -s -o /dev/null -w '%{http_code}\n' https://<public-host>/
Good to know
- Requests are JSON, and errors come back with a clear status and message.
- Large requests are capped and abusive rates are limited (you'll get a
429with aRetry-Afterif you go too fast). - Webhooks aren't deduplicated — sending the same event twice fires the trigger twice, so handle retries on your side if your source might resend.
Next steps
- Triggers → — create the triggers these URLs fire.
- Integrations → — connect services that send signed webhooks.
- Users & Access → — create and manage API keys.