Flag of Ukraine
Our /sftp/store Robot

Export files to SFTP servers

🤖/sftp/store exports encoding results to your own SFTP server.

Usage example

Export uploaded files to my_target_folder on an SFTP server:

  "steps": {
    "exported": {
      "robot": "/sftp/store",
      "use": ":original",
      "credentials": "YOUR_SFTP_CREDENTIALS",
      "path": "my_target_folder/${unique_prefix}/${file.url_name}"


  • use

    String / Array of Strings / Object required

    Specifies which Step(s) to use as input.

    • You can pick any names for Steps except ":original" (reserved for user uploads handled by Transloadit)

    • You can provide several Steps as input with arrays:

      "use": [

    💡 That’s likely all you need to know about use, but you can view advanced use cases:

    › Advanced use cases
    • Step bundling. Some Robots can gather several Step results for a single invocation. For example, 🤖/file/compress would normally create one archive for each file passed to it. If you'd set bundle_steps to true, however, it will create one archive containing all the result files from all Steps you give it. To enable bundling, provide an object like the one below to the use parameter:

      "use": {
        "steps": [
        "bundle_steps": true

      This is also a crucial parameter for 🤖/video/adaptive, otherwise you'll generate 1 playlist for each viewing quality.
      Keep in mind that all input Steps must be present in your Template. If one of them is missing (for instance it is rejected by a filter), no result is generated because the Robot waits indefinitely for all input Steps to be finished.

      Here’s a demo that showcases Step bundling.

    • Group by original. Sticking with 🤖/file/compress example, you can set group_by_original to true, in order to create a separate archive for each of your uploaded or imported files, instead of creating one archive containing all originals (or one per resulting file). This is important for for 🤖/media/playlist where you'd typically set:

      "use": {
        "steps": [
        "bundle_steps": true,
        "group_by_original": true
    • Fields. You can be more discriminatory by only using files that match a field name by setting the fields property. When this array is specified, the corresponding Step will only be executed for files submitted through one of the given field names, which correspond with the strings in the name attribute of the HTML file input field tag for instance. When using a back-end SDK, it corresponds with myFieldName1 in e.g.: $transloadit->addFile('myFieldName1', './chameleon.jpg').

      This parameter is set to true by default, meaning all fields are accepted.


      "use": {
        "steps": [ ":original" ],
        "fields": [ "myFieldName1" ]
    • Use as. Sometimes Robots take several inputs. For instance, 🤖/video/merge can create a slideshow from audio and images. You can map different Steps to the appropriate inputs.


      "use": {
        "steps": [
          { "name": "audio_encoded", "as": "audio" },
          { "name": "images_resized", "as": "image" }

      Sometimes the ordering is important, for instance, with our concat Robots. In these cases, you can add an index that starts at 1. You can also optionally filter by the multipart field name. Like in this example, where all files are coming from the same source (end-user uploads), but with different <input> names:


      "use": {
        "steps": [
          { "name": ":original", "fields": "myFirstVideo", "as": "video_1" },
          { "name": ":original", "fields": "mySecondVideo", "as": "video_2" },
          { "name": ":original", "fields": "myThirdVideo", "as": "video_3" }

      For times when it is not apparent where we should put the file, you can use Assembly Variables to be specific. For instance, you may want to pass a text file to 🤖/image/resize to burn the text in an image, but you are burning multiple texts, so where do we put the text file? We specify it via ${use.text_1}, to indicate the first text file that was passed.


      "watermarked": {
        "robot": "/image/resize",
        "use"  : {
          "steps": [
            { "name": "resized", "as": "base" },
            { "name": "transcribed", "as": "text" },
        "text": [
            "text"  : "Hi there",
            "valign": "top",
            "align" : "left",
            "text"    : "From the 'transcribed' Step: ${use.text_1}",
            "valign"  : "bottom",
            "align"   : "right",
            "x_offset": 16,
            "y_offset": -10,
  • credentials


    Please create your associated Template Credentials in your Transloadit account and use the name of your Template Credentials as this parameter's value. They will contain the values for your SFTP host, user and optional custom public key.

    While we recommend to use Template Credentials at all times, some use cases demand dynamic credentials for which using Template Credentials is too unwieldy because of their static nature. If you have this requirement, feel free to use the following parameters instead: "host", "port", "user", "public_key" (optional).

  • path

    String ⋅ default: "${unique_prefix}/${file.url_name}"

    The path at which the file is to be stored. This may include any available Assembly variables.

  • url_template

    String ⋅ default: "http://host/path"

    The URL of the file in the result JSON. This may include any of the following supported Assembly variables.

  • ssl_url_template

    String ⋅ default: "https://{HOST}/{PATH}"

    The SSL URL of the file in the result JSON. The following Assembly variables are supported.

  • file_chmod

    String ⋅ default: auto[?]

    This optional parameter controls how an uploaded file's permission bits are set. You can use any string format that the chmod command would accept, such as "755". If you don't specify this option, the file's permission bits aren't changed at all, meaning it's up to your server's configuration (e.g. umask).

Note: The URLs in the result JSON already point to the file on your target storage platform, so you can just save that URL in your database.

You can influence the URL using the Robot's url_template parameter.

Things to keep in mind

  • Your server might be unresponsive from time to time. Since (S)FTP servers are harder to scale, we see this happen a lot. As a result, if this Robot fails to store one of your files, it will retry to store the file 5 times over 15 minutes.
  • This also means that when you provided the wrong credentials (when testing for example) Assemblies can take up to 15 minutes to complete.
  • If an Assembly has an export Step and takes long to execute, it is almost always due to the export Step having wrong credentials or the server being unreachable.

This robot uses SSH keys, which offer more security at the trade-off of a moderate degree of complexity in setting up. If you prefer a less secure, but easier-to-setup solution, then please have a look at 🤖/ftp/store and 🤖/ftp/import.

Installation on your server

The method explained here is slightly more elaborate than handing out an SSH account with an authorized public key, but is more secure as the user will not be able to traverse outside their home directory. This way, even if the user account is compromised at some point, the accessible data would be recently uploaded files.

The following explains how to create a dedicated user and group for Transloadit, and restrict access as shown in this article.

First, let's create a dedicated group and user:

# Change these variables to suit your needs.
# For additional security, use a randomly picked username.

groupadd sftponly
useradd -g sftponly -m ${TL_USER}

# This line sets a random 20-character password for the user.
# We work with keys, but this is required because some operating systems will
# consider the account locked without one.
echo "${TL_USER}:$(cat /dev/urandom | tr -dc _A-Z-a-z-0-9 | head -c20)" |chpasswd

mkdir -p /home/${TL_USER}/.ssh /home/${TL_USER}/${TL_PUBLIC_PATH}

echo "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA57GdwNLqsWz03X8MBEe4KoMSY2HOURjnUUe9zeTivASI+BLEe3cZcuJjsEBaRpISvCH04hosWUI0H4BQeB1dZZUUW1s4ttnVohCD9CfNiXJ7pwJAvgWb01dTW4YUWFKUTpTeUwQzgcNVLDtSVaQOYh4lAKvCZEcz17X9iZ7AeSEuQKe+QsrcwQoBdSpQ6FnzKwSZsggK81dPiGIW9Cw2z/EZWJpl9QBTYhw25NbNRtZj3fXVbrejnQQ985eZ6TlrvQFpUVwyk0QNHDsN+7zVISM3eXNpxof+vJyQNDLN9tb8vNPf/HXuw7MDJWMphrQevF5V26aMzszl3ZeO1779Mw== sftp@transloadit.com" >> /home/${TL_USER}/.ssh/authorized_keys

chown -R ${TL_USER}.sftponly /home/${TL_USER}
chown root.root /home/${TL_USER}

chmod -R 600 /home/${TL_USER}/.ssh /home/${TL_USER}/${TL_PUBLIC_PATH}
chmod -R u+X /home/${TL_USER}/.ssh /home/${TL_USER}/${TL_PUBLIC_PATH}

Make sure to change ${TL_USER} and ${TL_PUBLIC_PATH} to suit your needs. Remember to use a random username for additional security.

Please also give the subfolder where you intend to store your files sufficient permissions for your user to be able create sub-directories via mkdir. A combination of chown and chmod should achieve this.

Next, we will set up SSH to cage SFTP users in their home directory, and deny them regular shell access.

Enter ${EDITOR} /etc/ssh/sshd_config, and then find and comment out this line:

# Subsystem sftp /usr/lib/openssh/sftp-server

At the bottom of the same file, add the following:

Subsystem sftp internal-sftp

Match group sftponly
    ChrootDirectory /home/%u
    X11Forwarding no
    AllowTcpForwarding no
    ForceCommand internal-sftp

Finally, enter /etc/init.d/ssh restart to reflect the changes made.


Related blog posts

20% off any plan for the Uppy community
Use the UPPY20 code when upgrading.
Sign up
20% off any plan for the tus community
Use the TUS20 code when upgrading.
Sign up
Product Hunt
20% off any plan for Product Hunters
Use the PRH20 code when upgrading.
Sign up