> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://uploadcare.com/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://uploadcare.com/docs/_mcp/server.

Every Upload API request identifies the project with the public key, sent as the `UPLOADCARE_PUB_KEY` parameter or `pub_key` where the endpoint says so.

## Signed access \[#signed-access]

A project with [signed uploads](/docs/security/secure-uploads/) enabled additionally requires a credential generated on your backend with the project secret key. You create secret keys yourself in the [API keys](https://app.uploadcare.com/projects/-/api-keys/) section of your Dashboard. A project can have more than one.

There are two credential schemes:

* **Token**, a JSON Web Token in the `Authorization` header. It can restrict which endpoints the client may call and how many operations it may spend.
* **Signature** (deprecated), the `signature` and `expire` request parameters. It allows every endpoint until it expires. This is the legacy scheme; use tokens for new integrations.

> **Note**
>
> **Note:** If a request carries both a `signature` and an `Authorization` header, the signature is checked and the header is ignored.

Credentials are checked only when signed uploads are enabled for the project. The [signed uploads guide](/docs/security/secure-uploads/) explains how to enable them and which credential to choose.

### Token \[#jwt]

A token is a JSON Web Token ([RFC 7519](https://www.rfc-editor.org/rfc/rfc7519)) sent in the `Authorization` header with the `Bearer` scheme:

```http
Authorization: Bearer <jwt>
```

#### Signing \[#jwt-signing]

The token MUST be signed with `HS256`. The signing key is the SHA-256 digest of the project secret key, as raw 32 bytes rather than their hex representation. Any of the project's secret keys works.

#### Claims \[#jwt-claims]

Standard claims sit at the root of the payload. Everything Uploadcare interprets sits under the `uc` claim.

| Claim                               | Required | Description                                                                                                                                                                                                      |
| ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iat`                               | Yes      | Issue time, a Unix timestamp in seconds. MUST NOT be later than the current server time, with a 30-second clock-skew allowance. A token issued further in the future is rejected with `AccessTokenInvalidError`. |
| `exp`                               | Yes      | Expiration time, a Unix timestamp in seconds. The token lifetime, `exp` minus `iat`, MUST NOT exceed 24 hours. A 30-second clock skew is tolerated.                                                              |
| `iss`                               | No       | Any string identifying the issuer. Not interpreted.                                                                                                                                                              |
| `sub`                               | No       | Any string identifying the token user, such as an internal user identifier. Not interpreted.                                                                                                                     |
| `jti`                               | No       | Unique token identifier. Not interpreted.                                                                                                                                                                        |
| `uc.restrictions.scope`             | No       | The endpoints the token opens, as a list of paths. See [Scope and limits](#jwt-scope). Without it, the token opens every signed endpoint.                                                                        |
| `uc.restrictions.limits.operations` | No       | An integer from 1 to 100000, the maximum number of operations the token may spend during its lifetime. Without it, there is no limit.                                                                            |

Other standard claims are ignored. Keys under `uc` other than the ones listed above are not allowed: such a token is rejected.

> **Note**
>
> **Note:**
> Use `sub` or `jti` claims, especially if you generate tokens for multiple clients within a short period of time. Having all the same claims for different clients can lead to incorrect accounting of the client quota (the shared quota for different clients).

Payload example:

```json
{
  "iat": 1700000000,
  "exp": 1700003600,
  "sub": "example_user_01",
  "uc": {
    "restrictions": {
      "scope": ["/base/", "/multipart/*"],
      "limits": { "operations": 10 }
    }
  }
}
```

#### Scope and limits \[#jwt-scope]

Each signed endpoint has a scope path. A scope item matches a path exactly, or as a prefix when it ends with `/*`. A bare `/*` matches every endpoint. The list holds from 1 to 16 items, each up to 64 characters and starting with `/`.

| Endpoint                                                                      | Scope path          | Operations spent            |
| ----------------------------------------------------------------------------- | ------------------- | --------------------------- |
| [Direct upload](/docs/api/upload/upload/base-upload)                          | `/base/`            | One per file in the request |
| [Multipart upload start](/docs/api/upload/upload/multipart-file-upload-start) | `/multipart/start/` | One per request             |
| [Upload from URL](/docs/api/upload/upload/from-url-upload)                    | `/from_url/`        | One per request             |
| [Create group](/docs/api/upload/groups/create-files-group)                    | `/group/`           | None                        |

The `operations` counter lives as long as the token. A request that would exceed the limit is refused. Requests that fail with a 4xx response do not consume operations. A multipart upload that is started but never completed still consumes its operation.

#### Generate a token \[#jwt-generate]

Use any JWT library that supports `HS256`:

#### NodeJS

```javascript
    const crypto = require('crypto');
    const jwt = require('jsonwebtoken');

    const secret = 'YOUR_SECRET_KEY';
    const key = crypto.createHash('sha256').update(secret, 'utf8').digest();
    const now = Math.floor(Date.now() / 1000);

    const token = jwt.sign(
      {
        iat: now,
        exp: now + 60 * 30, // expire in 30 minutes
        uc: {
          restrictions: {
            scope: ['/base/'],
            limits: { operations: 5 },
          },
        },
      },
      key,
      { algorithm: 'HS256' }
    );
```

#### Python

```python
    import hashlib
    import time

    import jwt  # PyJWT

    secret = 'YOUR_SECRET_KEY'
    key = hashlib.sha256(secret.encode('utf-8')).digest()
    now = int(time.time())

    token = jwt.encode(
        {
            'iat': now,
            'exp': now + 60 * 30,  # expire in 30 minutes
            'uc': {
                'restrictions': {
                    'scope': ['/base/'],
                    'limits': {'operations': 5},
                },
            },
        },
        key,
        algorithm='HS256',
    )
```

#### Ruby

```ruby
    require 'digest'
    require 'jwt'

    secret = 'YOUR_SECRET_KEY'
    key = Digest::SHA256.digest(secret)
    now = Time.now.to_i

    token = JWT.encode(
      {
        iat: now,
        exp: now + 60 * 30, # expire in 30 minutes
        uc: {
          restrictions: {
            scope: ['/base/'],
            limits: { operations: 5 }
          }
        }
      },
      key,
      'HS256'
    )
```

#### Upload example \[#jwt-example]

```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
     -F "UPLOADCARE_PUB_KEY=YOUR_PUBLIC_KEY" \
     -F "file=@image.jpg" \
     "https://upload.uploadcare.com/base/"
```

```json
{
  "file": "c0d776d4-8c8e-47df-9e92-03b68b99c2ba"
}
```

#### Errors \[#jwt-errors]

| Error code                     | HTTP status | Message                                        | Cause                                                                                                                |
| ------------------------------ | ----------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `AccessTokenInvalidError`      | 401         | Invalid token. Reason: …                       | Wrong header format, unreadable token, signature that matches no secret key, or a bad claim. The reason names which. |
| `AccessTokenExpiredError`      | 401         | Expired token.                                 | `exp` is in the past, beyond the 30-second clock-skew allowance.                                                     |
| `ScopeForbiddenError`          | 403         | `uc.restrictions.scope` does not allow …       | The endpoint is outside the token's scope. The message names the required path.                                      |
| `OperationsLimitExceededError` | 403         | The operation limit of the token is exhausted. | The token has spent all of its `operations`.                                                                         |

### Signature \[#signature]

> **Warning**
>
> **Warning:** The signature scheme stays supported. However, for your control, security and to preserve the billing balance, we recommend using [tokens](#jwt) for new integrations.

The `signature` and `expire` parameters are sent together with the request, either in the query string or as form fields.

| Parameter   | Description                                                                                                                                                                                                                                 |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `expire`    | A Unix timestamp in seconds after which the signature stops working.                                                                                                                                                                        |
| `signature` | A hex-encoded [HMAC-SHA256](https://en.wikipedia.org/wiki/HMAC) digest computed from two inputs: the project secret key (UTF-8 encoded) as the key, and the `expire` value as a decimal string, for example `"1700000000"`, as the message. |

Any of the project's secret keys works.

#### Generate a signature \[#signature-generate]

> **Warning**
>
> **Warning:** The `@uploadcare/signed-uploads` package accepts milliseconds and converts to seconds internally. If you implement signing without this package, `expire` must be in seconds, not milliseconds.

#### JavaScript

```javascript
// Option 1: by expiration timestamp (milliseconds since epoch)
import { generateSecureSignature } from '@uploadcare/signed-uploads'

const { secureSignature, secureExpire } = generateSecureSignature('YOUR_SECRET_KEY', {
  expire: Date.now() + 60 * 30 * 1000 // expire in 30 minutes
})
```

```javascript
// Option 2: by expiration date
import { generateSecureSignature } from '@uploadcare/signed-uploads'

const { secureSignature, secureExpire } = generateSecureSignature('YOUR_SECRET_KEY', {
  expire: new Date("2099-01-01") // expire on 2099-01-01
})
```

```javascript
// Option 3: by lifetime
import { generateSecureSignature } from '@uploadcare/signed-uploads'

const { secureSignature, secureExpire } = generateSecureSignature('YOUR_SECRET_KEY', {
  lifetime: 60 * 30 * 1000 // expire in 30 minutes
})
```

#### NodeJS

```javascript
    const crypto = require('crypto');

    function generateSignature(secret, expire) {
      const hmac = crypto.createHmac('sha256', secret);
      hmac.update(expire);
      return hmac.digest('hex');
    }

    // secret key
    const secret = 'YOUR_SECRET_KEY';

    // define expiry (e.g. 120 seconds)
    const expire = (Math.round(Date.now() / 1000) + 120).toString();
    const signature = generateSignature(secret, expire);
```

#### Python

```python
    import time
    import hashlib
    import hmac

    def generate_secure_signature(secret, expire):
        k, m = secret, str(expire).encode('utf-8')
        if not isinstance(k, (bytes, bytearray)):
            k = k.encode('utf-8')

        return hmac.new(k, m, hashlib.sha256).hexdigest()

    # Expire in 30 min, e.g., 1454903856
    expire = int(time.time()) + 60 * 30

    # Secret key of your project
    secret = 'YOUR_SECRET_KEY'

    # Example result: '9890adc8886d27c93befed595abfbca514b7592a88e2f64dbddbea2d369a68dc'
    signature = generate_secure_signature(secret, expire)
```

#### Ruby

```ruby
    require "openssl" # if necessary

    def generate_signature(secret, expire_at)
      OpenSSL::HMAC.hexdigest("SHA256", secret, expire_at.to_i.to_s)
    end

    secret = 'secret'
    expire_at = Time.now + 60 * 2 # 2 minutes from now
    generate_signature(secret, expire_at)
```

#### Elixir

```elixir
    def generate_signature(secret_key, expire_at) when is_integer(expire_at),
    do: generate_signature(secret_key, Integer.to_string(expire_at))

    def generate_signature(secret_key, expire_at),
    do:
    :hmac
    |> :crypto.mac(:sha256, secret_key, expire_at)
    |> Base.encode16(case: :lower)
```

#### Upload example \[#signature-example]

```bash
curl -F "UPLOADCARE_PUB_KEY=YOUR_PUBLIC_KEY" \
     -F "signature=YOUR_SIGNATURE" \
     -F "expire=YOUR_EXPIRE" \
     -F "file=@image.jpg" \
     "https://upload.uploadcare.com/base/"
```

#### Errors \[#signature-errors]

| Error code                         | HTTP status | Message                            | Cause                                   |
| ---------------------------------- | ----------- | ---------------------------------- | --------------------------------------- |
| `SignatureRequiredError`           | 400         | `signature` is required.           | Missing `signature` parameter.          |
| `SignatureExpirationRequiredError` | 400         | `expire` is required.              | Missing `expire` parameter.             |
| `SignatureExpirationInvalidError`  | 400         | `expire` must be a UNIX timestamp. | `expire` is not a valid integer.        |
| `SignatureExpirationError`         | 403         | Expired signature.                 | `expire` is in the past.                |
| `SignatureInvalidError`            | 403         | Invalid signature.                 | The HMAC matches no project secret key. |

For the complete list of Upload API errors, see [Errors](/docs/api/upload/errors/).