Agents & API
SDKs
Official clients for TypeScript/JavaScript and Python. Both have zero dependencies, cover every API operation, and include helpers for heartbeats, deploys and webhook verification.
Install
npm install @upbutler/sdk
# or: bun add @upbutler/sdk · pnpm add @upbutler/sdkpip install upbutler@upbutler/sdk runs on Node 18+, Bun, Deno, Cloudflare Workers and browsers. upbutler for Python needs 3.9+ and uses only the standard library. Create an API key under Settings → API keys, or let an agent create a workspace with agent.bootstrap (see Quickstart for agents).
Quickstart
import { UpButler } from "@upbutler/sdk";
const ub = new UpButler({ apiKey: process.env.UPBUTLER_API_KEY });
// Monitor a URL and publish it on a status page in one call
const { monitor, statusPageUrl } = await ub.services.add({
name: "Search API",
url: "https://api.example.com/health",
statusPage: "acme",
});
// Operation `a.b.c` is the typed method ub.a.b.c(input)
const { data: monitors } = await ub.monitors.list();
await ub.incidents.create({ title: "Elevated errors", message: "Investigating 5xx on search", impact: "minor" });
// Public operations need no API key
const status = await new UpButler().public.status({ slug: "acme" });import os
from upbutler import UpButler
ub = UpButler(api_key=os.environ["UPBUTLER_API_KEY"])
ub.services.add(name="Search API", url="https://api.example.com/health", statusPage="acme")
monitors = ub.monitors.list()["data"]
ub.incidents.create(title="Elevated errors", message="Investigating 5xx on search", impact="minor")
ub.pages.verify_domain(pageId="pg_...") # snake_case maps to the camelCase operation id
ub.call("public.status", slug="acme") # any operation by idEvery operation in the REST API reference is a method: the operation id a.b.c becomes client.a.b.c(input), for example monitors.create, pages.push or public.subscriber.get. In TypeScript every input is typed. The types are generated from the same schemas the API validates against. Results are the JSON documented for each endpoint.
Client options
new UpButler({
apiKey: "ub_live_...", // optional for public operations
baseUrl: "https://upbutler.com", // default
fetch, // custom fetch for tests or proxies
maxRetries: 2, // 429 / 5xx / network errors, idempotent requests only
timeoutMs: 30_000,
autoIdempotency: false, // true: random Idempotency-Key on every POST
});
// Per call
await ub.monitors.create(input, { idempotencyKey: "deploy-42-api", signal });Requests are retried with exponential backoff (honoring Retry-After) on 429, 5xx and network errors, but only when repeating them is safe: GET, PUT, DELETE, and POSTs that carry an Idempotency-Key. The server replays the first response for a repeated key for 24 hours.
Low-level calls
await ub.call("monitors.get", { id: "mon_..." }); // by operation id
await ub.request("GET", "/monitors/:id", { id: "mon_..." }); // method + path under /api/v1Path parameters are taken from the input. For GET and DELETE the remaining fields become the query string, otherwise a JSON body. OPERATIONS exports the full operation table (id, method, path, auth, scope, summary).
Errors
import { UpButlerError } from "@upbutler/sdk";
try {
await ub.monitors.get({ id: "mon_missing" });
} catch (err) {
if (err instanceof UpButlerError) {
console.log(err.status, err.code, err.message, err.hint, err.details);
// 404 "not_found" "Monitor not found" ...
}
}Failed calls throw UpButlerError (Python: UpButlerError exception) with the API's code, message, hint and details, plus the HTTP status. Network failures use code: "network_error" or "timeout" with status 0. See Errors & rate limits.
Heartbeats
import { heartbeat, withHeartbeat } from "@upbutler/sdk";
const HB = process.env.UPBUTLER_HB_URL!; // https://upbutler.com/hb/hb_Xf3kq9LmR2vT8wYzN4bC1dE7
await heartbeat(HB, { message: "backup 42GB ok", durationMs: 48210 });
await heartbeat(HB, { status: "down", message: "pg_dump exited 1" });
// Success with the duration, or failure with the error message (rethrown)
await withHeartbeat(HB, () => runBackup());from upbutler import heartbeat, with_heartbeat
HB = os.environ["UPBUTLER_HB_URL"]
heartbeat(HB, message="backup 42GB ok", duration_ms=48210)
heartbeat(HB, status="down", message="pg_dump exited 1")
with_heartbeat(HB, run_backup)Heartbeat helpers need no API key, because the token is the secret. They accept the full ping URL (including the /fail and /api/v1/heartbeat/… forms) or the bare hb_… token. withHeartbeat reports the job's duration so it shows up in charts. If the ping itself fails, your job's result is unaffected. See Heartbeats.
Deploys
await ub.markDeploy({
service: "api",
version: "v1.42.0",
environment: "production",
commit: process.env.GITHUB_SHA,
});ub.mark_deploy(service="api", version="v1.42.0", environment="production", commit=os.environ.get("GITHUB_SHA"))Deploy markers appear on monitor timelines and give AI incident analysis the "what changed" context. markDeploy wraps deploys.create.
Verify webhooks
import { verifyWebhook } from "@upbutler/sdk";
export default {
async fetch(req: Request) {
const body = await req.text(); // the raw body, exactly as received
const ok = await verifyWebhook(process.env.UPBUTLER_WEBHOOK_SECRET!, req.headers, body);
if (!ok) return new Response("invalid signature", { status: 401 });
const event = JSON.parse(body);
// ...
return new Response("ok");
},
};from upbutler import verify_webhook
@app.post("/hooks/upbutler") # Flask
def hook():
if not verify_webhook(os.environ["UPBUTLER_WEBHOOK_SECRET"], request.headers, request.get_data()):
return "invalid signature", 401
event = request.get_json()
...UpButler signs webhooks with Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature). verifyWebhook(secret, headers, body, toleranceSec = 300) accepts a Headers object or a plain header record. In JavaScript it is async and uses WebCrypto, so the same code runs on Node, Bun, Deno and edge runtimes.
Other languages
The OpenAPI spec works with any OpenAPI generator. The CLI covers shell scripts and CI (see CLI), and agents can connect to the MCP server directly.