Search docs ⌘K
esc
Type to search across all documentation pages
Storage

Storage

Object storage backed by Cloudflare R2. Upload and download files up to 100 MB. Objects are organized into buckets scoped per project. Supports signed URLs for direct client-side uploads and downloads.

Virtual buckets: SaaSignal uses a single R2 bucket under the hood. Your "buckets" are logical namespaces stored in D1. Objects are keyed by {projectId}/{bucketId}/{objectKey}.

Create Bucket

POST
/infra/storage/buckets
storage:write0.0000044 tokens

Create a new storage bucket for the project.

curl
curl -X POST https://api.saasignal.saastemly.com/infra/storage/buckets \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"example"}'
json — 201 Created
{
  "id": "sb_01JNXS...",
  "name": "assets",
  "visibility": "private",
  "created_at": "2026-03-09T12:00:00Z"
}
typescript
await ss.infra.storage.createBucket('assets', { visibility: 'private' })
  1. Open Dashboard and select your project
  2. Go to Storage in the sidebar
  3. Click Create Bucket
  4. Enter a name and choose visibility, then click Create
Body field
Type
Description
name required
string
Bucket name (1–63 chars, lowercase alphanumeric and hyphens)
1–63 charspattern ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$
visibility
string
Bucket visibility: private or public. Default private.
values: private, publicdefault private
Error responses
400 Bad request
401 Unauthorized
402 Insufficient tokens
409 Conflict
429 Rate limited

List Buckets

GET
/infra/storage/buckets
storage:read0.0000004 tokens

List all storage buckets for the project.

curl
curl https://api.saasignal.saastemly.com/infra/storage/buckets \
  -H "Authorization: Bearer sk_live_..."
json — 200 OK
{
  "buckets": [
    { "id": "sb_01JNXS...", "name": "assets", "visibility": "private", "object_count": 42, "total_size": 10485760, "created_at": "2026-03-09T12:00:00Z" }
  ]
}
typescript
const { buckets } = await ss.infra.storage.listBuckets()
  1. Open Dashboard and select your project
  2. Go to Storage in the sidebar
  3. All buckets are listed with their object count and total size
Error responses
401 Unauthorized
402 Insufficient tokens
429 Rate limited

Get Bucket

GET
/infra/storage/buckets/{bucketId}
storage:read0.0000004 tokens

Get details of a storage bucket.

curl
curl https://api.saasignal.saastemly.com/infra/storage/buckets/{bucketId} \
  -H "Authorization: Bearer sk_live_..."
json — 200 OK
{
  "id": "sb_01JNXS...",
  "name": "assets",
  "visibility": "private",
  "object_count": 42,
  "total_size": 10485760,
  "created_at": "2026-03-09T12:00:00Z"
}
typescript
const bucket = await ss.infra.storage.getBucket('sb_...')
  1. Open Dashboard and select your project
  2. Go to Storage and click on a bucket to view its details
Path param
Type
Description
bucket_id required
string
Bucket ID
Error responses
401 Unauthorized
402 Insufficient tokens
404 Not found
429 Rate limited

Delete Bucket

DELETE
/infra/storage/buckets/{bucketId}
storage:write

Delete a bucket and every object under it. Cost is conservatively prepaid for up to 10,000 objects, then refunded down to the actual delete/list work performed.

curl
curl -X DELETE https://api.saasignal.saastemly.com/infra/storage/buckets/{bucketId} \
  -H "Authorization: Bearer sk_live_..."
typescript
await ss.infra.storage.deleteBucket('sb_...')
  1. Open Dashboard and select your project
  2. Go to Storage and click on the bucket
  3. Click Delete Bucket and confirm
Path param
Type
Description
bucket_id required
string
Bucket ID to delete
Error responses
401 Unauthorized
402 Insufficient tokens
404 Not found
429 Rate limited

Upload Object

PUT
/infra/storage/buckets/{bucketId}/objects/*
storage:write

Upload or overwrite an object. Send raw bytes with appropriate Content-Type. Max 100 MB. Cost varies with stored bytes and prepaid retention.

curl
curl -X PUT https://api.saasignal.saastemly.com/infra/storage/buckets/{bucketId}/objects/* \
  -H "Authorization: Bearer sk_live_..."
json — 201 Created
{
  "key": "images/logo.png",
  "size": 24576,
  "content_type": "image/png",
  "etag": ""d41d8cd98f00b204e9800998ecf8427e"",
  "uploaded_at": "2026-03-09T12:00:00Z"
}
typescript
await ss.infra.storage.putObject('sb_...', 'images/logo.png', file, { contentType: 'image/png' })
  1. Open Dashboard and select your project
  2. Go to Storage and open a bucket
  3. Click Upload and select a file
  4. Optionally set the object key (defaults to filename)
Path param
Type
Description
bucket_id required
string
Bucket ID
key required
string
Object key (path within the bucket, e.g. images/logo.png)
Body field
Type
Description
body required
binary
File content (max 100 MB)
Query param
Type
Description
content_type
string
MIME type of the object
Error responses
400 Bad request
401 Unauthorized
402 Insufficient tokens
404 Not found
429 Rate limited

List Objects

GET
/infra/storage/buckets/{bucketId}/objects
storage:read0.0000004 tokens

List objects in a bucket.

curl
curl https://api.saasignal.saastemly.com/infra/storage/buckets/{bucketId}/objects \
  -H "Authorization: Bearer sk_live_..."
json — 200 OK
{
  "objects": [
    { "key": "images/logo.png", "size": 24576, "content_type": "image/png", "etag": ""d41d8..."", "uploaded_at": "2026-03-09T12:00:00Z" }
  ],
  "next_cursor": null,
  "truncated": false
}
typescript
const { objects } = await ss.infra.storage.listObjects('sb_...', { prefix: 'images/' })
  1. Open Dashboard and select your project
  2. Go to Storage and open a bucket
  3. Browse objects or filter by prefix using the search bar
Path param
Type
Description
bucket_id required
string
Bucket ID
Query param
Type
Description
prefix
string
Filter objects by key prefix
max 512 chars
cursor
string
Pagination cursor from a previous next_cursor response
limit
integer
Max objects to return per page. Range 1–1000, default 100.
range ≥1 .. ≤1000default 100
Error responses
401 Unauthorized
402 Insufficient tokens
404 Not found
429 Rate limited

Download Object

GET
/infra/storage/buckets/{bucketId}/objects/*
storage:read0.0000004 tokens

Download an object by key. Returns raw bytes with appropriate Content-Type.

curl
curl https://api.saasignal.saastemly.com/infra/storage/buckets/{bucketId}/objects/* \
  -H "Authorization: Bearer sk_live_..."
json — 200 OK
{ "status": "ok" }
typescript
const response = await ss.infra.storage.getObject('sb_...', 'images/logo.png')
  1. Open Dashboard and select your project
  2. Go to Storage and open a bucket
  3. Click on an object, then click Download
Path param
Type
Description
bucket_id required
string
Bucket ID
key required
string
Object key to download
Error responses
401 Unauthorized
402 Insufficient tokens
404 Not found
429 Rate limited

Head Object

HEAD
/infra/storage/buckets/{bucketId}/objects/*
storage:read0.0000004 tokens

Get metadata for an object without downloading the body.

curl
curl -X HEAD https://api.saasignal.saastemly.com/infra/storage/buckets/{bucketId}/objects/* \
  -H "Authorization: Bearer sk_live_..."
json — 200 OK
{
  "key": "images/logo.png",
  "size": 24576,
  "content_type": "image/png",
  "etag": ""d41d8cd98f00b204e9800998ecf8427e"",
  "uploaded_at": "2026-03-09T12:00:00Z"
}
typescript
const meta = await ss.infra.storage.headObject('sb_...', 'images/logo.png')
  1. Open Dashboard and select your project
  2. Go to Storage, open a bucket, and click on an object to view its metadata
Path param
Type
Description
bucket_id required
string
Bucket ID
key required
string
Object key
Error responses
401 Unauthorized
402 Insufficient tokens
404 Not found
429 Rate limited

Delete Object

DELETE
/infra/storage/buckets/{bucketId}/objects/*
storage:write0.0000004 tokens

Delete an object by key.

curl
curl -X DELETE https://api.saasignal.saastemly.com/infra/storage/buckets/{bucketId}/objects/* \
  -H "Authorization: Bearer sk_live_..."
typescript
await ss.infra.storage.deleteObject('sb_...', 'images/logo.png')
  1. Open Dashboard and select your project
  2. Go to Storage, open a bucket, and click on the object
  3. Click Delete and confirm
Path param
Type
Description
bucket_id required
string
Bucket ID
key required
string
Object key to delete
Error responses
401 Unauthorized
402 Insufficient tokens
429 Rate limited

Generate Signed URL

POST
/infra/storage/buckets/{bucketId}/signed-url
storage:write

Generate a time-limited signed URL for direct upload or download. The signed URL creation call is billed separately from the eventual upload/download it authorizes.

curl
curl -X POST https://api.saasignal.saastemly.com/infra/storage/buckets/{bucketId}/signed-url \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"key":"...","method":"..."}'
json — 200 OK
{
  "url": "https://storage.saasignal.saastemly.com/sb_.../images/logo.png?X-Amz-Signature=...",
  "expires_at": "2026-03-09T13:00:00Z"
}
typescript
const { url } = await ss.infra.storage.signedUrl('sb_...', 'images/logo.png', { method: 'PUT', expires_in: 3600 })
  1. Open Dashboard and select your project
  2. Go to Storage, open a bucket, and click on an object
  3. Click Signed URL, configure method and expiry, then copy the URL
Path param
Type
Description
bucket_id required
string
Bucket ID
key required
string
Object key
1–1024 charspattern ^(?!.*\.\.)[^\x00-\x1f\x7f]+$
Body field
Type
Description
method
string
HTTP method the signed URL permits: GET or PUT. Default GET.
values: GET, PUT
expires_in
integer
URL validity in seconds. Range 1–86400, default 3600 (1 hour).
range ≥60 .. ≤604800default 3600
Error responses
400 Bad request
401 Unauthorized
402 Insufficient tokens
404 Not found
429 Rate limited

Consume Signed Download URL

GET
/infra/storage/signed/{token}

Download an object using a previously issued signed URL token. This route is intentionally public and authenticated entirely by the signed URL path token and query signature. Required query parameters are embedded in the signed URL returned by POST /infra/storage/buckets/:bucketId/signed-url.

curl
curl https://api.saasignal.saastemly.com/infra/storage/signed/{token}?expires=...&p=...&b=...&k=...&m=... \
  -H "Authorization: Bearer <session_token>"
json — 200 OK
{ "status": "ok" }
typescript
const response = await ss.infra.storage.downloadSignedUrl(signedUrl.url)
  1. Generate a signed download URL from Storage
  2. Paste the resulting URL into a new tab to validate public download access, or call the underlying route from API Console
Path param
Type
Description
token required
string
Signed URL token
Query param
Type
Description
expires required
string
Signed expiry timestamp embedded in the URL
p required
string
Signed project payload
b required
string
Signed bucket payload
k required
string
Signed object key payload
m required
string
Signed HTTP method. Must be GET.
values: GET
Error responses
403 Forbidden
404 Not found
429 Rate limited

Consume Signed Upload URL

PUT
/infra/storage/signed/{token}

Upload an object using a previously issued signed URL token. Send raw bytes in the request body and optionally pass ct as a query parameter to lock the content type. This route is intentionally public and authenticated entirely by the signed URL path token and query signature.

curl
curl -X PUT https://api.saasignal.saastemly.com/infra/storage/signed/{token}?expires=...&p=...&b=...&k=...&m=... \
  -H "Authorization: Bearer <session_token>"
json — 200 OK
{ "status": "ok" }
typescript
await ss.infra.storage.uploadSignedUrl(signedUrl.url, file, { contentType: 'image/png' })
  1. Generate a signed upload URL from Storage
  2. Use API Console in raw mode to send file bytes through the public signed upload route
  3. Or hand the signed URL to any browser client that should upload directly without exposing your API key
Path param
Type
Description
token required
string
Signed URL token
Query param
Type
Description
expires required
string
Signed expiry timestamp embedded in the URL
p required
string
Signed project payload
b required
string
Signed bucket payload
k required
string
Signed object key payload
m required
string
Signed HTTP method. Must be PUT.
values: PUT
ct
string
Optional content type embedded in the signed URL
Body field
Type
Description
body required
binary
Raw file bytes to upload
Error responses
403 Forbidden
429 Rate limited