<span aria-hidden="true" id="authentication"></span>



# Authentication

<span aria-hidden="true" id="auth-keys"></span>



## Auth Keys

For multipart Assembly-creation requests authenticated with an Auth Key, include an `auth` object
inside the JSON-encoded `params` form field. The smallest such `params` value is shown below.

```json
{
  "auth": {
    "key": "23c96d084c744219a2ce156772ec3211"
  }
}
```

The `key` field refers to the *Auth Key* associated with your Transloadit Workspace, found
on the [Credentials](/c/template-credentials/) page. The above is the minimum authentication object for
an Assembly request that uses an Auth Key. Other endpoints and authentication methods can use
different request formats, as described by their endpoint documentation and below.

Assemblies that use `/transloadit/import`, directly or through a Template, require
either a bearer token or signed `params` with a future `params.auth.expires` timestamp.
This applies even when Workspace Signature Authentication is disabled. An Auth Key alone
is not sufficient; bearer-authenticated requests do not need a separate signature or expiry.
Public Storage Builtin Templates are the exception: they can serve unsigned requests only
when every Storage import remains within a declared public prefix. This does not authorize
unsigned Assemblies with arbitrary Steps or access to private Storage paths.

<span aria-hidden="true" id="signature-authentication"></span><span aria-hidden="true" id="enabling-authentication"></span>



## Signature Authentication

We recommend enabling *Signature Authentication* on your account, especially if you're
integrating with Transloadit from an untrusted environment (such as from the browser with
[Uppy](https://uppy.io/)). You can enable *Signature Authentication* from your
[Workspace Settings](/c/settings/).

> [!Warning]
> We *strongly recommend* enabling *Signature Authentication* when interfacing
> with our API, particularly in untrusted environments where users may be able to access your
> *Auth Key*.

With *Signature Authentication* enabled, your Workspace's *Auth Secret* (found
next to your *Auth Key* on the [Credentials](/c/template-credentials/) page) is
then used as the key for an HMAC generated from the exact serialized `params` value. That value contains both a `key` (which is your
*Auth Key* as mentioned earlier), and an `expires` param, which is a timestamp in the near
future used as an expiry date for the request.

For creating *Assemblies* with Transloadit, your back-end could calculate a
*Signature* that only covers certain parameters, authenticated users, and a timeframe that
it deems legitimate usage. For instance, it would refuse to generate a signature for users that are
not logged in. You could use any business logic on the server-side here to decide if you hand out a
signature, or not. Transloadit can require a correct signature for the payload when an API
request authenticates with your Auth Key.

To require Signature Authentication for API requests authenticated with your Auth Key:

1. Go to the [Workspace Settings](/c/settings/) in your account.
2. In the API Settings section, enable the **Require a correct Signature** option.
3. Wait for the automatic save to complete.

Valid [bearer tokens](#bearer-tokens-client-credentials) bypass signature requirements, including
Workspace and Template settings; token scopes and audience restrictions still apply. These
settings do not add authentication to capability-based access such as Assembly status,
cancellation or resumable upload URLs. Keep Assembly IDs and capability URLs private, and follow
each endpoint’s authentication guidance.

> [!Note]
> Most back-end [SDKs](/docs/sdks.md) automatically use Signature Authentication when you
> supply your *Auth Secret*. So perhaps, just this introduction is all you need to know. If
> you are integrating Transloadit into untrusted environments, however, such as browsers
> ([Uppy](/docs/sdks/uppy.md)!), you’ll want to continue reading to see how your back-end can supply
> signatures to it.

<span aria-hidden="true" id="how-to-generate-signatures"></span><span aria-hidden="true" id="using-authentication"></span><span aria-hidden="true" id="signatures-for-templates"></span>



## How to generate Signatures

So, how does this all look?

The typical `params` field when creating an *Assembly* **without** Signature Authentication
is as follows:

```jsonc
{
  "auth": {
    "key": "23c96d084c744219a2ce156772ec3211"
  },
  "steps": {
    // …
  }
}
```

The `auth.key` in this example is the *Auth Key* from
[API Credentials in your account](/c/template-credentials/).

To sign this request, the additional `auth.expires` field needs to be added. This adds it to our
payload, which is protected by our signature. If someone would change it, Transloadit would reject
the request as the signature no longer matches. You signed a different payload than the one we
received. If the signature does match, then we will naturally compare and reject by date as
instructed. This way, requests become very hard to indefinitely repeat by a third party that got a
hold of this payload. Because even though our A+ grade HTTPS should already go a long way in
preventing that, browser cache could be easier to snoop on.

The `expires` property must contain a timestamp in the (near) future. Use ISO 8601 format
(`YYYY-MM-DDTHH:mm:ss.sssZ`) for the date, making sure that UTC is used for the timezone. For
example:

```jsonc
{
  "auth": {
    "key": "23c96d084c744219a2ce156772ec3211",
    "expires": "YOUR_FUTURE_ISO_8601_TIMESTAMP"
  },
  "steps": {
    // …
  }
}
```

To calculate the signature for this request:

1. From your front-end, stringify the above JavaScript object into JSON and send it to your
   back-end.
2. From your back-end, calculate an [RFC 6234-compliant](https://www.ietf.org/rfc/rfc6234.txt) HMAC
   hex signature on the string, with your *Auth Secret* as the key and the algorithm
   configured in your Auth Key’s `signature_algo`. New Auth Keys default to `sha384`.
   Legacy Auth Keys without a configured algorithm accept `sha384`, `sha256`, or `sha1`.
   Prefix the `signature` string with the algorithm name in lowercase. For example, the default
   algorithm uses `sha384:<HMAC-signature>`. You can send that string to your front-end (as long as you
   made the appropriate checks to guarantee it was a genuine request from your front-end).
3. From your front-end, add a `signature` multipart POST field containing this value to your request
   (e.g., with a hidden field in an HTML form).

> [!Note]
> If your implementation uses a `template_id` instead of `steps`, there’s no need to
> generate a signature for the *Instructions* that your *Template* contains. We
> should only sign communication payloads.

> [!Note] We strongly recommend including a randomly generated `params.nonce` value for each request
> at the top level of `params`. This makes independently generated signatures distinct and avoids accidental
> signature reuse. A `nonce` does not make retries idempotent: retrying an Assembly-creation request
> can create another Assembly. Handle retry deduplication in your application when needed.
> Do not reuse signed `params` across endpoints. Template reads, Auth Key listing, Template Credential
> listing, and billing reads reject signatures already recorded for a different endpoint or used
> to create an Assembly, with `SIGNATURE_REUSE_DETECTED`. Generate fresh signed params for each request.

The full request should look similar to the below:

```jsonc
{
  "params": {
    "auth": {
      "key": "23c96d084c744219a2ce156772ec3211",
      "expires": "YOUR_FUTURE_ISO_8601_TIMESTAMP",
    },
    "nonce": "04ac6cb6-df43-41fb-a7fd-e5dd711a64e1",
    "steps": {
      // …
    },
  },
  "signature": "sha384:YOUR_SIGNATURE",
}
```

Once the request was received by Transloadit, we also generate a signature following the same
process, and compare the two signatures. If the signatures are different, then our servers will
respond with `INVALID_SIGNATURE`.

In summary, the process is as follows:

1. Generate a JSON payload to send to Transloadit as the `params` field.
2. Calculate a signature based off the contents of the payload, using your *Auth Secret* as
   a key.
3. Send the request to Transloadit, with the signature passed in the `signature` field
4. Transloadit will calculate the same signature using your account's *Auth Secret*, and
   the contents of the payload.
5. If the signatures match, the request is permitted, and an appropriate response is sent.
   Otherwise, the request is denied, and an error will be returned with the code
   `INVALID_SIGNATURE`.

This lets Transloadit authenticate the caller and verify the integrity of `params`, since a third
party could not calculate a matching signature without access to your *Auth Secret*. TLS authenticates Transloadit to your client
and protects the connection. Webhook signing is a separate flow for requests Transloadit sends to
your servers.

Below are a few examples of how to perform a POST request to create an *Assembly*. We
strongly recommend using one of our [SDKs](/docs/sdks.md), which handle signature generation
automatically and are well-tested.

> [!Note] Webhooks are signed differently from API requests.
> Transloadit signs the exact JSON string in the `transloadit` form field using
> HMAC-SHA1 and the applicable Auth Secret. The `signature` field contains
> the hexadecimal digest without an algorithm prefix. Follow the
> [webhook verification instructions](/docs/api/webhooks.md#verify-the-signature), including how to select the Auth Secret,
> instead of using the API-request signing examples below.

<span aria-hidden="true" id="example-code-for-different-languages"></span><span aria-hidden="true" id="auth-example"></span><span aria-hidden="true" id="authentication-implementations"></span>



## Example code for different languages

The examples below demonstrate creating Assemblies using our official SDKs. The SDKs handle all
signature generation internally, making integration simpler and more secure.

**Node.js**

```js
// yarn add @transloadit/node
// or
// npm install --save @transloadit/node

import { Transloadit } from '@transloadit/node'

const transloadit = new Transloadit({
  authKey: 'YOUR_TRANSLOADIT_KEY',
  authSecret: 'YOUR_TRANSLOADIT_SECRET',
})

const response = await transloadit.createAssembly({
  params: {
    template_id: 'YOUR_TRANSLOADIT_TEMPLATE_ID',
    // your other params like notify_url, fields, etc.
  },
  waitForCompletion: true,
})

console.log(response)
```

If you need to calculate a signature separately (e.g., for front-end use), you can use
`calcSignature`:

```js
const { signature, params } = transloadit.calcSignature({
  template_id: 'YOUR_TRANSLOADIT_TEMPLATE_ID',
})

console.log(signature, params)
```

[View signature implementation source code](https://github.com/transloadit/node-sdk/blob/main/packages/node/src/Transloadit.ts)

**PHP**

```php
<?php
// composer require transloadit/php-sdk

require 'vendor/autoload.php';

use transloadit\Transloadit;

$transloadit = new Transloadit([
  'key'    => 'YOUR_TRANSLOADIT_KEY',
  'secret' => 'YOUR_TRANSLOADIT_SECRET',
]);

$response = $transloadit->createAssembly([
  'params' => [
    'template_id' => 'YOUR_TRANSLOADIT_TEMPLATE_ID',
    // your other params like notify_url, fields, etc.
  ],
]);

print_r($response);
```

[View signature implementation source code](https://github.com/transloadit/php-sdk/blob/main/lib/transloadit/Transloadit.php)

**Ruby**

```ruby
# gem install transloadit

require 'transloadit'

transloadit = Transloadit.new(
  :key    => 'YOUR_TRANSLOADIT_KEY',
  :secret => 'YOUR_TRANSLOADIT_SECRET'
)

assembly = transloadit.assembly
response = assembly.create!(
  :template_id => 'YOUR_TRANSLOADIT_TEMPLATE_ID'
  # your other params like notify_url, fields, etc.
)

puts response.reload!
```

`reload!` fetches the current status once; it does not wait for processing to finish.

[View signature implementation source code](https://github.com/transloadit/ruby-sdk/blob/main/lib/transloadit.rb)

**Python**

```python
# pip install pytransloadit

from transloadit import client

tl = client.Transloadit('YOUR_TRANSLOADIT_KEY', 'YOUR_TRANSLOADIT_SECRET')

assembly = tl.new_assembly({
    'template_id': 'YOUR_TRANSLOADIT_TEMPLATE_ID',
    # your other params like notify_url, fields, etc.
})
with open('myfile.jpg', 'rb') as f:
    assembly.add_file(f)
    response = assembly.create()

print(response.data)
```

[View signature implementation source code](https://github.com/transloadit/python-sdk/blob/main/transloadit/client.py)

**Java**

```java
// implementation 'com.transloadit.sdk:transloadit:1.0.1'
// Check https://github.com/transloadit/java-sdk for the latest version

package com.transloadit.examples;

import com.transloadit.sdk.Transloadit;
import com.transloadit.sdk.Assembly;
import com.transloadit.sdk.response.AssemblyResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        Transloadit transloadit = new Transloadit(
            "YOUR_TRANSLOADIT_KEY",
            "YOUR_TRANSLOADIT_SECRET"
        );

        Assembly assembly = transloadit.newAssembly();
        assembly.addOption("template_id", "YOUR_TRANSLOADIT_TEMPLATE_ID");
        // your other params like notify_url, fields, etc.

        AssemblyResponse response = assembly.save();
        System.out.println(response.json());
    }
}
```

[View signature implementation source code](https://github.com/transloadit/java-sdk/blob/main/src/main/java/com/transloadit/sdk/Transloadit.java)

> [!Note]
> If you prefer to see the raw signature implementation details (e.g., to implement signing in a
> language we don’t have an SDK for), check the source code links above. The signature is an
> [RFC 6234-compliant](https://www.ietf.org/rfc/rfc6234.txt) HMAC hex digest calculated on the
> JSON-encoded params string, using your *Auth Secret* as
> the key and the algorithm configured in your Auth Key’s `signature_algo`. New Auth Keys default
> to `sha384`. Prefix the signature with its lowercase algorithm name
> (e.g., `sha384:...`).

**cURL**

```sh
curl --fail-with-body -sS --location 'https://api2.transloadit.com/assemblies' \
  --form 'params={"auth":{"key":"23c96d084c744219a2ce156772ec3211","expires":"YOUR_FUTURE_ISO_8601_TIMESTAMP"},"template_id":"9cf67cbba601e37ee10c442b037e0"}' \
  --form 'signature=sha384:YOUR_SIGNATURE' \
  --form 'files=@/path/to/your/file.jpg'
```

**HTML**

```html
<!--
  We’ll be inlining the following Assembly Instructions into a hidden params field,
  escaping HTML entities such as &lt;, a task best left to machines :)

  {
    "redirect_url": "https://example.com/success",
    "auth": {
      "key": "YOUR_TRANSLOADIT_KEY",
      "expires": "YOUR_FUTURE_ISO_8601_TIMESTAMP"
    },
    "template_id": "YOUR_TEMPLATE_ID"
  }
-->

<form
  method="POST"
  enctype="multipart/form-data"
  action="https://api2.transloadit.com/assemblies"
>
  <input
    type="hidden"
    name="params"
    value='{"redirect_url":"https://example.com/success","auth":{"key":"YOUR_TRANSLOADIT_KEY","expires":"YOUR_FUTURE_ISO_8601_TIMESTAMP"},"template_id":"YOUR_TEMPLATE_ID"}'
  >
  <input type="hidden" name="signature" value="sha384:YOUR_SIGNATURE">
  <input type="file" name="myfile_1" multiple="multiple">
  <input type="submit" value="Upload">
</form>
```

**JavaScript**

```js
const formData = new FormData()
formData.append(
  'params',
  '{"auth":{"key":"23c96d084c744219a2ce156772ec3211","expires":"YOUR_FUTURE_ISO_8601_TIMESTAMP"},"template_id":"9cf67cbba601e37ee10c442b037e0"}',
)
formData.append('signature', 'sha384:YOUR_SIGNATURE')
const file = new File(['example upload'], 'example.txt', { type: 'text/plain' })
formData.append('files', file)

const requestOptions = {
  method: 'POST',
  body: formData,
  redirect: 'follow',
}

fetch('https://api2.transloadit.com/assemblies', requestOptions)
  .then((response) => {
    if (!response.ok) {
      throw new Error(`Assembly creation failed with HTTP ${response.status}`)
    }
    return response.text()
  })
  .then((result) => console.log(result))
  .catch((error) => console.error(error))
```

<span aria-hidden="true" id="smart-cdn-signed-urls"></span><span aria-hidden="true" id="smart-cdn"></span>



## Smart CDN signed URLs

To sign a Smart CDN URL, a similar process as for regular API signatures is used. A HMAC digest is
calculated on a string, that is derived from the Smart CDN URL, with the *Auth Secret* as
the key. For the signature to be valid, the used *Auth Key* must be enabled for Smart CDN
use.

> [!Important]
> To generate a signed Smart CDN URL, use the Auth Key designated for Smart CDN usage from your
> Credentials page. Smart CDN URLs require `sha256`. Regular API request
> signatures use the algorithm configured in the Auth Key’s `signature_algo`; new Auth Keys default
> to `sha384`.

> [!Note]
> Legacy Smart CDN signatures based on `s=` and `expires=` are deprecated. New integrations should
> always use `sig=` with `exp=`.

The generation of a Smart CDN signature must be performed on the back-end. The process uses the
*Auth Secret*, which is confidential and must not be exposed to your users on the
front-end.

A typical Smart CDN URL has the following structure:

```
https://[your-workspace].tlcdn.com/[template-name]/[file-path]?[parameters]
```

* `[your-workspace]` is your Transloadit Workspace name
* `[template-name]` is the name of your Template
* `[file-path]` is the path to the file you want to transform
* `[parameters]` are desired transformation parameters (e.g. `h=100`)

A signed Smart CDN URL is generated by following these steps:

1. Add the `exp` query parameter for defining a time in the future after which the signature is not
   accepted by the Smart CDN anymore. This is useful to limit temporal access to a file. The
   expiration moment is represented by the number of milliseconds since the UNIX epoch (the midnight
   at the beginning of January 1, 1970, UTC). While this parameter is optional, we highly recommend
   always setting an expiration time. For example, a signature using `exp=1722517200000` is valid
   until Thu, 01 Aug 2024 13:00:00 GMT.
2. Add the `auth_key` query parameter to define the *Auth Key* corresponding to the
   *Auth Secret* that is used to create the signature. If this parameter is not set,
   Transloadit's API assumes that the oldest, Smart-CDN-enabled *Auth Key* pair has been
   used for the signature. Setting the `auth_key` parameters allows you to rotate your *Auth
   Key* without interrupting your users, and we thus highly recommend setting it. For example:
   `auth_key=23c96d084c744219a2ce156772ec3211`
3. Sort the query parameters by key in ascending order using UTF-16 code units, matching
   `URLSearchParams.sort()`. The
   sorting should be stable, i.e. if a key appears multiple times in the query string, the
   corresponding values should retain their relative ordering. For example,
   `h=100&f=png&f=jpg&auth_key=hello&exp=123` is sorted into
   `auth_key=hello&exp=123&f=png&f=jpg&h=100`.
4. Construct the string to sign by concatenating the values:
   ```
   [your-workspace]/[template-name]/[file-path]?[sorted-parameters]
   ```
   The values for `[your-workspace]`, `[template-name]`, and `[file-path]` must be URL-encoded to
   ensure they only contain URL-safe characters. Note that the string does not start with a leading
   slash. The `?` character must be omitted if `[sorted-parameters]` is empty.
5. Calculate an [RFC 6234-compliant](https://www.ietf.org/rfc/rfc6234.txt) HMAC hex signature on the
   string to sign, with your *Auth Secret* as the key, and SHA256 as the hash algorithm.
   Prefix the hex signature with the algorithm name in lowercase and a colon, i.e. `sha256:`. For
   example, for SHA256, use `sha256:[hmac-signature]`.
6. Append the prefixed hex signature under the `sig` query parameter to the URL, giving the signed
   Smart CDN URL:
   ```
   https://[your-workspace].tlcdn.com/[template-name]/[file-path]?[parameters]&sig=sha256:[hmac-signature]
   ```
   The values for `[your-workspace]`, `[template-name]`, and `[file-path]` must be URL-encoded to
   ensure they only contain URL-safe characters. This signed URL can then be sent to or used in your
   front-end until the expiration date is reached.

<span aria-hidden="true" id="security-and-cache-lifetime"></span>



### Security and cache lifetime

Signed Smart CDN URLs are not just an access-control mechanism. Their expiration also determines how
long a freshly generated result may remain cacheable.

* Shorter `exp` values reduce the replay window and tighten access control.
* Longer `exp` values increase cache reuse, reduce origin work, and generally lower latency and
  encoding volume.
* In practice, the effective cache lifetime of a signed Smart CDN response is bounded by the
  remaining signature lifetime.

That means the trade-off is straightforward:

* More security sensitivity: use a shorter `exp`, which also means a shorter effective cache TTL.
* More cache reuse and lower cost: use a longer `exp`, which also means the URL remains usable for
  longer.

Choose the expiration window according to the sensitivity of the content and how much cache reuse
you want. For many image and preview use cases, a moderate expiration window gives a good balance.
For highly sensitive content, use a much shorter one.

<span aria-hidden="true" id="example-code"></span>



### Example code

Below you can find examples in different languages for generating signed Smart CDN URLs using our
SDKs.

**Node.js**

```js
// yarn add @transloadit/node
// or
// npm install --save @transloadit/node

import { Transloadit } from '@transloadit/node'

const transloadit = new Transloadit({
  authKey: 'YOUR_TRANSLOADIT_KEY',
  authSecret: 'YOUR_TRANSLOADIT_SECRET',
})

const url = transloadit.getSignedSmartCDNUrl({
  workspace: 'YOUR_WORKSPACE',
  template: 'YOUR_TEMPLATE',
  input: 'image.png',
  urlParams: { height: 100, width: 100 },
})

console.log(url)
```

**PHP**

```php
// composer require transloadit/php-sdk

require 'vendor/autoload.php';

use transloadit\Transloadit;

$transloadit = new Transloadit([
  'key'    => 'YOUR_TRANSLOADIT_KEY',
  'secret' => 'YOUR_TRANSLOADIT_SECRET',
]);

$url = $transloadit->signedSmartCDNUrl(
  'YOUR_WORKSPACE',
  'YOUR_TEMPLATE',
  'image.png',
  [
    'width' => 100,
    'height' => 100
  ],
);

echo $url;
```

**Ruby**

```ruby
# gem install transloadit

require 'transloadit'

transloadit = Transloadit.new(
  :key    => 'YOUR_TRANSLOADIT_KEY',
  :secret => 'YOUR_TRANSLOADIT_SECRET'
)

url = transloadit.signed_smart_cdn_url(
  workspace: 'YOUR_WORKSPACE',
  template: 'YOUR_TEMPLATE',
  input: 'image.png',
  url_params: {
    width: 100,
    height: 100
  }
)
```

**Python**

```python
# pip install pytransloadit

from transloadit import client

tl = client.Transloadit('YOUR_TRANSLOADIT_KEY', 'YOUR_TRANSLOADIT_SECRET')

url = tl.get_signed_smart_cdn_url(
  workspace='YOUR_WORKSPACE',
  template='YOUR_TEMPLATE',
  input='image.png',
  url_params={
    'height': 100,
    'width': 100,
  }
)

print(url)
```

**Go**

```go
// go get github.com/transloadit/go-sdk

package main

import (
  "fmt"
  "net/url"

  transloadit "github.com/transloadit/go-sdk"
)

func main() {
  fmt.Println(GetSmartCDNUrl())
}

func GetSmartCDNUrl() string {
  options := transloadit.DefaultConfig
  options.AuthKey = "YOUR_TRANSLOADIT_KEY"
  options.AuthSecret = "YOUR_TRANSLOADIT_SECRET"
  client := transloadit.NewClient(options)

  params := url.Values{}
  params.Add("height", "100")
  params.Add("width", "100")

  url := client.CreateSignedSmartCDNUrl(transloadit.SignedSmartCDNUrlOptions{
    Workspace: "YOUR_WORKSPACE",
    Template:  "YOUR_TEMPLATE",
    Input:     "image.png",
    URLParams: params,
  })

  return url
}
```

**Java**

```java
// implementation 'com.transloadit.sdk:transloadit:1.0.1'
// Check https://github.com/transloadit/java-sdk for the latest version

package com.transloadit.examples;

import com.transloadit.sdk.Transloadit;
import com.transloadit.sdk.exceptions.LocalOperationException;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws LocalOperationException {
        System.out.println(getSmartCDNUrl());
    }

    public static String getSmartCDNUrl() throws LocalOperationException {
        Transloadit transloadit = new Transloadit("YOUR_TRANSLOADIT_KEY", "YOUR_TRANSLOADIT_SECRET");

        Map<String, List<String>> params = new HashMap<>();
        params.put("height", Collections.singletonList("100"));
        params.put("width", Collections.singletonList("100"));

        return transloadit.getSignedSmartCDNUrl(
                "YOUR_WORKSPACE",
                "YOUR_TEMPLATE",
                "image.png",
                params
        );
    }
}

```

<span aria-hidden="true" id="read-access-after-cancellation"></span>



## Read access after cancellation

Canceled Workspaces cannot create new bearer tokens. Endpoints that explicitly permit access
after cancellation link to this recipe. Use an existing active Auth Key and its Auth Secret;
the key must still grant the scopes listed for the endpoint. This does not restore write access
or make other endpoints available after cancellation.

In a trusted server-side Node.js project, install the SDK with `yarn add @transloadit/node`.
Set `TRANSLOADIT_KEY` and `TRANSLOADIT_SECRET`, then set `TRANSLOADIT_URL` to the complete HTTPS
URL shown on the endpoint page, replacing any path parameters with your resource values.
Keep both credentials, the signed URL and the response private. Never run this setup in browser code.

The SDK signs `params` with `sha384`, the default for new Auth Keys, and supplies the expiry.
The example adds a fresh nonce to avoid signature reuse. If your key uses another signature
algorithm, pass it as the second argument to `calcSignature`. Put any endpoint filters inside
the object passed as its first argument, alongside the nonce.
The SDK does not call `/token` for this recipe.

Save this as `read-api.mjs` and run `node read-api.mjs`:

```js
import { randomUUID } from 'node:crypto'

import { Transloadit } from '@transloadit/node'

const { TRANSLOADIT_KEY, TRANSLOADIT_SECRET, TRANSLOADIT_URL } = process.env
if (!TRANSLOADIT_KEY || !TRANSLOADIT_SECRET || !TRANSLOADIT_URL) {
  throw new Error('Set TRANSLOADIT_KEY, TRANSLOADIT_SECRET, and TRANSLOADIT_URL')
}
const transloadit = new Transloadit({
  authKey: TRANSLOADIT_KEY,
  authSecret: TRANSLOADIT_SECRET,
})
const { params, signature } = transloadit.calcSignature({ nonce: randomUUID() })
const url = new URL(TRANSLOADIT_URL)
url.searchParams.set('params', params)
url.searchParams.set('signature', signature)
const response = await fetch(url, { redirect: 'error' })
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`)
const result = await response.json()
if (result.error) throw new Error(result.error)
console.log(JSON.stringify(result, null, 2))
```

<span aria-hidden="true" id="connect-an-mcp-client-by-url"></span>



## Connect an MCP client by URL

Agent clients such as ChatGPT, Codex, Claude.ai, Claude Desktop, Claude Code and Cursor connect
to the hosted MCP server at `https://api2.transloadit.com/mcp` with nothing but that URL. The Transloadit API
is an OAuth 2.1 authorization server for it: the client discovers the endpoints, identifies
itself, sends you to the Console to log in and consent, and ends up with a short-lived Bearer
token for the `mcp` audience plus a refresh token. No Auth Secret ever leaves your account.

1. **Discovery.** The MCP server answers unauthenticated requests with `401` and a
   `WWW-Authenticate: Bearer resource_metadata="https://api2.transloadit.com/.well-known/oauth-protected-resource/mcp"` header.
   That [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) document names `https://api2.transloadit.com` as the authorization server, whose
   [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414) metadata at `https://api2.transloadit.com/.well-known/oauth-authorization-server` lists the endpoints below.
2. **Client identification.** Either present a
   [Client ID Metadata Document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/):
   an HTTPS URL that is the `client_id` and serves JSON with the same `client_id`, a
   `client_name` and the `redirect_uris`. Or register once through
   [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591) Dynamic Client Registration at `https://api2.transloadit.com/oauth/register`;
   clients either send only their `client_id` (`token_endpoint_auth_method: "none"`, also when
   omitted), authenticate with a client secret (`client_secret_basic` or `client_secret_post`), or
   authenticate with `private_key_jwt` and a `jwks_uri`. The answer carries an opaque `client_id`,
   plus a `client_secret` for the secret methods, which is shown only in that response.
   Redirect URIs must be HTTPS URLs, matched exactly, or loopback `http://localhost` /
   `http://127.0.0.1` URLs, matched on any port. A client registered with a secret sends it on
   every token and revocation request, through HTTP Basic or as `client_id` and `client_secret`
   in the form. Other token requests authenticate the client with `none` or `private_key_jwt`,
   whichever its document lists (in `token_endpoint_auth_methods_supported`, or else as
   `token_endpoint_auth_method`) or its registration declares. The first token request binds
   the grant to the method it used: once a request for a grant carries an
   [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523) `client_assertion`, every later token and
   revocation request for it must carry one too. That assertion is a JWT signed (RS256 or ES256)
   with a key from the client’s `jwks_uri`, with the client ID as `iss` and `sub`. Its `aud` is a
   configured token endpoint URL or its origin (without a trailing slash) for this deployment.
   It has an `exp` within five minutes and a `jti` that is accepted once per client per region
   while the assertion remains valid. Use a fresh `jti` for each request.
3. **Consent.** The client opens `https://transloadit.com/c/oauth/authorize` with `response_type=code`, its `client_id`,
   `redirect_uri`, a PKCE `code_challenge` (`S256` only), optionally `scope` and `state`, and
   `resource=https://api2.transloadit.com/mcp`. You log in, pick a Workspace and approve; the Console redirects the
   browser back with `code`, your `state` and `iss=https://api2.transloadit.com`. The code is bound to the
   client, the redirect URI, the challenge and the Workspace, and expires after 120 seconds.
4. **Tokens.** The client posts `grant_type=authorization_code` with `code`, `code_verifier`,
   `client_id` and `redirect_uri` to `https://api2.transloadit.com/token` and receives an `access_token` (valid for
   604,800 seconds, 7 days, by default), its `scope` and a `refresh_token`. Posting
   `grant_type=refresh_token` with `refresh_token` and `client_id` returns a new pair and
   retires the presented refresh token; refresh tokens live 30 days and reusing a retired one
   revokes the whole lineage. Clients that authenticate with a client secret get the same
   refresh token back instead, valid for another 30 days from that refresh.
5. **Revocation.** Post `token` (an access or refresh token) to `https://api2.transloadit.com/oauth/revoke` per
   [RFC 7009](https://www.rfc-editor.org/rfc/rfc7009), or revoke the connection in the Console.

The `resource` indicator ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707)) decides what the
token is for. `resource=https://api2.transloadit.com/mcp` (the default when omitted) mints an `mcp` token that
carries at most the safe MCP scopes (`assemblies:write`, `assemblies:read`, `templates:read`), narrowed to what the Workspace’s
Auth Key grants, and that only the MCP server accepts. `resource=https://api2.transloadit.com` (with or
without a trailing slash) mints an `api2` token for applications that call the REST API directly,
such as [Vercel Connect](https://vercel.com/docs/connect/providers) integrations: it carries the
requested scopes narrowed to the Auth Key’s scopes, or everything the key grants when `scope` is
omitted, except that a grant never carries Auth Key or Template Credential management
(`auth_keys:*`, `template_credentials:*`, or the global `read`/`write` that imply them), so it cannot
create or reveal lasting credentials. Any other `resource` answers `invalid_target`.

OAuth protocol errors from client registration, authorization, and revocation use
the standard `error` and `error_description` body; rate limits can return `RATE_LIMIT_REACHED`.

At `/token`, OAuth grant errors for `authorization_code` and `refresh_token` use the standard
`error` and `error_description` body. Rate limits return `RATE_LIMIT_REACHED` (429), and
unexpected internal failures return `SERVER_500` (500). Malformed requests rejected
before the grant is identified can receive an HTTP Basic challenge (401), or
`TOKEN_INVALID_REQUEST` if Basic credentials were supplied.

<span aria-hidden="true" id="bearer-tokens-client-credentials"></span><span aria-hidden="true" id="bearer-tokens"></span><span aria-hidden="true" id="post-token"></span>



## Bearer tokens (client credentials)

CI jobs, servers and other headless clients that hold an
*Auth Key* and *Auth Secret* exchange them for a short-lived Bearer token
with the OAuth 2.0 `client_credentials` grant, handled directly by the Transloadit API. Use this when there is no browser
to consent in; interactive MCP clients should connect by URL as described above. For the full endpoint
reference, see the [/token API docs](/docs/api/token-post.md).

<span aria-hidden="true" id="using-the-token"></span>



### Using the token

Pass the token as `Authorization: Bearer <access_token>` on API requests. When a request is
authenticated with a valid Bearer token, API2 treats
*Signature Authentication* as satisfied and
skips signature validation. Signature Authentication is enforced only for key/secret requests.
Scope and audience checks still apply. The `mcp` audience is accepted by the MCP server, which
relays it to API2 with a service credential; it is rejected when presented directly to regular
API2 endpoints. You can omit `auth.key` in `params`, but the `params` envelope is still required
for endpoints that expect it.

```bash
curl --fail-with-body -sS --request POST \
  --url 'https://api2.transloadit.com/assemblies' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --form 'params={"template_id":"YOUR_TEMPLATE_ID"}'
```

<span aria-hidden="true" id="mcp-auto-auth-for-ai-chat"></span><span aria-hidden="true" id="mcp-auto-auth-for-aichat"></span>



### MCP auto-auth for `/ai/chat`

If your `/ai/chat` steps call a Transloadit-hosted MCP server, API2 can mint and inject a short-lived
Bearer token automatically (auto-auth). This is opt-in per MCP server entry:

```json
{
  "mcp_servers": [
    {
      "type": "http",
      "url": "https://api2.transloadit.com/mcp",
      "auth": "transloadit"
    }
  ]
}
```

Behavior:

* If `auth: "transloadit"` is set and no `Authorization` header is present, API2 mints a token and
  injects `Authorization: Bearer <token>`.
* If `Authorization` is already provided in `mcp_servers[].headers`, it is left untouched.
* Auto-auth only works over HTTPS for Transloadit-managed apex hosts and subdomains:
  `transloadit.com`, `*.transloadit.com`, `transloadit.dev`, `*.transloadit.dev`, `transloadit.website`, `*.transloadit.website`, `transloadit.work`, `*.transloadit.work`.
* The URL must use port `443` and the exact `/mcp` path or a subpath below `/mcp/`.
* The URL must not contain credentials, a query string, or a fragment.
* The Auth Key must grant at least one of these safe MCP scopes: `assemblies:write`, `assemblies:read`, `templates:read`.
* The minted token is narrowed to the intersection of those safe MCP scopes and the Auth Key’s scopes;
  it never gains a scope that the Auth Key does not grant.

<span aria-hidden="true" id="faqs"></span>



## FAQs

<span aria-hidden="true" id="does-transloadit-include-signatures-with-its-requests"></span>



### Does Transloadit include signatures with its requests?

Transloadit signs [Webhook](/docs/topics/webhooks.md) requests so your server can verify their
authenticity. Webhook signing is separate from the HMAC that authenticates API requests sent to
Transloadit.

<span aria-hidden="true" id="why-cant-i-use-my-auth-secret-as-the-bearer-token"></span><span aria-hidden="true" id="why-can-t-i-use-my-auth-secret-as-the-bearer-token"></span>



### Why can’t I use my Auth Secret as the Bearer token?

Except for the server-to-server token exchange described above, your *Auth Secret* must
never be transmitted to a client or included in API request parameters. `POST /token` sends it as
the HTTP Basic password over HTTPS and must be called only from your back-end. For signed requests,
the secret remains on your back-end and is used as the HMAC key. This prevents a malicious actor
from intercepting a signed request and spoofing requests to your account. Keep your *Auth Secret* safe by using
whichever secret management system you prefer. Examples are: [Vault](https://www.vaultproject.io/),
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/),
[GCP Secret Manager](https://cloud.google.com/security/products/secret-manager), and
[Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/), but there are many
more that could be suitable depending on your back-end platform of choice.

> [!Note] You should ensure that *Auth Secrets* are *never* included as part of the
> front-end of your application, or exposed to users.

<span aria-hidden="true" id="what-order-do-the-keys-in-the-body-need-to-be-in"></span>



### What order do the keys in the body need to be in?

The order you choose for the keys in the body can be arbitrary, however it’s important to note that
whichever order you choose needs to be consistent with your signature generation. The hash generated
depends on the contents of the JSON, and a different ordering will generate a different hash,
meaning your request will be denied.
