---
title: "Upload a CSV file as a queryable data source."
method: POST
path: "/v1/uploads"
tags: ["uploads"]
---

# Upload a CSV file as a queryable data source.

`POST /v1/uploads`

## Request

- Base URL: `http://localhost:8080`
- URL: `http://localhost:8080/v1/uploads`
- Auth: one of:
  - API key in cookie `dvt_session`
  - HTTP bearer

Converts a CSV upload to Parquet via the analytics engine and stores it in R2 as a
`parquet_s3` data source (DVT-753 PR-2). This durable CSV→R2 storage path is
**trial-only**: per ADR-0011 (as amended by SEC-61), a paid org's data must land only in
its own warehouse, never dvt-hosted R2, so a paid or converted org's request is refused
with `403` even though it holds `connection:manage`.

**Auth:** Requires `connection:manage` scope (admin or owner role) on a plan entitled to
it — this is the same entitlement check every warehouse-connection write path uses — **AND**
the org must be an active or pending (minted, clock not yet started) unconverted trial. Both
checks must pass; either failing returns `403`.

**Side effect:** a successful upload by a minted-but-unstarted trial starts the 14-day trial
clock (`trial_ends_at`); clients should refetch `/v1/me` after a 201.

**File limits:** Maximum 50 MB. Filename must end in `.csv`. Content-Type must be one of:
`text/csv`, `application/csv`, `application/vnd.ms-excel`, `text/plain`.

**Cap:** Up to 5 uploaded sources per org. Uploaded sources are counted separately from
warehouse connections and do not consume a trial's 1-connection budget.

**Internal representation:** Uploaded sources are stored with `auth_method=platform_managed`.
This is an internal implementation detail — platform_managed sources are **never** returned by
`GET /v1/connections` or `GET /v1/connections/{id}`; use `GET /v1/uploads` /
`DELETE /v1/uploads/{id}` to list or remove them. Use `POST /v1/data/query` with the
returned `name` as the `sourceId` to query the uploaded data.

## Response `201`

CSV uploaded and registered as a queryable data source.

- object
  - `data` UploadedSource: A CSV file that has been converted to Parquet and registered as a queryable data source (DVT-753 PR-2). Use the `name` as `sourceId` in `POST /v1/data/query`. Note: `authMethod` is intentionally absent. Uploaded sources use the internal `platform_managed` auth method, which is never part of the public connections contract.
    - `id` string, uuid, required: The data source row UUID. This is the database primary key — not the query handle. Use `name` (not `id`) as `sourceId` in `POST /v1/data/query`.
    - `name` string, required: The source name derived from the uploaded filename (without extension). This is the query handle: use this value as `sourceId` in `POST /v1/data/query`. The query path resolves sources by name within the org, not by id.
    - `type` 'parquet_s3', required: Always `parquet_s3` for uploaded sources.
    - `columns` object[], required: Schema inferred by the engine from the CSV header row.
      - `name` string
      - `type` string
    - `createdAt` string, date-time, required
  - `meta` Meta: Response metadata.
    - `requestId` string, required: Correlates with the X-Request-Id header.
    - `total` integer: Item count, present on collection responses.
    - `nextCursor` string, nullable: Opaque keyset-pagination cursor, present on paginated responses (e.g. GET /v1/dashboards) when more rows exist beyond this page. Pass it back as the `cursor` query param to fetch the next page; absent or null on the last page.
    - `roleResolveDegraded` boolean: Snowflake mode only (DVT-2260), on GET /v1/dashboards: present and true when the caller's live Snowflake role resolution failed or was unavailable — role-shared dashboards may be missing from this page. Omitted (not merely false) whenever resolution succeeded or the role-share arm does not apply (off snowflake mode, an org admin/owner, or off the ingress listener).

## Other responses

- `400`: The file is missing, has a wrong extension, or could not be parsed as CSV.
- `401`: Missing, invalid, or expired session.
- `403`: Caller lacks `connection:manage` scope (or the org's plan is not entitled to it), the per-org uploaded-source cap (5) has been reached, or the org is not an active or pending (minted, clock not yet started) trial (durable CSV upload is trial-only).
- `409`: An uploaded source with this filename (without extension) already exists.
- `413`: The uploaded file exceeds the 50 MB limit.

---

[API](https://skmtc.dev/dvt/apis/dvt-rest-api.md) · [All operations](https://skmtc.dev/dvt/apis/dvt-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dvt/apis/dvt-rest-api/revisions/00016f504165?raw)
