Build your own extensions
viewbus runs a small HTTP API on your machine while it's open. Read the resources it has indexed and drive the app from your own tools — no SDK, just HTTP.
Launcher commands
Search your queues from Raycast, Alfred, or PowerToys Run and jump straight to one.
Editor & CLI scripts
Wire viewbus into a VS Code task or a shell script in a few lines.
Status widgets
Ping it from a taskbar or menu-bar script to show if viewbus is running.
Looking to give an AI assistant access instead? That's the built-in MCP server, which is a separate interface from this one.
How it works
- 1
Discover
On launch viewbus writes a discovery file with the port and a fresh token.
- Windows
%APPDATA%\viewbus\local-api.json- macOS
~/Library/Application Support/viewbus/local-api.json- Linux
~/.local/share/viewbus/local-api.json
- 2
Authenticate
Send the token as
Authorization: Bearer <token>. The server binds127.0.0.1, requires that token, and rejects any request carrying anOriginheader — so it's reachable only from your machine, never the network or a web page. - 3
Call
Base URL is
http://127.0.0.1:<port>. Each endpoint is listed below.
Full example cURL · TypeScript · Python · C# · Rust
# read the discovery file (Windows path shown)
PORT=$(jq -r .port "$APPDATA/viewbus/local-api.json")
TOKEN=$(jq -r .token "$APPDATA/viewbus/local-api.json")
BASE="http://127.0.0.1:$PORT"
AUTH="Authorization: Bearer $TOKEN"
curl -H "$AUTH" "$BASE/v1/status"
curl -H "$AUTH" "$BASE/v1/search?q=orders&limit=10"
curl -X POST -H "$AUTH" -H "Content-Type: application/json" \
-d '{"id":"/subscriptions/…/queues/orders-dead"}' \
"$BASE/v1/focus" // read the discovery file (Windows path shown)
import { readFile } from "node:fs/promises";
const { port, token } = JSON.parse(
await readFile(`${process.env.APPDATA}/viewbus/local-api.json`, "utf8"),
);
const base = `http://127.0.0.1:${port}`;
const headers = { Authorization: `Bearer ${token}` };
const status = await fetch(`${base}/v1/status`, { headers }).then((r) => r.json());
const { results } = await fetch(
`${base}/v1/search?q=orders&limit=10`,
{ headers },
).then((r) => r.json());
await fetch(`${base}/v1/focus`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ id: results[0].id }),
}); # read the discovery file (Windows path shown)
import json, os, requests
info = json.load(open(os.path.join(os.environ["APPDATA"], "viewbus", "local-api.json")))
base = f"http://127.0.0.1:{info['port']}"
headers = {"Authorization": f"Bearer {info['token']}"}
status = requests.get(f"{base}/v1/status", headers=headers).json()
results = requests.get(
f"{base}/v1/search",
headers=headers,
params={"q": "orders", "limit": 10},
).json()["results"]
requests.post(f"{base}/v1/focus", headers=headers, json={"id": results[0]["id"]}) // read the discovery file (Windows path shown)
using System.Net.Http.Json;
using System.Text.Json;
var path = Path.Combine(
Environment.GetEnvironmentVariable("APPDATA")!, "viewbus", "local-api.json");
using var file = JsonDocument.Parse(File.ReadAllText(path));
var http = new HttpClient
{
BaseAddress = new Uri($"http://127.0.0.1:{file.RootElement.GetProperty("port").GetInt32()}"),
};
http.DefaultRequestHeaders.Authorization =
new("Bearer", file.RootElement.GetProperty("token").GetString());
var status = await http.GetFromJsonAsync<JsonElement>("/v1/status");
var search = await http.GetFromJsonAsync<JsonElement>("/v1/search?q=orders&limit=10");
var results = search.GetProperty("results");
var id = results[0].GetProperty("id").GetString();
await http.PostAsJsonAsync("/v1/focus", new { id }); // read the discovery file (Windows path shown) — inside an async fn
use serde_json::{json, Value};
let path = format!(r"{}\viewbus\local-api.json", std::env::var("APPDATA")?);
let d: Value = serde_json::from_str(&std::fs::read_to_string(path)?)?;
let base = format!("http://127.0.0.1:{}", d["port"]);
let token = d["token"].as_str().unwrap();
let http = reqwest::Client::new();
let status: Value = http.get(format!("{base}/v1/status"))
.bearer_auth(token).send().await?.json().await?;
let search: Value = http.get(format!("{base}/v1/search?q=orders&limit=10"))
.bearer_auth(token).send().await?.json().await?;
let results = search["results"].as_array().unwrap();
http.post(format!("{base}/v1/focus"))
.bearer_auth(token)
.json(&json!({ "id": results[0]["id"] }))
.send().await?; Endpoints
GET /v1/status # Check the app is running and read its version.
curl -H "$AUTH" "$BASE/v1/status" const status = await fetch(`${base}/v1/status`, { headers }).then((r) => r.json()); status = requests.get(f"{base}/v1/status", headers=headers).json() var status = await http.GetFromJsonAsync<JsonElement>("/v1/status"); let status: Value = http.get(format!("{base}/v1/status"))
.bearer_auth(token).send().await?.json().await?; { "app": "viewbus", "apiVersion": 1, "version": "0.8.x", "hasSelection": true } hasSelection is true only when a tenant and subscription are both selected. Search works without it.
GET /v1/search # Fuzzy-search indexed queues, topics, and subscriptions.
- q
- required · search term
- limit
- optional · default 20, max 50
curl -H "$AUTH" "$BASE/v1/search?q=orders&limit=10" const { results } = await fetch(
`${base}/v1/search?q=orders&limit=10`,
{ headers },
).then((r) => r.json()); results = requests.get(
f"{base}/v1/search",
headers=headers,
params={"q": "orders", "limit": 10},
).json()["results"] var search = await http.GetFromJsonAsync<JsonElement>("/v1/search?q=orders&limit=10");
var results = search.GetProperty("results"); let search: Value = http.get(format!("{base}/v1/search?q=orders&limit=10"))
.bearer_auth(token).send().await?.json().await?;
let results = search["results"].as_array().unwrap(); {
"results": [
{
"id": "/subscriptions/…/queues/orders-dead",
"displayName": "orders-dead",
"path": "Prod / rg-msg / ns-orders / orders-dead",
"resourceType": "queue",
"azureSubscription": "Prod",
"activeCount": 128,
"deadLetterCount": 4,
"lastIndexed": "2026-07-01T09:12:03Z"
}
]
} id is opaque — pass it to /v1/focus, don't parse it. Counts are last-indexed, not live; check lastIndexed.
POST /v1/focus # Bring viewbus to the front, on a specific resource.
- id
- required · body field — an id from /v1/search
curl -X POST -H "$AUTH" -H "Content-Type: application/json" \
-d '{"id":"/subscriptions/…/queues/orders-dead"}' \
"$BASE/v1/focus" await fetch(`${base}/v1/focus`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ id: results[0].id }),
}); requests.post(f"{base}/v1/focus", headers=headers, json={"id": results[0]["id"]}) var id = results[0].GetProperty("id").GetString();
await http.PostAsJsonAsync("/v1/focus", new { id }); http.post(format!("{base}/v1/focus"))
.bearer_auth(token)
.json(&json!({ "id": results[0]["id"] }))
.send().await?; { "ok": true } An unknown id returns 404.
Errors
Every error uses one shape; the HTTP status matches the code.
{ "error": { "code": "unauthorized", "message": "invalid token" } } | code | status | meaning |
|---|---|---|
bad_request | 400 | Missing q, malformed body |
unauthorized | 401 | Missing or wrong bearer token |
forbidden | 403 | Origin header present, or non-loopback Host |
not_found | 404 | Unknown endpoint, or a focus id that doesn't exist |
internal | 500 | Unexpected server-side error |
Enabled by default — disable with localApi.enabled: false in settings.json, then restart.
/v1 is stable: fields are only added, never removed or renamed. Breaking changes ship under /v2.