OpenAPI and typed clients

Download Halo's OpenAPI file, generate a typed TypeScript client with openapi-typescript and openapi-fetch, or import it into Postman or Bruno.

When you're done, you'll have a TypeScript client whose paths, bodies and answers are checked by the compiler, or a collection you can click through in Postman or Bruno. Both come from an OpenAPI 3.1 file cut from the one Halo generates from its server code, so it matches what the API does.

curl -sS -o halo-openapi.json https://dev.usehalo.net/openapi.json

It holds what third parties may call, and nothing of Halo Data's or the Console's:

TagPathsSign in with
Developer API/v1/developer/*, with operation ids such as developer-query-datasetapiKey: HTTP bearer, with format hak_…
Public reports/v1/public/report/*shareToken: a link's token or a signed embed token
CRM/v1/public/crm/ingestsourceToken: a Halo CRM webhook source's token (hci_…)

Its server is https://api.usehalo.net. The API reference is built from the same file.

A typed TypeScript client

openapi-typescript turns the file into types, and openapi-fetch is a small client that reads them. Halo's own apps use the same pair.

npm install openapi-fetch
npm install --save-dev openapi-typescript typescript
npx openapi-typescript ./halo-openapi.json --output ./src/halo-api.d.ts

Run the client on a server, so the key stays private. Set the key once, on the client:

import createClient from "openapi-fetch";
import type { paths } from "./halo-api";

export const halo = createClient<paths>({
  baseUrl: "https://api.usehalo.net",
  headers: { Authorization: `Bearer ${process.env.HALO_API_KEY}` },
});
import { halo } from "./halo";

const { data, error, response } = await halo.POST("/v1/developer/datasets/{id}/query", {
  params: { path: { id: process.env.HALO_DATASET_ID as string } },
  body: {
    dimensions: [{ field: "region" }],
    measures: [{ field: "revenue" }],
    sorts: [{ field: "revenue", direction: "desc" }],
    limit: 10,
  },
});

if (error) {
  throw new Error(`${error.code}: ${error.detail} (request ${error.requestId})`);
}

console.log(response.status, data.columns, data.rows);

The compiler now catches a misspelled path or an unknown body field. op, grain and aggregate are plain strings, so the server checks those. error is the problem, and data the answer. Keep the file and types in your repository, and regenerate when you want what's new.

openapi-fetch doesn't retry, so wrap calls that should: see Snippets.

Postman

  1. Choose Import, and pick halo-openapi.json or paste its address.

  2. Under the collection's Authorization, choose Bearer Token, and use a variable, such as {{HALO_API_KEY}}.

  3. Put the key in the variable's current value, in an environment that isn't shared.

Bruno

  1. Choose Import Collection, then OpenAPI, and pick halo-openapi.json.

  2. Set the collection's auth to Bearer, with {{HALO_API_KEY}} as the token.

  3. Add HALO_API_KEY as a secret variable, so it isn't written to the files you commit.

Bruno keeps collections as plain files, which makes it easy to commit a key by accident. Check before you push.

Things to know

  • The MCP server isn't in this file. Its tools are described at dev.usehalo.net/reference/mcp, and clients read them from tools/list.

  • Published SDKs and a CLI are planned. For now, generate your own as above.