---
title: "Publish an environment for user testing"
method: PUT
path: "/projects/{projectId}/environments/{environmentId}/scenario"
tags: ["User testing"]
---

# Publish an environment for user testing

`PUT /projects/{projectId}/environments/{environmentId}/scenario`

PUT, not POST, because publishing is IDEMPOTENT: one scenario per environment, and publishing an already-published environment returns the existing one rather than minting a second. `created` says which happened.

Requires project ADMIN.

## Request body

- ScenarioPublishRequest — Optional CREATE-TIME overrides, applied in the same call so the scenario is never briefly live in a wider mode than you asked for. Ignored on a republish — see `overridesIgnored`.
  - `name` string
  - `description` string
  - `mode` 'project_members' | 'invited_only' | 'anyone_with_link' — Who may open the share link: - `project_members` — signed-in members of the project only - `invited_only` — named members, invited individually - `anyone_with_link` — **anyone holding the URL**, signed in or not

## Response `200`

The scenario, new or existing.

- Scenario — A published environment: what a visitor lands on when you share a link.
  - `id` string, required
  - `environmentId` string, required
  - `name` string, required
  - `mode` 'project_members' | 'invited_only' | 'anyone_with_link', required — Who may open the share link: - `project_members` — signed-in members of the project only - `invited_only` — named members, invited individually - `anyone_with_link` — **anyone holding the URL**, signed in or not
  - `accessVersion` integer, required — Bumped whenever access NARROWS — a mode change, or a member removal. Sessions minted under an older version stop working, which is what makes those changes take effect at once rather than at expiry. A link ROTATION is deliberately not one of them: it mints a new URL without bumping this, so anyone who already redeemed the old link keeps working. Rotating is half of closing a leak — see the rotate-link operation.
  - `link` string, nullable, required — The share link. `null` when the scenario has no link token.
  - `created` boolean — False when the environment was ALREADY published and this returned the existing scenario. Publishing is idempotent, so this is how you tell "I published it" from "it was already there" without a preflight read.
  - `overridesIgnored` boolean — True when the create-time overrides (`name`, `description`, `mode`) were NOT applied because the environment was already published. Paired with `created: false` — change an existing scenario with `PATCH .../user-testing/scenarios/{scenarioId}`.

## Other responses

- `400` — Malformed body or parameters.
- `401` — Missing, invalid, revoked, or orphaned key (`UNAUTHORIZED`) — or the **target MCP server** needs an OAuth grant (`OAUTH_REQUIRED`), which is a property of the server, not your key.
- `403` — Key is valid but not allowed to do this.
- `404` — Unknown project, server, or resource.
- `429` — Per-key rate limit exceeded (60 requests/minute sustained, bursts up to 10). Honor `Retry-After` and back off with jitter.
- `500` — Something failed on MCPJam's side.

---

[API](https://skmtc.net/mcpjam/apis/mcpjam-api.md) · [All operations](https://skmtc.net/mcpjam/apis/mcpjam-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/mcpjam/mcpjam-api/revisions/d3adfe49fbbf/schema)
