> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instapods.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pods

> Create, manage, and control pod lifecycle via the API.

## Create a Pod

```
POST /api/pods
```

**Request Body:**

```json theme={null}
{
  "name": "my-app",
  "preset": "nodejs",
  "plan_slug": "launch",
  "region": "eu",
  "ssh_key": "ssh-ed25519 AAAA...",
  "description": "My Node.js app",
  "customizations": {
    "env_vars": {
      "NODE_ENV": "production"
    }
  }
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `name` | string | Yes | Pod name (lowercase, alphanumeric + hyphens) |
| `preset` | string | Yes | `static`, `php`, `nodejs`, `python`, or `go` |
| `plan_slug` | string | No | Plan slug (default: `launch`) |
| `region` | string | No | Region slug (auto-detected from IP if omitted). Fetch the current list from [`GET /api/regions`](/api-reference/overview) |
| `server_id` | string | No | **Admin accounts only.** Pins the pod to a specific server, overriding `region` and the orchestrator; the warm pool is used only if that server holds a ready warm pod. Returns `403` for anyone else |
| `ssh_key` | string | No | Public SSH key to inject |
| `description` | string | No | Pod description |
| `kind` | string | No | `worker` for a [worker pod](/guides/worker-pods) with no public URL. Omit for a web pod |
| `app_type` | string | No | Deploy a [1-Click App](/guides/one-click-apps) instead of a bare preset |
| `customizations` | object | No | Custom env vars, cron jobs, workers |

**Response: `202 Accepted`**

Returns the [Pod object](#pod-object) with `status: "creating"`.

**Errors:**

| Code | Reason |
| - | - |
| `400` | Invalid name, unknown preset, unknown region, an `app_type` the chosen preset can't run, validation error |
| `402` | No payment method or subscription suspended |
| `409` | Pod name already exists |
| `503` | Insufficient server capacity |

> [!NOTE]
>
> A region slug we have retired is translated to the region that replaced it rather than failing, so
> an older client keeps working. A slug that was never ours is a `400` - it is a typo, not a capacity
> problem, and it is not reported as one.
>
> Pairing `app_type` with a `preset` the app cannot run on is also a `400`, and the message names the
> presets it does run on. Omit `preset` and the app's own is used.

## Create a Pod From a Repo

```
POST /api/pods/from-repo
```

Creates a pod for a GitHub repository, attaches the repo, and deploys it once the pod is up. See
[Git Deployment](/guides/git-deployment).

**Request Body:**

```json theme={null}
{
  "repo_url": "https://github.com/you/my-api",
  "branch": "main",
  "name": "my-api",
  "preset": "nodejs",
  "plan_slug": "build",
  "install_command": "npm ci",
  "build_command": "npm run build",
  "start_command": "node server.js",
  "release_command": "npx prisma migrate deploy",
  "env_vars": { "LOG_LEVEL": "info" },
  "services": ["postgresql"],
  "generate_env": ["SESSION_SECRET"],
  "self_url_env": ["NEXTAUTH_URL"]
}
```

| Field | Type | Description |
| - | - | - |
| `repo_url` | string | Required. The repository to deploy |
| `branch` | string | Branch to deploy (default: the repo's default branch) |
| `name` | string | Pod name (derived from the repo name if omitted) |
| `preset` | string | Override the detected runtime; required when detection fails |
| `plan_slug` | string | Plan (default: `launch`, or the cheapest services-capable plan when `services` is set) |
| `region` | string | Region slug |
| `subdirectory` | string | Path within the repo to deploy, for monorepos |
| `install_command` / `build_command` / `start_command` | string | Override the detected commands |
| `release_command` | string | Migration command, run after build and services on every deploy |
| `env_vars` | object | Environment variables written to the app's dotenv file on first deploy |
| `services` | array | Managed services to install and wire in: `postgresql`, `mysql`, `redis`. Requires Build or higher |
| `service_env_keys` | object | Maps an app-specific env key to the service that fills it, e.g. `{"POSTGRES_PRISMA_URL": "postgresql"}`. `DATABASE_URL` and `REDIS_URL` are always written |
| `generate_env` | array | Env keys whose value is a random secret, minted server-side and kept stable across redeploys |
| `self_url_env` | array | Env keys filled with the pod's own public URL at deploy time (`APP_URL`, `NEXTAUTH_URL`) |
| `github_installation_id` | integer | GitHub App installation to clone a private repo with |

Two read-only endpoints support this flow:

| Method | Path | Description |
| - | - | - |
| `POST` | `/api/repos/detect` | Runtime, suggested plan and name for a repo URL, plus what an import does and doesn't carry. Creates nothing |
| `POST` | `/api/repos/analyze` | Full proposed deploy plan: commands, port, services, env vars. Creates nothing |

## Create a Pod From a ZIP

```
POST /api/pods/from-zip
```

Creates a pod from an uploaded project archive, then installs dependencies and runs the production
build on the pod. `multipart/form-data`, not JSON.

| Field | Description |
| - | - |
| `zip` | Required. The project `.zip`. Max **128MB** |
| `name` | Pod name (derived from the filename if omitted) |
| `plan_slug` | Plan (default: `launch`) |
| `region` | Region slug |
| `gemini_api_key` | Optional. Stored as an environment variable for apps that call the Gemini API |

The archive needs a `package.json` or an `index.html` at its top level, or the request is rejected
with `400` - that's how the runtime is detected.

```bash theme={null}
curl -X POST https://app.instapods.com/api/pods/from-zip \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "zip=@my-project.zip" \
  -F "name=my-app"
```

### Re-upload to an Existing Pod

```
POST /api/pods/{name}/from-zip
```

Replaces the source of a pod created by upload and rebuilds it in place - same URL, plan and
environment. Same multipart `zip` field and 128MB limit. A stopped pod is started first.

Returns `400` if the archive detects as a different runtime than the pod runs; create a new pod for
that.

## List Pods

```
GET /api/pods
```

| Query Param | Type | Default | Description |
| - | - | - | - |
| `limit` | int | — | Max pods to return |
| `offset` | int | 0 | Pagination offset |

**Response: `200 OK`**

```json theme={null}
{
  "pods": [{ ... }],
  "total": 12,
  "limit": 25,
  "offset": 0,
  "status_counts": {
    "running": 8,
    "stopped": 3,
    "creating": 1
  }
}
```

## Get a Pod

```
GET /api/pods/{name}
```

Returns the [Pod object](#pod-object) with live status synced from the container runtime.

## Delete a Pod

```
DELETE /api/pods/{name}
```

**Response: `200 OK`**

```json theme={null}
{
  "status": "deleted"
}
```

> [!NOTE]
>
> Pod names are reserved for 7 days after deletion to prevent SSH host key conflicts. Creating a pod with a recently deleted name will return a conflict error.

## Start / Stop / Restart

```
POST /api/pods/{name}/start
POST /api/pods/{name}/stop
POST /api/pods/{name}/restart
```

No request body. Returns the updated [Pod object](#pod-object).

## Reload

```
POST /api/pods/{name}/reload?build=false
```

Installs dependencies, builds the app where the preset has a build step, then restarts the pod's app
services and health-checks them. Dependencies come from whichever manifest is present:
`package.json`, `requirements.txt`, `composer.json` or `go.mod`. If the pod is stopped it is started
first.

| Query parameter | Description |
| - | - |
| `build` | Set to `false` to skip the build step. Only `nodejs` (`npm run build`) and `go` (`go mod download` + `go build`) have one, so it has no effect on the other presets. On `nodejs`, `npm install` still runs, with `--production` |

**Response: `200 OK`**

```json theme={null}
{
  "status": "reloaded",
  "services": ["app"],
  "preset": "nodejs",
  "deps_installed": "npm",
  "build_skipped": true,
  "health": {
    "service_active": true,
    "http_status": 200,
    "url": "https://my-app.nbg1-1.instapods.app"
  }
}
```

| Field | Description |
| - | - |
| `services` | Which systemd services were restarted |
| `preset` | Pod's preset |
| `deps_installed` | `"npm"`, `"pip"`, `"composer"` or `"go mod"` if deps were installed, omitted otherwise |
| `build_skipped` | `true` when the build did not run, either because `build=false` was passed or because the source tree hasn't changed since the last build. Omitted otherwise |
| `health.service_active` | Whether the main service was still running a couple of seconds after the restart |
| `health.http_status` | Status code from an HTTP request to the app. Present for `nodejs`, `python` and `go` only |
| `health.service_log` | Last 15 log lines, present only when the service failed to come back up |
| `health.url` | The pod's public URL, when it has one |

A failed dependency install or build returns `500` with `error` and `deps_output`.

## Resize

```
POST /api/pods/{name}/resize
```

**Request Body:**

```json theme={null}
{
  "plan_slug": "build"
}
```

Returns the updated [Pod object](#pod-object) with new CPU, memory, and disk values from the plan.

| Code | Reason |
| - | - |
| `400` | Invalid plan, disk usage exceeds new quota |
| `409` | Insufficient server resources for upgrade |

## Clone

```
POST /api/pods/{name}/clone
```

**Request Body:**

```json theme={null}
{
  "new_name": "my-app-copy"
}
```

**Response: `201 Created`** — returns the new [Pod object](#pod-object).

## Apply a Deploy Doctor Fix

```
POST /api/pods/{name}/apply-fix
```

Applies the fix for the pod's current [Deploy Doctor](/support/troubleshooting#deploy-doctor)
diagnosis. Owner-only, and only ever on this explicit call - a diagnosis alone never changes a pod.

No request body. The diagnosis itself decides what runs: re-pointing the proxy at the port the app
is really on, rewriting the start command, reinstalling dependencies, switching the Node major, or
converting the pod to a worker.

**Response: `200 OK`**

```json theme={null}
{
  "status": "applied",
  "message": "Re-pointed traffic to port 8080. Your app should be reachable within a minute."
}
```

| Status | Meaning |
| - | - |
| `400` | There's no diagnosis, or the current one has no automatic fix - use the manual steps in `fix` |

The pod's diagnosis is on the pod object as `app_health_diagnosis`, a JSON string with `class`,
`title`, `detail`, `fix`, `auto_fixable`, `confidence` and - when a one-click fix exists -
`fix_action`.

## Switch Node Version

```
POST /api/pods/{name}/node-version
```

Switches a Node.js pod to a different Node major in place, keeping files, environment, domain and
plan. Owner-only, Node.js pods only.

**Request Body:**

```json theme={null}
{ "version": "22" }
```

Supported majors: `18`, `20`, `22`, `24`.

**Response: `200 OK`**

```json theme={null}
{
  "status": "switching",
  "message": "Switching to Node 22 now. This can take a couple of minutes; your app will restart on the new version."
}
```

The install runs in the background, so this returns immediately. Returns `400` if the pod isn't a
Node.js pod, is already on that version, or the version isn't supported.

## Get Preset Config

```
GET /api/pods/{name}/preset
```

Returns the preset configuration for this pod (port, app root, public root, runtime details).

## Get Disk Usage

```
GET /api/pods/{name}/disk-usage
```

Returns current disk usage statistics for the pod.

***

## Run a Command

```
POST /api/pods/{name}/exec
```

Runs a command inside the pod and returns its output. This is the same thing
[`instapods exec`](/cli/exec-and-ssh) uses, so it needs no SSH key. The pod must be running.

**Request Body:**

```json theme={null}
{
  "command": ["npm", "run", "migrate"],
  "workdir": "/home/instapod/app"
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `command` | array | Yes | The command as an argv vector, one element per argument |
| `workdir` | string | No | Directory to run in. A **relative** path resolves against the pod's app root; an absolute one must be within `/home/instapod`, `/var/www` or `/tmp` |

Pass the command as an array, not a string. Each element is escaped before it reaches the pod, so
an argument containing spaces (a quoted SQL statement, a `bash -c` script) arrives intact.

`"workdir": "."` is the app root, the same rule the [`--workdir` flag](/cli/exec-and-ssh) and the
MCP `exec_command` tool's `cwd` follow, so one form works on every surface. Omit `workdir`
entirely and the command runs from `/home/instapod`.

**Response: `200 OK`**

```json theme={null}
{
  "output": "Migrations applied.\n",
  "exit_code": 0
}
```

`output` carries stdout and stderr together, in the order the command wrote them - a failing
command's error message is in there, not dropped.

A command that fails still returns `200` - the non-zero exit is reported in the body, because the
output is usually the thing you wanted:

```json theme={null}
{
  "output": "npm ERR! Missing script: \"migrate\"\n",
  "exit_code": 1,
  "error": "command exited with code 1"
}
```

`exit_code` is the command's **own** exit status, so the usual shell conventions carry through:
`127` is command not found, `126` is found but not executable, `137` is killed for memory. Read it
rather than testing for "non-zero", because those three want different fixes.

`output` is capped at 4 MB per stream. Past that, the middle is cut out - the first 1 MB and the
last 3 MB are kept around a marker line that says how many bytes were omitted - and the reply
carries `"truncated": true`. Redirect bulky output to a file in the pod and read the part you need.

The command runs as the `instapod` user, matching what you get over SSH. Returns `400` if the pod
isn't running, `command` is empty, or `workdir` resolves outside the allowed directories.

## Environment Variables

Reads and writes the pod's dotenv file. Values written here also reach the app's process
environment - see [Environment Variables](/guides/environment-variables) for how, and for the
cases where they do not.

### Get Variables

```
GET /api/pods/{name}/env
```

**Response: `200 OK`**

```json theme={null}
{
  "vars": {
    "NODE_ENV": "production",
    "DATABASE_URL": "postgres://..."
  },
  "path": "/home/instapod/app/.env",
  "warnings": []
}
```

`path` is the file the values were read from, which differs per preset and per 1-Click App. A pod
that has no env file yet returns an empty `vars` object rather than a `404`.

`warnings` is always present. It is non-empty when the variables in the file are **not** reaching
the app's process right now - either the file was edited outside the dashboard, which switches the
injected copy off, or the pod's app service is not wired to read it. Both are repaired by any
`PUT` to this endpoint. A 1-Click App that keeps its own env file, a `static` pod, and a pod with
no variables yet never warn.

### Set Variables

```
PUT /api/pods/{name}/env
```

```json theme={null}
{
  "vars": {
    "LOG_LEVEL": "debug",
    "API_KEY": "sk-..."
  }
}
```

Merges into the existing file: keys you send are updated or appended, keys you don't send are left
alone. Comments and blank lines in the file survive the edit.

**Response: `200 OK`**

```json theme={null}
{
  "status": "updated",
  "path": "/home/instapod/app/.env",
  "count": 2,
  "warnings": []
}
```

Returns `400` if `vars` is missing or empty.

`warnings` is always present. The write always happens; a warning says a value will not be
applied, and why:

* a key InstaPods sets itself - `PORT`, `HOST`, `HOSTNAME` and `NODE_ENV`;
* on a 1-Click App pod, a key the app's own service already fixes (n8n's `N8N_PORT`, for example);
* on a 1-Click App that cannot take environment variables at all (Excalidraw), every key.

Anything else comes back with an empty list.

> [!NOTE]
>
> Writing the file restarts the app - the `app` service on `nodejs`, `python` and `go` pods, the
> app's own service on a 1-Click App pod - so the values are picked up immediately. `static` and
> `php` pods have no such service and nothing is restarted, which is fine: PHP re-reads the file on
> the next request.
>
> The values reach the app two ways: InstaPods puts them in the app's process environment, and the
> file is there for an app that loads it itself. See
> [Environment Variables](/guides/environment-variables).

### Unset Variables

```
DELETE /api/pods/{name}/env
```

```json theme={null}
{
  "keys": ["API_KEY", "LOG_LEVEL"]
}
```

Returns `400` if `keys` is missing or empty, and `404` if the pod has no env file.

## Logs

```
GET /api/pods/{name}/logs
```

Reads the pod's journal.

| Query Param | Type | Default | Description |
| - | - | - | - |
| `lines` | int | `100` | How many entries to return. Capped at `10000`; anything unparseable falls back to the default |
| `service` | string | all | Restrict to one systemd unit, e.g. `app` or `nginx` |
| `grep` | string | none | Search pattern. Max 512 characters |

**Response: `200 OK`**

```json theme={null}
{
  "logs": "Feb 20 10:00:01 my-app app[1234]: Listening on :3000\n..."
}
```

Two behaviours of the underlying `journalctl` carry through and are worth knowing:

* **Smart case.** An all-lowercase `grep` pattern matches case-insensitively; a pattern containing
  any uppercase character matches case-sensitively.
* **Order.** A filtered read returns newest first, the opposite of an unfiltered tail.

`grep` takes a PCRE2 pattern, and it filters the *search* rather than the tail: you get the last
`lines` **matching** entries, not the matches within the last `lines`. A search that matches nothing
is a success, not an error - the body carries journald's own `-- No entries --`. A pattern that
doesn't compile returns `400` with the parser's complaint.

## Events

```
GET /api/pods/{name}/events
```

The pod's activity trail - what the dashboard's Activity tab renders.

| Query Param | Type | Default | Description |
| - | - | - | - |
| `limit` | int | `50` | How many events, newest first. Values outside 1-200 are ignored |

**Response: `200 OK`**

```json theme={null}
[
  {
    "id": "evt_abc123",
    "pod_name": "my-app",
    "type": "deploy_failed",
    "message": "Deployment failed (a1b2c3d) - Build ran out of memory",
    "metadata": "{\"class\":\"build_oom\",\"deployment_id\":\"dep_xyz\",\"sha\":\"a1b2c3d\"}",
    "created_at": "2026-02-20T10:00:00Z"
  }
]
```

Event `type` covers the pod lifecycle (`created`, `started`, `stopped`, `restarted`, `resized`,
`reloaded`, `deleted`), services (`service_install`, `service_running`, `service_removed`), Git
(`git_connected`, `git_disconnected`, `deployment`, `deploy_failed`), domains (`domain_added`,
`domain_removed`, `domain_verified`), SSH keys (`ssh_key_added`, `ssh_key_removed`), environment
changes (`env_updated`), and status (`error`, `progress`). Treat the list as open-ended - new types
are added as features ship.

> [!NOTE]
>
> A deploy that fails is logged as **`deploy_failed`**, not `deployment`. If you are matching on
> `deployment` to find deploys, you will only see the successful ones.

### Event Metadata

`metadata` is an optional JSON **string** carrying structured context for the event. It is omitted
entirely when there is nothing to record, so check before parsing.

The `message` is prose written for a human reading the Activity tab. `metadata` is what you group and
query by, so a failure's cause never has to be parsed back out of English.

Deploy events carry:

| Key | On | Meaning |
| - | - | - |
| `class` | Failed deploys only | The Deploy Doctor verdict, e.g. `build_oom`, `wrong_entrypoint`, `no_application_code` |
| `deployment_id` | Both | Joins back to the full deployment record |
| `sha` | Both | The commit deployed |

`class` is present on **every** failed deploy. When the failure matched no known signature it is the
literal string `unknown` rather than being left out, so you can count unexplained failures rather
than inferring them from absence.

```bash theme={null}
# Group your pod's deploy failures by cause
curl -s -H "Authorization: Bearer $INSTAPOD_TOKEN" \
  https://app.instapods.com/api/pods/my-app/events \
  | jq -r '.[] | select(.type=="deploy_failed") | (.metadata | fromjson).class' \
  | sort | uniq -c
```

## Metrics

```
GET /api/pods/{name}/metrics
```

The most recent sample. Returns `null` if none has been recorded yet, so check before reading
fields.

```json theme={null}
{
  "id": "met_abc123",
  "pod_name": "my-app",
  "cpu_usage": 3.4,
  "memory_usage": 41.2,
  "storage_used": 1073741824,
  "network_in": 5242880,
  "network_out": 10485760,
  "request_count": 0,
  "recorded_at": "2026-02-20T10:00:00Z"
}
```

`cpu_usage` and `memory_usage` are percentages of the pod's own plan allocation. `storage_used`,
`network_in` and `network_out` are bytes.

### Metrics History

```
GET /api/pods/{name}/metrics/history
```

| Query Param | Type | Default | Description |
| - | - | - | - |
| `since` | string | 24 hours ago | RFC 3339 timestamp. An unparseable value falls back to the default |

Returns an array of the same objects, oldest first. This is what the Metrics tab charts.

```bash theme={null}
curl "https://app.instapods.com/api/pods/my-app/metrics/history?since=2026-02-19T00:00:00Z" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

## AI Coding Agents

Installs a terminal coding agent inside the pod, so you can run it over SSH or in the
[web terminal](/dashboard/web-terminal) against your own code. The same thing is available on the
pod's [Dev Tools tab](/dashboard/pod-detail#dev-tools) and from the CLI as `instapods agents`.

### List the Catalog

```
GET /api/agents
```

Every agent you can install, with the presets each one supports.

```json theme={null}
[
  {
    "type": "claude-code",
    "name": "Claude Code",
    "description": "Anthropic's CLI for Claude - an agentic coding tool",
    "runtime": "nodejs",
    "compatible_presets": ["static", "nodejs", "python", "php", "go"],
    "install_cmd": "npm install -g @anthropic-ai/claude-code",
    "verify_cmd": "claude --version"
  }
]
```

### List Installed Agents

```
GET /api/pods/{name}/agents
```

```json theme={null}
[
  {
    "id": "pag_abc123",
    "pod_name": "my-app",
    "agent_type": "claude-code",
    "status": "installed",
    "version": "1.0.44",
    "created_at": "2026-02-20T10:00:00Z",
    "updated_at": "2026-02-20T10:02:00Z"
  }
]
```

**Agent statuses:** `installing`, `installed`, `error`. An `error` entry carries `error_msg`.

### Install an Agent

```
POST /api/pods/{name}/agents
```

```json theme={null}
{ "agent_type": "claude-code" }
```

**Response: `202 Accepted`** - returns the agent record with `status: "installing"`. The install
runs in the background, so poll `GET /api/pods/{name}/agents` until the status settles.

| Code | Reason |
| - | - |
| `400` | Unknown `agent_type`, or the pod isn't running |
| `409` | That agent is already installed on this pod |

> [!NOTE]
>
> Agents are installed, not configured. You still sign in to the agent yourself (with your own API
> key or account) the first time you run it inside the pod. InstaPods never holds those credentials.

### Remove an Agent

```
DELETE /api/pods/{name}/agents/{agentType}
```

**Response: `200 OK`**

```json theme={null}
{ "status": "deleted" }
```

***

## Pod Object

All pod endpoints return this shape:

```json theme={null}
{
  "id": "abc123",
  "name": "my-app",
  "preset": "nodejs",
  "status": "running",
  "ip": "10.0.0.5",
  "domain": "my-app.nbg1-1.instapods.app",
  "ssh_port": 2201,
  "ssh_user": "instapod",
  "cpu": 1,
  "memory": "512MB",
  "disk": "10GB",
  "plan_slug": "launch",
  "app_root": "/home/instapod/app",
  "region": "eu",
  "server_name": "instapod-nbg1-1",
  "description": "",
  "created_at": "2026-02-20T10:00:00Z",
  "updated_at": "2026-02-20T10:00:00Z"
}
```

**Pod statuses:** `creating`, `running`, `stopped`, `deleted`, `error`, `suspended`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.