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

# Authentication

> Exchange your client credentials for an access token, and request the scopes your integration needs.

The eMSP API authenticates with OAuth2 client credentials. You exchange a client ID and secret for a short-lived access token, then send that token as a bearer token on every request.

This is different from the rest of the Spirii API, which uses a long-lived API key. If you're integrating against those endpoints, see [Authentication](/developers/authentication) instead.

## How access is structured

Four things sit between your company and an endpoint.

```mermaid theme={null}
flowchart TD
  O("Organisation<br/>(your company)") --> W("Workspace<br/>(your eMSP tenant)")
  W --> C("Credential<br/>(ID and secret)")
  C --> S("Scopes<br/>(what it may do)")
```

Your **organisation** is your company as Spirii knows it. Inside it, a **workspace** groups credentials; on the eMSP API a workspace is a tenant, so the workspace your credential belongs to decides whose networks, locations and webhooks you're working with. A **credential** is one OAuth2 client: a client ID and a secret. The **scopes** attached to that credential set the capabilities it can ask for.

You never pass an organisation, workspace or tenant ID in a request. All three travel inside the access token, and Spirii resolves them from it.

## Get your credentials

Spirii provisions credentials during onboarding. Your Spirii contact sends you a client ID, a client secret, and the list of scopes attached to the credential.

Store the secret in a secret manager or an environment variable, never in source control. The [security best practices](/developers/authentication#security-best-practices) for API keys apply here unchanged.

When your integration grows into a capability the credential doesn't cover, ask Spirii to attach the scope. That's a grant against your existing credential, not a new credential and not a code change on your side.

## Get an access token

Post your credentials to the token endpoint of Spirii's `public-api` realm, naming the scopes you want the token to carry:

```bash theme={null}
curl -X POST 'https://auth.spirii.dk/realms/public-api/protocol/openid-connect/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d "client_id=$SPIRII_CLIENT_ID" \
  -d "client_secret=$SPIRII_CLIENT_SECRET" \
  --data-urlencode 'scope=emsp:networks:read emsp:locations:read'
```

Scope values are space-separated, so they have to be URL-encoded. With `curl` that means `--data-urlencode` rather than `-d`.

The response carries four fields worth reading:

| Field          | What it's for                                                                |
| -------------- | ---------------------------------------------------------------------------- |
| `access_token` | The token you send as a bearer token                                         |
| `expires_in`   | Seconds until the token expires. Access tokens live for 300 seconds          |
| `token_type`   | Always `Bearer`                                                              |
| `scope`        | The scopes the token actually carries. Check this matches what you asked for |

A token is good for five minutes. Cache it and fetch a new one shortly before it expires. Requesting a token per API call works, but it adds a round trip to every request for no benefit.

## Scopes

A scope attached to your credential makes it *allowed* to request that capability. It does not put the capability in every token. The token carries a scope only when you name it in the `scope` parameter. Two things follow from that, and both look like a broken integration when you meet them for the first time.

<Warning>
  **Omit the `scope` parameter and the token request still succeeds.** You get a valid token carrying none of your capabilities, and every eMSP endpoint answers `403`. **Request a scope that isn't attached to your credential and the whole token request fails** with `invalid_scope`, so a single typo costs you the token rather than that one scope.
</Warning>

These are the scopes available across the eMSP API:

| API       | Scope                     | Grants                                                          |
| --------- | ------------------------- | --------------------------------------------------------------- |
| Tenants   | `emsp:tenants:read`       | View tenant profile, EMP identities, and webhooks               |
| Tenants   | `emsp:tenants:write`      | Manage tenant profile, EMP identities, and webhooks             |
| Networks  | `emsp:networks:read`      | View network and access control configuration                   |
| Networks  | `emsp:networks:write`     | Create, update and delete SUB networks, and manage access rules |
| Networks  | `emsp:networks:publish`   | Publish and drain networks                                      |
| Networks  | `emsp:networks:subscribe` | Attach to another tenant's published base network as a consumer |
| Locations | `emsp:locations:read`     | Search and view locations, EVSEs and connectors                 |
| Webhooks  | `emsp:webhooks:read`      | View webhook subscriptions and delivery history                 |
| Webhooks  | `emsp:webhooks:write`     | Create, update, pause and delete webhook subscriptions          |

Ask for the scopes a given token needs rather than everything attached to the credential. A token minted for a nightly location sync has no reason to carry `emsp:networks:publish`, and keeping it out means a leaked token can't be used to publish anything.

Changing what's attached to a credential doesn't reach tokens already issued. After Spirii attaches a scope, request a new token to pick it up.

## Authorize a request

Send the access token in the `Authorization` header, prefixed with `Bearer`:

```bash theme={null}
curl 'https://api.spirii.com/emsp/v1/locations' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Every eMSP endpoint sits under `https://api.spirii.com/emsp/v1`.

## What's in the token

The access token is a signed JWT issued by `https://auth.spirii.dk/realms/public-api` using RS256. Decode it and you'll find the claims Spirii uses to identify you:

| Claim             | What it holds                                       |
| ----------------- | --------------------------------------------------- |
| `azp`             | Your client ID                                      |
| `organisation_id` | The organisation the credential belongs to          |
| `workspace_id`    | The workspace, which is your eMSP tenant            |
| `credential_id`   | The credential itself, useful when you hold several |
| `scope`           | The granted scopes, space-separated                 |
| `exp`             | Expiry, as a Unix timestamp                         |

Reading these claims helps when you're working out why a call was refused. Spirii verifies the token and derives your identity from it on every request, so treat the claims as diagnostic information rather than something your own code makes access decisions from.

Alongside your scopes, `scope` also carries Keycloak's built-in defaults such as `email` and `profile`. They grant nothing on the eMSP API and are safe to ignore.

## Errors

| Response           | Returned by    | Cause                                                                                                                          |
| ------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `invalid_client`   | Token endpoint | Wrong client ID or secret, or the credential has been disabled                                                                 |
| `invalid_scope`    | Token endpoint | A requested scope isn't attached to the credential, or is misspelled                                                           |
| `401` Unauthorized | eMSP API       | The `Authorization` header is missing or malformed, or the token has expired or wasn't issued by Spirii                        |
| `403` Forbidden    | eMSP API       | The token is valid but carries no scope covering this endpoint. Request a new token naming the scope you need                  |
| `403` Forbidden    | eMSP API       | The request presented an `x-spirii-*` header. Spirii sets those from your token; sending them yourself is rejected at the edge |

## Rotate and revoke

Ask Spirii to rotate the secret and you get a new one immediately; the old secret stops working for new token requests at the same moment.

Tokens already issued are a different matter. A token stays valid for its full 300 seconds, and Spirii caches the result of verifying it for up to 300 seconds more, so a credential can keep working for roughly ten minutes after it's rotated or disabled. Neither action is an instant kill switch; plan rotations and revocations around that window.

To rotate without downtime: take the new secret, fetch a token with it to confirm it works, roll it into your integration, and drop the old one.

## Related

<Columns cols={3}>
  <Card title="API overview" icon="square-terminal" href="/api-reference/overview">
    The four Spirii APIs, their path prefixes, and where to start.
  </Card>

  <Card title="Errors" icon="triangle-alert" href="/developers/errors">
    The status codes the API returns and how to handle them.
  </Card>

  <Card title="Rate limits" icon="gauge" href="/developers/rate-limits">
    The request limits and how to stay under them.
  </Card>
</Columns>
