---
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 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.

## 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.

## 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
  - `county_coverage` 'granted' | 'national', required — Which counties a workspace may open projects on. `GRANTED` is the counties its `workspace_counties` rows grant. `NATIONAL` is every county the shared corpus holds, for a workspace whose data is served from the corpus; a workspace still on its own branch reads only what that branch holds, so national coverage widens nothing for it until it moves. Stored so every gate reads one column; its value is a copy of the billing account's paid access, written only by the billing writes.
  - `county_data_residency` 'local' | 'commons', required — 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.
  - `reads_entire_corpus` boolean, required — The entitlement model's corpus verdict, resolved once here: national coverage on the shared-corpus lane. Not `county_coverage == 'national'` — a national workspace still on its own branch reads only the counties loaded into that branch. Carries the same value the customer-facing `national_county_coverage` does.
  - `origin` 'staff' | 'self_serve_discovery' | 'direct_paid', required — Which door a workspace was acquired through. An acquisition fact, written once at creation and never rewritten. Orthogonal to `WorkspaceType`, which is the billing cohort, and to `CountyDataResidency`, which is topology: a direct-paid workspace opens as a discovery-type workspace on the joining grant and converts in place, so the type says nothing about the door. Keeping it separate is what makes an abandoned direct signup countable — it holds a free workspace whose type is indistinguishable from a Discovery one. `STAFF` is every workspace a person at MAIA provisioned, and is what a row written before this column reports.
  - `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-19** `924eeadc29ad` — 4 info
  - added the required property `county_coverage` to the response with the `200` status
  - added the required property `county_data_residency` to the response with the `200` status
  - added the required property `origin` to the response with the `200` status
  - added the required property `reads_entire_corpus` to the response with the `200` status
- **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/8dc47faf0418?raw)
