# API Endpoints

Learn how to use the Hatchbox REST API Endpoints

# 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](#authentication)
- [Base URL](#base-url)
- [Active subscription required](#active-subscription-required)
- [Finding IDs](#finding-ids)
- [Errors](#errors)
- [Pagination](#pagination)
- [Accounts](#accounts)
  - List your accounts
- [Apps](#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](#environment-variables)
  - Create environment variables
  - Add or update environment variables
  - Remove environment variables
- [Domains](#domains)
  - List domains
  - Get a domain
  - Add a domain
  - Update a domain
  - Remove a domain
- [Processes](#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](#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](#clusters)
  - List clusters
  - Get a cluster
- [Servers](#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](#git-providers)
  - List git providers
- [Database clusters](#database-clusters)
  - List database clusters
- [Databases](#databases)
  - The database object
  - SQLite databases
  - List databases
  - Get a database
  - Create a database
  - Update a database
  - List an app&#39;s databases
  - Get an app&#39;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](#logs)
  - The log object
  - Where the output lives
  - Get a log
  - List an app&#39;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&#39;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&#39;t, the response is `402 Payment Required`:

```
{ &quot;error&quot;: &quot;An active subscription is required for [Account Name]&quot; }
```

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&#39;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&#39;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&#39;t queue yourself — an auto-deploy from a git push, say — use List an app&#39;s logs.

## Errors

| Status | Meaning | Body |
| --- | --- | --- |
| `401 Unauthorized` | Missing or invalid token | (empty) |
| `402 Payment Required` | The owning account has no active subscription | `{&quot;error&quot;: &quot;An active subscription is required for ...&quot;}` |
| `404 Not Found` | The record doesn&#39;t exist, or your token can&#39;t access it | `{&quot;error&quot;: &quot;App not found&quot;}` |
| `422 Unprocessable Content` | Validation failed | `{&quot;errors&quot;: [&quot;...&quot;]}` or `{&quot;error&quot;: &quot;...&quot;}` |
| Resources belonging to accounts you&#39;re not a member of return `404`, not `403`. The `404` body names the resource type — e.g. `{&quot;error&quot;: &quot;Cluster not found&quot;}`. |  |  |
| 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&#39;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 &quot;https://hatchbox.io/api/v1/apps/[APP_ID]/logs?page=2&amp;limit=50&quot; \
  -H &quot;Authorization: Bearer [TOKEN]&quot;
```

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

```
URL=&quot;https://hatchbox.io/api/v1/apps/[APP_ID]/logs?limit=100&quot;

while [ -n &quot;$URL&quot; ]; do
  BODY=$(curl -sD /tmp/h &quot;$URL&quot; -H &quot;Authorization: Bearer $TOKEN&quot;)
  echo &quot;$BODY&quot; | jq -r &#39;.[] | &quot;\(.state)\t\(.name)&quot;&#39;
  URL=$(grep -i &#39;^link:&#39; /tmp/h | tr &#39;,&#39; &#39;\n&#39; | grep &#39;rel=&quot;next&quot;&#39; | sed -e &#39;s/.*&lt;//&#39; -e &#39;s/&gt;.*//&#39;)
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&#39;s `id` to the account-scoped list endpoints below.

### List your accounts

Returns the accounts your token&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
[
  { &quot;id&quot;: 3, &quot;name&quot;: &quot;My Team&quot; }
]
```

An account&#39;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

```
{
  &quot;id&quot;: 1,
  &quot;name&quot;: &quot;my-app&quot;,
  &quot;cluster_id&quot;: 12,
  &quot;connected_account_id&quot;: 5,
  &quot;repo_path&quot;: &quot;myuser/my-app&quot;,
  &quot;branch&quot;: &quot;main&quot;,
  &quot;auto_deploy&quot;: false,
  &quot;pre_build_script&quot;: null,
  &quot;build_script&quot;: null,
  &quot;post_build_script&quot;: null,
  &quot;post_deploy_script&quot;: null,
  &quot;failed_deploy_script&quot;: null,
  &quot;dns_provider&quot;: null,
  &quot;dns_api_user&quot;: null,
  &quot;caddyfile&quot;: null,
  &quot;health_check_uri&quot;: null,
  &quot;last_deploy_at&quot;: &quot;2026-07-14T18:22:10.000Z&quot;,
  &quot;last_deploy_sha&quot;: &quot;abc1234def5678&quot;
}
```

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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns an array of app objects. An account you&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Account not found&quot;}`.

### Get an app

```
GET /api/v1/apps/[APP_ID]
```

```
curl https://hatchbox.io/api/v1/apps/[APP_ID] \
  -H &quot;Authorization: Bearer [TOKEN]&quot;
```

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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;app&quot;: {&quot;cluster_id&quot;: 12, &quot;name&quot;: &quot;my-app&quot;, &quot;branch&quot;: &quot;main&quot;, &quot;repo_path&quot;: &quot;myuser/my-app&quot;, &quot;connected_account_id&quot;: 5}}&#39;
```

Returns `201 Created` with the app object. A `cluster_id` you don&#39;t own returns `404 {&quot;error&quot;: &quot;Cluster not found&quot;}`; validation failures return `422` with `errors`.

### Update an app

Updates an app&#39;s configuration. Accepts the same fields as create except `cluster_id` — an app can&#39;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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;app&quot;: {&quot;build_script&quot;: &quot;bundle install&quot;, &quot;branch&quot;: &quot;production&quot;}}&#39;
```

Returns `200 OK` with the updated app object.

### Caddy configuration

Two fields control an app&#39;s Caddy setup:

| Parameter | Description |
| --- | --- |
| `caddyfile` | The app&#39;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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;app&quot;: {&quot;caddyfile&quot;: &quot;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*&quot;, &quot;health_check_uri&quot;: &quot;/up&quot;}}&#39;
```

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:

```
{ &quot;errors&quot;: [&quot;Health check uri must be a relative path like &#39;/example&#39;&quot;] }
```

### Delete an app

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

- Deletes the app&#39;s folder and its contents from all the servers
- Stops the app&#39;s processes and completely removes them from all the servers
- Detaches the app&#39;s databases — **the databases themselves are not deleted**

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

```
DELETE /api/v1/apps/[APP_ID]
```

| Parameter | Required | Description |
| --- | --- | --- |
| `name` | Yes | Must exactly match the app&#39;s current `name`. |
```
curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID] \
  -H &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;name&quot;: &quot;my-app&quot;}&#39;
```

```
{ &quot;id&quot;: 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&#39;t match, the response is `422` and nothing is queued:

```
{ &quot;error&quot;: &quot;App name does not match. Resend with the app&#39;s name to confirm deletion.&quot; }
```

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`:

```
{ &quot;error&quot;: &quot;App is already being deleted&quot; }
```

**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 {&quot;error&quot;: &quot;Log not found&quot;}` — 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&#39;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&#39;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&#39;s branch. |
```
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/deploy \
  -H &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{ &quot;id&quot;: 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:

```
{ &quot;error&quot;: &quot;No active servers available to deploy to&quot; }
```

### Restart an app

Restarts all of the app&#39;s processes.

```
POST /api/v1/apps/[APP_ID]/restart
```

```
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/restart \
  -H &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{ &quot;id&quot;: 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns `200 OK` with the app object (`auto_deploy` now `true`). If the app has no connected git provider:

```
{ &quot;error&quot;: &quot;Auto deploy requires a connected git provider&quot; }
```

If the git host rejects the webhook, the response is `502`:

```
{ &quot;error&quot;: &quot;Could not enable auto deploy&quot; }
```

### Disable auto-deploy

Removes the webhook and turns off automatic deploys. Idempotent — succeeds even if auto-deploy wasn&#39;t on.

```
DELETE /api/v1/apps/[APP_ID]/auto_deploy
```

```
curl -X DELETE https://hatchbox.io/api/v1/apps/[APP_ID]/auto_deploy \
  -H &quot;Authorization: Bearer [TOKEN]&quot;
```

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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;env_vars&quot;: [{&quot;name&quot;: &quot;FOO&quot;, &quot;value&quot;: &quot;bar&quot;}, {&quot;name&quot;: &quot;BAZ&quot;, &quot;value&quot;: &quot;qux&quot;}]}&#39;
```

```
[
  { &quot;id&quot;: 91, &quot;name&quot;: &quot;FOO&quot; },
  { &quot;id&quot;: 92, &quot;name&quot;: &quot;BAZ&quot; }
]
```

### Add or update environment variables

Updates variables that already exist (matched by name) and creates any that don&#39;t. Include `&quot;_destroy&quot;: &quot;1&quot;` 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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;env_vars&quot;: [{&quot;name&quot;: &quot;FOO&quot;, &quot;value&quot;: &quot;new-value&quot;}, {&quot;name&quot;: &quot;BAZ&quot;, &quot;_destroy&quot;: &quot;1&quot;}]}&#39;
```

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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;env_vars&quot;: [&quot;FOO&quot;, &quot;BAZ&quot;]}&#39;
```

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
```

```
[
  { &quot;id&quot;: 5, &quot;name&quot;: &quot;example.com&quot;, &quot;created_at&quot;: &quot;2026-07-14T18:22:10.000Z&quot;, &quot;updated_at&quot;: &quot;2026-07-14T18:22:10.000Z&quot; }
]
```

### Get a domain

```
GET /api/v1/apps/[APP_ID]/domains/[DOMAIN_NAME]
```

Returns a single domain. If it isn&#39;t on this app: `{&quot;error&quot;: &quot;Domain not found&quot;}`.

### Add a domain

```
POST /api/v1/apps/[APP_ID]/domains
```

```
curl -X POST https://hatchbox.io/api/v1/apps/[APP_ID]/domains \
  -H &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;domain&quot;: {&quot;name&quot;: &quot;example.com&quot;}}&#39;
```

Returns `201 Created` with the domain. A domain already connected to another app in the same cluster returns `422`:

```
{ &quot;errors&quot;: [&quot;Name is already connected to another App in this Cluster&quot;] }
```

### 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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;domain&quot;: {&quot;name&quot;: &quot;www.example.com&quot;}}&#39;
```

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

```
{
  &quot;id&quot;: 15,
  &quot;name&quot;: &quot;web&quot;,
  &quot;start_command&quot;: &quot;bin/rails server&quot;,
  &quot;stop_command&quot;: &quot;&quot;,
  &quot;reload_command&quot;: &quot;&quot;,
  &quot;restart_on_deploy&quot;: true,
  &quot;server_id&quot;: 6,
  &quot;socket&quot;: false,
  &quot;systemd_type&quot;: &quot;simple&quot;,
  &quot;active&quot;: true,
  &quot;roles&quot;: [&quot;web&quot;],
  &quot;appsignal&quot;: false,
  &quot;appsignal_options&quot;: null,
  &quot;created_at&quot;: &quot;2026-07-02T17:08:01.887Z&quot;,
  &quot;updated_at&quot;: &quot;2026-07-02T17:08:01.887Z&quot;
}
```

| 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. `[&quot;web&quot;]`. |
| `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&#39;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&#39;s push API key, and without one the process would launch with a malformed flag. Attempting it returns `422`: |  |
```
{ &quot;errors&quot;: [&quot;Appsignal monitoring requires connecting AppSignal to this app first&quot;] }
```

### List processes

```
GET /api/v1/apps/[APP_ID]/processes
```

```
curl https://hatchbox.io/api/v1/apps/[APP_ID]/processes \
  -H &quot;Authorization: Bearer [TOKEN]&quot;
```

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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns a single process object. A process that isn&#39;t on this app returns `404 {&quot;error&quot;: &quot;Process not found&quot;}`.

### 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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;process&quot;: {&quot;name&quot;: &quot;worker&quot;, &quot;start_command&quot;: &quot;bundle exec sidekiq&quot;, &quot;roles&quot;: [&quot;worker&quot;]}}&#39;
```

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

```
{
  &quot;id&quot;: 16,
  &quot;name&quot;: &quot;worker&quot;,
  &quot;...&quot;: &quot;...&quot;,
  &quot;log_id&quot;: 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:

```
{ &quot;errors&quot;: [&quot;Only one option can be selected between roles and server&quot;] }
```

### Update a process

Accepts the same parameters as create; send only the fields you&#39;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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;process&quot;: {&quot;start_command&quot;: &quot;bundle exec sidekiq -c 10&quot;}}&#39;
```

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&#39;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&#39;s last active `web` process, the app&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{ &quot;id&quot;: 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{ &quot;id&quot;: 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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

```
{
  &quot;id&quot;: 15,
  &quot;name&quot;: &quot;web&quot;,
  &quot;active&quot;: true,
  &quot;...&quot;: &quot;...&quot;,
  &quot;log_id&quot;: 4333
}
```

Safe to repeat. Enabling a process that is already enabled changes nothing, queues nothing, and returns `&quot;log_id&quot;: 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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 `&quot;log_id&quot;: null`.

---

## Cron jobs

Cron jobs are scheduled commands that run inside your app&#39;s current deploy, on every server in the cluster carrying the `cron` role.

Hatchbox writes all of an app&#39;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&#39;t schedule anything, so creating or updating returns `422`:

```
{ &quot;error&quot;: &quot;No active servers with the cron role in this cluster&quot; }
```

Deleting is still allowed in that case, so a cluster that lost its cron server can be tidied up.

### The cron job object

```
{
  &quot;id&quot;: 17,
  &quot;app_id&quot;: 1,
  &quot;name&quot;: &quot;Nightly sync&quot;,
  &quot;run_at&quot;: &quot;0 0 * * *&quot;,
  &quot;command&quot;: &quot;bin/rails sync:nightly&quot;,
  &quot;appsignal&quot;: false,
  &quot;honeybadger_checkin_id&quot;: null,
  &quot;created_at&quot;: &quot;2026-08-10T09:14:02.000Z&quot;,
  &quot;updated_at&quot;: &quot;2026-08-10T09:14:02.000Z&quot;
}
```

| 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&#39;s current release directory as the `deploy` user. |
| `appsignal` | Whether to wrap the command in AppSignal&#39;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&#39;s push API key, and without one the job would run unmonitored with a malformed flag. Attempting it returns `422`: |  |
```
{ &quot;errors&quot;: [&quot;Appsignal monitoring requires connecting AppSignal to this app first&quot;] }
```

`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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns a single cron job object. A cron job belonging to a different app returns `404 {&quot;error&quot;: &quot;Cron job not found&quot;}`.

### 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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;cron_job&quot;: {&quot;name&quot;: &quot;Nightly sync&quot;, &quot;run_at&quot;: &quot;0 0 * * *&quot;, &quot;command&quot;: &quot;bin/rails sync:nightly&quot;}}&#39;
```

Returns `201 Created` with the cron job. An unparseable schedule returns `422`:

```
{ &quot;errors&quot;: [&quot;Run at must be a valid cron expression&quot;] }
```

### 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&#39;re changing.

```
curl -X PATCH https://hatchbox.io/api/v1/apps/[APP_ID]/cron_jobs/[CRON_JOB_ID] \
  -H &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;cron_job&quot;: {&quot;run_at&quot;: &quot;@daily&quot;}}&#39;
```

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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
[
  {
    &quot;id&quot;: 12,
    &quot;name&quot;: &quot;my-cluster&quot;,
    &quot;provider&quot;: &quot;digitalocean&quot;,
    &quot;region&quot;: &quot;nyc3&quot;,
    &quot;servers_count&quot;: 2
  }
]
```

An account you&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Account not found&quot;}`.

### 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{
  &quot;id&quot;: 12,
  &quot;name&quot;: &quot;my-cluster&quot;,
  &quot;provider&quot;: &quot;digitalocean&quot;,
  &quot;region&quot;: &quot;nyc3&quot;,
  &quot;servers_count&quot;: 2,
  &quot;servers&quot;: [
    {
      &quot;id&quot;: 34,
      &quot;name&quot;: &quot;web-1&quot;,
      &quot;state&quot;: &quot;active&quot;,
      &quot;roles&quot;: [&quot;web&quot;, &quot;worker&quot;],
      &quot;cluster_id&quot;: 12,
      &quot;provider_id&quot;: &quot;405177344&quot;,
      &quot;public_ip&quot;: &quot;203.0.113.10&quot;,
      &quot;public_ipv6&quot;: null,
      &quot;private_ip&quot;: &quot;10.0.0.5&quot;,
      &quot;ssh_port&quot;: 22,
      &quot;size&quot;: &quot;s-2vcpu-4gb&quot;,
      &quot;ubuntu_version&quot;: &quot;noble&quot;,
      &quot;last_configured_at&quot;: &quot;2026-07-14T09:12:44.000Z&quot;
    }
  ]
}
```

The `servers` array is sorted by name and contains every server in the cluster. A cluster on an account you&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Cluster not found&quot;}`.

---

## Servers

A server is one machine in a cluster. Its roles determine what runs on it — your app&#39;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&#39;t need these endpoints to list them. Use them when you want a single server on its own.

### The server object

```
{
  &quot;id&quot;: 34,
  &quot;name&quot;: &quot;web-1&quot;,
  &quot;state&quot;: &quot;active&quot;,
  &quot;roles&quot;: [&quot;web&quot;, &quot;worker&quot;],
  &quot;cluster_id&quot;: 12,
  &quot;provider_id&quot;: &quot;405177344&quot;,
  &quot;public_ip&quot;: &quot;203.0.113.10&quot;,
  &quot;public_ipv6&quot;: null,
  &quot;private_ip&quot;: &quot;10.0.0.5&quot;,
  &quot;ssh_port&quot;: 22,
  &quot;size&quot;: &quot;s-2vcpu-4gb&quot;,
  &quot;ubuntu_version&quot;: &quot;noble&quot;,
  &quot;last_configured_at&quot;: &quot;2026-07-14T09:12:44.000Z&quot;
}
```

| Field | Description |
| --- | --- |
| `state` | One of `pending`, `creating`, `created`, `active`, `destroyed`. A server only runs your apps once it&#39;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&#39;s id at your hosting provider, for cross-referencing with your own tooling. `null` for servers Hatchbox didn&#39;t create there. |
| `public_ip`, `private_ip` | The server&#39;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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns an array of server objects. A cluster on an account you&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Cluster not found&quot;}`.

### 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns a single server object. A server that exists but lives in a different cluster returns `404 {&quot;error&quot;: &quot;Server not found&quot;}` — 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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;server&quot;: {&quot;roles&quot;: [&quot;web&quot;, &quot;worker&quot;, &quot;cron&quot;]}}&#39;
```

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

```
{
  &quot;added_roles&quot;: [&quot;cron&quot;],
  &quot;removed_roles&quot;: [&quot;load_balancer&quot;],
  &quot;logs&quot;: [
    { &quot;id&quot;: 4340, &quot;name&quot;: &quot;Servers::AddRoles&quot; },
    { &quot;id&quot;: 4341, &quot;name&quot;: &quot;Servers::RemoveRoles&quot; }
  ]
}
```

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:

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

**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&#39;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&#39;s refused with `409` and a description of what would be lost:

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

Resend the identical request with `&quot;confirm_destructive&quot;: 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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;server&quot;: {&quot;roles&quot;: [&quot;web&quot;]}, &quot;confirm_destructive&quot;: true}&#39;
```

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`:

```
{ &quot;error&quot;: &quot;Server is currently being created&quot; }
```

```
{ &quot;error&quot;: &quot;Server is currently being provisioned&quot; }
```

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`:

```
{ &quot;error&quot;: &quot;Server has no IP address to connect to&quot; }
```

### 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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{ &quot;id&quot;: 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`:

```
{ &quot;error&quot;: &quot;Server is currently being created&quot; }
```

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

```
{ &quot;error&quot;: &quot;Provisioning is already running for this server&quot; }
```

If the server record exists but hasn&#39;t been created at your cloud provider yet, the response is `422`:

```
{ &quot;error&quot;: &quot;This server has not been created on its cloud provider yet&quot; }
```

This endpoint only re-provisions servers that already exist. Creating the underlying machine at your provider isn&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{ &quot;id&quot;: 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`:

```
{ &quot;error&quot;: &quot;Server is currently being created&quot; }
```

```
{ &quot;error&quot;: &quot;Server is currently being provisioned&quot; }
```

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`:

```
{ &quot;error&quot;: &quot;Server has no IP address to connect to&quot; }
```

### 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 `&quot;removable&quot;: false`.

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

| Field | Description |
| --- | --- |
| `port` | The port the rule applies to, `0`–`65535`. |
| `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&#39;t set one. |
| `removable` | `false` for rules Hatchbox manages itself. Those can&#39;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](#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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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

### Add a firewall rule

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

| Parameter | Required | Description |
| --- | --- | --- |
| `port` | Yes | `0`–`65535`. |
| `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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;firewall_rule&quot;: {&quot;port&quot;: 9000, &quot;action&quot;: &quot;allow&quot;, &quot;from&quot;: &quot;203.0.113.0/24&quot;, &quot;description&quot;: &quot;Metrics scraper&quot;}}&#39;
```

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

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

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

You can add rules to a server that hasn&#39;t been created at your provider yet. They&#39;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`:

```
{ &quot;errors&quot;: [&quot;Port has already been taken&quot;] }
```

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

```
{ &quot;errors&quot;: [&quot;From must be an IP address or range&quot;] }
```

### 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns `202 Accepted`:

```
{ &quot;id&quot;: 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&#39;s done — fetch the log to be sure, and treat a failed log as a rule that&#39;s still in force.

Rules Hatchbox manages itself can&#39;t be removed. Those return `422`:

```
{ &quot;error&quot;: &quot;This firewall rule is managed by Hatchbox and cannot be removed&quot; }
```

If the server has no address to connect to, the removal can&#39;t reach `ufw` and the response is `422`:

```
{ &quot;error&quot;: &quot;Server has no IP address to connect to&quot; }
```

---

## Git providers

Git providers are the connected git accounts (GitHub, GitLab, Bitbucket) for one of your accounts. Use one&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
[
  { &quot;id&quot;: 5, &quot;provider&quot;: &quot;github&quot;, &quot;name&quot;: &quot;my-org&quot; }
]
```

An account you&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Account not found&quot;}`.

---

## 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
[
  {
    &quot;id&quot;: 8,
    &quot;name&quot;: &quot;my-database-cluster&quot;,
    &quot;engine&quot;: &quot;postgresql&quot;,
    &quot;version&quot;: &quot;17&quot;,
    &quot;region&quot;: &quot;nyc3&quot;,
    &quot;managed&quot;: true,
    &quot;active&quot;: true,
    &quot;databases_count&quot;: 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&#39;t reachable yet. Unmanaged clusters are always `true`. |
| `databases_count` | How many databases are on the cluster. |
| An account you&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Account not found&quot;}`. |  |
---

## 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&#39;d treat any other secret — don&#39;t log them, and don&#39;t cache them anywhere you wouldn&#39;t cache a password.

### The database object

```
{
  &quot;id&quot;: 42,
  &quot;name&quot;: &quot;my_app_production&quot;,
  &quot;engine&quot;: &quot;postgresql&quot;,
  &quot;database_cluster_id&quot;: 8,
  &quot;username&quot;: &quot;my_app&quot;,
  &quot;password&quot;: &quot;sup3rs3cret&quot;,
  &quot;private_connection_uri&quot;: &quot;postgresql://my_app:sup3rs3cret@10.0.0.5:5432/my_app_production&quot;,
  &quot;public_connection_uri&quot;: &quot;postgresql://my_app:sup3rs3cret@203.0.113.10:5432/my_app_production&quot;,
  &quot;app_attachments&quot;: [
    { &quot;app_id&quot;: 1, &quot;env_var&quot;: &quot;DATABASE_URL&quot; },
    { &quot;app_id&quot;: 4, &quot;env_var&quot;: &quot;RED_DATABASE_URL&quot; }
  ]
}
```

| Field | Description |
| --- | --- |
| `engine` | Inherited from the cluster: `postgresql`, `mysql`, `redis`, `memcached`, `elasticsearch`, or `sqlite`. |
| `username`, `password` | The database&#39;s credentials. For Redis, Memcached, and Elasticsearch these are the cluster&#39;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&#39;s &quot;public&quot; host is just your server&#39;s IP. |
| `path` | SQLite only. The absolute path to the database file on your server. Replaces the credentials and connection URIs above, which don&#39;t apply to a local file. |
| `app_attachments` | One entry per app this database is attached to. Empty when it isn&#39;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&#39;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:

```
{
  &quot;id&quot;: 51,
  &quot;name&quot;: &quot;my-app-production&quot;,
  &quot;engine&quot;: &quot;sqlite&quot;,
  &quot;database_cluster_id&quot;: 12,
  &quot;path&quot;: &quot;/home/deploy/my-app-production.sqlite3&quot;,
  &quot;app_attachments&quot;: [
    { &quot;app_id&quot;: 1, &quot;env_var&quot;: &quot;DATABASE_PATH&quot; }
  ]
}
```

Everything else works the same way: they&#39;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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns an array of database objects. A cluster on an account you&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Database cluster not found&quot;}`.

### 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns a single database object. A database that exists but lives on a different cluster returns `404 {&quot;error&quot;: &quot;Database not found&quot;}` — 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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;database&quot;: {&quot;name&quot;: &quot;my_app_production&quot;}}&#39;
```

The body is optional — `curl -X POST ... -H &quot;Authorization: Bearer [TOKEN]&quot;` 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&#39;re ready as soon as the call returns.

A cluster on an account you&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Database cluster not found&quot;}`. A managed cluster that&#39;s still provisioning returns `422 {&quot;error&quot;: &quot;Database cluster is not active yet&quot;}` — wait until the cluster reports `&quot;active&quot;: 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 {&quot;error&quot;: &quot;Only SQLite databases can be updated&quot;}` — 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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;database&quot;: {&quot;path&quot;: &quot;/home/deploy/moved.sqlite3&quot;}}&#39;
```

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&#39;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&#39;t be blank.

### List an app&#39;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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns an array of database objects — the same shape as the database object. An app you can&#39;t access returns `404 {&quot;error&quot;: &quot;App not found&quot;}`.

### Get an app&#39;s database

```
GET /api/v1/apps/[APP_ID]/databases/[DATABASE_ID]
```

```
curl https://hatchbox.io/api/v1/apps/[APP_ID]/databases/[DATABASE_ID] \
  -H &quot;Authorization: Bearer [TOKEN]&quot;
```

Returns a single database object. A database that isn&#39;t attached to this app returns `404 {&quot;error&quot;: &quot;Database not found&quot;}`, even when you can read that same database through its cluster — this endpoint answers &quot;is this database attached to this app,&quot; so an unattached one is indistinguishable from one that doesn&#39;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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;env_var&quot;: &quot;REPORTING_DATABASE_URL&quot;}&#39;
```

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&#39;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&#39;s already taken on the app returns `422 {&quot;errors&quot;: [&quot;Name has already been taken&quot;]}`. 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 {&quot;error&quot;: &quot;Database not found&quot;}`, and an app you can&#39;t access returns `404 {&quot;error&quot;: &quot;App not found&quot;}`.

### Detach a database from an app

Removes the environment variable (or variables) connecting the database to the app, and updates the app&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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&#39;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.

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

| 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&#39;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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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 {&quot;error&quot;: &quot;Managed databases should be backed up on your hosting provider&quot;}`.

### 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&#39;s missing.

```
PATCH /api/v1/databases/[DATABASE_ID]/backup_configuration
```

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

| 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: |  |
```
{
  &quot;database_id&quot;: 88,
  &quot;backup_enabled&quot;: true,
  &quot;backup_provider&quot;: &quot;s3&quot;,
  &quot;...&quot;: &quot;...&quot;,
  &quot;log_id&quot;: 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 &quot;Authorization: Bearer [TOKEN]&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;backup_configuration&quot;: {&quot;backup_frequency&quot;: &quot;@hourly&quot;}}&#39;
```

Invalid settings return `422` with the problems listed:

```
{ &quot;errors&quot;: [&quot;Backup bucket can&#39;t be blank&quot;, &quot;Backup region can&#39;t be blank&quot;] }
```

An unrecognized provider returns `422`:

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

### 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{ &quot;id&quot;: 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`:

```
{ &quot;error&quot;: &quot;Local backups have no remote connection to test&quot; }
```

### 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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{ &quot;id&quot;: 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`:

```
{ &quot;error&quot;: &quot;A backup is already running for this database&quot; }
```

If backups aren&#39;t enabled, or the database is managed, the response is `422`:

```
{ &quot;error&quot;: &quot;Backups are not enabled for this database&quot; }
```

```
{ &quot;error&quot;: &quot;Managed databases should be backed up on your hosting provider&quot; }
```

### 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&#39;s issued.

```
GET /api/v1/databases/[DATABASE_ID]/backups/latest
```

```
curl https://hatchbox.io/api/v1/databases/[DATABASE_ID]/backups/latest \
  -H &quot;Authorization: Bearer [TOKEN]&quot;
```

```
{
  &quot;url&quot;: &quot;https://your-bucket.s3.amazonaws.com/...&quot;,
  &quot;last_backup_at&quot;: &quot;2026-07-16T03:00:00.000Z&quot;
}
```

Download it with the returned URL: `curl -o backup.tar.gz &quot;[URL]&quot;`.

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

```
{ &quot;error&quot;: &quot;No backup has completed for this database yet&quot; }
```

```
{ &quot;error&quot;: &quot;Downloading backups is only supported for S3 and S3-compatible providers&quot; }
```

If the backup can&#39;t be reached at your storage provider (for example, expired credentials), the response is `502`:

```
{ &quot;error&quot;: &quot;Could not generate a download URL from your backup provider&quot; }
```

---

## 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&#39;s logs to find them.

### The log object

```
{
  &quot;id&quot;: 4212,
  &quot;name&quot;: &quot;Apps::Deploy&quot;,
  &quot;description&quot;: &quot;Add the logs endpoint&quot;,
  &quot;state&quot;: &quot;completed&quot;,
  &quot;parent_id&quot;: null,
  &quot;loggable_type&quot;: &quot;App&quot;,
  &quot;loggable_id&quot;: 91,
  &quot;commit_sha&quot;: &quot;0d1e2f3a4b5c6d7e8f90a1b2c3d4e5f6a7b8c9d0&quot;,
  &quot;username&quot;: &quot;Jane Doe&quot;,
  &quot;body&quot;: &quot;-----&gt; Starting deployment\n-----&gt; Deploying 0d1e2f3 from main branch\n&quot;,
  &quot;created_at&quot;: &quot;2026-08-04T20:31:45.000Z&quot;,
  &quot;started_at&quot;: &quot;2026-08-04T20:31:46.000Z&quot;,
  &quot;completed_at&quot;: &quot;2026-08-04T20:33:02.000Z&quot;,
  &quot;child_logs&quot;: []
}
```

| 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&#39;s subject line. `null` for most other operations. |
| `state` | One of `pending`, `processing`, `completed`, `failed`, `aborted`. |
| `parent_id` | The parent log&#39;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&#39;t a deploy. |
| `username` | Who started it — a Hatchbox user&#39;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&#39;s output to its own child log. The parent&#39;s `body` holds only the high-level narrative:

```
-----&gt; Starting deployment
-----&gt; Deploying 0d1e2f3 from main branch as 20260804203145 release
-----&gt; 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&#39;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&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

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&#39;re not a member of returns `404 {&quot;error&quot;: &quot;Log not found&quot;}`.

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&#39;s logs

Returns the operations run against an app — deploys, restarts, process changes — newest first. This is how you find a log you weren&#39;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 &quot;Authorization: Bearer [TOKEN]&quot;
```

```
[
  {
    &quot;id&quot;: 4212,
    &quot;name&quot;: &quot;Apps::Deploy&quot;,
    &quot;description&quot;: &quot;Fix the retry backoff&quot;,
    &quot;state&quot;: &quot;failed&quot;,
    &quot;parent_id&quot;: null,
    &quot;loggable_type&quot;: &quot;App&quot;,
    &quot;loggable_id&quot;: 91,
    &quot;commit_sha&quot;: &quot;1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d&quot;,
    &quot;username&quot;: &quot;octocat&quot;,
    &quot;created_at&quot;: &quot;2026-08-26T17:41:02.000Z&quot;,
    &quot;started_at&quot;: &quot;2026-08-26T17:41:03.000Z&quot;,
    &quot;completed_at&quot;: &quot;2026-08-26T17:42:19.000Z&quot;
  }
]
```

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

Two differences from Get a log:

- **No** `**body**`**.** Listing them would mean returning every deploy&#39;s output at once. Fetch a log by id to read it.
- **No** `**child\_logs**`**.** Only the app&#39;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 &quot;Authorization: Bearer $TOKEN&quot; \
  | jq -r &#39;[.[] | select(.name == &quot;Apps::Deploy&quot; and .state == &quot;failed&quot;)][0].id&#39;)

curl -s https://hatchbox.io/api/v1/logs/$LOG_ID \
  -H &quot;Authorization: Bearer $TOKEN&quot; | jq -r &#39;.child_logs[].body&#39;
```

### 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 &quot;Authorization: Bearer $TOKEN&quot; | jq -r .id)

while :; do
  LOG=$(curl -s https://hatchbox.io/api/v1/logs/$LOG_ID -H &quot;Authorization: Bearer $TOKEN&quot;)
  STATE=$(echo &quot;$LOG&quot; | jq -r .state)
  case $STATE in
    completed) echo &quot;Deployed.&quot;; break ;;
    failed|aborted) echo &quot;$LOG&quot; | jq -r &#39;.child_logs[].body&#39;; exit 1 ;;
    *) sleep 5 ;;
  esac
done
```
