# Reservas desde tu sitio web

Source: /docs/calendar-integration

Inserta el formulario de Autochat o conecta tu servidor a la API de Calendar. Una misma agenda, disponibilidad y recordatorios.

Recibe citas de tu página en los calendarios que ya usas en Autochat. Elige el widget para empezar sin desarrollo, o usa la API desde tu servidor para diseñar tu propio formulario.

## Elige cómo conectar tu sitio

| Modo | Para quién | Qué necesitas |
| --- | --- | --- |
| Widget | Un sitio de WordPress, una página comercial o un constructor web. | El código iframe y la dirección HTTPS de tu sitio. |
| API | Un equipo que desarrolla su propio formulario. | Un servidor y una clave de integración guardada en ese servidor. |

La integración está disponible para todas las cuentas de pago con acceso operativo activo. Solo los administradores principales (master admins) pueden configurarla y administrar sus claves. La habilitación depende del entorno. Las citas se gestionan después desde Autochat; esta versión no publica consultas de contactos, listas de citas, pagos ni nuevas operaciones de cancelación o reprogramación.

## Configura y publica

1. Abre Integraciones → Calendario y selecciona calendarios existentes.
2. Elige los servicios que aceptarás y revisa anticipación, horizonte y márgenes de tiempo.
3. Publica los campos que quieres pedir. Si un recordatorio necesita una respuesta, vincula ese campo a su variable y márcalo obligatorio.
4. Selecciona el calendario del widget, el título, el color y el logo del negocio.
5. Para usar el widget, agrega los enlaces HTTPS de tu política de privacidad y tratamiento de datos y de tus términos y condiciones. El visitante debe aceptarlos antes de reservar. Si usarás solo la API, estos enlaces del widget son opcionales.
6. Guarda la configuración. Activa Widget o API de manera independiente cuando todo esté listo.

Los calendarios compartidos mantienen su capacidad actual. Esta versión no ofrece una reserva independiente por empleado. Los campos adicionales se guardan con la cita y no sobrescriben los datos confiables de un contacto existente.

## Inserta el widget

Agrega una dirección HTTPS por línea en Sitios permitidos. Incluye https://negocio.com y https://www.negocio.com por separado si usas ambas. No añadas rutas ni comodines. Copia el código del panel Widget en un bloque HTML de tu sitio.

### Ejemplo: sustituye la URL por la de tu panel

```html
<iframe
  src="https://book-dev.autochat.cloud/embed/PUBLIC_BOOKING_ID"
  title="Reserva una cita"
  width="100%" height="900"
  loading="lazy" referrerpolicy="no-referrer"
  style="border:0;width:100%;max-width:1120px"
></iframe>
```

El visitante no necesita una cuenta de Autochat ni cookies de terceros. El iframe no contiene una clave secreta. La verificación de seguridad se realiza antes de una reserva nueva. Algunos constructores usan varios iframes: cada sitio que actúe como contenedor debe estar permitido. Una dirección permitida controla dónde se incrusta el formulario; no convierte su URL pública en una contraseña.

## Autenticación de la API

La integración pertenece al negocio, no a cada administrador. Si hay varios administradores principales en el mismo negocio, comparten la configuración y la misma organización de credenciales en ese entorno. Solo puede existir una clave activa a la vez; una emisión en curso bloquea otra simultánea. Desarrollo y producción mantienen organizaciones y claves separadas.

Crea una clave en el panel API, copia el secreto una sola vez y guárdalo en una variable de entorno de tu servidor. Nunca lo pongas en JavaScript público, HTML, una URL o una aplicación móvil. Usa la URL del entorno configurado: desarrollo y producción no comparten claves ni datos.

### Encabezado del servidor

```http
Authorization: Bearer YOUR_API_KEY
```

| Permiso | Operación |
| --- | --- |
| calendar.config.read | Leer el formulario publicado. |
| calendar.availability.read | Consultar horarios disponibles. |
| calendar.appointments.create | Crear una cita. |

Elige una vigencia de 1 semana (7 días), 1 mes (30 días), 6 meses (180 días), 1 año (365 días) o Para siempre. Para siempre significa sin vencimiento automático; sigue siendo revocable y depende del acceso operativo del negocio. La clave solo alcanza los calendarios configurados. Revocarla bloquea nuevas solicitudes en Autochat inmediatamente; la revocación en el proveedor se reconcilia por separado. Para rotar, revoca la anterior, crea una nueva y actualiza tu servidor. No se revela de nuevo un secreto perdido.

## 1. Lee el formulario

```http
GET /v1/calendars/{calendarId}/schema
Authorization: Bearer YOUR_API_KEY
```

Copia calendarId desde el panel API. La respuesta contiene schemaVersion, la zona horaria, los servicios permitidos y los campos publicados. Usa la propiedad key de cada campo como identificador estable; su etiqueta puede cambiar. No incluye datos de clientes ni definiciones internas de recordatorios.

## 2. Consulta disponibilidad

```http
GET /v1/calendars/{calendarId}/availability?serviceId=SERVICE_ID&from=2026-11-10&to=2026-11-10&limit=100
Authorization: Bearer YOUR_API_KEY
```

from y to son fechas locales de la zona del calendario. Solicita como máximo 31 días por consulta y 500 horarios por página. El tamaño predeterminado es 100. Continúa con nextCursor y los mismos filtros si existe otra página. Si no se puede completar una comprobación dentro del límite seguro, reduce el rango; no interpretes un error como una agenda vacía.

Para resaltar un calendario mensual, usa view=days con el mismo servicio y rango de hasta 31 días. Recibes dates (fechas locales con al menos un horario), schemaVersion y requestId. Esta vista no usa cursor ni se trunca por limit. Después consulta los horarios del día elegido sin view. Un horario disponible no está reservado. Autochat vuelve a comprobar capacidad, horarios, festivos y límites al guardar. Los resultados usan marcas de tiempo con desplazamiento UTC y la zona IANA del calendario.

## 3. Crea la cita

```http
POST /v1/calendars/{calendarId}/appointments
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Idempotency-Key: 89c6f5fe-f3e8-4e10-b21b-0d3798a8b735
```

### Cuerpo de la solicitud

```json
{
  "serviceId": "SERVICE_ID",
  "startsAt": "2026-11-10T10:00:00-05:00",
  "schemaVersion": "VERSION_FROM_SCHEMA",
  "customer": {
    "name": "Cliente de ejemplo",
    "phone": "+573000000000"
  },
  "answers": {
    "PUBLISHED_FIELD_KEY": "Respuesta de ejemplo"
  }
}
```

Incluye solo campos publicados. startsAt debe tener Z o un desplazamiento UTC explícito. El servidor determina duración, estado y recordatorios. El cuerpo no puede superar 64 KiB. Los campos adicionales son texto, texto largo, número o selección; no se admiten archivos ni HTML.

### Respuesta mínima · 201 para una cita nueva

```json
{
  "appointment": {
    "reference": "PUBLIC_REFERENCE",
    "startsAt": "2026-11-10T10:00:00.000-05:00",
    "endsAt": "2026-11-10T10:30:00.000-05:00",
    "timezone": "America/Bogota",
    "status": "scheduled"
  },
  "requestId": "REQUEST_ID"
}
```

La referencia confirma que se registró la cita; no es una consulta de estado ni una confirmación de entrega por WhatsApp. Los recordatorios respetan su configuración, elegibilidad y exclusiones de contacto.

## Ejemplo de servidor · JavaScript

Usa esta función en tu backend con tu clave guardada en una variable de entorno. Persiste operationKey y payload antes de enviar, y guarda el status devuelto. En cada reintento pasa ese estado guardado como previousStatus y conserva la misma clave y cuerpo. Si una respuesta se perdió, una pausa o revocación posterior puede impedir recuperar el recibo sin demostrar que la cita fue rechazada; no inicies otra reserva por ese error de acceso. Protege también la ruta pública de tu formulario.

### Función completa para Node.js

```javascript
/** Server-only example. Persist operationKey + payload before calling, then persist the returned status.
 * On retries pass that stored status as previousStatus and keep the same operationKey + payload.
 */
export async function createCalendarBooking({ apiOrigin, apiKey, calendarId, payload, operationKey, previousStatus, fetcher = fetch }) {
  const origin = new URL(apiOrigin);
  if (origin.protocol !== "https:" || origin.pathname !== "/" || origin.search || origin.hash || origin.username || origin.password)
    throw new Error("Configure an HTTPS API origin.");
  if (!apiKey || !/^[a-zA-Z0-9:_-]{16,128}$/.test(operationKey) || !/^[a-zA-Z0-9_-]+$/.test(calendarId))
    throw new Error("Supply a server key, calendar ID, and persisted operation key.");
  try {
    const response = await fetcher(new URL(`/v1/calendars/${calendarId}/appointments`, origin), {
      method: "POST",
      redirect: "error",
      signal: AbortSignal.timeout(20_000),
      headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "Idempotency-Key": operationKey },
      body: JSON.stringify(payload),
    });
    const body = await response.json();
    if (response.ok) return { status: "accepted", appointment: body.appointment, requestId: body.requestId };
    const code = body.error?.code ?? "TEMPORARILY_UNAVAILABLE";
    // Access denial can hide a completed receipt. Only these conflicts, checked
    // after receipt recovery, establish that a previously uncertain intent did not commit.
    const unresolved = previousStatus === "uncertain" && !["SCHEMA_CHANGED", "SLOT_UNAVAILABLE"].includes(code);
    return {
      status: response.status >= 500 || code === "IDEMPOTENCY_CONFLICT" || unresolved ? "uncertain" : "rejected",
      code,
      requestId: body.requestId,
      retryAfter: Number(response.headers.get("retry-after") ?? "0"),
      operationKey,
    };
  } catch {
    // Never log credentials or intake; a timeout can occur after the appointment commits.
    return { status: "uncertain", code: "NETWORK_ERROR", operationKey };
  }
}

```

## Reintentos sin duplicar citas

- Genera un identificador impredecible para cada intención de reserva y guárdalo en tu servidor junto con el cuerpo original.
- Si la respuesta se pierde, repite exactamente esa intención con el mismo Idempotency-Key. No generes otra clave por un timeout.
- Una repetición exacta devuelve la cita original con HTTP 200 y no programa otro recordatorio. Un cuerpo diferente con la misma clave devuelve 409.
- El período de recuperación garantizado es de 24 horas; los recibos se conservan inicialmente 48 horas. Fuera de ese período, no asumas que un reintento sigue protegido.
- Si cambia schemaVersion, vuelve a leer el formulario y revisa los valores antes de iniciar una nueva intención.
- En el widget, usa Comprobar esta misma reserva cuando el resultado sea incierto. No cierres o recargues la página durante esa recuperación: el formulario no guarda datos personales en almacenamiento del navegador.

## Errores y límites

| HTTP | Código o motivo | Acción |
| --- | --- | --- |
| 401 | INVALID_API_KEY | Revisa vigencia y credencial. |
| 403 | INTEGRATION_DISABLED / FEATURE_UNAVAILABLE | Revisa modo y acceso del negocio. |
| 404 | RESOURCE_UNAVAILABLE | Revisa el calendario y sus permisos. |
| 409 | SLOT_UNAVAILABLE / SCHEMA_CHANGED | Actualiza formulario u horario. |
| 409 | IDEMPOTENCY_CONFLICT | No reutilices una clave para otra intención. |
| 413 / 422 | Tamaño o validación | Corrige el cuerpo o los campos. |
| 429 | RATE_LIMITED | Respeta Retry-After y conserva la clave original. |
| 503 | Dependencia o capacidad temporal | Reintenta con espera limitada; conserva la misma intención si el resultado es incierto. |

Las respuestas incluyen requestId para soporte. No envíes claves o datos del cliente al reportar un error. Tu servidor también debe proteger su formulario público contra abuso; la clave de Autochat identifica a tu integración, no a la persona que llena el formulario. CORS no es autenticación y las rutas de clave secreta no admiten uso directo desde el navegador.

## Consulta las reservas en Autochat

En Estadísticas → Calendario, Eventos creados por método separa API externa del calendario, Widget de reservas, enlaces directos, manual e IA. Los filtros de fecha y calendario también aplican a este desglose. Cuenta citas creadas, no solicitudes HTTP ni mensajes enviados; los reintentos no duplican el total.

El origen se conserva al confirmar, cancelar o reprogramar. Eliminar una cita descuenta su contribución. Estos contadores se actualizan mediante el procesamiento habitual de estadísticas y pueden aparecer unos instantes después de guardar.

## Referencia y compatibilidad

La referencia OpenAPI describe las rutas /v1, tipos, encabezados y errores. Una actualización compatible puede agregar campos de respuesta; no cambies el significado de campos existentes. La API actual no incluye cancelación, reprogramación, contactos ni un endpoint público de estadísticas.

Las rutas del widget usan /v1/public/booking/{publicId} con las mismas operaciones. No usan Authorization; las reservas nuevas requieren Turnstile y acceptedPolicies=true. La aceptación queda vinculada a los enlaces publicados y a schemaVersion; si cambian las políticas, vuelve a cargar el formulario antes de reservar. Para un formulario propio usa siempre las rutas de servidor autenticadas.
