API Endpoints
Learn how to use the Hatchbox REST API Endpoints
Updated
Hatchbox API
The Hatchbox API lets you create and deploy apps, manage environment variables, domains, processes, and cron jobs, discover your accounts, clusters, database clusters, and git providers, inspect the servers in a cluster, create databases and attach them to your apps, download or trigger database backups, and read the logs of any operation you queued — all from your own scripts and tooling.
Table of contents
- Authentication
- Base URL
- Active subscription required
- Finding IDs
- Errors
- Accounts
- Apps
- Environment variables
- Domains
- Processes
- Cron jobs
- Clusters
- Servers
- Git providers
- Database clusters
- Databases
- Logs
Authentication
All requests require an API token, sent as a Bearer token:
Authorization: Bearer [TOKEN]
You can find your API token in your Hatchbox account under API Tokens.
Requests with a missing or invalid token return 401 Unauthorized with an empty body.
Base URL
https://hatchbox.io/api/v1
All responses are JSON.
Active subscription required
The API is available on accounts with an active Hatchbox subscription. Every endpoint scoped to an account's resources — listing, reading, creating, or acting on apps, clusters, servers, database clusters, git providers, domains, processes, environment variables, and database backups — requires the account that owns the resource to have an active subscription (a trial counts). When it doesn't, the response is 402 Payment Required:
{ "error": "An active subscription is required for [Account Name]" }
Listing your accounts is the one exception — it stays available without a subscription, so a client can always discover which accounts it can act on.
Finding IDs
Most endpoints are scoped to a resource and need its ID. Start from your accounts and work down:
- Account ID — use List your accounts. Accepts either the numeric id or the prefixed form (
acct_…) shown in the dashboard URL. - App ID — use List apps for an account.
- Cluster ID — use List clusters for an account.
- Server ID — use Get a cluster, which includes the cluster's servers.
- Database cluster ID — use List database clusters for an account.
- Database ID — use List databases for a database cluster, or read it from the database's dashboard URL.
Errors
Status | Meaning | Body |
|---|---|---|
401 Unauthorized | Missing or invalid token | (empty) |
402 Payment Required | The owning account has no active subscription |
|
404 Not Found | The record doesn't exist, or your token can't access it |
|
422 Unprocessable Content | Validation failed |
|
Resources belonging to accounts you're not a member of return 404, not 403. The 404 body names the resource type — e.g. {"error": "Cluster not found"}.
Some endpoints return additional statuses (409 Conflict, 502 Bad Gateway) documented alongside them.
Accounts
Your accounts are the top of the hierarchy — apps, clusters, and git providers all belong to one. Use this endpoint to discover the accounts your token can act on, then pass an account's id to the account-scoped list endpoints below.
List your accounts
Returns the accounts your token's user belongs to. Unlike the rest of the API, this endpoint does not require an active subscription, so you can always determine which accounts are available.
GET /api/v1/accounts
curl https://hatchbox.io/api/v1/accounts \
-H "Authorization: Bearer [TOKEN]"
[
{ "id": 3, "name": "My Team" }
]
An account's id accepts either the numeric form or the prefixed acct_… form from the dashboard URL wherever an [ACCOUNT_ID] is required.
Apps
The app object
{
"id": 1,
"name": "my-app",
"cluster_id": 12,
"connected_account_id": 5,
"repo_path": "myuser/my-app",
"branch": "main",
"auto_deploy": false,
"pre_build_script": null,
"build_script": null,
"post_build_script": null,
"post_deploy_script": null,
"failed_deploy_script": null,
"dns_provider": null,
"dns_api_user": null,
"caddyfile": null,
"health_check_uri": null,
"last_deploy_at": "2026-07-14T18:22:10.000Z",
"last_deploy_sha": "abc1234def5678"
}
The DNS access token is never returned, even though it can be set.
List apps
Returns the apps for a single account, sorted by name.
GET /api/v1/accounts/[ACCOUNT_ID]/apps
curl https://hatchbox.io/api/v1/accounts/[ACCOUNT_ID]/apps \
-H "Authorization: Bearer [TOKEN]"
Returns an array of app objects. An account you're not a member of returns 404 {"error": "Account not found"}.
Get an app
GET /api/v1/apps/[APP_ID]
curl https://hatchbox.io/api/v1/apps/[APP_ID] \
-H "Authorization: Bearer [TOKEN]"
Returns a single app object.
Create an app
Creates an app on a cluster. cluster_id and name are required; the app is created with auto-deploy off (enable it separately once a git provider is connected).
POST /api/v1/apps
Parameter | Required | Description |
|---|---|---|
| Yes | The cluster to create the app on (see List clusters). |
| Yes | Letters, numbers, hyphens, and underscores only. |
| No | Defaults to |
| No | e.g. |
| No | The git provider to deploy from (see List git providers). Leave blank for a public/custom git host. |
| No | Deploy lifecycle scripts. |
| No | DNS credentials. |
| No | Caddy configuration. Stored on create, but only applied to your servers on update — see Caddy configuration. |
curl -X POST https://hatchbox.io/api/v1/apps \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"app": {"cluster_id": 12, "name": "my-app", "branch": "main", "repo_path": "myuser/my-app", "connected_account_id": 5}}'
Returns 201 Created with the app object. A cluster_id you don't own returns 404 {"error": "Cluster not found"}; validation failures return 422 with errors.
Update an app
Updates an app's configuration. Accepts the same fields as create except cluster_id — an app can't be moved between clusters via the API. To toggle auto-deploy, use the auto-deploy endpoints below.
PATCH /api/v1/apps/[APP_ID]
curl -X PATCH https://hatchbox.io/api/v1/apps/[APP_ID] \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"app": {"build_script": "bundle install", "branch": "production"}}'
Returns 200 OK with the updated app object.
Caddy configuration
Two fields control an app's Caddy setup:
Parameter | Description |
|---|---|
| The app's Caddy configuration. |
| A relative path for Caddy to health-check, e.g. |
Including either field in an update applies the configuration to the app's servers. The apply is queued, so a successful response means it was accepted, not that it finished. An update containing neither field leaves the Caddy configuration untouched.
curl -X PATCH https://hatchbox.io/api/v1/apps/[APP_ID] \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"app": {"caddyfile": "import /home/deploy/my-app/current/hatchbox/Caddyfile.pre*\n\n{{encode}}\n{{file_server}}\n{{default}}\n\nimport /home/deploy/my-app/current/hatchbox/Caddyfile.post*", "health_check_uri": "/up"}}'
The configuration is applied on submission, not on change. Sending a caddyfile identical to the stored one still re-applies it. This is deliberate: a version-controlled Caddyfile is typically a wrapper that imports other files out of your repository, so the wrapper your deploy script sends is byte-for-byte the same every time even though the imported files have changed. Sending it on every deploy is the intended usage.
health_check_uri must be a relative path. An absolute URL returns 422 and nothing is applied:
{ "errors": ["Health check uri must be a relative path like '/example'"] }
Deploy an app
Queues a deploy of the app's configured branch.
POST /api/v1/apps/[APP_ID]/deploy
Parameter | Required | Description |
|---|---|---|
| No | Deploy a specific commit. Defaults to the latest commit on the app's branch. |
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/deploy \
-H "Authorization: Bearer [TOKEN]"
{ "id": 4212 }
The returned id is the log for this deploy. Deploys are queued, so a successful response means the deploy was accepted, not that it finished — fetch the log to follow it, and see Waiting for an operation to finish. If the app has no active servers:
{ "error": "No active servers available to deploy to" }
Restart an app
Restarts all of the app's processes.
POST /api/v1/apps/[APP_ID]/restart
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/restart \
-H "Authorization: Bearer [TOKEN]"
{ "id": 4213 }
The returned id is the log for this restart.
Enable auto-deploy
Registers a webhook on the git host and turns on automatic deploys. Requires a connected git provider.
POST /api/v1/apps/[APP_ID]/auto_deploy
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/auto_deploy \
-H "Authorization: Bearer [TOKEN]"
Returns 200 OK with the app object (auto_deploy now true). If the app has no connected git provider:
{ "error": "Auto deploy requires a connected git provider" }
If the git host rejects the webhook, the response is 502:
{ "error": "Could not enable auto deploy" }
Disable auto-deploy
Removes the webhook and turns off automatic deploys. Idempotent — succeeds even if auto-deploy wasn't on.
DELETE /api/v1/apps/[APP_ID]/auto_deploy
curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID]/auto_deploy \
-H "Authorization: Bearer [TOKEN]"
Returns 204 No Content.
Environment variables
Environment variable names are normalized to uppercase and may only contain letters, numbers, and underscores. Changing environment variables triggers an update on your servers. Values are never returned by the API.
Create environment variables
Adds new variables. All-or-nothing: if any variable is invalid, none are created. To change an existing variable, use Add or update — creating a duplicate name returns 422.
POST /api/v1/apps/[APP_ID]/env_vars
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/env_vars \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"env_vars": [{"name": "FOO", "value": "bar"}, {"name": "BAZ", "value": "qux"}]}'
[
{ "id": 91, "name": "FOO" },
{ "id": 92, "name": "BAZ" }
]
Add or update environment variables
Updates variables that already exist (matched by name) and creates any that don't. Include "_destroy": "1" to remove one in the same request.
PUT /api/v1/apps/[APP_ID]/env_vars
curl -X PUT https://hatchbox.io/api/v1/apps/[APP_ID]/env_vars \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"env_vars": [{"name": "FOO", "value": "new-value"}, {"name": "BAZ", "_destroy": "1"}]}'
Returns 200 OK with an empty body.
Remove environment variables
DELETE /api/v1/apps/[APP_ID]/env_vars
curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID]/env_vars \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"env_vars": ["FOO", "BAZ"]}'
Returns 200 OK with an empty body.
Domains
Domains are identified by their name — e.g. /domains/example.com. Wildcard domains use *.example.com.
List domains
GET /api/v1/apps/[APP_ID]/domains
[
{ "id": 5, "name": "example.com", "created_at": "2026-07-14T18:22:10.000Z", "updated_at": "2026-07-14T18:22:10.000Z" }
]
Get a domain
GET /api/v1/apps/[APP_ID]/domains/[DOMAIN_NAME]
Returns a single domain. If it isn't on this app: {"error": "Domain not found"}.
Add a domain
POST /api/v1/apps/[APP_ID]/domains
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/domains \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"domain": {"name": "example.com"}}'
Returns 201 Created with the domain. A domain already connected to another app in the same cluster returns 422:
{ "errors": ["Name is already connected to another App in this Cluster"] }
Update a domain
Renames an existing domain.
PATCH /api/v1/apps/[APP_ID]/domains/[DOMAIN_NAME]
curl -X PATCH https://hatchbox.io/api/v1/apps/[APP_ID]/domains/example.com \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"domain": {"name": "www.example.com"}}'
Returns 200 OK with the updated domain.
Remove a domain
DELETE /api/v1/apps/[APP_ID]/domains/[DOMAIN_NAME]
Returns 200 OK with an empty body.
Processes
Processes are identified by their numeric ID. Use List processes to find it.
List processes
GET /api/v1/apps/[APP_ID]/processes
[
{
"id": 15,
"name": "web",
"start_command": "bin/rails server",
"stop_command": "",
"reload_command": "",
"restart_on_deploy": true,
"server_id": 6,
"socket": false,
"systemd_type": "simple",
"active": true,
"roles": ["web"],
"created_at": "2026-07-02T17:08:01.887Z",
"updated_at": "2026-07-02T17:08:01.887Z"
}
]
Get a process
GET /api/v1/apps/[APP_ID]/processes/[PROCESS_ID]
Returns a single process. If it isn't on this app: {"error": "Process not found"}.
Restart a process
POST /api/v1/apps/[APP_ID]/processes/[PROCESS_ID]/restart
Returns 200 OK with an empty body. Restarts are queued and run in the background.
Cron jobs
Cron jobs are scheduled commands that run inside your app's current deploy, on every server in the cluster carrying the cron role.
Hatchbox writes all of an app's cron jobs into a single crontab file on those servers, so every change here rewrites the whole file. That rewrite is queued and runs in the background — a successful response means the change was saved, not that it has reached the servers yet.
An app whose cluster has no active cron server can't schedule anything, so creating or updating returns 422:
{ "error": "No active servers with the cron role in this cluster" }
Deleting is still allowed in that case, so a cluster that lost its cron server can be tidied up.
The cron job object
{
"id": 17,
"app_id": 1,
"name": "Nightly sync",
"run_at": "0 0 * * *",
"command": "bin/rails sync:nightly",
"appsignal": false,
"honeybadger_checkin_id": null,
"created_at": "2026-08-10T09:14:02.000Z",
"updated_at": "2026-08-10T09:14:02.000Z"
}
Field | Description |
|---|---|
| A label for the job. Also the name AppSignal reports it under. |
| When to run it. Any valid cron expression ( |
| The command to run. Executed from the app's current release directory as the |
| Whether to wrap the command in AppSignal's cron monitoring. See below. |
| A Honeybadger check-in ID to report to. |
Monitoring. Setting appsignal to true requires the app to be connected to AppSignal — the generated command embeds the app's push API key, and without one the job would run unmonitored with a malformed flag. Attempting it returns 422:
{ "errors": ["Appsignal monitoring requires connecting AppSignal to this app first"] }
honeybadger_checkin_id has no such requirement — create a check-in and pass its ID.
List cron jobs
GET /api/v1/apps/[APP_ID]/cron_jobs
curl https://hatchbox.io/api/v1/apps/[APP_ID]/cron_jobs \
-H "Authorization: Bearer [TOKEN]"
Returns an array of cron job objects.
Get a cron job
GET /api/v1/apps/[APP_ID]/cron_jobs/[CRON_JOB_ID]
curl https://hatchbox.io/api/v1/apps/[APP_ID]/cron_jobs/[CRON_JOB_ID] \
-H "Authorization: Bearer [TOKEN]"
Returns a single cron job object. A cron job belonging to a different app returns 404 {"error": "Cron job not found"}.
Create a cron job
POST /api/v1/apps/[APP_ID]/cron_jobs
Parameter | Required | Description |
|---|---|---|
| Yes | A label for the job. |
| Yes | Cron expression or shorthand. |
| Yes | The command to run. |
| No | Defaults to |
| No | A Honeybadger check-in ID. |
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/cron_jobs \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"cron_job": {"name": "Nightly sync", "run_at": "0 0 * * *", "command": "bin/rails sync:nightly"}}'
Returns 201 Created with the cron job. An unparseable schedule returns 422:
{ "errors": ["Run at must be a valid cron expression"] }
Update a cron job
PATCH /api/v1/apps/[APP_ID]/cron_jobs/[CRON_JOB_ID]
Accepts the same parameters as create; send only the fields you're changing.
curl -X PATCH https://hatchbox.io/api/v1/apps/[APP_ID]/cron_jobs/[CRON_JOB_ID] \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"cron_job": {"run_at": "@daily"}}'
Returns the updated cron job.
Delete a cron job
DELETE /api/v1/apps/[APP_ID]/cron_jobs/[CRON_JOB_ID]
curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID]/cron_jobs/[CRON_JOB_ID] \
-H "Authorization: Bearer [TOKEN]"
Returns 200 OK with an empty body.
Clusters
List clusters
Returns the clusters for a single account, sorted by name.
GET /api/v1/accounts/[ACCOUNT_ID]/clusters
curl https://hatchbox.io/api/v1/accounts/[ACCOUNT_ID]/clusters \
-H "Authorization: Bearer [TOKEN]"
[
{
"id": 12,
"name": "my-cluster",
"provider": "digitalocean",
"region": "nyc3",
"servers_count": 2
}
]
An account you're not a member of returns 404 {"error": "Account not found"}.
Get a cluster
Returns a single cluster with its servers included, so you can render a cluster in one request. See the server object for the shape of each entry.
GET /api/v1/clusters/[CLUSTER_ID]
curl https://hatchbox.io/api/v1/clusters/[CLUSTER_ID] \
-H "Authorization: Bearer [TOKEN]"
{
"id": 12,
"name": "my-cluster",
"provider": "digitalocean",
"region": "nyc3",
"servers_count": 2,
"servers": [
{
"id": 34,
"name": "web-1",
"state": "active",
"roles": ["web", "worker"],
"cluster_id": 12,
"provider_id": "405177344",
"public_ip": "203.0.113.10",
"public_ipv6": null,
"private_ip": "10.0.0.5",
"ssh_port": 22,
"size": "s-2vcpu-4gb",
"ubuntu_version": "noble",
"last_configured_at": "2026-07-14T09:12:44.000Z"
}
]
}
The servers array is sorted by name and contains every server in the cluster. A cluster on an account you're not a member of returns 404 {"error": "Cluster not found"}.
Servers
A server is one machine in a cluster. Its roles determine what runs on it — your app's web, worker, and cron processes, a load balancer, and/or a database engine.
Servers are included in Get a cluster, so you usually don't need these endpoints to list them. Use them when you want a single server on its own.
The server object
{
"id": 34,
"name": "web-1",
"state": "active",
"roles": ["web", "worker"],
"cluster_id": 12,
"provider_id": "405177344",
"public_ip": "203.0.113.10",
"public_ipv6": null,
"private_ip": "10.0.0.5",
"ssh_port": 22,
"size": "s-2vcpu-4gb",
"ubuntu_version": "noble",
"last_configured_at": "2026-07-14T09:12:44.000Z"
}
Field | Description |
|---|---|
| One of |
| What the server does. Any of |
| The cluster the server belongs to. |
| The server's id at your hosting provider, for cross-referencing with your own tooling. |
| The server's addresses. |
|
|
| The SSH port Hatchbox connects on, |
| The provider's size slug, e.g. |
| The OS codename, e.g. |
| When Hatchbox last configured the server. |
List servers
Returns every server in a cluster, sorted by name.
GET /api/v1/clusters/[CLUSTER_ID]/servers
curl https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers \
-H "Authorization: Bearer [TOKEN]"
Returns an array of server objects. A cluster on an account you're not a member of returns 404 {"error": "Cluster not found"}.
Get a server
GET /api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]
curl https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID] \
-H "Authorization: Bearer [TOKEN]"
Returns a single server object. A server that exists but lives in a different cluster returns 404 {"error": "Server not found"} — the ID has to match the cluster in the path.
Git providers
Git providers are the connected git accounts (GitHub, GitLab, Bitbucket) for one of your accounts. Use one's id as connected_account_id when creating an app.
List git providers
Lists the git providers for a single account.
GET /api/v1/accounts/[ACCOUNT_ID]/git_providers
curl https://hatchbox.io/api/v1/accounts/[ACCOUNT_ID]/git_providers \
-H "Authorization: Bearer [TOKEN]"
[
{ "id": 5, "provider": "github", "name": "my-org" }
]
An account you're not a member of returns 404 {"error": "Account not found"}.
Database clusters
A database cluster is the database server your individual databases live on — either one Hatchbox installed on your own server (unmanaged) or one hosted by your provider (managed).
List database clusters
Returns the database clusters for a single account, sorted by name.
GET /api/v1/accounts/[ACCOUNT_ID]/database_clusters
curl https://hatchbox.io/api/v1/accounts/[ACCOUNT_ID]/database_clusters \
-H "Authorization: Bearer [TOKEN]"
[
{
"id": 8,
"name": "my-database-cluster",
"engine": "postgresql",
"version": "17",
"region": "nyc3",
"managed": true,
"active": true,
"databases_count": 3
}
]
Field | Description |
|---|---|
| One of |
| The engine version, when known. |
| The provider region for managed clusters; |
|
|
|
|
| How many databases are on the cluster. |
An account you're not a member of returns 404 {"error": "Account not found"}.
Databases
A database lives on a database cluster and can be attached to any number of apps.
These responses contain live credentials. Every database on a networked engine includes its username, password, and fully-formed connection URIs. Treat them the way you'd treat any other secret — don't log them, and don't cache them anywhere you wouldn't cache a password.
The database object
{
"id": 42,
"name": "my_app_production",
"engine": "postgresql",
"database_cluster_id": 8,
"username": "my_app",
"password": "sup3rs3cret",
"private_connection_uri": "postgresql://my_app:sup3rs3cret@10.0.0.5:5432/my_app_production",
"public_connection_uri": "postgresql://my_app:sup3rs3cret@203.0.113.10:5432/my_app_production",
"app_attachments": [
{ "app_id": 1, "env_var": "DATABASE_URL" },
{ "app_id": 4, "env_var": "RED_DATABASE_URL" }
]
}
Field | Description |
|---|---|
| Inherited from the cluster: |
| The database's credentials. For Redis, Memcached, and Elasticsearch these are the cluster's master credentials. Not present for SQLite. |
| Connection URI over the private network. Present for every engine except SQLite. |
| Connection URI over the public internet. Only present for databases on managed clusters — the key is omitted entirely otherwise, since an unmanaged cluster's "public" host is just your server's IP. |
| SQLite only. The absolute path to the database file on your server. Replaces the credentials and connection URIs above, which don't apply to a local file. |
| One entry per app this database is attached to. Empty when it isn't attached to anything. |
Attaching a database to an app works by setting an environment variable on that app, so each entry in app_attachments reports both the app and the variable it's reachable through:
Field | Description |
|---|---|
| The app the database is attached to. |
| The name of the environment variable on that app holding the connection URI — usually |
URI schemes follow the engine — postgresql://, mysql2://, redis://, and HTTPS for Elasticsearch. Clusters with SSL enabled append ?sslmode=require. SQLite is the exception: the variable holds the file path rather than a URI.
SQLite databases
A SQLite database is a file on one of your servers rather than a networked service, so its object is shaped differently — path instead of credentials and connection URIs:
{
"id": 51,
"name": "my-app-production",
"engine": "sqlite",
"database_cluster_id": 12,
"path": "/home/deploy/my-app-production.sqlite3",
"app_attachments": [
{ "app_id": 1, "env_var": "DATABASE_PATH" }
]
}
Everything else works the same way: they're listed and read through the same endpoints, attached and detached the same way, and backed up the same way. The differences worth knowing:
pathis required when you create one. Bothnameandpathcan be changed afterwards — SQLite databases are the only ones the API lets you update at all.- Attaching one sets
DATABASE_PATHto the file path instead of a connection URI. - There's nothing to provision, so a new SQLite database is usable immediately rather than after a background job.
List databases
Returns every database on a database cluster, sorted by name — attached to an app or not.
GET /api/v1/database_clusters/[DATABASE_CLUSTER_ID]/databases
curl https://hatchbox.io/api/v1/database_clusters/[DATABASE_CLUSTER_ID]/databases \
-H "Authorization: Bearer [TOKEN]"
Returns an array of database objects. A cluster on an account you're not a member of returns 404 {"error": "Database cluster not found"}.
Get a database
GET /api/v1/database_clusters/[DATABASE_CLUSTER_ID]/databases/[DATABASE_ID]
curl https://hatchbox.io/api/v1/database_clusters/[DATABASE_CLUSTER_ID]/databases/[DATABASE_ID] \
-H "Authorization: Bearer [TOKEN]"
Returns a single database object. A database that exists but lives on a different cluster returns 404 {"error": "Database not found"} — the ID has to match the cluster in the path.
Create a database
Creates a database on a cluster. Creating and attaching are separate steps: a new database isn't connected to any app until you attach it.
POST /api/v1/database_clusters/[DATABASE_CLUSTER_ID]/databases
Parameter | Required | Description |
|---|---|---|
| No | Defaults to a generated name like |
| SQLite only | The absolute path to the database file on your server, e.g. |
curl -X POST https://hatchbox.io/api/v1/database_clusters/[DATABASE_CLUSTER_ID]/databases \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"database": {"name": "my_app_production"}}'
The body is optional — curl -X POST ... -H "Authorization: Bearer [TOKEN]" on its own creates a database with a generated name, username, and password. On a SQLite cluster the body is required, since path has no default.
Returns 201 Created with the database object, including its credentials and connection URIs. The database is created on your server in the background, so it may take a moment before it accepts connections. SQLite databases are the exception — nothing is provisioned, so they're ready as soon as the call returns.
A cluster on an account you're not a member of returns 404 {"error": "Database cluster not found"}. A managed cluster that's still provisioning returns 422 {"error": "Database cluster is not active yet"} — wait until the cluster reports "active": true. Validation failures return 422 with errors, including a missing or relative path on a SQLite cluster.
Update a database
SQLite databases only. Every other engine returns 422 {"error": "Only SQLite databases can be updated"} — their names and credentials are fixed once created.
PATCH /api/v1/database_clusters/[DATABASE_CLUSTER_ID]/databases/[DATABASE_ID]
Parameter | Required | Description |
|---|---|---|
| No | A new name for the database. |
| No | A new absolute path for the database file. |
curl -X PATCH https://hatchbox.io/api/v1/database_clusters/[DATABASE_CLUSTER_ID]/databases/[DATABASE_ID] \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"database": {"path": "/home/deploy/moved.sqlite3"}}'
Returns 200 OK with the updated database object.
Changing path rewrites the environment variable on every app the database is attached to, and writes it out to the servers of any app that has deployed before — so attached apps follow the file rather than breaking. If the database has backups enabled, its backup configuration is rewritten on the server too.
Note that this changes where Hatchbox looks for the file; it doesn't move the file for you. Move it on the server yourself, or point the path at a file that already exists.
Validation failures return 422 with errors — a path has to be absolute and can't be blank.
List an app's databases
Returns the databases currently attached to an app, sorted by name. This is the app-side view of the same objects: the cluster endpoints above list everything on a cluster whether or not it's attached to anything, while these two are limited to what the app can actually reach.
GET /api/v1/apps/[APP_ID]/databases
curl https://hatchbox.io/api/v1/apps/[APP_ID]/databases \
-H "Authorization: Bearer [TOKEN]"
Returns an array of database objects — the same shape as the database object. An app you can't access returns 404 {"error": "App not found"}.
Get an app's database
GET /api/v1/apps/[APP_ID]/databases/[DATABASE_ID]
curl https://hatchbox.io/api/v1/apps/[APP_ID]/databases/[DATABASE_ID] \
-H "Authorization: Bearer [TOKEN]"
Returns a single database object. A database that isn't attached to this app returns 404 {"error": "Database not found"}, even when you can read that same database through its cluster — this endpoint answers "is this database attached to this app," so an unattached one is indistinguishable from one that doesn't exist.
Attach a database to an app
Attaching a database sets an environment variable on the app holding the connection URI. The app picks up the new variable on its next deploy; if it has deployed before, Hatchbox also writes the variable to your servers right away.
POST /api/v1/apps/[APP_ID]/databases/[DATABASE_ID]/attachment
The database has to belong to the same account as the app, but it does not have to be on the same cluster.
Parameter | Required | Description |
|---|---|---|
| No | The environment variable to attach it as. Defaults to the engine's usual name — |
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/databases/[DATABASE_ID]/attachment \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"env_var": "REPORTING_DATABASE_URL"}'
Returns 201 Created with the database object. Read app_attachments in the response to learn the variable you actually got — the two names behave differently when one is already in use:
- Without
env_var, a default name that's taken gets a colour prefix instead of failing, so a secondDATABASE_URLbecomesRED_DATABASE_URL, thenBLUE_DATABASE_URL. This is what lets an app hold connections to several databases at once. - With
env_var, a name that's already taken on the app returns422 {"errors": ["Name has already been taken"]}. A name you asked for is never silently changed.
Names must match [A-Z_][A-Z0-9_]*; anything else returns 422 with errors. A database on another account returns 404 {"error": "Database not found"}, and an app you can't access returns 404 {"error": "App not found"}.
Detach a database from an app
Removes the environment variable (or variables) connecting the database to the app, and updates the app's servers. The database itself is untouched and stays on its cluster.
DELETE /api/v1/apps/[APP_ID]/databases/[DATABASE_ID]/attachment
curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID]/databases/[DATABASE_ID]/attachment \
-H "Authorization: Bearer [TOKEN]"
Returns 200 OK. If the database is attached to the app more than once — say as both DATABASE_URL and REPORTING_DATABASE_URL — this removes every one of those attachments. Detaching a database that isn't attached also returns 200 OK, so the call is safe to repeat.
The two backup endpoints below are addressed by database ID alone, without the cluster. They require a database whose backups are enabled and stored on Amazon S3 or an S3-compatible provider. Managed databases aren't supported (back them up on your hosting provider).
Download the latest backup
Returns a temporary, presigned URL to the most recent backup. Fetch the URL to download the backup directly from your storage provider; it expires a few minutes after it's issued.
GET /api/v1/databases/[DATABASE_ID]/backups/latest
curl https://hatchbox.io/api/v1/databases/[DATABASE_ID]/backups/latest \
-H "Authorization: Bearer [TOKEN]"
{
"url": "https://your-bucket.s3.amazonaws.com/...",
"last_backup_at": "2026-07-16T03:00:00.000Z"
}
Download it with the returned URL: curl -o backup.tar.gz "[URL]".
If no downloadable backup is available, the response is 422 with one of:
{ "error": "Managed databases should be backed up on your hosting provider" }
{ "error": "Backups are not enabled for this database" }
{ "error": "No backup has completed for this database yet" }
{ "error": "Downloading backups is only supported for S3 and S3-compatible providers" }
If the backup can't be reached at your storage provider (for example, expired credentials), the response is 502:
{ "error": "Could not generate a download URL from your backup provider" }
Trigger a backup
Queues a new backup of the database.
POST /api/v1/databases/[DATABASE_ID]/backups
curl -X POST https://hatchbox.io/api/v1/databases/[DATABASE_ID]/backups \
-H "Authorization: Bearer [TOKEN]"
{ "id": 4214 }
The returned id is the log for this backup. Backups are queued, so a successful response means it was accepted, not that it finished — fetch the log to find out whether it did.
If a backup is already running for this database, the response is 409:
{ "error": "A backup is already running for this database" }
Backups not enabled, or a managed database, return 422 with the corresponding message shown above.
Logs
Every queued operation writes a log. Deploy an app, Restart an app, and Trigger a backup each return the id of the log they created, which is how you find out whether the work actually succeeded — those endpoints return as soon as the job is accepted, long before it finishes.
The log object
{
"id": 4212,
"name": "Apps::Deploy",
"description": "Add the logs endpoint",
"state": "completed",
"parent_id": null,
"loggable_type": "App",
"loggable_id": 91,
"commit_sha": "0d1e2f3a4b5c6d7e8f90a1b2c3d4e5f6a7b8c9d0",
"username": "Jane Doe",
"body": "-----> Starting deployment\n-----> Deploying 0d1e2f3 from main branch\n",
"created_at": "2026-08-04T20:31:45.000Z",
"started_at": "2026-08-04T20:31:46.000Z",
"completed_at": "2026-08-04T20:33:02.000Z",
"child_logs": []
}
Field | Description |
|---|---|
| The operation that produced the log, e.g. |
| A one-line summary. For deploys this is the commit's subject line. |
| One of |
| The parent log's id on a child log, |
| What the operation ran against. One of |
| The commit that was deployed. |
| Who started it — a Hatchbox user's name, or the git username for a push-triggered auto-deploy. |
| The output. Retains ANSI colour codes, so it prints as-is in a terminal. |
|
|
| Per-server output. See below. |
Where the output lives
An operation that touches several servers opens one SSH connection per server and writes that server's output to its own child log. The parent's body holds only the high-level narrative:
-----> Starting deployment
-----> Deploying 0d1e2f3 from main branch as 20260804203145 release
-----> Building on web-1
The actual command output — git, bundle install, asset compilation, and the error when a deploy fails — is in child_logs. If you're reporting why something failed, read the children, not the parent.
Backups are the exception: they run against a single server and write everything to the parent, so child_logs comes back empty.
Each child is a complete log object. Its loggable_type is Server, naming the machine the output came from, and its id works at GET /api/v1/logs/[LOG_ID] if you'd rather fetch one on its own. Logs nest exactly one level, so children carry no child_logs of their own.
Because the children come back with their bodies, a deploy across several servers can be a large response.
Get a log
GET /api/v1/logs/[LOG_ID]
curl https://hatchbox.io/api/v1/logs/[LOG_ID] \
-H "Authorization: Bearer [TOKEN]"
Returns a single log object with its children embedded.
You can read any log on an account you belong to, including operations a teammate started and auto-deploys triggered by a push. A log on an account you're not a member of returns 404 {"error": "Log not found"}.
Waiting for an operation to finish
Poll the log until state leaves pending/processing:
LOG_ID=$(curl -sX POST https://hatchbox.io/api/v1/apps/[APP_ID]/deploy \
-H "Authorization: Bearer $TOKEN" | jq -r .id)
while :; do
LOG=$(curl -s https://hatchbox.io/api/v1/logs/$LOG_ID -H "Authorization: Bearer $TOKEN")
STATE=$(echo "$LOG" | jq -r .state)
case $STATE in
completed) echo "Deployed."; break ;;
failed|aborted) echo "$LOG" | jq -r '.child_logs[].body'; exit 1 ;;
*) sleep 5 ;;
esac
done