Skip to content

API Client

The Framedash API Client (@framedash/api-client) is a typed TypeScript/JavaScript client for the Framedash Developer Platform REST API. It is the same client that powers @framedash/cli and @framedash/mcp-server, published as a standalone package so you can integrate Framedash telemetry directly into your own in-house tools, dashboards, and automation.

It handles authentication, project-scoped paths, response unwrapping, and structured errors, plus a few security guardrails (HTTPS-only base URLs, no credential-leaking redirects, request timeouts), so you can focus on the data.

  • Node.js 20 or later (uses the global fetch, AbortSignal.timeout, and node:net)
  • An API key or OAuth 2.1 access token with the scopes required by the calls you make. The raw SQL examples need a Full API key; simple read requests work with a Read-only key or an access token granted analytics:read.
Terminal window
npm install @framedash/api-client
import { ApiClient, ApiError } from "@framedash/api-client";
const client = new ApiClient({
baseUrl: "https://app.framedash.dev",
apiKey: process.env.FRAMEDASH_API_KEY ?? "",
projectId: process.env.FRAMEDASH_PROJECT_ID ?? "",
onError: (err: ApiError) => {
// onError is called for any non-success response. It MUST throw (or exit);
// its return type is `never`. The thrown value propagates to your await.
throw err;
},
});
// Project-scoped GET -> /api/v1/projects/{projectId}/dashboard?days=30
const dashboard = await client.get(client.projectPath("dashboard?days=30"));
console.log(dashboard);

The client unwraps the API envelope automatically: a successful { "success": true, "data": ... } response resolves to just data.

The ApiClient constructor takes a single options object:

OptionTypeDescription
baseUrlstringApplication host URL, e.g. https://app.framedash.dev. Must be HTTPS (HTTP allowed only for localhost/loopback).
apiKeystringAPI key, sent as the X-API-Key header on every request. Provide exactly one of apiKey or accessToken.
accessTokenstringOAuth 2.1 access token, sent as a Bearer token in the Authorization header on every request. Provide exactly one of accessToken or apiKey.
projectIdstringDefault project UUID. Required for projectPath(); also sent as the X-Project-Id header for non-project paths.
onError(error: ApiError) => neverCalled on any non-success response. Must throw or exit the process.

Provide exactly one of apiKey or accessToken. The constructor throws if both or neither are set.

The base URL is validated when the client is constructed; an insecure or malformed URL throws immediately. You can run the same check yourself with the exported assertSafeBaseUrl(baseUrl).

Four HTTP helpers cover the Web API. Each returns the unwrapped data payload, typed via the generic parameter:

const data = await client.get<MyType>("/api/v1/...");
await client.post("/api/v1/...", body);
await client.patch("/api/v1/...", body);
await client.delete("/api/v1/...");

Most endpoints are scoped to a project. projectPath(suffix) builds /api/v1/projects/{projectId}/{suffix} from the client’s configured projectId:

// GET /api/v1/projects/{projectId}/status
const status = await client.get(client.projectPath("status"));
// GET /api/v1/projects/{projectId}/retention?days=30
const retention = await client.get(client.projectPath("retention?days=30"));
// GET /api/v1/projects/{projectId}/maps
const maps = await client.get(client.projectPath("maps"));

Calling projectPath() without a configured projectId throws.

withProject(projectId) returns a new client bound to a different project, reusing the same base URL, credential, and error handler. The original client is unchanged:

const other = client.withProject("another-project-uuid");
const otherStatus = await other.get(other.projectPath("status"));
// The currently bound project ID:
console.log(client.currentProjectId);
// GET /api/v1/whoami: confirms the key is valid and returns non-sensitive metadata.
const me = await client.get("/api/v1/whoami");
// GET /api/v1/projects/{projectId}/heatmap?mapId=arena&days=7
const cells = await client.get(client.projectPath("heatmap?mapId=arena&days=7"));
const rows = await client.post("/api/v1/query", {
sql: "SELECT event_name, count() FROM events GROUP BY event_name",
project_id: client.currentProjectId,
limit: 100,
});
// List alert rules
const alerts = await client.get(client.projectPath("alerts"));
// Create an alert rule
const created = await client.post(client.projectPath("alerts"), {
name: "FPS Alert",
// ...remaining rule fields
});
// Deactivate an alert rule
await client.delete(client.projectPath(`alerts/${alertId}`));
// Content endpoints are not project-scoped in the path; the client adds the
// X-Project-Id header automatically from the configured projectId.
// Bulk upsert wraps the array in an "entries" property (max 500 per request).
await client.post("/api/v1/content", { entries });
const content = await client.get("/api/v1/content");

See the API reference for the full list of endpoints, query parameters, and payload shapes.

Failed server responses (unexpected redirects, non-2xx statuses, non-JSON bodies, or bodies without success: true) are turned into ApiError instances and passed to your onError callback. Network failures and the 30-second timeout reject with native fetch errors and do not call onError, so handle both paths in the caller’s try / catch. ApiError carries parsed RFC 9457 Problem Details when the response provides them:

MemberTypeDescription
statusnumberHTTP status code.
headersHeadersResponse headers (e.g. X-RateLimit-Reset).
messagestringHuman-readable error message.
retryablebooleanWhether the API marked the error as retryable.
retryAfternumber | undefinedSuggested retry delay in seconds.
errorCategorystring | undefinedAPI error category.
problemProblemDetailsThe raw parsed Problem Details object.

A rate-limit-aware handler:

const client = new ApiClient({
baseUrl: "https://app.framedash.dev",
apiKey: process.env.FRAMEDASH_API_KEY ?? "",
projectId: process.env.FRAMEDASH_PROJECT_ID ?? "",
onError: (err: ApiError): never => {
if (err.status === 429) {
const retryAfter = err.retryAfter ?? err.headers.get("X-RateLimit-Reset");
throw new Error(`Rate limited; retry after ${retryAfter}`);
}
throw err;
},
});

The client enforces a few safeguards so credentials are never leaked:

  • HTTPS-only base URLs. Plain HTTP is rejected except for localhost/loopback dev endpoints, and embedded credentials (https://...@host) are rejected.
  • No redirects. The API never redirects a programmatic JSON request, so any 3xx is treated as an error, and the client never re-sends the X-API-Key or Authorization header to a redirect target.
  • A 30-second request timeout per call (via AbortSignal.timeout).