Google Calendar
Conectá la cuenta de Google de cada negocio con un link y tu sistema puede crear, mover y borrar eventos en sus calendarios, listar la agenda y consultar horarios ocupados. El negocio autoriza una vez; Sabado se ocupa del OAuth, de renovar el acceso y de la verificación de la app con Google.
Los calendarios son del negocio. Sabado no crea, no
nombra y no modifica calendarios: usás los que la cuenta de Google ya
tiene y en los que puede escribir. Cada pedido indica en qué calendario
(calendar_id), o se usa el calendario por defecto que el
negocio eligió en el panel.
Conectar el calendario
GET /v1/calendar/connect
Devuelve el link al que entra el dueño del negocio: se loguea con su cuenta de Google, acepta los permisos y listo. El link vence a los 15 minutos y sirve una sola vez: pedilo justo antes de mostrarlo.
Scope: calendar.write.
GET /v1/calendar/connect?tenant_ref=inmobiliaria-centro
Authorization: Bearer TU_API_KEY
{
"tenant_ref": "inmobiliaria-centro",
"connect_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=..."
}
Google le muestra a la persona los permisos con casillas: tiene que
dejar todos tildados (ver la lista de calendarios,
crear y editar eventos, ver disponibilidad). Si destilda alguno, la
conexión no se hace y se le explica por qué. Al terminar ve una página
de Sabado con el resultado, o vuelve a tu sistema si mandaste
return_url.
También se conecta desde el panel: Google Calendar → Conectar Google Calendar. Para pasar de una cuenta de prueba a la real, desconectá y volvé a conectar con la otra cuenta (o conectá directo con la otra: reemplaza a la anterior).
Volver a tu sistema al terminar
Si la conexión se arranca desde tu sistema, mandá return_url
(https) y al terminar la persona vuelve ahí, en vez de quedarse en la
página de Sabado:
GET /v1/calendar/connect?tenant_ref=inmobiliaria-centro&return_url=https%3A%2F%2Fcrm.tuempresa.com%2Fajustes%2Fcalendario
Authorization: Bearer TU_API_KEY
Vuelve con el resultado en la URL:
https://crm.tuempresa.com/ajustes/calendario?status=connected&tenant_ref=inmobiliaria-centro
https://crm.tuempresa.com/ajustes/calendario?status=error&reason=denied&tenant_ref=inmobiliaria-centro
| reason | Qué pasó |
|---|---|
denied | La persona canceló en la pantalla de Google. |
scopes_missing | Destildó algún permiso. Hay que volver a conectar y dejar todos tildados. |
token_exchange_failed | Google no completó la autorización. Reintentar. |
missing_params, internal_error, calendar_unavailable | Algo falló en el camino. Reintentar. |
La URL de vuelta no es prueba de nada: cualquiera puede
escribirla a mano. Usala para mostrar el mensaje y confirmá siempre con
GET /v1/calendar/status. Si el link
de conexión ya había vencido, no hay forma de saber a dónde volver: la
persona ve la página de Sabado y tiene que arrancar de nuevo desde tu
sistema.
Estado de la conexión
GET /v1/calendar/status
Scope: calendar.read.
GET /v1/calendar/status?tenant_ref=inmobiliaria-centro
Authorization: Bearer TU_API_KEY
{
"tenant_ref": "inmobiliaria-centro",
"connected": true,
"revoked": false,
"account_email": "agenda@inmobiliariacentro.com.ar",
"connected_at": "2026-10-05T14:02:11+00:00",
"revoked_at": null,
"default_calendar_id": "c_1a2b3c@group.calendar.google.com"
}
revoked: true significa que Google cortó el
acceso (el dueño lo sacó desde su cuenta de Google, cambió algo
de seguridad, o la conexión estuvo seis meses sin usarse). Hay que
volver a conectar: mientras tanto las operaciones devuelven
409 calendar_access_revoked.
Calendarios
GET /v1/calendar/calendars
Los calendarios de la cuenta conectada en los que se puede escribir (propios y compartidos con permiso de edición). Usalo para que el negocio elija uno desde tu sistema.
Scope: calendar.read.
{
"tenant_ref": "inmobiliaria-centro",
"data": [
{
"id": "agenda@inmobiliariacentro.com.ar",
"name": "agenda@inmobiliariacentro.com.ar",
"primary": true,
"access_role": "owner",
"time_zone": "America/Argentina/Buenos_Aires",
"is_default": false
},
{
"id": "c_1a2b3c@group.calendar.google.com",
"name": "Visitas",
"primary": false,
"access_role": "writer",
"time_zone": "America/Argentina/Buenos_Aires",
"is_default": true
}
]
}
is_default marca el calendario por defecto que el negocio
eligió en el panel. Es opcional: si no hay, cada pedido tiene que traer
calendar_id.
Crear un evento
POST /v1/calendar/events
Scope: calendar.write.
POST /v1/calendar/events
Authorization: Bearer TU_API_KEY
Idempotency-Key: visita-V-12
Content-Type: application/json
{
"tenant_ref": "inmobiliaria-centro",
"calendar_id": "c_1a2b3c@group.calendar.google.com",
"title": "Visita — depto Oroño 1200",
"start": "2026-10-10T15:00:00",
"end": "2026-10-10T15:45:00",
"time_zone": "America/Argentina/Buenos_Aires",
"location": "Bv. Oroño 1200, Rosario",
"description": "Cliente: Ana Pérez"
}
| Campo | Qué es |
|---|---|
title | Obligatorio. |
start, end | Obligatorios. Fecha y hora ISO 8601. Con time_zone pueden ir sin offset; sin time_zone tienen que traerlo (2026-10-10T15:00:00-03:00). end posterior a start. |
time_zone | Opcional. Zona IANA, ej. America/Argentina/Buenos_Aires. |
calendar_id | Opcional si el negocio tiene calendario por defecto. |
location, description | Opcionales. |
Respuesta (201):
{
"tenant_ref": "inmobiliaria-centro",
"replayed": false,
"event": {
"id": "9f2c4e1a7b3d4c5e8f60718293a4b5c6",
"calendar_id": "c_1a2b3c@group.calendar.google.com",
"status": "confirmed",
"title": "Visita — depto Oroño 1200",
"description": "Cliente: Ana Pérez",
"location": "Bv. Oroño 1200, Rosario",
"all_day": false,
"start": "2026-10-10T15:00:00-03:00",
"end": "2026-10-10T15:45:00-03:00",
"time_zone": "America/Argentina/Buenos_Aires",
"html_link": "https://www.google.com/calendar/event?eid=...",
"created_at": "2026-10-05T14:10:00.000Z",
"updated_at": "2026-10-05T14:10:00.000Z"
}
}
Guardá event.id (y el calendar_id): es lo que
usás para cambiarlo o borrarlo. Sin invitados: el evento no le manda
nada a nadie.
Crear dos veces no duplica
Mandá Idempotency-Key con un código único de tu sistema (por
ejemplo el de la visita). Si reintentás con la misma key —porque se
cortó la conexión, o tu sistema no supo si salió— te devolvemos el
mismo evento con 200 y
"replayed": true, en vez de crear otro. La key vale por
negocio: dos negocios pueden usar el mismo código.
Si el evento de esa key ya fue borrado, reintentar no lo
vuelve a crear: devuelve 404 event_not_found con
"deleted": true. Para agendarlo de nuevo, usá otra key.
Cambiar un evento
PATCH /v1/calendar/events/{event_id}
Solo cambia lo que mandes (title, start,
end, time_zone, location,
description); lo demás queda igual. Para mover una visita,
mandá start y end.
Scope: calendar.write.
PATCH /v1/calendar/events/9f2c4e1a7b3d4c5e8f60718293a4b5c6
Authorization: Bearer TU_API_KEY
Content-Type: application/json
{
"tenant_ref": "inmobiliaria-centro",
"calendar_id": "c_1a2b3c@group.calendar.google.com",
"start": "2026-10-11T10:00:00-03:00",
"end": "2026-10-11T10:45:00-03:00"
}
Devuelve { "tenant_ref", "event" } con el evento actualizado.
Borrar un evento
DELETE /v1/calendar/events/{event_id}
Scope: calendar.write.
DELETE /v1/calendar/events/9f2c4e1a7b3d4c5e8f60718293a4b5c6?tenant_ref=inmobiliaria-centro&calendar_id=c_1a2b3c%40group.calendar.google.com
Authorization: Bearer TU_API_KEY
{
"tenant_ref": "inmobiliaria-centro",
"deleted": true,
"event_id": "9f2c4e1a7b3d4c5e8f60718293a4b5c6",
"already_deleted": false
}
Borrar algo que ya estaba borrado no es error:
200 con "already_deleted": true. Un id que no
existe en ese calendario es 404 event_not_found (también
si el calendar_id no es el del evento).
Ver un evento
GET /v1/calendar/events/{event_id}
Scope: calendar.read.
GET /v1/calendar/events/9f2c4e1a7b3d4c5e8f60718293a4b5c6?tenant_ref=inmobiliaria-centro&calendar_id=c_1a2b3c%40group.calendar.google.com
Authorization: Bearer TU_API_KEY
Devuelve { "tenant_ref", "event" }. Si no existe, o fue
borrado, 404 event_not_found (con "deleted": true
cuando Google todavía lo recuerda como borrado).
Listar la agenda
GET /v1/calendar/events
Eventos de un rango, ordenados por inicio. Los eventos que se repiten vienen expandidos (una entrada por cada vez).
Scope: calendar.read.
GET /v1/calendar/events?tenant_ref=inmobiliaria-centro&time_min=2026-10-10T00:00:00&time_max=2026-10-17T00:00:00&time_zone=America/Argentina/Buenos_Aires
Authorization: Bearer TU_API_KEY
{
"tenant_ref": "inmobiliaria-centro",
"data": [ { "id": "...", "title": "...", "start": "...", ... } ],
"next_page_token": null
}
time_minytime_maxson obligatorios. Entra todo evento que se superpone con el rango: uno que empezó antes detime_minpero termina después, aparece.calendar_idopcional (default del negocio),max_resultsde 1 a 2500 (250 por defecto).- Si vuelve
next_page_token, hay más: pasalo comopage_token. Una página puede venir corta o vacía y aun así tener siguiente. - Los eventos de día completo vienen con
"all_day": trueystart/endcomo fecha (endes el día siguiente al último).
Horarios ocupados
POST /v1/calendar/freebusy
Los tramos ocupados de uno o más calendarios (hasta 50) en un rango. Lo que no figura como ocupado está libre.
Scope: calendar.read.
POST /v1/calendar/freebusy
Authorization: Bearer TU_API_KEY
Content-Type: application/json
{
"tenant_ref": "inmobiliaria-centro",
"time_min": "2026-10-10T08:00:00-03:00",
"time_max": "2026-10-10T20:00:00-03:00",
"calendar_ids": ["c_1a2b3c@group.calendar.google.com"]
}
{
"tenant_ref": "inmobiliaria-centro",
"calendars": [
{
"calendar_id": "c_1a2b3c@group.calendar.google.com",
"busy": [ { "start": "2026-10-10T18:00:00Z", "end": "2026-10-10T18:45:00Z" } ],
"errors": []
}
]
}
Sin calendar_ids se consulta el calendario por defecto. Si
un calendario vuelve con errors (por ejemplo
notFound), su ocupación es desconocida, no
libre.
Desconectar
POST /v1/calendar/disconnect
Scope: calendar.write.
POST /v1/calendar/disconnect
Authorization: Bearer TU_API_KEY
Content-Type: application/json
{ "tenant_ref": "inmobiliaria-centro" }
{
"tenant_ref": "inmobiliaria-centro",
"disconnected": true,
"revoked_at_google": true,
"already_revoked": false
}
Le saca a Sabado el acceso a esa cuenta de Google. La baja es honesta:
si Google no la confirma, devolvemos 502 y la conexión sigue
activa. Si otro negocio de Sabado usa la misma cuenta de
Google, no se le revoca el acceso a Google (le cortaría el calendario al
otro): se desconecta solo este negocio y
revoked_at_google viene en false. Los eventos
ya creados quedan en el calendario.
Errores
| HTTP | error | Qué pasó |
|---|---|---|
| 409 | calendar_not_connected | El negocio no conectó Google Calendar. |
| 409 | calendar_access_revoked | Google cortó el acceso. Hay que volver a conectar (connect). |
| 422 | calendar_required | Falta calendar_id y el negocio no eligió calendario por defecto. |
| 404 | calendar_not_found | El calendario no existe o la cuenta conectada no tiene acceso. |
| 404 | event_not_found | El evento no existe en ese calendario. Con "deleted": true, fue borrado. |
| 403 | calendar_forbidden | Google no permite la operación (por ejemplo, un calendario de solo lectura). |
| 429 | calendar_rate_limited | Google pide esperar. Reintentá en unos segundos (con la misma Idempotency-Key si estabas creando). |
| 503 | calendar_unavailable | Google no responde. Reintentá en un rato. |
| 422 | invalid_request | Google rechazó los datos; el message dice por qué. |
| 502 | calendar_error | Otra respuesta inesperada de Google. |
| 403 | plan_not_included | El plan de tu cuenta no incluye Google Calendar. |
| 503 | not_configured | Google Calendar todavía no está habilitado en la plataforma. |
Los errores generales (401, 403 insufficient_scope,
404 tenant_not_found, 422 de validación) son los
de siempre: ver Errores. Ver el estado y
desconectar funcionan aunque el plan no incluya el servicio.
Para probar
Conectá una cuenta de Google de prueba (o un calendario de prueba dentro
de la cuenta real, y usá su calendar_id). Cuando esté todo
andando, desconectá y conectá la cuenta real, o cambiá el
calendar_id.