---
title: "Quickstart: your first API call"
description: Create an API application in Coevera, copy its credentials and list your accounts, in REST or GraphQL, with cURL, TypeScript or Python.
canonical: https://developers.coevera.com/get-started/quickstart/
section: get-started
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Quickstart: your first API call

**Answer:** An admin creates an API application in the CRM and copies its user name, password, space ID and service URL. Your code sends the user name and password with HTTP Basic auth to https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}. A GET on /entities/Accounts with first=3 returns the first three accounts as JSON.

From an empty terminal to a first successful call: create an API application, copy its
credentials, and list the accounts in your space. It takes about five minutes, in REST or
GraphQL.

## Before you begin

- A Coevera space in which you hold the **Admin API Access** right. Only such a user can open
  **Applications**; for everyone else the **Obtain API Key** button is disabled.
- A space to test in. Coevera has no self-serve developer sandbox, so use a space without
  production data: a [14-day free trial](https://www.coevera.com/crm/free-trial/) gives you a
  space of your own. Every Coevera plan includes the API; the monthly call limits are in
  [Plans and limits](/get-started/plans-and-limits/).
- `curl`, Node.js 18 or later, or Python 3.9 or later with `requests`.

## Get API credentials

Every integration authenticates as an **API application**. Each one has its own user name,
password and permissions, so you can give an integration only the access it needs and revoke it
without breaking the others.

1. **Open the applications list.** In the CRM, open **Administration** from the space menu (the
   app switcher calls it **Admin**) and go to **Units, Users & Roles › Applications**. The
   button **Administration › General › Obtain API Key** opens the same place.
2. **Create the application.** Click **Create New** and name it after your integration, for
   example _Northwind ERP sync_. Then click **Edit Permissions**: **Read Only Access** on
   **Accounts**, in the **Records** tab, is enough for this quickstart.
3. **Copy the access values.** Click **Show Access**. The **API Access** dialog shows four
   values, each with a copy button:

   | Value           | Where it goes                                                                                                                                                                    |
   | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
   | **User Name**   | `COEVERA_API_KEY`. It starts with your space ID.                                                                                                                                 |
   | **Password**    | `COEVERA_API_SECRET`. It is shown in full only right after the application is created or its credentials are regenerated, so copy it then.                                       |
   | **Space ID**    | The context bar at the top of this page, and every URL.                                                                                                                          |
   | **Service URL** | The host to call, in your space's region. Take it from here or from the [discovery endpoint](/get-started/regions-and-base-urls/#find-a-spaces-region); do not build it by hand. |

**Generate New**, in the same dialog, replaces both the user name and the password, and the
previous pair stops working at once.

Only API applications of type Basic have **Show Access**. An application that an AI connector
created is of type OAuth and offers **View Permissions** only.

## Set your context

Pick your **region** and enter your **space ID** in the context bar at the top of the page:
every sample on this site then fills in your base URL. Keep the credentials out of your code
and out of every URL; export them as environment variables:

```bash
export COEVERA_API_KEY="<application user name>"
export COEVERA_API_SECRET="<application password>"   # never commit it
```

The REST API for a space answers at:

```text
https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}
```

The region is one of `us-east`, `eu-central`, `ca-central` or `ap-southeast`; see
[Regions and base URLs](/get-started/regions-and-base-urls/).

## Make your first request

List the first three accounts in the space, with only the fields you need: `first=3` asks for
three records and `load-only` names the fields each one carries. Pick REST or GraphQL, and your
language:

**REST · cURL**

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

curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Accounts" \
  -d first=3 \
  -d load-only=id,name,email1,modified \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET"
```

**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}`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");

const params = new URLSearchParams({ first: "3", "load-only": "id,name,email1,modified" });
const res = await fetch(`${base}/entities/Accounts?${params}`, {
  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));
```

**REST · Python**

```python
import json
import os
import sys

import requests

BASE = "https://{region}.api.coevera.com/api/v100/rest/spaces/{space_id}"
AUTH = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])

response = requests.get(
    f"{BASE}/entities/Accounts",
    params={"first": 3, "load-only": "id,name,email1,modified"},
    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))
```

**GraphQL · cURL**

```bash
COEVERA_GRAPHQL="https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public"

curl -sS --fail-with-body "$COEVERA_GRAPHQL" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ entities { account { getByCriteria(first: 3) { totalCount pageInfo { hasNextPage endCursor } edges { node { id name email1 modified } } } } } }"}'
```

**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`;
const auth = Buffer.from(`${env.COEVERA_API_KEY}:${env.COEVERA_API_SECRET}`).toString("base64");

const res = await fetch(endpoint, {
  method: "POST",
  headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    query: `{
      entities {
        account {
          getByCriteria(first: 3) {
            totalCount
            pageInfo { hasNextPage endCursor }
            edges { node { id name email1 modified } }
          }
        }
      }
    }`,
  }),
});
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));
```

**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"
)
AUTH = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])

QUERY = """
{
  entities {
    account {
      getByCriteria(first: 3) {
        totalCount
        pageInfo { hasNextPage endCursor }
        edges { node { id name email1 modified } }
      }
    }
  }
}
"""
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 response looks like this; the ids and names in your own space differ:

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "total": 4,
  "page_info": {
    "start_cursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDEiXQ==",
    "end_cursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDMiXQ==",
    "has_previous_page": false,
    "has_next_page": true
  },
  "data": [
    {
      "id": "0a000000-0000-4000-a000-000000000001",
      "modified": "2026-10-14 09:00:00.000000+00:00",
      "email1": "northwind@example.com",
      "name": "Northwind Analytics"
    },
    {
      "id": "0a000000-0000-4000-a000-000000000002",
      "modified": "2026-10-14 09:00:00.740429+00:00",
      "email1": "contoso@example.com",
      "name": "Contoso Freight"
    },
    {
      "id": "0a000000-0000-4000-a000-000000000003",
      "modified": "2026-10-14 09:00:01.285049+00:00",
      "email1": "wideworld@example.com",
      "name": "Wide World Importers"
    }
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "entities": {
      "account": {
        "getByCriteria": {
          "totalCount": 4,
          "pageInfo": {
            "hasNextPage": true,
            "endCursor": "WyIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDMiXQ=="
          },
          "edges": [
            {
              "node": {
                "id": "0a000000-0000-4000-a000-000000000001",
                "name": "Northwind Analytics",
                "email1": "northwind@example.com",
                "modified": "2026-10-14T09:00:00.000000+00:00"
              }
            },
            {
              "node": {
                "id": "0a000000-0000-4000-a000-000000000002",
                "name": "Contoso Freight",
                "email1": "contoso@example.com",
                "modified": "2026-10-14T09:00:00.740429+00:00"
              }
            },
            {
              "node": {
                "id": "0a000000-0000-4000-a000-000000000003",
                "name": "Wide World Importers",
                "email1": "wideworld@example.com",
                "modified": "2026-10-14T09:00:01.285049+00:00"
              }
            }
          ]
        }
      }
    }
  }
}
```

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

A REST list comes back in an envelope: `success`, `total` (how many accounts match, not how many
came back), `page_info` with the cursors for the next page, and `data` with the records. A
single record comes back as `success` and `data`. In GraphQL the list comes back as
`totalCount`, `pageInfo` and `edges`. Continue with [pagination](/concepts/pagination/) to read
more than one page, and [querying](/concepts/querying/) to filter, sort and trim a list. Every
entity and its operations are in the [REST reference](/reference/rest/) and the
[GraphQL reference](/reference/graphql/).

> **Limit:** A REST list request returns at most 100 records per page, and 30 when you do not set `first`.
> A larger `first` answers `400` with `ERROR_PARAMETER_INVALID`. In GraphQL, set `first` on every
> list query.

## If you get 401

Every authentication failure answers HTTP `401` with `ERROR_UNAUTHORIZED`, and the reason is in
`message`; the other errors are in [errors](/concepts/errors/). This is what a request without
credentials gets; 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 causes, each with its fix:

- **No credentials.** The reason reads `Unauthorized call.` Check that both environment
  variables are set in the shell that runs the program.
- **Wrong space.** The user name starts with the ID of the space it belongs to, and works only
  there. If the space in the URL is a different one, the call is refused like a wrong password:
  the reason reads `Invalid Api user credentials`, and it counts as a failed sign-in. Use the
  space ID from **Show Access**.
- **Credentials regenerated.** Someone pressed **Generate New**, which replaces the user name
  and the password. Copy both new values.

> **A wrong region is not a cause:** A call to another region's host is forwarded to the region that holds your space and answers
> normally, with the same credentials, only more slowly. Use the region the
> [discovery endpoint](/get-started/regions-and-base-urls/#find-a-spaces-region) returns.

## Next steps

- [Create records](/recipes/create-account-with-contacts/): An account with its contacts, in one batch.
- [Query data](/concepts/querying/): Filters, sort, expand and the query builder.
- [React to changes](/webhooks/overview/): Webhooks with signed deliveries.

Before you build on it, read [authentication](/get-started/authentication/) for how the
credentials are sent and rotated, [permissions and scopes](/get-started/permissions-and-scopes/)
for what an application may do, and [REST or GraphQL?](/get-started/rest-or-graphql/) for which
API to use. [Troubleshooting](/support/troubleshooting/) has the common failures in one place.
