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

# Authentication

> Trade an API key for a session token, send the token on every request, and get a new one before it runs out.

Every request except the two that hand out tokens carries a session token. An integration gets its
token from an API key. A script a person runs for themselves can sign in with an email and password
instead.

## Get an API key

An administrator issues keys on the [API keys](/config/api-keys) screen. Every key belongs to an
identity, which holds roles and site access the same way a person does. The key can never do more
than its identity, so give the identity the roles your integration needs before you issue the key.

The key is shown once, when it is issued. Store it the way you would store a password.

## Trade the key for a token

Call `exchange_api_key` with no token:

```bash theme={null}
curl -X POST https://app.assetinfinity.ai/api/rpc/exchange_api_key \
  -H 'Content-Type: application/json' \
  -d '{"p_key": "cmk_8d1a2136_…"}'
```

The answer is a list with one row:

```json theme={null}
[
  {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "expires_at": "2026-10-08T01:22:01.207994+00:00",
    "organization": "Northwind Manufacturing",
    "service_account": "ERP sync",
    "acting_as": "ERP sync",
    "scopes": [],
    "error_code": null,
    "message": null
  }
]
```

Send the token as a bearer token on every other request:

```bash theme={null}
curl "https://app.assetinfinity.ai/api/sites?select=id,code,name" \
  -H "Authorization: Bearer $TOKEN"
```

<Warning>
  A refused key still answers with HTTP 200. Check `error_code` before you use `token`: on a refusal
  `token` is `null` and `message` says why.
</Warning>

| `error_code` | What it means |
| - | - |
| `INVALID_KEY` | The key is unknown, expired or revoked, or its identity is suspended. The message is the same for all four on purpose |
| `TOO_MANY_EXCHANGES` | The key was traded too often in the last minute. Reuse the token you have |
| `SERVICE_ACCOUNT_HAS_NO_ROLES` | The identity holds no role, so a token could do nothing. Give it a role on the **Access** screen |
| `MAY_NOT_ACT_AS_USERS` | You passed `p_on_behalf_of`, and the identity is not allowed to act as a person |
| `NO_SUCH_PERSON` | You passed `p_on_behalf_of` with an address that names nobody in this organisation |

## Keep the token fresh

A token from a key lasts fifteen minutes. Reuse it for all of them. Trading the key again on every
request is refused as a rate limit.

Before the token runs out, either trade the key again or call `refresh_session` with the token you
have. Both answer with a new `token`.

```bash theme={null}
curl -X POST https://app.assetinfinity.ai/api/rpc/refresh_session \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{}'
```

A request with a missing, malformed or expired token is answered with HTTP 401.

## Check who the token is

`whoami` returns the account the token belongs to, its roles and its organisation. It is the quickest
way to confirm a new key works.

```bash theme={null}
curl -X POST https://app.assetinfinity.ai/api/rpc/whoami \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{}'
```

```json theme={null}
{
  "user": "ERP sync",
  "email": "erp-sync@service-account.invalid",
  "roles": ["MAINT_MANAGER"],
  "organization": "Northwind Manufacturing"
}
```

## Act on behalf of a person

If the identity is set to act as **A person** on the [API keys](/config/api-keys) screen, pass
`p_on_behalf_of` with that person's email address. The token then names the person, so what your
integration writes is recorded against them. The token can do only what the person, the identity and
the key all allow.

```json theme={null}
{ "p_key": "cmk_8d1a2136_…", "p_on_behalf_of": "meera.shah@northwind.example" }
```

## Sign in with an email and password

A script that a person runs for themselves can sign in as that person instead of using a key:

```bash theme={null}
curl -X POST https://app.assetinfinity.ai/api/rpc/sign_in \
  -H 'Content-Type: application/json' \
  -d '{"p_email": "you@example.com", "p_password": "…"}'
```

The response carries a `token` that you use the same way. Prefer a key for anything that runs
unattended: a key does not stop working when a person leaves or changes their password.

<Note>
  A tenant on its own hostname uses that hostname in place of `app.assetinfinity.ai`. Every example
  on these pages uses `https://app.assetinfinity.ai/api`.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.