REST API

REST API

Upload, list, search, and organize files over HTTP.

First request #

The CLI and both MCP servers call this API. Call it directly when you build your own integration. With the API you can:

  • Upload a file and get a public URL and a GitHub-ready embed URL.
  • List, search, and tag files with metadata.
  • Create galleries and live links.
  • Read usage for a workspace.

The base URL is https://api.uploads.sh, and workspace routes start with /v1/workspaces/{workspace}. Upload a file:

curl -X PUT \
https://api.uploads.sh/v1/workspaces/$WORKSPACE/files/screenshots/shot.png \
-H "Authorization: Bearer $UPLOADS_TOKEN" \
-H "Content-Type: image/png" \
--data-binary @shot.png

The response is JSON with the stored key, its public url, and an embedUrl for GitHub markdown.

Building an agent? The MCP server wraps these routes as tools →

Authentication #

Send a workspace token on every request:

Authorization: Bearer up_<workspace>_…
  • Personal token. uploads login creates one, or create one under Developers in your account. Tokens last 90 days by default.
  • Service token. For CI and bots, a workspace admin creates an ups_<workspace>_… token in the workspace settings. It is not tied to a member.

The workspace in the URL must match the token’s workspace. An unknown workspace and a bad token both return 401.

Each endpoint needs one scope. The endpoint reference shows it next to each route.

Scope Grants
files:read List, search, and read files, metadata, usage, galleries, feeds
files:write Upload, update metadata, galleries, feeds, usage reconcile
files:delete Delete files and purge expired ones

The REST API does not accept OAuth access tokens. Those work only on the hosted MCP server.

Pagination #

List endpoints return a cursor field. It is an opaque string when there is another page, and null at the end. Pass it back as ?cursor= with the same query to read the next page.

  • Never parse, build, or edit a cursor. A cursor sent with a different query returns 400.
  • The gallery and feed lists name the field nextCursor. It works the same way.

Errors #

Every error response uses one envelope:

Error response
{
"error": {
"code": "storage_quota_exceeded",
"type": "insufficient_storage",
"message": "storage quota exceeded (…)",
"details": { "maxStorageBytes": 1000 }
}
}
Field Use
code A stable string for one failure. Branch your client logic on it.
type A broad category that sets the HTTP status.
message Text for people. It can change, so don’t parse it.
details Extra data for some codes. Optional.

Rate limits #

Each workspace has separate burst limits for reads and writes. They are set well above normal CLI and app traffic. A refused request returns 429 with type: rate_limited and a Retry-After header in seconds. Wait that long, then retry.

The monthly upload budget also returns 429, with the code upload_budget_exceeded. It resets at the start of the next UTC month, so it has no Retry-After. See Plans & limits.

To make an upload or gallery create safe to retry, send an Idempotency-Key header. A repeat with the same key and request returns the first response for 24 hours.

Endpoints #

Every route in the OpenAPI document, grouped by resource. {workspace} is your workspace name. {key} is the file’s object key and can include slashes.

Files

GET/v1/workspaces/{workspace}/files

List workspace filesfiles:read

ParameterInTypeDescription
prefixquerystring
delimiterquerystringReturn common prefixes for folder-style navigation.
limitqueryinteger
cursorquerystringOpaque cursor returned by the previous page.

200 A page of files and an opaque continuation cursor.Returnsfilesprefixescursor

Errors401403

GET/v1/workspaces/{workspace}/files/search

Search workspace filesfiles:read

Requires at least one meta.<key> equality filter or a name substring. Metadata filter names are dynamic query parameters.

ParameterInTypeDescription
namequerystring
prefixquerystring
limitqueryinteger
cursorquerystringOpaque cursor returned by the previous page.

200 A bounded page. truncated is true and cursor is non-null when more matches remain; pass cursor back to continue.Returnsitemstruncatedcursor

Errors400401403

GET/v1/workspaces/{workspace}/files/facets

List metadata keys or valuesfiles:read

ParameterInTypeDescription
keyquerystringWhen present, list values for this metadata key. Otherwise list keys.

200 Metadata key facets or value facets.Returnskeystruncatedkeyvalues

Errors401403

POST/v1/workspaces/{workspace}/files/sign

Create a presigned uploadfiles:write

ParameterInTypeDescription
keyrequiredbodystring
contentTyperequiredbodystring
maxSizebodyinteger
expiresInbodyintegerSeconds until the signed URL expires. Capped at 900 for SVG and XML content types (issue #929).
replacebodyboolean

200 Presigned upload details.

Errors400401403409429

GET/v1/workspaces/{workspace}/files/by-path

Group workspace files by GitHub pathfiles:read

Powers the web Screenshots page's grouped-by-path view. Groups recently uploaded gh.*-tagged files by project and path.

ParameterInTypeDescription
mergedquery"1"Set to 1 to include only files tagged from a merged PR.

200 Path groups, a flat catalog, known projects, and a flat recent feed.ReturnsgroupscatalogprojectslatesttruncatedcatalogTruncated

Errors401403

GET/v1/workspaces/{workspace}/files/file-url

Resolve a usable URL for a filefiles:read

Returns the workspace's public URL when configured, otherwise a short-lived signed download URL. Resolves across storage lanes so a file uploaded before a storage switch still resolves.

ParameterInTypeDescription
keyrequiredquerystring

200 A usable URL.Returnsurl

Errors400401403404

GET/v1/workspaces/{workspace}/files/{key}

Read file metadatafiles:read

ParameterInTypeDescription
keyrequiredpathstringObject key. It may include slashes.
metadataquerybooleanReturn only the queryable metadata map.

200 Object metadata, or a metadata-only response when requested.ReturnskeysizecontentTypeuploadedurlembedUrlmetadatavisibility

Errors401403404

PUT/v1/workspaces/{workspace}/files/{key}

Upload a filefiles:writeBody application/octet-stream

Send an Idempotency-Key to make retries safe for 24 hours. Reusing the key with the same effective request (keyed on the uploaded content) returns the original 201 response. Reusing it with a different request returns 409.

ParameterInTypeDescription
keyrequiredpathstringObject key. It may include slashes.
replacequerybooleanAllow replacement. X-Uploads-Replace: 1 is an equivalent header form.
dryRunquerybooleanValidate and resolve the final key without writing.
Idempotency-KeyheaderstringA caller-scoped retry key retained for 24 hours. Use 1 to 255 visible ASCII characters.

200 Dry-run result when dryRun is enabled.ReturnsworkspacekeyurlembedUrlreplacedwouldRefusedryRun

201 The file was stored.ReturnskeysizecontentTypeuploadedurlembedUrlmetadatavisibilityworkspacereplaced

Errors400401403409413415429

PATCH/v1/workspaces/{workspace}/files/{key}

Merge file metadatafiles:write

ParameterInTypeDescription
keyrequiredpathstringObject key. It may include slashes.
setbodyMetadata
deletebodystring[]

200 The merged metadata map.Returnsmetadata

Errors400401403404429

Galleries

GET/v1/workspaces/{workspace}/galleries

List workspace galleriesfiles:read

ParameterInTypeDescription
limitqueryinteger
cursorquerystringOpaque cursor returned by the previous page.

200 Enriched gallery summaries and an opaque continuation cursor.ReturnsgalleriesnextCursor

Errors401403

POST/v1/workspaces/{workspace}/galleries

Create a galleryfiles:write

Send an Idempotency-Key to make retries safe for 24 hours. Reusing the key with the same effective body returns the original 201 response. Reusing it with a different body returns 409.

ParameterInTypeDescription
Idempotency-KeyheaderstringA caller-scoped retry key retained for 24 hours. Use 1 to 255 visible ASCII characters.
titlerequiredbodystring
descriptionbodystring | null

201 The gallery was created.ReturnsidurlworkspacetitledescriptionvisibilitycoverItemIdversioncreatedAtupdatedAtitemCountreferencespreviewUrlitems

Errors400401403409429

GET/v1/workspaces/{workspace}/galleries/by-reference

Find galleries by external referencefiles:read

ParameterInTypeDescription
providerrequiredquerystring
coordinaterequiredquerystring
limitqueryinteger
cursorquerystringOpaque cursor returned by the previous page.

200 Gallery summaries and an opaque continuation cursor.ReturnsgalleriesnextCursor

Errors400401403

GET/v1/workspaces/{workspace}/galleries/{galleryId}

Read a galleryfiles:read

ParameterInTypeDescription
galleryIdrequiredpathstring

200 The gallery and its items.ReturnsidurlworkspacetitledescriptionvisibilitycoverItemIdversioncreatedAtupdatedAtitemCountreferencespreviewUrlitems

Errors401403404

PATCH/v1/workspaces/{workspace}/galleries/{galleryId}

Update a galleryfiles:write

ParameterInTypeDescription
galleryIdrequiredpathstring
expectedVersionrequiredbodyinteger
titlebodystring
descriptionbodystring | null
coverItemIdbodystring | null

200 The updated gallery.ReturnsidurlworkspacetitledescriptionvisibilitycoverItemIdversioncreatedAtupdatedAtitemCountreferencespreviewUrlitems

Errors400401403404409429

POST/v1/workspaces/{workspace}/galleries/{galleryId}/items

Add a file to a galleryfiles:write

ParameterInTypeDescription
galleryIdrequiredpathstring
expectedVersionrequiredbodyinteger
objectKeyrequiredbodystring
captionbodystring | null
altTextbodystring | null

200 The file was already a member; the existing item is returned.ReturnsidobjectKeypositioncaptionaltTextcreatedAtstatusurlembedUrlpageUrlcontentTypesizeuploadedmodifiedposterUrlvideoDimensions

201 The gallery item was created.ReturnsidobjectKeypositioncaptionaltTextcreatedAtstatusurlembedUrlpageUrlcontentTypesizeuploadedmodifiedposterUrlvideoDimensions

Errors400401403404409429

Live links

GET/v1/workspaces/{workspace}/feeds

List workspace feedsfiles:read

ParameterInTypeDescription
limitqueryinteger
cursorquerystringOpaque cursor returned by the previous page.

200 A page of feed summaries.ReturnsfeedsnextCursor

Errors401403

POST/v1/workspaces/{workspace}/feeds

Create a change feedfiles:write

Creates a public newest-first screenshot feed for a GitHub owner/repo, optionally scoped to one pull request or issue. The same scope returns the existing feed.

ParameterInTypeDescription
reporequiredbodystringGitHub owner/repo.
pathbodystring | nullOptional exact page-path metadata filter.
numberbodyinteger | nullOptional pull request or issue number. Filters on gh.number.
kindbody"pull" | "issue"Display-only. Set automatically when you pass pr or issue.
prbodyinteger | nullAlias for number with kind=pull. Mutually exclusive with number and issue.
issuebodyinteger | nullAlias for number with kind=issue. Mutually exclusive with number and pr.

200 The existing feed for this scope.ReturnsidurlworkspacerepopathnumberkindtitlesourcecreatedAtupdatedAtitems

201 The feed was created.ReturnsidurlworkspacerepopathnumberkindtitlesourcecreatedAtupdatedAtitems

Errors400401403409429

GET/v1/workspaces/{workspace}/feeds/{feedId}

Read one owned feedfiles:read

ParameterInTypeDescription
feedIdrequiredpathstring

200 The feed and its current newest-first items.ReturnsidurlworkspacerepopathnumberkindtitlesourcecreatedAtupdatedAtitems

Errors401403404

Usage

GET/v1/workspaces/{workspace}/usage

Read workspace usagefiles:read

200 Usage counters, configured limits, credential scopes, and plan.ReturnsworkspacebytesobjectssharedBytessharedObjectsuploadsInPeriodperiodStartupdatedAtscopesplanmaxStorageBytesstorageRemainingBytesmaxUploadsPerPerioduploadsRemaining

Errors401403

POST/v1/workspaces/{workspace}/usage/reconcile

Rebuild workspace usage countersfiles:write

Potentially expensive: scans every object in the workspace's active storage lanes and rebuilds the usage ledger. Requires a bearer token with files:write; browser sessions are not accepted.

200 Reconciliation result and updated usage.

Errors401403

POST/v1/workspaces/{workspace}/usage/purge-expired

Purge files past the retention windowfiles:delete

Destructive: permanently deletes objects older than the workspace's retentionDays, then reconciles usage. Requires a bearer token with files:delete; browser sessions are not accepted. The response is either { skipped: true } or a purge and reconciliation result.

200 Purge result or a skipped result.

Errors401403

Public reads

GET/public/feeds/{feedId}

Read a public change feedNo auth

Returns one page (up to 50) of the public newest-first item list for a feed, plus nextCursor for the next page. It omits workspace ownership and object keys. Items are resolved at view time from gh.repo and optional gh.number metadata.

ParameterInTypeDescription
feedIdrequiredpathstring
cursorquerystringOpaque cursor returned by the previous page.

200 The public feed and its current items.

Errors404503

GET/public/galleries/{galleryId}

Read a public galleryNo auth

Returns the public allowlisted projection. It omits workspace ownership and object keys.

ParameterInTypeDescription
galleryIdrequiredpathstring

200 The public gallery and its public item projection.

Errors404503