Skip to main content

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​

GatewayWhere
AuthentikApplications → Providers → your proxy provider → Advanced protocol settings → Unauthenticated Paths, one regex per line
Autheliaaccess_control.rules with policy: bypass for the domain plus resources regexes
oauth2-proxyone --skip-auth-route per pattern
Pomeriuma separate route for each path (prefix or regex) with allow_public_unauthenticated_access: true
nginx / openresty with auth_requesta location block for each path without the auth_request directive
Cloudflare Accessa 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 as location ^~ /s/ so no regex location overrides it.

  • A regex location whose pattern contains braces must be quoted, or nginx reads the { as the start of a block and nginx -t fails with unknown 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 429 with a Retry-After if 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​