Skip to navigation

Upload control with signed uploads

Signed uploads restrict who can upload files to your Uploadcare project, and for how long. Your backend issues a short-lived credential, the client sends it with every upload, and Upload API rejects anything without a valid one.

alt [Token valid] [Expired, out of scope, or spent] Ask for an upload credential Return the token Upload with Authorization: Bearer <token> 200 File 401 or 403 with an error_code Authenticate the user,mint a short-lived token Client Your backend Upload API

What gets signed

Two project settings require a credential. Signed Uploads is off until you turn it on. Signed AI image generation is on by default for every project, so the AI Image Editor needs an auth token even when uploads are unsigned:

SettingProtects
Signed UploadsUploads through Upload API, from File Uploader, Upload Client or your own requests
Signed AI image generationImage generation and editing in the AI Image Editor. Auth tokens only: a signature does not count here

Turn on Signed Uploads last, once your clients send a credential. You can turn Signed AI image generation off, but then anyone with your public key can spend your project’s generations.

Two kinds of credential

Both are generated on your backend:

  • Auth token (recommended). A short-lived JWT sent in the Authorization header. You can limit it to particular endpoints (its scope) and to a number of operations, one operation being one uploaded file or one AI generation or edit. Both are covered in scope and operation limits. Without limits, a token works for every protected endpoint, any number of times, until it expires.
  • Signature. The legacy scheme: a signature and expire pair sent with the request, with no endpoint or operation limits. Still supported, and still the only option for the deprecated jQuery File Uploader.

The Upload API authentication reference has the wire format for both: the claims, each endpoint’s scope path and operation cost, and the error codes.

Issue tokens from your backend

Before you start:

  • Create a secret key in your project’s API keys and keep it on the server, for example in UPLOADCARE_SECRET_KEY. Never send it to the browser.
  • In Node, install the SDK: npm install @uploadcare/signed-uploads. Use 6.23.0 or later, which also ships the browser-side AuthTokenCache.

Expose an endpoint that checks who is asking and returns a fresh token:

import express from 'express'
import { generateAuthToken } from '@uploadcare/signed-uploads'
const app = express()
// `requireUser` stands for your own auth middleware.
app.get('/uploadcare-token', requireUser, (req, res) => {
const token = generateAuthToken(process.env.UPLOADCARE_SECRET_KEY, {
lifetime: 30 * 60 * 1000, // 30 minutes, in milliseconds
// Optional. Without it the token works any number of times until it
// expires. You can also pass `scope`, but it must list every path your
// client calls: /docs/api/upload/authentication/#jwt-scope
operations: 10,
})
res.json({ token })
})

The client gets back { "token": "eyJhbGciOiJIUzI1NiIs…" }.

For other languages, use any HS256 JWT library. The reference has samples for Python and Ruby.

Warning: Anyone holding a token can upload with it until it expires or runs out of operations. Protect the endpoint that issues it, and keep the lifetime short.

Send the token from your client

Every client follows the same two rules:

  • Leaving authToken unset (null, undefined or '') sends requests without a token.
  • A token function that returns nothing (null, undefined or '') fails the request with an AuthTokenResolverError, the same as one that throws. Leave authToken unset for unsigned requests rather than return an empty value.

authToken takes three shapes, and the shape decides how much the library can do on its own:

ShapeWhat the library does
A token stringSends it as is. Never refreshes it, never retries when it is refused.
A function returning a tokenRetries a refused upload once with a new token. File Uploader and the AI Image Editor also cache the token and refresh it before exp. Upload Client calls the function for every request, so wrap it in AuthTokenCache to cache it.
{ getToken, invalidate }The same, except your invalidate() is what the library calls to drop a refused token. Use this when you keep the cache yourself. An AuthTokenCache instance is already this shape, so you can pass one straight in.
Note: The retry covers uploads, which File Uploader and Upload Client both make through Upload Client. The AI Image Editor’s generation, edit and status requests are one-shot: they raise the error and stop, so recovering a spent token there takes an invalidateAuthToken() call.

Each client caches and refreshes the token its own way:

Note: Versions that predate authToken ignore it without a warning, so the request goes out unsigned and fails with SignatureRequiredError.

The cache is in memory

Every one of these caches lives in the page. Nothing is written to localStorage, a cookie or anywhere else, so a token never outlives the tab that fetched it. A reload, a second tab, and a second uploader or editor instance each start empty and call your function again. The one way to end up with no cache at all is the AI Image Editor’s cacheAuthToken: false, which calls your function for every request and leaves nothing to invalidate.

Each cache watches the clock and nothing else. It replaces a token it can see expiring, but it cannot tell that a token has spent its last operation, so a spent one keeps going out until something drops it.

That decides when you need invalidateAuthToken(), or invalidate() with AuthTokenCache:

SituationCall it?
The token expiredNo. The cache fetches a new one 30 seconds before exp, a lead you can change with AuthTokenCache’s skew option.
The page reloaded, or the user opened a new tabNo. The cache starts empty, so there is nothing to drop.
You assigned a different token function, for example on a React re-renderNo. The cached token is kept on purpose, so an inline function does not call your endpoint on every render.
The signed-in user changed within the same pageYes. The cached token still carries the previous user’s claims, and assigning a new token function does not replace it. Invalidating is the only thing that does.
OperationsLimitExceededError or AccessTokenExpiredError came back from an uploadNo, as long as authToken is a function or a provider: the library already dropped the refused token and retried the upload once. If the error still reached your code, that retry failed too, so troubleshoot the cause instead of invalidating. A plain token string is never retried.
Either came back from an AI Image Editor generation or editYes. Those requests are not retried, so the spent token stays cached until you drop it.
Your tokens have no readable expYes. The cache cannot tell when such a token goes stale, so it never replaces one on its own.

Turn it on

Warning: Do this last. A setting applies immediately to every client of the project, so one that doesn’t send a credential yet starts failing. Ship the credential first, and try it on a staging project.
  1. Go to your Dashboard and select the project.
  2. In the uploading settings, turn on Signed Uploads.

Signed AI image generation, right below it, is already on, so the AI Image Editor needs a token whether or not you do this.

When a credential is refused

Uploadcare refused it. Upload API answers with an error_code: AccessTokenInvalidError or AccessTokenExpiredError (401), ScopeForbiddenError or OperationsLimitExceededError (403), or SignatureRequiredError (400) when an upload request carried no credential. AI image requests without a token get AccessTokenInvalidError instead. A refused token is not always your problem to solve: on an upload, when the client holds a token function, an expired token and a spent operation limit are each retried once with a freshly fetched token, so the error only reaches you if the second token is refused too. ScopeForbiddenError is never retried, since a reissued token carries the same scope. AI image requests are never retried either, whatever the code. The reference has the cause of each.

Your token function failed. No request is sent, so there is no error_code. The JavaScript libraries wrap what your function threw in an AuthTokenResolverError, and nothing is cached or retried. To tell failures apart, include your endpoint’s HTTP status in what the function throws: a 401 means the session is gone and the user has to sign in again, while a 500 or a network error is worth retrying.

LibraryWhat you getOriginal error
Upload ClientA rejected AuthTokenResolverErrorerror.cause
File UploaderAn AUTH_TOKEN_ERRORpayload.error.cause
AI Image Editorcode: 'auth_token_failed' on uc:errorerror.cause.cause

Troubleshooting

SymptomLikely cause
SignatureRequiredError although you set authTokenThe library predates the option and ignores it. Check the minimum versions.
Uploads fail only sometimes, with SignatureRequiredError or a token errorYour token function returned nothing on a path you didn’t check: fetch doesn’t throw on a 500, and a body without a token gives undefined. Older library versions send that request unsigned, newer ones reject it as a token function failure. Check the status and the value.
AI Image Editor fails with AccessTokenInvalidError although uploads workSigned AI image generation is on by default, even when Signed Uploads is off. Set authToken on the editor, or on File Uploader when you use the plugin.
Every client broke when you turned the setting onIt applies immediately. Ship the credential first, then turn it on.
Invalid token. Reason: signature does not matchThe signing key is derived from the secret key, and the two plausible ways of deriving it are both wrong. See signing.
AccessTokenExpiredError mid-uploadA plain token is never refreshed. Pass a function instead, so the library can fetch a fresh one.
ScopeForbiddenError on an endpoint you listedScope paths match exactly, trailing slash included, and the message names the path the endpoint wanted. The scope table lists every endpoint’s path.
OperationsLimitExceededError reaches your code from an uploadThe client already retried once with a fresh token, so the second one was refused too. Usually a limit of 1 on a flow that needs more, or a token function handing back the same spent token.
Every AI generation fails once the limit is spentThe editor does not retry or drop the token by itself, so the spent one is sent again each time. Call invalidateAuthToken() when you see the error.
Your token endpoint is called more often than expectedEvery spent or expired token now costs one extra mint, because the client fetches a replacement and retries. A limit set too low turns that into a mint per upload.
OperationsLimitExceededError sooner than expectedThe counter is keyed by the token itself, so two tokens minted in the same second with identical claims are one token sharing one counter, and a user can exhaust the limit another was issued. Give each a unique sub or jti: see one counter per token. Failing that, the limit is per token rather than per user, so per-user quotas belong in the endpoint that issues them.

Moving from signatures

Signatures keep working against the same project setting, so you can move a piece at a time.

  • Requests you build yourself can move gradually. Upload API checks a token only when a request carries no signature, so a request sending both is checked the old way. Start sending tokens alongside signatures, then drop the signature.
  • File Uploader switches all at once. Setting authToken makes it send the token and ignore secureSignature, secureExpire and secureUploadsSignatureResolver, with a console warning if you set both.

What you gain: a token can be limited to particular endpoints and a number of operations, and it travels in a header instead of request parameters. A signature only carries an expiry.