Skip to main content
The Storage API is Supabase Storage v1.33.0 mounted at /storage/v1/* on your project URL. It manages files in buckets backed by S3, with object metadata mirrored into the storage.buckets and storage.objects tables in Postgres so RLS can gate access. For the conceptual model, see Storage model. For uploading, see Storage uploads. For RLS, see Storage policies.

Common headers

apikey: <Anon Publishable Key | Service Role Key>
Authorization: Bearer <same key as apikey, or a user access token>
The Anon Key combined with a user’s access token in Authorization is the right pattern for browser-direct uploads. Service Role in both is for server-side admin operations. The Storage API uses bearer auth for most endpoints. Public-URL reads (/object/public/*) work without auth; signed-URL reads pass auth via a ?token= query parameter.

Buckets

A bucket is a top-level container with a name, public flag, optional MIME allowlist, and optional file-size override.

POST /storage/v1/bucket

Create a new bucket.
id
string
required
Bucket id (used in URLs). Must be unique. Lowercase alphanumeric, dashes, and dots.
name
string
Human-readable name. Defaults to id.
public
boolean
When true, the bucket’s public URL (/object/public/{bucket}/{path}) returns files without auth. Default false.
file_size_limit
integer
Per-bucket override for the project’s default file size limit. In bytes.
allowed_mime_types
string[]
Restrict uploads to specific Content-Types. Default null = anything.
{
  "id": "avatars",
  "public": true,
  "allowed_mime_types": ["image/png", "image/jpeg", "image/webp"],
  "file_size_limit": 5242880
}
requests.post(
    f"{BASE_URL}/storage/v1/bucket",
    headers={"apikey": SERVICE_ROLE_KEY, "Authorization": f"Bearer {SERVICE_ROLE_KEY}", "Content-Type": "application/json"},
    json={"id": "avatars", "public": True, "allowed_mime_types": ["image/png", "image/jpeg"]},
)
Response: { "name": "avatars" } on 200.

GET /storage/v1/bucket

List all buckets. Returns the bucket metadata rows from storage.buckets, filtered by SELECT policies.
requests.get(f"{BASE_URL}/storage/v1/bucket", headers={"apikey": ANON_KEY, "Authorization": f"Bearer {ANON_KEY}"})
Response: array of { id, name, public, owner, created_at, updated_at, file_size_limit, allowed_mime_types }.

GET /storage/v1/bucket/

Get a specific bucket’s metadata.

PUT /storage/v1/bucket/

Update bucket metadata. Body fields are the same as create (public, file_size_limit, allowed_mime_types).

DELETE /storage/v1/bucket/

Delete an empty bucket. Returns 409 not_empty if the bucket contains any objects — empty it first via POST /bucket/{id}/empty or DELETE the objects individually.

POST /storage/v1/bucket//empty

Delete all objects in a bucket without removing the bucket itself. Returns 200 OK.

Objects

The core file operations. Object endpoints are routed by bucket + path.

POST /storage/v1/object//

Upload a new file. The path can include slashes — they’re stored verbatim, not interpreted as folders by Storage. Returns 409 Duplicate if a file already exists at that path; use PUT or set the x-upsert: true header to overwrite. Headers:
  • Content-Type — the MIME type of the file. Checked against the bucket’s allowed_mime_types if set.
  • x-upsert: true (optional) — overwrite if exists.
Body: raw file bytes.
with open("avatar.png", "rb") as f:
    requests.post(
        f"{BASE_URL}/storage/v1/object/avatars/{user_id}/avatar.png",
        headers={"apikey": ANON_KEY, "Authorization": f"Bearer {access_token}", "Content-Type": "image/png"},
        data=f,
    )
Response: { "Id": "...", "Key": "avatars/user-123/avatar.png" }.

PUT /storage/v1/object//

Overwrite an existing file at the same path. Same body shape as POST. Returns 200 OK.

GET /storage/v1/object/public//

Fetch a file from a public bucket. No auth required. Returns the file bytes with the stored Content-Type. Returns 400 if the bucket is not public.

GET /storage/v1/object/authenticated//

Fetch a file from any bucket. Requires Authorization: Bearer <access token>. RLS on storage.objects decides whether the request succeeds.
response = requests.get(
    f"{BASE_URL}/storage/v1/object/authenticated/documents/{path}",
    headers={"apikey": ANON_KEY, "Authorization": f"Bearer {access_token}"},
)
file_bytes = response.content

DELETE /storage/v1/object//

Delete a single file. Requires DELETE policy match on storage.objects for the calling role.

POST /storage/v1/object/list/

List files in a bucket. Filtered by SELECT policies on storage.objects.
prefix
string
Only return files whose name starts with this prefix.
limit
integer
Max files to return. Default 100.
offset
integer
Pagination offset.
sortBy
object
{ column: "name" | "created_at" | "updated_at", order: "asc" | "desc" }.
Substring search on name. Case-insensitive.
{
  "prefix": "user-123/",
  "limit": 50,
  "sortBy": { "column": "created_at", "order": "desc" }
}
requests.post(
    f"{BASE_URL}/storage/v1/object/list/avatars",
    headers={"apikey": ANON_KEY, "Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
    json={"prefix": f"{user_id}/", "limit": 50},
)
Response: array of { id, name, bucket_id, owner, created_at, updated_at, last_accessed_at, metadata }.

POST /storage/v1/object/copy

Copy a file to a new location (potentially in a different bucket).
bucketId
string
required
Source bucket.
sourceKey
string
required
Source path.
destinationBucket
string
Destination bucket. Defaults to source bucket.
destinationKey
string
required
Destination path.

POST /storage/v1/object/move

Move a file to a new location. Same parameters as copy; the source is deleted on success.

Signed URLs

For sharing files via TTL-bounded URLs that work without auth.

POST /storage/v1/object/sign//

Mint a download signed URL.
expiresIn
integer
URL TTL in seconds. Default 60. Max 604800 (7 days).
response = requests.post(
    f"{BASE_URL}/storage/v1/object/sign/documents/{path}",
    headers={"apikey": SERVICE_ROLE_KEY, "Authorization": f"Bearer {SERVICE_ROLE_KEY}", "Content-Type": "application/json"},
    json={"expiresIn": 3600},
)
signed_url = f"{BASE_URL}{response.json()['signedURL']}"
Response: { "signedURL": "/object/sign/documents/report.pdf?token=..." }. The URL is path-relative — prepend your BASE_URL to get the full URL.

GET /storage/v1/object/sign//?token=…

Fetch a file via a signed URL. The token is the signature; no other auth required.

POST /storage/v1/object/upload/sign//

Mint an upload signed URL — your server creates the URL, the client PUTs the file directly to it. Useful for high-bandwidth flows or offline clients.
expiresIn
integer
URL TTL in seconds. Default 7200 (2 hours).
Response: { "url": "/object/upload/sign/documents/report.pdf?token=..." }. The client then PUTs to <BASE_URL>{url} with the file bytes and Content-Type header.

POST /storage/v1/object/sign

Sign multiple URLs in one request. Body: { "paths": ["bucket1/path1", "bucket2/path2"], "expiresIn": 3600 }. Returns an array of signed URLs.

Image transformations

The render/image endpoints route through imgproxy. The path shape mirrors the object endpoints — public, authenticated, sign — with the same auth requirements.

GET /storage/v1/render/image/public//

Fetch a transformed image from a public bucket. Pass transformations as query parameters.
width
integer
Output width in pixels.
height
integer
Output height in pixels.
resize
string
cover, contain, or fill. Default cover.
quality
integer
JPEG/WebP quality, 0-100. Default 80.
format
string
webp, png, jpeg, or origin. Default origin (preserves the original format).
// Direct URL — no fetch wrapper needed
const thumbnailUrl =
  `${BASE_URL}/storage/v1/render/image/public/avatars/${userId}/photo.png` +
  `?width=200&height=200&resize=cover&quality=80`;
// Use directly in <img src={thumbnailUrl} />

GET /storage/v1/render/image/authenticated//

Same as above but auth-gated.

GET /storage/v1/render/image/sign//?token=…

Same as above via signed URL. To mint a signed transformation URL, hit POST /storage/v1/object/sign/{bucket}/{path} with a transform parameter in the body:
{
  "expiresIn": 3600,
  "transform": { "width": 200, "height": 200 }
}
Returns a signed URL that already includes the transformation parameters.

TUS resumable uploads

For files larger than 50MB (the per-request limit). Implements the TUS 1.0 protocol. Use a TUS client library rather than hand-rolling.

POST /storage/v1/upload/resumable

Initiate a resumable upload. Subsequent PATCH requests with Upload-Offset headers stream the chunks. HEAD requests query progress. Required headers:
  • apikey
  • Authorization: Bearer <token>
  • Upload-Length: <total bytes>
  • Upload-Metadata: bucketName <base64>,objectName <base64>,contentType <base64>
  • Tus-Resumable: 1.0.0
The TUS protocol is multi-step; see Storage uploads for a worked example with tus-js-client.

Error responses

Storage returns errors as {"statusCode": "<code>", "error": "<machine-readable>", "message": "<human-readable>"}. Common cases:
StatusErrorWhen
400invalid_mime_typeContent-Type not in bucket’s allowed_mime_types
400invalid_signatureSigned URL token doesn’t validate (tampered, expired, or wrong key)
401Invalid JWTMissing or expired access token on an authenticated endpoint
403not_authorizedRLS denied the operation
404not_foundBucket or object doesn’t exist
409DuplicatePOSTing to an existing path (use PUT or x-upsert: true)
409not_emptyDELETE bucket with objects still in it
413Payload too largeFile exceeds the 50MB per-request limit (or bucket override)

Next steps

Storage uploads

The four upload flows with worked examples.

Storage policies

RLS patterns on storage.objects.

Storage model

Buckets, public vs private, the underlying tables.

Auth Reference

The auth surface that mints the tokens you’ll use here.