DurinDoor
Guides

API keys

Create, restrict, expire, pause, and replace DurinDoor client keys.

Open Dashboard → API Keys → Create. Give the key a name. Creation shows the full secret. List and detail views mask it as sk-••••••••. The dashboard can still copy the stored secret later through a dedicated reveal route, after a signed-in session.

Use one key per tool or person so you can pause or delete a single client without touching the rest.

Store the secret when it is shown. Paste that same value into client configs and media-test forms. The management list does not print it.

Authenticate a client

Use the DurinDoor key, not the vendor credential. OpenAI clients send Authorization: Bearer YOUR_DURINDOOR_API_KEY; Anthropic clients can send x-api-key. The gateway also accepts x-goog-api-key and a key query parameter.

With Require API Key enabled on Endpoint, missing, unknown, paused, and expired keys cannot authenticate. With enforcement off, an unknown local placeholder may pass, but a paused or expired saved key is still rejected. Use a real key whenever you need client scoping or usage attribution.

New keys use the sk- prefix. Older stored keys remain supported. Keep the full secret unchanged when copying it.

Expiry

Each key stores expiresAt as a UTC ISO timestamp, or null for never.

PresetStored value
Never expiresnull
1 daynow + 1 × 86400000 ms
7 daysnow + 7 × 86400000 ms
30 daysnow + 30 × 86400000 ms
90 daysnow + 90 × 86400000 ms
Custom date and timelocal datetime-local converted with toISOString()

Custom input is shown back in the operator's local timezone. Edit the key and choose Never expires to clear a timestamp.

Expiry is compared to server time. The key is expired as soon as that clock is greater than or equal to the stored value. Expired keys stay on the list and in backups; they cannot authenticate. Clients see the same invalid-key response used for other bad credentials.

Keep API_KEY_SECRET and the persistent data directory stable across redeploys. Changing the signing secret can invalidate existing keys.

Scopes

A key can carry several independent restrictions. Empty means unrestricted for that mechanism.

providerConnectionIds lists the provider-connection rows the key may route through. Zero rows means every account. Details: API key provider-account scoping.

allowedCombos is a list of combo names. A non-empty list rejects any other combo with HTTP 403. policy.allowedModels is an exact-match allowlist of model strings; empty or missing means all models.

auto/<family> ids (auto/glm, auto/gemini, ...) are virtual: they are synthesised in the catalog, not stored as combo rows, so allowedCombos and policy.allowedModels cannot scope them out. policy.allowAutoCombos: false denies every auto/* id for that key with HTTP 403; omitted or true keeps the existing behavior (allowed).

policy.maxTokens and policy.maxCostUsd are lifetime caps on that key. dailyLimitTokens is a calendar-day token cap that resets at local midnight. isActive false keeps the row visible and stops authentication.

In the Edit dialog's committed usage display, empty lifetime token/cost fields mean unlimited and show only the amount used, not a false “Limit reached”. An explicit 0 remains a real lifetime limit; this differs from the windowed-limit convention below.

Key groups are labels only. Membership never changes what a key can call.

Model access rules

policy.modelAccess is { mode, patterns }:

  • mode: "all" (default) puts no restriction on the key.
  • mode: "allow" lets the key call only models that match a pattern. An empty list blocks every model.
  • mode: "deny" lets the key call anything except models that match a pattern.

Patterns are case-insensitive globs, up to 200 per key. * matches any run of characters, / included. Write them against the ids /v1/models shows, for example openai/* or cx/gpt-5.6-sol.

The rule is checked after model resolution, in the same place as policy.allowedModels, on every /v1 endpoint. A model matches under the name it was requested by, its canonical providerId/model, and its alias/model or provider-node prefix/model form, so an alias cannot get around a rule. A blocked model returns HTTP 403 before any credential lookup. Combos are checked member by member; combo names stay under allowedCombos. /v1/models and /v1/models/{kind} only list what the calling key may use. Trusted loopback requests without a key are not restricted while Require API key is off. Remote HTTP callers still need a valid gateway key or CLI credential at the network boundary.

Windowed limits

These fields also live in policy. Empty, null or 0 means no limit.

FieldWindow
rpmLimitRequests in a sliding 60 seconds
tpmLimitInput plus output tokens in a sliding 60 seconds
dailyInputTokenLimit, dailyOutputTokenLimitInput or output tokens since local midnight
monthlyTokenLimit, monthlyInputTokenLimit, monthlyOutputTokenLimitTokens since the 1st of the month
monthlyRequestLimitRequests since the 1st of the month
monthlyBudgetEstimated cost in USD since the 1st of the month

The daily total stays in dailyLimitTokens. An over-limit request gets HTTP 429 with Retry-After and a message naming the limit. Chat checks the limits once per request before combo dispatch, so a key's own 429 never counts against a combo member's health. count_tokens is not limited.

RPM and TPM are counted in memory per process: RPM when a request is admitted, TPM when its usage is recorded. Token, request and budget windows sum usageHistory, which is written after a response finishes, so requests already in flight can go slightly over. Only endpoints that record usage move the token and budget windows.

Monthly budgets use the estimated costs saved with request usage. They are not provider billing limits. Deleting usage history can affect usage-derived limits.

PUT /api/keys/{id} accepts modelAccess and each limit field at the top level of the body (the upstream 9router shape) and merges them into the stored policy. GET /api/keys/usage returns the current usage against every limit, keyed by key id, and ?apiKeyId=<id> narrows it to one key. The API key limits card below the key list on Dashboard → API Keys polls it; that card is no longer on Usage.

Rotation

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 isActive: false. Delete removes the key and its account-scope assignments.

POST /api/keys requires a non-empty name and the install's machineId. The 201 body includes key, id, expiresAt, policy, and providerConnectionIds. Later GET /api/keys and GET /api/keys/[id] return maskedKey instead of key. GET /api/keys/[id]/reveal returns { key } for a dashboard session. PUT /api/keys/[id] patches name, isActive, expiry, combos, policy, daily limit, provider connections, and groups in one transaction.

If API_KEY_SECRET is unset, DurinDoor mints DATA_DIR/api-key-secret (mode 0600) when DATA_DIR is set. Without either, key minting throws.

After creating or changing a key, send a request from its intended client. Confirm the key in Usage. If authentication fails, check active state, expiry, the copied secret, and server time. For 403 or 429, inspect model scopes and usage limits before rotating the key.

On this page

Edit on GitHub