Apply by key
201. A subsequent apply returns 200 with the same
resource ID, including after a rename. externalKey is returned with the resource.
Keys match ^[a-z0-9][a-z0-9._-]{0,127}$ and are immutable while the resource exists.
Choose keys independently from display names and keep them in version control.
All five resource types accept bare write bodies. {team: ...},
{llmProvider: ...}, {desktopPolicy: ...}, and {item: ...} are response
envelopes only; never wrap a PUT body in them. apply.mjs submits each manifest
entry after environment expansion and team-reference resolution, without adding
an envelope. The write fields below describe the released API shape; see the
complete manifest for example values.
For a custom provider, models live in
customConfig.models, not top-level
models; its audience uses top-level allMembers/memberIds/teamIds, not an
access object. Catalog providers instead use source:"models_dev", providerId,
and modelIds.
Teams, providers, policies, and marketplaces support GET at the same keyed URL
and at /v1/{resource}/{id}. For MCP connections, read the returned ID at
GET /v1/mcp-connections/{id}. Provider credentials are not returned by these reads.
The new team, provider, and marketplace writes require an organization admin;
policy writes require an owner or super-admin and the applicable plan entitlement.
Existing resource permissions still apply. SCIM-managed teams must be managed by
the identity provider. MCP routes retain their existing permission and credential
restrictions. API keys are bound to their organization and the issuing member’s
permissions; they do not grant extra privileges.
A complete example
The repository includes an executable example with teams, an inference provider, two MCP connections (no-auth HTTP and per-member OAuth), desktop policy assignments, and a marketplace. Use Node.js 24 or later; no additional packages are needed.${VAR} in JSON string values after parsing (including
OAuth secrets), and fails before writing if a variable is missing. No vault-side
JSON templating is needed.
The manifest’s object keys are stable identities. Providers, policies, and MCP
connections can use "teams": ["platform"] to refer to a team key in the manifest.
The example creates teams first and resolves these references to IDs. For MCP,
the IDs go into access.teamIds; do not also supply access.teamIds when using
teams. Providers and policies instead receive top-level teamIds. Without
symbolic references, resolve actual organization-specific member/team IDs from
GET /v1/org and use the resource’s write fields above. That is the member/team
inventory endpoint; do not infer collection GET routes from the keyed PUT paths.
Review membership: the sample Platform team starts empty. Populate
teams.platform.memberIds with the intended organization member IDs before applying;
otherwise ordinary members receive no access through that team. Marketplace access
and plugin attachments use separate endpoints. Provider creator access remains even
when the creator is not in Platform.
Both MCP examples explicitly set authType, credentialMode, exposeDirectly,
and access. The no-auth documentation connection intentionally grants org-wide
access and has no OAuth client. The internal-tools connection restricts access to
Platform and requires each member to sign in. Its oauthClient contains
clientId, clientSecret, and tokenEndpointAuthMethod; the route accepts issuer
and scopes as top-level authorizationServerIssuer and requestedScopes, not
inside oauthClient. A successful configuration write does not complete OAuth
consent or prove tool execution works.
Provider usability gate after apply
A successful provider PUT proves configuration persistence, not usable credentials. After applying the custom provider, the deployment Job must perform this separate check;apply.mjs does not run it automatically:
- Resolve
llmProvider.idwithGET /v1/llm-providers/by-key/{key}, then readGET /v1/llm-providers/{id}/connectusing the authorized caller. Unlike ordinary provider reads, itsllmProviderresponse can contain the stored credential. Keep this payload in memory; never print it or save it to Job logs. - For the example’s scalar-key, OpenAI-compatible custom provider, map
llmProvider.providerConfig.apitoapi,llmProvider.apiKeytoapiKey, andllmProvider.models[].idtomodelIdsin the bare body ofPOST /v1/llm-providers/test-connection:{api, apiKey, modelIds}. Stop if the endpoint or required stored credential is missing. Verify all configured models in batches of at most eight. Do not wrap this probe in{llmProvider: ...}or merely reuse the manifest’s input secret: the gate must exercise what Den actually stored and supplies to this caller. - Check both HTTP success and
result.ok === truein the response JSON. When requesting model verification, also check the returnedverificationscover those model IDs withoutstatus:"failed"; review anyadjustedresults. Exit nonzero when the body indicates failure, even if the HTTP status is200.
result.ok:false (upstream 401), while configured credentials
passed endpoint and model verification. Treat result.hint/result.status as
failure diagnostics, not evidence of success. This gate tests the custom endpoint,
not a worker or Gateway inference session; real-provider access remains a separate
operational check.
MCP conditional writes and validation
The by-key route does not require bodyexpectedUpdatedAt. It accepts an
optional If-Match header containing the current ISO updatedAt timestamp; the
server supplies expectedUpdatedAt internally for replacement. The example opts
into this protection: it finds the key in
GET /v1/mcp-connections?scope=manageable, reads that connection by ID, and sends
its updatedAt as If-Match on the keyed PUT. MCP has no GET-by-key endpoint.
On 409, the client re-reads and retries the desired configuration once;
a second conflict stops the apply. Serialize writers: this bounded retry does not
merge concurrent edits. After a connection is bound to a marketplace plugin,
resending authorizationServerIssuer or requestedScopes can return 409 even
when their values have not changed. This includes the example’s OAuth manifest.
Review marketplace-owned identity conflicts rather than repeatedly applying; for
name/direct-access changes only, use the same-ID recipe below without those fields.
A 502 means the proposed connection could not be validated. The client stops
with that message, rather than retrying or reporting success. Check upstream
reachability and authentication before applying again. Network/5xx uncertainty
also stops MCP writes; inspect current state before rerunning. The example rejects
missing or malformed MCP access locally, before any manifest write (deletion
needs only keys). This guard is essential: the server itself accepts omitted
access and defaults it to org-wide.
Run the same command twice. The second run sends PUTs again and should update the
existing resources, not create duplicates: this is stable-identity reconciliation,
not zero writes, and timestamps or assignment-row IDs may change.
Rename a display label and remove a direct assignment, then run again: the resource
ID stays the same. For MCP team access, keep access.orgWide:false and change
teams:["platform"] to teams:[]; reapply that edited manifest twice. The team
grant is removed without changing identity or widening direct access. Removing a
direct assignment does not revoke access from other sources, such as provider
creator access or marketplace grants. Resources absent from the file are untouched.
This is resource-level convergence, not a transaction across the entire manifest.
If a later resource fails, earlier writes remain; correct the request and rerun.
Omission semantics per resource
PUT is not a universal JSON merge. Send the complete reviewed desired configuration, including credential mode and access, rather than relying on these defaults.
OAuth client-secret retention on omission was confirmed on released 0.18.46 by a
fresh authorization-code/PKCE exchange and tool call against a synthetic consent
server after reapply. This establishes unchanged-identity retention, not permission to
change URL, auth type, credential mode, issuer, or client ID while expecting the
old credentials/grants to remain usable. Provisioning the OAuth client does not
make initial consent unattended.
Existing unkeyed resources are not adopted: manage them by ID
The API never adopts an existing resource by matching its display name or URL. Applying a new key for an unkeyed connection creates another connection; it does not bind the key to the existing ID. Preserve existing OAuth grants and plugin bindings by retaining the ID, not deleting and recreating the connection. For an existing external MCP connection, GET and PUT the same ID. Unlike the keyed route, PUT-by-ID requiresexpectedUpdatedAt in the bare request body.
This Bash/curl/jq rename sequence preserves the fetched configuration/direct access
and omits write-only secrets; use it only with unchanged connection/client identity:
CONNECTION_ID and desired MCP_NAME; supply the API key
securely without shell tracing. On 409, GET again and review the latest state
before another PUT. Do not copy a stale timestamp or invent an external key.
Other conflicts
Teams retain their existing unique-name rule. If a keyed team would use another team’s name, the API returns409; it does not overwrite that team. Providers,
policies, and marketplaces retain their existing duplicate-name behavior. An
archived marketplace retains its key and must be explicitly restored through its
lifecycle endpoint before applying metadata.
A concurrent first apply can fail; the database prevents two resources from owning
the same key. Serialize deployments that write the same resource, and inspect
state after uncertain MCP creates rather than blindly retrying. The other four
resource types use last-write-wins and reject If-Match and If-None-Match on PUT
rather than silently ignoring them. MCP’s optional If-Match behavior remains
available. This release does not
provide a universal compare-and-swap contract or guarantee that a no-op apply
leaves timestamps and assignment-row IDs unchanged.
Remove managed resources
DELETE /v1/{resource}/by-key/{key} returns
{"ok":true,"deleted":true} for a removal and
{"ok":true,"deleted":false} if it is already absent. Delete dependents before
teams. Deleting a marketplace removes the marketplace and its relationships,
not the underlying plugins. A policy deletion retains its normal soft-delete
behavior and releases the key. A later apply of a deleted key creates a new ID.
To remove exactly the resources listed in the example manifest:
Scope and compatibility
The five keyed APIs are available in v0.18.43+. The behavioral release checks cited above used v0.18.46, not every earlier version; for example, failed MCP creation cleanup changed after v0.18.43. Inspect your deployed API schema rather than assuming a movingdev example matches an older deployment:
teams, llm-providers, mcp-connections,
desktop-policies, and marketplaces, and inspect their request schemas. Schema
presence establishes route availability, not upstream connectivity or equivalent
runtime behavior across releases.
Keep SSO outside the repeated apply: saving organization OIDC configuration again
resets it to disabled/untested; configure, verify, test, and enable it deliberately.
Legacy/Gateway migration warning: PUT /v1/llm-providers/by-key/{key} manages
legacy providers. Explicit conversion to Gateway deletes the legacy source and
drops its actionable external key; Gateway has no by-key equivalent. Stop and
revise the keyed writer before conversion, or its next apply creates another
legacy provider instead of reconciling the Gateway provider. Installing a
Gateway-capable build alone does not invoke that conversion.
Existing create, update-by-ID, and delete-by-ID routes keep their request behavior.
Responses add the nullable externalKey field. Existing rows remain unkeyed; the
release does not enforce new display-name uniqueness or rename existing resources.
This workflow covers configuration of five resource types, not every organization
setting. Bootstrap still requires an authenticated administrator to create the
organization and issue an API key. Invitations, access grants, versioned skills,
plugins, automations, member credentials, and organization settings retain their
existing APIs and lifecycle rules. Use their current endpoints alongside this
manifest when required; the script does not claim to provision those resources.