Local dev HTTP API.
Everything `hs-x dev invoke` can do is also four 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.
The 30-second answer
GET /_hsx/health, GET /_hsx/manifest, POST /_hsx/invoke/<capability-id> with optional {input, enrolledObject, install} JSON, and POST /_hsx/cards/<card-backend-id> with the same body, answering with the handler's object itself. 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, because 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.
curl -s http://127.0.0.1:8787/_hsx/health{ "ok": true, "cliVersion": "0.4.1" }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, typed input/output fields, and the runtimes it needs. This is the discovery step. The MCP server deliberately exposes no capability listing or invocation, so an agent that needs the inventory reads this endpoint or runs hs-x status --json. The example below is abridged to the fields you'll key on.
curl -s http://127.0.0.1:8787/_hsx/manifest{
"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/<capability-id>
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.
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" } }
}'{
"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 | fixture: each declared input field gets its default, else a typed sample (0 for number, false for boolean, the first option for enumeration, 2026-01-01 / 2026-01-01T00:00:00.000Z for date / datetime, sample-<name> otherwise). An explicit input replaces the whole fixture, so partial input yields the real validation error | The handler's typed input |
enrolledObject | fixture (id: fixture-001, capability's object type — contacts when it has none, empty properties) | {id, objectType, properties} |
install | { "id": "install-local-fixture", "portalId": "0", "state": "active", "config": {} } | 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 (if you run the CLI that way), 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. If .dev.vars defines HSX_HUBSPOT_CLIENT_SECRET, local dispatches are signed with it and pass through the production HubSpot v3 signature verifier instead of the unsigned local escape hatch.
What ok means
Over HTTP, ok mirrors the status code: true for any 2xx. It does not read the runtime envelope, so a failContinue result or a handler error that the router still answers with 200 comes back ok: true; gate on response.ok and response.result.status for the runtime's own verdict. The CLI's hs-x dev invoke exit code combines both.
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 rather than only 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 health, manifest, and invoke endpoints.
POST /_hsx/cards/<card-backend-id>
The same dispatch for a card backend, answering the way a deployed Worker answers a card's hubspot.fetch: the body is the handler's object itself, with no {ok, capabilityId, worker, kind, response, logs} wrapper. The request body takes the same optional input, enrolledObject, and install keys. This is the route a migrated card points at during hs-x dev, so its temporary dev origin dispatches the current local handler body.
curl -s -X POST http://127.0.0.1:8787/_hsx/cards/email-health \
-H 'content-type: application/json' \
-d '{ "enrolledObject": { "id": "3301452", "objectType": "contacts", "properties": { "email": "mia@example.com" } } }'An id that exists but is not a card backend gets a 404 with error: "unknown_card_backend"; invalid JSON, unknown ids, and loader failures answer the same way as the invoke route.
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, and the route requires a HubSpot v3 signature (the runtime HTTP reference covers the policy). hs-x dev invoke <id> --remote resolves the origin and signs for you (it needs CLOUDFLARE_API_TOKEN plus the deployed pointer to resolve the origin, or --url <origin> to skip resolution); from raw HTTP, hit the Worker URL directly:
curl -s -X POST https://<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": {} } }'