---
title: "Provision Workspace"
method: POST
path: "/api/v1/internal/workspaces/provision"
tags: ["internal", "workspaces"]
---

# Provision Workspace

`POST /api/v1/internal/workspaces/provision`

Provision a new workspace end-to-end with a Neon sandbox + counties.

Replaces the legacy bare-create `POST /`. A workspace granted no counties
has zero geographic query access, so provisioning is the only sanctioned
create path. Fires the `stand_up_workspace` Dagster job and returns once the
rows are persisted (5-15s for Neon create).

A `local` workspace — every workspace today — seeds its county rows claimed
for the loader, and its county data loads asynchronously thereafter. A
`commons` workspace is thin: its rows record the grant, no county load is
queued, and the counties are available as soon as the call returns. Thin
provisioning is refused unless the request also marks the workspace
MAIA-staff, which is what keeps the path dark.

## Request body

- AdminWorkspaceProvisionRequest — Request body for provisioning a new workspace end-to-end. Wraps `AdminWorkspaceService.provision_workspace_with_counties`: creates the Neon sandbox and seeds `workspace_counties` rows. At least one county is mandatory — a workspace granted no counties has zero geographic query access, whether or not its own branch would have held their data. Rejects unknown fields with 422. `counties` non-emptiness and in-list duplicates are validated in the service (400) rather than here, so the same guard covers the script caller (`scripts/provision_prod_admin_workspace.py`) and the error is a 400, not a 422, matching the rest of the provisioning contract.
  - `name` string, required
  - `counties` WorkspaceCountySelection[], required
    - `state_fips` string, required
    - `county_fips` string, required
  - `initial_credits` integer
  - `region_id` string, nullable
  - `icp_category_id` string, uuid, nullable — Optional ICP category to assign at creation. Omit or pass null to leave the workspace unassigned.
  - `county_data_residency` 'local' | 'commons' — Where a workspace's county data physically lives. Orthogonal to `WorkspaceType`, which is a billing cohort: a discovery workspace and a thin workspace are independent facts, and folding one into the other would conflate plan with topology. `LOCAL` is every deployed workspace: the counties it may query were copied into its own branch, so a statement over them is workspace-only. `COMMONS` is a thin workspace, whose branch holds none of them — it is entitled to counties the shared corpus holds, and that entitlement is what binds its model-authored statements to the commons router.
  - `is_internal` boolean — Mark the new workspace as a MAIA-staff workspace. Required alongside a 'commons' residency while the shared corpus is dark.

## Response `200`

Successful Response

- AdminWorkspaceDetailResponse — Full workspace detail response for admin view.
  - `id` string, uuid, required
  - `name` string, required
  - `budget_authority` 'credits' | 'paid_balance', required — Which budget a metered run draws down. Two members, because there are two budgets. Free usage and paid usage are the same balance on the same account, spent at the same prices and refused by the same check; what a workspace paid for that balance decides what its refusal offers next, not how the run is admitted. A third member for the free tier is what let the two drift — different prices, different exemptions, different per-run sizing — while both claimed to meter usage.
  - `avatar_color` string, nullable, required
  - `available_enrichment_credits` integer, required
  - `used_enrichment_credits` integer, required
  - `remaining_credits` integer, required
  - `is_suspended` boolean, required
  - `is_internal` boolean, required
  - `icp_category_id` string, uuid, nullable, required
  - `member_count` integer, required
  - `project_count` integer, required
  - `members` AdminWorkspaceMemberItem[], required
    - `id` string, uuid, required
    - `email` string, required
    - `display_name` string, nullable, required
    - `workspace_role` string, nullable, required
    - `is_internal` boolean, required
    - `is_suspended` boolean, required
    - `created_at` string, date-time, nullable, required
    - `used_enrichment_credits` integer, required
    - `last_active_at` string, date-time, nullable, required
  - `created_at` string, date-time, nullable, required
  - `updated_at` string, date-time, nullable, required

## Other responses

- `422` — Validation Error

## Changes

- **2026-09-15** `499d0b94aa94` — 2 info
  - added the new optional request property `county_data_residency`
  - added the new optional request property `is_internal`
- **2026-09-12** `604ea84d0ff7` — 1 warning, 1 info
  - added the new `paid_balance` enum value to the `budget_authority` response property for the response status `200`
  - removed the `discovery_allowance` enum value from the `budget_authority` response property for the response status `200`
- **2026-08-26** `b8fec14143f2` — 1 info
  - added the required property `budget_authority` to the response with the `200` status
- **2026-08-05** `bd2347b6f591` — 2 info
  - added the new optional request property `icp_category_id`
  - added the required property `icp_category_id` to the response with the `200` status

[Change history](https://skmtc.dev/maia-analytics/apis/maia-api/changes/api/v1/internal/workspaces/provision/post.md)

---

[API](https://skmtc.dev/maia-analytics/apis/maia-api.md) · [All operations](https://skmtc.dev/maia-analytics/apis/maia-api/llms.txt) · [OpenAPI document](https://skmtc.dev/maia-analytics/apis/maia-api/revisions/499d0b94aa94?raw)
