# Local dev HTTP API

Everything `hs-x dev invoke` and the MCP tools can do is also three plain HTTP endpoints on the dev server — same engine, same production-router dispatch, same envelopes. This is the door for CI smoke jobs, REST clients, and anything that speaks HTTP and nothing else.

**TL;DR** — `GET /_hsx/health`, `GET /_hsx/manifest`, `POST /_hsx/invoke/<capability-id>` with optional `{input, enrolledObject, install}` JSON. The invoke response carries the runtime envelope plus `logs: [{level, message, fields}]` from the handler. When the dev server runs under Bun, edits to worker files apply on the next request — the server hot-reloads modules per invocation; under Node, restart to pick them up.

## GET /_hsx/health

Liveness plus identity — useful as a CI readiness gate.

```bash
curl -s http://127.0.0.1:8787/_hsx/health
```

```json
{ "ok": true, "cliVersion": "0.2.5" }
```

`cliVersion` is the identity half: it names the process that answered, and it's how `hs-x dev status` tells an hs-x dev server apart from anything else that happens to be holding the port.

## GET /_hsx/manifest

The project's full capability inventory: every worker, every capability with kind, label, object type, and typed input/output fields. This is the discovery step — the HTTP counterpart of `hsx.dev.capabilities` over MCP, and the richer of the two: the MCP tool reports capability ids, kinds, and object types, while the manifest adds labels and the typed field maps.

```bash
curl -s http://127.0.0.1:8787/_hsx/manifest
```

```json
{
  "workers": [
    {
      "name": "deals",
      "capabilities": [
        { "kind": "tool", "id": "tag-high-value-deals", "label": "Tag high value deals", "objectType": "deal" }
      ]
    }
  ]
}
```

One nuance: the manifest is computed once at server start. A capability you add or rename afterwards is already invokable — `/_hsx/invoke` reloads worker modules per request — but it won't show up here until you restart `hs-x dev`.

## POST /_hsx/invoke/:capabilityId

Dispatch one capability through the production runtime router — identical code path to a deployed Worker: payload validation, context construction, handler, result envelope. All three body keys are optional; fixture defaults fill what you omit.

```bash
curl -s -X POST http://127.0.0.1:8787/_hsx/invoke/tag-high-value-deals \
  -H 'content-type: application/json' \
  -d '{
    "input": { "threshold": 25000 },
    "enrolledObject": { "id": "d1", "objectType": "deals", "properties": { "amount": "50000" } }
  }'
```

```json
{
  "ok": true,
  "capabilityId": "tag-high-value-deals",
  "worker": "deals",
  "kind": "tool",
  "response": { "ok": true, "capabilityId": "tag-high-value-deals", "result": { "status": "ok", "output": { "tagged": true } } },
  "logs": [
    { "level": "info", "message": "tagging deal", "fields": { "amount": 50000 } }
  ]
}
```

| Body key | Default | What it is |
| --- | --- | --- |
| `input` | `{}` | The handler's typed input |
| `enrolledObject` | fixture (`id: fixture-001`, capability's object type — `contacts` when it has none) | `{id, objectType, properties}` |
| `install` | local fixture install | Install identity `{id, portalId, state, config}` |

Three behaviors worth knowing:

- **Hot reload — under Bun.** Running under Bun, the server re-reads edited worker modules (and their helper imports) on every invocation — edit, save, re-POST; no restart. Under Node (how the published `hs-x` binary runs by default), modules stay cached, so restart the server to pick up edits.
- **Handler env from `.dev.vars`.** Handlers get `ctx.env` from `.dev.vars` at the project root — the Wrangler convention — re-read on every invocation, so secret edits apply without a restart on either runtime.
- **Honest exit semantics.** `ok` is false when the dispatch failed *or* the runtime envelope reports failure, so a CI job can gate on the HTTP response alone.

Failures stay JSON too — always `{ "ok": false, "error": ... }`:

| Status | `error` | When |
| --- | --- | --- |
| 400 | `invalid_json` | The request body isn't valid JSON |
| 404 | `HSX_E_DEV_UNKNOWN_CAPABILITY` | No capability with that id — the `hint` lists what the project does declare |
| 500 | `HSX_E_INTERNAL` | The dispatch itself threw — a worker module failed to load, for instance; `message` carries the full cause chain, not just the top-level error |

Other dev-loader failures — no worker modules under `src/workers`, for instance — also return 500, with their own `HSX_E_DEV_*` code and a `hint`. Any other path gets a 404 whose `error` names the three endpoints.

## The same dispatch against a deployed Worker

A deployed HS-X Worker serves the same dispatch at `POST /capabilities/<id>/invoke` on its own origin — that's where install tokens live, so handlers using `ctx.hubspot` run against the real portal. `hs-x dev invoke <id> --remote` resolves the origin for you; from raw HTTP, hit the Worker URL directly:

```bash
curl -s -X POST https://<your-worker>.workers.dev/capabilities/tag-high-value-deals/invoke \
  -H 'content-type: application/json' \
  -d '{ "input": { "threshold": 25000 }, "install": { "id": "hubspot-app:<appId>:portal:<portalId>", "portalId": "<portalId>", "state": "active", "config": {} } }'
```

**Where next**

- [Test a migrated app: the same engine, three doors](/docs/guides/test-a-migrated-app)
- [MCP reference: the agent door to the same operations](/docs/mcp)
- [Dev mode: sessions, overrides, and the full local loop](/docs/guides/dev-mode)

