> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hebbianrobotics.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authenticate with client credentials

> Exchange client credentials for an access token to call the Quality Indexing API, and see which datasets and scopes the token can read.

## Every endpoint requires a credential

There is no anonymous read here. The measurements describe a customer's own
footage, so a request without a recognized credential is refused rather than
answered with public data:

```json 401 theme={null}
{
  "code": "credential_required",
  "message": "this endpoint requires an operator credential; …"
}
```

## Getting a token

The API is called by your servers, unattended, so the credential is a machine
one. Onboarding gives you a **client id**, a **client secret**, and the token
endpoint to exchange them at. Nothing here involves a browser, a user account,
or a self-service signup form — so if you do not have those three things yet,
ask [brandon.ong@hebbianrobotics.com](mailto:brandon.ong@hebbianrobotics.com)
rather than looking for a signup page.

Exchange them with the OAuth 2.0 client-credentials grant:

```bash theme={null}
curl -s -X POST "$SQUASH_TOKEN_ENDPOINT" \
  -d grant_type=client_credentials \
  -d "client_id=$SQUASH_CLIENT_ID" \
  -d "client_secret=$SQUASH_CLIENT_SECRET"
```

```json Response theme={null}
{ "access_token": "eyJhbGciOi…", "token_type": "Bearer", "expires_in": 3600 }
```

The **access token** is what you send us. The client secret never leaves your
side, and we never see it. Tokens are short-lived: cache one until `expires_in`
lapses and exchange again, rather than minting one per request.

<Warning>
  Treat the client secret as you would a database password. It is a
  long-lived credential for your whole organization's data; the access token it
  mints is the thing designed to be passed around. Rotation and revocation
  happen at the issuer and take effect without any change on our side.
</Warning>

## Presenting it

```bash theme={null}
curl -s "$SQUASH_API/api/quality/datasets" \
  -H "Authorization: Bearer $SQUASH_TOKEN"
```

## Checking what you got

`GET /api/access` is the authoritative answer for a given credential, and it
never errors on a missing or malformed one. Call it first rather than inferring
your access from a `403`.

```bash theme={null}
curl -s "$SQUASH_API/api/access" -H "Authorization: Bearer $SQUASH_TOKEN" | jq
```

```json Response theme={null}
{
  "scope": { "kind": "organization", "organization_id": "org_01JACME…" },
  "capabilities": ["view_data"],
  "writes_require_admin": true
}
```

A customer credential holds exactly one capability, `view_data`, which is what
every endpoint in this documentation requires. The other capabilities the
reference enumerates exist for operator credentials and are not issuable to a
customer; `writes_require_admin` reports that the deployment is gated, and is
the same for everybody.

## Scoping comes from the token

Your organization is read from a **verified claim inside the access token** —
not from a header, and not from a table we maintain. That is what makes the
boundary an assertion by the issuer rather than by our configuration, and it is
why a rotated or revoked client stops working without us doing anything.

Two consequences worth knowing:

* **Do not send `X-Squash-Organization-Id`.** It is an operator-only selector
  and a request carrying it is refused with `403
  organization_selector_forbidden`, even when the id is your own.
* **A dataset id is interpreted within your scope.** The same id in two scopes
  names two different corpora, and a dataset outside your scope is simply not
  there. `GET /api/access` echoes the scope your token actually resolved to,
  which is the quickest way to confirm you are reading what you think you are.

## Failure modes

Distinct on purpose, so a client can branch without parsing prose.

| Situation                                                           | Status | `code`                            |
| ------------------------------------------------------------------- | ------ | --------------------------------- |
| No credential presented                                             | `401`  | `credential_required`             |
| Credential not recognized, malformed, or expired                    | `401`  | `credential_invalid`              |
| Valid credential, capability it does not hold                       | `403`  | `insufficient_capability`         |
| `X-Squash-Organization-Id` sent by a customer credential            | `403`  | `organization_selector_forbidden` |
| Dataset id that is not a well-formed id at all                      | `400`  | `dataset_id_invalid`              |
| Dataset absent, outside your scope, or not measured for that metric | `404`  | `quality_enrichment_unavailable`  |
| The measurements behind the answer moved on since it was written    | `409`  | `quality_enrichment_stale`        |

A `401` means the credential itself is the problem, and retrying with it will
not help until you mint a new one. A `403` means the credential is fine and the
request is not. A `404` is a fact about the corpus, not about your access —
which is why an unmeasured metric and an unknown dataset share one code: both
mean there is nothing there to read, and a client should branch once.
