Skip to content
Beta

Get started Quickstart

Quickstart

Make your first requests to the Totem API in a few minutes. Check your key, find your team, list leads and start a sync.

This guide takes you from a new key to your first leads in four steps.

Before you begin

You need:

  • A Totem team on Pro or a higher plan, or with API access agreed with Totem, whose legal setup is saved in Privacy & agreements.
  • A secret key. A team administrator opens Settings → Developers, chooses Create API key and confirms read-only access to the team. Totem shows the key once: copy it straight into your server's secret store.
  • curl, and jq for step 2.

If Create API key is missing or disabled, Create a key lists every requirement and what Settings says about each.

To follow this guide, load the key into an environment variable without typing it on the command line, so it never stays in your shell history. Run this, paste the key and press Enter; nothing is shown:

Bash
read -rs TOTEM_API_KEY && export TOTEM_API_KEY

On a server, have your secret store provide TOTEM_API_KEY to the process instead.

1. Check your key

Call GET /v1/me:

cURL
curl https://api.totemstudio.ai/v1/me \
  -H "Authorization: Bearer $TOTEM_API_KEY"

The answer describes your key: the integration it belongs to, the last four characters of the key, the teams it reads and its permissions.

JSON
{
  "object": "integration",
  "id": "prn_m1NFIcoIU8smXfqR4ymgt6j07TfRF",
  "livemode": false,
  "name": "Example CRM sync",
  "key": {
    "last4": "Q7xZ"
  },
  "teams": [
    "team_r9uYrIW4gSFJJ6hZQ9ip1UIpPaAD1CF7NR"
  ],
  "scopes": [
    "teams:read",
    "crm:read",
    "calls:read",
    "messages:read",
    "calendar:read"
  ]
}

Your answer carries your own values and "livemode": true. The examples in these docs are fictitious, so they say "livemode": false.

The same request from Node.js 22 or later, as an ES module, and from Python 3 with the requests library (pip install requests), which the samples of the reference use too:

me.mjsNode.js
const response = await fetch('https://api.totemstudio.ai/v1/me', {
  headers: { Authorization: `Bearer ${process.env.TOTEM_API_KEY}` },
  signal: AbortSignal.timeout(30_000),
})
// Read the body as text: a refusal from the firewall in front of the API is not JSON.
console.log(response.status, await response.text())
me.pyPython
import os

import requests

response = requests.get(
    "https://api.totemstudio.ai/v1/me",
    headers={"Authorization": f"Bearer {os.environ['TOTEM_API_KEY']}"},
    timeout=30,
)
# Print the body as text: a refusal from the firewall in front of the API is not JSON.
print(response.status_code, response.text)

A 401 means the key is missing, mistyped or no longer valid. Authentication lists every case.

2. Find your team

GET /v1/teams lists the teams your key reads:

cURL
curl https://api.totemstudio.ai/v1/teams \
  -H "Authorization: Bearer $TOTEM_API_KEY"
JSON
{
  "object": "list",
  "data": [
    {
      "id": "team_r9uYrIW4gSFJJ6hZQ9ip1UIpPaAD1CF7NR",
      "object": "team",
      "livemode": false,
      "name": "Example Dental",
      "timezone": "America/New_York",
      "default_country": "US"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Keep the first team's id for the next steps. If the request fails, the command says so instead of keeping a wrong value:

Bash
TOTEM_TEAM_ID=$(curl -sS https://api.totemstudio.ai/v1/teams \
  -H "Authorization: Bearer $TOTEM_API_KEY" | jq -er '.data[0].id') \
  && export TOTEM_TEAM_ID \
  || { unset TOTEM_TEAM_ID; echo 'Step 2 failed: run the curl above to see the error.' >&2; }

Requests that read one team's data, such as lists and the changes feed, name the team in the Totem-Team header. It only matters when a key reads several teams: a key with one team can leave it out, and a key with several gets 400 team_required without it. The next steps always send it, so they work for both.

3. List leads

Ask GET /v1/leads for the first five leads:

cURL
curl "https://api.totemstudio.ai/v1/leads?limit=5" \
  -H "Authorization: Bearer $TOTEM_API_KEY" \
  -H "Totem-Team: $TOTEM_TEAM_ID"

The answer is a list: the leads are in data. A lead has many more fields than this example shows; the reference lists them all.

JSON
{
  "object": "list",
  "data": [
    {
      "id": "lead_r72EfbRNUzuqORFJR0O8HxAwEXd93jGUTYMaMMXKSF0QbLS8CiUKJpEP34LFQG",
      "object": "lead",
      "livemode": false,
      "team": "team_r9uYrIW4gSFJJ6hZQ9ip1UIpPaAD1CF7NR",
      "record_kind": "lead",
      "name": "Alex",
      "surname": "Rivera",
      "email": "alex.rivera@example.com",
      "phone": "+12025550123",
      "status": "contacted",
      "contact_permissions": {
        "do_not_contact": false,
        "email_opt_out": false,
        "email_opt_out_at": null,
        "whatsapp_consent": null,
        "provenance": {
          "do_not_contact": null,
          "email_opt_out": null,
          "whatsapp_consent": null
        }
      },
      "board": "brd_r72EfbRNUzuqORFJR0O8HxAwEXd93jGUTYMaMMX3trzsVUZQuHcMSuDh1aRIst",
      "stage": "stg_r72EfbRNUzuqORFJR0O8HxAwEXd93jGUTYMaMMXWBYWcKwVTY3CRZlVS3oYvs0",
      "created_at": "2026-09-28T10:15:00.000Z",
      "version": 3,
      "updated_at": "2026-09-28T10:20:01.250Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJ2IjoxLCJrIjoibGVhZCIsImEiOiJsZWFkX3I3MkVmYlJOVXp1cU9SRkpSME84SHhBd0VYZDkzakdVVFlNYU1NWEtTRjBRYkxTOENpVUtKcEVQMzRMRlFHIiwicyI6ImYzNTI3NzI2MTkwYiJ93iHyRg"
}

has_more: true means there are more leads. Send next_cursor back as the cursor parameter, with the same filters and the same Totem-Team, to read the next page. Lists and pagination explains paging and filters.

4. Start a sync

A list is a snapshot. To keep a copy of a team that stays current, read the changes feed, GET /v1/sync/changes. The first request has no cursor and starts the initial sync of the whole team:

cURL
curl https://api.totemstudio.ai/v1/sync/changes \
  -H "Authorization: Bearer $TOTEM_API_KEY" \
  -H "Totem-Team: $TOTEM_TEAM_ID"

Each item in data is a record of one kind, such as a lead, a call or a booking, or a removal that tells you to delete a record. From there, your sync runs a simple loop:

  1. Apply every item of the page to your copy. Skip an item whose version is not greater than the one you already hold.
  2. Once the whole page is saved, store next_cursor.
  3. While has_more is true, ask again, sending the stored cursor as cursor.
  4. When has_more is false, your copy is current. Keep asking with the stored cursor every 1 to 5 minutes, and store the next_cursor of every page, even an empty one.

Keep a copy in sync covers the rest: removals, merges and transfers, crash recovery and a full resync. It also includes a tested reference client, in TypeScript, that you can copy into your server.

Next steps

API version 1.0a