On this page
Guides / Scheduling and bookings

Scheduling and bookings

Look up a service's free slots, book appointments for your contacts, move or cancel them, and hear about every change by webhook.

Updated View .md

Met has a single schedule: the one the business sees in the panel, the one the Met uses when it books over WhatsApp, and the one you read and write through the API. An appointment you create here shows up on the Schedule screen, is carried over to the connected calendar (Google or Outlook) and, if the contact's record has an email, the contact gets the invitation.

The path is always the same: what can be booked (services) → when there is room (free slots) → the appointment.

Before you start

ScopeWhat forWith a test key
agenda:readServices, resources, free slots and appointments (list and detail). Also the booking.* events.Yes
agenda:writeCreate, reschedule and cancel appointments.No: it responds 403 test_mode_restricted, because it writes to the connected calendar and sends the invitation to the contact.

Routes carry the workspace: /workspaces/{workspaceId}/agenda/…. With the SDK you pass it once when creating the client. Services, resources and business hours are configured in the panel (Schedule); through the API they are read.

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. What can be booked

const { data: services } = await met.agenda.eventTypes();
// [{ id: 3, object: 'event_type', name: 'Consulta inicial', duration_min: 30, modality: 'both', price: 80000, resource_ids: [5] }]

const { data: resources } = await met.agenda.resources();
// [{ id: 5, object: 'agenda_resource', name: 'Dra. Ruiz', kind: 'staff', timezone: 'America/Bogota' }]
services = met.agenda.event_types()["data"]
resources = met.agenda.resources()["data"]

A service (event_type) is what gets booked: it lasts duration_min minutes, is delivered in_person, virtual or both, and is handled by the resources in resource_ids. A resource is who or where the service is delivered: a person (staff), a location (location) or a piece of equipment (asset). Only active ones are returned, up to 200 of each.

2. Free slots

Ask for a service's slots between two local dates of the business (YYYY-MM-DD), up to 31 days per call:

const { data: slots, 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.' }]
slots = 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"

A slot already accounts for business hours, exceptions, blocks, booked appointments and what is busy on the connected calendar. label is the date written out to show to a person. Up to 200 arrive: if has_more is true, ask for a shorter range. With resourceId (resource_id) you only see one resource's slots.

In Python the parameter is called from_ because from is a reserved word; on the wire it travels as from.

3. Booking the appointment

Pass a slot's starts_at and resource_id exactly as they arrived:

const appointment = await met.agenda.bookings.create({
  eventTypeId: 3,
  contactId: 4821,
  startsAt: slots[0].starts_at,
  resourceId: slots[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, … }
appointment = met.agenda.bookings.create(
    event_type_id=3,
    contact_id=4821,
    starts_at=slots[0]["starts_at"],
    resource_id=slots[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}'

calendar_synced says whether the appointment made it to the connected calendar. video_call_url carries the video call link when the calendar generates one.

Reading appointments

// A contact's upcoming ones
const { data, has_more } = await met.agenda.bookings.list({
  from: new Date().toISOString(),
  contactId: 4821,
  status: 'confirmed',
});

// All of November's, without handling cursors
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 one = await met.agenda.bookings.retrieve(91);
from datetime import datetime, timezone

upcoming = 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"])

The list goes from oldest to newest by start time. Without from it starts with the ones already past: to see only upcoming ones, pass from with the current time. from and to are ISO 8601 instants with a zone (unlike slots, which use local dates). You can also filter by resourceId and by status: confirmed, cancelled, rescheduled, no_show or completed. Paginate with limit (1–100, 20 by default) and startingAfter = the id of the last appointment on the page.

Rescheduling and cancelling

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")

Hearing about every change

Appointments change from four places: the Met in a conversation, a person in the panel, the business's operations app and your integration. All four emit the same events (see Webhooks):

EventWhen
booking.createdAn appointment was booked.
booking.updatedIt was rescheduled or its details were edited (modality, duration, location, notes).
booking.cancelledIt was cancelled.
await met.webhooks.subscriptions.create({
  url: 'https://your-system.com/met/events',
  enabled_events: ['booking.created', 'booking.updated', 'booking.cancelled'],
});

The data of all three is the appointment, with the same shape as bookings.retrieve(), plus the app_url that opens the contact's conversation in Met. booking.updated also carries previous_attributes: only the fields that changed, with their previous value.

{
  "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"
  }
}

Saving an appointment without changing anything emits no event, and neither does cancelling one that was already cancelled. The event is sent with livemode: false only when the appointment was booked by a Met inside a Run of a test key.

From an assistant (MCP)

Meteor's MCP server has met_list_event_types, met_find_slots and met_list_bookings, which require agenda:read. The ones that write —met_create_booking, met_reschedule_booking and met_cancel_booking— require agenda:write, which an MCP client does not get by default: it has to ask for it on purpose.

Common errors

CodeWhat happened
400 validation_errorA malformed date, to before from, more than 31 days of slots or a modality the service does not have. When rescheduling: a time that already passed, outside business hours, or an appointment that is not confirmed.
403 missing_scopeThe key does not carry agenda:read or agenda:write. Keys issued before the scope existed do not carry it.
403 test_mode_restrictedA met_test_ key tried to write to the schedule.
403 workspace_mismatchThe route's workspaceId is not the key's.
404 event_type_not_foundThe service does not exist, belongs to another workspace or is inactive.
404 contact_not_foundThe contact does not belong to this workspace.
404 booking_not_foundThe appointment does not exist in this workspace. When rescheduling it arrives as not_found.
404 resource_not_foundWhen rescheduling, the resource does not handle that service in this workspace.
409 slot_unavailableWhen booking, the time is no longer free (or already passed, or does not match a slot). Ask for the slots again.
409 booking_existsThe contact already has that appointment; the error carries its booking_id.
409 booking_not_confirmedTried to cancel an appointment that is not confirmed.
409 conflictWhen rescheduling, the new time overlaps another appointment.