# Timone: full reference for AI agents Timone runs apps as containers and gives each one a public HTTPS URL (a PaaS, not Kubernetes-as-a-service: users get no cluster access, kubeconfig or kubectl) (https://.timone.dev, or the user's own domain on paid plans). This document is the complete, current reference. A shorter index is at https://timone.dev/llms.txt. Base URL: https://dev-api.timone.dev Format: JSON requests and responses. Every error is {"code": "...", "error": "readable message"}. Unknown routes return 404 JSON. Rate limits return 429 with code `rate_limited`. ## 1. Authentication Header on every request: Authorization: Bearer tmn_... - The user creates the token in the dashboard (Settings > API tokens). The plain value is shown once and cannot be recovered; if lost, revoke it and create another. - Manage tokens: GET /settings/tokens, POST /settings/tokens, DELETE /settings/tokens/:id. - A token acts as the user: same apps, same plan, same quota, same deploys-per-hour limit. - A token acts only as the user who created it: it can never use the platform administration (/admin/*), even when that user is the platform administrator. - Only a browser session (not a token) can: disconnect GitHub, delete the account, turn the delete-protection lock on/off, or choose repositories on GitHub. Those return 403 for tokens. ## 2. Concepts - App (project): a running container. Sources: GitHub repo (built from its Dockerfile), a template (ready-made), or a public Docker image. Each app has an id, a slug and a URL. - Deployment: one build/deploy of an app. status: queued -> building -> deploying -> live | failed. Template and image apps skip `building`. - Database: a private managed service (PostgreSQL, MariaDB, MySQL, Valkey (Redis-compatible), nginx). It is reachable only inside the user's own namespace, by its internal host name. - Lock: per-app/per-database delete protection (paid plans). A locked resource cannot be deleted. Every database you create on a paid plan is created LOCKED. Only the user, signed in to the dashboard, can unlock it: no API token can, not even an admin token (forcing a delete of a locked resource also needs a browser session). If you need to delete a database, ask the user to unlock it. - Plan: free | starter | plus | pro. Defines containers, memory and storage. ## 3. GitHub repositories Timone uses a GitHub App. The user picks, on GitHub, exactly which repositories Timone can see (public and private). Timone never sees the others. It registers a webhook so every push to the default branch redeploys automatically. - GET /github/repos -> [{id,name,fullName,private,defaultBranch,htmlUrl,hasDockerfile}] (only the repos the user chose on GitHub) - GET /github/repos/manage -> {manage:[{account,type,url}]} links where the USER adds or removes repos on GitHub (a token cannot do it) 403 {"error":"repo_access_required"} means no repositories were chosen yet. - POST /projects {"repoFullName":"owner/repo","envVars":[{"key","value"}]?} Connects the repo, registers the push webhook and starts the first deploy. Requirements: the repo is one the user chose on GitHub, the user is an admin of it, and it has a `Dockerfile` at the root of its default branch. Repeating the call for the same repo returns the same project. Errors: github_not_connected, repo_not_selected (choose it on GitHub first), repo_not_found, repo_access_denied (needs admin on the repo), no_dockerfile, slug_taken, capacity (503, Free only), rate_limited, and the container-limit error of the plan. Response: {project, webhookRegistered, deployment, deployNote}. `deployNote` explains when the project was created but the first deploy was postponed (deploy limit or capacity). - Private repositories are built with a read-only token limited to that single repository and valid for one hour. The token is used only to clone; the Dockerfile's RUN steps never see it. - The Dockerfile must listen on the port it EXPOSEs (default 80 if it declares none). ### What a container needs to run (check this BEFORE blaming the platform) Almost every deploy that ends `failed` with CrashLoopBackOff is the app, not Timone. Verify: 1. It keeps running: a server in the foreground. A script or one-time command that ends is restarted until it crash-loops. 2. It listens on 0.0.0.0 (not localhost/127.0.0.1) on the port declared with EXPOSE (default 80 if none). 3. CMD/ENTRYPOINT starts it with no manual step and no stdin. 4. Everything it needs is in the image or in env vars. A missing required variable (database URL, secret) usually stops it at startup. 5. Other services are reached by their Timone address (a Timone database is NOT on localhost: use its connectionString). 6. It fits its memory (about 256 MiB per app by default; Free shares 512 MiB between 2 containers). Over the limit = OOMKilled. 7. It does not rely on local files (ephemeral, ~192 MiB of disk). linux/amd64, no GPU, no privileged mode. Diagnose a failed deploy: GET /deployments/:id returns the log. Lines starting with `[app]` are the last output of the app before it stopped. GET /projects/:id/logs shows runtime output, `restarts` and `lastState`. Fix the repo or the env vars, then POST /projects/:id/deploy. Do not retry the same deploy in a loop: the same code crashes the same way. The user-facing guide is https://timone.dev/dashboard/faq. ### Choosing the version to deploy (repositories) GET /projects/:id/refs -> {defaultBranch, branches[], commits[{sha,message,author,date}], releases[{tag,name,publishedAt}]}. POST /projects/:id/deploy {"ref":"","refKind":"branch"|"tag"|"commit"} deploys that version AND saves the choice: - ref "" (or the default branch): follow the default branch; every push to it redeploys (the default). - refKind "branch" (another branch): the app follows that branch; pushes to it redeploy, pushes to the default branch do not. - refKind "tag" (a release) or "commit": the version is FIXED; pushes do NOT redeploy until a deploy with ref "" is requested. POST /projects/:id/deploy without a body rebuilds whatever the project is currently set to follow (see deployRef and deployRefKind in GET /projects/:id). Errors: invalid_ref, ref_not_found, not_a_repo (apps from images/templates have no versions). In GET /projects/:id the deployments list is history: only the newest `live` one runs; older `live` entries were replaced. ## 4. Apps - GET /projects list apps with the status of the last deployment. - GET /projects/:id detail: subdomain, env var NAMES, deployments (status, imageTag, times). - PATCH /projects/:id {"displayName":"My app"} label only; the URL does not change. - POST /projects/:id/deploy redeploy now (also applies env var changes). Errors: rate_limited, capacity, locked storage. - GET /projects/:id/logs?tail=200 stdout/stderr per pod, plus `restarts` and `lastState` (e.g. OOMKilled). - GET /deployments/:id one deployment with its build/deploy log lines (poll this while building). - POST /projects/:id/stop stop the app (0 replicas): frees its memory; the app, URL and volumes stay. 409 not_running if it is not running yet. - POST /projects/:id/start start it again (no rebuild). Free plan goes through the capacity check again (503 capacity). - DELETE /projects/:id remove the app. 409 {"code":"locked"} if locked. Response lists `orphanedDatabases`: a template's own database is NOT deleted. - Environment variables (stored encrypted): GET /projects/:id/env -> [{key,value}] (values in clear for the owner) PUT /projects/:id/env {"key":"DATABASE_URL","value":"..."} DELETE /projects/:id/env/:key Changes apply on the next deploy: call POST /projects/:id/deploy. ## 5. Templates and images - GET /templates -> the live list: key, name, description, license, port, typical memory, whether it needs keepData, whether it brings its own database, and which credentials it accepts. Examples: uptime-kuma, adminer, excalidraw, it-tools, gotify, pocketbase, umami, ghost, wordpress, forgejo, nextcloud, listmonk, whoami. - POST /templates {"templateKey":"uptime-kuma"} or {"image":"nginx:alpine","port":80} any public image from a known public registry Optional fields: "envVars":[{key,value}], "credentials":{"username","password"} (templates that support it), "keepData":true and "volumeMi":512 (persistent volume, paid plans; some templates require it: error keep_data_required). Unknown fields are rejected (unknown_field). Errors: unknown_template, port_required, image_not_found, invalid_env_vars, reserved_username. - Templates that bring a database (umami, wordpress, nextcloud) create and connect it. Deleting the app does not delete that database (it keeps the data); delete it from /catalog. - Some templates have no preset admin: whoever opens the URL first creates it (WordPress). Tell the user. ## 6. Databases (private) - GET /catalog/services -> available engines, versions, defaults, which of username/password you may set. - POST /catalog {"serviceKey":"postgres","version":"16","username"?,"password"?} Returns the database with its `connectionString` and internal host. Passwords: only [A-Za-z0-9._~-]. Versions come from a fixed list per engine (no free text). - GET /catalog the user's databases with connection strings. - PATCH /catalog/:id {"displayName"} - POST /catalog/:id/stop POST /catalog/:id/start stop frees memory and keeps the data volume; start brings it back - DELETE /catalog/:id delete (409 if locked). A database with a volume goes to the TRASH: it is switched off and its volume is kept 3 days; the response has {trashed:true, restorableUntil}. Use ?permanent=true to erase it immediately. - GET /catalog/trash deleted databases that can still be restored (id, deletedAt, expiresAt) - POST /catalog/:id/restore bring one back from the trash (same credentials and data; counts against your container limit) - DELETE /catalog/:id/permanent erase one that is in the trash for good - The internal host (e.g. .user-.svc.cluster.local) resolves ONLY inside the cluster. Connect an app by putting the connection string in an env var (postgres/mysql -> DATABASE_URL, valkey -> REDIS_URL), either in "envVars" at creation or with PUT /projects/:id/env followed by a redeploy. Stopped vs deleted: a stopped app or database is off but still exists. It keeps its URL/host name, its volumes (databases and "keepData" apps) and its place in the container limit. The container's own files are ephemeral and are lost on any restart, stop or redeploy. On the Free plan the 4-hour timer keeps running while stopped. Stopping is not blocked by the lock (it deletes nothing). ## 7. Usage, storage, plan - GET /usage CPU and memory per container as % of the plan quota, restarts, lastState. - GET /storage the account's volume pool: reserved, measured use per volume, warnings. - GET /settings/plan {plan, unlimited, containers:{used,limit}, expiresAt} (expiresAt: Free only). - GET /settings account info and whether GitHub repositories are connected. ## 8. Custom domains (paid plans) - GET /domains POST /projects/:id/domains {"hostname":"app.example.com"} Returns the DNS records the user must create (a TXT ownership record and a CNAME). Timone verifies the DNS, and only then issues HTTPS (via Cloudflare). POST /domains/:id/check re-checks now. DELETE /domains/:id removes it. ## 9. Plans and limits - Free ($0): 2 containers at a time sharing 512 MiB; each is deleted 4 h after creation (a redeploy does not reset the clock). No volumes, no custom domains, no lock, no MySQL. When the server is full, Free deploys fail with 503 and a message; Free containers may be stopped to make room for paid plans (`stoppedReason`: "no capacity"); redeploy/start brings them back. - Starter $5 (1 GiB, 10 containers, 5 GB), Plus $9 (2 GiB, 20, 9 GB), Pro $16 (4 GiB, 40, 16 GB). Not purchasable yet: the admin assigns plans manually. - Deploys per hour are limited per user (error rate_limited). - Images must be linux/amd64. AI/ML model workloads are rejected (no GPU). - Early access: single server, no uptime guarantee, daily server backup, no per-app restore. ## 10. Recipes Deploy a repo: 1. GET /github/repos (if 403 repo_access_required: ask the user to choose repos on GitHub) 2. POST /projects {"repoFullName":"owner/repo"} 3. Poll GET /projects/:id every few seconds until deployments[0].status is live or failed. 4. On failed: GET /deployments/:id (build log) and GET /projects/:id/logs (runtime). App + database: 1. POST /catalog {"serviceKey":"postgres"} -> read connectionString. 2. POST /projects {"repoFullName":"owner/repo","envVars":[{"key":"DATABASE_URL","value":""}]} Change an env var: PUT /projects/:id/env, then POST /projects/:id/deploy. App keeps restarting or deploy failed with CrashLoopBackOff: read the "What a container needs to run" checklist in section 3 first. GET /projects/:id/logs -> look at `restarts` and `lastState` (OOMKilled = raise the memory by upgrading the plan or reducing the app's memory use). Clean up: DELETE /projects/:id and DELETE /catalog/:id (a template's database must be deleted separately). ## 11. Safety rules for agents - Never delete anything the user did not ask you to delete. Check `locked` first; do not try to bypass it. - Treat connection strings and env var values as secrets: do not print them in shared places. - Ask before deploying something that costs the user's limited quota (containers, memory, storage). - A 409 `locked`, 403 `repo_access_denied` or 503 `capacity` is a real answer, not something to retry in a loop.