Developers
Set your API contextPick a region and space ID once. Every sample fills in your base URL.
Where do I find these?

In Coevera: open your space's API application and choose Show Access. It shows the space ID and the base URL, whose host starts with your region.

From the API: if you know the space ID, the discovery endpoint GET /api/discovery/spaces/{space_id} returns its region and base URLs, without credentials.

Only the region and space ID are kept, in this browser. Never paste a key or password here.

More in the quickstart

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_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, with the reason in message.
Missing permission HTTP 400 with code 50, ERROR_NOT_ENOUGH_PERMISSIONS. The API never answers 403; see 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.

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:

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:

401 UnauthorizedRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "code": 401,
  "name": "ERROR_UNAUTHORIZED",
  "message": "Access denied (Unauthorized call.).",
  "http_status": 401,
  "success": false
}
401 UnauthorizedRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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 /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
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.