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
reasonQué pasó
deniedLa persona canceló en la pantalla de Google.
scopes_missingDestildó algún permiso. Hay que volver a conectar y dejar todos tildados.
token_exchange_failedGoogle no completó la autorización. Reintentar.
missing_params, internal_error, calendar_unavailableAlgo 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"
}
CampoQué es
titleObligatorio.
start, endObligatorios. 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_zoneOpcional. Zona IANA, ej. America/Argentina/Buenos_Aires.
calendar_idOpcional si el negocio tiene calendario por defecto.
location, descriptionOpcionales.

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
}

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

HTTPerrorQué pasó
409calendar_not_connectedEl negocio no conectó Google Calendar.
409calendar_access_revokedGoogle cortó el acceso. Hay que volver a conectar (connect).
422calendar_requiredFalta calendar_id y el negocio no eligió calendario por defecto.
404calendar_not_foundEl calendario no existe o la cuenta conectada no tiene acceso.
404event_not_foundEl evento no existe en ese calendario. Con "deleted": true, fue borrado.
403calendar_forbiddenGoogle no permite la operación (por ejemplo, un calendario de solo lectura).
429calendar_rate_limitedGoogle pide esperar. Reintentá en unos segundos (con la misma Idempotency-Key si estabas creando).
503calendar_unavailableGoogle no responde. Reintentá en un rato.
422invalid_requestGoogle rechazó los datos; el message dice por qué.
502calendar_errorOtra respuesta inesperada de Google.
403plan_not_includedEl plan de tu cuenta no incluye Google Calendar.
503not_configuredGoogle 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.