> ## Documentation index
> Fetch the complete documentation index at: https://upstash.com/docs/llms.txt
> Ask it with GET https://upstash.com/docs/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.
> Feature requests: https://upstash.com/docs/common/help/feature-requests.md lists what users asked for and how to vote for a request or add one.
> Page rating: `GET https://upstash.com/docs/feedback?path=/docs/blob/uploads/upload-client&helpful=true` (or `false`) records whether this page helped. An optional `comment` (URL-encoded, at most 500 characters) says what was missing or wrong. It is anonymous and needs no key. Comments hold no code, secrets or personal data.

# Upload Client

> useUpload, useServerUpload and uploadHooks, for uploading from the browser straight to storage.

`@upstash/blob/react` is the browser side of a direct upload. `uploadHooks` binds the hooks to your [upload handler](/blob/uploads/upload-handler), and `useUpload` runs the upload and reports its progress. There is a plain function for apps without React, and `useServerUpload` for routes where the bytes do pass through your app.

The browser client is JavaScript, but the server behind it can be any server SDK. Every upload handler speaks the same protocol, so the hooks, the record and the errors on these pages are the same whichever language runs the route. See [With a server in another language](#with-a-server-in-another-language).

```ts lib/upload-hooks.ts
"use client"
import { uploadHooks } from "@upstash/blob/react"
import type { uploads } from "./uploads"

export const { useUpload } = uploadHooks<typeof uploads>()
```

```tsx app/page.tsx
const { start, upload, accept } = useUpload()
```

---

## uploadHooks

```ts lib/upload-hooks.ts
"use client"
import { uploadHooks } from "@upstash/blob/react"
import type { uploads } from "./uploads"

export const { useUpload } = uploadHooks<typeof uploads>({
  headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
  concurrency: 3,
  endpoint: "/api/upload",
  onError: ({ file, error }) => toast.error(`${file.name}: ${error.message}`),
})
```

`uploadHooks<typeof uploads>(defaults)` binds `useUpload` to one handler. The bound hook knows the route names, so a typo does not compile, and it knows each route's `input` and completion data. Called with no type parameter, `uploadHooks()` returns an unbound `useUpload` that takes a URL instead of a route name.

Every option is optional.

| Option | Default | Description |
| --- | --- | --- |
| `headers` | none | A function returning headers to send to your route. Re-read on every request. |
| `concurrency` | `3` | How many files upload at once. |
| `endpoint` | `'/api/upload'` | Where the handler is mounted. |
| `onError` | none | Runs for every failed upload. |

A call-site option on `useUpload` wins over the default, except `onError`, where the default runs first and the call-site one after it.

<Warning>
  A call-site `onError` must not throw. A throw there stops the rest of the upload queue from
  starting. A throw from the configured default `onError` is caught and logged.
</Warning>

### With a server in another language

```ts lib/upload-hooks.ts
"use client"
import { uploadHooks } from "@upstash/blob/react"

export const { useUpload } = uploadHooks({ endpoint: "/api/upload" })
```

```tsx app/page.tsx
const avatar = useUpload("avatar") // POSTs to /api/upload?route=avatar
```

`uploadHooks<typeof uploads>` needs the handler's TypeScript type. When the handler is written with another server SDK, such as [Python](/blob/sdks/py/upload-handler#mounting-it) or [Go](/blob/sdks/go/upload-handler#mounting-it), call `uploadHooks()` with no type parameter, or import `useUpload` from `@upstash/blob/react` directly. The unbound hook takes a route name, which resolves against `endpoint`, or the handler's URL. Route names are not checked at compile time, and `upload.blob.data` is `unknown`.

---

## useUpload

```tsx app/page.tsx
const { start, uploads, upload, clear, accept, constraints } = useUpload("attachment", {
  concurrency: 2,
  onDone: (record) => console.log(record.blob.data),
  onError: (record) => console.log(record.error.code),
})
```

| Returns | Description |
| --- | --- |
| `start` | Begins one upload or several. Returns the record(s). |
| `uploads` | Every record, in the order they were started. |
| `upload` | The newest record, or `null`. |
| `clear(id?)` | Removes one record, or all of them. |
| `accept` | The route's `contentTypes`, joined, for an `<input accept>`. |
| `constraints` | What the route's [`GET`](/blob/uploads/upload-handler#the-get-endpoint) served. `undefined` until it answers. |

```tsx app/page.tsx
<input
  type="file"
  multiple
  accept={accept}
  onChange={(e) => start({ files: e.target.files })}
/>
```

`start({ file })` returns one record, or `null` when the file is nullish, so an empty file picker is not an error. `start({ files })` takes a `File[]` or a `FileList` and returns an array.

Three files upload at once by default and the rest queue. `clear(id?)` removes records from the list; a cleared upload that is still running finishes anyway. Unmounting the component does not cancel anything either.

`start` needs a browser. Where there is no `XMLHttpRequest`, such as a Node script, it throws `invalid_input` before calling the route, as [`upload()`](#without-react) does.

### The record

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | Stable for the life of the record. Use it as the list key. |
| `file` | `File` | The file this record uploads. |
| `status` | `'queued' \| 'uploading' \| 'finishing' \| 'paused' \| 'done' \| 'canceled' \| 'error'` | |
| `loaded` | `number` | Bytes that have landed. |
| `total` | `number` | The file's size. |
| `percent` | `number` | 0 to 99 while running, 100 only once `done`. |
| `pending` | `boolean` | Not settled: queued, uploading, finishing or paused. |
| `stalled` | `boolean` | Every request in flight is waiting on a backoff. |
| `canPause` | `boolean` | Whether `pause()` would do anything. |
| `blob` | `CompletedBlob & { data }` | On `done` only. |
| `error` | `BlobError` | On `error` only. |
| `pause()` `resume()` `cancel()` `retry()` | `() => boolean` | Each answers whether it did anything. |

Drive UI off `pending` rather than deriving it from `status`. `percent` stays at 99 through `finishing`, because 100 means stored, not sent. See [Progress and status](/blob/uploads/large-files#progress-and-status).

`blob.data` is typed from that route's [`onUploadComplete`](/blob/sdks/ts/upload-handler#onuploadcomplete). Fields a status does not carry are `undefined` rather than absent, so `upload?.blob?.url` and `upload?.error?.message` read straight off the record with no narrowing.

`canPause` is `false` for a single PUT, which is every file under the multipart threshold. `retry()` works only from `error`, and resumes from the parts that already landed. It answers `false` when the route already accepted the upload and only its answer was lost: the file is stored and `onUploadComplete` ran. See [Large files](/blob/uploads/large-files).

### headers

```tsx app/page.tsx
const { start } = useUpload("attachment", {
  headers: async () => {
    const token = await auth.getToken() // throwing here refuses the upload
    return { authorization: `Bearer ${token}` }
  },
})
```

`headers` is a function, not an object. It is called before every request the SDK makes to your route, so a token that rotates mid-upload keeps working.

A throw from it fails the upload with that error and no retry. Use this to refuse an upload from the app side, for example when a token could not be refreshed.

---

## Without React

```ts app/uploader.ts
import { upload } from "@upstash/blob/browser"

const task = upload(file, {
  route: "/api/upload?route=attachment",
  headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
  input: { threadId },
})

const stop = task.subscribe(() => {
  const { status, percent, stalled } = task.snapshot()
  render(status, percent, stalled)
})

const blob = await task.done // CompletedBlob & { data }
stop()
```

`upload()` starts immediately and returns an `UploadTask`: `snapshot()` for the current state, `subscribe()` for changes, `done` as a promise, and `pause()`, `resume()`, `cancel()` and `retry()`. The snapshot has the same fields as the React record.

`upload()` needs a browser. Where there is no `XMLHttpRequest`, such as a Node script, it throws `invalid_input` before calling the route. Server code uploads with [`bucket.put`](/blob/bucket/writing) instead.

---

## useServerUpload

<CodeGroup>
```ts TypeScript
// app/api/avatar/route.ts
import { Bucket } from "@upstash/blob"

const bucket = Bucket.fromEnv()

export async function POST(request: Request) {
  const file = (await request.formData()).get("file")
  if (!(file instanceof File)) return Response.json({ error: "file field required" }, { status: 400 })

  const blob = await bucket.put(`avatars/${userId}`, file, { contentTypes: ["image/png"], maxSize: "2mb" })
  return Response.json({ url: blob.versionedUrl })
}
```

```python Python
# app/main.py (FastAPI)
from fastapi import FastAPI, UploadFile
from fastapi.responses import JSONResponse
from upstash_blob import AsyncBucket, BlobError

app = FastAPI()
bucket = AsyncBucket.from_env()


@app.post("/api/avatar")
async def avatar(file: UploadFile):
    try:
        blob = await bucket.put(
            f"avatars/{user_id}",
            file.file,
            content_type=file.content_type,
            content_types=["image/png"],
            max_size="2mb",
        )
    except BlobError as e:
        return JSONResponse(e.to_dict(), status_code=e.status)  # decoded by useServerUpload
    return {"url": blob.versioned_url}
```

```go Go
// avatar.go (net/http)
func avatar(w http.ResponseWriter, r *http.Request) {
	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "file field required", http.StatusBadRequest)
		return
	}
	defer file.Close()

	b, err := bucket.Put(r.Context(), "avatars/"+userID, file, &blob.PutOptions{
		ContentType:  header.Header.Get("Content-Type"),
		ContentTypes: []string{"image/png"},
		MaxSize:      2 * blob.MB,
	})
	w.Header().Set("Content-Type", "application/json")
	var be *blob.Error
	if errors.As(err, &be) {
		w.WriteHeader(be.Status)
		json.NewEncoder(w).Encode(be) // decoded by useServerUpload
		return
	} else if err != nil {
		http.Error(w, "upload failed", http.StatusInternalServerError)
		return
	}
	json.NewEncoder(w).Encode(map[string]string{"url": b.VersionedURL})
}
```
</CodeGroup>

```tsx app/avatar.tsx
"use client"
import { useServerUpload } from "@upstash/blob/react"

const { start, upload } = useServerUpload<{ url: string }>("/api/avatar", { field: "file" })

start({ file })
upload?.percent
upload?.status === "done" && upload.response.url // typed from the generic
```

For bytes that must pass through your app, do not use an upload handler. Write an ordinary route that calls `bucket.put`, and drive it with `useServerUpload`: one POST, with upload progress, cancellation and `BlobError` decoding. The route's JSON comes back as `response`.

Every option is optional.

| Option | Default | Description |
| --- | --- | --- |
| `field` | `'file'` | The form field the file is sent under. Must match what your route reads. |
| `headers` | none | A function returning headers to send with the request. |
| `concurrency` | `3` | How many files upload at once. |
| `onDone` | none | Runs for every upload that finishes. |
| `onError` | none | Runs for every failed upload. |

`start({ body })` sends a `File`, `Blob` or `FormData` as the raw body instead of a form field. The record has `cancel()` only and no pause, since there is no multipart. Statuses are `queued`, `uploading`, `finishing`, `done`, `canceled` and `error`.

Like `useUpload`, `start` throws `invalid_input` where there is no `XMLHttpRequest`.

A proxied upload is capped by your platform's request body limit rather than by `maxSize`. The SDK surfaces that refusal as `too_large` with the platform's limit as a hint; see [Platform body limits](/blob/reference/errors#platform-body-limits). Anything larger needs a direct upload with an [upload handler](/blob/uploads/upload-handler).
