Gestiona calendarios, disponibilidad y citas desde sistemas externos.
Base URL: https://muserelay.com/api/calendar/v1
Autenticación: Authorization: Bearer mrcal_...
Ver Introducción a la API para autenticación, rate limits y convenciones.
Scopes requeridos
| Scope | Endpoints que lo requieren |
|---|---|
calendar:read | GET /me, GET /calendars, GET /calendars/{calendar}/services, GET /calendars/{calendar}/resources |
calendar:availability | GET /calendars/{calendar}/availability |
calendar:appointments:read | GET /calendars/{calendar}/appointments, GET /appointments/{appointment} |
calendar:appointments:create | POST /calendars/{calendar}/appointments |
calendar:appointments:cancel | POST /appointments/{appointment}/cancel |
calendar:appointments:update | PATCH /appointments/{appointment} |
calendar:admin | Scope especial: acepta cualquier scope requerido |
Si el token no tiene el scope:
{ "error": "Insufficient scope" }
Me
GET /me
Devuelve información básica del token actual.
GET /api/calendar/v1/me
Authorization: Bearer mrcal_...
{
"token": {
"id": 10,
"name": "Retell AI clínica",
"scopes": ["calendar:read", "calendar:availability", "calendar:appointments:create"],
"calendar_ids": [12],
"expires_at": null
},
"workspace_id": 3,
"organization_id": 2
}
Calendarios
GET /calendars
Lista los calendarios activos accesibles por el token.
GET /api/calendar/v1/calendars
Authorization: Bearer mrcal_...
{
"data": [
{
"id": 12,
"name": "Agenda principal",
"slug": "agenda-principal",
"timezone": "Europe/Madrid",
"time_format": "24h",
"is_default": true,
"brand_color": "#2563eb",
"public_url": "https://muserelay.com/c/agenda-principal"
}
]
}
GET /calendars/{calendar}/services
Lista los tipos de cita activos del calendario.
GET /api/calendar/v1/calendars/12/services
Authorization: Bearer mrcal_...
{
"data": [
{
"id": 5,
"name": "Corte de pelo",
"description": "Servicio de ejemplo",
"duration_minutes": 30,
"buffer_before_minutes": 0,
"buffer_after_minutes": 0,
"requires_approval": false,
"allows_group_booking": false,
"max_party_size": 1,
"price": "25.00",
"color": "#2563eb"
}
]
}
GET /calendars/{calendar}/resources
Lista los recursos activos del calendario (personas, salas, equipos o cualquier recurso reservable).
GET /api/calendar/v1/calendars/12/resources
Authorization: Bearer mrcal_...
{
"data": [
{
"id": 3,
"name": "María",
"type": "person",
"capacity": 1,
"color": "#2563eb"
}
]
}
Disponibilidad
GET /calendars/{calendar}/availability
Devuelve slots libres para un servicio en un rango de fechas.
Parámetros:
| Parámetro | Tipo | Obligatorio | Reglas |
|---|---|---|---|
service_id | integer | Sí | min:1 |
from | date | Sí | Fecha/hora inicial |
to | date | Sí | Debe ser posterior a from |
resource_id | integer | No | min:1 |
limit | integer | No | min:1, max:500, default 120 |
GET /api/calendar/v1/calendars/12/availability?service_id=5&from=2026-05-12&to=2026-05-16&limit=20
Authorization: Bearer mrcal_...
{
"data": [
{
"start_at": "2026-05-12T08:00:00+00:00",
"end_at": "2026-05-12T08:30:00+00:00",
"day": "2026-05-12",
"start_local": "10:00",
"end_local": "10:30",
"timezone": "Europe/Madrid",
"resource_id": 3,
"resource_name": "María",
"capacity_total": 1,
"capacity_available": 1
}
]
}
start_local y end_local en disponibilidad son strings HH:mm en la zona horaria del calendario (no ISO 8601). En respuestas de cita son ISO 8601 completo con offset.Si to no es posterior a from:
{ "message": "to_must_be_after_from" }
Citas
La respuesta de cita usa la misma estructura en todos los endpoints:
{
"id": 4821,
"calendar_id": 12,
"service_id": 5,
"service_name": "Corte de pelo",
"resource_id": 3,
"resource_name": "María",
"contact_id": 88,
"customer": {
"id": 88,
"name": "Ana Pérez",
"email": "ana@example.com",
"phone": "+34600111222"
},
"start_at": "2026-05-12T10:30:00+02:00",
"end_at": "2026-05-12T11:00:00+02:00",
"start_local": "2026-05-12T10:30:00+02:00",
"end_local": "2026-05-12T11:00:00+02:00",
"timezone": "Europe/Madrid",
"status": "confirmed",
"appointment_kind": "booking",
"created_via": "api",
"party_size": 1,
"notes": "Reserva desde Retell AI",
"created_at": "2026-05-11T18:42:00+02:00"
}
start_local y end_local son las mismas fechas que start_at/end_at convertidas a la zona horaria del calendario (ISO 8601 con offset local).POST /calendars/{calendar}/appointments
Crea una cita. Usa Idempotency-Key para evitar duplicados en reintentos.
| Cabecera | Obligatoria | Descripción |
|---|---|---|
Authorization | Sí | Bearer mrcal_... |
Content-Type | Sí | application/json |
Idempotency-Key | No | Clave de idempotencia cacheada 24 horas |
Body:
| Campo | Tipo | Obligatorio | Reglas |
|---|---|---|---|
service_id | integer | Sí | min:1 |
start_at | date | Sí | Fecha/hora de inicio |
customer | object | Sí | Objeto requerido |
customer.name | string | No | max:255 |
customer.first_name | string | No | max:120 |
customer.last_name | string | No | max:120 |
customer.email | No | max:255 | |
customer.phone | string | No | max:64 |
resource_id | integer | No | min:1 |
any_resource | boolean | No | Selecciona el primer recurso libre |
status | string | No | pending o confirmed |
party_size | integer | No | min:1, max:999 |
notes | string | No | max:4000 |
source | string | No | max:64 |
external_id | string | No | max:191 |
POST /api/calendar/v1/calendars/12/appointments
Authorization: Bearer mrcal_...
Content-Type: application/json
Idempotency-Key: retell-call-abc123-slot-20260512-1030
{
"service_id": 5,
"start_at": "2026-05-12T10:30:00+02:00",
"customer": {
"name": "Ana Pérez",
"phone": "+34600111222",
"email": "ana@example.com"
},
"any_resource": true,
"notes": "Reserva desde Retell AI",
"source": "retell_ai"
}
Response 201 Created: objeto de cita completo envuelto en { "data": { ... } }.
Si se repite el mismo Idempotency-Key en 24h: 200 con X-Idempotent-Replay: true.
Si no hay recurso disponible: 422 { "message": "slot_unavailable" }
GET /calendars/{calendar}/appointments
Lista citas del calendario.
| Parámetro | Tipo | Reglas |
|---|---|---|
from | date | Opcional |
to | date | Opcional |
status | string | pending, confirmed, cancelled, completed, no_show |
limit | integer | min:1, max:200, default 100 |
GET /api/calendar/v1/calendars/12/appointments?from=2026-05-12&to=2026-05-19&status=confirmed&limit=50
Authorization: Bearer mrcal_...
Responde con { "data": [ ... ] } — array de objetos de cita.
GET /appointments/{appointment}
Devuelve una cita por su ID.
GET /api/calendar/v1/appointments/4821
Authorization: Bearer mrcal_...
Responde con { "data": { ... } }.
POST /appointments/{appointment}/cancel
Cancela una cita. Si ya está cancelada, devuelve la cita sin cambios.
| Campo | Tipo | Reglas |
|---|---|---|
reason | string | max:1000, default cancelled_via_calendar_api |
notify_customer | boolean | Default false |
POST /api/calendar/v1/appointments/4821/cancel
Authorization: Bearer mrcal_...
Content-Type: application/json
{
"reason": "El cliente solicitó cancelación por teléfono",
"notify_customer": true
}
Responde 200 con el objeto de cita con "status": "cancelled".
PATCH /appointments/{appointment}
Modifica o reprograma una cita. Todos los campos son opcionales.
| Campo | Tipo | Reglas |
|---|---|---|
service_id | integer | min:1 |
resource_id | integer | min:1 |
start_at | date | Nueva fecha/hora de inicio |
status | string | pending, confirmed, cancelled, completed, no_show |
notes | string | max:4000 |
notify_customer | boolean | Default false |
Enviar start_at dispara appointment.rescheduled. Otros campos disparan appointment.updated.
PATCH /api/calendar/v1/appointments/4821
Authorization: Bearer mrcal_...
Content-Type: application/json
{
"start_at": "2026-05-14T16:00:00+02:00",
"notify_customer": true
}
Responde 200 con el objeto de cita actualizado.
Guías de integración
Retell AI
Flujo típico para reservar durante una llamada:
1. Agente detecta intención de reserva
2. GET /calendars/{calendar}/availability con service_id
3. Caller elige horario
4. POST /calendars/{calendar}/appointments con Idempotency-Key
5. Cita creada con created_via = api
Scopes mínimos: calendar:read, calendar:availability, calendar:appointments:create
Usa Idempotency-Key con identificador de llamada + slot para evitar dobles reservas en reintentos.
n8n / Make
Method: GET
URL: https://muserelay.com/api/calendar/v1/calendars/{{$json["calendar_id"]}}/availability
Headers:
Authorization: Bearer mrcal_...
Query params:
service_id: {{$json["service_id"]}}
from: {{$today.format("YYYY-MM-DD")}}
to: {{$today.plus(7, "days").format("YYYY-MM-DD")}}
limit: 20
Webhooks
MuseRelay envía notificaciones HTTP POST cuando ocurren eventos de citas.
Eventos disponibles
| Evento | Cuándo se dispara |
|---|---|
appointment.created | Al crear una cita |
appointment.updated | Al actualizar campos |
appointment.cancelled | Al cancelar |
appointment.rescheduled | Al mover a otro horario |
Configuración
Panel → Ajustes → API e Integraciones → Webhooks.
| Campo | Descripción |
|---|---|
url | URL destino |
events | Lista de eventos; vacío = todos |
calendar_ids | Lista de calendarios; vacío = todos |
is_active | Activa o desactiva el endpoint |
El secreto generado usa prefijo mrcal_whsec_.
Cabeceras de firma
| Cabecera | Valor |
|---|---|
X-MuseRelay-Event | Nombre del evento, p. ej. appointment.created |
X-MuseRelay-Delivery | ID de entrega con prefijo mrcal_evt_ |
X-MuseRelay-Timestamp | Unix timestamp en segundos |
X-MuseRelay-Signature | sha256= + HMAC-SHA256 |
Verificar la firma
La firma es HMAC-SHA256 sobre {X-MuseRelay-Timestamp}.{raw_json_body}.
PHP:
$secret = 'mrcal_whsec_...';
$timestamp = $_SERVER['HTTP_X_MUSERELAY_TIMESTAMP'] ?? '';
$rawBody = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
$received = $_SERVER['HTTP_X_MUSERELAY_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Invalid signature');
}
Node.js:
const crypto = require('crypto');
const secret = 'mrcal_whsec_...';
const timestamp = req.headers['x-muserelay-timestamp'] ?? '';
const signed = `${timestamp}.${req.rawBody}`;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(signed).digest('hex');
const received = req.headers['x-muserelay-signature'] ?? '';
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
res.status(401).send('Invalid signature');
}
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE.Payload de ejemplo — appointment.created
{
"id": "mrcal_evt_550e8400-e29b-41d4-a716-446655440000",
"event": "appointment.created",
"created_at": "2026-05-11T18:42:00+00:00",
"data": {
"id": 4821,
"appointment_id": 4821,
"calendar_id": 12,
"workspace_id": 3,
"organization_id": 2,
"service_id": 5,
"service_name": "Corte de pelo",
"resource_id": 3,
"resource_name": "María",
"contact_id": 88,
"customer": {
"id": 88,
"name": "Ana Pérez",
"email": "ana@example.com",
"phone": "+34600111222"
},
"start_at": "2026-05-12T10:30:00+02:00",
"end_at": "2026-05-12T11:00:00+02:00",
"timezone": "Europe/Madrid",
"status": "confirmed",
"appointment_kind": "booking",
"created_via": "api",
"party_size": 1,
"notes": "Reserva desde Retell AI"
}
}
Política de reintentos
| Punto | Valor |
|---|---|
| Intentos máximos | 3 |
| Backoff | 30 segundos fijo |
| Timeout HTTP | 10 segundos |
| Éxito | Cualquier status HTTP 2xx |