# Upload presets

An upload preset is a saved set of upload settings with its own ID. Browsers and apps upload through it without an API key, and your server can upload through it too, so every upload of that kind follows the same rules. Manage presets in [Upload presets](/portal/upload-presets). For a step-by-step setup, see the [quick start](/docs).

## Who can upload

Each preset accepts browser uploads in one of two ways:

| Access | How a browser uploads | Use it for |
| --- | --- | --- |
| **Users your server approves** (`token`) | With an upload token your server signed for that upload | Apps with sign-in |
| **Anyone with the preset ID** (`anyone`) | With the preset ID alone, up to the preset's daily limit | Static sites, public forms and prototypes |

New presets require a token. A preset open to anyone must have a daily upload limit.

## Settings

| Setting | Effect |
| --- | --- |
| File types | MIME types the preset accepts, such as `image/*` or `video/mp4`. Empty accepts every supported type. |
| Maximum file size | Largest file the preset accepts. Multipart uploads are checked as their parts arrive. |
| Folder | Folder that uploads go into. Browser uploads can't choose another one. |
| Visibility | Visibility of uploaded assets. |
| Keep content after moderation | Keeps each uploaded file so you can serve it from the CDN. When off, the file is deleted once it's moderated and only its record and report remain. |
| Read text in images | Runs OCR on uploaded images and moderates the text it finds as well. |
| Show the decision to the uploader | When on, browser uploads see `status` for their own upload. Reports, metadata and URLs are never returned to the browser. |
| Daily upload limit | Most browser uploads per UTC day. Uploads from your server don't count against it. |
| Allowed sites | Origins allowed to upload from the browser, such as `https://example.com`. Requests from other sites, or without an `Origin` header, are refused. |
| Accept uploads | Turn off to pause the preset without deleting it. |

A browser can only name and describe its upload. The folder, visibility, metadata, processing options and `externalId` come from the preset or the upload token; sending them from the browser fails with `upload-preset/field-not-allowed`.

## Upload tokens

An upload token lets one browser upload through a preset that requires tokens. Sign tokens on your server with the JavaScript, Python, Go, Java, Ruby or Rust SDK. Signing happens locally with your API key and makes no request to CalmLens.

| SDK | Method | Options |
| --- | --- | --- |
| JavaScript | `client.uploadTokens.sign` | `uploadPresetId`, `externalId`, `expiresInSeconds` |
| Python | `client.upload_tokens.sign` | `upload_preset_id`, `external_id`, `expires_in_seconds` |
| Go | `client.UploadTokens.Sign` | `UploadPresetID`, `ExternalID`, `ExpiresInSeconds` |
| Java | `client.uploadTokens.sign` | `uploadPresetId`, `externalId`, `expiresInSeconds` |
| Ruby | `client.upload_tokens.sign` | `upload_preset_id:`, `external_id:`, `expires_in_seconds:` |
| Rust | `client.upload_tokens().sign` | `upload_preset_id`, `external_id`, `expires_in_seconds` |

Each returns the token and when it expires, in milliseconds since the Unix epoch.

**JavaScript**

```javascript
const { token, expiresAt } = await client.uploadTokens.sign({
  uploadPresetId: "your-upload-preset-id",
  externalId: "post-42",
  expiresInSeconds: 600,
});
```

**python**

```python
import os
from calmlens import CalmLensServerClient

client = CalmLensServerClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id="your-project-id",
)

signed = client.upload_tokens.sign(
    upload_preset_id="your-upload-preset-id",
    external_id="post-42",
    expires_in_seconds=600,
)
print(signed["token"], signed["expiresAt"])
client.close()
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    calmlens "github.com/Censkh/CalmLens/packages/go-sdk"
)

func main() {
    client, err := calmlens.NewServerClient(calmlens.Options{
        APIKey: os.Getenv("CALMLENS_API_KEY"),
        ProjectID: "your-project-id",
    })
    if err != nil { log.Fatal(err) }

    signed, err := client.UploadTokens.Sign(context.Background(), calmlens.SignUploadTokenOptions{
        UploadPresetID: "your-upload-preset-id",
        ExternalID: calmlens.Ptr("post-42"),
        ExpiresInSeconds: 600,
    })
    if err != nil { log.Fatal(err) }
    fmt.Println(signed.Token, signed.ExpiresAt)
}
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::ServerClient.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
signed = client.upload_tokens.sign(upload_preset_id: "your-upload-preset-id", external_id: "post-42", expires_in_seconds: 600)
puts signed["token"], signed["expiresAt"]
client.close
```

**rust**

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = ServerClient::new(std::env::var("CALMLENS_API_KEY")?, std::env::var("CALMLENS_PROJECT_ID")?)?;
    let signed = client.upload_tokens().sign(SignUploadToken { external_id: Some("post-42".into()), expires_in_seconds: Some(600), ..SignUploadToken::new("your-upload-preset-id") })?;
    println!("{} {}", signed.token, signed.expires_at);
    Ok(())
}
```

**java**

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

public class Example {
  public static void main(String[] args) {
    var client = new CalmLensServerClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    var signed = client.uploadTokens.sign(new SignUploadToken("your-upload-preset-id").externalId("post-42").expiresInSeconds(600));
    System.out.println(signed.token() + " " + signed.expiresAt());
  }
}
```

Each token:

- uploads one asset. A second upload with it fails with `upload-token/used`; an upload that fails frees the token for a retry.
- starts an upload until it expires, and stays valid for that upload's later parts and status checks for up to a day.
- acts for the API key that signed it. The key must be able to create assets in the preset's folder. Revoking the key stops its tokens working.

## Uploading from a browser

Only the JavaScript SDK runs in browsers and apps. Its `CalmLensAppClient`, from `@calmlens/sdk/app`, uploads through an upload preset instead of an API key:

```html
<script type="module">
  import { CalmLensAppClient } from "@calmlens/sdk/app";

  const client = new CalmLensAppClient({
    uploadPresetId: "your-upload-preset-id",
    token: () => fetchTokenFromYourServer(), // only for presets that require a token
  });
</script>
```

This client has only `assets.create`, `assets.moderate`, `assets.uploadPart` and `assets.retrieve`, for its own uploads. They return the asset's `id`, `name`, `uploadPresetId` and `createdAt`, plus `status` when the preset shows decisions. It refuses an API key with `sdk/invalid-config`.

Over HTTP, browser uploads use the same bodies and query parameters as the project upload endpoints, under the preset's path, with the token as `Authorization: Bearer <token>`:

| Request | Path |
| --- | --- |
| Upload a file or start a multipart upload | `POST /v1/upload-presets/{uploadPresetId}/assets` |
| Upload the next part | `POST /v1/upload-presets/{uploadPresetId}/assets/{assetId}/part` |
| Check an upload | `GET /v1/upload-presets/{uploadPresetId}/assets/{assetId}` |

## Uploading from your server through a preset

Pass `uploadPresetId` (`upload_preset_id` in Python and Ruby, `UploadPresetID` in Go, `upload_preset_id` in Rust) to `client.assets.create()` with your API key, in any SDK, to apply a preset's folder, visibility, file types and size limit. You can still set `externalId`, metadata and processing options. A folder or visibility that differs from the preset's fails with `upload-preset/field-not-allowed`.

## Errors

| Code | Meaning |
| --- | --- |
| `upload-preset/not-found` | No preset with this ID in the project. |
| `upload-preset/disabled` | The preset is paused. |
| `upload-preset/token-required` | The preset requires an upload token and none was sent. |
| `upload-preset/content-type-not-allowed` | The preset doesn't accept this file type. |
| `upload-preset/too-large` | The file is larger than the preset allows. |
| `upload-preset/daily-limit-reached` | The preset reached its daily limit. `Retry-After` says when the next UTC day starts. |
| `upload-preset/origin-not-allowed` | The site isn't in the preset's allowed sites. |
| `upload-preset/field-not-allowed` | The upload set a field the preset or token controls. |
| `upload-token/invalid` | The token is malformed, for another preset, or signed by a key that can't upload here. |
| `upload-token/expired` | The token expired before the upload started. |
| `upload-token/used` | The token already uploaded an asset. |
