All Collections Hatchbox API API Endpoints

API Endpoints

Learn how to use the Hatchbox REST API Endpoints

Updated

API Endpoints

Learn how to use the Hatchbox REST API Endpoints

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 and manage 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, whether you queued it or a git push did — all from your own scripts and tooling.

Table of contents

  • Authentication
  • Base URL
  • Active subscription required
  • Finding IDs
  • Errors
  • Pagination
  • Accounts
    • List your accounts
  • Apps
    • The app object
    • List apps
    • Get an app
    • Create an app
    • Update an app
    • Caddy configuration
    • Delete an app
    • Deploy an app
    • Restart an app
    • Enable auto-deploy
    • Disable auto-deploy
  • Environment variables
    • Create environment variables
    • Add or update environment variables
    • Remove environment variables
  • Domains
    • List domains
    • Get a domain
    • Add a domain
    • Update a domain
    • Remove a domain
  • Processes
    • The process object
    • List processes
    • Get a process
    • Create a process
    • Update a process
    • Delete a process
    • Restart a process
    • Enable a process
    • Disable a process
  • Cron jobs
    • The cron job object
    • List cron jobs
    • Get a cron job
    • Create a cron job
    • Update a cron job
    • Delete a cron job
  • Clusters
    • List clusters
    • Get a cluster
  • Servers
    • The server object
    • List servers
    • Get a server
    • Update server roles
    • Provision a server
    • Reboot a server
    • The firewall rule object
    • List firewall rules
    • Get a firewall rule
    • Add a firewall rule
    • Remove a firewall rule
  • Git providers
    • List git providers
  • Database clusters
    • List database clusters
  • Databases
    • The database object
    • SQLite databases
    • List databases
    • Get a database
    • Create a database
    • Update a database
    • List an app's databases
    • Get an app's database
    • Attach a database to an app
    • Detach a database from an app
    • The backup configuration object
    • Get the backup configuration
    • Enable or update backups
    • Disable backups
    • Test the backup connection
    • Trigger a backup
    • Download the latest backup
  • Logs
    • The log object
    • Where the output lives
    • Get a log
    • List an app's logs
    • Waiting for an operation to finish

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.
  • Process ID — use List processes for an app.
  • Firewall rule ID — use List firewall rules for a server.
  • Log ID — returned by the operation that created it. For an operation you didn't queue yourself — an auto-deploy from a git push, say — use List an app's logs.

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.




Pagination

Endpoints that can return an unbounded number of records are paginated. They still return a plain JSON array — the page state comes back in response headers:

Header

Description

current-page

The page you're on.

page-limit

Records per page.

total-count

Records across all pages.

total-pages

The last page number.

link

RFC 8288 links to the first, previous, next, and last pages. previous and next are absent at the ends.

Control it with two query parameters:


Parameter

Description

page

Defaults to 1. A page past the end returns an empty array, not an error.

limit

Records per page. Defaults to 20, capped at 100.

curl "https://hatchbox.io/api/v1/apps/[APP_ID]/logs?page=2&limit=50" \
-H "Authorization: Bearer [TOKEN]"

Follow link rather than incrementing page yourself if you're walking a whole collection — it's the stable way to know when to stop:

URL="https://hatchbox.io/api/v1/apps/[APP_ID]/logs?limit=100"

while [ -n "$URL" ]; do
BODY=$(curl -sD /tmp/h "$URL" -H "Authorization: Bearer $TOKEN")
echo "$BODY" | jq -r '.[] | "\(.state)\t\(.name)"'
URL=$(grep -i '^link:' /tmp/h | tr ',' '\n' | grep 'rel="next"' | sed -e 's/.*<//' -e 's/>.*//')
done

Endpoints not marked as paginated return every record.


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'"] }

Delete an app

Deletes an app and removes it from your servers. This runs the following, in order:

  • Deletes the app's folder and its contents from all the servers
  • Stops the app's processes and completely removes them from all the servers
  • Detaches the app's databases — the databases themselves are not deleted

This cannot be undone. To confirm, send the app's own name in the request body. A request without it, or with a name that doesn't match, is refused.

DELETE /api/v1/apps/[APP_ID]

Parameter

Required

Description

name

Yes

Must exactly match the app's current name.

curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID] \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"name": "my-app"}'
{ "id": 4320 }

The returned id is the log for the deletion. The work is queued, so a successful response means it was accepted, not that it finished — the app record still exists at the moment you get this response and is removed once the job reaches that step.

If the name doesn't match, the response is 422 and nothing is queued:

{ "error": "App name does not match. Resend with the app's name to confirm deletion." }

The expected name is deliberately not echoed back, so fetch the app first if you need it.

If a deletion is already running for this app, the response is 409:

{ "error": "App is already being deleted" }

Following the log. Logs belong to the app that produced them, so this log is deleted along with the app. Expect state to reach completed and then, moments later, for GET /api/v1/logs/[LOG_ID] to start returning 404 {"error": "Log not found"} — that 404 is the normal end state for this operation, not an error. Treat either a completed state or a subsequent 404 as success, and a failed state as failure.

Avoid deploying the app while a deletion is in flight. A deploy that starts mid-deletion can recreate the app's files and services after the deletion has removed them, leaving processes on your servers that Hatchbox no longer tracks.

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

A process is a long-running command Hatchbox runs for your app as a systemd service — your web server, background workers, and anything else that needs to stay up. Processes are identified by their numeric ID; use List processes to find it.

Every write here is queued and runs in the background, so a successful response means the change was saved, not that it has reached your servers yet. Each one returns the id of a log you can follow.

The process object

{
"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"],
"appsignal": false,
"appsignal_options": null,
"created_at": "2026-07-02T17:08:01.887Z",
"updated_at": "2026-07-02T17:08:01.887Z"
}

Field

Description

name

Letters, numbers, hyphens, and underscores only. Unique within the app. Combined with the app name to form the systemd unit name.

start_command

The command to run. Required.

stop_command, reload_command

Optional commands for systemd to use when stopping or reloading the service.

restart_on_deploy

Whether the process is restarted on every deploy. Defaults to true.

roles

Run the process on every server carrying any of these roles, e.g. ["web"].

server_id

Run the process on one specific server instead. Mutually exclusive with roles — set one or the other, never both.

socket

Whether to use systemd socket activation. At most one process per app can have this on.

systemd_type

simple or oneshot. Defaults to simple.

active

Whether the process is currently enabled. Change it with Enable a process / Disable a process rather than through update.

appsignal

Whether to run the process under AppSignal's monitoring wrapper. See below.

appsignal_options

Extra flags passed to appsignal-wrap, e.g. --heartbeat.

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 process would launch with a malformed flag. Attempting it returns 422:


{ "errors": ["Appsignal monitoring requires connecting AppSignal to this app first"] }

List processes

GET /api/v1/apps/[APP_ID]/processes
curl https://hatchbox.io/api/v1/apps/[APP_ID]/processes \
-H "Authorization: Bearer [TOKEN]"

Returns an array of process objects.

Get a process

GET /api/v1/apps/[APP_ID]/processes/[PROCESS_ID]
curl https://hatchbox.io/api/v1/apps/[APP_ID]/processes/[PROCESS_ID] \
-H "Authorization: Bearer [TOKEN]"

Returns a single process object. A process that isn't on this app returns 404 {"error": "Process not found"}.

Create a process

POST /api/v1/apps/[APP_ID]/processes

Parameter

Required

Description

name

Yes

Letters, numbers, hyphens, and underscores only.

start_command

Yes

The command to run.

roles

One of these

Array of server roles to run on.

server_id

One of these

A single server to run on. Can't be combined with roles.

stop_command, reload_command

No

Optional systemd commands.

restart_on_deploy

No

Defaults to true.

socket

No

Socket activation. At most one per app.

systemd_type

No

simple or oneshot. Defaults to simple.

appsignal, appsignal_options

No

AppSignal monitoring. Requires AppSignal connected to the app.

curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/processes \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"process": {"name": "worker", "start_command": "bundle exec sidekiq", "roles": ["worker"]}}'

Returns 201 Created with the process object plus the id of the log for installing it on your servers:

{
"id": 16,
"name": "worker",
"...": "...",
"log_id": 4330
}

Validation failures return 422 with errors — a duplicate name on the same app, a missing start_command, or both roles and server_id set at once:

{ "errors": ["Only one option can be selected between roles and server"] }

Update a process

Accepts the same parameters as create; send only the fields you're changing. To enable or disable a process, use the endpoints below rather than sending active.

PATCH /api/v1/apps/[APP_ID]/processes/[PROCESS_ID]
curl -X PATCH https://hatchbox.io/api/v1/apps/[APP_ID]/processes/[PROCESS_ID] \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"process": {"start_command": "bundle exec sidekiq -c 10"}}'

Returns 200 OK with the updated process object and a log_id for applying the change.

Renaming a process changes the name of its systemd unit. Hatchbox removes the old unit as part of the same operation, so a rename doesn't leave a stray service behind — but the process is stopped and restarted under its new name, so expect a brief interruption.

Delete a process

Stops the process, removes its systemd unit from every server it runs on, and deletes the record. If this was the app's last active web process, the app's Caddy configuration is rewritten too.

DELETE /api/v1/apps/[APP_ID]/processes/[PROCESS_ID]
curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID]/processes/[PROCESS_ID] \
-H "Authorization: Bearer [TOKEN]"
{ "id": 4331 }

The returned id is the log for the removal. The process record still exists at the moment you get this response and is deleted once the job has torn down the units.

Restart a process

POST /api/v1/apps/[APP_ID]/processes/[PROCESS_ID]/restart
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/processes/[PROCESS_ID]/restart \
-H "Authorization: Bearer [TOKEN]"
{ "id": 4332 }

The returned id is the log for the restart.

Enable a process

Enables the process and starts its systemd unit on every server it runs on.

POST /api/v1/apps/[APP_ID]/processes/[PROCESS_ID]/activation
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/processes/[PROCESS_ID]/activation \
-H "Authorization: Bearer [TOKEN]"

Returns 200 OK with the process object (active now true) and a log_id:

{
"id": 15,
"name": "web",
"active": true,
"...": "...",
"log_id": 4333
}

Safe to repeat. Enabling a process that is already enabled changes nothing, queues nothing, and returns "log_id": null — it will not restart a healthy process.

Disable a process

Stops the process and disables its systemd unit, leaving the record in place so it can be re-enabled later.

DELETE /api/v1/apps/[APP_ID]/processes/[PROCESS_ID]/activation
curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID]/processes/[PROCESS_ID]/activation \
-H "Authorization: Bearer [TOKEN]"

Returns 200 OK with the process object (active now false) and a log_id. As with enabling, disabling an already-disabled process is a no-op and returns "log_id": null.


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.

Update server roles

Sets which roles a server carries. This is a desired-state endpoint: send the complete list of roles the server should end up with, and Hatchbox works out what to add and what to remove. Roles you leave out are removed.

PATCH /api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/roles

Parameter

Required

Description

roles

Yes

The full list of roles the server should have.

confirm_destructive

No

Required when the change destroys data. See below.

Assignable roles are app, web, worker, cron, load_balancer, and the database engines postgresql, mysql, redis, memcached, elasticsearch. SQLite isn't assignable — SQLite database clusters are created as a byproduct of the app code roles.



curl -X PATCH https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/roles \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"server": {"roles": ["web", "worker", "cron"]}}'

Returns 200 OK with what changed and the logs for applying it:

{
"added_roles": ["cron"],
"removed_roles": ["load_balancer"],
"logs": [
{ "id": 4340, "name": "Servers::AddRoles" },
{ "id": 4341, "name": "Servers::RemoveRoles" }
]
}

Additions and removals run as separate operations, so a request that does both returns two logs. A request that changes nothing returns empty arrays and an empty logs array.

Adding an app code role (app, web, worker, cron) triggers a deploy of every app in the cluster, so the server comes up running current code.

An unrecognized role returns 422 and nothing is queued:

{ "error": "Unknown roles: databse. Assignable roles are app, cron, elasticsearch, load_balancer, memcached, mysql, postgresql, redis, web, worker" }

Destructive changes. Removing a database engine role uninstalls that engine from the machine and destroys the database clusters running on it. Dropping the last app code role also destroys the server's SQLite database clusters — so removing cron alone can take data with it even though cron itself is harmless.

When a request would do either, it's refused with 409 and a description of what would be lost:

{
"error": "This change destroys data. Resend with confirm_destructive: true to proceed.",
"destructive_changes": {
"roles": ["postgresql"],
"database_clusters": [
{ "id": 8, "name": "my-database-cluster", "engine": "postgresql" }
]
}
}

Resend the identical request with "confirm_destructive": true at the top level of the body to proceed:

curl -X PATCH https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/roles \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"server": {"roles": ["web"]}, "confirm_destructive": true}'

An engine removal is treated as destructive even when no database clusters are on it, because the engine is still uninstalled from the machine.

If the server is still being created, or is in the middle of provisioning, the response is 409:

{ "error": "Server is currently being created" }
{ "error": "Server is currently being provisioned" }

These are checked before the destructive-change prompt, so a destructive request against a server in either state reports the state rather than the confirmation.

If the server has no address to connect to, the response is 422:

{ "error": "Server has no IP address to connect to" }

Provision a server

Re-runs provisioning on a server that already exists: applies configuration updates, reinstalls the dependencies its roles require, and brings it in line with the current Hatchbox setup. This is the Update button on a server's page in the dashboard.

POST /api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/provision
curl -X POST https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/provision \
-H "Authorization: Bearer [TOKEN]"
{ "id": 4310 }

The returned id is the log for the run. Provisioning is queued and takes several minutes, so a successful response means the work was accepted, not that it finished — fetch the log to follow along.

Processes may restart while updates are applied, which can cause brief downtime. Prefer running this during low-traffic periods.

If the server is still being created, the response is 409:

{ "error": "Server is currently being created" }

If provisioning is already running for the server, the response is 409:

{ "error": "Provisioning is already running for this server" }

If the server record exists but hasn't been created at your cloud provider yet, the response is 422:

{ "error": "This server has not been created on its cloud provider yet" }

This endpoint only re-provisions servers that already exist. Creating the underlying machine at your provider isn't available through the API — do that from the dashboard first.

Reboot a server

Restarts the machine. Your apps are unavailable until it comes back up.

POST /api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/reboot
curl -X POST https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/reboot \
-H "Authorization: Bearer [TOKEN]"
{ "id": 4311 }

The returned id is the log for the reboot.

If the server is still being created, or is in the middle of provisioning, the response is 409:

{ "error": "Server is currently being created" }
{ "error": "Server is currently being provisioned" }

Rebooting during provisioning is refused because interrupting a package install can leave the server needing manual repair. Wait for the provisioning log to finish, then retry.

If the server has no address to connect to, the response is 422:

{ "error": "Server has no IP address to connect to" }

The firewall rule object

Firewall rules control which traffic reaches a server. Each one is a single ufw rule: an action, a port, and optionally an address the rule applies to.

Hatchbox adds rules of its own while provisioning — SSH on every server, ports 80 and 443 on web and load balancer servers, and the database engine ports for the private IPs of app servers in the same cluster. Those are marked "removable": false.

{
"id": 91,
"server_id": 34,
"port": 5432,
"action": "allow",
"from": "10.0.0.5",
"description": "App server",
"removable": true,
"created_at": "2026-08-27T15:02:11.000Z",
"updated_at": "2026-08-27T15:02:11.000Z"
}

Field

Description

port

The port the rule applies to, 065535.

action

allow or deny.

from

The address the rule applies to — a single IP (203.0.113.4) or a range in CIDR notation (10.0.0.0/24). null means the rule applies to any address.

description

Your own note about the rule. null if you didn't set one.

removable

false for rules Hatchbox manages itself. Those can't be deleted through the API or the dashboard.

server_id

The server the rule belongs to.

List firewall rules

Returns every firewall rule on a server, lowest port first.

Paginated — see Pagination.

GET /api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/firewall_rules
curl https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/firewall_rules \
-H "Authorization: Bearer [TOKEN]"

Returns an array of firewall rule objects.

Get a firewall rule

GET /api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/firewall_rules/[RULE_ID]
curl https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/firewall_rules/[RULE_ID] \
-H "Authorization: Bearer [TOKEN]"

Returns a single firewall rule object. A rule that belongs to a different server returns 404 {"error": "Firewall rule not found"}.

Add a firewall rule

POST /api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/firewall_rules

Parameter

Required

Description

port

Yes

065535.

action

No

allow or deny. Defaults to allow.

from

No

An IP address or CIDR range. Omit to apply the rule to any address.

description

No

A note for your own reference.

curl -X POST https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/firewall_rules \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"firewall_rule": {"port": 9000, "action": "allow", "from": "203.0.113.0/24", "description": "Metrics scraper"}}'

Returns 201 Created with the rule and the log for applying it:

{
"id": 92,
"server_id": 34,
"port": 9000,
"action": "allow",
"from": "203.0.113.0/24",
"description": "Metrics scraper",
"removable": true,
"created_at": "2026-08-28T10:41:03.000Z",
"updated_at": "2026-08-28T10:41:03.000Z",
"log_id": 4412
}

The rule is saved immediately, but it only reaches the server's firewall when the queued operation runs. Fetch log_id to confirm it was applied.

You can add rules to a server that hasn't been created at your provider yet. They're recorded now and applied the next time the server is provisioned.

A rule duplicating one that already exists — same port, action, and from — returns 422:

{ "errors": ["Port has already been taken"] }

Other validation failures return 422 the same way, listing what was wrong:

{ "errors": ["From must be an IP address or range"] }

Remove a firewall rule

DELETE /api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/firewall_rules/[RULE_ID]
curl -X DELETE https://hatchbox.io/api/v1/clusters/[CLUSTER_ID]/servers/[SERVER_ID]/firewall_rules/[RULE_ID] \
-H "Authorization: Bearer [TOKEN]"

Returns 202 Accepted:

{ "id": 4413 }

The returned id is the log for the removal. The rule is deleted only after ufw accepts the change on the server, so it keeps appearing in List firewall rules until that finishes. A 202 means the removal was queued, not that it's done — fetch the log to be sure, and treat a failed log as a rule that's still in force.

Rules Hatchbox manages itself can't be removed. Those return 422:

{ "error": "This firewall rule is managed by Hatchbox and cannot be removed" }

If the server has no address to connect to, the removal can't reach ufw and the response is 422:

{ "error": "Server has no IP address to connect to" }

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:


---

---

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_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 backup endpoints below are addressed by database ID alone, without the cluster.

The backup configuration object

Backups are configured per database. Managed databases are backed up by your hosting provider, so these endpoints only apply to unmanaged databases running on your own servers.

{
"database_id": 88,
"backup_enabled": true,
"backup_provider": "s3",
"backup_region": "us-east-1",
"backup_bucket": "my-backups",
"backup_endpoint": null,
"backup_frequency": "@daily",
"backup_retention_period": "7d",
"backup_credentials_set": true,
"appsignal": false,
"honeybadger_checkin_id": null,
"last_backup_at": "2026-08-11T03:00:00.000Z"
}

Field

Description

backup_enabled

Whether backups are currently scheduled.

backup_provider

Where backups are stored. One of local, s3, azureblob, r2, spaces, gcs, wasabi, other. local keeps them on the server itself.

backup_region

Required for s3, spaces, and wasabi. The other providers don't use it.

backup_bucket

Bucket or container name. Required for every provider except local.

backup_endpoint

Required for other (any S3-compatible service). Optional for r2.

backup_frequency

How often backups run, as a cron expression. Shorthand like @daily, @hourly, @weekly, and @monthly works, as does a full expression such as 0 12 * * *.

backup_retention_period

How long to keep backups, as a number and a unit suffix — one of ms, s, m, h, d, w, M, y. For example 7d or 6M. Leave blank to keep backups forever, or to let your storage provider manage retention.

backup_credentials_set

Whether access credentials are on file. The credentials themselves are never returned.

appsignal

Whether backup runs are monitored with AppSignal. Set by sending appsignal_app_push_api_key; the key itself is never returned.

honeybadger_checkin_id

Honeybadger check-in ID for monitoring backup runs, or null.

last_backup_at

When the most recent backup completed, or null.

Credentials are write-only. backup_access_key_id, backup_secret_access_key, and appsignal_app_push_api_key are accepted when you write the configuration but never appear in a response. Use backup_credentials_set and appsignal to check whether they're present.


Get the backup configuration

GET /api/v1/databases/[DATABASE_ID]/backup_configuration
curl https://hatchbox.io/api/v1/databases/[DATABASE_ID]/backup_configuration \
-H "Authorization: Bearer [TOKEN]"

Returns a backup configuration object. A database that has never had backups set up returns the object with backup_enabled: false and empty fields.

A managed database returns 422 {"error": "Managed databases should be backed up on your hosting provider"}.

Enable or update backups

Sets the backup configuration and enables backups in one call. Only the fields you send are changed, so you can adjust a single setting without resending the rest — but every request enables backups, so a partial payload against a database that was never configured fails validation and tells you what's missing.

PATCH /api/v1/databases/[DATABASE_ID]/backup_configuration
curl -X PATCH https://hatchbox.io/api/v1/databases/[DATABASE_ID]/backup_configuration \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{
"backup_configuration": {
"backup_provider": "s3",
"backup_region": "us-east-1",
"backup_bucket": "my-backups",
"backup_access_key_id": "[ACCESS_KEY_ID]",
"backup_secret_access_key": "[SECRET_ACCESS_KEY]",
"backup_frequency": "@daily",
"backup_retention_period": "7d"
}
}'

Parameter

Description

backup_provider

One of local, s3, azureblob, r2, spaces, gcs, wasabi, other.

backup_region

Required for s3, spaces, wasabi.

backup_bucket

Required for every provider except local.

backup_access_key_id

Required for every provider except local. For Azure, your storage account name.

backup_secret_access_key

Required for every provider except local. For Azure, your access key.

backup_endpoint

Required for other.

backup_frequency

Cron expression or shorthand.

backup_retention_period

Optional. Number plus unit suffix.

appsignal_app_push_api_key

Optional. Enables AppSignal monitoring of backup runs.

honeybadger_checkin_id

Optional. Enables Honeybadger check-in monitoring of backup runs.

Returns 202 with the configuration and the id of the log for applying it to the server:


{
"database_id": 88,
"backup_enabled": true,
"backup_provider": "s3",
"...": "...",
"log_id": 4288
}

Applying the configuration is queued, so a successful response means it was accepted — fetch the log to confirm the schedule actually landed on the server.

Changing a single setting works the same way:

curl -X PATCH https://hatchbox.io/api/v1/databases/[DATABASE_ID]/backup_configuration \
-H "Authorization: Bearer [TOKEN]" \
-H "Content-Type: application/json" \
-d '{"backup_configuration": {"backup_frequency": "@hourly"}}'

Invalid settings return 422 with the problems listed:

{ "errors": ["Backup bucket can't be blank", "Backup region can't be blank"] }

An unrecognized provider returns 422:

{ "error": "Unknown backup provider: dropbox. Valid providers are local, s3, azureblob, r2, spaces, gcs, wasabi, other" }

Disable backups

Turns backups off and clears the stored provider settings and credentials. The retention period and monitoring settings are kept.

DELETE /api/v1/databases/[DATABASE_ID]/backup_configuration
curl -X DELETE https://hatchbox.io/api/v1/databases/[DATABASE_ID]/backup_configuration \
-H "Authorization: Bearer [TOKEN]"

Returns 202 with the cleared configuration and a log_id for removing the schedule from the server. If backups were already disabled, returns 200 with the configuration and no log_id — disabling twice is not an error.

Test the backup connection

Uploads a small test file to your configured bucket so you can verify credentials without waiting for a real backup to run.

POST /api/v1/databases/[DATABASE_ID]/backups/test_connection
curl -X POST https://hatchbox.io/api/v1/databases/[DATABASE_ID]/backups/test_connection \
-H "Authorization: Bearer [TOKEN]"
{ "id": 4289 }

The returned id is the log for the test. The log records whether the upload succeeded, so fetch it to see the result.

Backups have to be enabled and stored remotely. A local provider returns 422:

{ "error": "Local backups have no remote connection to test" }

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" }

If backups aren't enabled, or the database is managed, the response is 422:

{ "error": "Backups are not enabled for this database" }
{ "error": "Managed databases should be backed 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]".

Along with the two 422 messages above, this endpoint returns 422 when there's nothing to download yet, or when the backup lives somewhere we can't generate a link for:

{ "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" }

Logs

Every queued operation writes a log. Deploy an app, Restart an app, Delete an app, Restart a process, Delete a process, Provision a server, Reboot a server, Trigger a backup, and Test the backup connection each return the id of the log they created as id. Create a process, Update a process, Enable a process, Disable a process, Enable or update backups, and Disable backups return theirs as log_id alongside the resource. Update server roles returns a logs array, because a single request can queue both an addition and a removal.

Either way, the log is how you find out whether the work actually succeeded — those endpoints return as soon as the job is accepted, long before it finishes.

Operations started outside the API — an auto-deploy from a git push, or a deploy someone ran from the dashboard — write logs too. Nothing hands you those ids, so use List an app's logs to find them.

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"}.

Logs belong to the resource that produced them, so a log is removed when that resource is. This matters for Delete an app — see the note there.

List an app's logs

Returns the operations run against an app — deploys, restarts, process changes — newest first. This is how you find a log you weren't handed the id for, such as an auto-deploy triggered by a git push.

GET /api/v1/apps/[APP_ID]/logs
curl https://hatchbox.io/api/v1/apps/[APP_ID]/logs \
-H "Authorization: Bearer [TOKEN]"
[
{
"id": 4212,
"name": "Apps::Deploy",
"description": "Fix the retry backoff",
"state": "failed",
"parent_id": null,
"loggable_type": "App",
"loggable_id": 91,
"commit_sha": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d",
"username": "octocat",
"created_at": "2026-08-26T17:41:02.000Z",
"started_at": "2026-08-26T17:41:03.000Z",
"completed_at": "2026-08-26T17:42:19.000Z"
}
]

Paginated — see Pagination. Defaults to the 20 most recent.

Two differences from Get a log:

  • No **body**. Listing them would mean returning every deploy's output at once. Fetch a log by id to read it.
  • No **child\_logs**. Only the app's own operations are listed, never the per-server children. Their loggable_type is always App and their parent_id always null.

So the usual flow for a failed push-triggered deploy is two requests — list to find the id, then Get a log to read the children that explain the failure:

LOG_ID=$(curl -s https://hatchbox.io/api/v1/apps/[APP_ID]/logs \
-H "Authorization: Bearer $TOKEN" \
| jq -r '[.[] | select(.name == "Apps::Deploy" and .state == "failed")][0].id')

curl -s https://hatchbox.io/api/v1/logs/$LOG_ID \
-H "Authorization: Bearer $TOKEN" | jq -r '.child_logs[].body'

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