Automating DurinDoor
Mint a key, script providers and combos with curl or MCP, then rotate and scope that key.
Use a dedicated DurinDoor key for permitted management routes and /v1 requests. Management access includes provider and combo changes; inference scopes do not turn the key into a read-only management credential. An MCP client can do the same work through POST /api/mcp/control. Route tables and exclusions: Management API. Tools and auth: MCP control.
Production listens on port 20128. Replace the host if you use a tunnel.
Store the secret when POST /api/keys returns it. List and detail views mask it. Reveal stays on a dashboard session.
Mint a key for automation
Dashboard path: API Keys → Create. Or POST:
curl http://localhost:20128/api/keys \
-H "Authorization: Bearer EXISTING_DURINDOOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"ci-bot"}'The 201 body includes key, id, expiresAt, policy, and providerConnectionIds. Later GET /api/keys returns maskedKey. First-time minting from the dashboard is easier if you have no key yet: the session cookie is enough.
Give this key a name you can revoke later. Optional allowedCombos, policy.allowedModels, policy.maxTokens, policy.maxCostUsd, dailyLimitTokens, expiresAt, and providerConnectionIds. Empty means unrestricted for that mechanism. Key groups are labels only. Shapes and expiry presets: API keys.
Script provider and combo setup with curl
Create an API-key connection. Before creating the example combo, also connect Anthropic and verify both model IDs on your instance. Replace the example secrets and CONNECTION_ID with your own values.
The example creates one connection, saves a combo, and refreshes quota:
curl http://localhost:20128/api/providers \
-H "Authorization: Bearer YOUR_DURINDOOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"openai","name":"openai-main","apiKey":"sk-upstream-secret"}'
curl http://localhost:20128/api/combos \
-H "Authorization: Bearer YOUR_DURINDOOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"coding-default","models":["openai/gpt-4.1","anthropic/claude-sonnet-4-5"]}'
curl "http://localhost:20128/api/usage/CONNECTION_ID?force=1" \
-H "Authorization: Bearer YOUR_DURINDOOR_API_KEY"POST /api/providers needs provider and name. apiKey is required unless the provider is noAuth or ollama-local. Duplicate (provider, apikey, name) is 409. OAuth accounts use /api/oauth/..., not this POST.
Combo names: letters, digits, -, _, .. The client then sends "model": "coding-default" on /v1/chat/completions. A successful chat response and a row on Usage verify routing; saving the combo alone does not test its members.
Confirm with GET /api/combos and GET /api/providers. Secrets are stripped on those reads. Proxy userinfo in connectionProxyUrl is *** for this key.
Same work through an MCP client
Add the control server (Claude Code shown; Cursor mcp.json is on MCP control):
claude mcp add --transport http durindoor-control http://localhost:20128/api/mcp/control \
--header "Authorization: Bearer YOUR_DURINDOOR_API_KEY"Then ask the MCP client:
- List my combos and their members.
- Create a fallback combo named
coding-defaultwith those two models. - Refresh quota for connection
CONNECTION_ID.
Those call list_combos, create_combo, and refresh_quota. list_connections is the read that matches GET /api/providers. There is no control tool that mints a DurinDoor API key; do that over REST or the dashboard.
On loopback with requireApiKey off, the MCP client may omit the header. Remote MCP always sends the key. A browser Origin that is not this server is 401 even on loopback.
Rotate and scope the key
There is no rotate endpoint. Create a new key, point the client at the new secret, then pause or delete the old row. Pause is PUT /api/keys/{id} with "isActive": false. Delete removes the key and its account-scope assignments.
Restrict the key's inference access before giving the secret to CI. These limits do not restrict the entire management API:
providerConnectionIds: only those account rowsallowedCombos: only those combo namespolicy.allowedModels: exact model stringspolicy.maxTokens/policy.maxCostUsd: lifetime capsdailyLimitTokens: calendar-day capexpiresAt: UTC ISO, or omit for never
Account scoping details: API key scoping.
Keep on a dashboard session only
Do not put these on the automation key:
GET /api/keys/{id}/revealand gateway-key reveal (?reveal=1included)/api/shutdown,/api/version/shutdown,/api/version/update/api/settings/databaseand cutover/rollback/importPATCH /api/settingskeys that change login, API-key enforcement, OIDC, outbound proxy, tunnel dashboard access, or observability- remote tunnel enable/disable, Tailscale, Headroom start/stop, CLI-tool spawners, password reset
/api/key-groups,/api/data-retentionPOST,/api/compression/preview,/api/pxpipe/*
Use the dashboard (or CLI token on the host) for those. The automation key is for connections, combos, catalog, usage, timeline, and the non-secret settings patch.