> ## 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.
</Note>

## 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.
</Note>

## 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).
</Note>

### 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.
</Note>

### 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.
</Note>

### 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.