---
title: "Create Sandbox Project From Features"
method: POST
path: "/api/v1/project/sandbox/from-features"
tags: ["project"]
---

# Create Sandbox Project From Features

`POST /api/v1/project/sandbox/from-features`

Create one sandbox project from several confirmed features (Door 1).

The multi-feature twin of ``/sandbox/from-feature``: derives one shared county
from every feature's marker before inserting the project, then enqueues a single
seed for all of them. Unknown ``feature_id`` → 404; a NULL/malformed marker →
422; features spanning more than one county → 422 (a project is scoped to one
county under RLS, so the paste must be split). ``status`` flips to ``ready`` and
the layers arrive via SSE.

``features`` is the paste flow. ``upload`` is the file-import flow, whose row
values come from the server's held inspect result rather than this request.

## Headers

- `Idempotency-Key` string, nullable

## Request body

- AddressBatchCreateFlowRequest — Create one sandbox project from several confirmed features. Every feature must derive to the same county (a project is scoped to one county under RLS); the server enforces this and 422s a cross-county batch. Exactly one source: ``features`` is the paste flow, which has no file behind it; ``upload`` is the file-import flow, which references a held inspect result so the server reads the user's rows from its own parse rather than from this request. ``spine="all_rows"`` is meaningful only for ``upload`` — there are no rows to seed a layer from without a file.
  - `features` AddressCreateFlowRequest[], nullable
    - `source_table` 'parcel' | 'building' | 'place' | 'address' | 'school', required — The table a resolved feature lives in — what a caller pulls its data from, and the feature's kind. A text match in the ``address``, ``place`` or ``school`` table resolves (point-in-feature) to a ``parcel``/``building``; a ``parcel`` attribute match resolves to the parcel itself.
    - `feature_id` string, required
    - `knowledge_skill_workspace_id` string, uuid, nullable
  - `upload` ImportUploadCommit — The held-upload half of a commit — references, never row values. The server re-reads the user's rows from its own inspect result via ``token``, so the browser's copy of the file stays display-only and the commit is not a trust boundary: no user-controlled dict keys, no reserved name collisions, no re-sanitization. ``mapped_columns`` names which held columns to carry through and is validated against the held column list.
    - `token` string, required
    - `mapped_columns` string[]
    - `matches` ImportRowMatch[], required
      - `row_index` integer, required
      - `source_table` 'parcel' | 'building' | 'place' | 'address' | 'school', required — The table a resolved feature lives in — what a caller pulls its data from, and the feature's kind. A text match in the ``address``, ``place`` or ``school`` table resolves (point-in-feature) to a ``parcel``/``building``; a ``parcel`` attribute match resolves to the parcel itself.
      - `feature_id` string, required
      - `included` boolean
    - `dropped_row_indices` integer[]
  - `spine` 'matched_only' | 'all_rows'
  - `knowledge_skill_workspace_id` string, uuid, nullable

## Response `200`

Successful Response

- AddressProjectResponse — Response for the address create-flow. ``status`` is the project's real status at sync-response time. A first creation is ``pending`` and the async DBOS seed flips it to ``ready``/``failed``, with the seeded layer arriving via the ``LayerStateChangedEvent`` SSE — but a replayed key returns the recorded project, which may already have reached either terminal state, so a caller must not read this as always-pending. ``county_fips`` is the 5-digit scope derived from the matched feature.
  - `project_id` string, uuid, required
  - `status` 'pending' | 'ready' | 'failed' | 'archived', required — Lifecycle status for a project. Replaces the boolean ``is_created`` whose name implied "was created" (always True once inserted) but semantically meant "is ready for the dashboard to show." Promoting to an enum lets new states (FAILED, ARCHIVED) join without contorting the boolean. - PENDING: row inserted, sandbox resources not yet materialized - READY: resources in place; visible on dashboard - FAILED: build raised mid-flight (sandbox saga rollback uses hard-delete instead, so this state is reserved for future async build paths that can't roll back atomically) - ARCHIVED: user-archived; hidden by default
  - `county_fips` string, required

## Other responses

- `422` — Validation Error

## Changes

- **2026-08-31** `4c1b045ef2af` — 1 info
  - added the new optional `header` request parameter `Idempotency-Key`
- **2026-08-08** `ba2b8d962161` — 2 info
  - added the new optional request property `features/anyOf[subschema #1]/items/knowledge_skill_workspace_id`
  - added the new optional request property `knowledge_skill_workspace_id`

[Change history](https://skmtc.dev/maia-analytics/apis/maia-api/changes/api/v1/project/sandbox/from-features/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/97ae7fd86d44?raw)
