[{"data":1,"prerenderedAt":951},["ShallowReactive",2],{"developers:\u002Fdevelopers\u002Fauthentication":3},{"doc":4,"lang":778,"nav":789,"alternatePath":891,"policy":892,"specVersion":936,"reference":7,"layout":937,"toc":938,"prev":948,"next":949,"operations":950,"overview":7,"heroExample":7},{"id":5,"title":6,"apiTag":7,"body":8,"description":775,"draft":776,"extension":777,"lang":778,"meta":779,"navTitle":7,"navigation":780,"order":781,"path":782,"rawbody":783,"section":784,"seo":785,"stem":786,"translationKey":787,"version":7,"__hash__":788},"developers_en\u002Fdevelopers\u002Fauthentication.md","Authentication",null,{"type":9,"value":10,"toc":761},"minimark",[11,16,25,35,53,64,69,91,101,128,143,147,150,161,181,185,220,223,226,236,239,253,265,304,312,323,326,373,377,398,423,429,432,446,458,470,490,500,504,527,533,547,551,557],[12,13,15],"h2",{"id":14},"secret-keys","Secret keys",[17,18,19,20,24],"p",{},"Every request carries a secret key in the ",[21,22,23],"code",{},"Authorization"," header:",[26,27,33],"pre",{"className":28,"code":30,"language":31,"meta":32},[29],"language-http","GET \u002Fv1\u002Fme HTTP\u002F1.1\nHost: api.totemstudio.ai\nAuthorization: Bearer tsk_live_…\n","http","",[21,34,30],{"__ignoreMap":32},[17,36,37,38,41,42,52],{},"A secret key starts with ",[21,39,40],{},"tsk_live_",", followed by a lookup id and the secret itself. Totem shows it once, when it is created, and keeps only a one-way digest, so a lost key cannot be recovered: roll it instead. ",[43,44,48],"a",{"href":45,"rel":46},"https:\u002F\u002Fapp.totemstudio.ai\u002Fsettings?tab=developers",[47],"nofollow",[49,50,51],"strong",{},"Settings → Developers"," shows the last four characters of each key so you can tell them apart. A key does not expire on its own: it works until you roll it or revoke it.",[17,54,55,56,59,60,63],{},"Keys that start with ",[21,57,58],{},"tsk_test_"," are reserved for future test environments. Today the API refuses them with ",[21,61,62],{},"401 invalid_credentials",".",[65,66,68],"h3",{"id":67},"create-a-key","Create a key",[17,70,71,72,77,78,81,82,85,86,90],{},"A team administrator opens ",[43,73,75],{"href":45,"rel":74},[47],[49,76,51],{},", chooses ",[49,79,80],{},"Create API key",", gives the integration a name and confirms read-only access to the current team. ",[49,83,84],{},"Advanced options"," hold the optional description, IP restrictions and, for a family administrator, more teams of the organization (",[43,87,89],{"href":88},"#teams-and-the-totem-team-header","Teams and the Totem-Team header"," explains who may manage what). If the administrator has not signed in recently, Totem asks them to sign in again first. Totem then shows the integration's secret key once. Copy it straight into your server's secret store.",[17,92,93,94,97,98,100],{},"The API is in ",[49,95,96],{},"Beta"," and available without an invitation. ",[49,99,80],{}," works when:",[102,103,104,108,111,114,125],"ul",{},[105,106,107],"li",{},"the person creating the key has the administrator role on the team and a verified email address;",[105,109,110],{},"the organization is on Pro or a higher plan, or has API access agreed with Totem;",[105,112,113],{},"the team holds data (a billing-only account does not) and is neither disabled nor archived;",[105,115,116,117,124],{},"the team has completed its legal setup: a team administrator saves its privacy policy, processing purposes and business sector in ",[43,118,121],{"href":119,"rel":120},"https:\u002F\u002Fapp.totemstudio.ai\u002Fprivacy-setup",[47],[49,122,123],{},"Privacy & agreements",";",[105,126,127],{},"the team is not, for now, in a sensitive business sector, such as health care.",[17,129,130,131,133,134,138,139,63],{},"If ",[49,132,80],{}," does not appear, check the role, the email address and that the team is not a billing-only account; when the plan is the reason, Settings says so. The button is disabled once the organization holds ",[135,136],"api-const",{"name":137},"MAX_INTEGRATIONS_PER_FAMILY"," active integrations. If Totem refuses a team, Settings names it and says why. Otherwise, ",[43,140,142],{"href":141},"\u002Fdevelopers\u002Fsupport","contact support",[65,144,146],{"id":145},"where-a-key-may-live","Where a key may live",[17,148,149],{},"A key reads administrator-level data, so keep it on a server you control:",[102,151,152,155,158],{},[105,153,154],{},"in a secret manager, or an environment variable that only the process calling the API can read;",[105,156,157],{},"never in a browser, a mobile app, client-side JavaScript or a public repository;",[105,159,160],{},"never in a URL, a log line, a command you type in a shell or a support request.",[17,162,163,164,167,168,171,172,175,176,180],{},"The API sends no CORS headers, and it answers ",[21,165,166],{},"400 secret_key_in_browser"," to a request that carries both a secret key and an ",[21,169,170],{},"Origin"," header. If a key has ever been in a web page, a mobile app or a browser extension, treat it as leaked: roll it and tick ",[49,173,174],{},"This key may have been compromised",", as ",[43,177,179],{"href":178},"#roll-and-revoke","Roll and revoke"," explains.",[65,182,184],{"id":183},"what-a-key-can-read","What a key can read",[17,186,187,188,194,195,198,199,202,203,202,206,202,209,212,213,216,217,219],{},"Every v1.0a key is read-only and carries the same fixed set of read permissions: there is nothing to choose. ",[43,189,191],{"href":190},"\u002Fdevelopers\u002Freference-account#me.get",[21,192,193],{},"GET \u002Fv1\u002Fme"," lists them in ",[21,196,197],{},"scopes",": ",[21,200,201],{},"teams:read",", ",[21,204,205],{},"crm:read",[21,207,208],{},"calls:read",[21,210,211],{},"messages:read"," and ",[21,214,215],{},"calendar:read",". ",[21,218,211],{}," covers conversation metadata only; no endpoint returns the text of a message.",[17,221,222],{},"A key reads everything an administrator of its teams can read, including channels and records marked Private. The administrator who creates the integration confirms this before the key is issued. Give an integration only the teams it needs.",[12,224,89],{"id":225},"teams-and-the-totem-team-header",[17,227,228,229,231,232,235],{},"An integration covers one team, or a list of teams of the same organization (a billing account and its sub-teams; in ",[49,230,51],{},", the access scope ",[49,233,234],{},"Selected family teams","). The list is explicit: a new team is never added on its own. An administrator adds it, and the administrators who manage the integration, and those of every team it covers, are told.",[17,237,238],{},"Who may manage an integration depends on what it covers:",[102,240,241,244,247],{},[105,242,243],{},"A team administrator creates and manages an integration for their own team.",[105,245,246],{},"Only a family administrator can create or change an integration that covers several teams: an administrator of the billing account who is also listed on it as a family administrator. The administrators of each team it covers see it in Settings, read-only.",[105,248,249,250,252],{},"An organization, or a team outside any organization, can hold up to ",[135,251],{"name":137}," active integrations, counting all of its teams. Revoke one you no longer use to free a place.",[17,254,255,261,262,63],{},[43,256,258],{"href":257},"\u002Fdevelopers\u002Freference-account#teams.list",[21,259,260],{},"GET \u002Fv1\u002Fteams"," lists the teams a key can read, each with an id that starts with ",[21,263,264],{},"team_",[102,266,267,274,281],{},[105,268,269,212,271,273],{},[21,270,193],{},[21,272,260],{}," describe the key and never take the header.",[105,275,276,277,280],{},"Requests about one record, such as ",[21,278,279],{},"GET \u002Fv1\u002Fleads\u002F{id}",", need no header: the record's id already names its team.",[105,282,283,284,202,287,290,291,202,294,212,297,300,301,24],{},"Requests that read one team's collection (lists such as ",[21,285,286],{},"GET \u002Fv1\u002Fleads",[21,288,289],{},"GET \u002Fv1\u002Fboards"," or ",[21,292,293],{},"GET \u002Fv1\u002Fmembers",[21,295,296],{},"POST \u002Fv1\u002Fleads\u002Fsearch",[21,298,299],{},"GET \u002Fv1\u002Fsync\u002Fchanges",") use the key's only team. When the key covers several teams, name one in the ",[21,302,303],{},"Totem-Team",[26,305,310],{"className":306,"code":308,"language":309,"meta":32},[307],"language-bash","curl \"https:\u002F\u002Fapi.totemstudio.ai\u002Fv1\u002Fleads?limit=50\" \\\n  -H \"Authorization: Bearer $TOTEM_API_KEY\" \\\n  -H \"Totem-Team: team_r9uYrIW4gSFJJ6hZQ9ip1UIpPaAD1CF7NR\"\n","bash",[21,311,308],{"__ignoreMap":32},[17,313,314,315,318,319,322],{},"The samples in the API reference send ",[21,316,317],{},"Totem-Team: $TOTEM_TEAM_ID"," on these requests. With a one-team key, leave ",[21,320,321],{},"TOTEM_TEAM_ID"," unset and they leave the header out: cURL drops a header whose value is empty, and the Node.js and Python samples check the variable.",[17,324,325],{},"A key with one team may send the header too, as long as it names that team. What each answer means for your copy of the team:",[102,327,328,338,348,359,366],{},[105,329,330,331,334,335,63],{},"Without the header, a key with several teams gets ",[21,332,333],{},"400 team_required",". A header that is not a team id, or that appears twice, gets ",[21,336,337],{},"400 invalid_request",[105,339,340,341,344,345,347],{},"A team the key does not cover, or that no longer exists, gets ",[21,342,343],{},"404 team_not_accessible",". Read ",[21,346,260],{},": if the team is no longer listed, delete its data and its cursor.",[105,349,350,351,212,353,355,356,358],{},"A one-team key covers its team only while the team stays where it was when the key was created: in the same organization, or outside any organization. If the team joins an organization, leaves it or moves to another one, the key gets ",[21,352,343],{},[21,354,260],{}," no longer lists the team. Revoke the key in ",[49,357,51],{}," and create a new one.",[105,360,361,362,365],{},"A team that is disabled or archived, or whose API access Totem has paused, gets ",[21,363,364],{},"403 team_state_restricted",". Keep the team's data and its cursor, skip it for this run and try again later. A disabled team or paused access can come back; an archived team stays restricted unless Totem restores it. While a team is restricted, the changes feed delivers nothing for it, removals included, so if the restriction lasts longer than your retention policy allows, delete your copy.",[105,367,368,369,372],{},"For now, Totem does not export teams in a sensitive business sector, such as health care. Such a team cannot be added to an integration, and a key that already covers one gets ",[21,370,371],{},"403 sector_restricted"," for it. Skip the team: waiting does not change it. The changes feed delivers nothing for it, removals included, so keep what you already have only as long as your retention policy allows, then delete your copy.",[12,374,376],{"id":375},"ip-allowlist","IP allowlist",[17,378,379,380,383,384,387,388,390,391,393,394,397],{},"An integration can be limited to up to ",[135,381],{"name":382},"MAX_IP_ALLOWLIST"," IP addresses or ranges in CIDR notation. A request from any other address gets ",[21,385,386],{},"403 ip_not_allowed",". Set the list in ",[49,389,51],{},", under ",[49,392,84],{}," when you create the key or with ",[49,395,396],{},"Edit integration"," later; changing it asks for a recent sign-in.",[102,399,400,403,414,417],{},[105,401,402],{},"The API is reachable over IPv4 only, so list the public IPv4 addresses your requests leave from. An IPv6 entry never matches today.",[105,404,405,406,409,410,413],{},"Write one address or range per line. A range must start at its network address, such as ",[21,407,408],{},"203.0.113.0\u002F24",", and ",[21,411,412],{},"\u002F0"," is refused. An empty list allows any address.",[105,415,416],{},"If your platform has no fixed outgoing address, as with many serverless hosts, send its traffic through a static IP first.",[105,418,419,420,422],{},"After you save the list, call ",[21,421,193],{}," from the deployed server to check it.",[17,424,425,426,428],{},"Totem checks the address only after the key, so a wrong key gets ",[21,427,62],{}," wherever it comes from.",[12,430,179],{"id":431},"roll-and-revoke",[17,433,434,437,438,441,442,445],{},[49,435,436],{},"Roll"," a key with ",[49,439,440],{},"Roll key"," to replace it. Totem issues a new key and keeps the old one working for the time you choose in the dialog: 1 hour, 24 hours (the default) or 7 days. Deploy the new key within that time and nothing stops. When that time is over, the old key answers ",[21,443,444],{},"401 key_expired",". Rolling again revokes any old key still in its grace period, and the dialog asks you to confirm it.",[17,447,448,449,451,452,455,456,63],{},"To stop the old key at once, tick ",[49,450,174],{}," when you roll, or choose ",[49,453,454],{},"End old key now"," later. The old key then answers ",[21,457,62],{},[17,459,460,463,464,466,467,180],{},[49,461,462],{},"If a key may have leaked",", roll it and tick ",[49,465,174],{},": the old key is revoked at once, with no grace period. Then tell us, as ",[43,468,469],{"href":141},"Support",[17,471,472,475,476,479,480,484,485,489],{},[49,473,474],{},"Revoke"," an integration with ",[49,477,478],{},"Revoke access"," to stop all of its keys. A revoked key stops working within ",[135,481],{"name":482,"divide":483},"KEY_CACHE_TTL_MS","1000"," seconds, and revoking cannot be undone. The data you already copied stays in your systems and receives no more removals from Totem: delete it, or keep it only as long as the customer's own rules allow. ",[43,486,488],{"href":487},"\u002Fdevelopers\u002Fsecurity#personal-data-you-receive","Security and personal data"," explains what to do with it.",[17,491,492,493,496,497,499],{},"Creating or rolling a key, adding a team and changing the IP allowlist ask the administrator to sign in again if they have not done so recently. Every change appears in the integration's ",[49,494,495],{},"Recent activity",", and the administrators who manage the integration, and those of every team it covers, are told about each one, in Totem and by email. Revoking and ",[49,498,454],{}," never ask for a sign-in: anyone allowed to manage the integration can stop it at once.",[12,501,503],{"id":502},"failed-attempts","Failed attempts",[17,505,506,507,510,511,514,515,518,519,522,523,526],{},"Totem counts failed attempts: requests with a key that is missing, malformed, unknown or wrong. After as few as ",[135,508],{"name":509},"AUTH_FAILURE_LIMITS.perIpPerMinute"," failures from one IP address in a minute, or ",[135,512],{"name":513},"AUTH_FAILURE_LIMITS.perLookupIdPerMinute"," with one key, Totem can lock that address or that key for ",[135,516],{"name":517,"divide":483},"AUTH_FAILURE_LIMITS.banMs"," seconds. While it is locked, requests get ",[21,520,521],{},"429 rate_limited"," with ",[21,524,525],{},"Retry-After"," set to the full lock length; the lock may end sooner.",[17,528,529,530,532],{},"A revoked or expired key, an integration that is no longer active and a request from outside the allowlist are not counted. While a key is locked, requests with its real secret keep working: only guesses are refused. While an address is locked, every request from it is refused. If you are locked out, fix the key first, then wait for ",[21,531,525],{}," before you try again.",[17,534,535,536,539,540,542,543,180],{},"Many requests without a well-formed key from one address can also make the firewall in front of the API answer ",[21,537,538],{},"429"," itself, with no ",[21,541,525],{}," and no JSON body, for up to 10 minutes. Stop the loop, fix the header, then retry with exponential backoff, as ",[43,544,546],{"href":545},"\u002Fdevelopers\u002Ferrors","Errors",[12,548,550],{"id":549},"authentication-errors","Authentication errors",[17,552,553,554,556],{},"Every code links to its full entry on the ",[43,555,546],{"href":545}," page.",[558,559,560,577],"table",{},[561,562,563],"thead",{},[564,565,566,570,574],"tr",{},[567,568,569],"th",{},"Status",[567,571,572],{},[21,573,21],{},[567,575,576],{},"What to do",[578,579,580,597,614,630,645,661,676,691,706,721,740],"tbody",{},[564,581,582,586,594],{},[583,584,585],"td",{},"400",[583,587,588],{},[43,589,591],{"href":590},"\u002Fdevelopers\u002Ferrors#secret_key_in_browser",[21,592,593],{},"secret_key_in_browser",[583,595,596],{},"The request came from a browser. Call the API from your server, and if the key was ever in a web page or an app, roll it as compromised.",[564,598,599,601,609],{},[583,600,585],{},[583,602,603],{},[43,604,606],{"href":605},"\u002Fdevelopers\u002Ferrors#team_required",[21,607,608],{},"team_required",[583,610,611,612,63],{},"The key covers several teams. Send ",[21,613,303],{},[564,615,616,619,627],{},[583,617,618],{},"401",[583,620,621],{},[43,622,624],{"href":623},"\u002Fdevelopers\u002Ferrors#invalid_credentials",[21,625,626],{},"invalid_credentials",[583,628,629],{},"The key is missing, malformed, unknown, wrong or revoked. Check the header and the key.",[564,631,632,634,642],{},[583,633,618],{},[583,635,636],{},[43,637,639],{"href":638},"\u002Fdevelopers\u002Ferrors#key_expired",[21,640,641],{},"key_expired",[583,643,644],{},"The key was rolled and its grace period is over. Use the new key.",[564,646,647,650,658],{},[583,648,649],{},"403",[583,651,652],{},[43,653,655],{"href":654},"\u002Fdevelopers\u002Ferrors#ip_not_allowed",[21,656,657],{},"ip_not_allowed",[583,659,660],{},"The request came from an address outside the integration's allowlist. The API is reachable over IPv4 only.",[564,662,663,665,673],{},[583,664,649],{},[583,666,667],{},[43,668,670],{"href":669},"\u002Fdevelopers\u002Ferrors#plan_not_entitled",[21,671,672],{},"plan_not_entitled",[583,674,675],{},"The organization's plan does not include the API. Keep your data and talk to the customer; the feed delivers no removals until access returns, so if that lasts longer than your retention policy allows, delete your copy.",[564,677,678,680,688],{},[583,679,649],{},[583,681,682],{},[43,683,685],{"href":684},"\u002Fdevelopers\u002Ferrors#insufficient_scope",[21,686,687],{},"insufficient_scope",[583,689,690],{},"The key lacks a permission this endpoint needs. Every v1.0a key has the same permissions, so tell us, with the request id.",[564,692,693,695,703],{},[583,694,649],{},[583,696,697],{},[43,698,700],{"href":699},"\u002Fdevelopers\u002Ferrors#team_state_restricted",[21,701,702],{},"team_state_restricted",[583,704,705],{},"The team is disabled, archived or paused. Keep its data and its cursor, skip it for this run and try again later; if the restriction lasts longer than your retention policy allows, delete your copy.",[564,707,708,710,718],{},[583,709,649],{},[583,711,712],{},[43,713,715],{"href":714},"\u002Fdevelopers\u002Ferrors#sector_restricted",[21,716,717],{},"sector_restricted",[583,719,720],{},"For now, Totem does not export teams in a sensitive sector, such as health care. Skip the team: waiting does not change it. The feed delivers no removals for it, so keep what you have only as long as your retention policy allows, then delete your copy.",[564,722,723,726,734],{},[583,724,725],{},"404",[583,727,728],{},[43,729,731],{"href":730},"\u002Fdevelopers\u002Ferrors#team_not_accessible",[21,732,733],{},"team_not_accessible",[583,735,736,737,739],{},"The key does not cover this team. If ",[21,738,260],{}," no longer lists it, delete its data and its cursor.",[564,741,742,744,752],{},[583,743,538],{},[583,745,746],{},[43,747,749],{"href":748},"\u002Fdevelopers\u002Ferrors#rate_limited",[21,750,751],{},"rate_limited",[583,753,754,755,757,758,760],{},"Too many failed attempts, or too many requests. Wait for the seconds in ",[21,756,525],{},". A ",[21,759,538],{}," without a JSON body comes from the firewall: back off exponentially.",{"title":32,"searchDepth":762,"depth":762,"links":763},3,[764,770,771,772,773,774],{"id":14,"depth":765,"text":15,"children":766},2,[767,768,769],{"id":67,"depth":762,"text":68},{"id":145,"depth":762,"text":146},{"id":183,"depth":762,"text":184},{"id":225,"depth":765,"text":89},{"id":375,"depth":765,"text":376},{"id":431,"depth":765,"text":179},{"id":502,"depth":765,"text":503},{"id":549,"depth":765,"text":550},"Secret keys and where they may live, the Totem-Team header for keys with several teams, IP allowlists, and how to roll or revoke a key.",false,"md","en",{},true,"3","\u002Fdevelopers\u002Fauthentication","---\ntitle: Authentication\ndescription: Secret keys and where they may live, the Totem-Team header for keys with several teams, IP allowlists, and how to roll or revoke a key.\nsection: start\norder: 3\ntranslationKey: authentication\nlang: en\ndraft: false\n---\n\n## Secret keys\n\nEvery request carries a secret key in the `Authorization` header:\n\n```http\nGET \u002Fv1\u002Fme HTTP\u002F1.1\nHost: api.totemstudio.ai\nAuthorization: Bearer tsk_live_…\n```\n\nA secret key starts with `tsk_live_`, followed by a lookup id and the secret itself. Totem shows it once, when it is created, and keeps only a one-way digest, so a lost key cannot be recovered: roll it instead. [**Settings → Developers**](https:\u002F\u002Fapp.totemstudio.ai\u002Fsettings?tab=developers) shows the last four characters of each key so you can tell them apart. A key does not expire on its own: it works until you roll it or revoke it.\n\nKeys that start with `tsk_test_` are reserved for future test environments. Today the API refuses them with `401 invalid_credentials`.\n\n### Create a key\n\nA team administrator opens [**Settings → Developers**](https:\u002F\u002Fapp.totemstudio.ai\u002Fsettings?tab=developers), chooses **Create API key**, gives the integration a name and confirms read-only access to the current team. **Advanced options** hold the optional description, IP restrictions and, for a family administrator, more teams of the organization ([Teams and the Totem-Team header](#teams-and-the-totem-team-header) explains who may manage what). If the administrator has not signed in recently, Totem asks them to sign in again first. Totem then shows the integration's secret key once. Copy it straight into your server's secret store.\n\nThe API is in **Beta** and available without an invitation. **Create API key** works when:\n\n- the person creating the key has the administrator role on the team and a verified email address;\n- the organization is on Pro or a higher plan, or has API access agreed with Totem;\n- the team holds data (a billing-only account does not) and is neither disabled nor archived;\n- the team has completed its legal setup: a team administrator saves its privacy policy, processing purposes and business sector in [**Privacy & agreements**](https:\u002F\u002Fapp.totemstudio.ai\u002Fprivacy-setup);\n- the team is not, for now, in a sensitive business sector, such as health care.\n\nIf **Create API key** does not appear, check the role, the email address and that the team is not a billing-only account; when the plan is the reason, Settings says so. The button is disabled once the organization holds :api-const{name=\"MAX_INTEGRATIONS_PER_FAMILY\"} active integrations. If Totem refuses a team, Settings names it and says why. Otherwise, [contact support](\u002Fdevelopers\u002Fsupport).\n\n### Where a key may live\n\nA key reads administrator-level data, so keep it on a server you control:\n\n- in a secret manager, or an environment variable that only the process calling the API can read;\n- never in a browser, a mobile app, client-side JavaScript or a public repository;\n- never in a URL, a log line, a command you type in a shell or a support request.\n\nThe API sends no CORS headers, and it answers `400 secret_key_in_browser` to a request that carries both a secret key and an `Origin` header. If a key has ever been in a web page, a mobile app or a browser extension, treat it as leaked: roll it and tick **This key may have been compromised**, as [Roll and revoke](#roll-and-revoke) explains.\n\n### What a key can read\n\nEvery v1.0a key is read-only and carries the same fixed set of read permissions: there is nothing to choose. [`GET \u002Fv1\u002Fme`](\u002Fdevelopers\u002Freference-account#me.get) lists them in `scopes`: `teams:read`, `crm:read`, `calls:read`, `messages:read` and `calendar:read`. `messages:read` covers conversation metadata only; no endpoint returns the text of a message.\n\nA key reads everything an administrator of its teams can read, including channels and records marked Private. The administrator who creates the integration confirms this before the key is issued. Give an integration only the teams it needs.\n\n## Teams and the Totem-Team header\n\nAn integration covers one team, or a list of teams of the same organization (a billing account and its sub-teams; in **Settings → Developers**, the access scope **Selected family teams**). The list is explicit: a new team is never added on its own. An administrator adds it, and the administrators who manage the integration, and those of every team it covers, are told.\n\nWho may manage an integration depends on what it covers:\n\n- A team administrator creates and manages an integration for their own team.\n- Only a family administrator can create or change an integration that covers several teams: an administrator of the billing account who is also listed on it as a family administrator. The administrators of each team it covers see it in Settings, read-only.\n- An organization, or a team outside any organization, can hold up to :api-const{name=\"MAX_INTEGRATIONS_PER_FAMILY\"} active integrations, counting all of its teams. Revoke one you no longer use to free a place.\n\n[`GET \u002Fv1\u002Fteams`](\u002Fdevelopers\u002Freference-account#teams.list) lists the teams a key can read, each with an id that starts with `team_`.\n\n- `GET \u002Fv1\u002Fme` and `GET \u002Fv1\u002Fteams` describe the key and never take the header.\n- Requests about one record, such as `GET \u002Fv1\u002Fleads\u002F{id}`, need no header: the record's id already names its team.\n- Requests that read one team's collection (lists such as `GET \u002Fv1\u002Fleads`, `GET \u002Fv1\u002Fboards` or `GET \u002Fv1\u002Fmembers`, `POST \u002Fv1\u002Fleads\u002Fsearch` and `GET \u002Fv1\u002Fsync\u002Fchanges`) use the key's only team. When the key covers several teams, name one in the `Totem-Team` header:\n\n```bash\ncurl \"https:\u002F\u002Fapi.totemstudio.ai\u002Fv1\u002Fleads?limit=50\" \\\n  -H \"Authorization: Bearer $TOTEM_API_KEY\" \\\n  -H \"Totem-Team: team_r9uYrIW4gSFJJ6hZQ9ip1UIpPaAD1CF7NR\"\n```\n\nThe samples in the API reference send `Totem-Team: $TOTEM_TEAM_ID` on these requests. With a one-team key, leave `TOTEM_TEAM_ID` unset and they leave the header out: cURL drops a header whose value is empty, and the Node.js and Python samples check the variable.\n\nA key with one team may send the header too, as long as it names that team. What each answer means for your copy of the team:\n\n- Without the header, a key with several teams gets `400 team_required`. A header that is not a team id, or that appears twice, gets `400 invalid_request`.\n- A team the key does not cover, or that no longer exists, gets `404 team_not_accessible`. Read `GET \u002Fv1\u002Fteams`: if the team is no longer listed, delete its data and its cursor.\n- A one-team key covers its team only while the team stays where it was when the key was created: in the same organization, or outside any organization. If the team joins an organization, leaves it or moves to another one, the key gets `404 team_not_accessible` and `GET \u002Fv1\u002Fteams` no longer lists the team. Revoke the key in **Settings → Developers** and create a new one.\n- A team that is disabled or archived, or whose API access Totem has paused, gets `403 team_state_restricted`. Keep the team's data and its cursor, skip it for this run and try again later. A disabled team or paused access can come back; an archived team stays restricted unless Totem restores it. While a team is restricted, the changes feed delivers nothing for it, removals included, so if the restriction lasts longer than your retention policy allows, delete your copy.\n- For now, Totem does not export teams in a sensitive business sector, such as health care. Such a team cannot be added to an integration, and a key that already covers one gets `403 sector_restricted` for it. Skip the team: waiting does not change it. The changes feed delivers nothing for it, removals included, so keep what you already have only as long as your retention policy allows, then delete your copy.\n\n## IP allowlist\n\nAn integration can be limited to up to :api-const{name=\"MAX_IP_ALLOWLIST\"} IP addresses or ranges in CIDR notation. A request from any other address gets `403 ip_not_allowed`. Set the list in **Settings → Developers**, under **Advanced options** when you create the key or with **Edit integration** later; changing it asks for a recent sign-in.\n\n- The API is reachable over IPv4 only, so list the public IPv4 addresses your requests leave from. An IPv6 entry never matches today.\n- Write one address or range per line. A range must start at its network address, such as `203.0.113.0\u002F24`, and `\u002F0` is refused. An empty list allows any address.\n- If your platform has no fixed outgoing address, as with many serverless hosts, send its traffic through a static IP first.\n- After you save the list, call `GET \u002Fv1\u002Fme` from the deployed server to check it.\n\nTotem checks the address only after the key, so a wrong key gets `401 invalid_credentials` wherever it comes from.\n\n## Roll and revoke\n\n**Roll** a key with **Roll key** to replace it. Totem issues a new key and keeps the old one working for the time you choose in the dialog: 1 hour, 24 hours (the default) or 7 days. Deploy the new key within that time and nothing stops. When that time is over, the old key answers `401 key_expired`. Rolling again revokes any old key still in its grace period, and the dialog asks you to confirm it.\n\nTo stop the old key at once, tick **This key may have been compromised** when you roll, or choose **End old key now** later. The old key then answers `401 invalid_credentials`.\n\n**If a key may have leaked**, roll it and tick **This key may have been compromised**: the old key is revoked at once, with no grace period. Then tell us, as [Support](\u002Fdevelopers\u002Fsupport) explains.\n\n**Revoke** an integration with **Revoke access** to stop all of its keys. A revoked key stops working within :api-const{name=\"KEY_CACHE_TTL_MS\" divide=\"1000\"} seconds, and revoking cannot be undone. The data you already copied stays in your systems and receives no more removals from Totem: delete it, or keep it only as long as the customer's own rules allow. [Security and personal data](\u002Fdevelopers\u002Fsecurity#personal-data-you-receive) explains what to do with it.\n\nCreating or rolling a key, adding a team and changing the IP allowlist ask the administrator to sign in again if they have not done so recently. Every change appears in the integration's **Recent activity**, and the administrators who manage the integration, and those of every team it covers, are told about each one, in Totem and by email. Revoking and **End old key now** never ask for a sign-in: anyone allowed to manage the integration can stop it at once.\n\n## Failed attempts\n\nTotem counts failed attempts: requests with a key that is missing, malformed, unknown or wrong. After as few as :api-const{name=\"AUTH_FAILURE_LIMITS.perIpPerMinute\"} failures from one IP address in a minute, or :api-const{name=\"AUTH_FAILURE_LIMITS.perLookupIdPerMinute\"} with one key, Totem can lock that address or that key for :api-const{name=\"AUTH_FAILURE_LIMITS.banMs\" divide=\"1000\"} seconds. While it is locked, requests get `429 rate_limited` with `Retry-After` set to the full lock length; the lock may end sooner.\n\nA revoked or expired key, an integration that is no longer active and a request from outside the allowlist are not counted. While a key is locked, requests with its real secret keep working: only guesses are refused. While an address is locked, every request from it is refused. If you are locked out, fix the key first, then wait for `Retry-After` before you try again.\n\nMany requests without a well-formed key from one address can also make the firewall in front of the API answer `429` itself, with no `Retry-After` and no JSON body, for up to 10 minutes. Stop the loop, fix the header, then retry with exponential backoff, as [Errors](\u002Fdevelopers\u002Ferrors) explains.\n\n## Authentication errors\n\nEvery code links to its full entry on the [Errors](\u002Fdevelopers\u002Ferrors) page.\n\n| Status | `code` | What to do |\n|---|---|---|\n| 400 | [`secret_key_in_browser`](\u002Fdevelopers\u002Ferrors#secret_key_in_browser) | The request came from a browser. Call the API from your server, and if the key was ever in a web page or an app, roll it as compromised. |\n| 400 | [`team_required`](\u002Fdevelopers\u002Ferrors#team_required) | The key covers several teams. Send `Totem-Team`. |\n| 401 | [`invalid_credentials`](\u002Fdevelopers\u002Ferrors#invalid_credentials) | The key is missing, malformed, unknown, wrong or revoked. Check the header and the key. |\n| 401 | [`key_expired`](\u002Fdevelopers\u002Ferrors#key_expired) | The key was rolled and its grace period is over. Use the new key. |\n| 403 | [`ip_not_allowed`](\u002Fdevelopers\u002Ferrors#ip_not_allowed) | The request came from an address outside the integration's allowlist. The API is reachable over IPv4 only. |\n| 403 | [`plan_not_entitled`](\u002Fdevelopers\u002Ferrors#plan_not_entitled) | The organization's plan does not include the API. Keep your data and talk to the customer; the feed delivers no removals until access returns, so if that lasts longer than your retention policy allows, delete your copy. |\n| 403 | [`insufficient_scope`](\u002Fdevelopers\u002Ferrors#insufficient_scope) | The key lacks a permission this endpoint needs. Every v1.0a key has the same permissions, so tell us, with the request id. |\n| 403 | [`team_state_restricted`](\u002Fdevelopers\u002Ferrors#team_state_restricted) | The team is disabled, archived or paused. Keep its data and its cursor, skip it for this run and try again later; if the restriction lasts longer than your retention policy allows, delete your copy. |\n| 403 | [`sector_restricted`](\u002Fdevelopers\u002Ferrors#sector_restricted) | For now, Totem does not export teams in a sensitive sector, such as health care. Skip the team: waiting does not change it. The feed delivers no removals for it, so keep what you have only as long as your retention policy allows, then delete your copy. |\n| 404 | [`team_not_accessible`](\u002Fdevelopers\u002Ferrors#team_not_accessible) | The key does not cover this team. If `GET \u002Fv1\u002Fteams` no longer lists it, delete its data and its cursor. |\n| 429 | [`rate_limited`](\u002Fdevelopers\u002Ferrors#rate_limited) | Too many failed attempts, or too many requests. Wait for the seconds in `Retry-After`. A `429` without a JSON body comes from the firewall: back off exponentially. |\n","start",{"title":6,"description":775},"developers\u002Fauthentication","authentication","t_L6q_n8Dco9hND4Uco_xASZip5UQquVPtaPF-uhGHU",[790,801,827,874],{"section":784,"items":791},[792,796,800],{"path":793,"title":794,"description":795},"\u002Fdevelopers","Overview","Read the leads, calls, conversations and bookings of your Totem teams from your own systems, and keep a copy in sync.",{"path":797,"title":798,"description":799},"\u002Fdevelopers\u002Fquickstart","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.",{"path":782,"title":6,"description":775},{"section":802,"items":803},"guides",[804,808,812,816,820,824],{"path":805,"title":806,"description":807},"\u002Fdevelopers\u002Ftypescript","TypeScript SDK","Use the official TypeScript client to read your Totem data, paginate lists and handle API errors from your server.",{"path":809,"title":810,"description":811},"\u002Fdevelopers\u002Fsync","Keep a copy in sync","Copy a team with the changes feed and keep the copy current. Apply pages safely, recover from errors, and start from a reference client that Totem tests.",{"path":813,"title":814,"description":815},"\u002Fdevelopers\u002Fpagination","Lists and pagination","How list endpoints return records a page at a time, how to read every page, and what order and filters a list supports.",{"path":817,"title":818,"description":819},"\u002Fdevelopers\u002Fcoverage","Data coverage","What Totem records, since when, and how to tell an empty value from a fact that was never recorded.",{"path":821,"title":822,"description":823},"\u002Fdevelopers\u002Ftesting","Testing and go-live","How to build and test an integration safely while the API has no test environment, and what to check before you go live.",{"path":825,"title":488,"description":826},"\u002Fdevelopers\u002Fsecurity","How Totem protects API keys and the data the API serves, what it records about each key, where the API runs, and what to do with the personal data you copy.",{"section":828,"items":829},"reference",[830,834,836,840,845,850,855,860,865,870],{"path":831,"title":832,"description":833},"\u002Fdevelopers\u002Fconventions","Conventions","The notation these docs use, and the rules every response follows: ids, timestamps, empty values, amounts, enums, versions, stability, errors and untrusted text.",{"path":545,"title":546,"description":835},"The error format, how your integration should handle each kind of error, and every error code with when it happens and what to do.",{"path":837,"title":838,"description":839},"\u002Fdevelopers\u002Frate-limits","Rate limits","How much a key can read per minute and per day, what counts toward each limit, how to retry after a 429, and how long an initial sync takes.",{"path":841,"title":842,"description":843,"apiTag":844},"\u002Fdevelopers\u002Freference-account","Account, teams and members","Check that a key works and what it can read, list its teams with their time zones, list each team's members, and check that the API is up.","account",{"path":846,"title":847,"description":848,"apiTag":849},"\u002Fdevelopers\u002Freference-leads","Leads, boards and activities","List, fetch and search a team's leads and clients, read each lead's activities, and list the boards and stages they move through.","leads",{"path":851,"title":852,"description":853,"apiTag":854},"\u002Fdevelopers\u002Freference-calls","Calls","List and fetch a team's calls, with their direction, timing and a normalized outcome. No recordings or transcripts.","calls",{"path":856,"title":857,"description":858,"apiTag":859},"\u002Fdevelopers\u002Freference-conversations","Conversations","List and fetch a team's conversations on every channel, with their status, assignee, whether a person has taken them over and when the last messages came and went. Never message content.","conversations",{"path":861,"title":862,"description":863,"apiTag":864},"\u002Fdevelopers\u002Freference-bookings","Bookings and services","List and fetch a team's bookings, and list the services its customers can book.","bookings",{"path":866,"title":867,"description":868,"apiTag":869},"\u002Fdevelopers\u002Freference-sync","Changes feed","One endpoint that copies a team and then returns every change, page by page.","sync",{"path":871,"title":872,"description":873},"\u002Fdevelopers\u002Fobjects","All objects","Every object the API returns, field by field, with an example of each.",{"section":875,"items":876},"resources",[877,881,883,887],{"path":878,"title":879,"description":880},"\u002Fdevelopers\u002Fchangelog","Changelog","Changes to the Totem API, newest first, each under the date it shipped.",{"path":141,"title":469,"description":882},"How to get help with the Totem API, what to include in a request, and what to do if a key leaks.",{"path":884,"title":885,"description":886},"\u002Fdevelopers\u002Fglossary","Glossary","The terms these docs use, defined in one place, each with a link to the page that explains it.",{"path":888,"title":889,"description":890},"\u002Fdevelopers\u002Ffaq","FAQ","Short answers to the questions developers ask most about the Totem API, with links to the details.","\u002Fdevelopers\u002Fes\u002Fauthentication",{"ACTIVATION_MARGIN_MS":893,"ADMIN_ACCESS_DISCLOSURE_VERSION":894,"AUDIT_RETENTION_DAYS":895,"AUTH_FAILURE_LIMITS":896,"CAPTURE_LAG_FLOOR_MS":900,"CAPTURE_LAG_P99_MS":7,"CURSOR_MAX_AGE_DAYS":901,"DIRTY_TTL_DAYS":901,"FAMILY_TEAM_SCOPE":902,"FAMILY_TEAM_SCOPE_OPTIONS":903,"FEED_SETTLE_LAG_MS":905,"GATE_REFRESH_MS":893,"INTEGRATION_NAME_LENGTH":906,"INTEGRATION_PURPOSE_LENGTH":908,"KEY_CACHE_TTL_MS":911,"KEY_STATS_RETENTION_DAYS":912,"LEAD_RESPONSE_READ_BUDGET":913,"LIST_SCAN_MAX":916,"MAX_FAMILY_INTEGRATION_TEAMS":901,"MAX_INTEGRATIONS_PER_FAMILY":917,"MAX_IP_ALLOWLIST":898,"PAGE_LIMIT":918,"PLATFORM_CONFIG_TTL_MS":921,"REMOVAL_RETENTION_DAYS":922,"REQUEST_BODY_MAX_BYTES":923,"ROLL_GRACE_DEFAULT_HOURS":924,"ROLL_GRACE_HOURS":925,"SEARCH_MAX_RESULTS":917,"SENSITIVE_SECTOR_POLICY":928,"SENSITIVE_SECTOR_POLICY_OPTIONS":929,"SYNC_DAILY_BUDGET":931,"SYNC_VELOCITY":933,"TEAM_CACHE_TTL_MS":935},60000,"2026-10-17",365,{"perIpPerMinute":897,"perLookupIdPerMinute":898,"banMs":899},60,20,600000,300000,30,"explicit_list",[902,904],"all_family_teams",10000,{"min":765,"max":907},80,{"min":909,"max":910},1,280,5000,90,{"conversations":898,"messageReads":914,"calls":915},200,50,1000,10,{"default":919,"max":920},25,100,15000,45,16384,24,[926,909,924,927],0,168,"exclude",[928,930],"allow",{"floor":932,"recordsMultiplier":762},250000,{"perMinute":934,"burst":905},2000,30000,"1.0a","guide",[939,940,941,942,943,944,945,946,947],{"id":14,"text":15,"depth":765},{"id":67,"text":68,"depth":762},{"id":145,"text":146,"depth":762},{"id":183,"text":184,"depth":762},{"id":225,"text":89,"depth":765},{"id":375,"text":376,"depth":765},{"id":431,"text":179,"depth":765},{"id":502,"text":503,"depth":765},{"id":549,"text":550,"depth":765},{"path":797,"title":798,"section":784},{"path":805,"title":806,"section":802},[],1792502787028]