[{"data":1,"prerenderedAt":1354},["ShallowReactive",2],{"developers:\u002Fdevelopers\u002Fes\u002Fsync":3},{"doc":4,"lang":1173,"nav":1184,"alternatePath":1283,"policy":1284,"specVersion":1328,"reference":7,"layout":1329,"toc":1330,"prev":1351,"next":1352,"operations":1353,"overview":7,"heroExample":7},{"id":5,"title":6,"apiTag":7,"body":8,"description":1170,"draft":1171,"extension":1172,"lang":1173,"meta":1174,"navTitle":7,"navigation":1175,"order":1176,"path":1177,"rawbody":1178,"section":1179,"seo":1180,"stem":1181,"translationKey":1182,"version":7,"__hash__":1183},"developers_es\u002Fdevelopers\u002Fes\u002Fsync.md","Sincroniza una copia",null,{"type":9,"value":10,"toc":1142},"minimark",[11,24,27,32,43,50,92,104,107,112,119,129,144,150,179,183,186,240,243,247,254,262,292,295,298,301,353,361,365,371,377,384,388,399,407,410,414,418,430,436,470,474,491,495,508,525,539,543,549,572,575,578,581,584,759,764,768,774,789,792,796,803,817,821,824,835,838,867,871,891,895,906,921,932,940,946,950,959,1040,1066,1076,1122,1126,1130,1139],[12,13,14,15,23],"p",{},"El feed de cambios, ",[16,17,19],"a",{"href":18},"\u002Fdevelopers\u002Fes\u002Freference-sync#sync.changes",[20,21,22],"code",{},"GET \u002Fv1\u002Fsync\u002Fchanges",", es la forma de tener tu propia copia de un equipo. Tu primera petición copia todos los registros. A partir de ahí, cada petición devuelve solo lo que ha cambiado desde la anterior, así que sincronizar cada pocos minutos cuesta poco y nunca se pierde un cambio.",[25,26],"dev-sync-diagram",{},[28,29,31],"h2",{"id":30},"cómo-funciona-el-feed","Cómo funciona el feed",[12,33,34,35,42],{},"Hay un feed por equipo. Una clave que lee varios equipos indica el equipo en la ",[16,36,38,39],{"href":37},"\u002Fdevelopers\u002Fes\u002Fauthentication#equipos-y-la-cabecera-totem-team","cabecera ",[20,40,41],{},"Totem-Team"," y guarda un cursor por equipo.",[12,44,45,46,49],{},"Cada elemento del feed es el estado actual de un registro, de un tipo (",[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"," y ",[20,65,66],{},"lead_response",": un lead y sus dos facetas;",[54,69,70,73],{},[20,71,72],{},"activity",": un cambio de etapa, de estado o de miembros asignados de un lead;",[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",", con sus etapas dentro.",[12,93,94,95,98,99,103],{},"La atribución y la respuesta de un lead viajan como tipos propios, con el identificador del lead y su propia ",[20,96,97],{},"version",". Por eso un mensaje nuevo cambia la respuesta del lead sin cambiar el lead. Los miembros y los servicios reservables no están en el feed: los ",[16,100,102],{"href":101},"#actualiza-tableros-miembros-y-servicios","lees aparte",".",[12,105,106],{},"Los elementos llegan en el orden en que Totem registró los cambios, del más antiguo al más reciente. Un elemento nunca es una diferencia: trae el registro entero tal como está ahora.",[108,109,111],"h3",{"id":110},"registros-y-eliminaciones","Registros y eliminaciones",[12,113,114,115,118],{},"La mayoría de los elementos traen el registro en ",[20,116,117],{},"object",". Es el mismo objeto que devuelven los endpoints de lista; aquí aparece abreviado:",[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],{},"Una ",[133,134,135],"strong",{},"eliminación"," trae ",[20,138,139],{},"removed"," en lugar de ",[20,142,117],{},". Te indica que borres el registro:",[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"," es ",[20,157,158],{},"erased"," cuando el registro se suprimió: por privacidad, por ejemplo a petición de la persona, o porque alguien eliminó definitivamente en Totem un lead, una llamada o una conversación, lo que siempre los suprime. Es ",[20,161,162],{},"deleted"," cuando el registro desapareció de cualquier otra forma, por ejemplo porque ya no forma parte de los datos de la API del equipo o porque Totem lo identifica ahora con un identificador nuevo. Trata los dos casos igual, como explica ",[16,165,167],{"href":166},"#aplica-una-p%C3%A1gina","Aplica una página",". Un registro que vuelve es un registro nuevo con un identificador nuevo (",[16,170,172],{"href":171},"\u002Fdevelopers\u002Fes\u002Fconventions#identificadores","Convenciones","), así que relaciona a las personas entre identificadores por teléfono o correo electrónico con la ",[16,175,177],{"href":176},"\u002Fdevelopers\u002Fes\u002Freference-leads#leads.search","búsqueda",", nunca por identificador.",[28,180,182],{"id":181},"planifica-tu-almacenamiento","Planifica tu almacenamiento",[12,184,185],{},"Una copia construida así puede aplicar cada página sin riesgos, lleguen los registros en el orden que lleguen:",[51,187,188,207,222,228,234],{},[54,189,190,193,194,196,197,200,201,203,204,206],{},[133,191,192],{},"Una fila por registro",", con la clave formada por el equipo, el ",[20,195,48],{}," y el ",[20,198,199],{},"id",", y con su ",[20,202,97],{},". Un lead y sus dos facetas comparten identificador, así que el ",[20,205,48],{}," forma parte de la clave.",[54,208,209,212,213,215,216,218,219,221],{},[133,210,211],{},"Una marca por eliminación",": el ",[20,214,48],{},", el ",[20,217,199],{}," y la ",[20,220,97],{}," de un registro que borraste, sin sus datos.",[54,223,224,227],{},[133,225,226],{},"Un cursor por equipo",", guardado junto con la página a la que sigue cuando tu base de datos lo permita.",[54,229,230,233],{},[133,231,232],{},"Los tableros, los miembros y los servicios reservables"," en listas propias, que sustituyes enteras cada vez que las lees.",[54,235,236,239],{},[133,237,238],{},"Ninguna clave foránea entre registros mientras aplicas páginas."," Un registro puede indicar otro que aún no te ha llegado, como un lead que indica una etapa de un tablero o una actividad que indica su lead.",[12,241,242],{},"Después, ejecuta una sincronización por clave cada 1 a 5 minutos, y vigila los equipos que se omiten o se aplazan una y otra vez, o cuyo cursor lleva un día sin guardarse.",[28,244,246],{"id":245},"haz-la-carga-inicial","Haz la carga inicial",[12,248,249,250,253],{},"Tu primera petición de un equipo no lleva cursor. Empieza la ",[133,251,252],{},"carga inicial",": el feed recorre todos los registros del equipo, del cambio más antiguo al más reciente.",[120,255,260],{"className":256,"code":258,"language":259,"meta":126},[257],"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,261,258],{"__ignoreMap":126},[12,263,264,265,268,269,272,273,276,277,280,281,276,283,286,287,291],{},"Aplica la página, guarda su ",[20,266,267],{},"next_cursor"," y vuelve a pedir con ",[20,270,271],{},"cursor"," igual a ese valor. Repite mientras ",[20,274,275],{},"has_more"," sea ",[20,278,279],{},"true",". Cuando ",[20,282,275],{},[20,284,285],{},"false",", tu copia estará completa. Pide la página más grande, de ",[288,289],"api-const",{"name":290},"PAGE_LIMIT.max"," elementos, para que la carga inicial necesite las menos peticiones posibles.",[12,293,294],{},"Durante una carga inicial hay dos cosas normales. Un registro que cambia mientras se ejecuta vuelve a llegar más tarde, con una versión mayor. Y, tras un reintento, puedes recibir la misma página dos veces. La comprobación de versiones del apartado siguiente hace que ninguna de las dos cause problemas.",[28,296,167],{"id":297},"aplica-una-página",[12,299,300],{},"Para cada elemento de la página, en orden:",[302,303,304,319,332],"ol",{},[54,305,306,309,310,312,313,315,316,318],{},[133,307,308],{},"Omite lo que ya tienes."," Si ya tienes ese ",[20,311,48],{}," e ",[20,314,199],{}," con la misma ",[20,317,97],{}," o una mayor, omite el elemento. Compara también con las versiones de tus marcas de eliminación.",[54,320,321,324,325,327,328,331],{},[133,322,323],{},"Guarda un registro."," Si el elemento trae ",[20,326,117],{},", inserta tu copia o sustitúyela. Si es un lead, guarda sus ",[20,329,330],{},"contact_permissions"," antes de que nada pueda actuar sobre él.",[54,333,334,324,337,339,340,342,343,345,346,348,349,352],{},[133,335,336],{},"Aplica una eliminación.",[20,338,139],{},", borra tu copia, pero conserva su ",[20,341,48],{},", su ",[20,344,199],{}," y su ",[20,347,97],{}," como ",[133,350,351],{},"marca de eliminación",". Si el registro eliminado es un lead, borra también su atribución, su respuesta y sus actividades, e ignora cualquiera de ellas que llegue después.",[354,355,358],"dev-callout",{"title":356,"type":357},"Conserva la marca de eliminación","warning",[12,359,360],{},"Sin la marca, el paso 1 no tiene con qué comparar, y una copia anterior del registro, entregada otra vez en una página repetida, lo haría reaparecer. Si el registro se suprimió a petición de una persona, eso es un incidente de privacidad, no un fallo de sincronización.",[28,362,364],{"id":363},"guarda-el-cursor","Guarda el cursor",[12,366,367,368,370],{},"Guarda ",[20,369,267],{}," solo cuando hayas guardado la página entera. Si tu proceso se detiene entre medias, la siguiente ejecución vuelve a leer la misma página, y el paso 1 omite lo que ya aplicaste. Si tu base de datos lo permite, guarda la página y su cursor en una misma transacción.",[12,372,373,374,103],{},"Un cursor es opaco: guárdalo tal como lo recibiste y no construyas ni modifiques ninguno. Pertenece a un solo equipo. Si lo envías con otro equipo, recibe ",[20,375,376],{},"400 invalid_request",[12,378,379,380,383],{},"Un cursor caduca ",[288,381],{"name":382},"CURSOR_MAX_AGE_DAYS"," días después del punto del feed al que llegó. Mientras una carga inicial sigue en curso, esa antigüedad se cuenta desde que empezó la carga. El cursor de un equipo que consultas con regularidad nunca llega a caducar.",[28,385,387],{"id":386},"consulta-los-cambios-periódicamente","Consulta los cambios periódicamente",[12,389,390,391,276,393,395,396,398],{},"Cuando ",[20,392,275],{},[20,394,285],{},", vuelve a pedir con el cursor guardado cada 1 a 5 minutos. Si no ha cambiado nada, recibes una página vacía con un ",[20,397,267],{}," nuevo. Guárdalo también: una página vacía hace avanzar el cursor, así que el cursor de un equipo con poca actividad nunca envejece.",[12,400,401,402,406],{},"El feed retiene los últimos ",[288,403],{"name":404,"divide":405},"FEED_SETTLE_LAG_MS","1000"," segundos de cambios, para no saltarse nunca un cambio que aún se está escribiendo. Un cambio aparece en el feed poco después de producirse en Totem.",[12,408,409],{},"Sincroniza cada combinación de clave y equipo desde un solo proceso a la vez. Dos procesos que aplican páginas del mismo equipo pueden guardar sus cursores en un orden equivocado.",[28,411,413],{"id":412},"eliminaciones-fusiones-y-traspasos","Eliminaciones, fusiones y traspasos",[108,415,417],{"id":416},"eliminaciones","Eliminaciones",[12,419,420,421,424,425,429],{},"Aplica cada eliminación en cuanto la recibas: borra el registro de tu copia y de todos los sistemas a los que lo copiaste. Las eliminaciones siguen en el feed durante ",[288,422],{"name":423},"REMOVAL_RETENTION_DAYS"," días, más de lo que dura un cursor, así que una sincronización que sigue leyendo el feed con éxito nunca se pierde ninguna. Mientras la API rechaza un equipo, no entrega nada de él, tampoco eliminaciones; en ",[16,426,428],{"href":427},"#recup%C3%A9rate-de-los-errores","Recupérate de los errores"," se explica qué hacer entonces.",[12,431,432,433,435],{},"Cuando se elimina un lead, su atribución, su respuesta y sus actividades se eliminan con él, y el feed entrega también sus eliminaciones. Lo que ocurre con los demás registros del lead depende de ",[20,434,154],{},":",[51,437,438,459],{},[54,439,440,444,445,448,449,452,453,455,456,103],{},[133,441,442],{},[20,443,158],{},", también cuando alguien elimina definitivamente el lead en Totem: sus llamadas y conversaciones también se suprimen, igual que los leads archivados cuyo ",[20,446,447],{},"merged_into"," lo indica, con sus propias llamadas y conversaciones. Cada registro llega como una eliminación propia, con ",[20,450,451],{},"reason: \"erased\"",". Sus citas no se eliminan: vuelven a llegar con ",[20,454,58],{}," igual a ",[20,457,458],{},"null",[54,460,461,465,466,455,468,103],{},[133,462,463],{},[20,464,162],{},": sus llamadas, conversaciones y citas no se eliminan. Vuelven a llegar con ",[20,467,58],{},[20,469,458],{},[108,471,473],{"id":472},"fusiones","Fusiones",[12,475,476,477,155,480,345,483,485,486,155,488,490],{},"Cuando en Totem se fusionan dos leads de la misma persona, el lead que no se conserva queda archivado, no eliminado. Su ",[20,478,479],{},"archive_reason",[20,481,482],{},"merged",[20,484,447],{}," indica el lead que se conservó. Enlaza tu copia con ese lead en lugar de crear a la persona dos veces. El lead que se conserva se queda además con los permisos de contacto más restrictivos de los dos. ",[20,487,447],{},[20,489,458],{}," cuando el lead que indicaba ya no existe.",[108,492,494],{"id":493},"traspasos","Traspasos",[12,496,497,498,455,500,503,504,507],{},"Cuando un lead pasa a otro equipo de la misma organización, el lead original queda archivado con ",[20,499,479],{},[20,501,502],{},"family_transfer",", y ",[20,505,506],{},"transferred_to"," indica adónde fue:",[51,509,510,516],{},[54,511,512,515],{},[20,513,514],{},"transferred_to.team"," siempre tiene valor. Puede ser un equipo que tu clave no lee.",[54,517,518,521,522,524],{},[20,519,520],{},"transferred_to.lead"," es el lead en ese equipo, o ",[20,523,458],{}," cuando Totem no puede indicarlo.",[12,526,527,528,534,535,538],{},"Si tu clave lee el equipo de destino, el lead nuevo llega en el feed de ese equipo. Si no lo lee, ",[16,529,531],{"href":530},"\u002Fdevelopers\u002Fes\u002Freference-leads#leads.get",[20,532,533],{},"GET \u002Fv1\u002Fleads\u002F{id}"," sobre el lead de destino responde ",[20,536,537],{},"404 resource_missing",". Conserva el lead archivado como constancia de adónde fue la persona, y no intentes adivinarlo.",[28,540,542],{"id":541},"primero-los-permisos-de-contacto","Primero, los permisos de contacto",[12,544,545,546,548],{},"Antes de que cualquiera de tus sistemas contacte con un lead, guarda sus ",[20,547,330],{}," y aplícalos:",[51,550,551,557,563],{},[54,552,553,556],{},[20,554,555],{},"do_not_contact: true",": la persona pidió que no se la contacte nunca. No uses ningún canal.",[54,558,559,562],{},[20,560,561],{},"email_opt_out: true",": la persona se dio de baja de los correos comerciales y de seguimiento.",[54,564,565,568,569,571],{},[20,566,567],{},"whatsapp_consent: false",": la persona rechazó WhatsApp. ",[20,570,458],{}," significa que nadie registró su respuesta, así que nunca lo trates como un consentimiento.",[12,573,574],{},"Que un lead aparezca en el feed es un dato, no un permiso para contactar con la persona. La base legal para contactar con alguien sigue siendo responsabilidad tuya.",[12,576,577],{},"La API es de solo lectura, así que Totem no puede conocer una baja registrada en tus sistemas. Cuando una persona se dé de baja en ellos, asegúrate de que también quede registrada en el lead en Totem, para que Totem deje de contactarla.",[28,579,428],{"id":580},"recupérate-de-los-errores",[12,582,583],{},"Cuando falla la petición de una página, no guardes ningún cursor. La siguiente petición con el mismo cursor vuelve a pedir la misma página.",[585,586,587,603],"table",{},[588,589,590],"thead",{},[591,592,593,597,600],"tr",{},[594,595,596],"th",{},"Respuesta",[594,598,599],{},"Qué significa",[594,601,602],{},"Qué hacer",[604,605,606,627,641,654,688,707,719,746],"tbody",{},[591,607,608,614,620],{},[609,610,611],"td",{},[20,612,613],{},"410 sync_cursor_expired",[609,615,616,617,619],{},"El cursor tiene más de ",[288,618],{"name":382}," días.",[609,621,622,623,103],{},"Haz una ",[16,624,626],{"href":625},"#resincronizaci%C3%B3n-completa","resincronización completa",[591,628,629,634,637],{},[609,630,631],{},[20,632,633],{},"409 resync_required",[609,635,636],{},"Totem no puede continuar desde este cursor, por ejemplo tras restaurar una copia de seguridad.",[609,638,622,639,103],{},[16,640,626],{"href":625},[591,642,643,648,651],{},[609,644,645],{},[20,646,647],{},"409 capture_not_ready",[609,649,650],{},"Totem aún está preparando los datos de este equipo. Ocurre una vez, cuando un equipo se añade por primera vez a una clave.",[609,652,653],{},"Conserva todo y vuelve a intentarlo en unos minutos. Nunca resincronices por esto. Los demás equipos de la clave siguen sincronizándose.",[591,655,656,661,672],{},[609,657,658],{},[20,659,660],{},"403",[609,662,663,664,667,668,671],{},"El acceso está denegado por ahora: por ejemplo, el equipo está restringido (",[20,665,666],{},"team_state_restricted",") o el endpoint está desactivado (",[20,669,670],{},"capability_disabled",").",[609,673,674,675,679,680,683,684,687],{},"Conserva los datos y el cursor del equipo, y omítelo en esta ejecución. La página de ",[16,676,678],{"href":677},"\u002Fdevelopers\u002Fes\u002Ferrors","Errores"," indica, código a código, si sirve de algo esperar. ",[20,681,682],{},"plan_not_entitled"," rechaza la clave entera, así que la ejecución se detiene en ",[20,685,686],{},"GET \u002Fv1\u002Fteams",". Mientras el acceso está denegado, el feed no entrega nada del equipo, tampoco eliminaciones: si la denegación dura más de lo que permite tu política de conservación, borra la copia del equipo.",[591,689,690,695,698],{},[609,691,692],{},[20,693,694],{},"404 team_not_accessible",[609,696,697],{},"Puede que la clave ya no lea este equipo.",[609,699,700,701,706],{},"Lee ",[16,702,704],{"href":703},"\u002Fdevelopers\u002Fes\u002Freference-account#teams.list",[20,705,686],{},". Si el equipo no aparece, borra sus datos y su cursor. Si aparece, conserva todo y vuelve a intentarlo en la siguiente ejecución.",[591,708,709,713,716],{},[609,710,711],{},[20,712,376],{},[609,714,715],{},"El cursor se ha modificado o es de otro equipo.",[609,717,718],{},"Usa el cursor que guardaste para este equipo.",[591,720,721,726,733],{},[609,722,723],{},[20,724,725],{},"429",[609,727,728,729,103],{},"Has alcanzado el ",[16,730,732],{"href":731},"\u002Fdevelopers\u002Fes\u002Frate-limits","límite de frecuencia o la cuota diaria",[609,734,735,736,739,740,742,743,745],{},"Espera los segundos que indique ",[20,737,738],{},"Retry-After"," y repite con el mismo cursor. Un ",[20,741,725],{}," del cortafuegos no trae ",[20,744,738],{},": reintenta con espera exponencial.",[591,747,748,753,756],{},[609,749,750],{},[20,751,752],{},"5xx",[609,754,755],{},"Algo ha fallado dentro de Totem.",[609,757,758],{},"Reintenta con espera exponencial.",[12,760,761,762,103],{},"Todos los códigos se explican en la página de ",[16,763,678],{"href":677},[108,765,767],{"id":766},"resincronización-completa","Resincronización completa",[12,769,770,771,435],{},"Una resincronización completa reconstruye tu copia de un equipo ",[133,772,773],{},"marcando y barriendo",[302,775,776,779,782],{},[54,777,778],{},"Olvida las versiones y el cursor que guardaste del equipo. Conserva los registros: tu copia sigue siendo utilizable mientras dura la resincronización.",[54,780,781],{},"Repite la carga inicial, sin cursor. Aplica cada página como siempre y marca cada registro que entregue el feed.",[54,783,390,784,276,786,788],{},[20,785,275],{},[20,787,285],{},", borra todos los registros del equipo que no hayas marcado.",[12,790,791],{},"Guarda las marcas de forma duradera. Si el proceso se detiene a medias, la siguiente ejecución debe continuar la misma resincronización, no barrer con la mitad de las marcas. También importa olvidar antes las versiones: después de una restauración, el feed puede entregar versiones más bajas que las que tenías.",[28,793,795],{"id":794},"cuando-cambian-los-equipos","Cuando cambian los equipos",[12,797,700,798,802],{},[16,799,800],{"href":703},[20,801,686],{}," al principio de cada ejecución. Lista todos los equipos que lee tu clave, también los que están restringidos por ahora.",[51,804,805,808,811],{},[54,806,807],{},"Un equipo que aún no has sincronizado: haz su carga inicial.",[54,809,810],{},"Un equipo que tienes y que ya no aparece: ha salido de la clave. Borra sus datos y su cursor.",[54,812,813,814,816],{},"Un equipo que aparece pero responde ",[20,815,660],{},": consérvalo, como se explica más arriba.",[28,818,820],{"id":819},"actualiza-tableros-miembros-y-servicios","Actualiza tableros, miembros y servicios",[12,822,823],{},"Los miembros y los servicios reservables no están en el feed. Los tableros sí, pero un lead puede indicar una etapa nueva antes de que te llegue el cambio de su tablero. Por eso, lee los tres:",[51,825,826,829,832],{},[54,827,828],{},"cuando empieces a sincronizar un equipo;",[54,830,831],{},"cuando un elemento indique un tablero, una etapa, un miembro o un servicio que no conoces;",[54,833,834],{},"al menos una vez al día.",[12,836,837],{},"Un registro puede seguir indicando un miembro que salió del equipo, o un tablero, una etapa o un servicio que se borró. Si una actualización no encuentra un identificador, recuérdalo y no vuelvas a actualizar por él antes de la actualización diaria.",[12,839,840,846,847,63,853,859,860,863,864,866],{},[16,841,843],{"href":842},"\u002Fdevelopers\u002Fes\u002Freference-leads#boards.list",[20,844,845],{},"GET \u002Fv1\u002Fboards"," se pagina como cualquier lista. ",[16,848,850],{"href":849},"\u002Fdevelopers\u002Fes\u002Freference-account#members.list",[20,851,852],{},"GET \u002Fv1\u002Fmembers",[16,854,856],{"href":855},"\u002Fdevelopers\u002Fes\u002Freference-bookings#booking_services.list",[20,857,858],{},"GET \u002Fv1\u002Fbooking_services"," devuelven la lista entera en una sola respuesta y no admiten ",[20,861,862],{},"limit"," ni ",[20,865,271],{},". Sustituye entera tu copia de cada lista. Un miembro que deja el equipo, o un servicio que se borra, sale de su lista, pero los registros anteriores siguen indicando su identificador: si muestras nombres en registros anteriores, conserva el de una lectura anterior. Los miembros y servicios desactivados siguen en la lista, sin nada que indique que lo están.",[28,868,870],{"id":869},"ritmo","Ritmo",[12,872,873,874,877,878,881,882,884,885,887,888,890],{},"Una clave lee hasta ",[288,875],{"name":876},"SYNC_VELOCITY.perMinute"," elementos por minuto, así que la carga inicial de un equipo grande lleva un tiempo. En ",[16,879,880],{"href":731},"Límites de frecuencia y cuota"," tienes las cifras, una estimación de cuánto tarda una carga inicial y por qué las cifras son aproximadas durante la Beta. Si recibes un ",[20,883,725],{},", espera lo que indique ",[20,886,738],{}," y continúa con el mismo cursor: no se pierde nada. Los límites son de la clave, así que un ",[20,889,725],{}," detiene todos los equipos que lee, no solo el que estabas sincronizando.",[28,892,894],{"id":893},"cliente-de-referencia-probado","Cliente de referencia probado",[12,896,897,898,901,902,905],{},"El código TypeScript de más abajo es un cliente de sincronización completo que hace todo lo que describe esta página. Totem lo ejecuta en sus propias pruebas contra un feed simulado, con caídas, páginas repetidas, eliminaciones, resincronizaciones y errores; el código que se muestra aquí es ese mismo archivo, y por eso sus comentarios están en inglés. No tiene dependencias y solo necesita ",[20,899,900],{},"fetch",": compílalo con tu build de TypeScript o ejecútalo directamente con Node.js 22.18 o posterior. Cópialo en tu servidor e implementa ",[20,903,904],{},"SyncStore"," sobre tu base de datos.",[354,907,910],{"title":908,"type":909},"¿Cliente de referencia o SDK?","note",[12,911,912,913,917,918,920],{},"Para sincronizar, parte de este cliente: implementa las marcas de eliminación, el marcado y barrido de la resincronización completa y un resultado por equipo. El ",[16,914,916],{"href":915},"\u002Fdevelopers\u002Fes\u002Ftypescript","SDK de TypeScript"," ofrece peticiones tipadas y paginación; su propio ejemplo de sincronización trata un solo equipo y deja la ",[16,919,626],{"href":625}," a tu almacenamiento, así que impleméntala como explica esta página.",[12,922,923,924,927,928,931],{},"Ejecútalo desde una tarea programada cada 1 a 5 minutos, con una sola ejecución por clave a la vez. El ejemplo es un módulo ES; con la resolución de módulos de Node.js, importa el archivo con la extensión ",[20,925,926],{},".js"," que genera tu build, o con ",[20,929,930],{},".ts"," si Node.js ejecuta el TypeScript directamente:",[120,933,938],{"className":934,"code":936,"language":937,"meta":126},[935],"language-ts","import { syncKey } from '.\u002FsyncTeam.js'\n\nconst outcomes = await syncKey({ key: process.env.TOTEM_API_KEY!, store })\n","ts",[20,939,936],{"__ignoreMap":126},[12,941,942,943,945],{},"La clave nunca debe llegar a un navegador. Cada método de ",[20,944,904],{}," debe escribir de forma duradera antes de que se resuelva su promesa, y las escrituras de un mismo equipo no deben ejecutarse en paralelo.",[108,947,949],{"id":948},"opciones-y-resultados","Opciones y resultados",[12,951,952,955,956,958],{},[20,953,954],{},"syncKey"," lee ",[20,957,686],{},", borra los equipos que han salido de la clave y sincroniza, uno tras otro, todos los equipos de la lista. Devuelve un resultado por equipo:",[51,960,961,967,977,983,1005],{},[54,962,963,966],{},[20,964,965],{},"synced",": el equipo está al día y su cursor está guardado.",[54,968,969,972,973,976],{},[20,970,971],{},"partial",": el equipo leyó ",[20,974,975],{},"maxPagesPerTeam"," páginas y todavía no está al día. Su cursor está guardado, y la siguiente ejecución continúa desde él.",[54,978,979,982],{},[20,980,981],{},"dropped",": el equipo ha salido de la clave, y su copia y su cursor se han borrado.",[54,984,985,988,989,991,992,59,994,997,998,1001,1002,1004],{},[20,986,987],{},"skipped",", con el ",[20,990,20],{},": ahora mismo no se puede leer el equipo (",[20,993,660],{},[20,995,996],{},"capture_not_ready"," o un ",[20,999,1000],{},"team_not_accessible"," que ",[20,1003,686],{}," no confirma). Se conservan su copia y su cursor, y la siguiente ejecución lo vuelve a intentar.",[54,1006,1007,988,1010,63,1012,1015,1016,997,1018,1020,1021,1024,1025,1027,1028,1030,1031,1033,1034,1036,1037,1039],{},[20,1008,1009],{},"retry_later",[20,1011,20],{},[20,1013,1014],{},"retryAt"," (milisegundos desde la época Unix): la API pidió esperar más de lo que permite esta ejecución, o un ",[20,1017,725],{},[20,1019,752],{}," seguía fallando tras ",[20,1022,1023],{},"maxRetries"," reintentos. Se conservan la copia y el cursor. No vuelvas a sincronizar ese equipo antes de ",[20,1026,1014],{},". Un ",[20,1029,725],{}," es de la clave, no de un equipo, así que en cuanto uno aparta la clave, todos los equipos a los que esta ejecución aún no ha llegado se marcan ",[20,1032,1009],{}," con el mismo ",[20,1035,20],{}," y el mismo ",[20,1038,1014],{},", sin hacer ninguna petición.",[12,1041,1042,1043,1045,1046,1048,1049,1051,1052,1054,1055,1058,1059,1062,1063,1065],{},"Un equipo restringido, todavía en preparación o que ya no está nunca detiene a los demás; un ",[20,1044,725],{}," que esta ejecución no puede esperar detiene la clave entera hasta ",[20,1047,1014],{},". ",[20,1050,954],{}," lanza un error cuando se rechaza la propia clave, cuando no se puede leer ",[20,1053,686],{}," o ante un error que apunta a un fallo de programación y no al equipo: un ",[20,1056,1057],{},"400",", un ",[20,1060,1061],{},"404"," inesperado, un feed que vuelve a pedir una resincronización completa justo después de otra, una lista o un feed que repite un cursor o no envía ninguno, o un fallo de red. Tampoco entonces se pierde ningún cursor; si el error trae ",[20,1064,1014],{},", programa la siguiente ejecución para ese momento.",[12,1067,1068,1069,63,1072,1075],{},"Además de ",[20,1070,1071],{},"key",[20,1073,1074],{},"store",", admite:",[51,1077,1078,1090,1100,1108],{},[54,1079,1080,1083,1084,1086,1087,1089],{},[20,1081,1082],{},"maxWaitMs",": lo máximo que espera de una vez, en milisegundos. Por defecto, 60.000. Un ",[20,1085,738],{}," más largo, como la espera hasta que la cuota diaria se renueva a las 00:00 UTC, termina la ejecución con ",[20,1088,1009],{}," en lugar de dejar tu proceso dormido.",[54,1091,1092,1094,1095,997,1097,1099],{},[20,1093,1023],{},": cuántas veces se reintenta una petición tras un ",[20,1096,725],{},[20,1098,752],{},". Por defecto, 3.",[54,1101,1102,1104,1105,1107],{},[20,1103,975],{},": el máximo de páginas del feed que lee un equipo en una ejecución. Por defecto no hay límite. Con un límite, una carga inicial larga se detiene con ",[20,1106,971],{},", se sincronizan los demás equipos de la clave y la siguiente ejecución continúa desde el cursor guardado.",[54,1109,1110,59,1112,59,1115,63,1118,1121],{},[20,1111,900],{},[20,1113,1114],{},"baseUrl",[20,1116,1117],{},"sleep",[20,1119,1120],{},"now",": sustitúyelos en tus pruebas.",[1123,1124],"dev-snippet",{"file":1125},"sync-team",[28,1127,1129],{"id":1128},"almacén-en-memoria-para-pruebas","Almacén en memoria para pruebas",[12,1131,1132,1135,1136,1138],{},[20,1133,1134],{},"MemorySyncStore"," implementa en memoria todos los métodos de ",[20,1137,904],{},". Muestra lo que debe hacer cada método y hace que tus pruebas sean deterministas. Lo pierde todo al reiniciarse, así que sustitúyelo por tu base de datos antes de pasar a producción.",[1123,1140],{"file":1141},"memory-store",{"title":126,"searchDepth":1143,"depth":1143,"links":1144},3,[1145,1149,1150,1151,1152,1153,1154,1159,1160,1163,1164,1165,1166,1169],{"id":30,"depth":1146,"text":31,"children":1147},2,[1148],{"id":110,"depth":1143,"text":111},{"id":181,"depth":1146,"text":182},{"id":245,"depth":1146,"text":246},{"id":297,"depth":1146,"text":167},{"id":363,"depth":1146,"text":364},{"id":386,"depth":1146,"text":387},{"id":412,"depth":1146,"text":413,"children":1155},[1156,1157,1158],{"id":416,"depth":1143,"text":417},{"id":472,"depth":1143,"text":473},{"id":493,"depth":1143,"text":494},{"id":541,"depth":1146,"text":542},{"id":580,"depth":1146,"text":428,"children":1161},[1162],{"id":766,"depth":1143,"text":767},{"id":794,"depth":1146,"text":795},{"id":819,"depth":1146,"text":820},{"id":869,"depth":1146,"text":870},{"id":893,"depth":1146,"text":894,"children":1167},[1168],{"id":948,"depth":1143,"text":949},{"id":1128,"depth":1146,"text":1129},"Copia un equipo con el feed de cambios y mantén la copia al día. Aplica las páginas sin riesgos, recupérate de los errores y parte de un cliente de referencia que Totem prueba.",false,"md","es",{},true,"10","\u002Fdevelopers\u002Fes\u002Fsync","---\ntitle: Sincroniza una copia\ndescription: Copia un equipo con el feed de cambios y mantén la copia al día. Aplica las páginas sin riesgos, recupérate de los errores y parte de un cliente de referencia que Totem prueba.\nsection: guides\norder: 10\ntranslationKey: sync\nlang: es\ndraft: false\n---\n\nEl feed de cambios, [`GET \u002Fv1\u002Fsync\u002Fchanges`](\u002Fdevelopers\u002Fes\u002Freference-sync#sync.changes), es la forma de tener tu propia copia de un equipo. Tu primera petición copia todos los registros. A partir de ahí, cada petición devuelve solo lo que ha cambiado desde la anterior, así que sincronizar cada pocos minutos cuesta poco y nunca se pierde un cambio.\n\n::dev-sync-diagram\n::\n\n## Cómo funciona el feed\n\nHay un feed por equipo. Una clave que lee varios equipos indica el equipo en la [cabecera `Totem-Team`](\u002Fdevelopers\u002Fes\u002Fauthentication#equipos-y-la-cabecera-totem-team) y guarda un cursor por equipo.\n\nCada elemento del feed es el estado actual de un registro, de un tipo (`kind`):\n\n- `lead`, `lead_attribution` y `lead_response`: un lead y sus dos facetas;\n- `activity`: un cambio de etapa, de estado o de miembros asignados de un lead;\n- `call`, `conversation` y `booking`;\n- `board`, con sus etapas dentro.\n\nLa atribución y la respuesta de un lead viajan como tipos propios, con el identificador del lead y su propia `version`. Por eso un mensaje nuevo cambia la respuesta del lead sin cambiar el lead. Los miembros y los servicios reservables no están en el feed: los [lees aparte](#actualiza-tableros-miembros-y-servicios).\n\nLos elementos llegan en el orden en que Totem registró los cambios, del más antiguo al más reciente. Un elemento nunca es una diferencia: trae el registro entero tal como está ahora.\n\n### Registros y eliminaciones\n\nLa mayoría de los elementos traen el registro en `object`. Es el mismo objeto que devuelven los endpoints de lista; aquí aparece abreviado:\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\nUna **eliminación** trae `removed` en lugar de `object`. Te indica que borres el registro:\n\n```json\n{\n  \"kind\": \"call\",\n  \"id\": \"call_m1pf4APjJWVi3Z7ulyNaW8y3lUi7V\",\n  \"version\": 5,\n  \"removed\": { \"reason\": \"deleted\" }\n}\n```\n\n`reason` es `erased` cuando el registro se suprimió: por privacidad, por ejemplo a petición de la persona, o porque alguien eliminó definitivamente en Totem un lead, una llamada o una conversación, lo que siempre los suprime. Es `deleted` cuando el registro desapareció de cualquier otra forma, por ejemplo porque ya no forma parte de los datos de la API del equipo o porque Totem lo identifica ahora con un identificador nuevo. Trata los dos casos igual, como explica [Aplica una página](#aplica-una-página). Un registro que vuelve es un registro nuevo con un identificador nuevo ([Convenciones](\u002Fdevelopers\u002Fes\u002Fconventions#identificadores)), así que relaciona a las personas entre identificadores por teléfono o correo electrónico con la [búsqueda](\u002Fdevelopers\u002Fes\u002Freference-leads#leads.search), nunca por identificador.\n\n## Planifica tu almacenamiento\n\nUna copia construida así puede aplicar cada página sin riesgos, lleguen los registros en el orden que lleguen:\n\n- **Una fila por registro**, con la clave formada por el equipo, el `kind` y el `id`, y con su `version`. Un lead y sus dos facetas comparten identificador, así que el `kind` forma parte de la clave.\n- **Una marca por eliminación**: el `kind`, el `id` y la `version` de un registro que borraste, sin sus datos.\n- **Un cursor por equipo**, guardado junto con la página a la que sigue cuando tu base de datos lo permita.\n- **Los tableros, los miembros y los servicios reservables** en listas propias, que sustituyes enteras cada vez que las lees.\n- **Ninguna clave foránea entre registros mientras aplicas páginas.** Un registro puede indicar otro que aún no te ha llegado, como un lead que indica una etapa de un tablero o una actividad que indica su lead.\n\nDespués, ejecuta una sincronización por clave cada 1 a 5 minutos, y vigila los equipos que se omiten o se aplazan una y otra vez, o cuyo cursor lleva un día sin guardarse.\n\n## Haz la carga inicial\n\nTu primera petición de un equipo no lleva cursor. Empieza la **carga inicial**: el feed recorre todos los registros del equipo, del cambio más antiguo al más reciente.\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\nAplica la página, guarda su `next_cursor` y vuelve a pedir con `cursor` igual a ese valor. Repite mientras `has_more` sea `true`. Cuando `has_more` sea `false`, tu copia estará completa. Pide la página más grande, de :api-const{name=\"PAGE_LIMIT.max\"} elementos, para que la carga inicial necesite las menos peticiones posibles.\n\nDurante una carga inicial hay dos cosas normales. Un registro que cambia mientras se ejecuta vuelve a llegar más tarde, con una versión mayor. Y, tras un reintento, puedes recibir la misma página dos veces. La comprobación de versiones del apartado siguiente hace que ninguna de las dos cause problemas.\n\n## Aplica una página\n\nPara cada elemento de la página, en orden:\n\n1. **Omite lo que ya tienes.** Si ya tienes ese `kind` e `id` con la misma `version` o una mayor, omite el elemento. Compara también con las versiones de tus marcas de eliminación.\n2. **Guarda un registro.** Si el elemento trae `object`, inserta tu copia o sustitúyela. Si es un lead, guarda sus `contact_permissions` antes de que nada pueda actuar sobre él.\n3. **Aplica una eliminación.** Si el elemento trae `removed`, borra tu copia, pero conserva su `kind`, su `id` y su `version` como **marca de eliminación**. Si el registro eliminado es un lead, borra también su atribución, su respuesta y sus actividades, e ignora cualquiera de ellas que llegue después.\n\n::dev-callout{type=\"warning\" title=\"Conserva la marca de eliminación\"}\nSin la marca, el paso 1 no tiene con qué comparar, y una copia anterior del registro, entregada otra vez en una página repetida, lo haría reaparecer. Si el registro se suprimió a petición de una persona, eso es un incidente de privacidad, no un fallo de sincronización.\n::\n\n## Guarda el cursor\n\nGuarda `next_cursor` solo cuando hayas guardado la página entera. Si tu proceso se detiene entre medias, la siguiente ejecución vuelve a leer la misma página, y el paso 1 omite lo que ya aplicaste. Si tu base de datos lo permite, guarda la página y su cursor en una misma transacción.\n\nUn cursor es opaco: guárdalo tal como lo recibiste y no construyas ni modifiques ninguno. Pertenece a un solo equipo. Si lo envías con otro equipo, recibe `400 invalid_request`.\n\nUn cursor caduca :api-const{name=\"CURSOR_MAX_AGE_DAYS\"} días después del punto del feed al que llegó. Mientras una carga inicial sigue en curso, esa antigüedad se cuenta desde que empezó la carga. El cursor de un equipo que consultas con regularidad nunca llega a caducar.\n\n## Consulta los cambios periódicamente\n\nCuando `has_more` sea `false`, vuelve a pedir con el cursor guardado cada 1 a 5 minutos. Si no ha cambiado nada, recibes una página vacía con un `next_cursor` nuevo. Guárdalo también: una página vacía hace avanzar el cursor, así que el cursor de un equipo con poca actividad nunca envejece.\n\nEl feed retiene los últimos :api-const{name=\"FEED_SETTLE_LAG_MS\" divide=\"1000\"} segundos de cambios, para no saltarse nunca un cambio que aún se está escribiendo. Un cambio aparece en el feed poco después de producirse en Totem.\n\nSincroniza cada combinación de clave y equipo desde un solo proceso a la vez. Dos procesos que aplican páginas del mismo equipo pueden guardar sus cursores en un orden equivocado.\n\n## Eliminaciones, fusiones y traspasos\n\n### Eliminaciones\n\nAplica cada eliminación en cuanto la recibas: borra el registro de tu copia y de todos los sistemas a los que lo copiaste. Las eliminaciones siguen en el feed durante :api-const{name=\"REMOVAL_RETENTION_DAYS\"} días, más de lo que dura un cursor, así que una sincronización que sigue leyendo el feed con éxito nunca se pierde ninguna. Mientras la API rechaza un equipo, no entrega nada de él, tampoco eliminaciones; en [Recupérate de los errores](#recupérate-de-los-errores) se explica qué hacer entonces.\n\nCuando se elimina un lead, su atribución, su respuesta y sus actividades se eliminan con él, y el feed entrega también sus eliminaciones. Lo que ocurre con los demás registros del lead depende de `reason`:\n\n- **`erased`**, también cuando alguien elimina definitivamente el lead en Totem: sus llamadas y conversaciones también se suprimen, igual que los leads archivados cuyo `merged_into` lo indica, con sus propias llamadas y conversaciones. Cada registro llega como una eliminación propia, con `reason: \"erased\"`. Sus citas no se eliminan: vuelven a llegar con `lead` igual a `null`.\n- **`deleted`**: sus llamadas, conversaciones y citas no se eliminan. Vuelven a llegar con `lead` igual a `null`.\n\n### Fusiones\n\nCuando en Totem se fusionan dos leads de la misma persona, el lead que no se conserva queda archivado, no eliminado. Su `archive_reason` es `merged` y su `merged_into` indica el lead que se conservó. Enlaza tu copia con ese lead en lugar de crear a la persona dos veces. El lead que se conserva se queda además con los permisos de contacto más restrictivos de los dos. `merged_into` es `null` cuando el lead que indicaba ya no existe.\n\n### Traspasos\n\nCuando un lead pasa a otro equipo de la misma organización, el lead original queda archivado con `archive_reason` igual a `family_transfer`, y `transferred_to` indica adónde fue:\n\n- `transferred_to.team` siempre tiene valor. Puede ser un equipo que tu clave no lee.\n- `transferred_to.lead` es el lead en ese equipo, o `null` cuando Totem no puede indicarlo.\n\nSi tu clave lee el equipo de destino, el lead nuevo llega en el feed de ese equipo. Si no lo lee, [`GET \u002Fv1\u002Fleads\u002F{id}`](\u002Fdevelopers\u002Fes\u002Freference-leads#leads.get) sobre el lead de destino responde `404 resource_missing`. Conserva el lead archivado como constancia de adónde fue la persona, y no intentes adivinarlo.\n\n## Primero, los permisos de contacto\n\nAntes de que cualquiera de tus sistemas contacte con un lead, guarda sus `contact_permissions` y aplícalos:\n\n- `do_not_contact: true`: la persona pidió que no se la contacte nunca. No uses ningún canal.\n- `email_opt_out: true`: la persona se dio de baja de los correos comerciales y de seguimiento.\n- `whatsapp_consent: false`: la persona rechazó WhatsApp. `null` significa que nadie registró su respuesta, así que nunca lo trates como un consentimiento.\n\nQue un lead aparezca en el feed es un dato, no un permiso para contactar con la persona. La base legal para contactar con alguien sigue siendo responsabilidad tuya.\n\nLa API es de solo lectura, así que Totem no puede conocer una baja registrada en tus sistemas. Cuando una persona se dé de baja en ellos, asegúrate de que también quede registrada en el lead en Totem, para que Totem deje de contactarla.\n\n## Recupérate de los errores\n\nCuando falla la petición de una página, no guardes ningún cursor. La siguiente petición con el mismo cursor vuelve a pedir la misma página.\n\n| Respuesta | Qué significa | Qué hacer |\n|---|---|---|\n| `410 sync_cursor_expired` | El cursor tiene más de :api-const{name=\"CURSOR_MAX_AGE_DAYS\"} días. | Haz una [resincronización completa](#resincronización-completa). |\n| `409 resync_required` | Totem no puede continuar desde este cursor, por ejemplo tras restaurar una copia de seguridad. | Haz una [resincronización completa](#resincronización-completa). |\n| `409 capture_not_ready` | Totem aún está preparando los datos de este equipo. Ocurre una vez, cuando un equipo se añade por primera vez a una clave. | Conserva todo y vuelve a intentarlo en unos minutos. Nunca resincronices por esto. Los demás equipos de la clave siguen sincronizándose. |\n| `403` | El acceso está denegado por ahora: por ejemplo, el equipo está restringido (`team_state_restricted`) o el endpoint está desactivado (`capability_disabled`). | Conserva los datos y el cursor del equipo, y omítelo en esta ejecución. La página de [Errores](\u002Fdevelopers\u002Fes\u002Ferrors) indica, código a código, si sirve de algo esperar. `plan_not_entitled` rechaza la clave entera, así que la ejecución se detiene en `GET \u002Fv1\u002Fteams`. Mientras el acceso está denegado, el feed no entrega nada del equipo, tampoco eliminaciones: si la denegación dura más de lo que permite tu política de conservación, borra la copia del equipo. |\n| `404 team_not_accessible` | Puede que la clave ya no lea este equipo. | Lee [`GET \u002Fv1\u002Fteams`](\u002Fdevelopers\u002Fes\u002Freference-account#teams.list). Si el equipo no aparece, borra sus datos y su cursor. Si aparece, conserva todo y vuelve a intentarlo en la siguiente ejecución. |\n| `400 invalid_request` | El cursor se ha modificado o es de otro equipo. | Usa el cursor que guardaste para este equipo. |\n| `429` | Has alcanzado el [límite de frecuencia o la cuota diaria](\u002Fdevelopers\u002Fes\u002Frate-limits). | Espera los segundos que indique `Retry-After` y repite con el mismo cursor. Un `429` del cortafuegos no trae `Retry-After`: reintenta con espera exponencial. |\n| `5xx` | Algo ha fallado dentro de Totem. | Reintenta con espera exponencial. |\n\nTodos los códigos se explican en la página de [Errores](\u002Fdevelopers\u002Fes\u002Ferrors).\n\n### Resincronización completa\n\nUna resincronización completa reconstruye tu copia de un equipo **marcando y barriendo**:\n\n1. Olvida las versiones y el cursor que guardaste del equipo. Conserva los registros: tu copia sigue siendo utilizable mientras dura la resincronización.\n2. Repite la carga inicial, sin cursor. Aplica cada página como siempre y marca cada registro que entregue el feed.\n3. Cuando `has_more` sea `false`, borra todos los registros del equipo que no hayas marcado.\n\nGuarda las marcas de forma duradera. Si el proceso se detiene a medias, la siguiente ejecución debe continuar la misma resincronización, no barrer con la mitad de las marcas. También importa olvidar antes las versiones: después de una restauración, el feed puede entregar versiones más bajas que las que tenías.\n\n## Cuando cambian los equipos\n\nLee [`GET \u002Fv1\u002Fteams`](\u002Fdevelopers\u002Fes\u002Freference-account#teams.list) al principio de cada ejecución. Lista todos los equipos que lee tu clave, también los que están restringidos por ahora.\n\n- Un equipo que aún no has sincronizado: haz su carga inicial.\n- Un equipo que tienes y que ya no aparece: ha salido de la clave. Borra sus datos y su cursor.\n- Un equipo que aparece pero responde `403`: consérvalo, como se explica más arriba.\n\n## Actualiza tableros, miembros y servicios\n\nLos miembros y los servicios reservables no están en el feed. Los tableros sí, pero un lead puede indicar una etapa nueva antes de que te llegue el cambio de su tablero. Por eso, lee los tres:\n\n- cuando empieces a sincronizar un equipo;\n- cuando un elemento indique un tablero, una etapa, un miembro o un servicio que no conoces;\n- al menos una vez al día.\n\nUn registro puede seguir indicando un miembro que salió del equipo, o un tablero, una etapa o un servicio que se borró. Si una actualización no encuentra un identificador, recuérdalo y no vuelvas a actualizar por él antes de la actualización diaria.\n\n[`GET \u002Fv1\u002Fboards`](\u002Fdevelopers\u002Fes\u002Freference-leads#boards.list) se pagina como cualquier lista. [`GET \u002Fv1\u002Fmembers`](\u002Fdevelopers\u002Fes\u002Freference-account#members.list) y [`GET \u002Fv1\u002Fbooking_services`](\u002Fdevelopers\u002Fes\u002Freference-bookings#booking_services.list) devuelven la lista entera en una sola respuesta y no admiten `limit` ni `cursor`. Sustituye entera tu copia de cada lista. Un miembro que deja el equipo, o un servicio que se borra, sale de su lista, pero los registros anteriores siguen indicando su identificador: si muestras nombres en registros anteriores, conserva el de una lectura anterior. Los miembros y servicios desactivados siguen en la lista, sin nada que indique que lo están.\n\n## Ritmo\n\nUna clave lee hasta :api-const{name=\"SYNC_VELOCITY.perMinute\"} elementos por minuto, así que la carga inicial de un equipo grande lleva un tiempo. En [Límites de frecuencia y cuota](\u002Fdevelopers\u002Fes\u002Frate-limits) tienes las cifras, una estimación de cuánto tarda una carga inicial y por qué las cifras son aproximadas durante la Beta. Si recibes un `429`, espera lo que indique `Retry-After` y continúa con el mismo cursor: no se pierde nada. Los límites son de la clave, así que un `429` detiene todos los equipos que lee, no solo el que estabas sincronizando.\n\n## Cliente de referencia probado\n\nEl código TypeScript de más abajo es un cliente de sincronización completo que hace todo lo que describe esta página. Totem lo ejecuta en sus propias pruebas contra un feed simulado, con caídas, páginas repetidas, eliminaciones, resincronizaciones y errores; el código que se muestra aquí es ese mismo archivo, y por eso sus comentarios están en inglés. No tiene dependencias y solo necesita `fetch`: compílalo con tu build de TypeScript o ejecútalo directamente con Node.js 22.18 o posterior. Cópialo en tu servidor e implementa `SyncStore` sobre tu base de datos.\n\n::dev-callout{type=\"note\" title=\"¿Cliente de referencia o SDK?\"}\nPara sincronizar, parte de este cliente: implementa las marcas de eliminación, el marcado y barrido de la resincronización completa y un resultado por equipo. El [SDK de TypeScript](\u002Fdevelopers\u002Fes\u002Ftypescript) ofrece peticiones tipadas y paginación; su propio ejemplo de sincronización trata un solo equipo y deja la [resincronización completa](#resincronización-completa) a tu almacenamiento, así que impleméntala como explica esta página.\n::\n\nEjecútalo desde una tarea programada cada 1 a 5 minutos, con una sola ejecución por clave a la vez. El ejemplo es un módulo ES; con la resolución de módulos de Node.js, importa el archivo con la extensión `.js` que genera tu build, o con `.ts` si Node.js ejecuta el TypeScript directamente:\n\n```ts\nimport { syncKey } from '.\u002FsyncTeam.js'\n\nconst outcomes = await syncKey({ key: process.env.TOTEM_API_KEY!, store })\n```\n\nLa clave nunca debe llegar a un navegador. Cada método de `SyncStore` debe escribir de forma duradera antes de que se resuelva su promesa, y las escrituras de un mismo equipo no deben ejecutarse en paralelo.\n\n### Opciones y resultados\n\n`syncKey` lee `GET \u002Fv1\u002Fteams`, borra los equipos que han salido de la clave y sincroniza, uno tras otro, todos los equipos de la lista. Devuelve un resultado por equipo:\n\n- `synced`: el equipo está al día y su cursor está guardado.\n- `partial`: el equipo leyó `maxPagesPerTeam` páginas y todavía no está al día. Su cursor está guardado, y la siguiente ejecución continúa desde él.\n- `dropped`: el equipo ha salido de la clave, y su copia y su cursor se han borrado.\n- `skipped`, con el `code`: ahora mismo no se puede leer el equipo (`403`, `capture_not_ready` o un `team_not_accessible` que `GET \u002Fv1\u002Fteams` no confirma). Se conservan su copia y su cursor, y la siguiente ejecución lo vuelve a intentar.\n- `retry_later`, con el `code` y `retryAt` (milisegundos desde la época Unix): la API pidió esperar más de lo que permite esta ejecución, o un `429` o un `5xx` seguía fallando tras `maxRetries` reintentos. Se conservan la copia y el cursor. No vuelvas a sincronizar ese equipo antes de `retryAt`. Un `429` es de la clave, no de un equipo, así que en cuanto uno aparta la clave, todos los equipos a los que esta ejecución aún no ha llegado se marcan `retry_later` con el mismo `code` y el mismo `retryAt`, sin hacer ninguna petición.\n\nUn equipo restringido, todavía en preparación o que ya no está nunca detiene a los demás; un `429` que esta ejecución no puede esperar detiene la clave entera hasta `retryAt`. `syncKey` lanza un error cuando se rechaza la propia clave, cuando no se puede leer `GET \u002Fv1\u002Fteams` o ante un error que apunta a un fallo de programación y no al equipo: un `400`, un `404` inesperado, un feed que vuelve a pedir una resincronización completa justo después de otra, una lista o un feed que repite un cursor o no envía ninguno, o un fallo de red. Tampoco entonces se pierde ningún cursor; si el error trae `retryAt`, programa la siguiente ejecución para ese momento.\n\nAdemás de `key` y `store`, admite:\n\n- `maxWaitMs`: lo máximo que espera de una vez, en milisegundos. Por defecto, 60.000. Un `Retry-After` más largo, como la espera hasta que la cuota diaria se renueva a las 00:00 UTC, termina la ejecución con `retry_later` en lugar de dejar tu proceso dormido.\n- `maxRetries`: cuántas veces se reintenta una petición tras un `429` o un `5xx`. Por defecto, 3.\n- `maxPagesPerTeam`: el máximo de páginas del feed que lee un equipo en una ejecución. Por defecto no hay límite. Con un límite, una carga inicial larga se detiene con `partial`, se sincronizan los demás equipos de la clave y la siguiente ejecución continúa desde el cursor guardado.\n- `fetch`, `baseUrl`, `sleep` y `now`: sustitúyelos en tus pruebas.\n\n::dev-snippet{file=\"sync-team\"}\n::\n\n## Almacén en memoria para pruebas\n\n`MemorySyncStore` implementa en memoria todos los métodos de `SyncStore`. Muestra lo que debe hacer cada método y hace que tus pruebas sean deterministas. Lo pierde todo al reiniciarse, así que sustitúyelo por tu base de datos antes de pasar a producción.\n\n::dev-snippet{file=\"memory-store\"}\n::\n","guides",{"title":6,"description":1170},"developers\u002Fes\u002Fsync","sync","UPo8lOMWVOqW9CrmyTp89Bapqf84SFRX0xWbZERozDM",[1185,1200,1221,1264],{"section":1186,"items":1187},"start",[1188,1192,1196],{"path":1189,"title":1190,"description":1191},"\u002Fdevelopers\u002Fes","Introducción","Lee desde tus propios sistemas los leads, las llamadas, las conversaciones y las citas de tus equipos de Totem, y mantén una copia sincronizada.",{"path":1193,"title":1194,"description":1195},"\u002Fdevelopers\u002Fes\u002Fquickstart","Inicio rápido","Haz tus primeras peticiones a la API de Totem en unos minutos. Comprueba tu clave, busca tu equipo, lista leads e inicia una sincronización.",{"path":1197,"title":1198,"description":1199},"\u002Fdevelopers\u002Fes\u002Fauthentication","Autenticación","Las claves secretas y dónde pueden guardarse, la cabecera Totem-Team de las claves con varios equipos, la lista de IP permitidas y cómo rotar o revocar una clave.",{"section":1179,"items":1201},[1202,1204,1205,1209,1213,1217],{"path":915,"title":916,"description":1203},"Usa el cliente oficial de TypeScript para leer tus datos de Totem, paginar listas y gestionar errores de la API desde tu servidor.",{"path":1177,"title":6,"description":1170},{"path":1206,"title":1207,"description":1208},"\u002Fdevelopers\u002Fes\u002Fpagination","Listas y paginación","Cómo devuelven los endpoints de lista los registros página a página, cómo leer todas las páginas y qué orden y qué filtros admite una lista.",{"path":1210,"title":1211,"description":1212},"\u002Fdevelopers\u002Fes\u002Fcoverage","Cobertura de los datos","Qué registra Totem, desde cuándo y cómo distinguir un valor vacío de un dato que nunca se registró.",{"path":1214,"title":1215,"description":1216},"\u002Fdevelopers\u002Fes\u002Ftesting","Pruebas y puesta en producción","Cómo construir y probar una integración con seguridad mientras la API no tiene entorno de pruebas, y qué comprobar antes de pasar a producción.",{"path":1218,"title":1219,"description":1220},"\u002Fdevelopers\u002Fes\u002Fsecurity","Seguridad y datos personales","Cómo protege Totem las claves de la API y los datos que sirve, qué registra de cada clave, dónde funciona la API y qué hacer con los datos personales que copias.",{"section":1222,"items":1223},"reference",[1224,1227,1229,1231,1236,1241,1246,1251,1256,1260],{"path":1225,"title":172,"description":1226},"\u002Fdevelopers\u002Fes\u002Fconventions","La notación de esta documentación y las reglas que siguen todas las respuestas: identificadores, fechas, valores vacíos, importes, enumeraciones, versiones, estabilidad, errores y textos no fiables.",{"path":677,"title":678,"description":1228},"El formato de los errores, cómo debe tratar tu integración cada tipo de error y todos los códigos de error, con cuándo ocurren y qué hacer.",{"path":731,"title":880,"description":1230},"Cuánto puede leer una clave por minuto y por día, qué cuenta para cada límite, cómo reintentar tras un 429 y cuánto tarda una carga inicial.",{"path":1232,"title":1233,"description":1234,"apiTag":1235},"\u002Fdevelopers\u002Fes\u002Freference-account","Cuenta, equipos y miembros","Comprueba que una clave funciona y qué puede leer, lista sus equipos con su zona horaria, lista los miembros de cada equipo y comprueba que la API está funcionando.","account",{"path":1237,"title":1238,"description":1239,"apiTag":1240},"\u002Fdevelopers\u002Fes\u002Freference-leads","Leads, tableros y actividades","Lista, obtén y busca los leads y los clientes de un equipo, lee las actividades de cada lead y lista los tableros y las etapas por los que pasan.","leads",{"path":1242,"title":1243,"description":1244,"apiTag":1245},"\u002Fdevelopers\u002Fes\u002Freference-calls","Llamadas","Lista y obtén las llamadas de un equipo, con su dirección, su horario y un resultado normalizado. Sin grabaciones ni transcripciones.","calls",{"path":1247,"title":1248,"description":1249,"apiTag":1250},"\u002Fdevelopers\u002Fes\u002Freference-conversations","Conversaciones","Lista y obtén las conversaciones de un equipo en todos los canales, con su estado, la persona asignada, si una persona ha tomado el control y cuándo llegaron y salieron los últimos mensajes. Nunca el contenido de los mensajes.","conversations",{"path":1252,"title":1253,"description":1254,"apiTag":1255},"\u002Fdevelopers\u002Fes\u002Freference-bookings","Citas y servicios","Lista y obtén las citas de un equipo, y lista los servicios que pueden reservar sus clientes.","bookings",{"path":1257,"title":1258,"description":1259,"apiTag":1182},"\u002Fdevelopers\u002Fes\u002Freference-sync","Feed de cambios","Un único endpoint que copia un equipo y, después, devuelve cada cambio, página a página.",{"path":1261,"title":1262,"description":1263},"\u002Fdevelopers\u002Fes\u002Fobjects","Todos los objetos","Todos los objetos que devuelve la API, campo a campo, con un ejemplo de cada uno.",{"section":1265,"items":1266},"resources",[1267,1271,1275,1279],{"path":1268,"title":1269,"description":1270},"\u002Fdevelopers\u002Fes\u002Fchangelog","Registro de cambios","Los cambios de la API de Totem, del más reciente al más antiguo, cada uno bajo la fecha en que se publicó.",{"path":1272,"title":1273,"description":1274},"\u002Fdevelopers\u002Fes\u002Fsupport","Soporte","Cómo pedir ayuda con la API de Totem, qué incluir en una solicitud y qué hacer si se filtra una clave.",{"path":1276,"title":1277,"description":1278},"\u002Fdevelopers\u002Fes\u002Fglossary","Glosario","Los términos de esta documentación, definidos en un solo lugar, cada uno con un enlace a la página que lo explica.",{"path":1280,"title":1281,"description":1282},"\u002Fdevelopers\u002Fes\u002Ffaq","Preguntas frecuentes","Respuestas breves a las preguntas más habituales sobre la API de Totem, con enlaces a los detalles.","\u002Fdevelopers\u002Fsync",{"ACTIVATION_MARGIN_MS":1285,"ADMIN_ACCESS_DISCLOSURE_VERSION":1286,"AUDIT_RETENTION_DAYS":1287,"AUTH_FAILURE_LIMITS":1288,"CAPTURE_LAG_FLOOR_MS":1292,"CAPTURE_LAG_P99_MS":7,"CURSOR_MAX_AGE_DAYS":1293,"DIRTY_TTL_DAYS":1293,"FAMILY_TEAM_SCOPE":1294,"FAMILY_TEAM_SCOPE_OPTIONS":1295,"FEED_SETTLE_LAG_MS":1297,"GATE_REFRESH_MS":1285,"INTEGRATION_NAME_LENGTH":1298,"INTEGRATION_PURPOSE_LENGTH":1300,"KEY_CACHE_TTL_MS":1303,"KEY_STATS_RETENTION_DAYS":1304,"LEAD_RESPONSE_READ_BUDGET":1305,"LIST_SCAN_MAX":1308,"MAX_FAMILY_INTEGRATION_TEAMS":1293,"MAX_INTEGRATIONS_PER_FAMILY":1309,"MAX_IP_ALLOWLIST":1290,"PAGE_LIMIT":1310,"PLATFORM_CONFIG_TTL_MS":1313,"REMOVAL_RETENTION_DAYS":1314,"REQUEST_BODY_MAX_BYTES":1315,"ROLL_GRACE_DEFAULT_HOURS":1316,"ROLL_GRACE_HOURS":1317,"SEARCH_MAX_RESULTS":1309,"SENSITIVE_SECTOR_POLICY":1320,"SENSITIVE_SECTOR_POLICY_OPTIONS":1321,"SYNC_DAILY_BUDGET":1323,"SYNC_VELOCITY":1325,"TEAM_CACHE_TTL_MS":1327},60000,"2026-10-17",365,{"perIpPerMinute":1289,"perLookupIdPerMinute":1290,"banMs":1291},60,20,600000,300000,30,"explicit_list",[1294,1296],"all_family_teams",10000,{"min":1146,"max":1299},80,{"min":1301,"max":1302},1,280,5000,90,{"conversations":1290,"messageReads":1306,"calls":1307},200,50,1000,10,{"default":1311,"max":1312},25,100,15000,45,16384,24,[1318,1301,1316,1319],0,168,"exclude",[1320,1322],"allow",{"floor":1324,"recordsMultiplier":1143},250000,{"perMinute":1326,"burst":1297},2000,30000,"1.0a","guide",[1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350],{"id":30,"text":31,"depth":1146},{"id":110,"text":111,"depth":1143},{"id":181,"text":182,"depth":1146},{"id":245,"text":246,"depth":1146},{"id":297,"text":167,"depth":1146},{"id":363,"text":364,"depth":1146},{"id":386,"text":387,"depth":1146},{"id":412,"text":413,"depth":1146},{"id":416,"text":417,"depth":1143},{"id":472,"text":473,"depth":1143},{"id":493,"text":494,"depth":1143},{"id":541,"text":542,"depth":1146},{"id":580,"text":428,"depth":1146},{"id":766,"text":767,"depth":1143},{"id":794,"text":795,"depth":1146},{"id":819,"text":820,"depth":1146},{"id":869,"text":870,"depth":1146},{"id":893,"text":894,"depth":1146},{"id":948,"text":949,"depth":1143},{"id":1128,"text":1129,"depth":1146},{"path":915,"title":916,"section":1179},{"path":1206,"title":1207,"section":1179},[],1792502791291]