Integração com Google Agenda: guarde o id do evento e sobreviva ao acesso revogado
Modele compromissos no seu banco com o id do evento remoto e um status de sincronização, para que token expirado ou acesso revogado não apague a agenda da tela.
Segunda de manhã, a clínica abre o sistema e a semana está vazia. Ninguém desmarcou nada: a dona da conta Google trocou a senha no fim de semana, o token de acesso parou de valer e a tela, que lia os eventos direto da agenda dela, passou a mostrar zero compromissos. A recepção começa a ligar para os pacientes perguntando o horário. A resposta curta: o compromisso confirmado mora no seu banco, com o id do evento remoto guardado ao lado para atualizar ou apagar depois. A agenda externa é uma cópia sincronizada. Quando a sincronização falha, a tela mostra os seus dados e um aviso de falha, nunca uma semana vazia.
Provedor externo é integração, não disco. Ele pode sair do ar, revogar o acesso ou ser desconectado pela própria pessoa, e o seu sistema precisa continuar sabendo quem vem às 9h.
Por que a agenda some quando o acesso cai
Componentes de calendário como o FullCalendar conseguem ler eventos direto de uma agenda Google. Para uma página pública de eventos, isso basta. Para um sistema que marca consultas, é um atalho perigoso: o único lugar onde o compromisso existe passa a ser uma conta que você não controla.
O acesso de um app a uma conta Google depende de tokens OAuth. O token de acesso expira em pouco tempo e é renovado com um token de atualização. Esse token de atualização pode deixar de valer por vários motivos: a pessoa remove o acesso do app na conta, troca a senha em certas configurações, o app fica muito tempo sem usá-lo. Quando isso acontece, a renovação falha com o erro invalid_grant, e a única solução é a pessoa autorizar de novo.
Se a tela trata "não consegui ler" como "não há eventos", a falha vira um dado falso. É pior do que um erro, porque parece verdade.
Como modelar o compromisso com o evento remoto
O exemplo é a agenda da clínica: cada consulta nasce no sistema e é copiada para a agenda Google da profissional. A tabela guarda o compromisso inteiro, o id que o Google devolveu e o estado da última sincronização.
CREATE TABLE compromissos (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
titulo VARCHAR(200) NOT NULL,
inicio_utc DATETIME NOT NULL,
fim_utc DATETIME NOT NULL,
provedor VARCHAR(20) NULL,
evento_remoto_id VARCHAR(1024) CHARACTER SET ascii NULL,
sync_status VARCHAR(30) NOT NULL DEFAULT 'pendente',
sync_erro VARCHAR(500) NULL,
sincronizado_em DATETIME NULL,
UNIQUE KEY uk_evento_remoto (provedor, evento_remoto_id)
);
Três escolhas importam aqui. Os horários ficam em UTC, e a conversão para o fuso da clínica acontece na exibição. O id remoto é texto em ASCII, o que mantém o índice único dentro do limite de tamanho do MySQL. E sync_status separa os estados que a tela precisa distinguir: pendente, ok, falhou, acesso_revogado e removido_no_provedor.
O passo a passo da sincronização
- Grave o compromisso no seu banco primeiro, com
sync_status = 'pendente'. A consulta está marcada a partir daqui, aconteça o que acontecer com o Google. - Envie para o provedor fora da requisição da tela, numa fila ou tarefa agendada. A recepção não espera a API do Google para ver a confirmação.
- Guarde o id devolvido e marque
ok. Sem o id, você não consegue atualizar nem apagar o evento depois. - Na alteração, use o id para atualizar o mesmo evento, em vez de criar outro.
- Classifique a falha: acesso revogado pede nova autorização; erro temporário pede nova tentativa; evento apagado do lado de lá pede uma decisão, não uma recriação automática.
- Peça só o escopo necessário. Para criar e editar eventos, o escopo
https://www.googleapis.com/auth/calendar.eventsbasta; não peça acesso completo à agenda se não vai gerenciar calendários.
O código que envia e registra o resultado
O exemplo usa a API REST do Google Calendar com fetch, em Node.js. A função obterAccessToken é a sua rotina de renovação OAuth; ela lança um erro com codigo = 'invalid_grant' quando a renovação é recusada.
const BASE = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
async function enviarParaGoogle(token, c) {
const url = c.evento_remoto_id ? BASE + '/' + encodeURIComponent(c.evento_remoto_id) : BASE;
const resposta = await fetch(url, {
method: c.evento_remoto_id ? 'PATCH' : 'POST',
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({
summary: c.titulo,
start: { dateTime: c.inicio_utc.toISOString() },
end: { dateTime: c.fim_utc.toISOString() }
})
});
if (!resposta.ok) {
const erro = new Error('Google respondeu ' + resposta.status);
erro.status = resposta.status;
throw erro;
}
return resposta.json();
}
async function sincronizar(db, c) {
try {
const token = await obterAccessToken(c.usuario_id);
const evento = await enviarParaGoogle(token, c);
await db.query(
'UPDATE compromissos SET evento_remoto_id = ?, sync_status = ?, sync_erro = NULL, sincronizado_em = UTC_TIMESTAMP() WHERE id = ?',
[evento.id, 'ok', c.id]
);
} catch (erro) {
let status = 'falhou';
if (erro.codigo === 'invalid_grant') status = 'acesso_revogado';
else if (erro.status === 404 || erro.status === 410) status = 'removido_no_provedor';
await db.query(
'UPDATE compromissos SET sync_status = ?, sync_erro = ? WHERE id = ?',
[status, String(erro.message).slice(0, 500), c.id]
);
}
}
Repare que nenhum caminho de erro apaga o compromisso. O pior que acontece é ele ficar marcado como não sincronizado, e a tela saber disso.
Existe um buraco clássico nessa sequência: o Google cria o evento, mas o UPDATE que guarda o id falha. Na próxima tentativa, o sistema cria um segundo evento. A API do Google Calendar permite que você informe o id do evento na criação, seguindo as regras de formato da documentação dela. Gerar esse id a partir do id local torna a criação repetível sem duplicar, a mesma ideia de idempotência usada em pagamentos.
O que a tela mostra quando a sincronização falha
A grade da clínica lê a sua API, não a agenda Google. A resposta traz os compromissos do banco e um resumo do estado da integração:
{
"compromissos": [
{ "id": 812, "titulo": "Consulta - Ana P.", "inicio": "2026-10-19T12:00:00Z", "sync": "acesso_revogado" }
],
"integracao": {
"ok": false,
"mensagem": "A agenda Google de Dra. Lúcia está desconectada. Os horários abaixo continuam valendo. Reconecte para voltar a sincronizar."
}
}
A tela desenha a semana normalmente e mostra o aviso no topo, com um botão para reconectar. A recepção trabalha; a profissional resolve a conexão quando puder. O post sobre a grade de calendário que lê a sua API mostra o outro lado: como a tela pede alterações sem gravar direto no provedor. E se a dúvida é onde o compromisso deve nascer, o guia agenda do Google não é o banco do pedido trata disso.
Erros comuns
- Ler os eventos direto do provedor como fonte única: no dia em que o token cai, a agenda some e ninguém sabe quem está marcado.
- Não guardar o id do evento remoto: cada alteração cria um evento novo e a agenda da profissional fica cheia de duplicatas.
- Tratar falha de leitura como lista vazia: a tela afirma que não há compromissos, e a recepção acredita.
- Recriar automaticamente o evento apagado do lado de lá: se a profissional apagou de propósito, o sistema desfaz a decisão dela em silêncio.
- Pedir o escopo mais amplo "por garantia": a tela de consentimento assusta, e um vazamento do token expõe mais do que o necessário.
- Sincronizar dentro da requisição de quem marcou: uma lentidão do provedor vira lentidão da sua tela, e um erro dele vira erro ao marcar.
Checklist
- Todo compromisso confirmado existe no seu banco, independente do provedor.
- O id do evento remoto fica gravado ao lado do compromisso.
- Existe um status de sincronização com estados distintos para revogado, falha e removido.
- A falha de sincronização aparece como aviso, com os compromissos ainda visíveis.
- A sincronização roda fora da requisição da tela, com nova tentativa para erro temporário.
- O escopo OAuth pedido é o mínimo para o que o sistema faz.
- Horários são gravados em UTC e convertidos na exibição.
Num ambiente de teste, remova o acesso do seu app nas configurações de segurança da conta Google e abra a agenda do sistema. O que a sua tela ainda mostra é o que você realmente possui.
Referência: https://fullcalendar.io/docs/google-calendar