Agenda y reservas
Consulta los huecos libres de un servicio, agenda citas para tus contactos, muévelas o cancélalas, y entérate de cada cambio por webhook.
La agenda de Met es una sola: la que ve el negocio en el panel, la que usa el Met cuando agenda por WhatsApp y la que lees y escribes por la API. Una cita que creas aquí aparece en la pantalla de Agenda, se lleva al calendario conectado (Google u Outlook) y, si la ficha del contacto tiene correo, le llega la invitación.
El recorrido es siempre el mismo: qué se puede agendar (servicios) → cuándo hay espacio (huecos libres) → la cita.
Antes de empezar
| Scope | Para qué | Con una key de test |
|---|---|---|
agenda:read | Servicios, recursos, huecos libres y las citas (lista y detalle). También los eventos booking.*. | Sí |
agenda:write | Crear, reprogramar y cancelar citas. | No: responde 403 test_mode_restricted, porque escribe en el calendario conectado y le manda la invitación al contacto. |
Las rutas llevan el workspace: /workspaces/{workspaceId}/agenda/…. Con el SDK lo pasas una vez al crear el cliente. Los servicios, los recursos y el horario de atención se configuran en el panel (Agenda); por la API se leen.
import { Met } from '@meteor.ia/sdk';
const met = new Met(process.env.METEOR_API_KEY!, { workspaceId: 42 });import os
from meteor_ia import Met
met = Met(os.environ["METEOR_API_KEY"], workspace_id=42)1. Qué se puede agendar
const { data: servicios } = await met.agenda.eventTypes();
// [{ id: 3, object: 'event_type', name: 'Consulta inicial', duration_min: 30, modality: 'both', price: 80000, resource_ids: [5] }]
const { data: recursos } = await met.agenda.resources();
// [{ id: 5, object: 'agenda_resource', name: 'Dra. Ruiz', kind: 'staff', timezone: 'America/Bogota' }]servicios = met.agenda.event_types()["data"]
recursos = met.agenda.resources()["data"]Un servicio (event_type) es lo que se reserva: dura duration_min minutos, se atiende in_person, virtual o both, y lo atienden los recursos de resource_ids. Un recurso es quien o donde se atiende: una persona (staff), una sede (location) o un equipo (asset). Solo salen los activos, hasta 200 de cada uno.
2. Los huecos libres
Pide los huecos de un servicio entre dos fechas locales del negocio (YYYY-MM-DD), hasta 31 días por llamada:
const { data: huecos, has_more } = await met.agenda.slots({
eventTypeId: 3,
from: '2026-11-03',
to: '2026-11-07',
});
// [{ object: 'slot', resource_id: 5, resource_name: 'Dra. Ruiz', starts_at: '2026-11-03T14:00:00.000Z',
// ends_at: '2026-11-03T14:30:00.000Z', label: 'martes 3 de noviembre de 2026, 09:00 a. m.' }]huecos = met.agenda.slots(event_type_id=3, from_="2026-11-03", to="2026-11-07")["data"]curl "https://api.met.meteor.com.co/api/v1/workspaces/42/agenda/slots?event_type_id=3&from=2026-11-03&to=2026-11-07" \
-H "Authorization: Bearer $METEOR_API_KEY"Un hueco ya descuenta el horario de atención, las excepciones, los bloqueos, las citas tomadas y lo ocupado en el calendario conectado. label es la fecha escrita para mostrarle a una persona. Llegan hasta 200: si has_more es true, pide un rango más corto. Con resourceId (resource_id) ves solo los de un recurso.
En Python el parámetro se llama from_ porque from es palabra reservada; por la red viaja como from.
3. Agendar la cita
Pasa el starts_at y el resource_id de un hueco tal cual llegaron:
const cita = await met.agenda.bookings.create({
eventTypeId: 3,
contactId: 4821,
startsAt: huecos[0].starts_at,
resourceId: huecos[0].resource_id,
modality: 'virtual',
notes: 'Primera consulta, viene por recomendación.',
});
// { id: 91, object: 'booking', status: 'confirmed', video_call_url: 'https://meet.google.com/…', calendar_synced: true, … }cita = met.agenda.bookings.create(
event_type_id=3,
contact_id=4821,
starts_at=huecos[0]["starts_at"],
resource_id=huecos[0]["resource_id"],
)curl -X POST https://api.met.meteor.com.co/api/v1/workspaces/42/agenda/bookings \
-H "Authorization: Bearer $METEOR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"event_type_id":3,"contact_id":4821,"starts_at":"2026-11-03T14:00:00.000Z","resource_id":5}'contact_ides un contacto del CRM de este workspace (ver CRM y contactos). Uno que no existe o que es de otro workspace responde404contact_not_found.- Sin
resource_id, la cita va al primer recurso libre a esa hora. modalitysolo hace falta si el servicio esboth; si el servicio es solo virtual o solo presencial, mandar la otra responde400validation_error.- Antes de escribir, Met recalcula los huecos de ese día. Si alguien tomó el horario mientras tanto, responde
409slot_unavailabley no crea nada: vuelve a pedir los huecos. - Si el contacto ya tiene una cita confirmada de ese servicio a esa hora, responde
409booking_existscon elbooking_idde la que ya existe. - El SDK manda
Idempotency-Keysolo: un reintento de red no duplica la cita. Con cURL, mándalo tú.
calendar_synced dice si la cita quedó en el calendario conectado. video_call_url trae el enlace de la videollamada cuando el calendario lo genera.
Leer las citas
// Las próximas de un contacto
const { data, has_more } = await met.agenda.bookings.list({
from: new Date().toISOString(),
contactId: 4821,
status: 'confirmed',
});
// Todas las de noviembre, sin manejar cursores
for await (const c of met.agenda.bookings.iterate({ from: '2026-11-01T00:00:00-05:00', to: '2026-11-30T23:59:59-05:00' })) {
console.log(c.id, c.starts_at, c.status);
}
const una = await met.agenda.bookings.retrieve(91);from datetime import datetime, timezone
proximas = met.agenda.bookings.list(from_=datetime.now(timezone.utc).isoformat(), contact_id=4821)
for c in met.agenda.bookings.iterate(from_="2026-11-01T00:00:00-05:00", to="2026-11-30T23:59:59-05:00"):
print(c["id"], c["starts_at"], c["status"])La lista va de la más antigua a la más reciente por hora de inicio. Sin from empieza por las que ya pasaron: para ver solo las próximas, pasa from con la hora actual. from y to son instantes ISO 8601 con zona (a diferencia de los huecos, que usan fechas locales). Filtra también por resourceId y por status: confirmed, cancelled, rescheduled, no_show o completed. Pagina con limit (1–100, 20 por defecto) y startingAfter = el id de la última cita de la página.
Reprogramar y cancelar
await met.agenda.bookings.reschedule(91, { startsAt: '2026-11-04T15:00:00.000Z' });
await met.agenda.bookings.cancel(91, { reason: 'El cliente viaja esa semana' });met.agenda.bookings.reschedule(91, starts_at="2026-11-04T15:00:00.000Z")
met.agenda.bookings.cancel(91, reason="El cliente viaja esa semana")- Reprogramar mueve una cita confirmada a otra hora (y, si pasas
resourceId, a otro recurso que atienda el servicio), dentro del horario de atención y sin pisar otra cita. El evento del calendario conectado se mueve con ella. - Cancelar cancela la cita y su evento en el calendario. El motivo queda en las notas. Una cita que no está confirmada (ya cancelada, atendida o ausente) responde
409booking_not_confirmed.
Enterarte de cada cambio
Las citas cambian desde cuatro lugares: el Met en una conversación, una persona en el panel, la app operativa del negocio y tu integración. Los cuatro emiten los mismos eventos (ver Webhooks):
| Evento | Cuándo |
|---|---|
booking.created | Se agendó una cita. |
booking.updated | Se reprogramó o se editó su detalle (modalidad, duración, sede, notas). |
booking.cancelled | Se canceló. |
await met.webhooks.subscriptions.create({
url: 'https://tu-sistema.com/met/eventos',
enabled_events: ['booking.created', 'booking.updated', 'booking.cancelled'],
});
El data de los tres es la cita, con la misma forma de bookings.retrieve(), más el app_url que abre la conversación del contacto en Met. booking.updated trae además previous_attributes: solo los campos que cambiaron, con su valor de antes.
{
"type": "booking.updated",
"data": {
"id": 91, "object": "booking", "status": "confirmed", "contact_id": 4821,
"starts_at": "2026-11-04T15:00:00.000Z", "ends_at": "2026-11-04T15:30:00.000Z",
"previous_attributes": { "starts_at": "2026-11-03T14:00:00.000Z", "ends_at": "2026-11-03T14:30:00.000Z" },
"app_url": "https://met.meteor.com.co/abrir/42/contacto/4821"
}
}
Guardar una cita sin cambiar nada no emite evento, y cancelar una que ya estaba cancelada tampoco. El evento sale con livemode: false solo cuando la cita la agendó un Met dentro de un Run de una key de test.
Desde un asistente (MCP)
El servidor MCP de Meteor tiene met_list_event_types, met_find_slots y met_list_bookings, que piden agenda:read. Las que escriben —met_create_booking, met_reschedule_booking y met_cancel_booking— piden agenda:write, que un cliente MCP no recibe por defecto: tiene que pedirlo a propósito.
Errores comunes
| Código | Qué pasó |
|---|---|
400 validation_error | Una fecha mal escrita, to antes que from, más de 31 días de huecos o una modalidad que el servicio no tiene. Al reprogramar: un horario que ya pasó, fuera del horario de atención, o una cita que no está confirmada. |
403 missing_scope | La key no trae agenda:read o agenda:write. Las keys emitidas antes de que existiera el scope no lo traen. |
403 test_mode_restricted | Una key met_test_ intentó escribir en la agenda. |
403 workspace_mismatch | El workspaceId de la ruta no es el de la key. |
404 event_type_not_found | El servicio no existe, es de otro workspace o está inactivo. |
404 contact_not_found | El contacto no es de este workspace. |
404 booking_not_found | La cita no existe en este workspace. Al reprogramar llega como not_found. |
404 resource_not_found | Al reprogramar, el recurso no atiende ese servicio en este workspace. |
409 slot_unavailable | Al agendar, el horario ya no está libre (o ya pasó, o no coincide con un hueco). Vuelve a pedir los huecos. |
409 booking_exists | El contacto ya tiene esa cita; el error trae su booking_id. |
409 booking_not_confirmed | Se intentó cancelar una cita que no está confirmada. |
409 conflict | Al reprogramar, el horario nuevo choca con otra cita. |