# Upload widget

The upload widget sends files from the browser straight to CalmLens, shows each file's progress and moderation decision, and gives your app the asset ID. Point it at an upload preset and a token endpoint, then send the asset ID to your server with the record it belongs to. [Widgets](/docs/widgets) covers installing it, styling and translating.

## Add the upload widget

The widget uploads through an upload preset, so files go straight from the browser to CalmLens and your API key stays on your server. Create a preset that requires a token and shows the decision to the uploader (see [Upload presets](/docs/guides/upload-presets)), then add an endpoint that signs a token for each file:

**JavaScript/TypeScript Server SDK**

```typescript
const { token, expiresAt } = await client.uploadTokens.sign({
  uploadPresetId: "preset_4Lk8Qw2Xm9Rt5Vb3Nc7P",
  externalId: "post-42",
  expiresInSeconds: 600,
});
```

**python**

```python
signed = client.upload_tokens.sign(
    upload_preset_id="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P",
    external_id="post-42",
    expires_in_seconds=600,
)
print(signed["token"], signed["expiresAt"])
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"

    calmlens "github.com/CalmLens/sdks/go"
)

func example(client *calmlens.ServerClient) {

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

**ruby**

```ruby
signed = client.upload_tokens.sign(upload_preset_id: "preset_4Lk8Qw2Xm9Rt5Vb3Nc7P", external_id: "post-42", expires_in_seconds: 600)
puts signed["token"], signed["expiresAt"]
```

**rust**

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

fn example(client: &ServerClient) -> Result<(), Box<dyn std::error::Error>> {
    let signed = client.upload_tokens().sign(SignUploadToken { external_id: Some("post-42".into()), expires_in_seconds: Some(600), ..SignUploadToken::new("preset_4Lk8Qw2Xm9Rt5Vb3Nc7P") })?;
    println!("{} {}", signed.token, signed.expires_at);
    Ok(())
}
```

**java**

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

public class Example {
  public static void example(CalmLensServerClient client) {
    var signed = client.uploadTokens.sign(new SignUploadToken("preset_4Lk8Qw2Xm9Rt5Vb3Nc7P").externalId("post-42").expiresInSeconds(600));
    System.out.println(signed.token() + " " + signed.expiresAt());
  }
}
```

Point `tokenUrl` at that endpoint. The widget sends it a `POST` with the file's `name`, `type` and `size` as JSON and expects `{ token }` back. To fetch tokens another way, pass `token`, a function that receives the file and returns a token.

**React**

```tsx
import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/react";

export function PhotoUploader({ onUploaded }: { onUploaded: (upload: CalmLensUpload) => void }) {
  return (
    <CalmLensUploader
      uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
      tokenUrl="/api/upload-token"
      accept="image/*"
      maxFileSize={10 * 1024 * 1024}
      onUploaded={onUploaded}
    />
  );
}
```

**Vue**

```vue
<script setup lang="ts">
import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/vue";

const emit = defineEmits<{ uploaded: [upload: CalmLensUpload] }>();
</script>

<template>
  <CalmLensUploader
    upload-preset-id="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
    token-url="/api/upload-token"
    accept="image/*"
    :max-file-size="10 * 1024 * 1024"
    @uploaded="(upload) => emit('uploaded', upload)"
  />
</template>
```

**Svelte**

```svelte
<script lang="ts">
  import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/svelte";

  let { onUploaded }: { onUploaded: (upload: CalmLensUpload) => void } = $props();
</script>

<CalmLensUploader
  uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
  tokenUrl="/api/upload-token"
  accept="image/*"
  maxFileSize={10 * 1024 * 1024}
  {onUploaded}
/>
```

**Plain JavaScript**

```javascript
// Defines <calmlens-uploader> and <calmlens-media>. In plain HTML, write the element in your
// markup instead: <calmlens-uploader upload-preset-id="…" token-url="/api/upload-token">.
import "@calmlens/sdk/elements";

export function mountPhotoUploader(container, { onUploaded }) {
  const uploader = document.createElement("calmlens-uploader");
  uploader.setAttribute("upload-preset-id", "preset_4Lk8Qw2Xm9Rt5Vb3Nc7P");
  uploader.setAttribute("token-url", "/api/upload-token");
  uploader.setAttribute("accept", "image/*");
  uploader.setAttribute("max-file-size", String(10 * 1024 * 1024));
  uploader.addEventListener("calmlens-uploaded", (event) => onUploaded(event.detail));
  container.replaceChildren(uploader);
}
```

For each file, the widget:

- Refuses files that don't match `accept` or are larger than `maxFileSize` before sending anything. Your preset's own file types and size limit still apply.
- Shows the bytes sent, then checks the upload until it has a decision, for up to 15 seconds by default (`decisionTimeoutMs`). An upload still `pending` after that is left for your server's webhook to settle.
- Shows the decision: `allowed`, `limited`, `age-restricted`, `blocked` or `purged`. Errors such as [`413 upload-preset/too-large`](/docs/api/assets#post-asset-error-upload-preset-too-large) get their own message, and a failed upload can be retried.
- Calls `onUploaded` with `{ assetId, status, file }` unless the file was blocked or purged. Vue emits `uploaded`, and the custom element dispatches a `calmlens-uploaded` event with the same `detail`.

Send the asset ID to your server with the record it belongs to, and publish from the status your server reads from CalmLens, as described in [Save the asset ID with the post](/docs/guides/upload-component#3-save-the-asset-id-with-the-post).

`multiple` accepts several files, `maxFiles` caps how many (10 by default) and `concurrency` sets how many upload at once (3 by default). `capture` opens the camera on phones. Without `multiple`, the widget shows the picked file in place of the drop zone, with a button to choose another.

## React Native

`@calmlens/sdk/react-native` has the same uploader for React Native and Expo apps, with a pick button in place of the drop zone. Pass `pick`, a function that opens your app's picker and resolves with the picked files, or none when the person cancels. A `File` from `expo-file-system` uploads as it is, and so does anything with a `name`, `type`, `size`, `uri` and `arrayBuffer()`. With `expo-image-picker`:

```tsx
import { type CalmLensUpload, CalmLensUploader, type PickFiles } from "@calmlens/sdk/react-native";
import { File } from "expo-file-system";
import * as ImagePicker from "expo-image-picker";

const pickPhotos: PickFiles = async ({ multiple }) => {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ["images"],
    allowsMultipleSelection: multiple,
  });
  return result.canceled ? [] : result.assets.map((asset) => new File(asset.uri));
};

export function PhotoUploader({
  onUploaded,
}: {
  onUploaded: (upload: CalmLensUpload) => void;
}) {
  return (
    <CalmLensUploader
      uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
      tokenUrl="https://your-app.example/api/upload-token"
      accept="image/*"
      pick={pickPhotos}
      onUploaded={onUploaded}
    />
  );
}
```

The other options work as on the web. To send your app's own sign-in header with each token request, pass `token`, a function that fetches a token, instead of `tokenUrl`. Each picked photo's preview comes from its `uri`, and its bytes are read when its upload starts. The pick buttons have the `testID` `calmlens-choose` for end-to-end tests. For your own screens, `useCalmLensUpload` runs the same upload queue; the [Calmsnap example](/docs/examples#calmsnap-mobile-photo-uploads-javascript) is built on it.

## Let anyone upload

For a preset open to anyone with its ID, leave out `tokenUrl`. The preset's daily limit, file types and allowed sites are then what protect your usage; see [Upload without a server](/docs/guides/upload-without-server).

**React**

```tsx
import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/react";

export function PublicPhotoUploader({
  onUploaded,
}: {
  onUploaded: (upload: CalmLensUpload) => void;
}) {
  return (
    <CalmLensUploader
      uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
      accept="image/*"
      maxFileSize={10 * 1024 * 1024}
      onUploaded={onUploaded}
    />
  );
}
```

**Vue**

```vue
<script setup lang="ts">
import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/vue";

const emit = defineEmits<{ uploaded: [upload: CalmLensUpload] }>();
</script>

<template>
  <CalmLensUploader
    upload-preset-id="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
    accept="image/*"
    :max-file-size="10 * 1024 * 1024"
    @uploaded="(upload) => emit('uploaded', upload)"
  />
</template>
```

**Svelte**

```svelte
<script lang="ts">
  import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/svelte";

  let { onUploaded }: { onUploaded: (upload: CalmLensUpload) => void } = $props();
</script>

<CalmLensUploader
  uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
  accept="image/*"
  maxFileSize={10 * 1024 * 1024}
  {onUploaded}
/>
```

**Plain JavaScript**

```javascript
// Defines <calmlens-uploader>. In plain HTML, write the element in your markup instead:
// <calmlens-uploader upload-preset-id="…" accept="image/*">.
import "@calmlens/sdk/elements";

export function mountPublicPhotoUploader(container, { onUploaded }) {
  const uploader = document.createElement("calmlens-uploader");
  uploader.setAttribute("upload-preset-id", "preset_4Lk8Qw2Xm9Rt5Vb3Nc7P");
  uploader.setAttribute("accept", "image/*");
  uploader.setAttribute("max-file-size", String(10 * 1024 * 1024));
  uploader.addEventListener("calmlens-uploaded", (event) => onUploaded(event.detail));
  container.replaceChildren(uploader);
}
```

## Upload when a form is submitted

To upload with the rest of a form, set `autoUpload: false` (the `no-auto-upload` attribute on `<calmlens-uploader>`). Picked files then wait in the `selected` stage until you call `start()` on the upload controller. Create the controller yourself with `useCalmLensUpload` (React, React Native and Vue) or `createUploadController` from your library's import, and pass it to the widget as `controller` (a property on the custom element). The widget then shows that controller's files and takes the upload options from it, so your form can read the picked files, call `start()` when it is submitted and `clear()` to reset. Uploads keep running if the widget unmounts, for example when a form moves to its next step.

## Upload through your own API

When files must pass through your server, for example to attach them to a signed-in user's records, pass an `upload` function instead of an upload preset. It receives the file with an abort `signal` and an `onProgress` callback, and returns the asset ID and, if your API has one, the moderation status. `createProgressFetch(onProgress)` returns a `fetch` that reports the bytes sent. Add `checkStatus` so the widget can check a pending upload; without it, the widget shows the upload as pending. [Upload with your server](/docs/guides/upload-component) builds both endpoints.

**React**

```tsx
import {
  type CalmLensUpload,
  CalmLensUploader,
  type CustomUploadContext,
  createProgressFetch,
} from "@calmlens/sdk/react";

// Your server stores the file with CalmLensServerClient and answers with the asset.
async function uploadPhoto(file: File, { signal, onProgress }: CustomUploadContext) {
  const send = createProgressFetch(onProgress);
  const response = await send("/api/photos", {
    method: "POST",
    headers: { "Content-Type": file.type, "X-File-Name": encodeURIComponent(file.name) },
    body: file,
    signal,
  });
  if (!response.ok) throw new Error(`Upload failed (${response.status})`);
  const { assetId, status } = await response.json();
  return { assetId, status };
}

async function checkPhoto(assetId: string, { signal }: { signal: AbortSignal }) {
  const response = await fetch(`/api/photos/${assetId}`, { signal });
  return (await response.json()).status;
}

export function OwnApiUploader({ onUploaded }: { onUploaded: (upload: CalmLensUpload) => void }) {
  return (
    <CalmLensUploader
      upload={uploadPhoto}
      checkStatus={checkPhoto}
      accept="image/*"
      onUploaded={onUploaded}
    />
  );
}
```

**Vue**

```vue
<script setup lang="ts">
import {
  type CalmLensUpload,
  CalmLensUploader,
  type CustomUploadContext,
  createProgressFetch,
} from "@calmlens/sdk/vue";

const emit = defineEmits<{ uploaded: [upload: CalmLensUpload] }>();

// Your server stores the file with CalmLensServerClient and answers with the asset.
async function uploadPhoto(file: File, { signal, onProgress }: CustomUploadContext) {
  const send = createProgressFetch(onProgress);
  const response = await send("/api/photos", {
    method: "POST",
    headers: { "Content-Type": file.type, "X-File-Name": encodeURIComponent(file.name) },
    body: file,
    signal,
  });
  if (!response.ok) throw new Error(`Upload failed (${response.status})`);
  const { assetId, status } = await response.json();
  return { assetId, status };
}

async function checkPhoto(assetId: string, { signal }: { signal: AbortSignal }) {
  const response = await fetch(`/api/photos/${assetId}`, { signal });
  return (await response.json()).status;
}
</script>

<template>
  <CalmLensUploader
    :upload="uploadPhoto"
    :check-status="checkPhoto"
    accept="image/*"
    @uploaded="(upload) => emit('uploaded', upload)"
  />
</template>
```

**Svelte**

```svelte
<script lang="ts">
  import {
    type CalmLensUpload,
    CalmLensUploader,
    type CustomUploadContext,
    createProgressFetch,
  } from "@calmlens/sdk/svelte";

  let { onUploaded }: { onUploaded: (upload: CalmLensUpload) => void } = $props();

  // Your server stores the file with CalmLensServerClient and answers with the asset.
  async function uploadPhoto(file: File, { signal, onProgress }: CustomUploadContext) {
    const send = createProgressFetch(onProgress);
    const response = await send("/api/photos", {
      method: "POST",
      headers: { "Content-Type": file.type, "X-File-Name": encodeURIComponent(file.name) },
      body: file,
      signal,
    });
    if (!response.ok) throw new Error(`Upload failed (${response.status})`);
    const { assetId, status } = await response.json();
    return { assetId, status };
  }

  async function checkPhoto(assetId: string, { signal }: { signal: AbortSignal }) {
    const response = await fetch(`/api/photos/${assetId}`, { signal });
    return (await response.json()).status;
  }
</script>

<CalmLensUploader upload={uploadPhoto} checkStatus={checkPhoto} accept="image/*" {onUploaded} />
```

**Plain JavaScript**

```javascript
import { createProgressFetch } from "@calmlens/sdk/elements";

// Your server stores the file with CalmLensServerClient and answers with the asset.
async function uploadPhoto(file, { signal, onProgress }) {
  const send = createProgressFetch(onProgress);
  const response = await send("/api/photos", {
    method: "POST",
    headers: { "Content-Type": file.type, "X-File-Name": encodeURIComponent(file.name) },
    body: file,
    signal,
  });
  if (!response.ok) throw new Error(`Upload failed (${response.status})`);
  const { assetId, status } = await response.json();
  return { assetId, status };
}

async function checkPhoto(assetId, { signal }) {
  const response = await fetch(`/api/photos/${assetId}`, { signal });
  return (await response.json()).status;
}

export function mountOwnApiUploader(container, { onUploaded }) {
  const uploader = document.createElement("calmlens-uploader");
  uploader.setAttribute("accept", "image/*");
  uploader.upload = uploadPhoto;
  uploader.checkStatus = checkPhoto;
  uploader.addEventListener("calmlens-uploaded", (event) => onUploaded(event.detail));
  container.replaceChildren(uploader);
}
```

Throw an error to show the file as failed. A `CalmLensError` shows the message for its code, so your API's error codes can have their own messages.
