# Multipart uploads

Use this guide when you need explicit control over upload chunks. For ordinary files, let your SDK select the upload mode.

## Sequential uploads

Use multipart upload when the full file is inconvenient to send in one request. Send parts in order, one at a time.

The JavaScript SDK streams multipart bytes through [`POST /projects/:projectId/assets/multipart-upload`](/docs/api/assets#start-multipart-file-upload) for the first part and [`POST /assets/:assetId/part`](/docs/api/assets#upload-asset-part) for later parts. The JSON/base64 equivalents shown below use [`POST /projects/:projectId/assets`](/docs/api/assets#create-asset) and the same flat continuation endpoint.

**python**

```python
import os
from calmlens import CalmLensClient

client = CalmLensClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id=os.environ["CALMLENS_PROJECT_ID"],
)

with open("part-1.bin", "rb") as first_part:
    uploading = client.assets.create({
        "name": "large-image.png",
        "part": {"data": first_part, "type": "image/png", "partNumber": 1, "isFinal": False},
    })
with open("part-2.bin", "rb") as final_part:
    completed = client.assets.upload_part({
        "assetId": uploading["id"],
        "part": {"data": final_part, "partNumber": 2, "isFinal": True},
    })
print(completed["id"])
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    calmlens "github.com/Censkh/CalmLens/packages/go-sdk"
)

func main() {
    client, err := calmlens.NewClient(calmlens.Options{APIKey: os.Getenv("CALMLENS_API_KEY"), ProjectID: os.Getenv("CALMLENS_PROJECT_ID")})
    if err != nil { log.Fatal(err) }
    ctx := context.Background()
    firstPart, err := os.ReadFile("part-1.bin")
    if err != nil { log.Fatal(err) }
    finalPart, err := os.ReadFile("part-2.bin")
    if err != nil { log.Fatal(err) }
    uploading, err := client.Assets.Create(ctx, calmlens.SubmitAssetOptions{Name: "large-image.png", Part: &calmlens.Content{Bytes: firstPart, Type: "image/png", PartNumber: 1, IsFinal: false}})
    if err != nil { log.Fatal(err) }
    completed, err := client.Assets.UploadPart(ctx, calmlens.UploadAssetPartOptions{AssetID: uploading.ID, Part: calmlens.Content{Bytes: finalPart, PartNumber: 2, IsFinal: true}})
    if err != nil { log.Fatal(err) }
    fmt.Println(completed.ID)
}

```

**JavaScript**

```javascript
const uploading = await client.assets.create({
  name: "large-image.png",
  part: {
    uint8Array: firstPartBytes,
    type: "image/png",
    partNumber: 1,
    isFinal: false,
  },
});

const completed = await client.assets.uploadPart({
  assetId: uploading.id,
  part: {
    uint8Array: finalPartBytes,
    partNumber: 2,
    isFinal: true,
  },
});
```

**ruby**

```ruby
require 'calmlens'
client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
uploading = client.assets.create(name: 'large-image.png', part: {data: File.binread('part-1.bin'), type: 'image/png', part_number: 1, is_final: false})
completed = client.assets.upload_part(asset_id: uploading['id'], part: {data: File.binread('part-2.bin'), part_number: 2, is_final: true})
puts completed['id']
client.close
```

**rust**

```rust
use calmlens::{models::*, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(std::env::var("CALMLENS_API_KEY")?, std::env::var("CALMLENS_PROJECT_ID")?)?;
    // Content::file streams each part from disk instead of reading it into memory.
    let first = Content::file("part-1.bin", "image/png")?.part(1, false);
    let uploading = client.assets().create(SubmitAsset::multipart("large-image.png", first)).await?;
    let last = Content::file("part-2.bin", "image/png")?.part(2, true);
    let completed = client.assets().upload_part(UploadPart { asset_id: uploading.id, part: last, ..Default::default() }).await?;
    println!("{}", completed.id);
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensClient;
import com.calmlens.Models.*;

public class Example {
  public static void main(String[] args) throws Exception {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    byte[] first = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("part-1.bin"));
    Asset uploading = client.assets.create(new SubmitAsset().name("large-image.png")
        .part(Content.bytes(first, "image/png").partNumber(1).isFinal(false)));
    byte[] last = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("part-2.bin"));
    Asset completed = client.assets.uploadPart(new UploadPart(uploading.id(), Content.bytes(last, "image/png").partNumber(2).isFinal(true)));
    System.out.println(completed.id());
  }
}
```

**curl**

```sh
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/assets/multipart-upload?name=large-image.png&contentType=image%2Fpng&queue=true&partNumber=1&isFinal=false" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "X-CalmLens-Upload: part" \
  -H "Content-Type: image/png" \
  --data-binary @part-1.bin

# Set ASSET_ID to the id returned by the first request.
curl -X POST "https://api.calmlens.com/v1/assets/$ASSET_ID/part?queue=true&partNumber=2&isFinal=true" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "X-CalmLens-Upload: part" \
  -H "Content-Type: image/png" \
  --data-binary @part-2.bin
```

1. Start the upload with [`POST /projects/:projectId/assets`](/docs/api/assets#create-asset).
2. Send the first chunk in `part.base64` with `part.partNumber: 1`.
3. Continue with [`POST /assets/:assetId/part`](/docs/api/assets#upload-asset-part).
4. Mark the last chunk with `part.isFinal: true`.

Multipart uploads currently expect:

- Chunks provided as either `base64` strings or `int8Array` number arrays.
- Sequential `partNumber` values starting at `1`.
- The first request to include `part.type` for the full asset MIME type.
- Optional `totalSizeInBytes` on the first request if you already know the size of the whole file. See [Declaring the file size](/docs/guides/multipart-uploads#declaring-the-file-size).
- Non-final parts to be at least `5MiB`; only the final part may be smaller.
- A concrete file type. Ambiguous/container request MIME types such as `application/octet-stream` and `multipart/*` are rejected because CalmLens cannot infer how to process or store them as assets.

Raw `ReadableStream` parts must include their exact `sizeInBytes`. Buffer, Blob, base64, and byte-array payload sizes are calculated by the SDK.

If you use base64, split the original bytes first and then base64-encode each part. Do not split one full base64 string into substrings.

Multipart image uploads must be between <code>10MB</code> and <code>20MB</code>. If needed, an image may be re-encoded to a more efficient format during finalization.

### Step 1: create the upload

```bash
curl -X POST "https://api.calmlens.com/v1/projects/your-project-id/assets?queue=true" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "large-image.png",
  "part": {
    "base64": "your-part-1-base64",
    "type": "image/png",
    "partNumber": 1,
    "isFinal": false
  },
  "sizeInBytes": 7340032
}'
```

The response returns an asset with `status: "uploading"` and an `id` you will use for the remaining parts.

### Step 2: send more parts

```bash
curl -X POST "https://api.calmlens.com/v1/assets/your-asset-id/part" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  --data '{
  "part": {
    "base64": "your-part-2-base64",
    "partNumber": 2,
    "isFinal": false
  }
}'
```

### Step 3: finalize the upload

```bash
curl -X POST "https://api.calmlens.com/v1/assets/your-asset-id/part" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  --data '{
  "part": {
    "base64": "your-final-part-base64",
    "partNumber": 3,
    "isFinal": true
  }
}'
```

The final part completes the upload and returns the asset. If a part number is skipped or repeated, the API returns `409`. If the temporary upload is no longer open, the part endpoint also returns `409`.

## Queue mode

Choose `queue` when you start the upload. CalmLens saves the choice with the upload and applies it when the final part arrives, so the final part can come from another process or a restarted client that only knows the asset ID. Leave `queue` off later parts to use the saved choice, or pass it on the final part to override it for that upload.

Uploads started without `queue` are classified synchronously when they finish, like other uploads with `queue=false`. Audio and video are always processed asynchronously.

## Declaring the file size

Pass the size of the whole file as `totalSizeInBytes` on the first request when you know it. CalmLens then:

- Rejects an upload that could never finish before storing any of it, for example an image declared below <code>10MB</code> ([`400 upload/multipart-image-too-small`](/docs/api/assets#post-asset-error-upload-multipart-image-too-small)) or above <code>20MB</code> ([`400 upload/multipart-image-too-large`](/docs/api/assets#post-asset-error-upload-multipart-image-too-large)).
- Refuses a part that would take the upload past the declared size, and a final part that leaves it short, with [`400 upload/size-mismatch`](/docs/api/assets#post-asset-part-error-upload-size-mismatch). The refused part is not stored, so you can send the correct part with the same part number.

Without `totalSizeInBytes`, CalmLens checks the accumulated size as parts arrive and the final total when the upload completes.

For raw uploads, `sizeInBytes` is the size of the request body: the first part on [`POST /projects/:projectId/assets/multipart-upload`](/docs/api/assets#start-multipart-file-upload) and each later part on [`POST /assets/:assetId/part`](/docs/api/assets#upload-asset-part).

## Retrying parts

Parts are sequential, so a part sent twice is refused with [`409 upload/invalid-part-number`](/docs/api/assets#post-asset-part-error-upload-invalid-part-number), and its `details.expectedPartNumber` tells you which part CalmLens needs next. If a request times out, retry it with the same part number: a part that already arrived is refused and you can move on, and a part that didn't arrive is accepted.

The final part closes the upload, so a retried final part normally returns [`409 upload/not-open`](/docs/api/assets#post-asset-part-error-upload-not-open) even though the upload succeeded. Send an `Idempotency-Key` header with the first request and with each part to get the original response back instead. See [Retrying requests safely](/docs/guides/error-handling#retrying-requests-safely).
