Ir al contenido

Sincronización incremental con updated_after

Usted quiere una copia de los clientes, las ventas o las OS en su sistema, y quiere mantenerla al día sin descargar todo de nuevo cada hora. Para eso existe el filtro updated_after: trae solo lo que cambió desde un instante, en el orden en que cambió.

Los ejemplos usan clientes, pero lo mismo vale para toda lista cuyo objeto tenga updated_at.

Toda lista acepta updated_after. Con él, cambian tres cosas:

  • El recorte. Solo viene el registro con updated_at posterior al instante que usted envió. El límite es exclusivo: el registro con updated_at exactamente igual al valor enviado no viene.
  • El orden. La lista pasa a venir por updated_at ascendente, de lo que cambió hace más tiempo a lo que cambió ahora. Sin updated_after, el orden es created_at descendente.
  • El desempate. Dos registros con el mismo updated_at salen siempre en el mismo orden, por el id. Es lo que evita que la paginación salte o repita registros entre una página y otra.

created_after y created_before también son exclusivos, y se pueden combinar con updated_after.

El cursor guarda el modo de ordenación. Por eso, envíe los mismos filtros en todas las páginas de la misma ronda, agregando solo cursor. Un cursor del orden por updated_at usado en una llamada sin updated_after responde 422: cambiar de modo a mitad de camino es empezar de nuevo. Vea Paginación.

  1. En la primera ronda no hay punto de partida. Envíe un instante muy antiguo, como 1970-01-01T00:00:00Z: eso trae todo, ya en el orden de sincronización.
  2. Lea página por página, siguiendo el next_cursor, hasta que has_more venga false.
  3. Grabe cada registro de su lado por el id, como actualización: si existe, lo sobrescribe; si no existe, lo crea.
  4. Después de cada página, guarde el mayor updated_at que vio. Ese es el punto de partida de la próxima ronda.
  5. En la próxima ronda, empiece un poco antes de ese punto, con una ventana de superposición.

Enviar exactamente el mayor updated_at visto funciona casi siempre. Los huecos están en los bordes:

  • el registro grabado en el mismo instante que el último que usted leyó, pero que todavía no estaba confirmado en la base cuando pasó su lectura, queda afuera, porque el límite es exclusivo;
  • una escritura larga puede recibir un updated_at un poco anterior al momento en que queda visible para la lectura.

Volver unos minutos resuelve los dos casos. Recomendamos 5 minutos: el costo es releer algunos registros en cada ronda, y la escritura por id del paso 3 ya vuelve inofensiva la relectura. Si quiere evitar el trabajo repetido, compare el updated_at que llegó con el que tiene e ignórelo cuando sea igual.

La ventana también lo protege cuando su proceso se cae a mitad de una ronda: usted reanuda desde el último punto guardado, menos la ventana, y nada queda atrás.

Ventana de terminal
# Una página de la ronda. El punto de partida es el mayor updated_at de la
# ronda anterior, menos la ventana de superposición.
curl -G "https://api.fatureihoje.com/public/v1/clients" \
-H "Authorization: Bearer $FH_API_KEY" \
--data-urlencode "updated_after=2026-09-24T12:55:00Z" \
--data-urlencode "limit=100"
# La página siguiente: los MISMOS filtros, más el next_cursor que vino.
curl -G "https://api.fatureihoje.com/public/v1/clients" \
-H "Authorization: Bearer $FH_API_KEY" \
--data-urlencode "updated_after=2026-09-24T12:55:00Z" \
--data-urlencode "limit=100" \
--data-urlencode "cursor=VALOR_DEL_NEXT_CURSOR"

El bucle respeta el 429: espera el Retry-After y repite la misma página. Una primera ronda sobre una base grande consume buena parte del presupuesto de llamadas de la empresa, que es uno solo para todas sus claves; use limit=100 y ejecútela fuera del horario pico. Vea Límites de uso.

Eliminaciones. Un registro eliminado desaparece de la lista, y por eso nunca aparece en una ronda. Lo mismo vale para una OS eliminada, que se archiva y pasa a responder 404. Para saber qué salió:

  • suscríbase a los eventos .deleted del área (client.deleted, service_order.deleted y los demás del Catálogo de eventos);
  • o, de vez en cuando, compare la lista completa de ids con su copia.

Una eliminación también puede tocar otros registros: cuando se elimina un cliente, sus tareas y citas pierden el vínculo. Al recibir un .deleted, vuelva a leer lo que en su sistema apuntaba al registro eliminado.

Listas sin updated_at. Tres listas tienen objetos que no cambian:

  • GET /public/v1/stock_movements y GET /public/v1/events: el movimiento de stock y el evento no cambian después de grabados. En ellas, updated_after recorta por el instante de creación, en orden ascendente, y la receta de arriba funciona igual usando created_at como punto de partida.
  • GET /public/v1/team: el objeto no trae fecha. La lista es chica; léala completa cuando la necesite.

La sincronización y el webhook no compiten. Cada uno cubre el punto débil del otro:

WebhookSincronización
Cuándo llegaEn el momentoEn la próxima ronda
Si su servidor se caeReintentos durante unos 3 días, después se pierdeReanuda desde el último punto guardado
Endpoint desactivadoLos eventos del período no lleganNo depende de un endpoint
Cambios sin eventoNo avisaLos trae, si el updated_at cambió

El diseño que funciona: el webhook avisa, la sincronización garantiza. Use el evento para actuar en el momento (leer el objeto y actualizar su lado) y ejecute la sincronización de vez en cuando, una vez por hora o por día, como red de seguridad. Las dos graban por id, así que recibir el mismo cambio por los dos caminos no hace daño.

Vea Buenas prácticas de webhooks y Entregas y reintentos.