Agenda e Google Calendar pela API

Reserve compromissos sem sobreposição, consulte horários livres e sincronize a agenda da equipe com o Google Calendar.

Use a base https://botozap.com.br/api/v1 com Authorization: Bearer $BOTOZAP_KEY ou X-API-Key. A Conta é derivada da chave; IDs de contato, serviço, pessoa, conversa e registros de CRM são validados no servidor. Os exemplos mostram o caminho depois desse prefixo.

A Agenda usa appointments:read e appointments:write; o Google Calendar usa calendar:read e calendar:write, escopos independentes. Todas estas rotas exigem chave live: chave sandbox recebe 403 sandbox_forbidden. Para vincular compromissos a oportunidades e demandas, consulte CRM e Radar.

Rotas

MétodoCaminhoResultado
GET / POST/appointmentsListar ou criar compromissos.
GET / PATCH / DELETE/appointments/{id}Consultar, editar ou excluir.
GET/appointments/{id}/historyHistórico do compromisso.
GET/appointments/availabilityHorários livres de uma pessoa para um serviço.
GET / POST/appointments/servicesListar ou criar serviços.
PATCH/appointments/services/{id}Editar ou desativar um serviço.
GET / POST/appointments/schedulesListar ou criar jornadas.
PATCH/appointments/schedules/{id}Editar uma jornada.
GET / POST/appointments/exceptionsListar ou criar exceções.
PATCH / DELETE/appointments/exceptions/{id}Editar ou remover uma exceção.

Rotas do Google Calendar estão em Google Calendar e Meet. IDs malformados no caminho ou em filtros retornam 422 invalid_id.

Compromissos

CampoTipoRegras
contact_idUUIDObrigatório na criação e imutável. O Cliente do compromisso vem do contato; customer_id no corpo é ignorado.
scheduled_atISO 8601Início, com fuso horário. Obrigatório na criação.
ends_atISO 8601Término. Padrão: início mais a duração do serviço, ou 60 minutos sem serviço. Remarcar só o início preserva a duração.
titletexto1 a 200 caracteres. Padrão Agendamento.
time_zonetextoFuso IANA, como America/Sao_Paulo (padrão).
statusenumpending, confirmed, cancelled, completed, no_show. Padrão confirmed.
service_idUUID ou nullServiço ativo do mesmo Cliente. Aplica duração, buffers, antecedência e horizonte.
owner_user_idUUID ou nullMembro da Conta responsável. Com responsável, a reserva segue a jornada e bloqueia sobreposições.
cancellation_reasontexto ou nullAté 2000 caracteres. Obrigatório para cancelled.
locationtexto ou nullAté 1000 caracteres.
notetexto ou nullAté 16000 caracteres.
conversation_idUUID ou nullConversa do mesmo contato.
opportunity_id, demand_idUUID ou nullRegistro de CRM do mesmo contato e Cliente.
meeting_requestedbooleanoSolicita link do Google Meet. Veja Meet.

A resposta traz o compromisso em data, com id, customer_id, revision, buffer_before_minutes, buffer_after_minutes, meeting_url, o estado da sincronização Google (google_sync_error, google_conflict) e os timestamps. Campos desconhecidos no corpo são ignorados.

Campos obsoletos: google_etag e google_base_data são dados internos da sincronização com o Google Calendar e deixam de vir na resposta em 26/10/2026. Não dependa deles; o SDK 0.6.0 já os marca como obsoletos.

Estados

EstadoNo painelEfeito
pendingA confirmarAguarda confirmação; aparece no Radar como compromisso a confirmar.
confirmedConfirmadoPadrão na criação. Registra o momento da confirmação.
cancelledCanceladoExige cancellation_reason. Libera o horário e interrompe as réguas do compromisso.
completedCompareceuComparecimento registrado. Só depois do início do compromisso e nunca a partir de cancelled (422 invalid_request). Encerra lembretes anteriores e mantém o pós-atendimento.
no_showNão compareceuAusência registrada. Só depois do início do compromisso e nunca a partir de cancelled (422 invalid_request). Libera o horário e interrompe as réguas do compromisso.

Regras de reserva

Com owner_user_id e estado pending ou confirmed, criar, remarcar, trocar serviço ou responsável ou reabrir um compromisso passa por estas verificações:

  • Outro compromisso ativo da mesma pessoa, somando os buffers, retorna 409 time_conflict.
  • A pessoa precisa ter jornada publicada no Cliente, e o horário precisa caber numa janela do dia local, sem atravessar a meia-noite. Fora disso, 422 invalid_request.
  • Exceção available no dia substitui as janelas semanais; exceção busy sobreposta retorna 409 time_conflict.
  • Com serviço, o início respeita a antecedência mínima e o horizonte de reserva; fora disso, 422 invalid_request.
  • Ocupação importada do Google sobreposta retorna 409 time_conflict; ocupação desatualizada bloqueia a reserva com 422 invalid_request até sincronizar.

Sem responsável, as regras de jornada e ocupação não se aplicam. Para trocar o responsável de um compromisso já enviado ao Google, cancele-o, aguarde a sincronização e só então escolha a nova pessoa e o novo horário.

Criar com Idempotency-Key

POST /appointments
Idempotency-Key: pedido-8431-reserva

{
  "contact_id": "UUID-DO-CONTATO",
  "scheduled_at": "2026-10-05T14:00:00-03:00",
  "service_id": "UUID-DO-SERVICO",
  "owner_user_id": "UUID-DA-PESSOA",
  "title": "Avaliação inicial"
}

A resposta é 201. Envie Idempotency-Key de 8 a 200 caracteres para repetir a chamada com segurança: a mesma chave com o mesmo corpo devolve o compromisso original, sem nova reserva; a mesma chave com outro corpo, ou depois de excluído o compromisso original, retorna 409 conflict. Chave fora do tamanho retorna 422 invalid_request. Consultar a disponibilidade não reserva o horário.

Editar

PATCH /appointments/{id} recebe os campos que mudam e expected_revision com a revision lida. Versão obsoleta retorna 409 conflict. O PATCH legado, que altera apenas note e/ou scheduled_at, continua aceito sem revisão e grava sobre a versão atual. Qualquer outro campo sem revisão retorna 422 revision_required.

PATCH /appointments/{id}
{
  "expected_revision": 2,
  "status": "cancelled",
  "cancellation_reason": "Contato pediu para remarcar"
}

Consultar e excluir

GET /appointments/{id} retorna o compromisso. DELETE /appointments/{id} responde 204. Um compromisso vinculado a evento do Google só pode ser excluído depois de cancelado e com o cancelamento confirmado no Google; antes disso, 409 calendar_sync_pending. Alteração simultânea ou sincronização em andamento retornam 409 conflict.

Listar

ParâmetroValores
customer_id, contact_id, service_idUUID.
owner_user_idUUID ou unassigned.
statusUm dos estados acima; outro valor retorna 422 invalid_status.
dateYYYY-MM-DD: compromissos que começam nesse dia em UTC. Tem precedência sobre from/to.
from, toISO 8601 com fuso: compromissos que se sobrepõem ao intervalo. to precisa ser posterior a from.
page, per_pagePadrão 20, máximo 100.

A ordem é pelo início, do mais cedo para o mais tarde. Data inválida retorna 422 invalid_date; intervalo invertido, 422 invalid_range.

Histórico

GET /appointments/{id}/history?page=N retorna 20 registros por página, do mais recente para o mais antigo. Cada registro tem event_type (created, rescheduled, updated ou o novo estado), source (api, dashboard ou uma origem da sincronização Google), actor_user_id (null pela API), before_data, after_data e created_at.

Erros dos compromissos

StatusCódigoQuando
400invalid_jsonCorpo que não é JSON.
404not_foundCompromisso inexistente ou de outra Conta.
409conflictRevisão obsoleta, Idempotency-Key reutilizada com outro corpo ou sincronização em andamento.
409time_conflictHorário ocupado por outro compromisso, bloqueio ou evento do Google.
409calendar_sync_pendingExclusão antes de o cancelamento chegar ao Google.
422missing_scheduled_at, invalid_scheduled_atInício ausente na criação ou fora do formato ISO com fuso.
422invalid_contactcontact_id inválido ou de outra Conta.
422revision_requiredEdição de campo novo sem expected_revision.
422no_updatable_fieldsPATCH sem campo editável.
422invalid_date, invalid_range, invalid_statusFiltros inválidos na listagem.
422invalid_requestCampo, vínculo, responsável, jornada, antecedência ou motivo de cancelamento inválidos. A mensagem indica a regra.

Disponibilidade

GET /appointments/availability exige customer_id, owner_user_id, service_id, from e to. Na remarcação, envie exclude_id com o compromisso atual para que ele não ocupe o próprio horário.

GET /appointments/availability?customer_id=UUID&owner_user_id=UUID&service_id=UUID&from=2026-10-05T00:00:00-03:00&to=2026-10-12T00:00:00-03:00

{
  "data": {
    "time_zone": "America/Sao_Paulo",
    "slots": [
      { "starts_at": "2026-10-05T12:00:00.000Z", "ends_at": "2026-10-05T13:00:00.000Z" }
    ],
    "schedule_published": true
  }
}

Os horários seguem a jornada e as exceções da pessoa, a grade, duração, buffers, antecedência e horizonte do serviço, os compromissos ativos e a ocupação importada do Google. time_zone é o fuso da jornada; schedule_published é false quando a jornada não tem janelas.

  • Consulte até 31 dias por chamada; intervalo maior ou invertido retorna 422 invalid_range.
  • Pessoa sem jornada no Cliente retorna 422 schedule_missing.
  • Serviço inexistente, de outro Cliente ou inativo retorna 404 not_found, assim como exclude_id de outro Cliente.

Serviços, jornadas e exceções

As listagens exigem customer_id e retornam páginas fixas de 100 itens em ordem de criação; per_page é ignorado. POST retorna 201. PATCH exige expected_revision (422 revision_required sem ela; 409 conflict quando obsoleta) e valida o registro resultante. O Cliente de uma configuração não muda depois de criada.

Serviços

CampoRegras
customer_idUUID do Cliente.
name1 a 120 caracteres.
descriptionOpcional, até 2000 caracteres.
duration_minutes5 a 1440.
slot_minutesGrade dos horários oferecidos, 5 a 240.
buffer_before_minutes, buffer_after_minutes0 a 240.
minimum_notice_minutesAntecedência mínima, 0 a 43200.
booking_horizon_daysHorizonte de reserva, 1 a 366.
activeBooleano.

Na criação, todos os campos são obrigatórios, exceto description. Não há DELETE: desative com active: false. Compromissos existentes são preservados, mas o serviço inativo deixa de aceitar novas reservas e remarcações.

Jornadas da equipe

POST /appointments/schedules
{
  "customer_id": "UUID-DO-CLIENTE",
  "owner_user_id": "UUID-DA-PESSOA",
  "time_zone": "America/Sao_Paulo",
  "windows": [
    { "dow": 1, "start": "09:00", "end": "12:00" },
    { "dow": 1, "start": "13:00", "end": "18:00" },
    { "dow": 6, "start": "08:00", "end": "24:00" }
  ]
}

Cada pessoa tem uma jornada por Cliente; para mudar, use PATCH. Criar outra jornada para a mesma pessoa, ou trocar o owner_user_id para alguém que já tem jornada, retorna 409 schedule_exists. dow vai de 0 (domingo) a 6 (sábado); start é HH:MM; end é HH:MM ou 24:00 e precisa ser posterior ao início. São até 50 janelas, no fuso IANA informado.

Exceções

CampoRegras
customer_id, owner_user_idCliente e pessoa da exceção.
local_dateData local YYYY-MM-DD, no fuso da jornada.
start_minute0 a 1439, minutos desde a meia-noite.
end_minute1 a 1440, maior que start_minute.
kindavailable substitui as janelas daquele dia; busy bloqueia o intervalo.
reasonOpcional, até 500 caracteres.

Uma exceção com a mesma pessoa, local_date, intervalo e kind de outra já existente retorna 409 exception_exists, no POST e no PATCH.

DELETE /appointments/exceptions/{id} exige um corpo JSON e responde 204:

DELETE /appointments/exceptions/{id}
{ "expected_revision": 1 }

Sem corpo JSON, retorna 400 invalid_json; sem expected_revision inteiro, 422 revision_required; revisão obsoleta ou exceção inexistente, 409 conflict.

Google Calendar e Meet

Cada pessoa conecta a própria conta Google pelo painel. A API não inicia OAuth nem aceita user_id para representar uma pessoa, e nunca retorna tokens: as credenciais ficam no Vault. A chave com calendar:* opera sobre todas as conexões da Conta.

Conectar e reconectar

  1. Em Calendários, o painel abre /api/calendar/oauth/connect (para reconectar, com ?connection_id=UUID).
  2. A pessoa vê a página de uso de dados e confirma o consentimento da política de 24/09/2026. O envio é aceito só pela sessão logada, na mesma origem e na mesma Conta.
  3. O BotoZap registra o consentimento e redireciona ao Google com PKCE e acesso offline.
  4. O Google volta para /api/calendar/oauth/callback; o BotoZap guarda as credenciais e carrega o catálogo de calendários.

É preciso conceder leitura e edição de eventos e leitura dos calendários e da ocupação. Uma autorização parcial é recusada. Quando o Google revoga o acesso, a conexão passa a reauth_required e deve ser reconectada.

Rotas

OperaçãoContrato
GET /calendar/connectionsConexões da Conta, paginadas: id, owner_user_id, label, status (connected, reauth_required, disconnected ou error), last_error, last_synced_at e created_at.
GET /calendar/connections/{id}/calendarsCatálogo completo da conexão, sem paginação.
POST /calendar/connections/{id}/refreshRelê o catálogo no Google e devolve a mesma lista.
PATCH /calendar/selections/{id}Define destino e ocupação de um calendário do catálogo.
GET /calendar/jobsFila de sincronização; filtro opcional connection_id.
POST /calendar/jobs/{id}/retrySolicita nova tentativa.
POST /calendar/conflicts/{id}/resolveResolve o conflito de um compromisso.
DELETE /calendar/connections/{id}Desconecta; 204.

Calendários e seleção

Cada item do catálogo traz id, connection_id, external_calendar_id, summary, time_zone, access_role, destination, include_busy, conference_supported, synced_at e revision. Use esse id em /calendar/selections/{id}; o ID do Google fica em external_calendar_id.

PATCH /calendar/selections/{id}
{ "destination": true, "include_busy": true, "expected_revision": 1 }

200
{ "data": { "updated": true } }
  • Os três campos são obrigatórios; faltando algum, 422 invalid_request.
  • destination recebe os compromissos novos da pessoa e exige calendário com permissão de escrita (owner ou writer); cada pessoa tem um destino, e marcar um desmarca o anterior.
  • include_busy importa a ocupação do calendário para bloquear horários. Desmarcar remove a ocupação importada.
  • Revisão obsoleta retorna 409 conflict; conexão não conectada retorna 404 not_found.
  • Calendário que some do catálogo perde destino e ocupação.

Mudar o destino vale para compromissos novos: os já vinculados continuam sincronizando no calendário original. A ocupação externa impede sobreposições sem revelar títulos nem convidados de eventos pessoais à equipe; cache ausente ou com mais de 15 minutos bloqueia novas reservas da pessoa até sincronizar.

Sincronização, tentativas e conflitos

Alterar compromisso de uma pessoa com destino conectado enfileira o envio ao Google; mudanças feitas no Google voltam pela leitura do calendário. Cada job traz id, connection_id, selection_id, appointment_id, kind (pull ou push), status (pending, processing, succeeded, failed ou conflict), attempts, last_error, available_at e updated_at, do mais recente para o mais antigo. A consulta não executa sincronizações.

POST /calendar/jobs/{id}/retry responde { "data": { "queued": true } }; job em processamento retorna 409 conflict. Quando o compromisso mudou nos dois lados, ele fica com google_conflict. Resolva com o ID do compromisso:

POST /calendar/conflicts/{appointment_id}/resolve
{ "choice": "local", "expected_revision": 4 }

local mantém a versão do BotoZap e a reenvia ao Google; remote aplica título, horários, local e cancelamento do Google. Um cancelamento desfeito no Google reabre o compromisso como pending; os demais estados do BotoZap são preservados. A resposta traz o compromisso atualizado. Sem conflito, 404 not_found; revisão obsoleta, 409 conflict; conflito ainda sem a versão do Google, 422 invalid_request.

Desconectar

DELETE /calendar/connections/{id} remove a conexão, as credenciais, as seleções e a ocupação importada, e desfaz o vínculo dos compromissos com os eventos (incluindo meeting_url). Compromissos locais e eventos já criados no Google são preservados. O BotoZap também tenta revogar o acesso no Google; uma falha nessa revogação não impede a desconexão local.

Meet

Envie meeting_requested: true no compromisso para pedir um link do Google Meet. A criação é assíncrona: consulte meeting_url e o estado da sincronização depois do envio. O calendário de destino precisa aceitar conferências (conference_supported).

Erros do Google Calendar

Falhas do Google em chamadas síncronas, como /calendar/connections/{id}/refresh, retornam google_error com o status HTTP recebido do Google. Sem credenciais do Google configuradas no ambiente, a resposta é 503 not_configured. Conexão, calendário ou job de outra Conta retornam 404 not_found.

Réguas e Radar

Réguas com gatilho de agendamento podem filtrar por appointment_service_id. Remarcar reprograma o acompanhamento; cancelar ou registrar ausência interrompe os passos pendentes. Comparecimento encerra lembretes anteriores e preserva o pós-atendimento configurado com horas positivas. Consulte Réguas e consentimento.

No Radar, compromissos aparecem como entity_type: "appointment", com os motivos confirmation_pending, appointment_outcome_missing, calendar_conflict e calendar_sync_failed. Um compromisso futuro vinculado a uma oportunidade ou demanda conta como próximo passo dela.

Eventos

Criar um compromisso publica crm.appointment_created; alterá-lo publica crm.appointment_updated, com before_data e after_data. Consulte o formato em Eventos de webhook.

Os métodos existentes de SDK, CLI e MCP mantêm compatibilidade. Métodos específicos de configuração da Agenda dependem de publicação própria desses pacotes; os endpoints REST acima já estão disponíveis.