# Authentication

> Authenticate with an API key for your own company, or with OAuth 2.1 and PKCE for an app that other Numina companies connect to.

Every request carries a bearer token in the `Authorization` header. A token belongs to exactly one company, and every request reads and writes that company's books only; there is no company ID in the URL.

```http
Authorization: Bearer numi_4f1c...
```

| | API key | OAuth access token |
| --- | --- | --- |
| For | Your own company, scripts and back-office jobs | Apps that other companies connect to |
| Acts as | The company, with no user | The user who approved the connection |
| Lifetime | Until revoked | 1 hour, refreshed with a rotating refresh token |
| Created in | Settings → API & apps | The consent screen in your app's flow |

## Scopes

Both kinds of token carry scopes. Each names a resource and what may be done with it: `read`, `write` to create and change drafts and records, or `book` to post to the ledger. A request without the scope it needs fails with `403 insufficient_scope`, and the error's `scope` field names what the route asks for.

| Resource | Read | Write | Book |
| --- | --- | --- | --- |
| Company, accounting years, chart of accounts and VAT codes | `setup:read` | | |
| Balances and trial balance | `reports:read` | | |
| Journal entries and ledger entries | `journal:read` | `journal:write` | `journal:book` |
| Invoices | `invoices:read` | `invoices:write` | `invoices:book`, which also sends |
| Contacts | `contacts:read` | `contacts:write` | |
| Products | `products:read` | `products:write` | |
| Documents | `documents:read` | `documents:write` | |
| Bank accounts and bank lines | `bank:read` | | |

Scopes are independent: `journal:book` does not include `journal:write`, and no write scope includes reading. Ask for every level you use, and no more. [`GET /v1/me`](https://developers.numina.app/api/company/check-credentials.md) shows what a token can do, and each endpoint in the [API reference](https://developers.numina.app/api.md) lists the scopes it accepts.

Booking scopes are for API keys, for your own app in your own company, and for reviewed partner apps. Booking through the API is your own approval of the entry: see [Drafts and booking](https://developers.numina.app/accounting-model.md#drafts-and-booking).

> [!NOTE]
> **The original scopes**
>
>
> `read:ledger`, `write:draft` and `write:ledger` still work. `read:ledger` reaches every read scope, `write:draft` every write scope, and `write:ledger` every write and book scope, without read access. Use the resource scopes in new integrations.

## API keys

Create a key in Numina under Settings → API & apps and pick its permissions: read only, read and draft, or full access, which can also post to the ledger. Custom permissions set a level for each resource. Copy the key when it is shown: it is shown once. Keys start with `numi_`, and any member of the company can create them and revoke them from the same page.

> [!WARNING]
> A key carries the company's access with no user attached. Keep it server-side, never in a browser or mobile app, and rotate it if it may have leaked.

## OAuth

Numina is an OAuth 2.1 authorization server. Apps use the authorization code flow with PKCE (`S256`); the user signs in to Numina, picks the company to connect, and approves your scopes.

> [!NOTE]
> **Registering an app**
>
>
> A company admin creates an OAuth application in Numina under Settings → API & apps and gets a client ID and secret right away. Your own company can connect it with any scope, booking included, and so can a sandbox company you are a member of, which is enough to build and test.
>
> To let other companies connect it, make the app available to other companies in its settings. An admin of the other company approves the connection, the consent screen says that Numina has not reviewed the app, and it works for up to 10 companies. With a booking scope, the consent screen also tells the admin that the app can book and that booking cannot be undone.
>
> When the app needs more than 10 companies, write to [support@numina.app](mailto:support@numina.app) with its name and redirect URIs: we review it and lift the limit on the same app, with the same client ID and secret.

### Send the user to the authorization endpoint

Generate a `code_verifier`, derive its `code_challenge`, and redirect:

```text
https://api.numina.app/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=setup:read journal:read journal:write
  &state=RANDOM_STATE
  &code_challenge=CODE_CHALLENGE
  &code_challenge_method=S256
```

### Exchange the code for tokens

Numina redirects back with `code` and your `state`. Exchange the code within a few minutes:

```bash title="cURL"
curl -X POST https://api.numina.app/oauth/token \
  -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
  -d grant_type=authorization_code \
  -d code=AUTHORIZATION_CODE \
  -d redirect_uri=https://yourapp.com/callback \
  -d code_verifier=CODE_VERIFIER
```

Send the client ID and secret as HTTP Basic authentication, as `-u` does here. A secret in the request body is refused with `invalid_client`.

The response holds an `access_token` valid for an hour and a `refresh_token` valid for 30 days.

### Refresh before the access token expires

```bash title="cURL"
curl -X POST https://api.numina.app/oauth/token \
  -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
  -d grant_type=refresh_token \
  -d refresh_token=REFRESH_TOKEN
```

Refresh tokens rotate: every refresh returns a new one and retires the old. Store the new token before you use the access token. A retired refresh token used more than 30 seconds after its refresh ends the connection, and the user has to connect again.

The server's endpoints are also published as metadata at [`/.well-known/oauth-authorization-server`](https://api.numina.app/.well-known/oauth-authorization-server), so standard OAuth libraries can discover them.

### Revocation

A user can disconnect your app in Numina at any time, which revokes its tokens immediately. Expect a `401` on any request and send the user through the flow again.
