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, andjqfor 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:
read -rs TOTEM_API_KEY && export TOTEM_API_KEYOn a server, have your secret store provide TOTEM_API_KEY to the process instead.
1. Check your key
Call GET /v1/me:
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.
{
"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:
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())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 https://api.totemstudio.ai/v1/teams \
-H "Authorization: Bearer $TOTEM_API_KEY"{
"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:
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 "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.
{
"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 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:
- Apply every item of the page to your copy. Skip an item whose
versionis not greater than the one you already hold. - Once the whole page is saved, store
next_cursor. - While
has_moreistrue, ask again, sending the stored cursor ascursor. - When
has_moreisfalse, your copy is current. Keep asking with the stored cursor every 1 to 5 minutes, and store thenext_cursorof 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
- TypeScript SDK: use typed methods and automatic pagination from Node.js.
- Keep a copy in sync: build a sync that survives restarts.
- Authentication: roll and revoke keys, and limit them to your IP addresses.
- Security and personal data: what Totem protects, and what to do with the data you copy.
- Errors: what each error code means and what to do about it.
- Testing and go-live: what to check before your integration goes live.
- Leads reference: every field of a lead, and how to search by phone or email.