[{"data":1,"prerenderedAt":1349},["ShallowReactive",2],{"developers:\u002Fdevelopers\u002Fsync":3},{"doc":4,"lang":1168,"nav":1179,"alternatePath":1278,"policy":1279,"specVersion":1323,"reference":7,"layout":1324,"toc":1325,"prev":1346,"next":1347,"operations":1348,"overview":7,"heroExample":7},{"id":5,"title":6,"apiTag":7,"body":8,"description":1165,"draft":1166,"extension":1167,"lang":1168,"meta":1169,"navTitle":7,"navigation":1170,"order":1171,"path":1172,"rawbody":1173,"section":1174,"seo":1175,"stem":1176,"translationKey":1177,"version":7,"__hash__":1178},"developers_en\u002Fdevelopers\u002Fsync.md","Keep a copy in sync",null,{"type":9,"value":10,"toc":1137},"minimark",[11,24,27,32,43,50,92,104,107,112,119,129,144,150,179,183,186,237,240,244,251,259,288,291,294,297,346,354,358,364,370,377,381,392,400,403,407,411,423,428,462,466,484,488,501,518,532,536,542,565,568,571,574,577,752,758,762,768,784,787,791,798,812,816,819,830,833,862,866,886,890,901,916,927,935,941,945,954,1034,1060,1070,1117,1121,1125,1134],[12,13,14,15,23],"p",{},"The changes feed, ",[16,17,19],"a",{"href":18},"\u002Fdevelopers\u002Freference-sync#sync.changes",[20,21,22],"code",{},"GET \u002Fv1\u002Fsync\u002Fchanges",", is how you keep your own copy of a team. Your first request copies every record. After that, each request returns only what changed since the last one, so a sync every few minutes is cheap and never misses a change.",[25,26],"dev-sync-diagram",{},[28,29,31],"h2",{"id":30},"how-the-feed-works","How the feed works",[12,33,34,35,42],{},"There is one feed per team. A key that reads several teams names the team in the ",[16,36,38,41],{"href":37},"\u002Fdevelopers\u002Fauthentication#teams-and-the-totem-team-header",[20,39,40],{},"Totem-Team"," header"," and keeps one cursor per team.",[12,44,45,46,49],{},"Each item of the feed is the current state of one record, of one ",[20,47,48],{},"kind",":",[51,52,53,68,74,86],"ul",{},[54,55,56,59,60,63,64,67],"li",{},[20,57,58],{},"lead",", ",[20,61,62],{},"lead_attribution"," and ",[20,65,66],{},"lead_response",": a lead and its two facets;",[54,69,70,73],{},[20,71,72],{},"activity",": a change of a lead's stage, status or assigned members;",[54,75,76,59,79,63,82,85],{},[20,77,78],{},"call",[20,80,81],{},"conversation",[20,83,84],{},"booking",";",[54,87,88,91],{},[20,89,90],{},"board",", with its stages inside it.",[12,93,94,95,98,99,103],{},"A lead's attribution and response travel as kinds of their own, with the lead's id and a ",[20,96,97],{},"version"," of their own. That is why a new message changes the lead's response without changing the lead. Members and bookable services are not in the feed: you ",[16,100,102],{"href":101},"#refresh-boards-members-and-services","read them separately",".",[12,105,106],{},"Items come in the order Totem recorded the changes, oldest first. An item is never a diff: it carries the whole record as it is now.",[108,109,111],"h3",{"id":110},"records-and-removals","Records and removals",[12,113,114,115,118],{},"Most items carry the record under ",[20,116,117],{},"object",". It is the same object the list endpoints return, shortened here:",[120,121,127],"pre",{"className":122,"code":124,"language":125,"meta":126},[123],"language-json","{\n  \"kind\": \"call\",\n  \"id\": \"call_m2kUbFcayHlZ5Lgs9xZNmDS3c3Jvk\",\n  \"version\": 2,\n  \"object\": {\n    \"id\": \"call_m2kUbFcayHlZ5Lgs9xZNmDS3c3Jvk\",\n    \"object\": \"call\",\n    \"livemode\": false,\n    \"team\": \"team_r9uYrIW4gSFJJ6hZQ9ip1UIpPaAD1CF7NR\",\n    \"direction\": \"inbound\",\n    \"outcome\": \"answered_ai\",\n    \"version\": 2\n  }\n}\n","json","",[20,128,124],{"__ignoreMap":126},[12,130,131,132,136,137,140,141,143],{},"A ",[133,134,135],"strong",{},"removal"," carries ",[20,138,139],{},"removed"," instead of ",[20,142,117],{},". It tells you to delete the record:",[120,145,148],{"className":146,"code":147,"language":125,"meta":126},[123],"{\n  \"kind\": \"call\",\n  \"id\": \"call_m1pf4APjJWVi3Z7ulyNaW8y3lUi7V\",\n  \"version\": 5,\n  \"removed\": { \"reason\": \"deleted\" }\n}\n",[20,149,147],{"__ignoreMap":126},[12,151,152,155,156,159,160,163,164,168,169,173,174,178],{},[20,153,154],{},"reason"," is ",[20,157,158],{},"erased"," when the record was erased: for privacy, for example at the person's request, or because someone permanently deleted a lead, a call or a conversation in Totem, which always erases it. It is ",[20,161,162],{},"deleted"," when the record went away in any other way, for example when it no longer belongs to the team's API data, or when Totem now names it with a new id. Handle both the same way, as ",[16,165,167],{"href":166},"#apply-a-page","Apply a page"," explains. A record that comes back is a new record with a new id (",[16,170,172],{"href":171},"\u002Fdevelopers\u002Fconventions#ids","Conventions","), so match people across ids by phone or email with ",[16,175,177],{"href":176},"\u002Fdevelopers\u002Freference-leads#leads.search","search",", never by id.",[28,180,182],{"id":181},"plan-your-storage","Plan your storage",[12,184,185],{},"A copy built this way can apply every page safely, whatever order the records arrive in:",[51,187,188,206,219,225,231],{},[54,189,190,193,194,63,196,199,200,202,203,205],{},[133,191,192],{},"One row per record",", keyed by team, ",[20,195,48],{},[20,197,198],{},"id",", with its ",[20,201,97],{},". A lead and its two facets share an id, so the ",[20,204,48],{}," is part of the key.",[54,207,208,211,212,59,214,63,216,218],{},[133,209,210],{},"One tombstone per removal",": the ",[20,213,48],{},[20,215,198],{},[20,217,97],{}," of a record you deleted, without its data.",[54,220,221,224],{},[133,222,223],{},"One cursor per team",", saved together with the page it follows when your database allows it.",[54,226,227,230],{},[133,228,229],{},"Boards, members and bookable services"," in lists of their own, replaced as a whole each time you read them.",[54,232,233,236],{},[133,234,235],{},"No foreign keys between records while you apply pages."," A record can name another that has not reached you yet, such as a lead naming a stage of a board, or an activity naming its lead.",[12,238,239],{},"Then run one sync per key every 1 to 5 minutes, and watch for a team that keeps being skipped or postponed, or whose cursor has not been saved for a day.",[28,241,243],{"id":242},"run-the-initial-sync","Run the initial sync",[12,245,246,247,250],{},"Your first request for a team has no cursor. It starts the ",[133,248,249],{},"initial sync",": the feed pages through every record of the team, oldest change first.",[120,252,257],{"className":253,"code":255,"language":256,"meta":126},[254],"language-bash","curl \"https:\u002F\u002Fapi.totemstudio.ai\u002Fv1\u002Fsync\u002Fchanges?limit=100\" \\\n  -H \"Authorization: Bearer $TOTEM_API_KEY\" \\\n  -H \"Totem-Team: $TOTEM_TEAM_ID\"\n","bash",[20,258,255],{"__ignoreMap":126},[12,260,261,262,265,266,269,270,155,273,276,277,155,279,282,283,287],{},"Apply the page, store its ",[20,263,264],{},"next_cursor",", and ask again with ",[20,267,268],{},"cursor"," set to it. Repeat while ",[20,271,272],{},"has_more",[20,274,275],{},"true",". When ",[20,278,272],{},[20,280,281],{},"false",", your copy is complete. Ask for the largest page, ",[284,285],"api-const",{"name":286},"PAGE_LIMIT.max"," items, so the initial sync takes as few requests as possible.",[12,289,290],{},"Two things are normal during an initial sync. A record that changes while it runs comes again later, with a higher version. And after a retry, you can receive the same page twice. The version check in the next section makes both harmless.",[28,292,167],{"id":293},"apply-a-page",[12,295,296],{},"For each item of the page, in order:",[298,299,300,314,327],"ol",{},[54,301,302,305,306,63,308,310,311,313],{},[133,303,304],{},"Skip what you already have."," If you hold this ",[20,307,48],{},[20,309,198],{}," with the same or a higher ",[20,312,97],{},", skip the item. Compare with the versions of your tombstones too.",[54,315,316,319,320,322,323,326],{},[133,317,318],{},"Store a record."," If the item has ",[20,321,117],{},", insert your copy or replace it. For a lead, store its ",[20,324,325],{},"contact_permissions"," before anything else can act on it.",[54,328,329,319,332,334,335,59,337,63,339,341,342,345],{},[133,330,331],{},"Apply a removal.",[20,333,139],{},", delete your copy, but keep its ",[20,336,48],{},[20,338,198],{},[20,340,97],{}," as a ",[133,343,344],{},"tombstone",". When the removed record is a lead, also delete its attribution, its response and its activities, and ignore any of them that arrives later.",[347,348,351],"dev-callout",{"title":349,"type":350},"Keep the tombstone","warning",[12,352,353],{},"Without the tombstone, step 1 has nothing to compare with, and an older copy of the record, delivered again by a replayed page, would bring it back. For a record erased at a person's request, that is a privacy incident, not a sync glitch.",[28,355,357],{"id":356},"save-the-cursor","Save the cursor",[12,359,360,361,363],{},"Store ",[20,362,264],{}," only after the whole page is saved. If your process stops in between, the next run reads the same page again, and step 1 skips what you already applied. When your database allows it, save the page and its cursor in one transaction.",[12,365,366,367,103],{},"A cursor is opaque: store it exactly as you received it, and never build or edit one. It belongs to one team. Sent with another team, it gets ",[20,368,369],{},"400 invalid_request",[12,371,372,373,376],{},"A cursor expires ",[284,374],{"name":375},"CURSOR_MAX_AGE_DAYS"," days after the point in the feed it reached. While an initial sync is still running, that age counts from when the initial sync started. A team you poll regularly never reaches it.",[28,378,380],{"id":379},"poll-for-changes","Poll for changes",[12,382,383,384,155,386,388,389,391],{},"Once ",[20,385,272],{},[20,387,281],{},", ask again with your stored cursor every 1 to 5 minutes. When nothing has changed, you get an empty page with a new ",[20,390,264],{},". Store it too: an empty page moves the cursor forward, so the cursor of a quiet team never grows old.",[12,393,394,395,399],{},"The feed holds back the last ",[284,396],{"name":397,"divide":398},"FEED_SETTLE_LAG_MS","1000"," seconds of changes, so that a change still being written is never skipped. A change shows up in the feed shortly after it happens in Totem.",[12,401,402],{},"Run at most one sync process per key and team. Two processes applying pages of the same team can save their cursors out of order.",[28,404,406],{"id":405},"removals-merges-and-transfers","Removals, merges and transfers",[108,408,410],{"id":409},"removals","Removals",[12,412,413,414,417,418,422],{},"Apply a removal as soon as you receive it: delete the record from your copy and from every system you copied it to. Removals stay in the feed for ",[284,415],{"name":416},"REMOVAL_RETENTION_DAYS"," days, longer than a cursor lives, so a sync that keeps reading the feed successfully never misses one. While the API refuses a team, it delivers nothing for that team, removals included; ",[16,419,421],{"href":420},"#recover-from-errors","Recover from errors"," says what to do then.",[12,424,425,426,49],{},"When a lead is removed, its attribution, response and activities are removed with it, and the feed delivers their removals too. What happens to the lead's other records depends on the ",[20,427,154],{},[51,429,430,451],{},[54,431,432,436,437,440,441,444,445,447,448,103],{},[133,433,434],{},[20,435,158],{},", which includes a lead deleted permanently in Totem: its calls and conversations are erased too, and so are the archived leads whose ",[20,438,439],{},"merged_into"," names it, with their own calls and conversations. Each record arrives as a removal of its own, with ",[20,442,443],{},"reason: \"erased\"",". Its bookings are not removed: they arrive again with ",[20,446,58],{}," set to ",[20,449,450],{},"null",[54,452,453,457,458,447,460,103],{},[133,454,455],{},[20,456,162],{},": its calls, conversations and bookings are not removed. They arrive again with ",[20,459,58],{},[20,461,450],{},[108,463,465],{"id":464},"merges","Merges",[12,467,468,469,155,472,475,476,478,479,155,481,483],{},"When two leads of the same person are merged in Totem, the lead that is not kept is archived, not removed. Its ",[20,470,471],{},"archive_reason",[20,473,474],{},"merged",", and its ",[20,477,439],{}," names the lead that was kept. Link your copy to that lead instead of creating the person twice. The lead that is kept also takes the stricter contact permissions of the two. ",[20,480,439],{},[20,482,450],{}," when the lead it named no longer exists.",[108,485,487],{"id":486},"transfers","Transfers",[12,489,490,491,447,493,496,497,500],{},"When a lead moves to another team of the same organization, the original lead is archived with ",[20,492,471],{},[20,494,495],{},"family_transfer",", and ",[20,498,499],{},"transferred_to"," says where it went:",[51,502,503,509],{},[54,504,505,508],{},[20,506,507],{},"transferred_to.team"," is always set. It can be a team your key does not read.",[54,510,511,514,515,517],{},[20,512,513],{},"transferred_to.lead"," is the lead in that team, or ",[20,516,450],{}," when Totem cannot name it.",[12,519,520,521,527,528,531],{},"If your key reads the destination team, the new lead arrives in that team's feed. If it does not, ",[16,522,524],{"href":523},"\u002Fdevelopers\u002Freference-leads#leads.get",[20,525,526],{},"GET \u002Fv1\u002Fleads\u002F{id}"," on the destination lead answers ",[20,529,530],{},"404 resource_missing",". Keep the archived lead as the record of where the person went, and do not guess.",[28,533,535],{"id":534},"contact-permissions-first","Contact permissions first",[12,537,538,539,541],{},"Before any of your systems contacts a lead, store its ",[20,540,325],{}," and apply them:",[51,543,544,550,556],{},[54,545,546,549],{},[20,547,548],{},"do_not_contact: true",": the person asked never to be contacted. Stop every channel.",[54,551,552,555],{},[20,553,554],{},"email_opt_out: true",": the person unsubscribed from marketing and follow-up email.",[54,557,558,561,562,564],{},[20,559,560],{},"whatsapp_consent: false",": the person refused WhatsApp. ",[20,563,450],{}," means nobody recorded an answer, so never treat it as consent.",[12,566,567],{},"A lead in the feed is data, not permission to contact the person. The legal basis for contacting someone stays your responsibility.",[12,569,570],{},"The API is read-only, so Totem cannot learn about an opt-out recorded in your systems. When a person opts out there, make sure it is also recorded on the lead in Totem, so that Totem stops contacting them too.",[28,572,421],{"id":573},"recover-from-errors",[12,575,576],{},"When a request for a page fails, do not save a cursor. The next request with the same cursor asks for the same page again.",[578,579,580,596],"table",{},[581,582,583],"thead",{},[584,585,586,590,593],"tr",{},[587,588,589],"th",{},"Answer",[587,591,592],{},"What it means",[587,594,595],{},"What to do",[597,598,599,620,634,647,681,700,712,739],"tbody",{},[584,600,601,607,613],{},[602,603,604],"td",{},[20,605,606],{},"410 sync_cursor_expired",[602,608,609,610,612],{},"The cursor is more than ",[284,611],{"name":375}," days old.",[602,614,615,616,103],{},"Run a ",[16,617,619],{"href":618},"#full-resync","full resync",[584,621,622,627,630],{},[602,623,624],{},[20,625,626],{},"409 resync_required",[602,628,629],{},"Totem cannot continue from this cursor, for example after restoring a backup.",[602,631,615,632,103],{},[16,633,619],{"href":618},[584,635,636,641,644],{},[602,637,638],{},[20,639,640],{},"409 capture_not_ready",[602,642,643],{},"Totem is still preparing this team's data. It happens once, when a team is first added to a key.",[602,645,646],{},"Keep everything and try again in a few minutes. Never resync for it. The key's other teams keep syncing.",[584,648,649,654,665],{},[602,650,651],{},[20,652,653],{},"403",[602,655,656,657,660,661,664],{},"Access is refused for now: for example, the team is restricted (",[20,658,659],{},"team_state_restricted",") or the endpoint is switched off (",[20,662,663],{},"capability_disabled",").",[602,666,667,668,672,673,676,677,680],{},"Keep the team's data and cursor, and skip the team in this run. The ",[16,669,671],{"href":670},"\u002Fdevelopers\u002Ferrors","Errors"," page says, code by code, whether waiting helps. ",[20,674,675],{},"plan_not_entitled"," refuses the whole key, so the run stops at ",[20,678,679],{},"GET \u002Fv1\u002Fteams",". While access is refused, the feed delivers nothing for the team, removals included: if the refusal lasts longer than your retention policy allows, delete the team's copy.",[584,682,683,688,691],{},[602,684,685],{},[20,686,687],{},"404 team_not_accessible",[602,689,690],{},"The key may no longer read this team.",[602,692,693,694,699],{},"Read ",[16,695,697],{"href":696},"\u002Fdevelopers\u002Freference-account#teams.list",[20,698,679],{},". If the team is not listed, delete its data and its cursor. If it is, keep everything and try again on the next run.",[584,701,702,706,709],{},[602,703,704],{},[20,705,369],{},[602,707,708],{},"The cursor was changed, or it belongs to another team.",[602,710,711],{},"Use the cursor you stored for this team.",[584,713,714,719,726],{},[602,715,716],{},[20,717,718],{},"429",[602,720,721,722,103],{},"You reached the ",[16,723,725],{"href":724},"\u002Fdevelopers\u002Frate-limits","rate limit or the daily quota",[602,727,728,729,732,733,735,736,738],{},"Wait the seconds in ",[20,730,731],{},"Retry-After",", then retry with the same cursor. A ",[20,734,718],{}," from the firewall has no ",[20,737,731],{},": back off exponentially.",[584,740,741,746,749],{},[602,742,743],{},[20,744,745],{},"5xx",[602,747,748],{},"Something failed inside Totem.",[602,750,751],{},"Retry with exponential backoff.",[12,753,754,755,757],{},"Every code is explained on the ",[16,756,671],{"href":670}," page.",[108,759,761],{"id":760},"full-resync","Full resync",[12,763,764,765,49],{},"A full resync rebuilds your copy of one team, by ",[133,766,767],{},"mark and sweep",[298,769,770,773,776],{},[54,771,772],{},"Forget the versions and the cursor you stored for the team. Keep the records themselves: your copy stays usable while the resync runs.",[54,774,775],{},"Run the initial sync again, without a cursor. Apply each page as usual, and mark every record the feed delivers.",[54,777,778,779,155,781,783],{},"When ",[20,780,272],{},[20,782,281],{},", delete every record of the team that you did not mark.",[12,785,786],{},"Store the marks durably. If the process stops halfway, the next run must continue the same resync, not sweep with half the marks. Forgetting the versions first matters too: after a restore, the feed can deliver lower versions than the ones you held.",[28,788,790],{"id":789},"when-teams-change","When teams change",[12,792,693,793,797],{},[16,794,795],{"href":696},[20,796,679],{}," at the start of every run. It lists every team your key reads, including teams that are restricted for now.",[51,799,800,803,806],{},[54,801,802],{},"A team you have not synced yet: run its initial sync.",[54,804,805],{},"A team you hold that is no longer listed: it has left the key. Delete its data and its cursor.",[54,807,808,809,811],{},"A listed team that answers ",[20,810,653],{},": keep it, as above.",[28,813,815],{"id":814},"refresh-boards-members-and-services","Refresh boards, members and services",[12,817,818],{},"Members and bookable services are not in the feed. Boards are, but a lead can name a new stage before the change to its board reaches you. So read all three:",[51,820,821,824,827],{},[54,822,823],{},"when you start syncing a team;",[54,825,826],{},"when an item names a board, stage, member or service you do not know;",[54,828,829],{},"at least once a day.",[12,831,832],{},"A record can keep naming a member who left the team, or a board, stage or service that was deleted. If a refresh does not find an id, remember it and do not refresh for it again before the daily refresh.",[12,834,835,841,842,63,848,854,855,858,859,861],{},[16,836,838],{"href":837},"\u002Fdevelopers\u002Freference-leads#boards.list",[20,839,840],{},"GET \u002Fv1\u002Fboards"," pages like any list. ",[16,843,845],{"href":844},"\u002Fdevelopers\u002Freference-account#members.list",[20,846,847],{},"GET \u002Fv1\u002Fmembers",[16,849,851],{"href":850},"\u002Fdevelopers\u002Freference-bookings#booking_services.list",[20,852,853],{},"GET \u002Fv1\u002Fbooking_services"," return the whole list in one response and take no ",[20,856,857],{},"limit"," or ",[20,860,268],{},". Replace your copy of each list as a whole. A member who leaves the team, or a service that is deleted, drops out of its list while older records keep naming its id: if you show names for older records, keep the name from an earlier read. Disabled members and services stay listed, with nothing that marks them as disabled.",[28,863,865],{"id":864},"pace","Pace",[12,867,868,869,872,873,876,877,879,880,882,883,885],{},"A key reads up to ",[284,870],{"name":871},"SYNC_VELOCITY.perMinute"," items a minute, so the initial sync of a large team takes a while. ",[16,874,875],{"href":724},"Rate limits"," has the numbers, an estimate of how long an initial sync takes, and why the numbers are approximate during the Beta. When you get a ",[20,878,718],{},", wait for ",[20,881,731],{}," and continue with the same cursor: nothing is lost. The limits belong to the key, so a ",[20,884,718],{}," pauses every team the key reads, not just the one you were syncing.",[28,887,889],{"id":888},"tested-reference-client","Tested reference client",[12,891,892,893,896,897,900],{},"The TypeScript below is a complete sync client that does everything this page describes. Totem runs it in its own tests against a simulated feed, with crashes, replayed pages, removals, resyncs and errors; the code shown here is that exact file. It has no dependencies and needs only ",[20,894,895],{},"fetch",": compile it with your TypeScript build, or run it directly on Node.js 22.18 or later. Copy it to your server and implement ",[20,898,899],{},"SyncStore"," on your database.",[347,902,905],{"title":903,"type":904},"Reference client or SDK?","note",[12,906,907,908,912,913,915],{},"For a sync, start from this client: it implements the tombstones, the mark and sweep of a full resync and one outcome per team. The ",[16,909,911],{"href":910},"\u002Fdevelopers\u002Ftypescript","TypeScript SDK"," gives typed requests and pagination; its own sync example handles one team and leaves the ",[16,914,619],{"href":618}," to your store, so implement it as this page explains.",[12,917,918,919,922,923,926],{},"Run it from a scheduled job every 1 to 5 minutes, one run per key at a time. The example is an ES module; under Node.js module resolution, import the file with the ",[20,920,921],{},".js"," extension your build emits, or with ",[20,924,925],{},".ts"," when Node.js runs the TypeScript directly:",[120,928,933],{"className":929,"code":931,"language":932,"meta":126},[930],"language-ts","import { syncKey } from '.\u002FsyncTeam.js'\n\nconst outcomes = await syncKey({ key: process.env.TOTEM_API_KEY!, store })\n","ts",[20,934,931],{"__ignoreMap":126},[12,936,937,938,940],{},"The key must never reach a browser. Every ",[20,939,899],{}," method must write durably before its promise resolves, and writes for one team must not run in parallel.",[108,942,944],{"id":943},"options-and-outcomes","Options and outcomes",[12,946,947,950,951,953],{},[20,948,949],{},"syncKey"," reads ",[20,952,679],{},", deletes the teams that left the key, and syncs every listed team in turn. It returns one outcome per team:",[51,955,956,962,972,978,1000],{},[54,957,958,961],{},[20,959,960],{},"synced",": the team is caught up and its cursor is saved.",[54,963,964,967,968,971],{},[20,965,966],{},"partial",": the team read ",[20,969,970],{},"maxPagesPerTeam"," pages and is not caught up yet. Its cursor is saved, and the next run continues from it.",[54,973,974,977],{},[20,975,976],{},"dropped",": the team left the key, and its copy and cursor are deleted.",[54,979,980,983,984,986,987,59,989,992,993,996,997,999],{},[20,981,982],{},"skipped",", with the ",[20,985,20],{},": the team cannot be read right now (",[20,988,653],{},[20,990,991],{},"capture_not_ready",", or a ",[20,994,995],{},"team_not_accessible"," that ",[20,998,679],{}," does not confirm). Its copy and cursor are kept, and the next run tries again.",[54,1001,1002,983,1005,63,1007,1010,1011,858,1013,1015,1016,1019,1020,1022,1023,1025,1026,1028,1029,63,1031,1033],{},[20,1003,1004],{},"retry_later",[20,1006,20],{},[20,1008,1009],{},"retryAt"," (epoch milliseconds): the API asked for a longer wait than this run allows, or a ",[20,1012,718],{},[20,1014,745],{}," was still failing after ",[20,1017,1018],{},"maxRetries"," retries. The copy and cursor are kept. Do not sync that team again before ",[20,1021,1009],{},". A ",[20,1024,718],{}," belongs to the key, not to one team, so once one sends the key away, every team this run has not reached yet is reported ",[20,1027,1004],{}," with the same ",[20,1030,20],{},[20,1032,1009],{},", without a request.",[12,1035,1036,1037,1039,1040,1042,1043,1045,1046,1048,1049,1052,1053,1056,1057,1059],{},"A team that is restricted, still being prepared or gone never stops the others; a ",[20,1038,718],{}," this run cannot wait out pauses the whole key until ",[20,1041,1009],{},". ",[20,1044,949],{}," throws when the key itself is refused, when ",[20,1047,679],{}," cannot be read, or on an error that points to a bug rather than to the team: a ",[20,1050,1051],{},"400",", an unexpected ",[20,1054,1055],{},"404",", a feed that still asks for a full resync right after one, a list or feed that repeats a cursor or leaves one out, or a network failure. No cursor is lost then either; when the error carries ",[20,1058,1009],{},", schedule the next run for that time.",[12,1061,1062,1063,63,1066,1069],{},"Besides ",[20,1064,1065],{},"key",[20,1067,1068],{},"store",", it takes:",[51,1071,1072,1084,1095,1103],{},[54,1073,1074,1077,1078,1080,1081,1083],{},[20,1075,1076],{},"maxWaitMs",": the longest it sleeps in one go, in milliseconds. The default is 60,000. A longer ",[20,1079,731],{},", such as the wait for the daily quota to reset at 00:00 UTC, ends the run with ",[20,1082,1004],{}," instead of keeping your process asleep.",[54,1085,1086,1088,1089,1091,1092,1094],{},[20,1087,1018],{},": how many times one request is retried after a ",[20,1090,718],{}," or a ",[20,1093,745],{},". The default is 3.",[54,1096,1097,1099,1100,1102],{},[20,1098,970],{},": the most feed pages one team reads in one run. There is no limit by default. With a limit, a long initial sync stops with ",[20,1101,966],{},", the key's other teams are synced, and the next run continues from the saved cursor.",[54,1104,1105,59,1107,59,1110,63,1113,1116],{},[20,1106,895],{},[20,1108,1109],{},"baseUrl",[20,1111,1112],{},"sleep",[20,1114,1115],{},"now",": replace them in your tests.",[1118,1119],"dev-snippet",{"file":1120},"sync-team",[28,1122,1124],{"id":1123},"in-memory-store-for-tests","In-memory store for tests",[12,1126,1127,1130,1131,1133],{},[20,1128,1129],{},"MemorySyncStore"," implements every ",[20,1132,899],{}," method in memory. It shows what each method must do and makes your tests deterministic. It loses everything on restart, so replace it with your database before you go live.",[1118,1135],{"file":1136},"memory-store",{"title":126,"searchDepth":1138,"depth":1138,"links":1139},3,[1140,1144,1145,1146,1147,1148,1149,1154,1155,1158,1159,1160,1161,1164],{"id":30,"depth":1141,"text":31,"children":1142},2,[1143],{"id":110,"depth":1138,"text":111},{"id":181,"depth":1141,"text":182},{"id":242,"depth":1141,"text":243},{"id":293,"depth":1141,"text":167},{"id":356,"depth":1141,"text":357},{"id":379,"depth":1141,"text":380},{"id":405,"depth":1141,"text":406,"children":1150},[1151,1152,1153],{"id":409,"depth":1138,"text":410},{"id":464,"depth":1138,"text":465},{"id":486,"depth":1138,"text":487},{"id":534,"depth":1141,"text":535},{"id":573,"depth":1141,"text":421,"children":1156},[1157],{"id":760,"depth":1138,"text":761},{"id":789,"depth":1141,"text":790},{"id":814,"depth":1141,"text":815},{"id":864,"depth":1141,"text":865},{"id":888,"depth":1141,"text":889,"children":1162},[1163],{"id":943,"depth":1138,"text":944},{"id":1123,"depth":1141,"text":1124},"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.",false,"md","en",{},true,"10","\u002Fdevelopers\u002Fsync","---\ntitle: Keep a copy in sync\ndescription: 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.\nsection: guides\norder: 10\ntranslationKey: sync\nlang: en\ndraft: false\n---\n\nThe changes feed, [`GET \u002Fv1\u002Fsync\u002Fchanges`](\u002Fdevelopers\u002Freference-sync#sync.changes), is how you keep your own copy of a team. Your first request copies every record. After that, each request returns only what changed since the last one, so a sync every few minutes is cheap and never misses a change.\n\n::dev-sync-diagram\n::\n\n## How the feed works\n\nThere is one feed per team. A key that reads several teams names the team in the [`Totem-Team` header](\u002Fdevelopers\u002Fauthentication#teams-and-the-totem-team-header) and keeps one cursor per team.\n\nEach item of the feed is the current state of one record, of one `kind`:\n\n- `lead`, `lead_attribution` and `lead_response`: a lead and its two facets;\n- `activity`: a change of a lead's stage, status or assigned members;\n- `call`, `conversation` and `booking`;\n- `board`, with its stages inside it.\n\nA lead's attribution and response travel as kinds of their own, with the lead's id and a `version` of their own. That is why a new message changes the lead's response without changing the lead. Members and bookable services are not in the feed: you [read them separately](#refresh-boards-members-and-services).\n\nItems come in the order Totem recorded the changes, oldest first. An item is never a diff: it carries the whole record as it is now.\n\n### Records and removals\n\nMost items carry the record under `object`. It is the same object the list endpoints return, shortened here:\n\n```json\n{\n  \"kind\": \"call\",\n  \"id\": \"call_m2kUbFcayHlZ5Lgs9xZNmDS3c3Jvk\",\n  \"version\": 2,\n  \"object\": {\n    \"id\": \"call_m2kUbFcayHlZ5Lgs9xZNmDS3c3Jvk\",\n    \"object\": \"call\",\n    \"livemode\": false,\n    \"team\": \"team_r9uYrIW4gSFJJ6hZQ9ip1UIpPaAD1CF7NR\",\n    \"direction\": \"inbound\",\n    \"outcome\": \"answered_ai\",\n    \"version\": 2\n  }\n}\n```\n\nA **removal** carries `removed` instead of `object`. It tells you to delete the record:\n\n```json\n{\n  \"kind\": \"call\",\n  \"id\": \"call_m1pf4APjJWVi3Z7ulyNaW8y3lUi7V\",\n  \"version\": 5,\n  \"removed\": { \"reason\": \"deleted\" }\n}\n```\n\n`reason` is `erased` when the record was erased: for privacy, for example at the person's request, or because someone permanently deleted a lead, a call or a conversation in Totem, which always erases it. It is `deleted` when the record went away in any other way, for example when it no longer belongs to the team's API data, or when Totem now names it with a new id. Handle both the same way, as [Apply a page](#apply-a-page) explains. A record that comes back is a new record with a new id ([Conventions](\u002Fdevelopers\u002Fconventions#ids)), so match people across ids by phone or email with [search](\u002Fdevelopers\u002Freference-leads#leads.search), never by id.\n\n## Plan your storage\n\nA copy built this way can apply every page safely, whatever order the records arrive in:\n\n- **One row per record**, keyed by team, `kind` and `id`, with its `version`. A lead and its two facets share an id, so the `kind` is part of the key.\n- **One tombstone per removal**: the `kind`, `id` and `version` of a record you deleted, without its data.\n- **One cursor per team**, saved together with the page it follows when your database allows it.\n- **Boards, members and bookable services** in lists of their own, replaced as a whole each time you read them.\n- **No foreign keys between records while you apply pages.** A record can name another that has not reached you yet, such as a lead naming a stage of a board, or an activity naming its lead.\n\nThen run one sync per key every 1 to 5 minutes, and watch for a team that keeps being skipped or postponed, or whose cursor has not been saved for a day.\n\n## Run the initial sync\n\nYour first request for a team has no cursor. It starts the **initial sync**: the feed pages through every record of the team, oldest change first.\n\n```bash\ncurl \"https:\u002F\u002Fapi.totemstudio.ai\u002Fv1\u002Fsync\u002Fchanges?limit=100\" \\\n  -H \"Authorization: Bearer $TOTEM_API_KEY\" \\\n  -H \"Totem-Team: $TOTEM_TEAM_ID\"\n```\n\nApply the page, store its `next_cursor`, and ask again with `cursor` set to it. Repeat while `has_more` is `true`. When `has_more` is `false`, your copy is complete. Ask for the largest page, :api-const{name=\"PAGE_LIMIT.max\"} items, so the initial sync takes as few requests as possible.\n\nTwo things are normal during an initial sync. A record that changes while it runs comes again later, with a higher version. And after a retry, you can receive the same page twice. The version check in the next section makes both harmless.\n\n## Apply a page\n\nFor each item of the page, in order:\n\n1. **Skip what you already have.** If you hold this `kind` and `id` with the same or a higher `version`, skip the item. Compare with the versions of your tombstones too.\n2. **Store a record.** If the item has `object`, insert your copy or replace it. For a lead, store its `contact_permissions` before anything else can act on it.\n3. **Apply a removal.** If the item has `removed`, delete your copy, but keep its `kind`, `id` and `version` as a **tombstone**. When the removed record is a lead, also delete its attribution, its response and its activities, and ignore any of them that arrives later.\n\n::dev-callout{type=\"warning\" title=\"Keep the tombstone\"}\nWithout the tombstone, step 1 has nothing to compare with, and an older copy of the record, delivered again by a replayed page, would bring it back. For a record erased at a person's request, that is a privacy incident, not a sync glitch.\n::\n\n## Save the cursor\n\nStore `next_cursor` only after the whole page is saved. If your process stops in between, the next run reads the same page again, and step 1 skips what you already applied. When your database allows it, save the page and its cursor in one transaction.\n\nA cursor is opaque: store it exactly as you received it, and never build or edit one. It belongs to one team. Sent with another team, it gets `400 invalid_request`.\n\nA cursor expires :api-const{name=\"CURSOR_MAX_AGE_DAYS\"} days after the point in the feed it reached. While an initial sync is still running, that age counts from when the initial sync started. A team you poll regularly never reaches it.\n\n## Poll for changes\n\nOnce `has_more` is `false`, ask again with your stored cursor every 1 to 5 minutes. When nothing has changed, you get an empty page with a new `next_cursor`. Store it too: an empty page moves the cursor forward, so the cursor of a quiet team never grows old.\n\nThe feed holds back the last :api-const{name=\"FEED_SETTLE_LAG_MS\" divide=\"1000\"} seconds of changes, so that a change still being written is never skipped. A change shows up in the feed shortly after it happens in Totem.\n\nRun at most one sync process per key and team. Two processes applying pages of the same team can save their cursors out of order.\n\n## Removals, merges and transfers\n\n### Removals\n\nApply a removal as soon as you receive it: delete the record from your copy and from every system you copied it to. Removals stay in the feed for :api-const{name=\"REMOVAL_RETENTION_DAYS\"} days, longer than a cursor lives, so a sync that keeps reading the feed successfully never misses one. While the API refuses a team, it delivers nothing for that team, removals included; [Recover from errors](#recover-from-errors) says what to do then.\n\nWhen a lead is removed, its attribution, response and activities are removed with it, and the feed delivers their removals too. What happens to the lead's other records depends on the `reason`:\n\n- **`erased`**, which includes a lead deleted permanently in Totem: its calls and conversations are erased too, and so are the archived leads whose `merged_into` names it, with their own calls and conversations. Each record arrives as a removal of its own, with `reason: \"erased\"`. Its bookings are not removed: they arrive again with `lead` set to `null`.\n- **`deleted`**: its calls, conversations and bookings are not removed. They arrive again with `lead` set to `null`.\n\n### Merges\n\nWhen two leads of the same person are merged in Totem, the lead that is not kept is archived, not removed. Its `archive_reason` is `merged`, and its `merged_into` names the lead that was kept. Link your copy to that lead instead of creating the person twice. The lead that is kept also takes the stricter contact permissions of the two. `merged_into` is `null` when the lead it named no longer exists.\n\n### Transfers\n\nWhen a lead moves to another team of the same organization, the original lead is archived with `archive_reason` set to `family_transfer`, and `transferred_to` says where it went:\n\n- `transferred_to.team` is always set. It can be a team your key does not read.\n- `transferred_to.lead` is the lead in that team, or `null` when Totem cannot name it.\n\nIf your key reads the destination team, the new lead arrives in that team's feed. If it does not, [`GET \u002Fv1\u002Fleads\u002F{id}`](\u002Fdevelopers\u002Freference-leads#leads.get) on the destination lead answers `404 resource_missing`. Keep the archived lead as the record of where the person went, and do not guess.\n\n## Contact permissions first\n\nBefore any of your systems contacts a lead, store its `contact_permissions` and apply them:\n\n- `do_not_contact: true`: the person asked never to be contacted. Stop every channel.\n- `email_opt_out: true`: the person unsubscribed from marketing and follow-up email.\n- `whatsapp_consent: false`: the person refused WhatsApp. `null` means nobody recorded an answer, so never treat it as consent.\n\nA lead in the feed is data, not permission to contact the person. The legal basis for contacting someone stays your responsibility.\n\nThe API is read-only, so Totem cannot learn about an opt-out recorded in your systems. When a person opts out there, make sure it is also recorded on the lead in Totem, so that Totem stops contacting them too.\n\n## Recover from errors\n\nWhen a request for a page fails, do not save a cursor. The next request with the same cursor asks for the same page again.\n\n| Answer | What it means | What to do |\n|---|---|---|\n| `410 sync_cursor_expired` | The cursor is more than :api-const{name=\"CURSOR_MAX_AGE_DAYS\"} days old. | Run a [full resync](#full-resync). |\n| `409 resync_required` | Totem cannot continue from this cursor, for example after restoring a backup. | Run a [full resync](#full-resync). |\n| `409 capture_not_ready` | Totem is still preparing this team's data. It happens once, when a team is first added to a key. | Keep everything and try again in a few minutes. Never resync for it. The key's other teams keep syncing. |\n| `403` | Access is refused for now: for example, the team is restricted (`team_state_restricted`) or the endpoint is switched off (`capability_disabled`). | Keep the team's data and cursor, and skip the team in this run. The [Errors](\u002Fdevelopers\u002Ferrors) page says, code by code, whether waiting helps. `plan_not_entitled` refuses the whole key, so the run stops at `GET \u002Fv1\u002Fteams`. While access is refused, the feed delivers nothing for the team, removals included: if the refusal lasts longer than your retention policy allows, delete the team's copy. |\n| `404 team_not_accessible` | The key may no longer read this team. | Read [`GET \u002Fv1\u002Fteams`](\u002Fdevelopers\u002Freference-account#teams.list). If the team is not listed, delete its data and its cursor. If it is, keep everything and try again on the next run. |\n| `400 invalid_request` | The cursor was changed, or it belongs to another team. | Use the cursor you stored for this team. |\n| `429` | You reached the [rate limit or the daily quota](\u002Fdevelopers\u002Frate-limits). | Wait the seconds in `Retry-After`, then retry with the same cursor. A `429` from the firewall has no `Retry-After`: back off exponentially. |\n| `5xx` | Something failed inside Totem. | Retry with exponential backoff. |\n\nEvery code is explained on the [Errors](\u002Fdevelopers\u002Ferrors) page.\n\n### Full resync\n\nA full resync rebuilds your copy of one team, by **mark and sweep**:\n\n1. Forget the versions and the cursor you stored for the team. Keep the records themselves: your copy stays usable while the resync runs.\n2. Run the initial sync again, without a cursor. Apply each page as usual, and mark every record the feed delivers.\n3. When `has_more` is `false`, delete every record of the team that you did not mark.\n\nStore the marks durably. If the process stops halfway, the next run must continue the same resync, not sweep with half the marks. Forgetting the versions first matters too: after a restore, the feed can deliver lower versions than the ones you held.\n\n## When teams change\n\nRead [`GET \u002Fv1\u002Fteams`](\u002Fdevelopers\u002Freference-account#teams.list) at the start of every run. It lists every team your key reads, including teams that are restricted for now.\n\n- A team you have not synced yet: run its initial sync.\n- A team you hold that is no longer listed: it has left the key. Delete its data and its cursor.\n- A listed team that answers `403`: keep it, as above.\n\n## Refresh boards, members and services\n\nMembers and bookable services are not in the feed. Boards are, but a lead can name a new stage before the change to its board reaches you. So read all three:\n\n- when you start syncing a team;\n- when an item names a board, stage, member or service you do not know;\n- at least once a day.\n\nA record can keep naming a member who left the team, or a board, stage or service that was deleted. If a refresh does not find an id, remember it and do not refresh for it again before the daily refresh.\n\n[`GET \u002Fv1\u002Fboards`](\u002Fdevelopers\u002Freference-leads#boards.list) pages like any list. [`GET \u002Fv1\u002Fmembers`](\u002Fdevelopers\u002Freference-account#members.list) and [`GET \u002Fv1\u002Fbooking_services`](\u002Fdevelopers\u002Freference-bookings#booking_services.list) return the whole list in one response and take no `limit` or `cursor`. Replace your copy of each list as a whole. A member who leaves the team, or a service that is deleted, drops out of its list while older records keep naming its id: if you show names for older records, keep the name from an earlier read. Disabled members and services stay listed, with nothing that marks them as disabled.\n\n## Pace\n\nA key reads up to :api-const{name=\"SYNC_VELOCITY.perMinute\"} items a minute, so the initial sync of a large team takes a while. [Rate limits](\u002Fdevelopers\u002Frate-limits) has the numbers, an estimate of how long an initial sync takes, and why the numbers are approximate during the Beta. When you get a `429`, wait for `Retry-After` and continue with the same cursor: nothing is lost. The limits belong to the key, so a `429` pauses every team the key reads, not just the one you were syncing.\n\n## Tested reference client\n\nThe TypeScript below is a complete sync client that does everything this page describes. Totem runs it in its own tests against a simulated feed, with crashes, replayed pages, removals, resyncs and errors; the code shown here is that exact file. It has no dependencies and needs only `fetch`: compile it with your TypeScript build, or run it directly on Node.js 22.18 or later. Copy it to your server and implement `SyncStore` on your database.\n\n::dev-callout{type=\"note\" title=\"Reference client or SDK?\"}\nFor a sync, start from this client: it implements the tombstones, the mark and sweep of a full resync and one outcome per team. The [TypeScript SDK](\u002Fdevelopers\u002Ftypescript) gives typed requests and pagination; its own sync example handles one team and leaves the [full resync](#full-resync) to your store, so implement it as this page explains.\n::\n\nRun it from a scheduled job every 1 to 5 minutes, one run per key at a time. The example is an ES module; under Node.js module resolution, import the file with the `.js` extension your build emits, or with `.ts` when Node.js runs the TypeScript directly:\n\n```ts\nimport { syncKey } from '.\u002FsyncTeam.js'\n\nconst outcomes = await syncKey({ key: process.env.TOTEM_API_KEY!, store })\n```\n\nThe key must never reach a browser. Every `SyncStore` method must write durably before its promise resolves, and writes for one team must not run in parallel.\n\n### Options and outcomes\n\n`syncKey` reads `GET \u002Fv1\u002Fteams`, deletes the teams that left the key, and syncs every listed team in turn. It returns one outcome per team:\n\n- `synced`: the team is caught up and its cursor is saved.\n- `partial`: the team read `maxPagesPerTeam` pages and is not caught up yet. Its cursor is saved, and the next run continues from it.\n- `dropped`: the team left the key, and its copy and cursor are deleted.\n- `skipped`, with the `code`: the team cannot be read right now (`403`, `capture_not_ready`, or a `team_not_accessible` that `GET \u002Fv1\u002Fteams` does not confirm). Its copy and cursor are kept, and the next run tries again.\n- `retry_later`, with the `code` and `retryAt` (epoch milliseconds): the API asked for a longer wait than this run allows, or a `429` or `5xx` was still failing after `maxRetries` retries. The copy and cursor are kept. Do not sync that team again before `retryAt`. A `429` belongs to the key, not to one team, so once one sends the key away, every team this run has not reached yet is reported `retry_later` with the same `code` and `retryAt`, without a request.\n\nA team that is restricted, still being prepared or gone never stops the others; a `429` this run cannot wait out pauses the whole key until `retryAt`. `syncKey` throws when the key itself is refused, when `GET \u002Fv1\u002Fteams` cannot be read, or on an error that points to a bug rather than to the team: a `400`, an unexpected `404`, a feed that still asks for a full resync right after one, a list or feed that repeats a cursor or leaves one out, or a network failure. No cursor is lost then either; when the error carries `retryAt`, schedule the next run for that time.\n\nBesides `key` and `store`, it takes:\n\n- `maxWaitMs`: the longest it sleeps in one go, in milliseconds. The default is 60,000. A longer `Retry-After`, such as the wait for the daily quota to reset at 00:00 UTC, ends the run with `retry_later` instead of keeping your process asleep.\n- `maxRetries`: how many times one request is retried after a `429` or a `5xx`. The default is 3.\n- `maxPagesPerTeam`: the most feed pages one team reads in one run. There is no limit by default. With a limit, a long initial sync stops with `partial`, the key's other teams are synced, and the next run continues from the saved cursor.\n- `fetch`, `baseUrl`, `sleep` and `now`: replace them in your tests.\n\n::dev-snippet{file=\"sync-team\"}\n::\n\n## In-memory store for tests\n\n`MemorySyncStore` implements every `SyncStore` method in memory. It shows what each method must do and makes your tests deterministic. It loses everything on restart, so replace it with your database before you go live.\n\n::dev-snippet{file=\"memory-store\"}\n::\n","guides",{"title":6,"description":1165},"developers\u002Fsync","sync","pG6Q_0R4sl0TUoPV6A1zKqpjo3UeHfZIE4Ro6tuzrbA",[1180,1195,1216,1259],{"section":1181,"items":1182},"start",[1183,1187,1191],{"path":1184,"title":1185,"description":1186},"\u002Fdevelopers","Overview","Read the leads, calls, conversations and bookings of your Totem teams from your own systems, and keep a copy in sync.",{"path":1188,"title":1189,"description":1190},"\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":1192,"title":1193,"description":1194},"\u002Fdevelopers\u002Fauthentication","Authentication","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.",{"section":1174,"items":1196},[1197,1199,1200,1204,1208,1212],{"path":910,"title":911,"description":1198},"Use the official TypeScript client to read your Totem data, paginate lists and handle API errors from your server.",{"path":1172,"title":6,"description":1165},{"path":1201,"title":1202,"description":1203},"\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":1205,"title":1206,"description":1207},"\u002Fdevelopers\u002Fcoverage","Data coverage","What Totem records, since when, and how to tell an empty value from a fact that was never recorded.",{"path":1209,"title":1210,"description":1211},"\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":1213,"title":1214,"description":1215},"\u002Fdevelopers\u002Fsecurity","Security and personal data","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":1217,"items":1218},"reference",[1219,1222,1224,1226,1231,1236,1241,1246,1251,1255],{"path":1220,"title":172,"description":1221},"\u002Fdevelopers\u002Fconventions","The notation these docs use, and the rules every response follows: ids, timestamps, empty values, amounts, enums, versions, stability, errors and untrusted text.",{"path":670,"title":671,"description":1223},"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":724,"title":875,"description":1225},"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":1227,"title":1228,"description":1229,"apiTag":1230},"\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":1232,"title":1233,"description":1234,"apiTag":1235},"\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":1237,"title":1238,"description":1239,"apiTag":1240},"\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":1242,"title":1243,"description":1244,"apiTag":1245},"\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":1247,"title":1248,"description":1249,"apiTag":1250},"\u002Fdevelopers\u002Freference-bookings","Bookings and services","List and fetch a team's bookings, and list the services its customers can book.","bookings",{"path":1252,"title":1253,"description":1254,"apiTag":1177},"\u002Fdevelopers\u002Freference-sync","Changes feed","One endpoint that copies a team and then returns every change, page by page.",{"path":1256,"title":1257,"description":1258},"\u002Fdevelopers\u002Fobjects","All objects","Every object the API returns, field by field, with an example of each.",{"section":1260,"items":1261},"resources",[1262,1266,1270,1274],{"path":1263,"title":1264,"description":1265},"\u002Fdevelopers\u002Fchangelog","Changelog","Changes to the Totem API, newest first, each under the date it shipped.",{"path":1267,"title":1268,"description":1269},"\u002Fdevelopers\u002Fsupport","Support","How to get help with the Totem API, what to include in a request, and what to do if a key leaks.",{"path":1271,"title":1272,"description":1273},"\u002Fdevelopers\u002Fglossary","Glossary","The terms these docs use, defined in one place, each with a link to the page that explains it.",{"path":1275,"title":1276,"description":1277},"\u002Fdevelopers\u002Ffaq","FAQ","Short answers to the questions developers ask most about the Totem API, with links to the details.","\u002Fdevelopers\u002Fes\u002Fsync",{"ACTIVATION_MARGIN_MS":1280,"ADMIN_ACCESS_DISCLOSURE_VERSION":1281,"AUDIT_RETENTION_DAYS":1282,"AUTH_FAILURE_LIMITS":1283,"CAPTURE_LAG_FLOOR_MS":1287,"CAPTURE_LAG_P99_MS":7,"CURSOR_MAX_AGE_DAYS":1288,"DIRTY_TTL_DAYS":1288,"FAMILY_TEAM_SCOPE":1289,"FAMILY_TEAM_SCOPE_OPTIONS":1290,"FEED_SETTLE_LAG_MS":1292,"GATE_REFRESH_MS":1280,"INTEGRATION_NAME_LENGTH":1293,"INTEGRATION_PURPOSE_LENGTH":1295,"KEY_CACHE_TTL_MS":1298,"KEY_STATS_RETENTION_DAYS":1299,"LEAD_RESPONSE_READ_BUDGET":1300,"LIST_SCAN_MAX":1303,"MAX_FAMILY_INTEGRATION_TEAMS":1288,"MAX_INTEGRATIONS_PER_FAMILY":1304,"MAX_IP_ALLOWLIST":1285,"PAGE_LIMIT":1305,"PLATFORM_CONFIG_TTL_MS":1308,"REMOVAL_RETENTION_DAYS":1309,"REQUEST_BODY_MAX_BYTES":1310,"ROLL_GRACE_DEFAULT_HOURS":1311,"ROLL_GRACE_HOURS":1312,"SEARCH_MAX_RESULTS":1304,"SENSITIVE_SECTOR_POLICY":1315,"SENSITIVE_SECTOR_POLICY_OPTIONS":1316,"SYNC_DAILY_BUDGET":1318,"SYNC_VELOCITY":1320,"TEAM_CACHE_TTL_MS":1322},60000,"2026-10-17",365,{"perIpPerMinute":1284,"perLookupIdPerMinute":1285,"banMs":1286},60,20,600000,300000,30,"explicit_list",[1289,1291],"all_family_teams",10000,{"min":1141,"max":1294},80,{"min":1296,"max":1297},1,280,5000,90,{"conversations":1285,"messageReads":1301,"calls":1302},200,50,1000,10,{"default":1306,"max":1307},25,100,15000,45,16384,24,[1313,1296,1311,1314],0,168,"exclude",[1315,1317],"allow",{"floor":1319,"recordsMultiplier":1138},250000,{"perMinute":1321,"burst":1292},2000,30000,"1.0a","guide",[1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345],{"id":30,"text":31,"depth":1141},{"id":110,"text":111,"depth":1138},{"id":181,"text":182,"depth":1141},{"id":242,"text":243,"depth":1141},{"id":293,"text":167,"depth":1141},{"id":356,"text":357,"depth":1141},{"id":379,"text":380,"depth":1141},{"id":405,"text":406,"depth":1141},{"id":409,"text":410,"depth":1138},{"id":464,"text":465,"depth":1138},{"id":486,"text":487,"depth":1138},{"id":534,"text":535,"depth":1141},{"id":573,"text":421,"depth":1141},{"id":760,"text":761,"depth":1138},{"id":789,"text":790,"depth":1141},{"id":814,"text":815,"depth":1141},{"id":864,"text":865,"depth":1141},{"id":888,"text":889,"depth":1141},{"id":943,"text":944,"depth":1138},{"id":1123,"text":1124,"depth":1141},{"path":910,"title":911,"section":1174},{"path":1201,"title":1202,"section":1174},[],1792502790012]