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

# Authentication

> Authenticate with the Nomos API using OAuth 2.0.

The Nomos API uses [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749). You'll need a `client_id` and `client_secret` issued to your organization. Keep the secret server-side.

Most integrations use the [Client Credentials](#client-credentials-grant) flow for server-to-server access against your own organization's data. If your app instead acts on behalf of a customer (for example, a HEMS provider reading prices for a specific subscription), use the [Authorization Code](#authorization-code-grant) flow.

## Client Credentials Grant

Server-to-server flow against your own organization's data ([RFC 6749 §4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)). No customer interaction is involved.

### Request a token

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.nomos.energy/oauth/token \
    -u "${CLIENT_ID}:${CLIENT_SECRET}" \
    -d grant_type=client_credentials
  ```

  ```ts Node.js theme={null}
  const credentials = Buffer.from(
    `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`,
  ).toString("base64");

  const res = await fetch("https://api.nomos.energy/oauth/token", {
    method: "POST",
    headers: {
      Authorization: `Basic ${credentials}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: "grant_type=client_credentials",
  });

  const { access_token, refresh_token, expires_in } = await res.json();
  ```

  ```python Python theme={null}
  import os, requests

  res = requests.post(
      "https://api.nomos.energy/oauth/token",
      auth=(os.environ["CLIENT_ID"], os.environ["CLIENT_SECRET"]),
      data={"grant_type": "client_credentials"},
  )
  res.raise_for_status()
  access_token = res.json()["access_token"]
  ```
</CodeGroup>

`/oauth/token` responds with:

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "21cc84a3ad98736f4e5eddc88a1f4b58a29ae96206027c9b59d874cb2a7f7e02",
  "scope": "read:* write:*"
}
```

The `scope` field is a space-delimited list of scopes granted on the token, mirroring the standard OAuth 2.0 [`scope` claim](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). See [Scopes](#scopes) for what each value allows.

### Use the access token

Send it in the `Authorization` header on every API request:

```bash theme={null}
curl https://api.nomos.energy/subscriptions \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"
```

### Refresh

Access tokens last **60 minutes**. When yours expires, exchange the refresh token at `/oauth/token` for a new pair:

```bash theme={null}
curl -X POST https://api.nomos.energy/oauth/token \
  -u "${CLIENT_ID}:${CLIENT_SECRET}" \
  -d grant_type=refresh_token \
  -d refresh_token=${REFRESH_TOKEN}
```

## Authorization Code Grant

If your app acts on a customer's behalf instead of running server-to-server, use this flow ([RFC 6749 §4.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1)). The token response, header usage, and refresh model are identical to [Client Credentials](#client-credentials-grant); only the way you obtain the initial token differs.

Your callback URI must be registered against your `client_id`; subpaths under the registered origin are accepted.

<Steps>
  <Step title="Redirect the customer to /oauth/authorize">
    ```
    https://api.nomos.energy/oauth/authorize?
      client_id=${CLIENT_ID}&
      response_type=code&
      redirect_uri=${REDIRECT_URI}&
      state=${STATE}
    ```

    `state` is a per-request opaque value you generate, store, and verify on callback (CSRF protection).

    <Tip>
      For [PKCE](#pkce), also send `code_challenge` and `code_challenge_method` (use `S256`). Public clients (no `client_secret`) must use PKCE.
    </Tip>

    <Note>
      While the redirect is open, the customer signs in and grants consent on Nomos-hosted screens. See the [third-party login guide](/guides/third-party-oauth) for the customer journey.
    </Note>
  </Step>

  <Step title="Handle the callback">
    Nomos redirects with a single-use code (10-minute TTL) and your `state`. Reject the callback if `state` doesn't match.

    ```
    https://your-app.com/callback?
      code=${AUTHORIZATION_CODE}&
      state=${STATE}
    ```
  </Step>

  <Step title="Exchange the code at /oauth/token">
    ```bash theme={null}
    curl -X POST https://api.nomos.energy/oauth/token \
      -u "${CLIENT_ID}:${CLIENT_SECRET}" \
      -d grant_type=authorization_code \
      -d code=${AUTHORIZATION_CODE} \
      -d redirect_uri=${REDIRECT_URI}
    ```

    For PKCE, also send `code_verifier`.
  </Step>
</Steps>

### PKCE

PKCE (Proof Key for Code Exchange) hardens the flow against code interception ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)). Prefer `S256` over `plain`.

<Steps>
  <Step title="Generate a verifier and challenge">
    Pick a random `code_verifier` of 43–128 characters from `[A-Za-z0-9-._~]`.
    With `S256`, the `code_challenge` is the base64url-encoded SHA-256 of the
    verifier; with `plain`, it's the verifier itself.
  </Step>

  <Step title="Send the challenge on /oauth/authorize">
    Add `code_challenge` and `code_challenge_method` to the authorize URL.
  </Step>

  <Step title="Send the verifier on /oauth/token">
    Include the original `code_verifier` in the token request. Nomos validates
    it against the challenge.
  </Step>
</Steps>

## Scopes

Each API key is issued with one or more scopes. Two scopes are defined today:

| Scope     | Allows                                                     |
| --------- | ---------------------------------------------------------- |
| `read:*`  | All `GET` requests across the API.                         |
| `write:*` | All non-`GET` requests (`POST`, `PATCH`, `PUT`, `DELETE`). |

A key with both scopes (`read:* write:*`) is full-access, the default. A read-only key carries only `read:*` and is rejected with `403 FORBIDDEN` on any mutating request. Choose read-only when issuing a key for a programmatic agent, ETL job, or BI consumer that should never mutate state.

Scopes are set when the key is created and cannot be changed later. To switch a key from full access to read-only (or vice versa), create a new key and rotate.

The `scope` field is `*` because future iterations may introduce resource-named scopes (`read:invoice`, `write:subscription`) that narrow access further. Keys with the wildcard scopes continue to behave as supersets.

<Note>
  Auth failures return `401 UNAUTHORIZED` with the standard error envelope.
  Scope failures return `403 FORBIDDEN`. See
  [Errors](/api-references/errors/UNAUTHORIZED) for the response shape and
  common causes.
</Note>
