Components (plugins, themes, modules)
Previewcapability-gated
The components resource is the differentiating surface of the managed.dev API — the
dynamic application layer that generic PaaS deliberately won’t touch and other WP
hosts expose only bluntly. It models plugins, themes, and (on other runtimes)
modules as one typed resource: you install, activate, update, and remove them per
environment, each call returns a job, and every action
is gated by both a scope and a runtime
capability.
the resource
Section titled “the resource”A component is a unit of the application layer addressed by kind and slug:
{ "id": "comp_01J9...", "kind": "plugin", // plugin | theme | module "slug": "wordpress-seo", "name": "Yoast SEO", "version": "23.4", "status": "active", // active | inactive "update_available": "23.5", "site_id": "site_01J7...", "env_id": "env_01J8..."}Components are environment-scoped by default: a plugin can be active on staging
but not production, so calls live under
/v1/sites/{siteID}/environments/{envID}/components. A site-level alias resolves to
the production environment as a convenience.
the two gates
Section titled “the two gates”Every components call is checked twice. First the scope on
your key — for example wp.plugins:write to install a plugin. Then the runtime
capability the route requires — components.plugins. A key can hold a scope it
can never use, because the capability simply isn’t present on a runtime that has no
plugins.
endpoints
Section titled “endpoints”list components
Section titled “list components”GET …/{envID}/components
| Parameter | Type | Required | Description |
|---|---|---|---|
kind |
string | no | Filter by plugin, theme, or module. Omit to list all kinds. |
status |
string | no | Filter by active or inactive. |
limit |
integer | no | Page size. See pagination. |
cursor |
string | no | Opaque cursor from a prior next_cursor. |
curl https://api.managed.dev/v1/sites/site_01J7.../environments/env_01J8.../components?kind=plugin&status=active \ -H "Authorization: Bearer mfk_live_9aF2..." \ -H "Forge-Version: 2026-06-23"comps, err := mf.Components.List(ctx, forge.ComponentListParams{ SiteID: "site_01J7...", EnvID: "env_01J8...", Kind: forge.Plugin, Status: forge.Active,})const comps = await mf.components.list({ siteId: "site_01J7...", envId: "env_01J8...", kind: "plugin", status: "active",});mf components list --env env_01J8... --kind plugin --status active{ "data": [ { "id": "comp_01J9...", "kind": "plugin", "slug": "woocommerce", "version": "9.1.2", "status": "active", "update_available": null }, { "id": "comp_01J9...", "kind": "plugin", "slug": "wordpress-seo", "version": "23.4", "status": "active", "update_available": "23.5" } ], "pagination": { "next_cursor": "eyJ0...", "has_more": false }, "request_id": "req_01J9..."}install a component
Section titled “install a component”POST …/{envID}/components
Returns 202 Accepted with a job — installs are asynchronous. Send an
Idempotency-Key so a retried request never installs twice.
| Parameter | Type | Required | Description |
|---|---|---|---|
kind |
string | yes | plugin, theme, or module. |
slug |
string | yes | The component slug, e.g. wordpress-seo. |
source |
string | no | registry (default), or a signed artifact reference. |
version |
string | no | A pinned version, or latest (default). |
activate |
boolean | no | Activate immediately after install. Defaults to false. |
update / change state
Section titled “update / change state”PATCH …/{envID}/components/{id}
Patch a component to update its version or flip its status between active and
inactive. Returns 202 with a job.
remove a component
Section titled “remove a component”DELETE …/{envID}/components/{id}
Deactivates and removes the component. Returns 202 with a job.
batch (per-environment bulk)
Section titled “batch (per-environment bulk)”POST …/{envID}/components:batch
Apply a set of component actions to one environment in a single call — the typed
equivalent of a fleet plugin update. Each item carries its own action; the call
returns one job whose result reports per-item outcomes.
{ "operations": [ { "action": "update", "kind": "plugin", "slug": "woocommerce", "version": "latest" }, { "action": "update", "kind": "plugin", "slug": "wordpress-seo", "version": "latest" }, { "action": "deactivate", "kind": "plugin", "slug": "hello-dolly" } ]}worked example — install a plugin on staging
Section titled “worked example — install a plugin on staging”Install Yoast SEO on a staging environment, then watch the job stream to completion.
-
Confirm the capability. Check the site advertises
components.pluginsbefore you branch — see capability discovery. -
Send the install. A scope-pinned key, an idempotency key, and a
202back.Install Yoast on staging curl -X POST https://api.managed.dev/v1/sites/site_01J7.../environments/env_01J8.../components \-H "Authorization: Bearer mfk_live_9aF2..." \-H "Forge-Version: 2026-06-23" \-H "Idempotency-Key: 6f1c-44a0-9e2b" \-H "Content-Type: application/json" \-d '{ "kind": "plugin", "slug": "wordpress-seo", "source": "registry", "version": "latest", "activate": true }'Install Yoast on staging job, err := mf.Components.Install(ctx, forge.ComponentInstallParams{SiteID: "site_01J7...",EnvID: "env_01J8...",Kind: forge.Plugin,Slug: "wordpress-seo",Version: "latest",Activate: true,IdempotencyKey: "6f1c-44a0-9e2b",})Install Yoast on staging const job = await mf.components.install({ siteId: "site_01J7...", envId: "env_01J8..." },{ kind: "plugin", slug: "wordpress-seo", version: "latest", activate: true },{ idempotencyKey: "6f1c-44a0-9e2b" },);Install Yoast on staging mf components install \--env env_01J8... \--kind plugin --slug wordpress-seo --activate --wait202 response HTTP/1.1 202 AcceptedLocation: /v1/jobs/job_01J9...X-RateLimit-Limit: 100X-RateLimit-Remaining: 99{"data": {"id": "job_01J9...", "type": "component.install", "status": "queued","created_at": "2026-06-23T18:04:11.412Z","resource": { "type": "component", "kind": "plugin", "slug": "wordpress-seo","site_id": "site_01J7...", "env_id": "env_01J8..." }},"request_id": "req_01J9..."} -
Tail the job. Follow it to
succeededover SSE, or ETag long-poll from CI. See async jobs for both paths.Live-tail the install curl -N https://api.managed.dev/v1/jobs/job_01J9.../stream \-H "Authorization: Bearer mfk_live_9aF2..."
The same call against a static site returns 409 capability.unsupported —
there are no plugins to install. That’s the capability gate doing its job.