---
title: Authentication
description: Integrations authenticate as an API application, with its user name and password over HTTP Basic. OAuth 2.0 is used only by Coevera's AI connectors.
canonical: https://developers.coevera.com/get-started/authentication/
section: get-started
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Authentication

**Answer:** Create an API application and send its user name and password with every request as Authorization: Basic, the base64 of "user name:password". Each application has its own permissions and works in one space only. A failed sign-in answers 401 ERROR_UNAUTHORIZED. OAuth 2.0 is used by Coevera's AI connectors; general OAuth apps are not yet available.

Every call to the REST or GraphQL API carries the credentials of an **API application**. OAuth
2.0 exists too, but only behind the AI connectors that Coevera registers.

## API keys (Basic auth)

API applications with HTTP Basic auth are the documented way to integrate. An admin creates the
application under **Administration › Units, Users & Roles › Applications** (the
[quickstart](/get-started/quickstart/#get-api-credentials) walks through it) and sets what it may
do with **Edit Permissions**, on three tabs: **Records**, **Administration** and **Misc**. Each
entry is **Full Access**, **Read Only Access** or **No Access**; see
[Permissions and scopes](/get-started/permissions-and-scopes/).

| What                     | How it works                                                                                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Credential belongs to    | The application, not the space or a person. A space can have many applications, each with its own credentials and permissions.                                                  |
| User name                | The space ID, an underscore and 40 letters and digits, such as `nv2_northwind_docs_` followed by 40 characters. It works only in that space.                                    |
| Password                 | Shown in full only when the application is created or its credentials are regenerated. It stays valid until someone presses **Generate New**, which replaces the user name too. |
| Failed sign-in           | HTTP `401`, [`ERROR_UNAUTHORIZED`](/reference/errors/#code-401), with the reason in `message`.                                                                                  |
| Missing permission       | HTTP `400` with code [`50`](/reference/errors/#code-50), `ERROR_NOT_ENOUGH_PERMISSIONS`. The API never answers `403`; see [errors](/concepts/errors/).                          |
| Too many failed sign-ins | HTTP `429`, `ERROR_TOO_MANY_REQUESTS`. Stop at the first `401` instead of retrying; see [rate limits and retries](/concepts/rate-limits-and-retries/).                          |

## Send the credentials

HTTP Basic auth sends `Authorization: Basic` and the base64 of the user name, a colon and the
password, on every request:

```text
GET /api/v100/rest/spaces/{space_id}/entities/Accounts?first=1 HTTP/1.1
Host: {region}.api.coevera.com
Authorization: Basic <base64 of "user name:password">
Accept: application/json
```

`curl` builds the header from `-u` and `requests` from its `auth` argument; in Node.js you build
it with `Buffer`. Never put the credentials in the URL or the query string. The program
below sends the same request twice, with the application's credentials and without them:

**REST · cURL**

```bash
COEVERA_BASE="https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"

# -u sends the user name and password as the Authorization: Basic header.
curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Accounts" \
  -d first=1 \
  -d load-only=id,name \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
echo

# The same request without -u: the API answers 401 and says why in the message.
curl -sS -G "$COEVERA_BASE/entities/Accounts" \
  -d first=1 \
  -d load-only=id,name \
  -w '\nHTTP %{http_code}\n'
```

**REST · TypeScript**

```ts
import { Buffer } from "node:buffer";
import { env } from "node:process";

const base = `https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}`;
// HTTP Basic: the base64 of "user name:password".
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");
const url = `${base}/entities/Accounts?${new URLSearchParams({ first: "1", "load-only": "id,name" })}`;

const res = await fetch(url, { headers: { Authorization: `Basic ${auth}` } });
const body = await res.json();
if (!res.ok) throw new Error(`HTTP ${res.status} ${body.name}: ${body.message}`);
console.log(JSON.stringify(body, null, 2));

// The same request without the Authorization header: the API answers 401 and says why.
const refused = await fetch(url);
console.log(`HTTP ${refused.status}`, JSON.stringify(await refused.json(), null, 2));
```

**REST · Python**

```python
import json
import os
import sys

import requests

BASE = "https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"
# requests sends a (user name, password) tuple as HTTP Basic auth.
AUTH = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])
PARAMS = {"first": 1, "load-only": "id,name"}

response = requests.get(f"{BASE}/entities/Accounts", params=PARAMS, auth=AUTH, timeout=30)
body = response.json()
if not response.ok:
    sys.exit(f"HTTP {response.status_code} {body.get('name')}: {body.get('message')}")
print(json.dumps(body, indent=2))

# The same request without auth: the API answers 401 and says why in the message.
refused = requests.get(f"{BASE}/entities/Accounts", params=PARAMS, timeout=30)
print(f"HTTP {refused.status_code}", json.dumps(refused.json(), indent=2))
```

**GraphQL · cURL**

```bash
COEVERA_GRAPHQL="https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public"
QUERY='{"query": "{ entities { account { getByCriteria(first: 1) { totalCount edges { node { id name } } } } } }"}'

# -u sends the user name and password as the Authorization: Basic header.
curl -sS --fail-with-body "$COEVERA_GRAPHQL" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "$QUERY"
echo

# The same request without -u: the API answers 401 and says why in the message.
curl -sS "$COEVERA_GRAPHQL" \
  -H "Content-Type: application/json" \
  -d "$QUERY" \
  -w '\nHTTP %{http_code}\n'
```

**GraphQL · TypeScript**

```ts
import { Buffer } from "node:buffer";
import { env } from "node:process";

const endpoint = `https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public`;
// HTTP Basic: the base64 of "user name:password".
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");
const query = JSON.stringify({
  query: "{ entities { account { getByCriteria(first: 1) { totalCount edges { node { id name } } } } } }",
});

const res = await fetch(endpoint, {
  method: "POST",
  headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
  body: query,
});
const body = await res.json();
// GraphQL reports errors in the body, often with HTTP 200: check both.
if (!res.ok || body.errors) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.errors ?? body)}`);
console.log(JSON.stringify(body.data, null, 2));

// The same request without the Authorization header: the API answers 401 and says why.
const refused = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: query });
console.log(`HTTP ${refused.status}`, JSON.stringify(await refused.json(), null, 2));
```

**GraphQL · Python**

```python
import json
import os
import sys

import requests

ENDPOINT = (
    "https://{region}.api.coevera.com/api/v100/app/space/"
    "{space_id}/graphql/public"
)
# requests sends a (user name, password) tuple as HTTP Basic auth.
AUTH = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])
QUERY = "{ entities { account { getByCriteria(first: 1) { totalCount edges { node { id name } } } } } }"

response = requests.post(ENDPOINT, json={"query": QUERY}, auth=AUTH, timeout=30)
body = response.json()
# GraphQL reports errors in the body, often with HTTP 200: check both.
if not response.ok or body.get("errors"):
    sys.exit(f"HTTP {response.status_code}: {json.dumps(body.get('errors', body))}")
print(json.dumps(body["data"], indent=2))

# The same request without auth: the API answers 401 and says why in the message.
refused = requests.post(ENDPOINT, json={"query": QUERY}, timeout=30)
print(f"HTTP {refused.status_code}", json.dumps(refused.json(), indent=2))
```

The first request returns one account. The second answers `401`, with the reason in `message`.
In GraphQL the status is the same and the body is shorter:

**Recorded response · REST · 401 Unauthorized**

```json
{
  "code": 401,
  "name": "ERROR_UNAUTHORIZED",
  "message": "Access denied (Unauthorized call.).",
  "http_status": 401,
  "success": false
}
```

**Recorded response · GraphQL · 401 Unauthorized**

```json
{
  "status": 401,
  "message": "Access denied (Unauthorized call.).",
  "errorcode": null,
  "traceback": null
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

The [quickstart](/get-started/quickstart/#if-you-get-401) lists the causes of a `401` and their
fixes.

## Rotate the credentials

Open the application, click **Show Access**, then **Generate New**. The CRM asks you to confirm,
then shows a new user name and a new password. The old pair stops working at once, so plan for
it:

- **With a short window.** Generate the new pair, then update the integration straight away;
  calls in between answer `401`.
- **Without a window.** Create a second application with the same permissions, switch the
  integration to its credentials, then delete the old application.

Copy both values every time: an integration that updates only the password still gets `401`.

> **The IP allow-list and API applications:** The [5.22.0 release notes](https://www.coevera.com/release-notes/outlook-add-in-improvements-advanced-sign-in-security-settings-expanded-automatizer-capabilities-and-other-various-improvements-v5-22-0/)
> say that a sign-in IP allow-list (Advanced Sign-In Security) restricts API applications too,
> while the integrations that Coevera provides and its AI tools are not restricted.

## Users, single sign-on and applications

An API application is not a person. People sign in to Coevera with a password or with single
sign-on, and two-factor authentication can apply to them; none of that is used for API calls. An
integration always authenticates as an application, with its own user name and password,
whatever way the space's users sign in.

## OAuth for AI connectors

The Claude, ChatGPT and custom MCP connectors sign users in with OAuth 2.0. You meet this flow
when you [connect an AI assistant](/ai/overview/); you do not build it yourself.

| Item            | Value                                                                                                   |
| --------------- | ------------------------------------------------------------------------------------------------------- |
| Grant           | Authorization code with PKCE (`S256`)                                                                   |
| Endpoints       | `/oauth2/app/authorize`, `/oauth2/app/token`, `/oauth2/app/introspection`, `/oauth2/app/revocation`     |
| Access token    | Bearer, valid for 1 hour                                                                                |
| Refresh token   | Valid for 90 days; every refresh returns a new one and revokes the old pair                             |
| Scopes          | `<area>.read` and `<area>.full`, such as `accounts.full`; see [OAuth and scopes](/ai/oauth-and-scopes/) |
| Who can consent | A user with the **Admin API Access** right in that space                                                |

> **General OAuth apps: not yet available:** Coevera registers OAuth clients itself; there is no self-service registration. If you are
> building a product for many Coevera customers, [contact us](/support/support-and-status/). Until
> then, use an API application.

## Which one applies to you

|            | API application (Basic)                     | OAuth (AI connectors)                                             |
| ---------- | ------------------------------------------- | ----------------------------------------------------------------- |
| Use it for | Your own integration, script, sync or agent | Claude, ChatGPT, a custom MCP client                              |
| Set up by  | A space admin, in the CRM                   | The user, in the assistant, after Coevera activates the connector |
| Acts with  | The application's permissions               | The scopes the user approved                                      |

## Limits

- A credential works in one space only. An integration that serves several spaces needs an
  application in each.
- An application has one user name and password at a time. They do not expire; they change only
  with **Generate New**.
- There is no self-service OAuth client registration.
