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
- Sign in to your Tributary and open Account.
- Under API keys, name the key after what will use it.
- Choose its scopes:
files:readto look,files:writeto change things. Give it only what it needs. - 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.pdfAn 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.pdfSearch
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.
| What | Request | Scope |
|---|---|---|
| List a folder | GET /api/v1/files | files:read |
| Get a file's details | GET /api/v1/files/{id} | files:read |
| Download a file | GET /api/v1/files/{id}/content | files:read |
| Get a thumbnail | GET /api/v1/files/{id}/thumbnail | files:read |
| List the trash | GET /api/v1/trash | files:read |
| Search | GET /api/v1/search?q=... | files:read |
| Upload a file | POST /api/v1/files?name=... | files:write |
| Rename a file | PATCH /api/v1/files/{id} | files:write |
| Move a file | POST /api/v1/files/{id}/move | files:write |
| Trash a file | POST /api/v1/files/{id}/trash | files:write |
| Restore a file | POST /api/v1/files/{id}/restore | files:write |
| Make a folder | POST /api/v1/folders | files:write |
| Rename a folder | PATCH /api/v1/folders/{id} | files:write |
| Move a folder | POST /api/v1/folders/{id}/move | files:write |
| Trash a folder | POST /api/v1/folders/{id}/trash | files:write |
| Restore a folder | POST /api/v1/folders/{id}/restore | files: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"
}
}| Status | Code | What it means |
|---|---|---|
| 401 | unauthorized | The key is missing, mistyped or revoked. |
| 403 | forbidden | The key doesn't have that scope, or keys can't do that at all. |
| 403 | password_change_required | The admin set this person's password. They need to sign in and change it. |
| 404 | not_found | That file or folder doesn't exist, or isn't yours. |
| 400 | invalid_name | The name is empty or isn't allowed. |
| 400 | invalid_upload | An upload needs ?name= in its address. |
| 409 | no_storage | No Google account is linked yet. |
| 507 | storage_full | No linked account has room. A file has to fit in one account. |
| 413 | file_too_large | The file is over the limit the admin set. |
| 502 | provider_unavailable | Google 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.