Public MCP & Agent API

Install first, then let the agent continue

The public flow is simple: install Node Proxy on the target machine, continue with the separate non-root registration command, ask the human to approve the device, then use the loopback-only account API on that machine. The daemon already has the account agent key after approval and uses it internally over the tailnet path when available.

Install

Run the base installer on the target machine

This matches the dashboard install flow. The installer only handles the base install, so it may prompt for sudo or admin access on Unix while it lays down the daemon and system-owned files. Registration stays separate.

Agent initiated

Run this on the device the agent will manage.

curl -fsSL https://nodeproxy.ai/install.sh | sh

PowerShell: iwr -useb https://nodeproxy.ai/install.ps1 | iex

Checksums: View checksums.txt for nodeproxy, nodeproxy-mcp, nodeproxyd, and localsmtp.

After Install

  1. After the installer finishes, run this without sudo to register the device and start a fresh approval flow. Managed mail is enabled automatically unless you pass --enable-mail=false.
    Registration
    nodeproxy up
  2. Then ask the human to approve and authorize the device. Human approval is the trust boundary.
  3. After approval, call http://127.0.0.1:18080/v1/agent/bootstrap on the installed device with X-NodeProxy-Token: <local_api_token> (or Authorization: Bearer <local_api_token>). That loopback-only endpoint returns account bootstrap state without exposing the synced account key.
  4. Use the localhost account and device actions from the manifest to enable WordPress or Immich on that machine, re-enable managed mail later if needed, and perform account operations through the daemon.

Public Manifest

Use https://nodeproxy.ai/mcp-agent-api?format=json for unauthenticated discovery, install guidance, and the full action catalog.

The public JSON manifest never includes agent_api_key.
After approval, call http://127.0.0.1:18080/v1/agent/bootstrap on the installed device with X-NodeProxy-Token: <local_api_token> (or Authorization: Bearer <local_api_token>) to read account bootstrap state through the local daemon.
The localhost actions in that manifest are where the agent can perform account operations and enable WordPress or Immich after the device is approved, or re-enable managed mail later if needed.

Local Account Bootstrap

Call the localhost /v1/agent/bootstrap alias with the daemon-issued local token. The daemon uses its synced account key internally and returns a redacted bootstrap payload.

GET /v1/agent/bootstrap
curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/bootstrap
Bootstrap Response
{
  "status": "ok",
  "next_step": "install_requested_services",
  "agent_api_url": "https://nodeproxy.ai/mcp-agent-api",
  "bootstrap_url": "http://127.0.0.1:18080/v1/agent/bootstrap",
  "agent_api_key_present": true,
  "auth": {
    "type": "local_daemon",
    "description": "The daemon used its synced account key internally; no account key is returned."
  }
}

Other account-scoped API paths are available through the same localhost device API with the same local token.

GET /v1/agent/configs/qwen-code
curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/configs/qwen-code
Bootstrap Response
{
  "next_step": "authorize_device | install_requested_services | configure_addresses_or_urls",
  "devices": [
    {
      "device": { "id": "dev_...", "auth_status": "authorized" },
      "service_requirements": {
        "mail_required": true,
        "mail_configured": false,
        "immich_required": false,
        "wordpress_required": true,
        "wordpress_configured": false,
        "wordpress_public_url": "https://blog-home-abc123.wordpress.test.nodeproxy.ai"
      }
    }
  ]
}

Agent Behavior

Use this sequence to keep the agent path aligned with the normal CLI flow.

Agents should start with curl -fsSL https://nodeproxy.ai/install.sh | sh to complete the base install. That step lays down the daemon and system-owned files, and it may prompt for elevation on Unix.

When the installer finishes, continue with nodeproxy up without sudo to register the device and start a fresh approval flow, then stop until the human approves the device.

After approval, call http://127.0.0.1:18080/v1/agent/bootstrap on the installed device with X-NodeProxy-Token: <local_api_token> (or Authorization: Bearer <local_api_token>) to discover the next account and localhost actions for that machine. The account key stays inside the local daemon.

High-impact account writes can still return 409 approval_required. The human opens the local web control plane on an approved device, chooses MCP & Agent API, reviews the pending approval, then the agent retries the exact same request once. The same queue is also available at http://127.0.0.1:18080/localapi/v0/cloud/approvals.

Advanced Agent Integration

Private integration reference

Node Proxy still includes the nodeproxy-mcp binary and MCP reference data, but that advanced external-agent lane is not part of the current public setup flow. The supported path today is the built-in Node Proxy Copilot plus the hosted Node Proxy API surfaces documented on this page. /mcp-agent-api remains the discovery and reference page; it is not an MCP transport endpoint.

Reference Config

This config shape is kept here as an advanced reference while public support for external model integrations is deferred.

{
  "mcpServers": {
    "nodeproxy": {
      "command": "nodeproxy-mcp",
      "env": {
        "NODEPROXY_LOCAL_URL": "http://127.0.0.1:18080"
      }
    }
  }
}

Add NODEPROXY_LOCAL_URL only when you need to override the default localhost endpoint.

Need the full advanced settings JSON? Call http://127.0.0.1:18080/v1/agent/configs/qwen-code on the installed device with X-NodeProxy-Token; the daemon handles account auth internally.

Current Status

This lane is kept for internal use and future advanced integrations; it is not part of the current public setup flow.

Use the built-in Node Proxy Copilot, install flow, and hosted Node Proxy APIs first.

If you experiment with nodeproxy-mcp, expect setup details to change before public support.

64 Tools

Email, mailboxes, website publishing, WordPress, Immich, DNS, device status, and full account management — all as native MCP tools.

4 Resources

Bootstrap state, device status, mail config, and recent messages — preloaded as context.

3 Prompts

Guided workflows for device setup, email check, and system health — one click to start.

OpenClaw Heartbeat

Use the Node Proxy model endpoint as a dedicated heartbeat model in OpenClaw. The non-fast model variant runs periodic background checks — inbox triage, calendar alerts, proactive notifications — where reasoning quality matters more than latency.

"heartbeat": { "every": "30m", "model": "nodeproxy/Qwen/Qwen3.6-35B-A3B", "target": "last", "lightContext": true }

Add this to agents.defaults in ~/.openclaw/openclaw.json. The generated OpenClaw config from /v1/agent/configs/openclaw already includes this block.

Optional Overrides

NODEPROXY_LOCAL_URLOptional local API URL. Set it only when the MCP client is running on the Node Proxy machine and you want local device tools (e.g., http://127.0.0.1:18080).

Generated Qwen Code and local MCP snippets already pin NODEPROXY_LOCAL_URL. Run nodeproxy-mcp on the Node Proxy machine so account tools use the registered device API path instead of cloud MCP credentials.

API Reference

One reference for agents and humans

The JSON manifest at https://nodeproxy.ai/mcp-agent-api?format=json stays the machine-readable entry point. The sections below are the human-readable anchors behind reference.docs_url and each action's docs_url.

Localhost Account API

Account-scoped endpoints reached through the approved device's local daemon. The daemon uses its synced account key internally and prefers the tailnet upstream.

Bootstrap & Identity

Use this on the approved device through localhost. It is the account-wide source of truth for next_step, next_actions, and per-device requirements, and the daemon keeps the synced account key internal.

GET /v1/agent/bootstrap

Read account state, device requirements, and the next actions the agent should take through the localhost daemon.

URL: http://127.0.0.1:18080/v1/agent/bootstrap

Workflow Note

Call this localhost path on the approved device with X-NodeProxy-Token (or Authorization: Bearer <local_api_token>). The daemon uses its synced account key internally and redacts account key material from the response.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/bootstrap

Example Response

{
  "status": "ok",
  "next_step": "install_requested_services",
  "services_url": "https://example.nodeproxy.ai/devices",
  "settings_url": "https://example.nodeproxy.ai/settings",
  "agent_api_url": "https://example.nodeproxy.ai/mcp-agent-api",
  "bootstrap_url": "http://127.0.0.1:18100/v1/agent/bootstrap",
  "agent_api_key_present": true,
  "auth": { "type": "local_daemon" },
  "devices": [
    {
      "device": {
        "id": "dev_123",
        "auth_status": "authorized",
        "connector_status": "online",
        "mail_service_status": "enabled",
        "immich_status": "enabled",
        "wordpress_status": "disabled"
      },
      "services_url": "https://example.nodeproxy.ai/devices?device_id=dev_123",
      "service_requirements": {
        "mail_required": true,
        "mail_configured": true,
        "managed_addresses": [
          "alice.home-abc123@mail.test.nodeproxy.ai"
        ],
        "immich_required": true,
        "immich_configured": true,
        "immich_url": "https://family-photos.test.nodeproxy.ai",
        "wordpress_required": true,
        "wordpress_configured": false,
        "wordpress_route_id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
        "wordpress_public_url": "https://home-abc123.test.nodeproxy.ai",
        "wordpress_admin_url": "https://home-abc123.test.nodeproxy.ai/wp-admin"
      }
    }
  ]
}

Agent API Keys

Use these through the localhost daemon to list, create, rotate, and revoke account-scoped API keys. Accounts are capped at 30 total keys, including the reserved MCP, localhost OpenAI, Qwen Code, and OpenClaw keys.

GET /v1/agent/api-keys

List all account API keys, including reserved keys for MCP, localhost OpenAI, Qwen Code, and OpenClaw.

URL: http://127.0.0.1:18080/v1/agent/api-keys

Workflow Note

Call this before delete or reserved-key rotation when you need the current key ids, purposes, created timestamps, or last-used timestamps.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/api-keys

Example Response

{
  "status": "ok",
  "limit": 30,
  "active_count": 5,
  "keys": [
    {
      "id": "agk_reserved_mcp",
      "name": "Node Proxy MCP",
      "key": "apk_mcp_reserved",
      "purpose": "mcp_env",
      "reserved": true,
      "created_at": "2026-04-26T09:10:00Z",
      "last_used_at": "2026-04-26T09:25:00Z"
    },
    {
      "id": "agk_reserved_qwen",
      "name": "Qwen Code",
      "key": "apk_qwen_reserved",
      "purpose": "qwen_code",
      "reserved": true,
      "created_at": "2026-04-26T09:10:00Z",
      "last_used_at": "2026-04-26T09:24:00Z"
    },
    {
      "id": "agk_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
      "name": "Billing export",
      "key": "apk_billing_export",
      "reserved": false,
      "created_at": "2026-04-26T09:20:00Z",
      "last_used_at": "2026-04-26T09:21:00Z"
    }
  ]
}
POST /v1/agent/api-keys

Create a new named custom account API key.

URL: http://127.0.0.1:18080/v1/agent/api-keys

Workflow Note

Use named custom keys for revocable automation lanes, external integrations, or future per-key billing attribution.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

name body required

Human-readable name for the new custom key.

Example: Billing export

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/api-keys \
  -d '{
  "name": "Billing export"
}'

Example JSON Body

{
  "name": "Billing export"
}

Example Response

{
  "status": "created",
  "key": {
    "id": "agk_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
    "name": "Billing export",
    "key": "apk_billing_export",
    "reserved": false,
    "created_at": "2026-04-26T09:20:00Z"
  }
}
POST /v1/agent/api-keys

Rotate one reserved account API key by purpose.

URL: http://127.0.0.1:18080/v1/agent/api-keys

Workflow Note

Reserved-key rotation invalidates the previous value immediately. Rewrite the affected localhost or MCP client config right after this call succeeds.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

purpose body required

Reserved key purpose to rotate. Valid values: mcp_env, qwen_code, openclaw, local_openai.

Example: qwen_code

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/api-keys \
  -d '{
  "purpose": "qwen_code"
}'

Example JSON Body

{
  "purpose": "qwen_code"
}

Example Response

{
  "status": "rotated",
  "key": {
    "id": "agk_reserved_qwen",
    "name": "Qwen Code",
    "key": "apk_qwen_reserved_rotated",
    "purpose": "qwen_code",
    "reserved": true,
    "created_at": "2026-04-26T09:10:00Z"
  }
}
DELETE /v1/agent/api-keys/{id}

Delete one custom account API key. Reserved keys must be rotated instead.

URL: http://127.0.0.1:18080/v1/agent/api-keys/{id}

Workflow Note

Only custom keys can be deleted. Reserved purposes stay fixed and must be rotated instead of removed.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

Custom API key id returned by GET or POST /v1/agent/api-keys.

Example: agk_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

curl

curl -sS \
  -X DELETE \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/api-keys/{id}

Example Response

{
  "status": "deleted",
  "id": "agk_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}

Hosted Inference Config

Use these through the localhost daemon after the device is approved. The Qwen Code config points at the device-local OpenAI proxy on localhost, while the OpenClaw config points at the private edge tailnet inference gateway.

GET /v1/agent/configs/qwen-code

Get a ready-to-copy Qwen Code config for the device-local OpenAI proxy that forwards to the hosted Qwen lane.

URL: http://127.0.0.1:18080/v1/agent/configs/qwen-code

Workflow Note

Use this on the registered device when you want Qwen Code to talk to the localhost OpenAI-compatible Node Proxy daemon instead of calling the private edge gateway directly.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/configs/qwen-code

Example Response

{
  "status": "ok",
  "client": "qwen-code",
  "model": "Qwen/Qwen3.6-35B-A3B",
  "fast_model": "Qwen/Qwen3.6-35B-A3B-fast",
  "base_url": "http://127.0.0.1:18080",
  "chat_completions_url": "http://127.0.0.1:18080/v1/chat/completions",
  "config_path": "~/.qwen/settings.json",
  "localhost_only": true,
  "auth_required": true,
  "dummy_api_key_ok": false,
  "api_key": "apk_qwen_reserved",
  "auth_header": "Authorization: Bearer apk_qwen_reserved",
  "config": {
    "$version": 3,
    "modelProviders": {
      "openai": [
        {
          "id": "Qwen/Qwen3.6-35B-A3B",
          "name": "Qwen 3.6 35B A3B",
          "baseUrl": "http://127.0.0.1:18080/v1",
          "description": "Local Node Proxy Qwen 3.6 35B A3B",
          "envKey": "NODEPROXY_KEY"
        },
        {
          "id": "Qwen/Qwen3.6-35B-A3B-fast",
          "name": "Qwen 3.6 35B A3B (Fast)",
          "baseUrl": "http://127.0.0.1:18080/v1",
          "description": "Local Node Proxy Qwen 3.6 35B A3B (Fast)",
          "envKey": "NODEPROXY_KEY"
        }
      ]
    },
    "env": {
      "NODEPROXY_KEY": "apk_qwen_reserved"
    },
    "mcpServers": {
      "nodeproxy": {
        "command": "nodeproxy-mcp",
        "env": {
          "NODEPROXY_LOCAL_URL": "http://127.0.0.1:18080"
        }
      }
    },
    "security": {
      "auth": {
        "selectedType": "openai"
      }
    },
    "model": {
      "name": "Qwen/Qwen3.6-35B-A3B-fast"
    }
  }
}
GET /v1/agent/configs/pi-coder

Get a ready-to-copy Pi Coder bundle: ~/.pi/agent/models.json plus the Node Proxy SKILL.md and extension/index.ts that register every np_* tool natively in Pi.

URL: http://127.0.0.1:18080/v1/agent/configs/pi-coder

Workflow Note

Use this on the registered device to wire the Pi coding agent into Node Proxy. The response includes a generated models.json plus two static asset files (SKILL.md + extension/index.ts) that surface every Node Proxy tool natively in Pi as typed np_* tools.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/configs/pi-coder

Example Response

{
  "status": "ok",
  "client": "pi-coder",
  "model": "Qwen/Qwen3.6-35B-A3B",
  "fast_model": "Qwen/Qwen3.6-35B-A3B-fast",
  "base_url": "http://127.0.0.1:18100",
  "chat_completions_url": "http://127.0.0.1:18100/v1/chat/completions",
  "config_path": "~/.pi/agent/models.json",
  "localhost_only": true,
  "auth_required": true,
  "dummy_api_key_ok": false,
  "api_key": "apk_local_openai_reserved",
  "auth_header": "Authorization: Bearer apk_local_openai_reserved",
  "config_json": "{ ... ~/.pi/agent/models.json contents ... }",
  "files": [
    {
      "path": "~/.pi/agent/models.json",
      "role": "models_config",
      "content_type": "application/json",
      "content": "{ ... }"
    },
    {
      "path": "~/.pi/agent/skills/nodeproxy/SKILL.md",
      "role": "skill",
      "content_type": "text/markdown",
      "content": "---\\nname: nodeproxy\\n... full SKILL.md ..."
    },
    {
      "path": "~/.pi/agent/extensions/nodeproxy/index.ts",
      "role": "extension",
      "content_type": "application/typescript",
      "content": "// Node Proxy extension for Pi.\\n// ... full index.ts ..."
    }
  ]
}
GET /v1/agent/configs/openclaw

Get a ready-to-copy OpenClaw config for the private hosted Qwen inference gateway.

URL: http://127.0.0.1:18080/v1/agent/configs/openclaw

Workflow Note

Use this when you want an external agent runtime to call the private hosted inference lane instead of the local daemon.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/agent/configs/openclaw

Example Response

{
  "status": "ok",
  "client": "openclaw",
  "model": "Qwen/Qwen3.6-35B-A3B",
  "fast_model": "Qwen/Qwen3.6-35B-A3B-fast",
  "base_url": "https://edge.tailnet.test.nodeproxy.ai:8081",
  "chat_completions_url": "https://edge.tailnet.test.nodeproxy.ai:8081/v1/chat/completions",
  "config_path": "~/.openclaw/openclaw.json",
  "auth_header": "Authorization: Bearer apk_openclaw_reserved",
  "config": {
    "agents": {
      "defaults": {
        "model": {
          "primary": "nodeproxy/Qwen/Qwen3.6-35B-A3B-fast"
        },
        "models": {
          "nodeproxy/Qwen/Qwen3.6-35B-A3B": {},
          "nodeproxy/Qwen/Qwen3.6-35B-A3B-fast": {}
        }
      }
    },
    "models": {
      "providers": {
        "nodeproxy": {
          "api": "openai-completions",
          "baseUrl": "https://edge.tailnet.test.nodeproxy.ai:8081/v1",
          "apiKey": "apk_openclaw_reserved",
          "models": [
            {
              "id": "Qwen/Qwen3.6-35B-A3B",
              "name": "Qwen 3.6 35B A3B"
            },
            {
              "id": "Qwen/Qwen3.6-35B-A3B-fast",
              "name": "Qwen 3.6 35B A3B (Fast)"
            }
          ]
        }
      }
    }
  }
}

Managed Mailboxes

After a device is approved, use these endpoints to list, create, and remove Node Proxy-hosted mailbox assignments. This is the same mailbox flow surfaced in Devices and Settings.

GET /v1/managed-addresses

List the managed email addresses currently reserved for this account.

URL: http://127.0.0.1:18080/v1/managed-addresses

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/managed-addresses

Example Response

{
  "addresses": [
    {
      "id": "addr_01HV6M7N8P9Q0R1S2T3U4V5W6X",
      "name": "alice",
      "address": "alice.home-abc123@mail.test.nodeproxy.ai",
      "domain": "mail.test.nodeproxy.ai",
      "device_id": "dev_123",
      "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
      "created_at": "2026-04-03T19:12:24Z"
    }
  ]
}
POST /v1/managed-addresses

Reserve a new managed mailbox and attach it to a specific device.

URL: http://127.0.0.1:18080/v1/managed-addresses

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

name body required

Mailbox local part to reserve before the managed mail domain.

Example: alice

device_id body required

Device ID from bootstrap or local status. The mailbox is attached to this device.

Example: dev_123

vanity body optional

Set true for a paid non-namespaced managed address. Leave false for the included namespaced format.

Example: false

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/managed-addresses \
  -d '{
  "name": "alice",
  "device_id": "dev_123",
  "vanity": false
}'

Example JSON Body

{
  "name": "alice",
  "device_id": "dev_123",
  "vanity": false
}

Example Response

{
  "id": "addr_01HV6M7N8P9Q0R1S2T3U4V5W6X",
  "name": "alice",
  "address": "alice.home-abc123@mail.test.nodeproxy.ai",
  "domain": "mail.test.nodeproxy.ai",
  "device_id": "dev_123",
  "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
  "created_at": "2026-04-03T19:12:24Z"
}
DELETE /v1/managed-addresses/{id}

Remove one managed mailbox assignment.

URL: http://127.0.0.1:18080/v1/managed-addresses/{id}

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

Managed mailbox assignment ID returned by GET or POST /v1/managed-addresses.

Example: addr_01HV6M7N8P9Q0R1S2T3U4V5W6X

curl

curl -sS \
  -X DELETE \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/managed-addresses/addr_01HV6M7N8P9Q0R1S2T3U4V5W6X

Example Response

{
  "status": "deleted"
}

BYOD Mailboxes

Use these once a custom domain is prepared and verified and you want mailbox addresses on that domain delivered to a specific device.

GET /v1/byod-addresses

List BYOD mailbox assignments backed by a user-owned domain.

URL: http://127.0.0.1:18080/v1/byod-addresses

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/byod-addresses

Example Response

{
  "addresses": [
    {
      "id": "byod_01HV6M7N8P9Q0R1S2T3U4V5W6X",
      "name": "alice",
      "address": "alice@example.com",
      "domain": "example.com",
      "device_id": "dev_123",
      "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
      "created_at": "2026-04-03T19:12:24Z"
    }
  ]
}
POST /v1/byod-addresses

Attach a mailbox on a user-owned domain to a specific device.

URL: http://127.0.0.1:18080/v1/byod-addresses

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

name body required

Mailbox local part to reserve on the custom domain.

Example: alice

domain body required

Prepared and verified custom domain that should receive mail through Node Proxy.

Example: example.com

device_id body required

Device ID that should receive mail for this address.

Example: dev_123

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/byod-addresses \
  -d '{
  "name": "alice",
  "domain": "example.com",
  "device_id": "dev_123"
}'

Example JSON Body

{
  "name": "alice",
  "domain": "example.com",
  "device_id": "dev_123"
}

Example Response

{
  "id": "byod_01HV6M7N8P9Q0R1S2T3U4V5W6X",
  "name": "alice",
  "address": "alice@example.com",
  "domain": "example.com",
  "device_id": "dev_123",
  "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
  "created_at": "2026-04-03T19:12:24Z"
}
DELETE /v1/byod-addresses/{id}

Remove one BYOD mailbox assignment.

URL: http://127.0.0.1:18080/v1/byod-addresses/{id}

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

BYOD mailbox assignment ID returned by GET or POST /v1/byod-addresses.

Example: byod_01HV6M7N8P9Q0R1S2T3U4V5W6X

curl

curl -sS \
  -X DELETE \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/byod-addresses/byod_01HV6M7N8P9Q0R1S2T3U4V5W6X

Example Response

{
  "status": "deleted"
}

Public Immich URLs

Use these to inspect, assign, and remove the public Immich hostname that fronts a device, and to keep the explicit public-share image cache manifest in sync with what the edge is allowed to cache.

GET /v1/immich-urls

List the public Immich subdomains currently assigned to devices.

URL: http://127.0.0.1:18080/v1/immich-urls

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/immich-urls

Example Response

{
  "urls": [
    {
      "device_id": "dev_123",
      "device_name": "mac-mini",
      "subdomain": "family-photos",
      "immich_url": "https://family-photos.test.nodeproxy.ai",
      "immich_status": "enabled",
      "cached_public_images": [
        {
          "asset_id": "asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
          "shared_link_key": "share-key-123",
          "shared_link_slug": "public-album",
          "cache_original": true
        }
      ],
      "cached_public_image_count": 1,
      "service_requirements": {
        "immich_required": true,
        "immich_configured": true,
        "immich_url": "https://family-photos.test.nodeproxy.ai"
      }
    }
  ],
  "cache_entitlements": {
    "tier": "free",
    "included_cached_public_images": 3,
    "allows_cached_public_image_overage": false
  }
}
POST /v1/immich-urls

Assign a public Immich subdomain to a specific device.

URL: http://127.0.0.1:18080/v1/immich-urls

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

device_id body required

Device ID that should receive the public Immich hostname.

Example: dev_123

subdomain body required

Public hostname label to place before the Node Proxy photos domain.

Example: family-photos

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/immich-urls \
  -d '{
  "device_id": "dev_123",
  "subdomain": "family-photos"
}'

Example JSON Body

{
  "device_id": "dev_123",
  "subdomain": "family-photos"
}

Example Response

{
  "status": "ok",
  "device_id": "dev_123",
  "device_name": "mac-mini",
  "subdomain": "family-photos",
  "immich_url": "https://family-photos.test.nodeproxy.ai",
  "cached_public_images": [],
  "cached_public_image_count": 0,
  "service_requirements": {
    "immich_required": true,
    "immich_configured": true,
    "immich_url": "https://family-photos.test.nodeproxy.ai"
  }
}
DELETE /v1/immich-urls/{device_id}

Remove the public Immich URL for one device.

URL: http://127.0.0.1:18080/v1/immich-urls/{device_id}

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

device_id path required

Device ID whose public Immich hostname should be removed.

Example: dev_123

curl

curl -sS \
  -X DELETE \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/immich-urls/dev_123

Example Response

{
  "status": "deleted",
  "device_id": "dev_123"
}
POST /v1/immich-urls/{device_id}/cache-manifest

Replace the explicit public-share image cache manifest for one Immich device.

URL: http://127.0.0.1:18080/v1/immich-urls/{device_id}/cache-manifest

Workflow Note

Use this after the agent or local daemon chooses which public-share image asset IDs are worth caching. The edge does not auto-pick images by popularity.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

device_id path required

Device ID that owns the public Immich hostname.

Example: dev_123

images body required

Replacement cache manifest entries. Each entry supplies asset_id plus shared_link_key or shared_link_slug, and may opt into cache_original.

Example: [{"asset_id":"asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM","shared_link_key":"share-key-123","shared_link_slug":"public-album","cache_original":true}]

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/immich-urls/dev_123/cache-manifest \
  -d '{
  "images": [
    {
      "asset_id": "asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
      "shared_link_key": "share-key-123",
      "shared_link_slug": "public-album",
      "cache_original": true
    },
    {
      "asset_id": "asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TN",
      "shared_link_key": "share-key-123",
      "shared_link_slug": "public-album"
    }
  ]
}'

Example JSON Body

{
  "images": [
    {
      "asset_id": "asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
      "shared_link_key": "share-key-123",
      "shared_link_slug": "public-album",
      "cache_original": true
    },
    {
      "asset_id": "asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TN",
      "shared_link_key": "share-key-123",
      "shared_link_slug": "public-album"
    }
  ]
}

Example Response

{
  "status": "ok",
  "device_id": "dev_123",
  "subdomain": "family-photos",
  "immich_url": "https://family-photos.test.nodeproxy.ai",
  "cached_public_images": [
    {
      "asset_id": "asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
      "shared_link_key": "share-key-123",
      "shared_link_slug": "public-album",
      "cache_original": true
    },
    {
      "asset_id": "asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TN",
      "shared_link_key": "share-key-123",
      "shared_link_slug": "public-album"
    }
  ],
  "cached_public_image_count": 2
}
POST /v1/immich-urls/{device_id}/cache-refresh

Purge cached public Immich image responses for one device host or a selected asset set.

URL: http://127.0.0.1:18080/v1/immich-urls/{device_id}/cache-refresh

Workflow Note

Use this to purge stale shared-image thumbnails or originals after the selected public share changes.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

device_id path required

Device ID that owns the public Immich hostname.

Example: dev_123

asset_ids body optional

Optional asset IDs to purge instead of invalidating the full public photo host cache.

Example: ["asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"]

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/immich-urls/dev_123/cache-refresh \
  -d '{
  "asset_ids": ["asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"]
}'

Example JSON Body

{
  "asset_ids": ["asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"]
}

Example Response

{
  "status": "ok",
  "device_id": "dev_123",
  "asset_ids": ["asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"]
}

Website Publishing

Use these to reserve published website hostnames, manage route lifecycle state, and keep the edge HTML cache manifest aligned with the local WordPress site.

GET /v1/website-routes

List public website routes plus website publishing entitlements for the account.

URL: http://127.0.0.1:18080/v1/website-routes

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/website-routes

Example Response

{
  "routes": [
    {
      "id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
      "device_id": "dev_123",
      "hostname": "home-abc123.test.nodeproxy.ai",
      "hostname_kind": "account_scoped",
      "publish_mode": "managed",
      "status": "active",
      "admin_access_mode": "public",
      "cache_manifest_paths": ["/", "/pricing"],
      "cache_manifest_path_count": 2,
      "cache_manifest_synced_at": "2026-04-03T19:12:24Z"
    }
  ],
  "entitlements": {
    "tier": "free",
    "included_routes": 1,
    "html_path_budget": 3,
    "allows_vanity_hostnames": false,
    "allows_custom_domains": false
  },
  "services_root": "/devices"
}
POST /v1/website-routes

Create or reserve a public website route for a device-local WordPress or HTTP origin.

URL: http://127.0.0.1:18080/v1/website-routes

Workflow Note

Reserve the published hostname first, then call the localhost WordPress enable endpoint on the target device so the edge has a live origin to serve.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

device_id body required

Device ID that should receive the published website route.

Example: dev_123

hostname_kind body optional

Route hostname type. Omit for the default account-scoped hostname, or set vanity/custom_domain when the plan allows it.

Example: account_scoped

hostname body optional

Optional vanity or custom-domain hostname.

Example: home-abc123.test.nodeproxy.ai

admin_access_mode body optional

WordPress admin access mode. The MVP default is public same-host admin.

Example: public

cache_manifest_paths body optional

Optional initial HTML cache manifest paths for the route.

Example: ["/","/pricing","/about"]

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/website-routes \
  -d '{
  "device_id": "dev_123",
  "hostname_kind": "account_scoped",
  "admin_access_mode": "public",
  "cache_manifest_paths": ["/", "/pricing", "/about"]
}'

Example JSON Body

{
  "device_id": "dev_123",
  "hostname_kind": "account_scoped",
  "admin_access_mode": "public",
  "cache_manifest_paths": ["/", "/pricing", "/about"]
}

Example Response

{
  "id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "device_id": "dev_123",
  "hostname": "home-abc123.test.nodeproxy.ai",
  "hostname_kind": "account_scoped",
  "publish_mode": "managed",
  "status": "active",
  "admin_access_mode": "public",
  "cache_manifest_paths": ["/", "/pricing"],
  "cache_manifest_path_count": 2,
  "cache_manifest_synced_at": "2026-04-03T19:12:24Z"
}
GET /v1/website-routes/{id}

Read one website route by ID.

URL: http://127.0.0.1:18080/v1/website-routes/{id}

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

Website route ID returned by GET or POST /v1/website-routes.

Example: wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/website-routes/wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

Example Response

{
  "id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "device_id": "dev_123",
  "hostname": "home-abc123.test.nodeproxy.ai",
  "hostname_kind": "account_scoped",
  "publish_mode": "managed",
  "status": "active",
  "admin_access_mode": "public",
  "cache_manifest_paths": ["/", "/pricing"],
  "cache_manifest_path_count": 2,
  "cache_manifest_synced_at": "2026-04-03T19:12:24Z"
}
PATCH /v1/website-routes/{id}

Update a website route's hostname settings, lifecycle status, admin mode, or cache manifest paths.

URL: http://127.0.0.1:18080/v1/website-routes/{id}

Workflow Note

Use this to pause, disable, or retarget a published website route without deleting it.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

Website route ID returned by GET or POST /v1/website-routes.

Example: wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

device_id body optional

Updated device ID for the route target.

Example: dev_123

hostname_kind body optional

Updated hostname type.

Example: account_scoped

hostname body optional

Updated vanity/custom hostname.

Example: home-abc123.test.nodeproxy.ai

status body optional

Lifecycle state such as active, paused, or disabled.

Example: paused

admin_access_mode body optional

Updated WordPress admin access mode.

Example: public

cache_manifest_paths body optional

Optional replacement HTML cache manifest paths.

Example: ["/","/pricing"]

curl

curl -sS \
  -X PATCH \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/website-routes/wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM \
  -d '{
  "status": "paused",
  "admin_access_mode": "public",
  "cache_manifest_paths": ["/", "/pricing"]
}'

Example JSON Body

{
  "status": "paused",
  "admin_access_mode": "public",
  "cache_manifest_paths": ["/", "/pricing"]
}

Example Response

{
  "id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "device_id": "dev_123",
  "hostname": "home-abc123.test.nodeproxy.ai",
  "hostname_kind": "account_scoped",
  "publish_mode": "managed",
  "status": "paused",
  "admin_access_mode": "public",
  "cache_manifest_paths": ["/", "/pricing"],
  "cache_manifest_path_count": 2,
  "cache_manifest_synced_at": "2026-04-03T19:12:24Z"
}
DELETE /v1/website-routes/{id}

Delete one published website route.

URL: http://127.0.0.1:18080/v1/website-routes/{id}

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

Website route ID returned by GET or POST /v1/website-routes.

Example: wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

curl

curl -sS \
  -X DELETE \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/website-routes/wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

Example Response

{
  "status": "deleted",
  "route": {
    "id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
    "hostname": "home-abc123.test.nodeproxy.ai",
    "status": "active"
  }
}
POST /v1/website-routes/{id}/cache-manifest

Replace the authoritative HTML cache manifest for a website route.

URL: http://127.0.0.1:18080/v1/website-routes/{id}/cache-manifest

Workflow Note

Call this after a WordPress publish or local cache-manifest sync so the edge knows which HTML paths are safe to cache.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

Website route ID returned by GET or POST /v1/website-routes.

Example: wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

paths body required

HTML paths that should remain edge-cacheable for this website route.

Example: ["/","/pricing","/about"]

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/website-routes/wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM/cache-manifest \
  -d '{
  "paths": ["/", "/pricing", "/about"]
}'

Example JSON Body

{
  "paths": ["/", "/pricing", "/about"]
}

Example Response

{
  "status": "ok",
  "route": {
    "id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
    "hostname": "home-abc123.test.nodeproxy.ai",
    "cache_manifest_paths": ["/", "/pricing", "/about"],
    "cache_manifest_path_count": 3,
    "cache_manifest_synced_at": "2026-04-03T19:12:24Z"
  },
  "html_path_budget": 3,
  "billable_html_paths": 0,
  "website_edge_profile": "web-free"
}
POST /v1/website-routes/{id}/cache-refresh

Purge cached website responses for a full host or a selected path set.

URL: http://127.0.0.1:18080/v1/website-routes/{id}/cache-refresh

Workflow Note

Use this to purge stale edge cache entries immediately after content or static assets change.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

Website route ID returned by GET or POST /v1/website-routes.

Example: wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

paths body optional

Optional HTML or asset paths to purge. Omit to flush the full host cache for the route.

Example: ["/","/wp-content/themes/site.css"]

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/website-routes/wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM/cache-refresh \
  -d '{
  "paths": ["/", "/wp-content/themes/site.css"]
}'

Example JSON Body

{
  "paths": ["/", "/wp-content/themes/site.css"]
}

Example Response

{
  "status": "ok",
  "route": {
    "id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
    "hostname": "home-abc123.test.nodeproxy.ai"
  },
  "refreshed_paths": ["/", "/wp-content/themes/site.css"],
  "scope": "paths"
}

BYOD DNS

Use these during bring-your-own-domain setup to inspect current MX state, preserve fallback routing, and generate the DNS records the user has to publish.

POST /v1/dns/scan

Inspect current DNS state before changing BYOD mail routing.

URL: http://127.0.0.1:18080/v1/dns/scan

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

domain body required

Custom domain whose MX state should be inspected.

Example: example.com

edge_mx_target body optional

Override the default edge MX host when scanning or testing a specific environment.

Example: edge.test.nodeproxy.ai

route_id body optional

Stable route ID to reuse when scan results should be attached to an existing BYOD route record.

Example: dns_example_com

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/dns/scan \
  -d '{
  "domain": "example.com",
  "edge_mx_target": "edge.test.nodeproxy.ai",
  "route_id": "dns_example_com"
}'

Example JSON Body

{
  "domain": "example.com",
  "edge_mx_target": "edge.test.nodeproxy.ai",
  "route_id": "dns_example_com"
}

Example Response

{
  "domain": "example.com",
  "edge_mx_target": "edge.test.nodeproxy.ai",
  "dns_status": {
    "txt_verify": {
      "status": "missing",
      "value": "nodeproxy-verification=example"
    },
    "mx_to_edge": {
      "status": "pending",
      "value": "10 edge.test.nodeproxy.ai"
    },
    "last_checked_at": "2026-04-03T19:12:24Z"
  },
  "ownership_dns_record": {
    "label": "Ownership TXT",
    "host": "_nodeproxy.example.com",
    "type": "TXT",
    "value": "nodeproxy-verification=example",
    "required": true,
    "status": "pending"
  },
  "existing_mx": [
    {
      "priority": 1,
      "host": "mx1.old-provider.com"
    },
    {
      "priority": 5,
      "host": "mx2.old-provider.com"
    }
  ],
  "fallback_hosts": ["mx1.old-provider.com", "mx2.old-provider.com"],
  "authoritative_hosts": [
    {
      "host": "mx1.old-provider.com",
      "port": 25,
      "tls_mode": "starttls"
    },
    {
      "host": "mx2.old-provider.com",
      "port": 25,
      "tls_mode": "starttls"
    }
  ],
  "route_id": "dns_example_com",
  "status": "ok"
}
POST /v1/dns/fallback

Apply fallback MX routing for a BYOD domain when the user wants Node Proxy in front.

URL: http://127.0.0.1:18080/v1/dns/fallback

Workflow Note

Call this before MX cutover when you want the previous upstream hosts preserved on the route for rollback or passthrough.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

domain body required

Custom domain whose previous MX state should be saved as fallback.

Example: example.com

route_id body optional

Existing route ID returned by scan or prepare. Supplying it makes the fallback update deterministic.

Example: dns_example_com

edge_mx_target body optional

Override the default edge MX host for this route.

Example: edge.test.nodeproxy.ai

previous_mx body optional

Existing upstream MX records to preserve for rollback or passthrough.

Example: [{"priority":1,"host":"mx1.old-provider.com"}]

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/dns/fallback \
  -d '{
  "domain": "example.com",
  "route_id": "dns_example_com",
  "previous_mx": [
    {
      "priority": 1,
      "host": "mx1.old-provider.com"
    },
    {
      "priority": 5,
      "host": "mx2.old-provider.com"
    }
  ]
}'

Example JSON Body

{
  "domain": "example.com",
  "route_id": "dns_example_com",
  "previous_mx": [
    {
      "priority": 1,
      "host": "mx1.old-provider.com"
    },
    {
      "priority": 5,
      "host": "mx2.old-provider.com"
    }
  ]
}

Example Response

{
  "id": "dns_example_com",
  "domain": "example.com",
  "type": "byod",
  "status": "pending_dns",
  "setup_stage": "ready_for_dns",
  "edge_mx_target": "edge.test.nodeproxy.ai",
  "previous_mx": [
    {
      "priority": 1,
      "host": "mx1.old-provider.com"
    },
    {
      "priority": 5,
      "host": "mx2.old-provider.com"
    }
  ],
  "fallback_hosts": ["mx1.old-provider.com", "mx2.old-provider.com"]
}
POST /v1/custom-domains/verify-ownership

Verify the ownership TXT record before preparing passthrough or SES for a BYOD mail domain.

URL: http://127.0.0.1:18080/v1/custom-domains/verify-ownership

Workflow Note

Run this after scan. Preparation should stay blocked until the ownership TXT returns status ok.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

domain body required

Custom domain whose `_nodeproxy.<domain>` TXT record should be verified.

Example: example.com

route_id body optional

Existing route ID returned by scan. If omitted, the server falls back to the route stored for the domain.

Example: dns_example_com

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/custom-domains/verify-ownership \
  -d '{
  "domain": "example.com",
  "route_id": "dns_example_com"
}'

Example JSON Body

{
  "domain": "example.com",
  "route_id": "dns_example_com"
}

Example Response

{
  "status": "ok",
  "domain": "example.com",
  "route_id": "dns_example_com",
  "message": "Domain ownership verified. You can prepare passthrough and SES now.",
  "ownership_dns_record": {
    "label": "Ownership TXT",
    "host": "_nodeproxy.example.com",
    "type": "TXT",
    "value": "nodeproxy-verification=example",
    "required": true,
    "status": "ok"
  }
}
POST /v1/custom-domains/prepare

Prepare SES identity state and the required DNS records for a BYOD mail domain.

URL: http://127.0.0.1:18080/v1/custom-domains/prepare

Workflow Note

Prefer this guided endpoint over hand-writing a full BYOD route. It returns the SES status and the DNS records the user still has to publish.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

domain body required

Custom domain to prepare for Node Proxy-managed inbound mail.

Example: example.com

route_id body optional

Optional stable route ID. If omitted, the server derives one from the domain.

Example: dns_example_com

edge_mx_target body optional

Override the default edge MX host for this domain.

Example: edge.test.nodeproxy.ai

mail_from_domain body optional

Custom subdomain SES should use for MAIL FROM / bounce handling.

Example: bounce.example.com

existing_mx body optional

Known current MX records. Supplying them avoids another live MX lookup and preserves rollback context.

Example: [{"priority":1,"host":"mx1.old-provider.com"}]

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/custom-domains/prepare \
  -d '{
  "domain": "example.com",
  "route_id": "dns_example_com",
  "mail_from_domain": "bounce.example.com",
  "existing_mx": [
    {
      "priority": 1,
      "host": "mx1.old-provider.com"
    }
  ]
}'

Example JSON Body

{
  "domain": "example.com",
  "route_id": "dns_example_com",
  "mail_from_domain": "bounce.example.com",
  "existing_mx": [
    {
      "priority": 1,
      "host": "mx1.old-provider.com"
    }
  ]
}

Example Response

{
  "status": "ok",
  "domain": "example.com",
  "route_id": "dns_example_com",
  "message": "Custom domain prepared. Update the DNS records below, then continue to verification.",
  "route": {
    "id": "dns_example_com",
    "domain": "example.com",
    "type": "byod",
    "status": "pending_dns",
    "setup_stage": "ready_for_dns",
    "authoritative_host": {
      "host": "mx1.old-provider.com",
      "port": 25,
      "tls_mode": "starttls"
    },
    "authoritative_hosts": [
      {
        "host": "mx1.old-provider.com",
        "port": 25,
        "tls_mode": "starttls"
      },
      {
        "host": "mx2.old-provider.com",
        "port": 25,
        "tls_mode": "starttls"
      }
    ]
  },
  "ses_status": {
    "region": "us-west-2",
    "verification_status": "PENDING",
    "dkim_status": "PENDING",
    "mail_from_status": "PENDING",
    "verified_for_sending": false,
    "last_checked_at": "2026-04-03T19:12:24Z"
  },
  "required_dns_records": [
    {
      "label": "Inbound MX",
      "host": "example.com",
      "type": "MX",
      "value": "edge.test.nodeproxy.ai",
      "priority": 10,
      "ttl": 300,
      "category": "mx_to_edge",
      "required": true
    },
    {
      "label": "MAIL FROM MX",
      "host": "bounce.example.com",
      "type": "MX",
      "value": "feedback-smtp.us-west-2.amazonses.com",
      "priority": 10,
      "ttl": 300,
      "category": "mail_from",
      "required": true
    }
  ]
}

BYOD Routing

These are the lower-level BYOD route records. Most guided flows should prepare the domain first, then read or patch the resulting route.

GET /v1/email-routes

List BYOD routing records and their DNS status.

URL: http://127.0.0.1:18080/v1/email-routes

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/email-routes

Example Response

{
  "routes": [
    {
      "id": "route_01HV6M7N8P9Q0R1S2T3U4V5W6X",
      "domain": "example.com",
      "type": "byod",
      "status": "pending_dns",
      "setup_stage": "ready_for_dns",
      "connector_id": "dev_123",
      "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
      "edge_mx_target": "edge.test.nodeproxy.ai"
    }
  ]
}
POST /v1/email-routes

Create a BYOD mail route for a domain and target connector.

URL: http://127.0.0.1:18080/v1/email-routes

Workflow Note

This is the low-level route record endpoint. Most agents should use POST /v1/custom-domains/prepare first and only patch the route when specific fields change.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

domain body required

Domain this route should receive mail for.

Example: example.com

type body required

Route type. Use byod for custom domains or subdomain for Node Proxy-managed domains.

Example: byod

connector_id body required

Target device ID that should receive the routed mail.

Example: dev_123

connector_smtp_addr body required

SMTP address the edge should forward to for this route.

Example: dev-123.tailnet.example.ts.net:2525

delivery_mode body optional

Inbound delivery mode for the route. split is typical for BYOD fallback routing.

Example: split

edge_mx_target body optional

MX hostname the domain should eventually point at.

Example: edge.test.nodeproxy.ai

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/email-routes \
  -d '{
  "domain": "example.com",
  "type": "byod",
  "connector_id": "dev_123",
  "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
  "delivery_mode": "split",
  "edge_mx_target": "edge.test.nodeproxy.ai"
}'

Example JSON Body

{
  "domain": "example.com",
  "type": "byod",
  "connector_id": "dev_123",
  "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
  "delivery_mode": "split",
  "edge_mx_target": "edge.test.nodeproxy.ai"
}

Example Response

{
  "id": "route_01HV6M7N8P9Q0R1S2T3U4V5W6X",
  "domain": "example.com",
  "type": "byod",
  "status": "active",
  "setup_stage": "ready",
  "connector_id": "dev_123",
  "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
  "delivery_mode": "split",
  "mail_from_domain": "bounce.example.com",
  "edge_mx_target": "edge.test.nodeproxy.ai"
}
PATCH /v1/email-routes/{id}

Update a BYOD mail route without recreating it from scratch.

URL: http://127.0.0.1:18080/v1/email-routes/{id}

Workflow Note

This is the low-level route record endpoint. Most agents should use POST /v1/custom-domains/prepare first and only patch the route when specific fields change.

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

id path required

Route ID returned by list, create, or prepare.

Example: route_01HV6M7N8P9Q0R1S2T3U4V5W6X

connector_id body optional

Updated device ID for the route target.

Example: dev_123

connector_smtp_addr body optional

Updated SMTP target for the route.

Example: dev-123.tailnet.example.ts.net:2525

status body optional

New route lifecycle status such as pending_dns or active.

Example: active

mail_from_domain body optional

Updated SES MAIL FROM subdomain for the route.

Example: bounce.example.com

curl

curl -sS \
  -X PATCH \
  -H "Content-Type: application/json" \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/email-routes/route_01HV6M7N8P9Q0R1S2T3U4V5W6X \
  -d '{
  "connector_id": "dev_123",
  "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
  "status": "active"
}'

Example JSON Body

{
  "connector_id": "dev_123",
  "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
  "status": "active"
}

Example Response

{
  "id": "route_01HV6M7N8P9Q0R1S2T3U4V5W6X",
  "domain": "example.com",
  "type": "byod",
  "status": "active",
  "setup_stage": "ready",
  "connector_id": "dev_123",
  "connector_smtp_addr": "dev-123.tailnet.example.ts.net:2525",
  "delivery_mode": "split",
  "mail_from_domain": "bounce.example.com",
  "edge_mx_target": "edge.test.nodeproxy.ai"
}

Mail Spam Controls

Account-scoped endpoints proxied by the localhost daemon for the public agent workflow.

GET /v1/mail/spam-rules

List account-level spam block rules enforced by the edge.

URL: http://127.0.0.1:18080/v1/mail/spam-rules

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/mail/spam-rules
POST /v1/mail/spam-rules

Create or reactivate an account-level sender or domain block rule. Body: kind=sender|domain, value, optional action=reject, note, and source message IDs.

URL: http://127.0.0.1:18080/v1/mail/spam-rules

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/mail/spam-rules
DELETE /v1/mail/spam-rules/{id}

Delete one account-level spam block rule.

URL: http://127.0.0.1:18080/v1/mail/spam-rules/{id}

Auth

Call through the approved device's localhost API with X-NodeProxy-Token: nplocal_... or Authorization: Bearer <local_api_token>. The daemon uses its synced account key internally.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X DELETE \
  -H "X-NodeProxy-Token: nplocal_your_local_token" \
  http://127.0.0.1:18080/v1/mail/spam-rules/{id}

Localhost Device API

Device-scoped loopback endpoints for registration, status, and local services after the base install.

Registration & Status

Use these from localhost to start device approval, poll registration progress, and inspect live device state across mail, WordPress, and Immich.

GET /localapi/v0/nodeproxy/status

Read the same live JSON snapshot returned by nodeproxy status, including environment, connector, mail, maildir_root, WordPress, Immich, and requirement state.

URL: http://127.0.0.1:18080/localapi/v0/nodeproxy/status

Workflow Note

Read this before and after local service changes when you need the current device, tunnel, mail, WordPress, or Immich state.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/nodeproxy/status

Example Response

{
  "nodeproxy": {
    "environment": "test",
    "namespace": "test",
    "local_api_base": "http://127.0.0.1:18100",
    "status_url": "http://127.0.0.1:18100/localapi/v0/nodeproxy/status",
    "equivalent_cli_command": "nodeproxy --namespace test status"
  },
  "device": {
    "hostname": "mac-mini",
    "device_id": "dev_123",
    "version": "0.1.0",
    "os": "darwin"
  },
  "connector": {
    "status": "online",
    "last_seen": "2026-04-03T19:12:24Z",
    "edge_url": "https://edge.test.nodeproxy.ai"
  },
  "tailnet": {
    "status": "online",
    "last_ok": "2026-04-03T19:12:22Z",
    "can_rejoin": true
  },
  "mail": {
    "status": "enabled",
    "imap_addr": "127.0.0.1:143",
    "smtp_submission_addr": "127.0.0.1:587",
    "maildir_root": "/var/lib/nodeproxy/maildir-test"
  },
  "immich": {
    "status": "enabled",
    "listen_url": "http://127.0.0.1:2283"
  },
  "wordpress": {
    "status": "disabled",
    "listen_url": "http://127.0.0.1:18180",
    "public_hostname": "home-abc123.test.nodeproxy.ai",
    "admin_access_mode": "public"
  },
  "account_email": "owner@example.com",
  "services_url": "https://example.nodeproxy.ai/devices?device_id=dev_123",
  "agent_api_url": "https://example.nodeproxy.ai/mcp-agent-api",
  "service_requirements": {
    "mail_required": true,
    "mail_configured": true,
    "managed_addresses": [
      "alice.home-abc123@mail.test.nodeproxy.ai"
    ],
    "immich_required": true,
    "immich_configured": true,
    "immich_url": "https://family-photos.test.nodeproxy.ai",
    "wordpress_required": true,
    "wordpress_configured": false,
    "wordpress_route_id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
    "wordpress_public_url": "https://home-abc123.test.nodeproxy.ai"
  }
}
POST /localapi/v0/login/start

Start a fresh device registration and approval flow.

URL: http://127.0.0.1:18080/localapi/v0/login/start

Workflow Note

This starts a new device approval flow and returns the browser URL the human must open.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/login/start

Example Response

{
  "auth_url": "https://example.nodeproxy.ai/device/verify?code=01HVLOGIN&device_id=dev_123&device_name=mac-mini"
}
GET /localapi/v0/login/status

Poll device approval state after registration starts.

URL: http://127.0.0.1:18080/localapi/v0/login/status

Workflow Note

Poll until status leaves pending. preauth_key only appears after authorization succeeds.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/login/status

Example Response

{
  "status": "authorized",
  "account_email": "alice@example.com",
  "preauth_key": "tskey-auth-example",
  "preauth_key_expires_at": "2026-04-03T20:12:24Z"
}

Local Mail Service

Use these from localhost to enable or disable the managed mail runtime and fetch client settings after install.

POST /localapi/v0/mail/enable

Enable the local managed mail runtime on the installed device.

URL: http://127.0.0.1:18080/localapi/v0/mail/enable

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/mail/enable

Example Response

{
  "status": "ok",
  "message": "Managed mail services enabled."
}
POST /localapi/v0/mail/disable

Disable the local managed mail runtime without removing the account assignment.

URL: http://127.0.0.1:18080/localapi/v0/mail/disable

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/mail/disable

Example Response

{
  "status": "ok",
  "message": "Managed mail services disabled."
}
POST /localapi/v0/mail/notifications/enable

Enable background desktop notifications for new inbound mail on the device.

URL: http://127.0.0.1:18080/localapi/v0/mail/notifications/enable

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/mail/notifications/enable

Example Response

{
  "status": "ok",
  "message": "Desktop mail notifications enabled."
}
POST /localapi/v0/mail/notifications/disable

Disable background desktop notifications for new inbound mail on the device.

URL: http://127.0.0.1:18080/localapi/v0/mail/notifications/disable

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/mail/notifications/disable

Example Response

{
  "status": "ok",
  "message": "Desktop mail notifications disabled."
}
GET /localapi/v0/mail/client-config

Return IMAP and SMTP details for local mail clients. Pass address when the device has multiple assigned mailboxes.

URL: http://127.0.0.1:18080/localapi/v0/mail/client-config

Workflow Note

This only succeeds after local mail is enabled on the device. When a device has multiple mailbox addresses, pass the address query to get the config for the specific mailbox being set up.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

address query optional

Specific assigned email address to configure. Use this when the device has multiple mailbox assignments.

Example: alerts@example.com

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/client-config

Example Response

{
  "status": "ok",
  "account_name": "alice.home-abc123@mail.test.nodeproxy.ai",
  "imap": {
    "host": "127.0.0.1",
    "port": 143,
    "security": "none",
    "auth_method": "password",
    "username": "alice.home-abc123@mail.test.nodeproxy.ai",
    "password": "local"
  },
  "smtp": {
    "host": "127.0.0.1",
    "port": 587,
    "security": "none",
    "auth_method": "none",
    "username": "",
    "password": ""
  },
  "identity": {
    "email": "alice.home-abc123@mail.test.nodeproxy.ai",
    "display_name": ""
  },
  "managed_addresses": [
    "alice.home-abc123@mail.test.nodeproxy.ai"
  ],
  "byod_addresses": [],
  "autoconfig_url": "http://127.0.0.1:18101/localapi/v0/mail/autoconfig.xml"
}

Local Mailbox

Use these from localhost to list, read, send, reply to, delete, and mark mailbox content on the device.

GET /localapi/v0/mail/messages

List local mail messages, with optional search and filters. Pass mailbox to target a non-default folder (in address-mailbox mode, each assigned address is its own folder).

URL: http://127.0.0.1:18080/localapi/v0/mail/messages

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

mailbox query optional

Mailbox folder to read. Defaults to INBOX. In address-mailbox mode, pass an assigned address (for example alerts@example.com) to read that address's dedicated folder.

Example: INBOX

limit query optional

Maximum number of messages to return. Defaults to 20 and caps at 100.

Example: 20

offset query optional

Number of matching messages to skip before returning results.

Example: 0

search query optional

Case-insensitive filter applied to subject, from, and preview text.

Example: invoice

unread_only query optional

Set true to return only unread messages.

Example: true

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/messages?mailbox=INBOX&limit=20&search=invoice&unread_only=true

Example Response

{
  "mailbox": "INBOX",
  "total": 1,
  "messages": [
    {
      "id": "1743700929.1.testhost",
      "proxy_message_id": "msg_01KNA97WGYC1ATSWR92GZW40CN",
      "mailbox": "INBOX",
      "subject": "test email",
      "from": "Jane Doe <jane@example.com>",
      "to": ["agent@mail.test.nodeproxy.ai"],
      "date": "Fri, 03 Apr 2026 18:30:00 +0000",
      "size_bytes": 842,
      "flags": ["\\Seen"],
      "preview": "Hello from the test message."
    }
  ]
}
GET /localapi/v0/mail/messages/{id}

Read one local mail message by canonical mailbox_id. Returns extracted bodies, attachment reveal_file_url/open_file_url actions for local-file access (prefer reveal first; raw open/download URLs are also included), and a lightweight header+attachment risk summary. Pass the mailbox query param that the id was listed under when reading from a non-default folder.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

id path required

Canonical mailbox_id from GET /localapi/v0/mail/messages or from the mailbox_id field returned by GET /localapi/v0/mail/recent or /stream. Proxy message IDs and Internet Message-ID values are accepted as compatibility aliases, but mailbox_id is the preferred identifier.

Example: 1743700929.1.testhost

mailbox query optional

Mailbox folder the id was listed under. Defaults to INBOX. Pass the address you used with list_messages when the id came from a non-default per-address folder. Inbound event payloads already encode this as part of message_url / reply_url / delete_url / mark_url.

Example: INBOX

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/messages/1743700929.1.testhost

Example Response

{
  "id": "1743700929.1.testhost",
  "proxy_message_id": "msg_01KNA97WGYC1ATSWR92GZW40CN",
  "mailbox": "INBOX",
  "subject": "test email",
  "from": "Jane Doe <jane@example.com>",
  "to": ["agent@mail.test.nodeproxy.ai"],
  "cc": [],
  "date": "Fri, 03 Apr 2026 18:30:00 +0000",
  "message_id": "<reply-target@example.com>",
  "in_reply_to": "",
  "references": [],
  "headers": "Subject: test email\r\nFrom: Jane Doe <jane@example.com>\r\n",
  "body_text": "Hello from the test message.",
  "body_html": "",
  "attachments": [{
    "index": 0,
    "filename": "invoice.pdf",
    "content_type": "application/pdf",
    "size_bytes": 4096,
    "open_url": "http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost/attachments/0",
    "download_url": "http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost/attachments/0?download=1",
    "open_file_url": "http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost/attachments/0/open-file",
    "reveal_file_url": "http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost/attachments/0/reveal-file",
    "risk": {"level":"medium","summary":"The attachment has some warning signs worth checking before opening.","method":"heuristic"}
  }],
  "risk": {
    "level": "medium",
    "summary": "Some caution signals were found in the message headers, links, or attachments.",
    "method": "header+heuristic",
    "authentication": {"spf":"fail","dkim":"pass","dmarc":"pass"},
    "spam": {"verdict":"Yes","score":"7.4","action":"add header","symbols":["PHISHING"]}
  },
  "flags": ["\\Seen"],
  "size_bytes": 842
}
GET /localapi/v0/mail/messages/{id}/attachments/{index}

Stream one decoded attachment from a local mail message by zero-based attachment index. For device-local workflows, prefer the open-file or reveal-file actions below. Pass the mailbox query param that the id was listed under for non-default folders, and pass download=1 to force download.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/attachments/{index}

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/attachments/{index}

Example Response

Binary attachment response

Headers:
Content-Type: application/pdf
Content-Disposition: inline; filename=invoice.pdf

Body: decoded attachment bytes
GET /localapi/v0/mail/messages/{id}/attachments/{index}/reveal-file

Preferred default: export one decoded attachment to a stable local file path and open the containing folder, selecting the file when the host OS supports that behavior. Pass the mailbox query param that the id was listed under for non-default folders.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/attachments/{index}/reveal-file

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/attachments/{index}/reveal-file

Example Response

{
  "status": "ok",
  "action": "reveal-file",
  "path": "/Users/localuser/Downloads/NodeProxy Attachments/INBOX/1743700929.1.testhost/attachment-0/invoice.pdf",
  "filename": "invoice.pdf",
  "selected": true,
  "message": "Opened Finder and selected the attachment."
}
GET /localapi/v0/mail/messages/{id}/attachments/{index}/open-file

Secondary direct-open action: export one decoded attachment to a stable local file path and open it with the desktop app associated with that file type. Pass the mailbox query param that the id was listed under for non-default folders.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/attachments/{index}/open-file

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/attachments/{index}/open-file

Example Response

{
  "status": "ok",
  "action": "open-file",
  "path": "/Users/localuser/Downloads/NodeProxy Attachments/INBOX/1743700929.1.testhost/attachment-0/invoice.pdf",
  "filename": "invoice.pdf",
  "selected": false,
  "message": "Opened the attachment with the default desktop app."
}
POST /localapi/v0/mail/send

Send a new email through the local submission flow.

URL: http://127.0.0.1:18080/localapi/v0/mail/send

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

from body optional

Optional sender address. If omitted, the local mailbox identity is used.

Example: alice.home-abc123@mail.test.nodeproxy.ai

to body required

Primary recipients. At least one address is required.

Example: ["friend@example.net"]

cc body optional

Optional CC recipients.

Example: ["team@example.net"]

bcc body optional

Optional BCC recipients.

Example: ["audit@example.net"]

subject body optional

Subject line for the outgoing message.

Example: Daily summary

body_text body optional

Plain-text message body.

Example: Everything completed successfully.

body_html body optional

Optional HTML message body.

Example: <p>Everything completed successfully.</p>

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/mail/send \
  -d '{
  "from": "alice.home-abc123@mail.test.nodeproxy.ai",
  "to": ["friend@example.net"],
  "cc": ["team@example.net"],
  "subject": "Daily summary",
  "body_text": "Everything completed successfully."
}'

Example JSON Body

{
  "from": "alice.home-abc123@mail.test.nodeproxy.ai",
  "to": ["friend@example.net"],
  "cc": ["team@example.net"],
  "subject": "Daily summary",
  "body_text": "Everything completed successfully."
}

Example Response

{
  "status": "ok",
  "message_id": "<1712171544000000000.01HVMAIL@agent-proxy>"
}
POST /localapi/v0/mail/messages/{id}/reply

Reply to a specific local mail message by canonical mailbox_id. Pass the mailbox query param that the id was listed under when replying to a message in a non-default folder.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/reply

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

id path required

Canonical mailbox_id from GET /localapi/v0/mail/messages or from the mailbox_id field returned by GET /localapi/v0/mail/recent or /stream. Proxy message IDs and Internet Message-ID values are accepted as compatibility aliases, but mailbox_id is the preferred identifier.

Example: 1743700929.1.testhost

mailbox query optional

Mailbox folder the id was listed under. Defaults to INBOX. Pass the address you used with list_messages when the id came from a non-default per-address folder. Inbound event payloads already encode this as part of message_url / reply_url / delete_url / mark_url.

Example: INBOX

body_text body optional

Plain-text reply body. Supply this, body_html, or both.

Example: Received your test email, thank you!

body_html body optional

Optional HTML reply body.

Example: <p>Received your test email, thank you!</p>

reply_all body optional

Set true to include the original To recipients when replying.

Example: false

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/mail/messages/1743700929.1.testhost/reply \
  -d '{
  "body_text": "Received your test email, thank you!",
  "reply_all": false
}'

Example JSON Body

{
  "body_text": "Received your test email, thank you!",
  "reply_all": false
}

Example Response

{
  "status": "ok",
  "message_id": "<1712171544000000000.01HVMAIL@agent-proxy>"
}
DELETE /localapi/v0/mail/messages/{id}

Delete one local mail message by canonical mailbox_id. Pass the mailbox query param that the id was listed under when deleting from a non-default folder.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

id path required

Canonical mailbox_id from GET /localapi/v0/mail/messages or from the mailbox_id field returned by GET /localapi/v0/mail/recent or /stream. Proxy message IDs and Internet Message-ID values are accepted as compatibility aliases, but mailbox_id is the preferred identifier.

Example: 1743700929.1.testhost

mailbox query optional

Mailbox folder the id was listed under. Defaults to INBOX. Pass the address you used with list_messages when the id came from a non-default per-address folder. Inbound event payloads already encode this as part of message_url / reply_url / delete_url / mark_url.

Example: INBOX

curl

curl -sS \
  -X DELETE \
  http://127.0.0.1:18080/localapi/v0/mail/messages/1743700929.1.testhost

Example Response

{
  "status": "ok",
  "message": "Message deleted."
}
POST /localapi/v0/mail/messages/{id}/mark

Mark a local message as read or unread by canonical mailbox_id. Pass the mailbox query param that the id was listed under when marking a message in a non-default folder.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/mark

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

id path required

Canonical mailbox_id from GET /localapi/v0/mail/messages or from the mailbox_id field returned by GET /localapi/v0/mail/recent or /stream. Proxy message IDs and Internet Message-ID values are accepted as compatibility aliases, but mailbox_id is the preferred identifier.

Example: 1743700929.1.testhost

mailbox query optional

Mailbox folder the id was listed under. Defaults to INBOX. Pass the address you used with list_messages when the id came from a non-default per-address folder. Inbound event payloads already encode this as part of message_url / reply_url / delete_url / mark_url.

Example: INBOX

read body required

true marks the message read; false marks it unread.

Example: true

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/mail/messages/1743700929.1.testhost/mark \
  -d '{
  "read": true
}'

Example JSON Body

{
  "read": true
}

Example Response

{
  "status": "ok",
  "flags": ["\\Seen"]
}
GET /localapi/v0/mail/spam-rules

List account-level spam block rules from the local daemon's account proxy.

URL: http://127.0.0.1:18080/localapi/v0/mail/spam-rules

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/spam-rules
POST /localapi/v0/mail/messages/{id}/report-spam

Report a local message as spam by canonical mailbox_id, train the account spam model, and create sender/domain block rules from its From header. Defaults to exact-sender blocking; pass block_domain=true only for domain-wide blocking.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/report-spam

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/report-spam
POST /localapi/v0/mail/messages/{id}/report-ham

Train a local message as ham for the account spam model by canonical mailbox_id.

URL: http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/report-ham

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/mail/messages/{id}/report-ham
DELETE /localapi/v0/mail/spam-rules/{id}

Delete one account-level spam block rule through the local daemon account proxy.

URL: http://127.0.0.1:18080/localapi/v0/mail/spam-rules/{id}

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X DELETE \
  http://127.0.0.1:18080/localapi/v0/mail/spam-rules/{id}

Local Immich

Use these from localhost to install, start, stop, or uninstall the local Immich runtime and to manage local albums plus shared-link maintenance workflows.

POST /localapi/v0/immich/enable

Install or start the local Immich runtime.

URL: http://127.0.0.1:18080/localapi/v0/immich/enable

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

base_dir body optional

Filesystem directory where Immich data and docker-compose files should live. When omitted, the server chooses a default sibling directory next to the local state file.

Example: /var/lib/nodeproxy/immich

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/immich/enable \
  -d '{
  "base_dir": "/var/lib/nodeproxy/immich"
}'

Example JSON Body

{
  "base_dir": "/var/lib/nodeproxy/immich"
}

Example Response

{
  "status": "ok",
  "message": "Immich enabled and running."
}
POST /localapi/v0/immich/disable

Stop the local Immich runtime without deleting data.

URL: http://127.0.0.1:18080/localapi/v0/immich/disable

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/immich/disable

Example Response

{
  "status": "ok",
  "message": "Immich stopped."
}
POST /localapi/v0/immich/delete

Uninstall Immich by stopping the service and permanently deleting local data.

URL: http://127.0.0.1:18080/localapi/v0/immich/delete

Workflow Note

This uninstalls the local Immich stack after stopping it and permanently removing local data. Re-enable later to reinstall from scratch.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/immich/delete

Example Response

{
  "status": "ok",
  "message": "Immich uninstalled."
}
GET /localapi/v0/immich/albums

List albums, optionally filtering by asset_id or shared state.

URL: http://127.0.0.1:18080/localapi/v0/immich/albums

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

asset_id query optional

Optional asset ID filter. Only return albums that contain this asset.

Example: asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

shared query optional

Optional shared-state filter. true returns only shared albums; false returns owned non-shared albums.

Example: true

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/immich/albums?asset_id=asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM&shared=true

Example Response

{
  "status": "ok",
  "albums": [
    {
      "id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
      "albumName": "Trips 2026",
      "assetCount": 42,
      "shared": true
    }
  ]
}
GET /localapi/v0/immich/albums

Read one album by id.

URL: http://127.0.0.1:18080/localapi/v0/immich/albums

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

id query required

Album ID to retrieve.

Example: alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

key query optional

Optional shared-link key passthrough for album fetches that need it.

Example: share-key-123

slug query optional

Optional shared-link slug passthrough for album fetches that need it.

Example: public-album

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/immich/albums?id=alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

Example Response

{
  "status": "ok",
  "album": {
    "id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
    "albumName": "Trips 2026",
    "description": "Vacation favorites",
    "assetCount": 42,
    "shared": true
  }
}
POST /localapi/v0/immich/albums/update

Update album metadata such as name, description, thumbnail, activity-feed state, or order.

URL: http://127.0.0.1:18080/localapi/v0/immich/albums/update

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

album_id body required

Album ID to update.

Example: alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

name body optional

Updated album name.

Example: Trips 2026

description body optional

Updated album description.

Example: Vacation favorites

album_thumbnail_asset_id body optional

Optional replacement thumbnail asset ID.

Example: asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

is_activity_enabled body optional

Enable or disable the album activity feed.

Example: false

order body optional

Optional sort order string accepted by Immich.

Example: desc

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/immich/albums/update \
  -d '{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "name": "Trips 2026",
  "description": "Vacation favorites",
  "is_activity_enabled": false
}'

Example JSON Body

{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "name": "Trips 2026",
  "description": "Vacation favorites",
  "is_activity_enabled": false
}

Example Response

{
  "status": "ok",
  "album": {
    "id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
    "albumName": "Trips 2026",
    "description": "Vacation favorites",
    "assetCount": 42,
    "shared": true
  }
}
POST /localapi/v0/immich/albums/remove-assets

Remove one or more assets from an album.

URL: http://127.0.0.1:18080/localapi/v0/immich/albums/remove-assets

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

album_id body required

Album ID to modify.

Example: alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

asset_ids body required

One or more asset IDs to remove from the album.

Example: ["asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"]

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/immich/albums/remove-assets \
  -d '{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "asset_ids": ["asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"]
}'

Example JSON Body

{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "asset_ids": ["asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"]
}

Example Response

{
  "status": "ok",
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "result": [{"id":"asset_01JZ3W8KPXKQ2GMSY5P9J7Q2TM","success":true}]
}
POST /localapi/v0/immich/albums/remove-user

Remove one collaborator from a shared album.

URL: http://127.0.0.1:18080/localapi/v0/immich/albums/remove-user

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

album_id body required

Album ID to modify.

Example: alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

user_id body required

User ID to remove from the shared album. Immich also accepts "me" to leave a shared album.

Example: usr_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/immich/albums/remove-user \
  -d '{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "user_id": "usr_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}'

Example JSON Body

{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "user_id": "usr_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}

Example Response

{
  "status": "ok",
  "removed": true,
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "user_id": "usr_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}
POST /localapi/v0/immich/albums/set-user-role

Set a shared album collaborator role to viewer or editor.

URL: http://127.0.0.1:18080/localapi/v0/immich/albums/set-user-role

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

album_id body required

Album ID to modify.

Example: alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

user_id body required

User ID whose album role should change.

Example: usr_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

role body required

Collaborator role. Must be viewer or editor.

Example: editor

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/immich/albums/set-user-role \
  -d '{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "user_id": "usr_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "role": "editor"
}'

Example JSON Body

{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "user_id": "usr_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "role": "editor"
}

Example Response

{
  "status": "ok",
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "user_id": "usr_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "role": "editor"
}
POST /localapi/v0/immich/albums/delete

Delete an album by id.

URL: http://127.0.0.1:18080/localapi/v0/immich/albums/delete

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

album_id body required

Album ID to delete.

Example: alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/immich/albums/delete \
  -d '{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}'

Example JSON Body

{
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}

Example Response

{
  "status": "ok",
  "deleted": true,
  "album_id": "alb_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}

Local WordPress

Use these from localhost to install, start, stop, or uninstall the local WordPress runtime that backs a published website route.

POST /localapi/v0/wordpress/enable

Install or start the local WordPress publishing stack.

URL: http://127.0.0.1:18080/localapi/v0/wordpress/enable

Workflow Note

Use this after the account already has a website route assigned to the device. The daemon uses the latest route metadata when route_id is omitted.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

route_id body optional

Optional website route ID. When omitted, the daemon uses the latest assigned WordPress route from service requirements.

Example: wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

base_dir body optional

Filesystem directory where WordPress data and docker-compose files should live. When omitted, the server chooses the default route-scoped directory next to the local state file.

Example: /var/lib/nodeproxy/wordpress/wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM

curl

curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  http://127.0.0.1:18080/localapi/v0/wordpress/enable \
  -d '{
  "route_id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "base_dir": "/var/lib/nodeproxy/wordpress/wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}'

Example JSON Body

{
  "route_id": "wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM",
  "base_dir": "/var/lib/nodeproxy/wordpress/wroute_01JZ3W8KPXKQ2GMSY5P9J7Q2TM"
}

Example Response

{
  "status": "ok",
  "message": "WordPress installed and running.",
  "listen_url": "http://127.0.0.1:18180",
  "public_hostname": "home-abc123.test.nodeproxy.ai",
  "admin_access_mode": "public",
  "tailnet_admin_hostname": ""
}
POST /localapi/v0/wordpress/disable

Stop the local WordPress runtime without deleting data.

URL: http://127.0.0.1:18080/localapi/v0/wordpress/disable

Workflow Note

This is non-destructive. The route can stay reserved in the control plane while the local WordPress stack is stopped.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/wordpress/disable

Example Response

{
  "status": "ok",
  "message": "WordPress stopped."
}
POST /localapi/v0/wordpress/delete

Uninstall WordPress by stopping the service and permanently deleting local site data.

URL: http://127.0.0.1:18080/localapi/v0/wordpress/delete

Workflow Note

This uninstalls the local WordPress stack after stopping it and permanently removing local site files. Re-enable later to reinstall from scratch.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -sS \
  -X POST \
  http://127.0.0.1:18080/localapi/v0/wordpress/delete

Example Response

{
  "status": "ok",
  "message": "WordPress uninstalled."
}

Local Event Streams

Use these from localhost when the agent wants recent mail activity or a long-lived server-sent events stream.

GET /localapi/v0/mail/recent

Read recent inbound mail events from the local device, including canonical mailbox_id and direct follow-up URLs when available.

URL: http://127.0.0.1:18080/localapi/v0/mail/recent

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

limit query optional

Maximum number of recent events to return. Defaults to 20.

Example: 10

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/recent?limit=10

Example Response

{
  "events": [
    {
      "direction": "inbound",
      "proxy_message_id": "msg_01KNA97WGYC1ATSWR92GZW40CN",
      "internet_message_id": "<reply-target@example.com>",
      "mailbox_id": "1743700929.1.testhost",
      "mailbox": "INBOX",
      "message_url": "http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost",
      "reply_url": "http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost/reply",
      "delete_url": "http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost",
      "mark_url": "http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost/mark",
      "subject": "test email",
      "recipients": ["agent@mail.test.nodeproxy.ai"],
      "received_at": "2026-04-03T19:12:24Z",
      "status": "accepted"
    }
  ]
}
GET /localapi/v0/mail/stream

Stream inbound mail events over server-sent events, including canonical mailbox_id and direct follow-up URLs when available.

URL: http://127.0.0.1:18080/localapi/v0/mail/stream

Workflow Note

This endpoint keeps the HTTP connection open and emits server-sent events as mail activity arrives.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -N -sS \
  -H "Accept: text/event-stream" \
  http://127.0.0.1:18080/localapi/v0/mail/stream

Example Event Stream

data: {"direction":"inbound","proxy_message_id":"msg_01KNA97WGYC1ATSWR92GZW40CN","internet_message_id":"<reply-target@example.com>","mailbox_id":"1743700929.1.testhost","mailbox":"INBOX","message_url":"http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost","reply_url":"http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost/reply","delete_url":"http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost","mark_url":"http://127.0.0.1:18100/localapi/v0/mail/messages/1743700929.1.testhost/mark","subject":"test email","status":"accepted"}

: ping
GET /localapi/v0/mail/outbound/recent

Read recent outbound submission events from the local device.

URL: http://127.0.0.1:18080/localapi/v0/mail/outbound/recent

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

limit query optional

Maximum number of recent events to return. Defaults to 20.

Example: 10

curl

curl -sS \
  http://127.0.0.1:18080/localapi/v0/mail/outbound/recent?limit=10

Example Response

{
  "events": [
    {
      "direction": "outbound",
      "proxy_message_id": "msg_01KNA97WGYC1ATSWR92GZW40CN",
      "subject": "Daily summary",
      "recipients": ["friend@example.net"],
      "received_at": "2026-04-03T19:12:24Z",
      "status": "submitted"
    }
  ]
}
GET /localapi/v0/mail/outbound/stream

Stream outbound submission events over server-sent events.

URL: http://127.0.0.1:18080/localapi/v0/mail/outbound/stream

Workflow Note

This endpoint keeps the HTTP connection open and emits server-sent events as mail activity arrives.

Auth

Loopback only. Use X-NodeProxy-Token: nplocal_... (or Authorization: Bearer <local_api_token>) for local control-plane routes.

Request Parameters

No path, query, or JSON body parameters. The method, URL, and auth shown above are enough to call this endpoint.

curl

curl -N -sS \
  -H "Accept: text/event-stream" \
  http://127.0.0.1:18080/localapi/v0/mail/outbound/stream

Example Event Stream

data: {"direction":"outbound","proxy_message_id":"msg_01KNA97WGYC1ATSWR92GZW40CN","subject":"Daily summary","status":"submitted"}

: ping