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

# Authentication

> Create an API client and get a token.

The Envoi API uses OAuth 2.0 *client credentials*. You exchange a client ID and secret for a token, and send the token with every call.

## Create an API client

Only the company's owner and admins can create API clients.

<Steps>
  <Step title="Go to API clients">
    Open **Settings → API clients** in Envoi and choose **Create API client**.
  </Step>

  <Step title="Choose a type">
    **Accounting client** comes set up with access to invoices, customers, journal entries and reports. **Advanced setup** lets you choose the environment and exactly which permissions the client gets.
  </Step>

  <Step title="Copy the secret">
    The secret is shown once, and Envoi does not store it. Copy it right away and keep it safe. You also need the **client ID** and **account ID**.
  </Step>
</Steps>

<Warning>
  The secret gives access to the company's data. Never put it in code that runs in a browser or an app, and never in a public repository.
</Warning>

## Get a token

```bash theme={null}
curl https://api.envoi.no/v1/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d audience=https://api.envoi.no/v1/accounts/P11112001
```

| Field | Value |
| - | - |
| `grant_type` | Always `client_credentials` |
| `audience` | `https://api.envoi.no/v1/accounts/{account ID}`. The account ID decides the environment: `P…` is production, `T…` is test |
| `client_id`, `client_secret` | In the `Authorization: Basic` header (as above) or in the form, not both |

The response:

```json theme={null}
{
  "access_token": "eyJhbGciOi…",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "bilag:read clients:write invoices:write reports:read"
}
```

The token lasts **15 minutes**. There is no refresh token: request a new one with the same credentials when it expires.

## Use the token

```bash theme={null}
curl https://api.envoi.no/v1/invoices \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

A token covers one company in one environment. Which company a call acts on is decided by the token and cannot be changed with a header or in the URL.

## Permissions

Permissions are written as `resource:level`, for example `invoices:write`. Levels build on each other: `admin` includes `write`, and `write` includes `read`.

| Permission | Gives access to |
| - | - |
| `invoices:read` | Listing and reading invoices |
| `invoices:write` | Creating, sending and recording payments |
| `clients:read` | Listing and reading customers |
| `clients:write` | Creating customers |
| `bilag:read` | Chart of accounts and journal entries |

If the client lacks the permission an endpoint needs, you get `403 insufficient_scope`.

## Changes apply immediately

Envoi checks the client against the database on every call, not just the token:

* **Delete the client**, and its tokens are refused on the next call.
* **Rotate the secret**, and tokens issued with the old one are refused on the next call.
* **Remove a permission or an environment**, and it applies from the next call.

## Token endpoint errors

The token endpoint answers with `{ "error": "<code>" }`, as OAuth 2.0 describes.

| Status | `error` | When |
| - | - | - |
| 400 | `invalid_request` | Not a form, or the secret both in the header and in the form |
| 400 | `unsupported_grant_type` | `grant_type` is not `client_credentials` |
| 400 | `invalid_target` | The `audience` is malformed, names another company, or an environment the client has no access to |
| 401 | `invalid_client` | Unknown client, wrong secret or deleted client. The same answer for all three |
| 403 | `access_denied` | The company is being closed |
| 429 | `rate_limited` | Too many attempts: 60 a minute per IP address and 30 a minute per client. `Retry-After` says when to try again |
