---
title: "Upload Layer"
method: POST
path: "/api/v1/layer/{project_id}/upload"
tags: ["layer"]
---

# Upload Layer

`POST /api/v1/layer/{project_id}/upload`

Upload a CSV or GeoJSON file as a generic sandbox layer.

``ignore_geometry`` is the table-route override: parsed geometries are
dropped so a geometry-bearing file lands as a plain table.

## Response `200`

Successful Response

- LayerUploadResponse — Response schema for uploaded generic layers. ``out_of_scope_rows`` counts features dropped by the project-county fence — surfaced so the client can report them, never silently swallowed.
  - `message` string, required
  - `layer` LayerModel, required — Model for a layer.
    - `id` string, uuid, required
    - `project_id` string, uuid, required
    - `reference_layer_id` string, uuid, nullable
    - `reference_layer_ids` string[]
    - `name` string, required
    - `description` string, nullable
    - `style_config` object, required
    - `feature_count` integer, required
    - `total_size_bytes` integer, required
    - `data_version` integer
    - `schema_version` integer
    - `metadata_version` integer
    - `base_attributes` object, required
    - `feature_enrichments` EnrichmentModel[]
      - `id` string, uuid
      - `layer_id` string, uuid, nullable
      - `name` string
      - `description` string, nullable
      - `tool` string
      - `params` object
      - `dtype` string — Must be one of string, boolean, list[str], jsonb, int, float, categorical, url
      - `created_at` string, date-time, nullable
      - `updated_at` string, date-time, nullable
      - `credits_per_row` integer, required — Return the number of credits per row for this tool.
      - `is_configured` boolean, required — Check if the enrichment is configured with a valid tool. Returns: bool: True if tool is configured, False otherwise
      - `is_configuration_failed` boolean, required — Check if the enrichment's configuration failed. Returns: bool: True if configuration was attempted but failed
      - `is_data_source` boolean, required — Check if the enrichment is a data source. Returns: bool: True if the enrichment is a data source, False otherwise
    - `sandbox_relation_name` string, nullable
    - `sandbox_schema` string, nullable — Schema holding sandbox_relation_name. None means the legacy public schema; per-project scratch schemas (maia_scratch_<hex>) are recorded here at registration (MAIA-3209).
    - `render_mode` 'detail_always' | 'agg_at_low_zoom'
    - `display_kind` 'data' | 'boundary' — Whether the layer is a queryable dataset (``data``) or a styling-only overlay (``boundary``). Boundary layers paint on the map but suppress the per-layer table, column manager, and feature-detail panel — there is no per-row data worth surfacing.
    - `extent_bounds` ThumbnailBounds — Bounding box for project thumbnail map rendering. Represents the geographic extent of project features for generating static map thumbnails via Mapbox.
      - `min_lon` number, required
      - `min_lat` number, required
      - `max_lon` number, required
      - `max_lat` number, required
    - `has_drawable_geometry` boolean, nullable — Whether the layer holds geometry the map can draw. False marks a table-only upload; None means unrecorded — every layer created before this field, and every creation path that does not state it. Consumers must treat None as unknown, not as False.
    - `created_at` string, date-time, nullable, required
    - `updated_at` string, date-time, nullable, required
    - `columns` LayerColumn[], nullable — Producer-declared, resolved per-column metadata (id, key, provenance, display name, data type) — schema/lineage only. Visibility and ordering live on the view, seeded at register-layer time. None for legacy pre-cutover layers; the composer derives sensible defaults from physical introspection.
      - `id` string, required — Stable column identifier within a layer. Hashed from the layer id plus the column's kind-specific stable identity. Source and merged columns omit `key` so alias renames keep the same id; agent-derived columns include `key` to distinguish multiple computations anchored on the same source.
      - `key` string, required
      - `provenance` ColumnProvenance, required — Where a column came from and how to look up its canonical metadata. `source_column` is the canonical Overture column path (e.g. `names.primary`, not the agent's SQL alias `name`). The resolver uses it to look up the display label and to derive a rename-stable column id — making alias mismatches impossible by construction.
        - `kind` 'source' | 'merged' | 'enrichment' | 'system' | 'agent_derived', required — Provenance discriminator for a layer column. * `source` — column SELECTed directly from the canonical Overture table. * `merged` — column joined in from another canonical source table (e.g. `parcel_owner` on a building layer joined parcel data). * `enrichment` — populated by an enrichment runner, not the agent's SQL. (Reserved for the upcoming enrichment unification — not used yet.) * `system` — agent-facing hide-by-default escape for lineage columns merged for enrichment plumbing the user shouldn't see by default (e.g. parcel address fields plumbed onto a building layer for Owner Contact Info). Stays in the registered schema and can be surfaced later via `modify_columns`. * `agent_derived` — computed in the agent's SQL (`SUM(...) AS total`, `CASE WHEN ... END AS bucket`). Still requires source_table+source_column anchoring so view deltas survive a column rename.
        - `source_table` string, nullable — Canonical Overture table this column derives from. None for fully derived (composite) columns with no canonical lineage.
        - `source_column` string, nullable — Canonical column path on `source_table` (e.g. `names.primary`). Used by the resolver for display-name lookup and stable column ids — NOT the agent's SQL alias.
      - `display_name` string, required — Resolved user-facing column label. Unit-agnostic — does NOT include unit tokens like `(ft)`, `(m)`, `(m²)`, or `(acres)`. The FE composes the column header as `display_name + (unit)` where `unit` is the single source of truth for the unit suffix. See MAIA-1872.
      - `data_type` 'string' | 'number' | 'integer' | 'boolean' | 'date' | 'geometry' | 'array' | 'object' — Optional frontend type hint for a layer column.
      - `unit` string, nullable — Display-unit suffix declared by the agent at register-layer time. Single source of truth for the user-visible unit on this column — the FE composes the header as `display_name + (unit)` and renders cells as `value + unit`. Round-trips through the resolver verbatim. Numeric formatting (decimal places) is picked from a unit→format lookup; unknown units fall back to integer formatting + raw suffix so the agent can declare arbitrary unit strings without code changes. Null for non-unit columns (names, IDs, categorical text). Currency uses the `currency` data_type / semantic type, NOT this field.
      - `semantic_type` 'identifier' | 'name' | 'address_part' | 'address_full' | 'currency' | 'area' | 'height' | 'count' | 'category' | 'zoning' | 'date' | 'phone' | 'website' | 'percent' | 'generic' — What a column *means* — drives FE rendering and formatting. Independent from ``ColumnDataType`` (the storage type hint): a column can be ``data_type=number, semantic_type=currency`` (parcel ``landval``) or ``data_type=number, semantic_type=area`` (``area_acres``). The FE renderer keys off ``semantic_type``, not ``data_type``. Lives here (with the wire model) rather than on ``LayerKind`` because under the self-describing contract this rides on ``LayerColumn`` across the wire — ``LayerKind`` is the backend-only producer that hydrates it.
      - `role` 'stat' | 'other' — Narrow detail-panel placement signal, orthogonal to ``semantic_type``. The one signal that cannot be derived from ``semantic_type``: parcel ``parval`` and ``improvval`` are both ``CURRENCY``, but only ``parval`` is a headline stat. ``role`` carries that; ``semantic_type`` carries content/format. Ordering and visibility live on the *view*, not here.
      - `description` string, nullable — Human-readable column description surfaced in the table column metadata. Hydrated server-side (canonical column metadata or the agent's declaration), not persisted as a separate wire bag.
    - `data_restricted` boolean — Share-response-only signal that the layer's geometry is shown while every per-row attribute is withheld (parcel layers on anonymous share links). Always False on authenticated reads; set True solely by the shared-project layer builder. The FE renders a 'data unavailable in the public version' state in place of an empty grid.
    - `title_template` string[] — Ordered column keys the FE concatenates to compose a feature's title (e.g. `[parcelnumb]`, `[primary_address_full]`). Sourced from the layer's `LayerKind`, empty for permissive kinds — the FE keeps its generic title fallback for the long tail.
    - `address_template` string[] — Ordered column keys the FE concatenates to compose a feature's address line. Sourced from the layer's `LayerKind`; may reference keys that aren't typed canonical columns (city, state_abbr, …). Empty for permissive kinds.
  - `input_rows` integer, required
  - `uploaded_rows` integer, required
  - `truncated` boolean, required
  - `geometry_mode` string, required
  - `out_of_scope_rows` integer

## Other responses

- `422` — Validation Error

---

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