Skip to content

API Reference

All endpoints are under /api/v1. Authentication is required unless noted otherwise.

See Using the API for authentication details and common patterns.


Authentication

Sign Up

POST /api/v1/auth/signupNo auth required

Create a new user account.

Tilde is currently in a private preview. Most signups land in a pending state and cannot sign in until an administrator approves them (typically within 24-48 hours). The user gets a confirmation email immediately and a second email with a link to sign in once approved.

Signups whose email matches an open organization invitation skip the approval step and are activated (status = "active") right away, so they can accept the invitation.

Request body:

Field Type Required Description
username string Yes Unique username
email string Yes Unique email address
full_name string Yes Display name
password string Yes Password
company string No Company name
import requests

resp = requests.post("https://tilde.run/api/v1/auth/signup", json={
    "username": "alice",
    "email": "alice@example.com",
    "full_name": "Alice Smith",
    "password": "s3cure-passw0rd",
})
curl -X POST https://tilde.run/api/v1/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "email": "alice@example.com",
    "full_name": "Alice Smith",
    "password": "s3cure-passw0rd"
  }'

Response: 201 Created

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "username": "alice",
  "email": "alice@example.com",
  "full_name": "Alice Smith",
  "source": "password",
  "status": "pending",
  "created_at": "2025-01-15T10:30:00Z"
}

Inspect status to decide what to do next: "pending" means to wait for the approval email; "active" means the user can immediately call /auth/login.

Errors: 400 (invalid input), 409 (EMAIL_TAKEN or USERNAME_TAKEN)


Log In

POST /api/v1/auth/loginNo auth required (IP rate-limited and per-account locked)

Authenticate and receive session cookies.

Two protections layer on top of each other:

  • An IP-based rate limit caps any single client to roughly 10 attempts per minute.
  • A per-account counter locks the account for 15 minutes after 10 failed attempts within a 15-minute window. A successful login resets the counter. While locked, requests for that account return 429 ACCOUNT_LOCKED with a Retry-After header — wait for the cooldown rather than retrying in a loop.

Request body:

Field Type Required Description
login string Yes Email or username
password string Yes Password
import requests

session = requests.Session()
resp = session.post("https://tilde.run/api/v1/auth/login", json={
    "login": "alice@example.com",
    "password": "s3cure-passw0rd",
})
curl -c cookies.txt \
  -X POST https://tilde.run/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"login": "alice@example.com", "password": "s3cure-passw0rd"}'

Response: 200 OK

Sets tilde_session (HttpOnly JWT) and tilde_csrf cookies.

{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "alice",
    "email": "alice@example.com",
    "full_name": "Alice Smith",
    "source": "password",
    "status": "active",
    "created_at": "2025-01-15T10:30:00Z"
  }
}

Errors: 401 (INVALID_CREDENTIALS), 403 (PENDING_APPROVAL — the account was created but has not been approved yet; the user will receive an email when they can sign in), 429 (ACCOUNT_LOCKED — too many recent failed attempts; honour Retry-After)


Log Out

POST /api/v1/auth/logout

Clear session cookies.

Response: 204 No Content


Get Current User

GET /api/v1/auth/me

Returns the authenticated user and their organization memberships.

Response: 200 OK

{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "alice",
    "email": "alice@example.com",
    "full_name": "Alice Smith",
    "source": "password",
    "status": "active",
    "created_at": "2025-01-15T10:30:00Z"
  },
  "organizations": [
    {
      "id": "...",
      "name": "my-team",
      "display_name": "My Team"
    }
  ]
}

Check Username Availability

GET /api/v1/auth/check-usernameNo auth required

Parameter Type Required Description
username query Yes Username to check

Response: 200 OK

{ "available": true }

Create API Key

POST /api/v1/auth/keys

Create a new API key. The token is returned only once in this response — save it securely.

Request body:

Field Type Required Description
name string Yes Human-readable key name
description string No Key description

Response: 201 Created

{
  "token": "tuk-..._...",
  "id": "opaque-key-id",
  "name": "ci-pipeline",
  "description": "CI/CD pipeline key"
}

The token field is your API token — use it as Authorization: Bearer <token>. The id field is used for management operations (listing, revocation) and is not the token itself.


List API Keys

GET /api/v1/auth/keys

Returns all API keys for the authenticated user. Tokens are never included — only a hint (last 4 characters) is shown for identification.

Response: 200 OK

{
  "results": [
    {
      "id": "opaque-key-id",
      "user_id": "...",
      "name": "ci-pipeline",
      "description": "CI/CD pipeline key",
      "token_hint": "Ab3x",
      "created_at": "2025-01-15T10:30:00Z",
      "last_used_at": "2025-01-16T08:00:00Z",
      "revoked_at": null
    }
  ]
}

Revoke API Key

DELETE /api/v1/auth/keys/{key_id}

Soft-revokes an API key. Revoked keys can no longer authenticate.

Response: 204 No Content


Add Email

POST /api/v1/auth/emails

Add a secondary email to the authenticated user.

Request body:

Field Type Required Description
email string Yes Email address to add

Response: 201 Created

{
  "id": "...",
  "user_id": "...",
  "email": "alice-work@example.com",
  "is_primary": false,
  "verified": false,
  "created_at": "2025-01-15T10:30:00Z"
}

List Emails

GET /api/v1/auth/emails

Response: 200 OK

{
  "results": [
    { "email": "alice@example.com", "is_primary": true, "verified": true },
    { "email": "alice-work@example.com", "is_primary": false, "verified": false }
  ]
}

Organizations

Create Organization

POST /api/v1/organizations

The creator is automatically added as an owner. Built-in RBAC policies (Owner, Member) are seeded.

Request body:

Field Type Required Description
name string Yes Unique slug (^[a-z0-9][a-z0-9-]{1,62}$). Reserved: api, auth, admin, system.
display_name string Yes Human-readable name

Response: 201 Created

{
  "id": "...",
  "name": "my-team",
  "display_name": "My Team",
  "created_at": "2025-01-15T10:30:00Z"
}

Errors: 400 (invalid name format or reserved name), 409 (name taken)


List Organizations

GET /api/v1/organizations

Returns organizations the authenticated user is a member of.

Response: 200 OK

{
  "results": [
    { "id": "...", "name": "my-team", "display_name": "My Team", "created_at": "..." }
  ]
}

Get Organization

GET /api/v1/organizations/{organization}

Response: 200 OK — same shape as a single organization object.


List Members

GET /api/v1/organizations/{organization}/members

Response: 200 OK

{
  "results": [
    {
      "organization_id": "...",
      "user_id": "...",
      "username": "alice",
      "full_name": "Alice Smith",
      "email": "alice@example.com",
      "joined_at": "2025-01-15T10:30:00Z"
    }
  ]
}

What a member can do is determined entirely by policies and groups, not by a role on the membership record itself.


Remove Member

DELETE /api/v1/organizations/{organization}/members/{user_id}

Remove a user from the organization. The user's group memberships and direct policy attachments in this organization are removed at the same time.

Response: 204 No Content

Errors: 404 (member not found)


Invite Member

POST /api/v1/organizations/{organization}/invitations

Members are added by inviting them — the invited user gets an email and joins the organization once they accept. You can target the invitation by email or by an existing username, and optionally pre-attach groups and policies that will be applied on accept.

Request body (must include exactly one of email or username):

Field Type Required Description
email string Conditional Email address of the invitee
username string Conditional Username of an existing user (the invitation is keyed to that user's email)
group_ids string[] (UUID) No Groups to add the invited user to on accept
policy_ids string[] (UUID) No Policies to attach to the invited user on accept

Response: 201 Created

{
  "id": "invitation-uuid",
  "organization_id": "...",
  "email": "alice@example.com",
  "invited_by": "...",
  "status": "pending",
  "created_at": "2025-01-15T10:30:00Z",
  "expires_at": "2025-01-22T10:30:00Z"
}

Errors: 400 (invalid input), 404 (username was provided but no such user), 409 (already a member or duplicate pending invitation)


List Pending Invitations (Organization)

GET /api/v1/organizations/{organization}/invitations

Response: 200 OK — array of Invitation objects (same shape as the create response).


Revoke Invitation

DELETE /api/v1/organizations/{organization}/invitations/{invitation_id}

Cancels a pending invitation. Once revoked, the invitee can no longer accept it.

Response: 204 No Content

Errors: 404 (invitation not found), 409 (invitation is no longer in pending status)


List My Invitations

GET /api/v1/invitations

Returns pending invitations for the authenticated user across all organizations. Each invitation includes the inviting organization's name and display name plus the inviter's name.


Accept Invitation

POST /api/v1/invitations/{invitation_id}/accept

Joins the inviting organization. Any group_ids / policy_ids set when the invitation was created are now attached to your user.

Response: 200 OK with { "status": "accepted" }.

Errors: 404 (not your invitation), 410 (invitation expired or revoked)


Decline Invitation

POST /api/v1/invitations/{invitation_id}/decline

Response: 200 OK with { "status": "declined" }.


Repositories

Create Repository

POST /api/v1/organizations/{organization}/repositories

Request body:

Field Type Required Description
name string Yes Repository name
description string No Description
retention_days integer No Days of commit history to retain (default: 14).
import tilde

org = tilde.organizations.get("my-team")
repo = org.repositories.create("my-data")
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST https://tilde.run/api/v1/organizations/my-team/repositories \
  -H "Content-Type: application/json" \
  -d '{"name": "my-data"}'

Response: 201 Created

{
  "id": "...",
  "organization_id": "...",
  "name": "my-data",
  "description": "",
  "retention_days": 14,
  "created_by_type": "user",
  "created_by": "...",
  "created_at": "2025-01-15T10:30:00Z"
}

List Repositories (in Organization)

GET /api/v1/organizations/{organization}/repositories

Parameter Type Required Description
after query No Pagination cursor
amount query No Page size (default: 100, max: 1000)

List All Repositories (Cross-Org)

GET /api/v1/repositories

Returns all repositories the user has access to across all organizations. Each result includes organization_slug and organization_display_name.


Get Repository

GET /api/v1/organizations/{organization}/repositories/{repository}


Update Repository

PUT /api/v1/organizations/{organization}/repositories/{repository}

Request body:

Field Type Required Description
description string No New description
retention_days integer No Days of commit history to retain.

Delete Repository

DELETE /api/v1/organizations/{organization}/repositories/{repository}

Response: 204 No Content (soft delete)


Sessions

Create Session

POST /api/v1/organizations/{organization}/repositories/{repository}/sessions

Create a new session for staging changes. A session is an isolated workspace where you can upload, modify, and delete objects before committing.

import tilde

repo = tilde.repository("my-team/my-data")
session = repo.session()
print(session.session_id)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sessions

Response: 201 Created

{
  "session_id": "session-uuid-here"
}

Commit Session

POST /api/v1/organizations/{organization}/repositories/{repository}/sessions/{session_id}

Commit all staged changes in the session. This creates a new commit and closes the session.

Request body:

Field Type Required Description
message string Yes Commit message
metadata object No Arbitrary key-value metadata
commit_id = session.commit("Add new dataset")
print(f"Committed: {commit_id}")
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sessions/SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{"message": "Add new dataset"}'

Response: 200 OK

{ "commit_id": "abc123def456..." }

Response (agent with approval required): 202 Accepted

When the repository has agents_require_approval enabled and the caller is an agent, the session is not committed. Instead, the response contains URLs for the approval page:

{
  "approval_required": true,
  "session_id": "SESSION_ID",
  "message": "this commit requires human approval before it can be applied",
  "api_url": "/api/v1/organizations/my-team/repositories/my-data/sessions/SESSION_ID/approve",
  "web_url": "https://tilde.run/console/organizations/my-team/repositories/my-data/sessions/SESSION_ID/approve"
}

A human reviewer must visit the web_url to approve or roll back the changes. Agents can poll HEAD /api/v1/organizations/{organization}/repositories/{repository}/sessions/{session_id}/approve — it returns 200 while the session is pending, and 404 once it has been committed or rolled back.

Errors:

Status Code Meaning
400 BAD_REQUEST Missing message or invalid session
404 NOT_FOUND Session does not exist
409 CONFLICT Session conflicts with another concurrent commit

Rollback Session

DELETE /api/v1/organizations/{organization}/repositories/{repository}/sessions/{session_id}

Discard all staged changes and close the session without creating a commit.

session.rollback()
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X DELETE "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sessions/SESSION_ID"

Response: 204 No Content

Errors: 404 (session not found)


Review Session Changes

GET /api/v1/organizations/{organization}/repositories/{repository}/sessions/{session_id}/approve

List uncommitted changes in a session for review before approving. This is the same data as the changes endpoint, but addressed by session path parameter.

Parameter Type Required Description
prefix query No Path prefix filter
after query No Pagination cursor
amount query No Page size (default: 100, max: 1000)
import requests

resp = requests.get(
    "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sessions/SESSION_ID/approve",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"},
)
changes = resp.json()["results"]
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sessions/SESSION_ID/approve"

Response: 200 OK

{
  "results": [
    {
      "path": "data/file.csv",
      "entry": { "size": 1024, "e_tag": "\"abc123\"" },
      "diff_type": "added"
    }
  ],
  "pagination": { "has_more": false, "next_offset": "", "max_per_page": 1000 }
}

Errors: 404 (session not found)


Approve Session

POST /api/v1/organizations/{organization}/repositories/{repository}/sessions/{session_id}/approve

Approve and commit a session's staged changes. The resulting commit metadata includes approved_by, approved_by_type, and approved_by_id fields identifying the approver.

Request body:

Field Type Required Description
message string Yes Commit message
metadata object No Arbitrary key-value metadata
import requests

resp = requests.post(
    "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sessions/SESSION_ID/approve",
    headers={"Authorization": "Bearer YOUR_API_TOKEN"},
    json={"message": "Approved: Add new dataset"},
)
commit_id = resp.json()["commit_id"]
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sessions/SESSION_ID/approve" \
  -H "Content-Type: application/json" \
  -d '{"message": "Approved: Add new dataset"}'

Response: 200 OK

{ "commit_id": "abc123def456..." }

Errors:

Status Code Meaning
400 BAD_REQUEST No changes to commit or invalid input
404 NOT_FOUND Session does not exist or already committed
409 CONFLICT Session conflicts with another concurrent commit

Commits

Get Commit

GET /api/v1/organizations/{organization}/repositories/{repository}/commits/{commit_id}

Response: 200 OK

{
  "id": "abc123def456...",
  "committer": "alice",
  "committer_type": "user",
  "committer_id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Add dataset",
  "creation_date": "2025-01-15T10:30:00Z",
  "parents": ["parent-commit-id"],
  "metadata": {},
  "is_stale": false
}

Get Commit Log

GET /api/v1/organizations/{organization}/repositories/{repository}/log

Parameter Type Required Description
ref query No Commit ID to start from (defaults to latest commit)
after query No Pagination cursor
amount query No Page size

Response: 200 OK

{
  "results": [
    {
      "id": "abc123...",
      "committer": "alice",
      "message": "Add dataset",
      "creation_date": "2025-01-15T10:30:00Z",
      "parents": ["..."],
    }
  ],
  "pagination": { "has_more": false, "next_offset": "", "max_per_page": 100 }
}

Diff

GET /api/v1/organizations/{organization}/repositories/{repository}/diff

Compare two commits and return the differences.

Parameter Type Required Description
left query Yes Left ref (commit ID)
right query Yes Right ref (commit ID)
prefix query No Path prefix filter
after query No Pagination cursor
amount query No Page size (default: 1000)
delimiter query No Hierarchy delimiter (e.g., "/")

Response: 200 OK

{
  "results": [
    {
      "path": "data/file.csv",
      "type": "object",
      "entry": {
        "size": 1024,
        "e_tag": "\"abc123\"",
        "content_type": "text/csv",
        "last_modified": "2025-01-15T10:30:00Z"
      },
      "status": "added"
    }
  ],
  "pagination": { "has_more": false, "next_offset": "", "max_per_page": 1000 }
}

Each result includes a status field: "added", "modified", or "removed".


Revert Commit

POST /api/v1/organizations/{organization}/repositories/{repository}/commits/{commit_id}/revert

Creates a new commit on the repository that undoes the changes introduced by the specified commit. The original commit is preserved in history.

Limitations: Merge commits (commits with more than one parent) and root commits (the initial commit with no parents) cannot be reverted.

Conflict behavior: The revert succeeds only if every path affected by the target commit is unchanged in the repository's latest commit since that commit. If any affected path has been modified by a subsequent commit, the endpoint returns 409 Conflict with details about the conflicting paths.

Request body:

Field Type Required Description
message string No Commit message. Defaults to Revert "<original message>"
metadata object No Key-value metadata to attach to the revert commit

Response: 201 Created

{
  "commit_id": "abc123def456..."
}

Errors:

Status Code Description
400 BAD_REQUEST Root commit, merge commit, or nothing to revert
404 NOT_FOUND Commit not found
409 CONFLICT Paths changed since the target commit

Objects

Get Object

GET /api/v1/organizations/{organization}/repositories/{repository}/object

Download an object. The server always returns a 307 Temporary Redirect to a short-lived download URL. Follow the redirect to download the object directly.

Parameter Type Required Description
session_id query No Session ID to read from (defaults to latest committed state)
path query Yes Object path
ref query No Commit ID to read from

Response: 307 Temporary Redirect

The Location header contains a short-lived download URL. The response includes Cache-Control: no-store.

import tilde

repo = tilde.repository("my-team/my-data")

# The SDK follows the redirect automatically
with repo.objects.get("data/file.csv") as reader:
    data = reader.read()

# Stream in chunks for large files
with repo.objects.get("data/file.csv") as reader:
    for chunk in reader.iter_bytes(chunk_size=8192):
        process(chunk)
# Follow the redirect automatically with -L
curl -L -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/object?path=data/file.csv" \
  -o file.csv

# Or capture the download URL without downloading
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/object?path=data/file.csv" \
  -s -o /dev/null -w '%{redirect_url}'

Errors: 403 (not authorized), 404 (object not found), 410 (commit expired)


Head Object

HEAD /api/v1/organizations/{organization}/repositories/{repository}/object

Check object existence and retrieve metadata without streaming the body. Returns the same headers as GET (ETag, Content-Type, Content-Length).

Parameter Type Required Description
session_id query No Session ID to read from (defaults to latest committed state)
path query Yes Object path

Response: 200 OK (headers only, no body)

Errors: 403 (not authorized), 404 (object not found)


Put Object

PUT /api/v1/organizations/{organization}/repositories/{repository}/object

Start an upload to a session. The server returns an opaque upload URL and an upload token. Upload the object body to the upload URL, then call Finalize Object to register it in the catalog.

Parameter Type Required Description
session_id query Yes Target session
path query Yes Object path

Errors: 400 (missing session_id or path), 403 (not authorized), 404 (session not found)

Response: 200 OK

{
  "upload_url": "https://...",
  "upload_token": "..."
}
Field Description
upload_url Opaque URL to PUT the object body to (expires in 15 minutes)
upload_token Opaque token to pass to the Finalize Object endpoint

Note

The Python SDK handles this entire flow automatically. session.objects.put() calls Put Object, uploads, then calls Finalize Object.


Delete Object

DELETE /api/v1/organizations/{organization}/repositories/{repository}/object

Stages a tombstone (delete marker) for the object in a session.

Parameter Type Required Description
session_id query Yes Target session
path query Yes Object path

Errors: 400 (missing session_id or path), 404 (session not found)

Response: 204 No Content


Copy Object

POST /api/v1/organizations/{organization}/repositories/{repository}/object/copy

Copy a catalog entry from one path to another within the same session. The underlying data is not transferred — the destination entry references the same physical object. Use together with DELETE /object to implement rename.

Parameter Type Required Description
session_id query Yes Session for the copy
source_path query Yes Path of the source object
destination_path query Yes Path for the destination object

Response: 201 Created

{ "source_path": "data/file.csv", "destination_path": "data/file.copy.csv" }

Errors: 400 (missing parameters), 403 (not authorized), 404 (session or source object not found)


Finalize Object

POST /api/v1/organizations/{organization}/repositories/{repository}/object/finalize

After uploading to the upload URL received from Put Object, call this endpoint to register the object in the catalog.

Note

Python SDK users do not call this endpoint directly. session.objects.put() handles the full flow automatically.

Parameter Type Required Description
session_id query Yes Target session
path query Yes Object path

Request body:

Field Type Required Description
upload_token string Yes Upload token from the Put Object response
checksum string No ETag from the upload response (without quotes)
content_type string No Content type of the uploaded object
size_bytes integer No Object size in bytes

Response: 201 Created

{ "path": "data/file.csv" }

Errors:

Status Code Meaning
400 BAD_REQUEST Missing or malformed parameters
403 FORBIDDEN Insufficient permissions
404 NOT_FOUND Session not found or already committed

Initiate Multipart Upload

POST /api/v1/organizations/{organization}/repositories/{repository}/object/multipart

Start a multipart upload for large files. Returns an upload ID and upload token that must be passed to all subsequent part, complete, and abort requests.

Use multipart uploads when a file is too large for a single PUT — the client uploads parts directly using upload URLs, then completes the upload to register the catalog entry.

Parameter Type Required Description
session_id query Yes Target session
path query Yes Object path

Response: 200 OK

{
  "upload_id": "...",
  "upload_token": "..."
}
Field Description
upload_id Multipart upload ID — pass to all subsequent multipart calls
upload_token Opaque token to pass to the Complete Multipart Upload endpoint

Errors: 400 (missing parameters), 403 (not authorized), 404 (session not found)


Get Part Upload URL

GET /api/v1/organizations/{organization}/repositories/{repository}/object/multipart/part

Get an upload URL for a single part. PUT the part data to the returned URL and capture the ETag response header.

Parameter Type Required Description
session_id query Yes Session ID
path query Yes Object path
upload_id query Yes Upload ID from the initiate response
part_number query Yes Part number (1-based, max 10,000)

Response: 200 OK

{ "upload_url": "https://..." }

Errors: 400 (missing or invalid parameters), 403 (not authorized)


Complete Multipart Upload

POST /api/v1/organizations/{organization}/repositories/{repository}/object/multipart/complete

Complete the multipart upload and register the catalog entry in the session. Call this after all parts have been uploaded successfully.

Parameter Type Required Description
session_id query Yes Session ID
path query Yes Object path

Request body:

Field Type Required Description
upload_id string Yes Upload ID from the initiate response
upload_token string Yes Upload token from the initiate response
parts array Yes List of completed parts, each with part_number (int) and etag (string)
content_type string No Content type (defaults to application/octet-stream)
checksum string No Final ETag / checksum of the completed upload
size_bytes integer No Total size in bytes

Response: 201 Created

{ "path": "data/large-file.parquet" }

Errors:

Status Code Meaning
400 BAD_REQUEST Missing or malformed parameters, empty parts list
403 FORBIDDEN Insufficient permissions
404 NOT_FOUND Session not found or already committed

Abort Multipart Upload

DELETE /api/v1/organizations/{organization}/repositories/{repository}/object/multipart

Abort a multipart upload and clean up any uploaded parts. Call this if an upload fails and you want to discard partial data.

Parameter Type Required Description
session_id query Yes Session ID
path query Yes Object path
upload_id query Yes Upload ID from the initiate response

Response: 204 No Content

Errors: 400 (missing parameters), 403 (not authorized)


Multipart Upload Example

import tilde

repo = tilde.repository("my-team/my-data")
with repo.session() as session:
    # The SDK handles multipart uploads automatically for large files.
    # Just call put() — if the file exceeds the part size threshold,
    # the SDK uses multipart upload behind the scenes.
    result = session.objects.put("data/large-file.parquet", open("large-file.parquet", "rb"))
    print(f"Uploaded: {result.path}, ETag: {result.etag}")
    session.commit("Add large dataset")
# 1. Initiate multipart upload
INIT=$(curl -s -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/object/multipart?session_id=SESSION_ID&path=data/large-file.parquet")

UPLOAD_ID=$(echo "$INIT" | jq -r .upload_id)
UPLOAD_TOKEN=$(echo "$INIT" | jq -r .upload_token)

# 2. Get upload URL for each part and upload
PART_URL=$(curl -s -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/object/multipart/part?session_id=SESSION_ID&path=data/large-file.parquet&upload_id=$UPLOAD_ID&part_number=1" \
  | jq -r .upload_url)

# Upload the part directly and capture the ETag
ETAG=$(curl -s -X PUT -T part1.bin "$PART_URL" -D - -o /dev/null | grep -i '^etag:' | tr -d '\r' | cut -d' ' -f2)

# 3. Complete the upload
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/object/multipart/complete?session_id=SESSION_ID&path=data/large-file.parquet" \
  -H "Content-Type: application/json" \
  -d "{\"upload_id\": \"$UPLOAD_ID\", \"upload_token\": \"$UPLOAD_TOKEN\", \"parts\": [{\"part_number\": 1, \"etag\": $ETAG}], \"content_type\": \"application/octet-stream\"}"

Note

For most use cases, prefer the Put Object + Finalize Object flow (single PUT) or the Python SDK, which automatically chooses the right upload strategy. Use the multipart API directly only when you need fine-grained control over part uploads (e.g., resumable uploads or very large files).


List Objects

GET /api/v1/organizations/{organization}/repositories/{repository}/objects

Parameter Type Required Description
session_id query No Session ID to list from (defaults to latest committed state)
prefix query No Path prefix filter
after query No Pagination cursor
amount query No Page size (default: 100, max: 1000)
delimiter query No Hierarchy delimiter (e.g., "/")

Response: 200 OK

{
  "results": [
    {
      "path": "data/file.csv",
      "type": "object",
      "entry": {
        "size": 1024,
        "e_tag": "\"abc123\"",
        "content_type": "text/csv",
        "last_modified": "2025-01-15T10:30:00Z"
      }
    }
  ],
  "common_prefixes": ["data/subdir/"],
  "pagination": { "has_more": false, "next_offset": "", "max_per_page": 100 }
}

When delimiter is set, directories are returned in common_prefixes and only direct children are in results.


Bulk Delete Objects

POST /api/v1/organizations/{organization}/repositories/{repository}/objects/delete

Stage tombstones for many objects in a single request. Up to 1,000 paths per call.

Parameter Type Required Description
session_id query Yes Session to stage the deletes into

Request body:

Field Type Required Description
paths string[] Yes Object paths to delete (1 to 1,000 entries)

Response: 200 OK

{ "deleted": 42 }

Errors: 400 (missing or invalid input), 403 (not authorized), 404 (session not found)


List Uncommitted Changes

GET /api/v1/organizations/{organization}/repositories/{repository}/changes

Parameter Type Required Description
session_id query Yes Session ID
prefix query No Path prefix filter
after query No Pagination cursor
amount query No Page size

Response: Same shape as List Objects, showing only staged changes with status field ("added", "modified", "removed").


Connectors (Organization-Level)

Create Connector

POST /api/v1/organizations/{organization}/connectors

Request body:

Field Type Required Description
name string Yes Connector name (unique within org)
type string Yes "s3"
config object Yes Type-specific configuration (see Connectors Reference)

Response: 201 Created

{
  "id": "...",
  "name": "production-s3",
  "type": "s3",
  "disabled": false,
  "created_at": "2025-01-15T10:30:00Z"
}

Note

The config object is AES-encrypted at rest and never returned in API responses.


List Connectors

GET /api/v1/organizations/{organization}/connectors


Get Connector

GET /api/v1/organizations/{organization}/connectors/{connector_id}


Delete Connector

DELETE /api/v1/organizations/{organization}/connectors/{connector_id}

Response: 204 No Content (soft delete)

Note

Deleting a connector does not affect previously imported objects — they are stored in Tilde and remain fully readable. Future imports using this connector will fail.


Connector Attachments (Repository-Level)

Attach Connector to Repository

POST /api/v1/organizations/{organization}/repositories/{repository}/connectors

Request body:

Field Type Required Description
connector_id string (UUID) Yes Connector to attach

Response: 201 Created


List Repository Connectors

GET /api/v1/organizations/{organization}/repositories/{repository}/connectors


Detach Connector from Repository

DELETE /api/v1/organizations/{organization}/repositories/{repository}/connectors/{connector_id}

Response: 204 No Content


Import

Start Import

POST /api/v1/organizations/{organization}/repositories/{repository}/import

Start an asynchronous import job that copies objects into the repository. Objects can be imported from an external connector (S3, GCS) or from another Tilde repository. The request body must contain exactly one of connector_id or (source_organization + source_repository). The import creates its own session internally, copies and stages the imported objects (up to 10 concurrently), and commits them automatically when complete.

Request body (connector import):

Field Type Required Description
connector_id string (UUID) Yes Connector to import from
destination_path string Yes Destination prefix in the repository
source_prefix string No Source prefix filter
commit_message string No Custom commit message

Request body (cross-repository import):

Field Type Required Description
source_organization string Yes Source organization name
source_repository string Yes Source repository name
destination_path string Yes Destination prefix in the repository
source_prefix string No Source prefix filter
commit_message string No Custom commit message

Connector import

import tilde

repo = tilde.repository("my-team/my-data")
job = repo.imports.create_from_connector(
    connector_id="connector-uuid-here",
    destination_path="imported/",
    source_prefix="datasets/",
    commit_message="Import production datasets",
)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/import" \
  -H "Content-Type: application/json" \
  -d '{
    "connector_id": "connector-uuid-here",
    "destination_path": "imported/",
    "source_prefix": "datasets/",
    "commit_message": "Import production datasets"
  }'

Cross-repository import

import tilde

repo = tilde.repository("my-team/my-data")
job = repo.imports.create_from_repository(
    repo_path="other-team/source-data",
    destination_path="external/",
    source_prefix="datasets/train/",
    commit_message="Import training data",
)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/import" \
  -H "Content-Type: application/json" \
  -d '{
    "source_organization": "other-team",
    "source_repository": "source-data",
    "destination_path": "external/",
    "source_prefix": "datasets/train/",
    "commit_message": "Import training data"
  }'

Response: 202 Accepted

{ "job_id": "job-uuid-here" }

Get Import Status

GET /api/v1/organizations/{organization}/repositories/{repository}/import/{job_id}

job = repo.imports.get("job-uuid-here")
print(f"Status: {job.status}, Objects: {job.objects_imported}")
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  https://tilde.run/api/v1/organizations/my-team/repositories/my-data/import/job-uuid-here

Response: 200 OK

{
  "id": "job-uuid-here",
  "repository_id": "...",
  "connector_id": "...",
  "source_prefix": "datasets/",
  "destination_path": "imported/",
  "commit_message": "Import production datasets",
  "status": "completed",
  "objects_imported": 1523,
  "commit_id": "abc123...",
  "error": "",
  "source_organization": "",
  "source_repository": "",
  "created_by": "...",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:35:00Z"
}

Job statuses: pendingrunningcompleted | failed


Sandboxes

Run containers with repository data mounted as a volume. Sandboxes execute asynchronously -- create one and poll for status or stream logs. See the Sandboxes guide for concepts and examples.

Create Sandbox

POST /api/v1/organizations/{organization}/repositories/{repository}/sandboxes

Request body:

Field Type Required Description
image string Yes Docker image reference (e.g. python:3.12, ubuntu:22.04, ghcr.io/my-org/my-image:latest)
command string[] No Command to execute
mountpoint string No Mount path inside container (default: /sandbox)
path_prefix string No Only mount objects under this prefix
timeout_seconds integer No Maximum execution time in seconds.
env_vars object No Environment variables to inject
run_as object No {"type": "agent"|"role", "id": "uuid"} — run as alternate principal.
mode string No Execution mode. One of one-shot (default; run command or image default, then exit), interactive (allocate a pty, attach to terminal), service (idle; only serve /exec requests).
import tilde

repo = tilde.repository("my-team/my-data")
sandbox = repo.sandboxes.create(
    image="python:3.12",
    command=["python", "process.py"],
    env={"OUTPUT_FORMAT": "json"},
)
print(sandbox.id)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandboxes \
  -H "Content-Type: application/json" \
  -d '{
    "image": "python:3.12",
    "command": ["python", "process.py"],
    "env_vars": {"OUTPUT_FORMAT": "json"}
  }'

Response: 201 Created

{ "sandbox_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }

Errors: 400 (invalid image, mountpoint, or env vars), 404 (agent/role not found), 503 (sandbox execution not available)


List Sandboxes

GET /api/v1/organizations/{organization}/repositories/{repository}/sandboxes

Query parameters:

Parameter Type Description
after string Pagination cursor
amount integer Page size (default/max: 1000)
import tilde

repo = tilde.repository("my-team/my-data")
for sb in repo.sandboxes.list():
    print(sb.id, sb.status().state)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandboxes

Response: 200 OK

{
  "results": [
    {
      "id": "a1b2c3d4-...",
      "image": "python:3.12",
      "command": ["python", "process.py"],
      "mountpoint": "/sandbox",
      "status": "committed",
      "commit_id": "abc123def456...",
      "exit_code": 0,
      "env_vars": {"OUTPUT_FORMAT": "json"},
      "created_by_type": "user",
      "created_by": "user-uuid",
      "created_at": "2026-03-11T10:30:00Z",
      "finished_at": "2026-03-11T10:32:00Z",
      "trigger_id": null,
      "trigger_name": ""
    }
  ],
  "pagination": { "has_more": false, "next_offset": "", "max_per_page": 1000 }
}

Get Sandbox

GET /api/v1/organizations/{organization}/repositories/{repository}/sandboxes/{sandbox_id}

Returns full sandbox details including status, exit code, commit ID (if committed), and error information.

status = sandbox.status()
print(status.state, status.exit_code, status.commit_id)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandboxes/SANDBOX_ID

Response: 200 OK — same shape as individual items in the list response, plus status_reason and error_message fields.

Errors: 404 (sandbox not found)


Get Sandbox Status

GET /api/v1/organizations/{organization}/repositories/{repository}/sandboxes/{sandbox_id}/status

Lightweight status-only endpoint for polling.

Response: 200 OK

{ "status": "running" }

Cancel Sandbox

DELETE /api/v1/organizations/{organization}/repositories/{repository}/sandboxes/{sandbox_id}

Cancels a running sandbox. The session is rolled back and no changes are committed.

Response: 202 Accepted

Errors: 404 (sandbox not found)


Stream Sandbox Logs

GET /api/v1/organizations/{organization}/repositories/{repository}/sandboxes/{sandbox_id}/logs/stdout

Stream the sandbox's merged stdout and stderr as a chunked response. The runner interleaves both streams in the order the sandbox produced them — there is no separate stderr endpoint.

Response: 200 OK (chunked text/plain)

Errors: 404 (sandbox not found), 410 (logs no longer available)


Stream Sandbox Network Logs

GET /api/v1/organizations/{organization}/repositories/{repository}/sandboxes/{sandbox_id}/logs/network

NDJSON stream of every outbound HTTP request the sandbox made, including the proxy's allow/deny decision and (for allowed requests) the upstream response metadata. One JSON object per line.

Response: 200 OK (chunked application/x-ndjson)

Errors: 404 (sandbox not found), 410 (logs no longer available)


Open Sandbox Terminal

GET /api/v1/organizations/{organization}/repositories/{repository}/sandboxes/{sandbox_id}/terminal

WebSocket endpoint for bidirectional terminal I/O with an interactive sandbox. Only available for sandboxes created with mode: "interactive" while they are still running.

The connection upgrades to WebSocket and uses binary frames with a single-byte type prefix:

Byte Direction Payload
0x00 client → server stdin data
0x01 server → client stdout/stderr data (TTY-merged)
0x02 client → server JSON resize: {"cols":N,"rows":N}
0x03 server → client JSON exit: {"exited":true,"exit_code":N}

Response: 101 Switching Protocols

Errors: 400 (sandbox is not interactive), 409 (terminal session unavailable, e.g. after crash recovery), 410 (sandbox already finished)


Sandbox Statuses

Status Description
starting Sandbox is being prepared. Wait — do not attach.
running Sandbox is ready. You may attach the terminal and/or stream logs.
committed Finished successfully; changes committed. Read commit_id.
awaiting_approval Finished but the session needs human approval. Surface web_url.
failed Sandbox errored. Read error_message.
cancelled Cancelled by a user or operator.

Sandbox Triggers

Triggers automatically create sandboxes when specific files change in a commit. See the Triggers Reference for concepts, condition types, and examples.

Create Trigger

POST /api/v1/organizations/{organization}/repositories/{repository}/sandbox-triggers

Request body:

Field Type Required Description
name string Yes Unique name (1–100 characters)
description string No Human-readable description
enabled boolean No Whether the trigger is active (default: true)
conditions array Yes 1–10 conditions (OR logic). See Condition types
sandbox_config object Yes Sandbox configuration (same fields as Create Sandbox minus run_as)
run_as object No {"type": "agent"|"role", "id": "uuid"} — delegate execution
import tilde

repo = tilde.repository("my-team/my-data")
trigger = repo.sandbox_triggers.create(
    name="Validate CSV uploads",
    conditions=[{"type": "prefix", "prefix": "datasets/"}],
    sandbox_config={
        "image": "python:3.12",
        "command": ["python", "-m", "validate"],
        "timeout_seconds": 300,
    },
)
print(trigger.id)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X POST https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandbox-triggers \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Validate CSV uploads",
    "conditions": [
      { "type": "prefix", "prefix": "datasets/" }
    ],
    "sandbox_config": {
      "image": "python:3.12",
      "command": ["python", "-m", "validate"],
      "timeout_seconds": 300
    }
  }'

Response: 201 Created — returns the full trigger object.

{
  "id": "trigger-uuid",
  "repository_id": "repo-uuid",
  "name": "Validate CSV uploads",
  "description": "",
  "enabled": true,
  "conditions": [
    { "type": "prefix", "prefix": "datasets/" }
  ],
  "sandbox_config": {
    "image": "python:3.12",
    "command": ["python", "-m", "validate"],
    "timeout_seconds": 300
  },
  "created_by": "user-uuid",
  "created_at": "2026-03-11T10:30:00Z",
  "updated_at": "2026-03-11T10:30:00Z"
}

Errors: 400 (invalid name, conditions, or config), 409 (name already exists in this repository)


Condition Types

Prefix

Matches any changed file whose path starts with the given prefix.

{ "type": "prefix", "prefix": "datasets/" }

Exact Path

Matches a specific file path, optionally filtered by change type.

{ "type": "path_exact", "path": "config.yaml", "diff_type": "changed" }
diff_type Fires when
any (default) File is added, removed, or modified
added File is newly created
removed File is deleted
changed File is modified (existed before and after)

List Triggers

GET /api/v1/organizations/{organization}/repositories/{repository}/sandbox-triggers

import tilde

repo = tilde.repository("my-team/my-data")
for trigger in repo.sandbox_triggers.list():
    print(trigger.name, trigger.enabled)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandbox-triggers

Response: 200 OK

{
  "results": [ ... ],
  "pagination": { "has_more": false, "next_offset": "", "max_per_page": 1000 }
}

Get Trigger

GET /api/v1/organizations/{organization}/repositories/{repository}/sandbox-triggers/{trigger_id}

Response: 200 OK — full trigger object.

Errors: 404 (trigger not found)


Update Trigger

PUT /api/v1/organizations/{organization}/repositories/{repository}/sandbox-triggers/{trigger_id}

Full replacement — all fields from Create Trigger are accepted.

trigger.update(
    name="Validate CSV uploads",
    conditions=[{"type": "prefix", "prefix": "datasets/v2/"}],
    sandbox_config={
        "image": "python:3.12",
        "command": ["python", "-m", "validate", "--strict"],
    },
)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X PUT https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandbox-triggers/TRIGGER_ID \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Validate CSV uploads",
    "conditions": [{ "type": "prefix", "prefix": "datasets/v2/" }],
    "sandbox_config": {
      "image": "python:3.12",
      "command": ["python", "-m", "validate", "--strict"]
    }
  }'

Response: 200 OK — updated trigger object.

Errors: 400 (invalid input), 404 (trigger not found), 409 (name conflict)


Toggle Trigger

PATCH /api/v1/organizations/{organization}/repositories/{repository}/sandbox-triggers/{trigger_id}

Enable or disable a trigger without changing its configuration.

Request body:

Field Type Required Description
enabled boolean Yes true to enable, false to disable
trigger.toggle(enabled=False)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X PATCH https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandbox-triggers/TRIGGER_ID \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

Response: 200 OK — updated trigger object.


Delete Trigger

DELETE /api/v1/organizations/{organization}/repositories/{repository}/sandbox-triggers/{trigger_id}

trigger.delete()
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X DELETE https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandbox-triggers/TRIGGER_ID

Response: 204 No Content

Errors: 404 (trigger not found)


List Trigger Runs

GET /api/v1/organizations/{organization}/repositories/{repository}/sandbox-triggers/{trigger_id}/runs

Returns the history of trigger evaluations for a specific trigger.

Query parameters:

Parameter Type Description
after string Pagination cursor
amount integer Page size (default/max: 1000)
for run in trigger.runs.list():
    print(run.status, run.sandbox_id, run.matched_paths)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  https://tilde.run/api/v1/organizations/my-team/repositories/my-data/sandbox-triggers/TRIGGER_ID/runs

Response: 200 OK

{
  "results": [
    {
      "id": "run-uuid",
      "trigger_id": "trigger-uuid",
      "commit_id": "abc123def456",
      "status": "created",
      "reason": "",
      "sandbox_id": "sandbox-uuid",
      "matched_paths": ["datasets/"],
      "created_at": "2026-03-11T10:30:00Z",
      "updated_at": "2026-03-11T10:30:00Z"
    }
  ],
  "pagination": { "has_more": false, "next_offset": "", "max_per_page": 1000 }
}

Run statuses:

Status Description
created Sandbox was successfully created
skipped Trigger matched but sandbox was not created (reason field explains why)
failed Trigger matched but sandbox creation failed

Secrets

Secrets are encrypted key-value pairs that are automatically injected as environment variables into sandboxes. They are stored with AES encryption at rest. Secret values are never returned in list responses — only metadata is shown.

There are two types of secrets:

  • Repository secrets — scoped to a repository. Injected into all sandboxes running in that repository.
  • Agent secrets — scoped to an agent. Injected into sandboxes running as that agent, overriding repository secrets with the same key.

Key format: Must match ^[A-Za-z_][A-Za-z0-9_-]{0,255}$ — start with a letter or underscore, then letters, digits, underscores, or hyphens (max 256 characters).

Repository Secrets

Repository secrets are injected into all sandboxes running in the repository, regardless of which principal runs the sandbox.

RBAC: Managing secrets (create, update, list, delete) requires ManageRepositorySecrets. Reading decrypted values requires ReadRepositorySecrets.

Create or Update Repository Secret

PUT /api/v1/organizations/{organization}/repositories/{repository}/secrets/{secret_key}

Creates a new secret or updates the value of an existing one. On update, the original created_by metadata is preserved.

Request body:

Field Type Required Description
value string Yes Secret value (max 64 KiB)
import tilde

repo = tilde.repository("my-team/my-data")
repo.secrets.create("API_KEY", "sk-abc123...")
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X PUT "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/secrets/API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value": "sk-abc123..."}'

Response: 200 OK

{ "key": "API_KEY" }

Errors: 400 (invalid key or empty value), 403 (insufficient permissions)


List Repository Secrets

GET /api/v1/organizations/{organization}/repositories/{repository}/secrets

Returns metadata for all active secrets in the repository. Values are never included.

import tilde

repo = tilde.repository("my-team/my-data")
for entry in repo.secrets.list():
    print(entry.name, entry.created_at)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/secrets"

Response: 200 OK

{
  "results": [
    {
      "key": "API_KEY",
      "created_by_type": "user",
      "created_by": "user-uuid",
      "created_at": "2025-01-15T10:30:00Z",
      "updated_at": "2025-01-15T10:30:00Z"
    }
  ]
}

Get Repository Secret

GET /api/v1/organizations/{organization}/repositories/{repository}/secrets/{secret_key}

Returns the decrypted value of a single secret. Requires the ReadRepositorySecrets permission (separate from ManageRepositorySecrets).

import tilde

repo = tilde.repository("my-team/my-data")
value = repo.secrets.get("API_KEY").value
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/secrets/API_KEY"

Response: 200 OK

The response includes Cache-Control: no-store to prevent caching of secret values.

{ "key": "API_KEY", "value": "sk-abc123..." }

Errors: 400 (invalid key), 403 (insufficient permissions), 404 (secret not found)


Delete Repository Secret

DELETE /api/v1/organizations/{organization}/repositories/{repository}/secrets/{secret_key}

Soft-deletes a secret. The secret is immediately removed from future sandbox injections.

import tilde

repo = tilde.repository("my-team/my-data")
repo.secrets.delete("API_KEY")
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X DELETE "https://tilde.run/api/v1/organizations/my-team/repositories/my-data/secrets/API_KEY"

Response: 204 No Content

Errors: 400 (invalid key), 403 (insufficient permissions), 404 (secret not found)


Agent Secrets

Agent secrets are injected into sandboxes when the sandbox runs as that agent (via run_as). They override repository secrets with the same key.

RBAC: Managing agent secrets (create, update, list, delete) requires ManageAgentSecrets. Reading decrypted values requires ReadAgentSecrets. Both actions are scoped to agents the principal owns via the built-in AgentManager policy.

Create or Update Agent Secret

PUT /api/v1/organizations/{organization}/agents/{agent_name}/secrets/{secret_key}

Creates a new agent secret or updates the value of an existing one. On update, the original created_by metadata is preserved.

Request body:

Field Type Required Description
value string Yes Secret value (max 64 KiB)
import tilde

org = tilde.organizations.get("my-team")
agent = org.agents.get("data-pipeline")
agent.secrets.create("OPENAI_KEY", "sk-abc123...")
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X PUT "https://tilde.run/api/v1/organizations/my-team/agents/data-pipeline/secrets/OPENAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value": "sk-abc123..."}'

Response: 200 OK

{ "key": "OPENAI_KEY" }

Errors: 400 (invalid key or empty value), 403 (insufficient permissions), 404 (agent not found)


List Agent Secrets

GET /api/v1/organizations/{organization}/agents/{agent_name}/secrets

Returns metadata for all active secrets of the agent. Values are never included.

import tilde

org = tilde.organizations.get("my-team")
agent = org.agents.get("data-pipeline")
for entry in agent.secrets.list():
    print(entry.name, entry.created_at)
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://tilde.run/api/v1/organizations/my-team/agents/data-pipeline/secrets"

Response: 200 OK

{
  "results": [
    {
      "key": "OPENAI_KEY",
      "created_by_type": "user",
      "created_by": "user-uuid",
      "created_at": "2025-01-15T10:30:00Z",
      "updated_at": "2025-01-15T10:30:00Z"
    }
  ]
}

Get Agent Secret

GET /api/v1/organizations/{organization}/agents/{agent_name}/secrets/{secret_key}

Returns the decrypted value of a single agent secret. Requires the ReadAgentSecrets permission (separate from ManageAgentSecrets).

import tilde

org = tilde.organizations.get("my-team")
agent = org.agents.get("data-pipeline")
value = agent.secrets.get("OPENAI_KEY").value
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://tilde.run/api/v1/organizations/my-team/agents/data-pipeline/secrets/OPENAI_KEY"

Response: 200 OK

The response includes Cache-Control: no-store to prevent caching of secret values.

{ "key": "OPENAI_KEY", "value": "sk-abc123..." }

Errors: 400 (invalid key), 403 (insufficient permissions), 404 (agent or secret not found)


Delete Agent Secret

DELETE /api/v1/organizations/{organization}/agents/{agent_name}/secrets/{secret_key}

Soft-deletes an agent secret. The secret is immediately removed from future sandbox injections.

import tilde

org = tilde.organizations.get("my-team")
agent = org.agents.get("data-pipeline")
agent.secrets.delete("OPENAI_KEY")
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  -X DELETE "https://tilde.run/api/v1/organizations/my-team/agents/data-pipeline/secrets/OPENAI_KEY"

Response: 204 No Content

Errors: 400 (invalid key), 403 (insufficient permissions), 404 (agent or secret not found)


Secrets in Sandboxes

When a sandbox is created, secrets are injected as environment variables with the following precedence (highest to lowest):

  1. User-supplied env_vars — values passed in the sandbox creation request
  2. Agent secrets — if the sandbox runs as an agent (via run_as), the agent's secrets are injected
  3. Repository secrets — secrets stored on the repository

At each level, a key that already exists from a higher-precedence source is not overwritten. This means user-supplied env vars override agent secrets, and agent secrets override repository secrets of the same key.

Example: A repository has secrets API_KEY=repo-key and DB_HOST=prod.db.example.com. The agent data-pipeline has secrets API_KEY=agent-key and OPENAI_KEY=sk-abc. A sandbox is created with run_as: {type: "agent", id: "..."} and env_vars: {"DB_HOST": "staging.db.example.com"}. The container sees:

Variable Value Source
DB_HOST staging.db.example.com User-supplied (overrides repo secret)
API_KEY agent-key Agent secret (overrides repo secret)
OPENAI_KEY sk-abc Agent secret

Note

Agent secrets are only injected when the sandbox runs as an agent. Sandboxes running as a user or role only receive repository secrets and user-supplied env vars.


Roles

See the RBAC Reference for full details. Roles are non-human identities for CI/CD pipelines and automation. They authenticate via API keys.

Method Endpoint Description
POST /organizations/{org}/roles Create role
GET /organizations/{org}/roles List roles
GET /organizations/{org}/roles/{role_name} Get role
DELETE /organizations/{org}/roles/{role_name} Delete role
POST /organizations/{org}/roles/{role_name}/auth/keys Create role API key
GET /organizations/{org}/roles/{role_name}/auth/keys List role API keys
DELETE /organizations/{org}/roles/{role_name}/auth/keys/{key_id} Revoke role API key

Agents

See the RBAC Reference for full details. Agents are non-human identities that carry metadata and support updates. They authenticate via API keys. Agents cannot create, update, or delete other agents.

Method Endpoint Description
POST /organizations/{org}/agents Create agent
GET /organizations/{org}/agents List agents
GET /organizations/{org}/agents/{agent_name} Get agent
PUT /organizations/{org}/agents/{agent_name} Update agent
DELETE /organizations/{org}/agents/{agent_name} Delete agent
POST /organizations/{org}/agents/{agent_name}/auth/keys Create agent API key
GET /organizations/{org}/agents/{agent_name}/auth/keys List agent API keys
DELETE /organizations/{org}/agents/{agent_name}/auth/keys/{key_id} Revoke agent API key
PUT /organizations/{org}/agents/{agent_name}/secrets/{key} Create or update agent secret
GET /organizations/{org}/agents/{agent_name}/secrets List agent secrets
GET /organizations/{org}/agents/{agent_name}/secrets/{key} Get agent secret
DELETE /organizations/{org}/agents/{agent_name}/secrets/{key} Delete agent secret

RBAC (Groups & Policies)

See the RBAC Reference for full details. Key endpoints:

Method Endpoint Description
POST /organizations/{org}/groups Create group
GET /organizations/{org}/groups List groups
GET /organizations/{org}/groups/{id} Get group
PUT /organizations/{org}/groups/{id} Update group
DELETE /organizations/{org}/groups/{id} Delete group
POST /organizations/{org}/groups/{id}/members Add group member
DELETE /organizations/{org}/groups/{id}/members Remove group member
POST /organizations/{org}/policies Create policy
GET /organizations/{org}/policies List policies
GET /organizations/{org}/policies/{id} Get policy
PUT /organizations/{org}/policies/{id} Update policy
DELETE /organizations/{org}/policies/{id} Delete policy
POST /organizations/{org}/policies:validate Validate policy
POST /organizations/{org}/policies/{id}/attachments Attach policy to user/group
DELETE /organizations/{org}/policies/{id}/attachments Detach policy
GET /organizations/{org}/attachments List all attachments
GET /organizations/{org}/effective-policies Get effective policies for current user

Health & Metrics

Health Check

GET /healthNo auth required

Response: 200 OK

{ "status": "ok" }

Prometheus Metrics

GET /metricsNo auth required

Returns Prometheus-formatted metrics.