Get startedAuthentication
Authentication
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.
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 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.
| 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_ 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_, with the reason in message. |
| Missing permission | HTTP 400 with code 50, ERROR_. The API never answers 403; see errors. |
| Too many failed sign-ins | HTTP 429, ERROR_. Stop at the first 401 instead of retrying; see 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:
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/jsoncurl 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:
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'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));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))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'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));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:
{
"code": 401,
"name": "ERROR_UNAUTHORIZED",
"message": "Access denied (Unauthorized call.).",
"http_status": 401,
"success": false
}{
"status": 401,
"message": "Access denied (Unauthorized call.).",
"errorcode": null,
"traceback": null
}The quickstart 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 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; you do not build it yourself.
| Item | Value |
|---|---|
| Grant | Authorization code with PKCE (S256) |
| Endpoints | /, /, /, / |
| 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>. and <area>., such as accounts.; see 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. 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.