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

ConceptsPagination

Pagination

Lists return one page at a time. Ask for up to 100 records with first, then follow page_info.end_cursor with after until has_next_page is false. Cursors must be URL-encoded.

Answer

Send first (default 30, maximum 100) and read page_info in the response. While has_next_page is true, send page_info.end_cursor as after, URL-encoded. In GraphQL the fields are pageInfo.hasNextPage and endCursor; always send first to getByCriteria, at most 100.

A list request returns one page of records, the total, and the cursors that fetch the pages around it. Loop until the response says there is no next page. Every list operation in the REST reference, such as list accounts, takes the parameters below; the pipeline report recipe pages through opportunities with a filter and expand.

How cursors work

A cursor is an opaque string that marks a position in the result. Keep the same filter and sort while you follow it, and never build or parse a cursor yourself.

Parameter Meaning
first Page size, counted from the start or from after. Default 30, maximum 100.
after A cursor: return the records after this position
last, before The same, backwards: last=2 returns the last two records, before the records before a cursor. With first, last takes the last records of that first page.

Read page_info

Every list response carries total, the number of all matching records rather than of this page, and the cursors of the page you received:

200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "success": true,
  "total": 3,
  "page_info": {
    "start_cursor": "WyJDb250b3NvIEZyZWlnaHQiLCAiMGEwMDAwMDAtMDAwMC00MDAwLWEwMDAtMDAwMDAwMDAwMDAxIl0=",
    "end_cursor": "WyJGYWJyaWthbSBSb2JvdGljcyIsICIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDIiXQ==",
    "has_previous_page": false,
    "has_next_page": true
  },
  "data": [
    {
      "id": "0a000000-0000-4000-a000-000000000001",
      "city": "Tacoma",
      "name": "Contoso Freight"
    },
    {
      "id": "0a000000-0000-4000-a000-000000000002",
      "city": "Vancouver",
      "name": "Fabrikam Robotics"
    }
  ]
}
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "data": {
    "entities": {
      "account": {
        "getByCriteria": {
          "totalCount": 3,
          "pageInfo": {
            "hasNextPage": true,
            "endCursor": "WyJGYWJyaWthbSBSb2JvdGljcyIsICIwYTAwMDAwMC0wMDAwLTQwMDAtYTAwMC0wMDAwMDAwMDAwMDIiXQ=="
          },
          "edges": [
            {
              "node": {
                "id": "0a000000-0000-4000-a000-000000000001",
                "name": "Contoso Freight",
                "city": "Tacoma"
              }
            },
            {
              "node": {
                "id": "0a000000-0000-4000-a000-000000000002",
                "name": "Fabrikam Robotics",
                "city": "Vancouver"
              }
            }
          ]
        }
      }
    }
  }
}
Show all 31 linesShow less
Field Meaning
page_info.has_next_page More records follow: ask again with after
page_info.end_cursor The value to send as after
page_info.start_cursor, has_previous_page The same for paging backwards with before

Loop through every page

This loop lists the accounts in Tacoma, Portland and Vancouver, two at a time so that these three accounts of the docs world span two pages. Use up to 100 per page in your own code:

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

# 2, so that the three accounts span two pages; send up to 100.
PAGE_SIZE=2
cursor=""
after=()
while :; do
  # The same filter, sort and page size on every page; only after changes.
  page=$(curl -sS --fail-with-body -G "$COEVERA_BASE/entities/Accounts" \
    -d "filter[city]=Tacoma,Portland,Vancouver" \
    -d order-by=name \
    -d load-only=id,name,city \
    -d "first=$PAGE_SIZE" \
    "${after[@]}" \
    -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
  jq -r '.data[] | "\(.name) (\(.city))"' <<<"$page"
  [ "$(jq -r '.page_info.has_next_page' <<<"$page")" = true ] || break
  next=$(jq -r '.page_info.end_cursor' <<<"$page")
  # An unchanged cursor would ask for the same page again: stop rather than loop forever.
  [ "$next" != "$cursor" ] || break
  cursor=$next
  # --data-urlencode: a raw "+" in the cursor would arrive as a space.
  after=(--data-urlencode "after=$cursor")
done
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");

// 2, so that the three accounts span two pages; send up to 100.
const PAGE_SIZE = 2;
// The same filter, sort and page size on every page. The commas in filter[city] stay raw: a raw
// comma separates OR values, while %2C (what URLSearchParams sends) is a literal comma.
const query = `filter[city]=Tacoma,Portland,Vancouver&order-by=name&load-only=id,name,city&first=${PAGE_SIZE}`;
let cursor: string | null = null;
do {
  // encodeURIComponent: a raw "+" in the cursor would arrive as a space.
  const after: string = cursor ? `&after=${encodeURIComponent(cursor)}` : "";
  const res = await fetch(`${base}/entities/Accounts?${query}${after}`, {
    headers: { Authorization: `Basic ${auth}` },
  });
  const page = await res.json();
  if (!res.ok) throw new Error(`HTTP ${res.status} ${page.name}: ${page.message}`);
  for (const account of page.data) console.log(`${account.name} (${account.city})`);
  const next: string | null = page.page_info.has_next_page ? page.page_info.end_cursor : null;
  // An unchanged cursor would ask for the same page again: stop rather than loop forever.
  cursor = next === cursor ? null : next;
} while (cursor);
import os
import sys
from urllib.parse import quote

import requests

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

# 2, so that the three accounts span two pages; send up to 100.
PAGE_SIZE = 2
# The same filter, sort and page size on every page. The commas in filter[city] stay raw: a raw
# comma separates OR values, while %2C (what params= sends) is a literal comma.
query = f"filter[city]=Tacoma,Portland,Vancouver&order-by=name&load-only=id,name,city&first={PAGE_SIZE}"
cursor = None
while True:
    # quote(..., safe=""): a raw "+" in the cursor would arrive as a space.
    after = f"&after={quote(cursor, safe='')}" if cursor else ""
    response = session.get(f"{BASE}/entities/Accounts?{query}{after}", timeout=30)
    page = response.json()
    if not response.ok:
        sys.exit(f"HTTP {response.status_code} {page.get('name')}: {page.get('message')}")
    for account in page["data"]:
        print(f"{account['name']} ({account['city']})")
    if not page["page_info"]["has_next_page"]:
        break
    # An unchanged cursor would ask for the same page again: stop rather than loop forever.
    if page["page_info"]["end_cursor"] == cursor:
        break
    cursor = page["page_info"]["end_cursor"]
COEVERA_GRAPHQL="https://{region}.api.coevera.com/api/v100/app/space/{space_id}/graphql/public"

# 2, so that the three accounts span two pages; send up to 100.
PAGE_SIZE=2
QUERY=$(cat <<'GRAPHQL'
query Accounts($first: Int!, $after: String) {
  entities {
    account {
      getByCriteria(
        first: $first
        after: $after
        filter: { city: { eq: ["Tacoma", "Portland", "Vancouver"] } }
        orderBy: [{ name: Asc }]
      ) {
        totalCount
        pageInfo { hasNextPage endCursor }
        edges { node { id name city } }
      }
    }
  }
}
GRAPHQL
)
cursor=null
while :; do
  page=$(jq -n --arg query "$QUERY" --argjson first "$PAGE_SIZE" --argjson after "$cursor" \
      '{ query: $query, variables: { first: $first, after: $after } }' |
    curl -sS --fail-with-body "$COEVERA_GRAPHQL" \
      -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
      -H "Content-Type: application/json" \
      -d @-)
  # GraphQL reports errors in the body, often with HTTP 200.
  if jq -e '.errors' <<<"$page" >/dev/null; then
    echo "$page" >&2
    exit 1
  fi
  jq -r '.data.entities.account.getByCriteria.edges[].node | "\(.name) (\(.city))"' <<<"$page"
  [ "$(jq -r '.data.entities.account.getByCriteria.pageInfo.hasNextPage' <<<"$page")" = true ] || break
  cursor=$(jq '.data.entities.account.getByCriteria.pageInfo.endCursor' <<<"$page")
done
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");

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

// 2, so that the three accounts span two pages; send up to 100.
const PAGE_SIZE = 2;
const ACCOUNTS = `query Accounts($first: Int!, $after: String) {
  entities {
    account {
      getByCriteria(
        first: $first
        after: $after
        filter: { city: { eq: ["Tacoma", "Portland", "Vancouver"] } }
        orderBy: [{ name: Asc }]
      ) {
        totalCount
        pageInfo { hasNextPage endCursor }
        edges { node { id name city } }
      }
    }
  }
}`;
let after: string | null = null;
do {
  const data = await gql(ACCOUNTS, { first: PAGE_SIZE, after });
  const page = data.entities.account.getByCriteria;
  for (const edge of page.edges) console.log(`${edge.node.name} (${edge.node.city})`);
  after = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
} while (after);
import json
import os
import sys

import requests

ENDPOINT = (
    "https://{region}.api.coevera.com/api/v100/app/space/"
    "{space_id}/graphql/public"
)
session = requests.Session()
session.auth = (os.environ["COEVERA_API_KEY"], os.environ["COEVERA_API_SECRET"])


def gql(query, variables=None):
    """One GraphQL request. GraphQL reports errors in the body, often with HTTP 200: check both."""
    response = session.post(ENDPOINT, json={"query": query, "variables": variables or {}}, timeout=30)
    body = response.json()
    if not response.ok or body.get("errors"):
        sys.exit(f"HTTP {response.status_code}: {json.dumps(body.get('errors', body))}")
    return body["data"]


# 2, so that the three accounts span two pages; send up to 100.
PAGE_SIZE = 2
ACCOUNTS = """
query Accounts($first: Int!, $after: String) {
  entities {
    account {
      getByCriteria(
        first: $first
        after: $after
        filter: { city: { eq: ["Tacoma", "Portland", "Vancouver"] } }
        orderBy: [{ name: Asc }]
      ) {
        totalCount
        pageInfo { hasNextPage endCursor }
        edges { node { id name city } }
      }
    }
  }
}
"""
after = None
while True:
    page = gql(ACCOUNTS, {"first": PAGE_SIZE, "after": after})["entities"]["account"]["getByCriteria"]
    for edge in page["edges"]:
        print(f"{edge['node']['name']} ({edge['node']['city']})")
    if not page["pageInfo"]["hasNextPage"]:
        break
    after = page["pageInfo"]["endCursor"]

URL-encode the cursor

Cursors are base64 and can contain +, / and =. Query values are URL-decoded, and a raw + becomes a space, which breaks the cursor. Encode it as the sample does: --data-urlencode, encodeURIComponent or urllib.parse.quote.

GraphQL connections

Use a repository's getByCriteria: it returns a connection with totalCount, pageInfo and edges, and takes first and after. getAll takes limit and offset and has no cursor; prefer getByCriteria for anything you loop over.

Always send first, at most 100.

Legacy limit and offset

limit and offset still work on REST lists, up to 100 per page, but they are deprecated: the response has total and data and no page_info. Use cursors.

Limits

Limit Value What happens above it
Page size (first, last) 100 HTTP 400, code 117 ERROR_PARAMETER_INVALID: "Limit must not be greater than 100"
Default page size 30 Used when you send no first
Legacy limit / offset 100 Accepted, but the response has no page_info; use cursors
GraphQL getByCriteria 100 Send first on every call