All Collections Hatchbox API API Endpoints

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

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

{"error": "An active subscription is required for ..."}

404 Not Found

The record doesn't exist, or your token can't access it

{"error": "App not found"}

422 Unprocessable Content

Validation failed

{"errors": ["..."]} or {"error": "..."}

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

cluster_id

Yes

The cluster to create the app on (see List clusters).

name

Yes

Letters, numbers, hyphens, and underscores only.

branch

No

Defaults to main.

repo_path

No

e.g. myuser/my-app.

connected_account_id

No

The git provider to deploy from (see List git providers). Leave blank for a public/custom git host.

pre_build_script, build_script, post_build_script, post_deploy_script, failed_deploy_script

No

Deploy lifecycle scripts.

dns_provider, dns_access_token, dns_api_user

No

DNS credentials.

caddyfile, health_check_uri

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

caddyfile

The app's Caddy configuration.

health_check_uri

A relative path for Caddy to health-check, e.g. /up.

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

sha

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

name

A label for the job. Also the name AppSignal reports it under.

run_at

When to run it. Any valid cron expression (0 0 * * *, 0,30 6/4 * * 1-5) or shorthand (@hourly, @daily, @weekly, @monthly).

command

The command to run. Executed from the app's current release directory as the deploy user.

appsignal

Whether to wrap the command in AppSignal's cron monitoring. See below.

honeybadger_checkin_id

A Honeybadger check-in ID to report to. null to disable.

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

name

Yes

A label for the job.

run_at

Yes

Cron expression or shorthand.

command

Yes

The command to run.

appsignal

No

Defaults to false. Requires AppSignal connected to the app.

honeybadger_checkin_id

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

state

One of pending, creating, created, active, destroyed. A server only runs your apps once it's active.

roles

What the server does. Any of web, worker, cron, load_balancer, app (custom processes), and the database engines postgresql, mysql, redis, memcached, elasticsearch. A server can hold several.

cluster_id

The cluster the server belongs to.

provider_id

The server's id at your hosting provider, for cross-referencing with your own tooling. null for servers Hatchbox didn't create there.

public_ip, private_ip

The server's addresses. private_ip is what apps use to reach databases in the same cluster.

public_ipv6

null unless the provider assigned one.

ssh_port

The SSH port Hatchbox connects on, 22 unless you changed it.

size

The provider's size slug, e.g. s-2vcpu-4gb. null for servers you brought yourself.

ubuntu_version

The OS codename, e.g. noble or jammy. Despite the name this can also be a Debian release such as bookworm.

last_configured_at

When Hatchbox last configured the server. null if it never has.

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

engine

One of postgresql, mysql, redis, memcached, elasticsearch, sqlite.

version

The engine version, when known.

region

The provider region for managed clusters; null for unmanaged ones.

managed

true when the cluster is hosted by your provider, false when it runs on your own server.

active

false while a managed cluster is still being provisioned and isn't reachable yet. Unmanaged clusters are always true.

databases_count

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

engine

Inherited from the cluster: postgresql, mysql, redis, memcached, elasticsearch, or sqlite.

username, password

The database's credentials. For Redis, Memcached, and Elasticsearch these are the cluster's master credentials. Not present for SQLite.

private_connection_uri

Connection URI over the private network. Present for every engine except SQLite.

public_connection_uri

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.

path

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.

app_attachments

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

app_id

The app the database is attached to.

env_var

The name of the environment variable on that app holding the connection URI — usually DATABASE_URL (or REDIS_URL, MEMCACHE_SERVERS, ELASTICSEARCH_URL, DATABASE_PATH by engine). An app that already has a variable of that name gets a colour-prefixed one instead, like RED_DATABASE_URL, so read this rather than assuming the default name.

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:

  • path is required when you create one. Both name and path can be changed afterwards — SQLite databases are the only ones the API lets you update at all.
  • Attaching one sets DATABASE_PATH to 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

name

No

Defaults to a generated name like db_9f2c1a4b8e07. Ignored for Redis, Memcached, and Elasticsearch, which name their databases themselves.

path

SQLite only

The absolute path to the database file on your server, e.g. /home/deploy/my-app-production.sqlite3. Required on SQLite clusters and ignored everywhere else.

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

name

No

A new name for the database.

path

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

env_var

No

The environment variable to attach it as. Defaults to the engine's usual name — DATABASE_URL, REDIS_URL, MEMCACHE_SERVERS, ELASTICSEARCH_URL, or DATABASE_PATH for SQLite. Lowercase input is upcased for you.

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 second DATABASE_URL becomes RED_DATABASE_URL, then BLUE_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 returns 422 {"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_URLthis 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

name

The operation that produced the log, e.g. Apps::Deploy, Apps::Restart, Servers::Databases::Backups::Run.

description

A one-line summary. For deploys this is the commit's subject line. null for most other operations.

state

One of pending, processing, completed, failed, aborted.

parent_id

The parent log's id on a child log, null on a top-level one.

loggable_type, loggable_id

What the operation ran against. One of App, Server, Cluster, DatabaseCluster, Database, Script.

commit_sha

The commit that was deployed. null for anything that isn't a deploy.

username

Who started it — a Hatchbox user's name, or the git username for a push-triggered auto-deploy. null when nothing recorded it.

body

The output. Retains ANSI colour codes, so it prints as-is in a terminal.

started_at, completed_at

null until the operation starts and finishes. A log that failed still gets a completed_at.

child_logs

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