# Uploads Source: https://www.hotdata.dev/docs/api-reference/uploads Site index: https://www.hotdata.dev/llms.txt Upload files, then reference them by ID when loading a managed table. You create an upload session, then `PUT` the bytes straight to the URL it returns (so the file never passes through this API), and finalize. ### Upload flow 1. `POST /v1/uploads` — returns a one-time `finalize_token` plus one of three shapes: • a single `url` (`mode: single`) — `PUT` the whole file to it; • a list of `part_urls` (`mode: multipart`) when you declared the size — `PUT` each part to its entry; • when you omit `declared_size_bytes` (streaming), `mode: multipart` with a `part_size` but no `part_urls` — call `POST /v1/uploads/{upload_id}/parts` with a batch of `part_numbers` to get a `PUT` URL for each part you're about to upload. 2. `PUT` the bytes: the whole file to `url`, or each part to its URL (keeping each part's `ETag`). For a streaming upload, slice the file into `part_size` chunks and mint part URLs as you go. 3. `POST /v1/uploads/{upload_id}/finalize` with the token (and, for multipart, the part `ETag`s) to confirm the upload and make it usable. ### Which approach to use - **One file, or a handful** — create a session, `PUT`, then finalize. - **Many small files** — `POST /v1/uploads/batch` creates up to 100 sessions in one call; upload and finalize each one independently, at your own pace. - **A large file** — small files are returned as a single `PUT`; larger files are returned as a multi-part upload automatically, which you can upload in parallel and resume part by part. ## Create upload session `POST /v1/uploads` Create an upload session for a file you will upload directly to the URL the response carries, without sending it through this API. The response is one of three shapes. For a small file (`mode: single`) it contains a short-lived `url` to `PUT` the whole file to. For a large file with a known size (`mode: multipart`) it contains `part_urls` and `part_size`: split the file into `part_size`-byte chunks (the last is the remainder) and `PUT` chunk *i* (1-based) to `part_urls[i - 1]`, keeping each response's `ETag`. Slice by `part_size`, not by an even division across the number of part URLs (which can make a non-final part too small). For a file whose size you do not know up front, omit `declared_size_bytes`: the response is `mode: multipart` with a `part_size` but NO `part_urls`. As you stream, call `POST /v1/uploads/{upload_id}/parts` with a batch of `part_numbers` to mint per-part `PUT` URLs, `PUT` each part and keep its `ETag`, then finalize as for any multipart upload. In all cases the response also includes a one-time `finalize_token`. After uploading, call the finalize endpoint with the token (and, for multipart, the `{part_number, e_tag}` list) to make the upload usable as managed-table contents. The returned upload ID can then be passed to the managed-table load endpoint. You may hint a preferred part size with `part_size`; the service clamps it to the allowed range and ignores it for single-`PUT` uploads. A `501` with error code `PRESIGN_UNSUPPORTED` means this deployment cannot issue upload URLs; send the data inline on the load endpoint instead. **Request body** - `checksum_algo` `string,null` — Integrity checksum algorithm you are volunteering for this file. Currently only `sha256` is accepted. Optional; pair with `checksum_value`. - `checksum_value` `string,null` — Integrity checksum value, paired with `checksum_algo`. Optional. - `content_encoding` `string,null` — Content encoding to record for the uploaded file (for example `gzip`). Optional. - `content_type` `string,null` — Content type to record for the uploaded file (for example the Parquet, CSV, or JSON MIME type). Optional. - `declared_size_bytes` `integer,null` — The exact size, in bytes, of the file you will upload. Optional. When provided, it is validated at create time against the maximum allowed size, and again at finalize against the bytes actually uploaded — a mismatch fails the finalize. Omit it to create a streaming (unknown-size) upload: the session is always multi-part and returns no part URLs up front; instead you mint part URLs on demand from `POST /v1/uploads/{upload_id}/parts` as you upload, and finalize validates only that the file is non-empty. Min: `0` - `filename` `string,null` — Original file name, recorded with the upload for your own bookkeeping. Optional and advisory — it does not affect how the file is uploaded or loaded. - `part_size` `integer,null` — Preferred size, in bytes, of each part for a large (multi-part) upload. Optional hint — the service clamps it to the allowed part-size range and to the maximum number of parts, and ignores it for small files uploaded with a single `PUT`. Omit to let the service choose. Min: `0` ```json { "checksum_algo": "sha256", "checksum_value": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "content_type": "application/vnd.apache.parquet", "declared_size_bytes": 10485760, "filename": "orders.parquet", "part_size": 8388608 } ``` **Response** `201` — Upload session created - `finalize_token` `string` — **required**. One-time token that authorizes finalizing this upload. Returned exactly once at create time — store it; it cannot be retrieved again. - `headers` `object` — **required**. Headers you must send verbatim with each `PUT`. Currently always empty; present so a future mode can require signed headers without changing the response shape. - `mode` `string` — **required**. Upload mode: `single` (upload the whole file with one `PUT` to `url`) or `multipart` (upload each part with one `PUT` to the matching entry in `part_urls`). Modeled as a string so additional modes can be added later without breaking clients. - `part_size` `integer,null` — For a `multipart` upload (both known-size and streaming), the size in bytes to split the file into: send bytes `[(i-1) * part_size, i * part_size)` as part *i*, with the last part carrying the remainder. Slice by this value — do **not** divide the file evenly by the number of parts, which can make a non-final part smaller than the 5 MiB minimum for a non-final part (the upload then fails at finalize). Absent for `single` uploads. Min: `0` - `part_urls` `string`[] | `null` — For a known-size `multipart` upload, the per-part URLs in ascending part order: `PUT` your file's part *i* (1-based) to `part_urls[i - 1]` and keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. Absent for `single` uploads, and also absent for a streaming (unknown-size) `multipart` upload — there, mint part URLs on demand via `POST /v1/uploads/{upload_id}/parts`. - `upload_id` `string` — **required**. Identifier for this upload. Pass it to the finalize endpoint and to the managed-table load endpoint once finalized. - `url` `string,null` — The URL to `PUT` the raw file bytes to, for a `single` upload. Short-lived — upload promptly and finalize. Absent for `multipart` uploads (use `part_urls`). ```json { "finalize_token": "string", "headers": {}, "mode": "string", "part_size": 0, "part_urls": [ "string" ], "upload_id": "string", "url": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid request (e.g. file too large, unsupported checksum algorithm) | | `501` | This deployment cannot issue upload URLs | ## Create upload sessions in bulk `POST /v1/uploads/batch` Create upload sessions for several files in one request. Each file is planned independently and the response returns one session per requested file, in the same order. Each session is finalized separately via the finalize endpoint, so you can upload and finalize files at your own pace. A `501` with error code `PRESIGN_UNSUPPORTED` means this deployment cannot issue upload URLs; send the data inline on the load endpoint instead. **Request body** - `uploads` `CreateUploadRequest`[] — **required** - `checksum_algo` `string,null` — Integrity checksum algorithm you are volunteering for this file. Currently only `sha256` is accepted. Optional; pair with `checksum_value`. - `checksum_value` `string,null` — Integrity checksum value, paired with `checksum_algo`. Optional. - `content_encoding` `string,null` — Content encoding to record for the uploaded file (for example `gzip`). Optional. - `content_type` `string,null` — Content type to record for the uploaded file (for example the Parquet, CSV, or JSON MIME type). Optional. - `declared_size_bytes` `integer,null` — The exact size, in bytes, of the file you will upload. Optional. When provided, it is validated at create time against the maximum allowed size, and again at finalize against the bytes actually uploaded — a mismatch fails the finalize. Omit it to create a streaming (unknown-size) upload: the session is always multi-part and returns no part URLs up front; instead you mint part URLs on demand from `POST /v1/uploads/{upload_id}/parts` as you upload, and finalize validates only that the file is non-empty. Min: `0` - `filename` `string,null` — Original file name, recorded with the upload for your own bookkeeping. Optional and advisory — it does not affect how the file is uploaded or loaded. - `part_size` `integer,null` — Preferred size, in bytes, of each part for a large (multi-part) upload. Optional hint — the service clamps it to the allowed part-size range and to the maximum number of parts, and ignores it for small files uploaded with a single `PUT`. Omit to let the service choose. Min: `0` ```json { "uploads": [ { "content_type": "text/csv", "declared_size_bytes": 4096, "filename": "orders.csv" }, { "content_type": "text/csv", "declared_size_bytes": 2048, "filename": "customers.csv" } ] } ``` **Response** `201` — Upload sessions created - `uploads` `UploadSessionResponse`[] — **required** - `finalize_token` `string` — **required**. One-time token that authorizes finalizing this upload. Returned exactly once at create time — store it; it cannot be retrieved again. - `headers` `object` — **required**. Headers you must send verbatim with each `PUT`. Currently always empty; present so a future mode can require signed headers without changing the response shape. - `mode` `string` — **required**. Upload mode: `single` (upload the whole file with one `PUT` to `url`) or `multipart` (upload each part with one `PUT` to the matching entry in `part_urls`). Modeled as a string so additional modes can be added later without breaking clients. - `part_size` `integer,null` — For a `multipart` upload (both known-size and streaming), the size in bytes to split the file into: send bytes `[(i-1) * part_size, i * part_size)` as part *i*, with the last part carrying the remainder. Slice by this value — do **not** divide the file evenly by the number of parts, which can make a non-final part smaller than the 5 MiB minimum for a non-final part (the upload then fails at finalize). Absent for `single` uploads. Min: `0` - `part_urls` `string`[] | `null` — For a known-size `multipart` upload, the per-part URLs in ascending part order: `PUT` your file's part *i* (1-based) to `part_urls[i - 1]` and keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. Absent for `single` uploads, and also absent for a streaming (unknown-size) `multipart` upload — there, mint part URLs on demand via `POST /v1/uploads/{upload_id}/parts`. - `upload_id` `string` — **required**. Identifier for this upload. Pass it to the finalize endpoint and to the managed-table load endpoint once finalized. - `url` `string,null` — The URL to `PUT` the raw file bytes to, for a `single` upload. Short-lived — upload promptly and finalize. Absent for `multipart` uploads (use `part_urls`). ```json { "uploads": [ { "finalize_token": "string", "headers": {}, "mode": "string", "part_size": 0, "part_urls": [ "string" ], "upload_id": "string", "url": "string" } ] } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid request (e.g. a file too large, unsupported checksum algorithm) | | `501` | This deployment cannot issue upload URLs | ## Finalize upload `POST /v1/uploads/{upload_id}/finalize` Confirm that a file has been uploaded and make it usable as managed-table contents. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header. When you declared a size at create time, the uploaded file's size is validated against it and a mismatch is rejected. An upload created without a declared size is finalized from its uploaded parts; it must be non-empty and is rejected if it exceeds the server's maximum upload size. Finalize is exactly-once: a second finalize of the same upload is rejected. **Path parameters** - `upload_id` `string` — **required**. Upload session ID returned at create time **Headers** - `X-Upload-Finalize-Token` `string` — **required**. One-time finalize token returned when the session was created **Request body** - `parts` `FinalizeUploadPart`[] | `null` — Parts to assemble, for a multi-part upload. Omit for single-`PUT` uploads (the common case). - `e_tag` `string` — **required**. The `ETag` response header returned by that part's `PUT`. - `part_number` `integer` — **required**. The 1-based part number you uploaded this part as. ```json { "parts": [ { "e_tag": "\"9f8c1e5b7a2d4f60b3c8e1a9d7f4b206\"", "part_number": 1 } ] } ``` **Response** `200` — Upload finalized - `content_type` `string,null` - `created_at` `string` — **required** - `size_bytes` `integer` — **required**. The validated size of the uploaded file in bytes. - `status` `string` — **required** - `upload_id` `string` — **required** ```json { "content_type": "string", "created_at": "2026-01-01T00:00:00Z", "size_bytes": 0, "status": "string", "upload_id": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid finalize token, uploaded size mismatch, missing file, or upload not finalizable | | `404` | Upload session not found | ## Mint upload part URLs `POST /v1/uploads/{upload_id}/parts` Get short-lived upload URLs for specific parts of a multi-part upload. This is required for a streaming (unknown-size) upload — created by omitting the declared size — which mints no part URLs up front. It also works for a known-size multi-part upload: use it to re-mint a part whose URL expired before you uploaded that part. Supply the `finalize_token` returned when the session was created, in the `X-Upload-Finalize-Token` header, and the 1-based `part_numbers` you want URLs for. `PUT` each part's bytes to its URL, keep each response's `ETag`, then pass the `{part_number, e_tag}` list to finalize. You may mint parts in batches as you upload, and re-mint a part number whose URL expired before you finished uploading it. **Path parameters** - `upload_id` `string` — **required**. Upload session ID returned at create time **Headers** - `X-Upload-Finalize-Token` `string` — **required**. One-time finalize token returned when the session was created **Request body** - `part_numbers` `integer`[] — **required**. The 1-based part numbers to mint URLs for. Must be non-empty; each number must be between 1 and the maximum number of parts allowed. ```json { "part_numbers": [ 1, 2, 3 ] } ``` **Response** `200` — Minted part URLs - `parts` `MintedUploadPartResponse`[] — **required**. The minted part URLs, in ascending part-number order. `PUT` each part's bytes to its URL and keep the response's `ETag` to pass to finalize. - `part_number` `integer` — **required**. The 1-based part number this URL is for. - `url` `string` — **required**. Short-lived URL to `PUT` this part's bytes to. Keep the response's `ETag` and pass the `{part_number, e_tag}` pair to finalize. ```json { "parts": [ { "part_number": 0, "url": "string" } ] } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid finalize token, invalid part numbers, batch too large, or the upload is not a multi-part upload | | `404` | Upload session not found | | `501` | This deployment cannot issue upload URLs |