---
title: signedUploadUrl
description: A presigned PUT-or-POST contract so a browser can upload straight to the bucket - bandwidth and CPU stay off your server.
---

`files.signedUploadUrl(key, options)`

Returns a discriminated PUT-or-POST contract so a client (typically a browser) can upload directly to the bucket without proxying bytes through your server. The flow is: your server calls `signedUploadUrl()`, returns the result to the browser, the browser uploads straight to the provider directly. Bandwidth and CPU stay off your server.

Without `maxSize`, the adapter returns a presigned PUT URL - simpler, but with no server-side size cap. With `maxSize`, providers that support upload policies switch to a presigned POST form whose policy enforces the size at the bucket via `content-length-range`. In practice you should pass `maxSize` when the adapter supports it - without it, anyone with the URL can DoS your storage costs until `expiresIn` elapses.

Vercel Blob, Bunny Storage, Appwrite, and PocketBase throw here - Vercel's upload model goes through `handleUpload()` from `@vercel/blob/client` instead of presigned URLs, Bunny Storage writes require the Storage API `AccessKey` header, and Appwrite/PocketBase have no presigned upload primitive at all (mint a short-lived auth token for the client instead). The R2 Workers binding throws unless you've configured hybrid mode (binding + HTTP credentials). Azure, Supabase, R2, Google Drive, OneDrive, and SharePoint have no `content-length-range` equivalent and **throw if you pass `maxSize` or `minSize`**; omit those options for a presigned PUT/session URL and enforce upload caps at your application gateway instead. Azure also throws if you pass `contentType`, because SAS does not bind Content-Type into the signature. UploadThing returns a PUT URL and treats `maxSize` as advisory rather than enforced - it caps uploads via the file-router config tied to the adapter's `slug` instead of via the URL signature. Enforce upload caps at your application gateway (or at the provider's dashboard-level bucket/route setting).

```ts lineNumbers
// On your server: hand back an upload contract that lets the browser
// PUT/POST the file directly to the bucket. Bytes never touch your server.
const upload = await files.signedUploadUrl("avatars/abc.png", {
  expiresIn: 60,
  contentType: "image/png",
  maxSize: 5_000_000,
});
// → { method: "PUT", url, headers? }
//   | { method: "POST", url, fields }

// In the browser: PUT path (no maxSize) is a plain fetch.
await fetch(upload.url, {
  method: "PUT",
  body: file,
  headers: upload.headers,
});

// POST path (with maxSize) is multipart with the signed policy fields.
const form = new FormData();
for (const [k, v] of Object.entries(upload.fields)) form.append(k, v);
form.append("file", file);
await fetch(upload.url, { method: "POST", body: form });
```

## Options

<AutoTypeTable
  path="../../packages/files-sdk/src/index.ts"
  name="SignUploadOptions"
/>
