The security model

What applies to every Developer API call, from the person reloaded as they are now to access levels, row policies, hidden fields, source errors and what Halo records.

An API key reads as the person who made it, through the same services as Halo Data. It can't see more than they can, it can't outlive their access, and it can't change anything.

What every request checks

Once a request has the API's shape, Halo checks these in order, and stops at the first that fails.

  1. No key is in the query string: 400 key_in_query.

  2. The caller's address is under 600 requests a minute: 429 rate_limited.

  3. The key is Halo's, unrevoked and unexpired: 401 invalid_api_key, whichever it is.

  4. Halo hasn't stopped the API, for this organization or everyone: 503 api_stopped.

  5. A playground key comes from the portal: 403 playground_key.

  6. The key is under its plan's request limit, and its person under twice that: 429 rate_limited. This runs before the person is loaded, so a flood never reaches the membership checks.

  7. The person is loaded as they are now: their account, their membership, the organization's status, its two-factor requirement against their enrollment, and its sign-in methods against the one the key was made with.

  8. Their role has api.use, and the organization's own Owner or Admin has turned the API on: 403 forbidden or 403 api_disabled.

  9. The key's scope allows the operation: 403 insufficient_scope.

  10. For data.query operations, today's rows aren't spent: 429 row_budget_exceeded.

  11. The key has fewer than four requests running: 429 rate_limited.

Errors has every code.

The person, live

Nothing about the person is stored in the key beyond who they are and how they signed in. A role change applies on the next call, with nothing to revoke. Changes to their credentials or membership revoke the key in the database, in the same transaction, whatever code made the change. See What stops a key.

Keys carry no staff roles, and staff viewing as someone can't make them.

Access levels

Workspaces, folders and grants apply as they do in Halo Data. Lists hold only what the person can see, and a dataset's level says theirs, such as view or manage.

  • Anything else answers 404 not_found, the same as an id that doesn't exist, including ids from other organizations.

  • Reports give the published version. A draft needs edit access, and asking without it gets 403 forbidden. To someone who can only view, a report that was never published isn't there.

  • A chart's rows run against its dataset as the person, so seeing a chart never opens a dataset they can't query.

Row policies

Row policies apply to every query with the person's user attributes, and the request can't remove them. The answer never says which policies applied.

Plan row caps and query time limits apply too.

Rows count as exporting

A query, or a chart's data, needs the role to have data.export, since an API makes copying easy. Owner, Admin, Builder and Analyst have it by default. Without it, 403 export_not_allowed.

Dataset descriptions, chart specs and report documents don't need it.

Hidden fields

Hiding a field keeps it from the API. A dataset's description leaves it out, and a query that names it gets 422 hidden_field. Calculated fields may still use a hidden field, as they can everywhere.

Marketplace data

Marketplace data stays in Halo. Such a dataset shows marketplaceData: true, and you can read its fields, but its rows answer 403 marketplace_data_not_available. The MCP server refuses those rows too.

No SQL, and no internals

  • A query takes fields, never SQL.

  • Answers never carry a query's SQL, even for someone who may read it in Halo Data.

  • Dataset descriptions leave out custom SQL and row policies, and name the connection without its details.

  • Source errors. The source's own words can name its host, user, database or SQL. 502 source_error carries them only for people who can view connections. Everyone else gets a general message.

Keys in logs

Halo stores only a key's SHA-256 hash, shows the secret once, and doesn't accept it in a URL. Its request log keeps the path alone. The call log and audit log hold the key's id, never the key, and never a request's body.

What Halo records

The call log has every request a valid key makes: person, key id, operation (such as developer-query-dataset), resource, status, problem code, rows, duration, address and request id. It's kept 90 days. Members see their own requests on the keys page, and those with members.manage see everyone's in Halo Data, under Settings, API access.

The audit log has reads and key events:

EventWhen
dataset.viewedA query or a chart's data, with via: api and the key's id
report.viewedA report's document, with via: api and the key's id
api_key.created, api_key.rolled, api_key.revokedA key's life
api.settings_updatedThe API was turned on or off

Reads are audited once per key and item an hour, in each API process, so a script polling every second adds one event an hour rather than thousands. The call log has the full record.

What it can't do

Halo can't control what your code does with data its person may read. The role, the organization's switch, the row budget and the logs are the controls, so store what you copy as carefully as its source.