API guide

Use your Tributary from your own scripts and apps with an API key.

What you can build

  • A nightly backup of a folder on your laptop or server.
  • A script that files invoices, scans or exports into the right folder as they arrive.
  • A website or bot that pulls photos and documents out of your pool.
  • A camera or phone workflow that uploads as soon as something new appears.
  • A clean-up job that finds old files and moves them to the trash.

Files you add this way appear in the app like any others, and uploads follow your upload routing.

Get a key

  1. Sign in to your Tributary and open Account.
  2. Under API keys, name the key after what will use it.
  3. Choose its scopes: files:read to look, files:write to change things. Give it only what it needs.
  4. Press Create key and copy it. It starts with dxt_, and it's shown once.

Keep the key and your server's address in environment variables. The examples here expect these two:

export TRIBUTARY_URL="https://files.example.com"
export TRIBUTARY_KEY="dxt_..."

Your first request

Every request carries the key in an Authorization: Bearer header. This lists the top level of your files: the folders and files in it, each with an id you can use in other requests.

# The top level. Add ?folder_id=<id> for a folder.
curl "$TRIBUTARY_URL/api/v1/files" \
  -H "Authorization: Bearer $TRIBUTARY_KEY"

Each file comes back like this. Sizes are in bytes, and folder_id is null at the top level.

{
  "id": "6f1c2a70-3b1e-4c58-9a44-0d2f6f1f7c11",
  "name": "report.pdf",
  "size": 482113,
  "content_type": "application/pdf",
  "preview": "pdf",
  "folder_id": null,
  "connection_id": "1b9e4d02-77ac-4a0e-8e1f-5a3c9b2d6e40",
  "external": false,
  "trashed_at": null,
  "created_at": "2026-03-02T09:14:07Z",
  "updated_at": "2026-03-02T09:14:07Z"
}

Upload a file

Send the file as the request body, with its name in the address. Add folder_id to put it in a folder, or connection_id to choose an account yourself. Left out, your upload routing chooses. The answer is the new file.

# The body is the file itself, not a form.
curl -X POST "$TRIBUTARY_URL/api/v1/files?name=report.pdf" \
  -H "Authorization: Bearer $TRIBUTARY_KEY" \
  -H "Content-Type: application/pdf" \
  --data-binary @report.pdf

An upload is one request, so if the connection drops, send the file again. Pausing and resuming are only in the app. Names don't have to be unique, so uploading twice gives you two files.

Download a file

Use the file's id. Downloads support Range requests, so you can fetch part of a large file.

curl "$TRIBUTARY_URL/api/v1/files/$FILE_ID/content" \
  -H "Authorization: Bearer $TRIBUTARY_KEY" \
  -o report.pdf

Search finds files and folders by name, up to 100 of each. Anything in the trash is left out.

curl -G "$TRIBUTARY_URL/api/v1/search" \
  -H "Authorization: Bearer $TRIBUTARY_KEY" \
  --data-urlencode "q=report"

Folders and moving

Moving here means into another folder. Moving a file to a different Google account is only in the app. To move something to the top level, send null as the folder.

# Make a folder. Use a folder's ID as parent_id to nest it.
curl -X POST "$TRIBUTARY_URL/api/v1/folders" \
  -H "Authorization: Bearer $TRIBUTARY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Reports", "parent_id": null}'

# Move a file into it, then rename the file.
curl -X POST "$TRIBUTARY_URL/api/v1/files/$FILE_ID/move" \
  -H "Authorization: Bearer $TRIBUTARY_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"folder_id\": \"$FOLDER_ID\"}"

curl -X PATCH "$TRIBUTARY_URL/api/v1/files/$FILE_ID" \
  -H "Authorization: Bearer $TRIBUTARY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "March report.pdf"}'

Trash and restore

A key can move things to the trash and bring them back, but it can't delete anything for good. Only a signed-in person can do that, so a leaked key can't destroy your files. Trashing a file ends its public Google link if it has one, and restoring it doesn't bring the link back.

# Into the trash, then back out again.
curl -X POST "$TRIBUTARY_URL/api/v1/files/$FILE_ID/trash" \
  -H "Authorization: Bearer $TRIBUTARY_KEY"

curl "$TRIBUTARY_URL/api/v1/trash" \
  -H "Authorization: Bearer $TRIBUTARY_KEY"

curl -X POST "$TRIBUTARY_URL/api/v1/files/$FILE_ID/restore" \
  -H "Authorization: Bearer $TRIBUTARY_KEY"

Nightly backup script

This backs up a folder and skips what's already there, so it's safe to run every night. It needs a key with both scopes, and Python's requests package.

"""Copies a local folder into Tributary. Run it every night with cron.

    python backup.py ~/Documents "Laptop backup"

Files already there with the same name and size are skipped.
"""
import mimetypes
import os
import sys
from pathlib import Path

import requests

base = os.environ["TRIBUTARY_URL"]
headers = {"Authorization": f"Bearer {os.environ['TRIBUTARY_KEY']}"}


def call(method, path, **options):
    options["headers"] = {**headers, **options.get("headers", {})}
    response = requests.request(method, f"{base}/api/v1{path}", **options)
    response.raise_for_status()
    return response.json()


def find_or_make_folder(name):
    for folder in call("GET", "/files")["folders"]:
        if folder["name"] == name:
            return folder["id"]
    return call("POST", "/folders", json={"name": name, "parent_id": None})["id"]


def main():
    source, name = Path(sys.argv[1]), sys.argv[2]
    folder_id = find_or_make_folder(name)
    stored = call("GET", "/files", params={"folder_id": folder_id})["files"]
    sizes = {file["name"]: file["size"] for file in stored}

    sent = skipped = 0
    for path in sorted(source.iterdir()):
        if not path.is_file():
            continue
        if sizes.get(path.name) == path.stat().st_size:
            skipped += 1
            continue
        kind = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
        with path.open("rb") as body:
            call(
                "POST",
                "/files",
                params={"name": path.name, "folder_id": folder_id},
                headers={"Content-Type": kind},
                data=body,
            )
        sent += 1
    print(f"{sent} uploaded, {skipped} already there")


main()

To run it at 2am every day, add this line with crontab -e:

0 2 * * * TRIBUTARY_URL=https://files.example.com TRIBUTARY_KEY=dxt_... python3 /home/you/backup.py /home/you/Documents "Laptop backup"

What a key can do

A key can make these requests, if it has the scope shown.

WhatRequestScope
List a folderGET /api/v1/filesfiles:read
Get a file's detailsGET /api/v1/files/{id}files:read
Download a fileGET /api/v1/files/{id}/contentfiles:read
Get a thumbnailGET /api/v1/files/{id}/thumbnailfiles:read
List the trashGET /api/v1/trashfiles:read
SearchGET /api/v1/search?q=...files:read
Upload a filePOST /api/v1/files?name=...files:write
Rename a filePATCH /api/v1/files/{id}files:write
Move a filePOST /api/v1/files/{id}/movefiles:write
Trash a filePOST /api/v1/files/{id}/trashfiles:write
Restore a filePOST /api/v1/files/{id}/restorefiles:write
Make a folderPOST /api/v1/foldersfiles:write
Rename a folderPATCH /api/v1/folders/{id}files:write
Move a folderPOST /api/v1/folders/{id}/movefiles:write
Trash a folderPOST /api/v1/folders/{id}/trashfiles:write
Restore a folderPOST /api/v1/folders/{id}/restorefiles:write

Everything else is closed to keys, and that includes anything new until it's added to this list.

Only a signed-in person can

  • Delete anything permanently, or empty the trash.
  • Make or change public Google links.
  • Move files between Google accounts, or link and remove accounts.
  • Download a zip, or use resumable uploads.
  • Change upload routing, keys, passwords or server settings.

Errors

Errors have a status, a code your program can check, and a message you can show to a person.

{
  "error": {
    "code": "storage_full",
    "message": "No linked account has room for this file.",
    "request_id": "U2YB7KRXTUXXOKHA"
  }
}
StatusCodeWhat it means
401unauthorizedThe key is missing, mistyped or revoked.
403forbiddenThe key doesn't have that scope, or keys can't do that at all.
403password_change_requiredThe admin set this person's password. They need to sign in and change it.
404not_foundThat file or folder doesn't exist, or isn't yours.
400invalid_nameThe name is empty or isn't allowed.
400invalid_uploadAn upload needs ?name= in its address.
409no_storageNo Google account is linked yet.
507storage_fullNo linked account has room. A file has to fit in one account.
413file_too_largeThe file is over the limit the admin set.
502provider_unavailableGoogle couldn't be reached. Try again.

If you ask the admin for help, give them the request_id. It finds the full details in the server's logs.

Keeping a key safe

  • Use a key from a server or script, never from a web page. Anyone can read a page's code, and Tributary doesn't allow calls from other sites' pages.
  • Make one key per job, so you can revoke one without touching the rest.
  • Give each key the fewest scopes it needs. A key that only reads can't change anything.
  • Always use your server's https:// address, so the key isn't sent in the clear.
  • If a key leaks, revoke it under Account. It stops working straight away.

Don't have a server yet? Set one up, or try the demo first.