NUEVOUna nueva forma de gestionar tu alojamiento.Conocé ArtechIA
ArtechIA
← Inicio

API Pública de Reservas

v1

Introducción

La API pública de Artechia permite construir desde cero un motor de reservas completo en cualquier sitio web externo: calendarios con disponibilidad, listado de habitaciones con precio, checkout con datos del huésped, pago con MercadoPago y confirmación.

No hace falta ningún token ni API key. No hay nada que pedir ni que activar: la API ya está andando para tu propiedad.

Lo único que identifica a tu hotel son org_slug y property_slug, dos textos que salen del panel. No son secretos: van en la URL y quedan a la vista en el navegador de cualquier visitante. Con ellos se pueden consultar tus fotos, tus habitaciones, tu disponibilidad y tus precios — la misma información que ya mostrás en tu sitio.

Nada sensible queda expuesto por saberlos. Para ver una reserva hay que tener el código y el email del huésped. Para crear una reserva hay que pasar por el checkout, que valida los precios contra la base de datos y no confía en lo que mande el navegador. Y todo tiene rate limit.

¿Ya tenés tu sitio y solo te falta que se pueda reservar?

Apretá Copiar para IA y pegalo en Claude Code (o cualquier IA CLI) parado en el repositorio de tu sitio. El prompt le indica respetar el diseño que ya tenés — reusa lo que exista y lo nuevo lo construye con tus colores y tipografías — y le pide armar las cuatro piezas que faltan: buscador de disponibilidad, listado con precios, checkout y página “mi reserva”. Lo único que tenés que darle son tu org_slug y property_slug.

Para devs

Esta página tiene todo lo necesario: endpoints, parámetros, respuestas, errores y notas sobre CORS, caching y webhooks. Los datos del hotel (org_slug, property_slug y los room_type_slug) salen del panel: Propiedades → tu propiedad → Configuración → Integración por API, tarjeta “Datos para conectar”.

Para IA

El prompt de Copiar para IA arranca reconociendo tu repositorio: detecta el stack, tu sistema de diseño y qué partes del flujo ya existen, antes de escribir una línea.

Qué necesitás, de inicio a fin

No hay que pedir acceso, generar credenciales ni esperar aprobación. Alcanza con esto:

1 — Los datos de tu propiedad

Están en el panel, en Propiedades → tu propiedad → Configuración → Integración por API, tarjeta “Datos para conectar”. Son tres:

  • org_slug — identifica tu cuenta.
  • property_slug — identifica el alojamiento.
  • room_type_slug — uno por habitación; hace falta para cotizar y reservar.

Los tres son permanentes: se arman solos al dar de alta y no cambian nunca, ni aunque el hotel renombre la propiedad o la habitación. Podés hardcodearlos.

2 — Un medio de pago configurado

Sin al menos uno (MercadoPago, transferencia o “coordinar con el alojamiento”), la API responde que las reservas no están habilitadas y no deja confirmar ninguna. Se configura en Configuración → Pagos. Es el paso que más se olvida.

3 — Nada más: no hace falta backend

El flujo entero —buscar, mostrar precios, tomar los datos del huésped, confirmar y consultar la reserva— se hace desde el navegador, con fetch. Todos los endpoints mandan las cabeceras CORS.

Cobrar tampoco pasa por tu servidor. Con MercadoPago, la API te devuelve un link y redirigís al huésped: paga en MercadoPago y el aviso vuelve solo a Artechia. Con transferencia te devuelve los datos bancarios, y con “coordinar con el alojamiento”, el teléfono y el email. Tu sitio nunca ve una tarjeta.

Dos formas de integrarlo

A · Todo en tu sitio

Buscador, precios, checkout y “mi reserva” con tu diseño. El huésped nunca sale de tu dominio (salvo para pagar con MercadoPago). Es lo que arma el prompt de Copiar para IA.

B · Buscador propio + checkout de Artechia

Si preferís no armar el checkout, hacés solo el buscador con tu diseño y cuando el huésped elige, lo mandás a tu página de reservas de Artechia con las fechas ya cargadas:

/tu-property-slug
  ?check_in=2026-11-10
  &check_out=2026-11-17
  &adults=2&children=0

Probá que responde antes de escribir una línea de código. Pegá esto en una terminal con tus datos: si devuelve { ok: true, ... }, ya está todo lo que hace falta del lado de Artechia.

curl "https://app.artechia.com/api/public/property\
?org_slug=TU_ORG&property_slug=TU_PROPIEDAD"

Base URL

https://app.artechia.com

Todos los endpoints aceptan y devuelven JSON. Los errores siempre retornan { ok: false, error: { code, message } }. Los endpoints de checkout incluyen headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset y Retry-After cuando hay 429.

Notas críticas para devs

CORS — está todo abierto al navegador.

Los 14 endpoints mandan Access-Control-Allow-Origin: * y contestan el preflight OPTIONS. Podés hacer el flujo entero con fetch desde el sitio del hotel, sin proxy y sin backend.

CORS no es autenticación ni tiene que ver con permisos: es una regla del navegador que decide si una página de otro dominio puede leer la respuesta. Lo que protege cada endpoint es otra cosa — el checkout_token firmado, el access_token, o el rate limit. Ver Qué protege cada endpoint.

Headers de rate limit presentes en checkout/start y checkout/confirm:

  • X-RateLimit-Limit — máximo de requests permitidas en la ventana.
  • X-RateLimit-Remaining — requests restantes en la ventana actual.
  • X-RateLimit-Reset — Unix timestamp (en segundos) del próximo reset.
  • Retry-After — segundos a esperar antes de reintentar tras un 429.

Rate limit por huésped (solo si usás un proxy)

El rate limit se cuenta por IP, así que llamando desde el navegador ya cuenta bien: cada huésped usa la suya. Si igual decidís pasar por un backend propio (por ejemplo para no exponer una lógica tuya), todas las requests salen de la IP de tu servidor y el límite pasa a ser por hotel — puede dar 429 a huéspedes legítimos con mucho tráfico. Para contarlo por huésped, generá un proxy secret en el panel (Propiedad → Integración por API) y reenviá desde tu proxy:

  • X-Artechia-Client-IP — IP real del huésped.
  • X-Artechia-Proxy-Secret — el secret de tu propiedad (server-side, nunca en el browser).

Es opcional y revocable (regenerás el secret cuando quieras).

Caching:

  • /property y /room-type → public, max-age=60. El origen manda además stale-while-revalidate=300, pero el edge lo recorta: lo que llega al cliente es public, max-age=60. No asumas SWR.
  • Todos los demás (disponibilidad, búsqueda, checkout, lookup) → no-store

Los precios que ve el huésped NO son autoritativos.

El único total que vale es el que devuelve /checkout/confirm. Entre el /search y el confirm el hotel puede cambiar tarifas, cerrar fechas o agotar el cupo: el server re-cotiza en cada paso y puede devolver 422 VALIDATION_FAILED o un total distinto. Nunca guardes el precio en tu base y lo des por firme antes del confirm.

Webhook de MercadoPago (automático):

Cuando el huésped paga, MercadoPago notifica a Artechia y la reserva pasa de pending a confirmed automáticamente. El sitio externo no recibe el webhook — consultá el estado de pago periódicamente con GET /api/public/booking/lookup o redirigí al manage_url que devuelve /checkout/confirm.

Qué protege cada endpoint

La API no usa API keys, y CORS está abierto. Lo que impide que cualquiera haga cualquier cosa es lo que exige cada endpoint:

EndpointsQué exige
/property · /room-type · /availability · /search · /quoteNada: es la información que ya mostrás en tu web. Solo rate limit.
/checkout/startNada — es la puerta de entrada. Bloquea inventario por poco tiempo y está limitado a 30 por minuto por IP.
/checkout/confirm · /extras · /lock-status · /status · /guest-previewEl checkout_token que devuelve /checkout/start, firmado con HMAC y con vencimiento. No se puede fabricar.
/checkout/payment-link · /reviewEl access_token de 64 caracteres que devuelve /checkout/confirm.
/booking/lookupEl código de reserva + el email del huésped. El código lleva 8 caracteres al azar (más de 4.000 millones de combinaciones), así que no se adivina.

Los precios los decide el servidor. Podés mostrar lo que quieras en tu sitio; al confirmar, Artechia recalcula el total contra su propia base y usa ese. Mandar un precio distinto desde el navegador no cambia lo que se cobra.

Las reglas del hotel también se validan del lado del servidor, no son datos decorativos que tu sitio decide respetar o no. Estas cuatro se chequean en /checkout/start y de nuevo en /checkout/confirm:

  • Reservas online apagadas → BOOKINGS_DISABLED
  • Fechas en un período cerrado → PROPERTY_CLOSED
  • Método de pago que el hotel no ofrece → PAYMENT_METHOD_NOT_AVAILABLE
  • Estadía mínima, cupo, disponibilidad real → VALIDATION_FAILED, NO_AVAILABILITY

Se revalida en confirm a propósito: el checkout_token vive hasta 30 minutos y el hotel puede cambiar un ajuste mientras el huésped completa sus datos. Por eso un error puede aparecer recién al confirmar aunque start haya dado OK.

Flujo completo de reserva

  1. 1
    GET /api/public/property

    Cargar branding, métodos de pago, campos custom y T&C. Acá también vienen los dos portones que hay que mirar ANTES de dibujar nada: bookings_enabled y closed_periods

  2. 2
    GET /api/public/availability

    Pintar el calendario: disponible / pocas unidades / no disponible / oferta

  3. 3
    POST /api/public/search

    Listar habitaciones disponibles con precio total, fotos, amenities y validación

  4. 4
    GET /api/public/room-type

    (Opcional) Detalle estático de una habitación si no se viene desde /search

  5. 5
    POST /api/public/quote

    (Opcional) Cotización detallada noche por noche + política de cancelación de una habitación

  6. 6
    POST /api/public/checkout/start

    Bloquear inventario (TTL configurable 1–30 min, default 15) → checkout_token

  7. 7
    GET /api/public/checkout/extras

    (Opcional) Listar extras opcionales para la habitación bloqueada

  8. 8
    PATCH /api/public/checkout/guest-preview

    (Opcional) Preview en vivo en el PMS mientras el huésped escribe

  9. 9
    GET /api/public/checkout/lock-status

    (Opcional) Polling cada ~10 s para detectar expiración del lock o cancelación admin

  10. 10
    POST /api/public/checkout/confirm

    Enviar datos del huésped + extras → booking_code + access_token (+ payment_link para MP, bank_data para transferencia, contact_hotel para coordinar, card_on_file para tarjeta manual)

  11. 11
    POST /api/public/checkout/payment-link

    (Opcional/fallback) Generar init_point de MercadoPago si confirm no lo devolvió, o para cobrar el saldo (balance)

  12. 12
    GET /api/public/booking/lookup

    Consultar estado, datos completos y a dónde pagar (payment): código + email, o código + access_token (el del manage_url)

  13. 13
    POST /api/public/review

    (Opcional) Recibir rating y feedback post-estadía

GET
/api/public/property

Configuración de la propiedad

Punto de entrada. Devuelve nombre, moneda, timezone, horarios de check-in/out y branding (color, logo, hero). Llamar primero para inicializar el motor.

Rate limit: 90 req / min por IPCORS: Sí — llamable desde el navegadorCache: public, max-age=60

Parámetros (query string)

org_slug·string

Slug de la organización (recomendado, multi-tenant)

property_slug·stringREQUERIDO

Slug de la propiedad

Ejemplo de request

curl https://app.artechia.com/api/public/property\
  ?org_slug=hotel-costa\
  &property_slug=mar-azul

Respuesta 200

{
  "ok": true,
  "property": {
    "name": "Hotel Mar Azul",
    "description": null,          // LEGADO — ver nota abajo; no construyas sobre este campo
    "address": "Av. Costanera 1234, Mar del Plata",               // puede ser null
    "currency": "ARS",
    "timezone": "America/Argentina/Buenos_Aires",  // puede ser null
    "check_in_time": "14:00",       // HH:MM — horario de check-in del hotel
    "check_out_time": "11:00",      // HH:MM — horario de check-out del hotel
    "closed_periods": [             // el hotel NO toma reservas en estas fechas
      { "start": "2026-08-03",      // inclusivo
        "end":   "2026-11-01",      // EXCLUSIVO: se puede reservar desde este día
        "reason": "Solo abrimos en temporada de verano." }   // puede ser null
    ],
    "branding": {                   // los 3 campos pueden venir null
      "primary_color": "#2D4F3C",
      "logo_url": "https://mi-hotel.com/logo.svg",
      "hero_image_url": "https://mi-hotel.com/hero.jpg"
    },
    "checkout": {
      "special_requests_enabled": true,
      "terms": "Política de cancelación: ...",  // null si no configurados
      "bookings_enabled": true,          // ← ver abajo: si es false, NO muestres el flujo
      "bookings_disabled_reason":  null, // "hotel_paused" | "no_payment_method"
      "bookings_disabled_message": null, // texto en español, mostrable al huésped
      "payment_methods": {
        "mercadopago": true,
        "transferencia": false,
        "contact_hotel": false,      // "coordinar con el alojamiento" (sin pago online)
        "card_manual": false         // el huésped manda la tarjeta y el hotel la cobra a mano
      },
      "contact_hotel": {             // null salvo que contact_hotel === true
        "email": "reservas@mi-hotel.com",   // puede ser null
        "phone": "+54 9 223 444-5555",      // el primero; null si no hay ninguno
        "phones": ["+54 9 223 444-5555", "+54 9 11 1234-5678"]  // todos los cargados
      },
      "custom_fields": [             // máx 10 por propiedad
        {
          "id": "uuid",
          "key": "hora_llegada",
          "label": "Hora estimada de llegada",
          "type": "select",            // "text" | "textarea" | "select"
          "required": true,
          "placeholder": "",           // PUEDE FALTAR (campo opcional del hotel)
          "options": ["Antes de 14hs", "14-18hs", "Después de 18hs"]  // PUEDE FALTAR
        }
      ]
    }
  }
}

// ⚠️ description — CAMPO DE LEGADO, NO LO USES
//   El panel ya no le pide una descripción al hotel, así que este campo llega
//   null en todo alojamiento creado desde el 27/08/2026. En los anteriores
//   trae lo que tenía cargado y ya no se puede cambiar.
//   Se sigue devolviendo para no romper integraciones que lo leen. Si tu página
//   necesita un texto de presentación, ponelo de tu lado.

// ⚠️ bookings_enabled — LO PRIMERO QUE TENÉS QUE MIRAR
//   false ⇒ el hotel NO toma reservas online. Hay dos motivos y conviene
//   distinguirlos, porque uno es una decisión del hotel y el otro un descuido:
//
//   1) "hotel_paused" — apagó el interruptor "Aceptar reservas online" en su
//      panel: temporada cerrada, obras, se va de viaje. bookings_disabled_message
//      trae EL TEXTO QUE ESCRIBIÓ EL HOTEL, que suele decir cuándo vuelve a abrir
//      ("Las reservas abren el 1 de noviembre."). Mostralo tal cual.
//
//   2) "no_payment_method" — no configuró ningún método de cobro. No es una
//      decisión: probablemente no sabe que está así, y no puede recibir plata.
//
//   En los dos casos NO muestres el calendario ni el buscador: mostrá
//   bookings_disabled_message junto a los datos de contacto del hotel (los de
//   tu propio sitio: teléfono, WhatsApp, mail).
//
//   Esto NO es un chequeo de cortesía que podés saltearte: /checkout/start y
//   /checkout/confirm validan lo mismo del lado del servidor y rechazan con
//   422 BOOKINGS_DISABLED. Ignorarlo solo hace que el huésped complete todo el
//   formulario para chocar contra un error al final.
//
// payment_methods refleja exactamente lo que /confirm va a aceptar: ofrecé solo
//   los que estén en true y no deduzcas nada por tu cuenta. La clave de acá es
//   el valor que va en payment_method — "contact_hotel" incluido. En las
//   RESPUESTAS ese método sale como "manual", que es como lo guarda el sistema.

// closed_periods — temporadas cerradas (solo las que no terminaron).
//   Grisá esas fechas en tu calendario y mostrá el "reason": el huésped tiene
//   que entender que no es que esté lleno, sino que el hotel abre más adelante.
//   "end" es EXCLUSIVO: con end 2026-11-01 se puede entrar el 1 de noviembre.
//   /checkout/start y /confirm rechazan esas fechas con 422 PROPERTY_CLOSED.

// custom_fields — placeholder y options son OPCIONALES en el objeto: usá
//   (f.placeholder ?? "") y (f.options ?? []). options solo tiene sentido con
//   type === "select".

// 404 ORG_NOT_FOUND también cuando la organización existe pero está suspendida
//   (aunque hayas omitido org_slug).
GET
/api/public/availability

Disponibilidad por día (calendario)

Estado de cada día en un rango. Pintar el calendario con este resultado antes de que el usuario elija fechas.

Rate limit: 120 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (query string)

org_slug·string

Slug de la organización

property_slug·stringREQUERIDO

Slug de la propiedad

from·YYYY-MM-DDREQUERIDO

Fecha inicio (inclusiva)

to·YYYY-MM-DDREQUERIDO

Fecha fin (exclusiva). Máx 93 días por request.

Ejemplo de request

curl "https://app.artechia.com/api/public/availability\
  ?org_slug=hotel-costa\
  &property_slug=mar-azul\
  &from=2025-07-01\
  &to=2025-07-31"

Respuesta 200

{
  "ok": true,
  "days": [
    {
      "date": "2025-07-01",
      "status": "available",
      "free_units": 8,          // unidades LIBRES reales ese día
      "total_units": 8,         // unidades operativas del hotel
      "has_offer": false,
      "promotion": null,
      "closed": false,          // el hotel no toma reservas ese día
      "closed_reason": null     // motivo que cargó el hotel, para mostrarlo
    },
    {
      "date": "2025-07-04",
      "status": "limited",
      "free_units": 1,
      "total_units": 8,
      "has_offer": true,
      "promotion": {
        "id": "promo-uuid",
        "name": "Oferta de verano",
        "rule_type": "percent",   // "percent" | "fixed" | "stay_pay"
        "rule_value": "10",       // "10" (%) | "5000" ($) | "3/2" (stay/pay)
        "label": "10% off"        // legible: "10% off" | "-5000" | "3×2"
      }
    },
    { "date": "2025-07-10", "status": "unavailable", "free_units": 0, "total_units": 8, "has_offer": false, "promotion": null }
  ]
}

// status (calculado con ocupación REAL: reservas + locks + cupo + mantenimiento):
//   "available"   → verde / accent. Hay lugar.
//   "limited"     → amarillo. Quedan pocas (umbral PROPORCIONAL al tamaño:
//                   nunca "limited" si total_units === 1). free_units === 1 →
//                   "¡Última!". Mostrar free_units para dar urgencia real.
//   "unavailable" → free_units === 0 (lleno / cerrado) o fecha pasada. Gris,
//                   no clickeable — salvo como fecha de CHECK-OUT (el día de
//                   salida no consume noche; validá que todas las noches del
//                   rango [check_in, check_out) sean != unavailable).
// closed === true → el alojamiento está CERRADO ese día (temporada baja,
//   refacciones). Siempre viene con status "unavailable" y free_units 0, pero
//   no es lo mismo que "lleno": mostrá closed_reason para que el huésped sepa
//   que puede volver en otra fecha, en vez de creer que no hay lugar.
//   Ponelo al PIE del calendario, no solo en el tooltip del día: esos días no
//   se pueden clickear y en celular no hay hover, así que nadie lo lee.
// promotion !== null → pintar VERDE con badge (promotion.label). Vale también
//   para promos GLOBALES del hotel (aplican a todas las propiedades).
//   Explicá stay_pay en la leyenda: rule_value "3/2" = "quedate 3, pagá 2".
// has_offer === (promotion !== null) — campo de retrocompat
//
// OJO: la promo del calendario es informativa (es la promo activa ese día por
//   fecha/prioridad). Puede tener condiciones que la reserva concreta no cumpla
//   (min_nights, room_type_ids). El descuento REAL lo dice /search o /quote.
//
// from/to inválidos (formato, from >= to, rango > 93 días) → 400 VALIDATION,
//   NO 422 INVALID_DATES.
GET
/api/public/room-type

Detalle estático de un tipo de habitación

Devuelve datos estáticos (nombre, fotos, capacidad, amenities) sin cotización. Útil cuando navegás directamente a la página de detalle sin pasar por /search.

Rate limit: 120 req / min por IPCORS: Sí — llamable desde el navegadorCache: public, max-age=60

Parámetros (query string)

org_slug·string

Slug de la organización

property_slug·stringREQUERIDO

Slug de la propiedad

room_type_slug·stringREQUERIDO

Slug del tipo de habitación

Ejemplo de request

curl "https://app.artechia.com/api/public/room-type\
  ?org_slug=hotel-costa\
  &property_slug=mar-azul\
  &room_type_slug=suite-panoramica"

Respuesta 200

{
  "ok": true,
  "room_type": {
    "id":                "uuid",
    "name":              "Suite Panorámica",
    "slug":              "suite-panoramica",
    "description":       "Vista al mar con jacuzzi.",
    "tagline":           "Lujo frente al mar",   // frase corta marketing
    "size_m2":           38,                      // null si no configurada
    "max_occupancy":     3,
    "max_adults":        2,
    "capacity_children": 1,
    "base_occupancy":    2,
    "photo_url":         "https://...",           // thumbnail
    "photos":            [{ "url": "...", "alt": "...", "thumb_url": "...|null" }],
    "amenities":         ["WiFi", "AC", "TV"]
  }
}
POST
/api/public/quote

Cotizar habitación específica

Desglose detallado noche por noche. Útil para mostrar el breakdown de precio antes del checkout.

Rate limit: 90 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

org_slug·string

Slug de la organización

property_slug·stringREQUERIDO

Slug de la propiedad

room_type_slug·stringREQUERIDO

Slug del tipo de habitación

check_in·YYYY-MM-DDREQUERIDO

Fecha de entrada

check_out·YYYY-MM-DDREQUERIDO

Fecha de salida

rate_plan_id·uuid

NO usar desde un sitio público. El server resuelve solo el único plan tarifario aplicable (por fechas/prioridad); el plan no es elegible por el huésped. Omitir siempre.

adults·number

Adultos (default: 2)

children·number

Niños (default: 0)

extras·array

Extras a incluir: [{ extra_id: uuid, qty: number }]

coupon_code·string

Código de descuento

email·string (email)

Email del huésped (valida límites de cupón por usuario)

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/quote\
  -H "Content-Type: application/json" \
  -d '{
    "org_slug": "hotel-costa",
    "property_slug": "mar-azul",
    "room_type_slug": "suite-panoramica",
    "check_in": "2025-07-10",
    "check_out": "2025-07-13",
    "adults": 2,
    "coupon_code": "VERANO10"
  }'

Respuesta 200

{
  "ok": true,
  "quote": {
    "nights_count": 3,
    "nights": [   // desglose noche por noche (NO se llama "nightly_breakdown")
      { "date": "2025-07-10", "base": 30000, "occ_adj": 0,
        "single_use": 0, "total": 30000, "rate_plan_id": "uuid", "source": "base" }
      // source: "base" (tarifa base) | "daily" (override por día)
      // single_use: descuento por uso individual (siempre <= 0)
    ],
    "extras": [   // líneas de extras (incluye obligatorios con mandatory:true)
      { "extra_id": "uuid", "name": "Spa", "unit_price": 5000,
        "qty": 1, "total": 5000, "mandatory": false }
    ],
    "coupon": {   // null si no se aplicó cupón
      "id": "uuid", "code": "VERANO10", "type": "percent", "value": 10, "amount": 9000
    },
    "coupon_error": null,   // ← ver abajo: por qué NO se aplicó el cupón
    "promotion": {  // null si no hay promo automática
      "id": "uuid", "description": "10% off temporada alta", "discount_amount": 9000
    },
    "totals": {
      "subtotal_base":  90000,
      "discount_amount": 9000,
      "extras_total":       0,
      "taxes_total":        0,
      "tax_pct":            0,
      "total":          81000,
      "deposit_pct":       50,
      "deposit_due":    40500,
      "currency":       "ARS"
    },
    "validation": {  // mismos campos que /search + min_stay / max_stay
      "has_rate": true, "stop_sell": false, "closed": false, "min_stay_ok": true,
      "max_stay_ok": true, "occupancy_ok": true, "min_stay": 1, "max_stay": 30
    },
    "policy": {   // ← POLÍTICA DE CANCELACIÓN (solo en /quote, no en /search)
      "is_refundable": true,
      "cancellation_type": "flexible",          // flexible|moderate|strict|non_refundable
      "cancellation_deadline_date": "2025-07-08", // hasta cuándo se cancela sin penalidad
      "cancellation_deadline_days": 2,
      "penalty_type": "first_night"             // tipo de penalidad si cancela tarde
    }
  }
}

// Para mostrar la POLÍTICA DE CANCELACIÓN en tu sitio: usá quote.policy.
// /search NO la incluye (devuelve un quote reducido), así que si la querés
// por habitación tenés que llamar a /quote para cada una.

// ── CUPONES: un cupón inválido NO hace fallar el request ────────────────────
// Si mandás coupon_code y el código no sirve, la respuesta sigue siendo 200 y
// el quote se calcula SIN el descuento (con la promo automática si aplica).
// La única señal está en quote.coupon_error:
//
//   "coupon_error": {
//     "code":    "VERANO10",              // el código que mandaste
//     "reason":  "EXPIRED",
//     "message": "El código de descuento venció."   // en español, mostrable
//   }
//
// reason: NOT_FOUND | NOT_STARTED | EXPIRED | MIN_NIGHTS_NOT_MET |
//         ROOM_NOT_ELIGIBLE | RATE_NOT_ELIGIBLE | LIMIT_REACHED |
//         USER_LIMIT_REACHED | NO_DISCOUNT | PROMO_IS_BETTER
//         (PROMO_IS_BETTER: la promoción automática ya descuenta más;
//          se mantiene la promoción)
//
// Regla para tu UI: cupón aplicado ⟺ quote.coupon !== null && discount > 0.
// Si quote.coupon_error !== null, mostrá coupon_error.message y NO marques el
// cupón como aceptado. Nunca existió un error 422 COUPON_INVALID.
//
// El límite por email (USER_LIMIT_REACHED) solo se puede evaluar si mandás
// "email". Sin él, un cupón agotado para ese huésped se ve válido acá y recién
// lo rechaza /checkout/confirm con 409 COUPON_USER_LIMIT_REACHED.

// check_out <= check_in → 422 INVALID_DATES. check_in en el pasado → 422 INVALID_DATES.
POST
/api/public/checkout/start

Iniciar reserva (bloquear inventario)

Paso 1 del checkout. Bloquea el inventario y devuelve un checkout_token firmado HMAC. El TTL es configurable (default 15 min, máx 30 min) — mostrá un countdown al usuario.

Rate limit: 30 req / min por IP · 200 req / min por orgCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

org_slug·string

Slug de la organización

property_slug·stringREQUERIDO

Slug de la propiedad

room_type_slug·stringREQUERIDO

Slug del tipo de habitación

check_in·YYYY-MM-DDREQUERIDO

Fecha de entrada

check_out·YYYY-MM-DDREQUERIDO

Fecha de salida

adults·number

Adultos (default: 2, máx: 20)

children·number

Niños (default: 0, máx: 20)

ttl_minutes·number (1–30)

TTL del lock (default: 15, máx: 30). Tiempo que se bloquea el inventario. Fuera de rango → 400 VALIDATION.

rate_plan_id·uuid

NO usar desde un sitio público: el plan tarifario es único y lo resuelve el server (por fechas/prioridad). Omitir siempre.

extras·array

Extras seleccionados: [{ extra_id: uuid, qty: number }]

coupon_code·string

Código de descuento

email·string (email)

Email del huésped para pre-validación de cupones por usuario

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/checkout/start\
  -H "Content-Type: application/json" \
  -d '{
    "org_slug": "hotel-costa",
    "property_slug": "mar-azul",
    "room_type_slug": "suite-panoramica",
    "check_in": "2025-07-10",
    "check_out": "2025-07-13",
    "adults": 2,
    "ttl_minutes": 15,
    "coupon_code": "VERANO10",
    "extras": [{ "extra_id": "uuid-del-extra", "qty": 2 }]
  }'

Respuesta 200

{
  "ok": true,
  "checkout_token": "eyJ...",   // token firmado con HMAC, ~600–900 chars
  "lock_key": "2a6527aa61dea2808a45b18fe9e38ae2d28174f3a34c20bc",  // hex 48, opaco
  "expires_at": "2026-07-24T23:45:19.605738+00:00",  // ISO con OFFSET (no "Z")
  "ttl_minutes": 15,
  "holds_inventory": true, // false si el alojamiento no guarda la habitación durante el checkout: sigue a la venta y /confirm puede dar 409 NO_AVAILABILITY
  "available_after": 4,   // ← ver nota: es el conteo ANTES de tomar este lock
  "quote": { ... }        // mismo formato que /quote (incluye coupon_error)
}

// El checkout_token va en el body de /checkout/confirm
// y como Bearer en /checkout/extras. Tratalo como opaco.
//
// ⚠️ available_after — el nombre engaña. Es la disponibilidad medida ANTES de
//    insertar este lock, así que después de tu request quedan
//    (available_after − 1) unidades. available_after === 1 significa que
//    acabás de tomar la ÚLTIMA: "¡Era la última!" es correcto, "queda 1" NO.
//
// ⚠️ lock_key NO se usa desde el front: todo se hace con el checkout_token.
//
// ⚠️ expires_at viene con offset "+00:00", no con "Z". new Date() lo parsea
//    igual; no lo cortes con slice() ni asumas el sufijo Z.
GET
/api/public/checkout/extras

Listar extras opcionales del lock

Devuelve los extras OPCIONALES disponibles para la habitación bloqueada. Los extras obligatorios (is_mandatory=true) ya están incluidos en el quote de /checkout/start y NO aparecen acá.

Rate limit: 120 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (query string)

Authorization·header: Bearer <checkout_token>REQUERIDO

Token devuelto por /checkout/start

Ejemplo de request

curl https://app.artechia.com/api/public/checkout/extras\
  -H "Authorization: Bearer eyJ..."

Respuesta 200

{
  "ok": true,
  "extras": [
    {
      "id": "uuid-extra-spa",
      "name": "Acceso al spa",
      "description": "Acceso ilimitado durante la estadía.",
      "price": 5000,
      "price_type": "per_booking",   // 9 valores posibles — ver abajo, NO son solo 3
      "max_qty": 1
    },
    {
      "id": "uuid-extra-parking",
      "name": "Estacionamiento",
      "description": null,
      "price": 1500,
      "price_type": "per_night",
      "max_qty": 2
    }
  ]
}

// price_type — el hotel puede configurar CUALQUIERA de estos valores:
//   per_booking / per_stay / fixed → precio único por toda la reserva
//   per_night                      → precio × cantidad de noches
//   per_person                     → precio × (adultos + niños)
//   per_person_per_stay            → precio × huéspedes (una vez)
//   per_pax_night / per_person_per_night → precio × huéspedes × noches
//   percent_of_room                → porcentaje del subtotal de habitación
//
// NO hardcodees la multiplicación: el total que vale es el de /quote o
// /checkout/start con el extra ya incluido. Si necesitás mostrar el importe
// antes de pedirlo, tratá price_type desconocido como "por reserva".
//
// max_qty puede venir null → sin tope (usá un máximo razonable, ej. 10).
// description puede ser null.
// Si extras está vacío → no mostrar sección de extras.
// Los extras seleccionados se envían en /confirm: extras: [{ extra_id, qty }]
//
// Errores (mismo formato que el resto: { ok:false, error:{ code, message } }):
//   401 TOKEN_MALFORMED / TOKEN_BAD_SIGNATURE / TOKEN_INVALID_JSON
//   410 TOKEN_EXPIRED   → el lock venció, reiniciá desde /checkout/start
//   429 RATE_LIMITED
POST
/api/public/checkout/guest-preview

Preview en vivo del huésped en el PMS (PATCH)

Fire-and-forget: llamar en el evento blur de nombre / apellido / email para que el hotel vea en tiempo real quién está completando el checkout. NO bloquear el flujo si falla. Método HTTP real: PATCH.

Rate limit: 90 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

checkout_token·stringREQUERIDO

Token de /checkout/start

guest.first_name·string (máx 100)

Nombre parcial

guest.last_name·string (máx 100)

Apellido parcial

guest.email·string (email)

Email parcial

guest.phone·string (máx 40)

Teléfono parcial

guest.document_number·string (máx 40)

Documento parcial

guest.special_requests·string (máx 1000)

Pedidos especiales

Ejemplo de request

curl -X PATCH https://app.artechia.com/api/public/checkout/guest-preview\
  -H "Content-Type: application/json" \
  -d '{
    "checkout_token": "eyJ...",
    "guest": {
      "first_name": "María",
      "last_name": "González",
      "email": "maria@ejemplo.com"
    }
  }'

Respuesta 200

{ "ok": true }

// Devuelve 200 aunque el lock ya no exista o no hayas mandado ningún campo
// dentro de "guest" (el objeto guest sí tiene que estar presente).
// Errores posibles: 400 VALIDATION / INVALID_BODY, 401 TOKEN_MALFORMED,
// 410 TOKEN_EXPIRED, 429 RATE_LIMITED — todos con { ok:false, error:{...} }.
//
// Es fire-and-forget: NUNCA bloquees el checkout por lo que devuelva.
GET
/api/public/checkout/lock-status

Polling del estado del lock

Verifica si el lock de inventario sigue activo. Pensado para polling cada ~10 s durante la pantalla de checkout. Detecta expiración del TTL o cancelación desde el panel admin antes del confirm.

Rate limit: 240 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (query string)

checkout_token·stringREQUERIDO

Token de /checkout/start (query string)

Ejemplo de request

curl "https://app.artechia.com/api/public/checkout/lock-status\
  ?checkout_token=eyJ..."

Respuesta 200

// Lock activo:
{
  "active": true,
  "expires_at": "2026-07-24T23:45:19.605738+00:00",
  "seconds_remaining": 600
}

// Lock inactivo:
{
  "active": false,
  "reason": "confirmed" | "expired" | "cancelled" | "invalid" | "rate_limited"
}

// reason:
//   "confirmed"  → el lock se consumió porque la reserva SE CREÓ. Es el final
//                  feliz, no un error. Cortá el polling apenas confirmás y, si
//                  igual llega, no muestres ningún cartel de cancelación.
//   "expired"    → venció el TTL → reiniciar desde /checkout/start.
//   "cancelled"  → el hotel dio de baja el hold desde el panel.
//   "invalid"    → token ausente, mal formado o con firma inválida.
//   "rate_limited" → único caso con status 429.
//
// Siempre devuelve 200 (excepto rate_limited → 429) para no revelar si el
// token es válido a través del status code. Este endpoint NO usa el formato
// { ok, error } del resto de la API.
//
// ⚠️ NO confundir con GET /api/public/checkout/status?token=... — existe, pero
// NO es un alias: es un endpoint viejo que usa la página de checkout alojada en
// Artechia, y contesta distinto.
//
//   caso                  /lock-status                     /status
//   lock vivo             active:true + expires_at +       active:true
//                         seconds_remaining                (sin contador)
//   reserva ya creada     active:FALSE, reason:"confirmed"  active:TRUE, reason:"confirmed"
//
// O sea que "active" sale INVERTIDO una vez que la reserva se creó. Si armás el
// countdown contra /status vas a mostrar el lock como vivo para siempre.
// Usá /lock-status.
POST
/api/public/checkout/confirm

Confirmar reserva

Paso 2 del checkout. Recibe el token + datos del huésped y crea la reserva definitivamente. Si el token expiró (410), mostrar error y pedir que repita el proceso. Si vino LOCK_USED (409), tratar como confirmación exitosa (doble submit). Si el email ya tiene una ficha de huésped en el hotel, los datos que mandes NO la pisan: sólo completan lo que estuviera vacío.

Rate limit: 30 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

checkout_token·stringREQUERIDO

Token de /checkout/start

guest.first_name·string (máx 100)REQUERIDO

Nombre

guest.last_name·string (máx 100)REQUERIDO

Apellido

guest.email·string (email, máx 200)REQUERIDO

Email (recibe confirmación)

guest.phone·string (máx 40)

Teléfono

guest.document_type·string (máx 40)

Tipo doc (ej: 'DNI', 'Pasaporte')

guest.document_number·string (máx 40)REQUERIDO

Número de documento (DNI/pasaporte) — obligatorio

guest.country·ISO 3166-1 α-2

País (ej: AR, UY, CL, BR)

guest.city·string (máx 100)

Ciudad

guest.address·string (máx 200)

Dirección

payment_method·'mercadopago' | 'transferencia' | 'contact_hotel' | 'card_manual'

Default: mercadopago. 'contact_hotel' = coordinar el pago con el alojamiento (solo si property.checkout.payment_methods.contact_hotel === true; se acepta 'manual' como sinónimo, que es el nombre con el que sale en las respuestas). 'card_manual' = el huésped deja la tarjeta y el alojamiento la cobra a mano (solo si payment_methods.card_manual === true; exige el objeto card). 'cash' es exclusivo del flujo admin.

card·object

OBLIGATORIO con payment_method 'card_manual'. { number, exp_month (1-12), exp_year (2 o 4 dígitos), cvv (3, o 4 en Amex), holder_name, document }. Se valida el dígito verificador (Luhn), el largo y el vencimiento: si algo no cierra, 422 VALIDATION_FAILED con el campo en error.details.field. Mandalo SIEMPRE sobre HTTPS y no lo guardes de tu lado.

extras·array

Extras seleccionados: [{ extra_id: uuid, qty: number }]

coupon_code·string (máx 50)

Código de descuento (si no se envió en /start)

special_requests·string (máx 1000)

Pedidos especiales (si special_requests_enabled)

custom_fields·object

{ [key]: string|number|boolean }. Solo se guardan las keys declaradas en property.checkout.custom_fields[]; el resto se descarta. Máx 50 keys, key ≤100, valor ≤2000 chars.

accept_terms·boolean

Aceptación de T&C. OBLIGATORIO (validado server-side) si property.checkout.terms tiene texto (no null ni vacío): sin true → 422 TERMS_NOT_ACCEPTED.

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/checkout/confirm\
  -H "Content-Type: application/json" \
  -d '{
    "checkout_token": "eyJ...",
    "guest": {
      "first_name": "María",
      "last_name": "González",
      "email": "maria@ejemplo.com",
      "phone": "+5491122334455",
      "document_type": "DNI",
      "document_number": "30123456",
      "country": "AR"
    },
    "payment_method": "mercadopago",
    "special_requests": "Llegada tardía, cuna extra",
    "accept_terms": true
  }'

Respuesta 200

{
  "ok": true,
  "booking_id":    "uuid-de-la-reserva",
  "booking_code":  "ART260705A647",  // código legible para mostrar
  "access_token":  "<64-char-hex-token>",
  "manage_url":    "https://app.artechia.com/my-booking?code=...&token=...",
  "grand_total":   81000,
  "deposit_due":   40500,
  "deposit_pct":   50,                    // % de seña del plan (0 si no hay seña)
  "currency":      "ARS",
  "status":        "pending",             // ver nota
  "idempotent":    false,                 // true si fue un re-submit y devolvió la reserva existente

  // SOLO si payment_method === "mercadopago" (preference generada inline):
  "payment_link": {
    "init_point":    "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=...",
    "preference_id": "1234567-abc",
    "pay_mode":      "deposit",           // "deposit" | "total" | "balance"
    "amount":        40500
  },
  // SOLO si payment_method === "transferencia":
  "bank_data": { "cbu": "...", "alias": "...", "bank": "...", "holder": "..." },
  // SOLO si payment_method === "manual" (coordinar con el alojamiento):
  // "phone" es el primero de la lista; "phones" los trae todos.
  "contact_hotel": {
    "email": "reservas@hotel.com",
    "phone": "+54 9 11 5555-0000",
    "phones": ["+54 9 11 5555-0000", "+54 9 223 444-5555"]
  },
  // SOLO si payment_method === "card_manual": lo único que devolvemos de la
  // tarjeta. Los datos completos no vuelven nunca.
  "card_on_file": { "brand": "Visa", "last4": "3704" }
}

// status: una reserva recién creada SIEMPRE nace "pending". Si idempotent es
//   true, status es el estado ACTUAL de la reserva que ya existía y puede ser
//   cualquiera de: pending | confirmed | checked_in | checked_out | no_show |
//   cancelled. No existe el estado "hold".
//
// idempotent: true → NO es un error. El mismo checkout_token ya había creado
//   una reserva y te la devolvemos igual (booking_code, access_token y
//   payment_link son los de esa reserva, incluida su seña real y el link de
//   pago ya emitido). Nunca reintentes ni dupliques.
//
// MercadoPago: si payment_link viene presente, redirigí directo a
//   payment_link.init_point — NO hace falta llamar a /checkout/payment-link.
//   payment_link puede ser null si MP falló, si el hotel no tiene credenciales
//   cargadas, o si la reserva ya está saldada: en ese caso usá
//   /checkout/payment-link como fallback (o para cobrar el "balance").
//   pay_mode === "balance" solo aparece en un re-submit de una reserva que ya
//   tenía pagos aplicados.
// Transferencia: mostrá bank_data (puede tener campos null si el hotel no los
//   cargó). Es null si el método no está habilitado.
// Manual ("coordinar con el alojamiento"): viene contact_hotel con el email y
//   el teléfono a los que escribir. La reserva queda pending hasta que el hotel
//   marque el pago a mano. Es null si el método no está habilitado.
// card_manual: la tarjeta queda guardada cifrada del lado del hotel, que la
//   cobra con su posnet. NO hay autorización online: la reserva queda pending
//   hasta que el hotel registre el pago, y card_on_file sólo trae marca y
//   últimos 4 para que le muestres al huésped cuál dejó. Los datos se borran
//   solos a los 7 días o cuando el hotel marca el cobro. No los guardes de tu
//   lado ni los pidas por otro canal.
// Si el hotel configuró custom_booking_url, manage_url apunta ahí.
// Guardá booking_code + access_token.
GET
/api/public/booking/lookup

Consultar reserva (por email o por el token del link)

Devuelve la información completa de una reserva. Se autoriza con el email del huésped (formulario 'consultá tu reserva') o con el access_token que viaja en el manage_url — que es lo que te deja armar tu propia página 'mi reserva': recibís al huésped por ese link y resolvés la reserva sin volver a pedirle el email.

Rate limit: 30 req / min por IP · 10 req / 5 min por booking_codeCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (query string)

booking_code·stringREQUERIDO

Código de la reserva (ej: ART260705A647). También el de una reserva de varias habitaciones (GRP-…): responde con la misma forma, con montos y personas sumados, todas las habitaciones en room (unidas con " + ") y una por una en rooms[].

email·string (email)

Email del huésped (case-insensitive). Obligatorio si no mandás access_token.

access_token·string (64 hex)

El token del manage_url. Alternativa al email. También se acepta como 'token', que es como viene en el link.

Ejemplo de request

curl "https://app.artechia.com/api/public/booking/lookup\
  ?booking_code=ART260705A647\
  &email=maria@ejemplo.com"

Respuesta 200

{
  "ok": true,
  "booking": {
    "booking_code":     "ART260705A647",
    "status":           "confirmed",   // pending|confirmed|checked_in|checked_out|no_show|cancelled
    "payment_status":   "partial",     // unpaid|partial|paid|refunded|chargeback
    "check_in":         "2025-07-10",
    "check_out":        "2025-07-13",
    "nights":           3,
    "adults":           2,
    "children":         0,
    "grand_total":        81000,      // lo cotizado al reservar
    "consumptions_total": 2500,       // consumos cargados durante la estadía
    "total_due":          83500,      // grand_total + consumptions_total
    "amount_paid":        40500,
    "balance_due":        43000,      // total_due − amount_paid (nunca negativo)
    "currency":           "ARS",
    "payment_method":   "mercadopago", // mercadopago|transferencia|manual|card_manual|null
    "special_requests": "Cuna para bebé",
    "cancelled_at":     null,
    "cancel_reason":    null,
    "created_at":       "2025-07-01T10:00:00.000Z"
  },
  "guest":     { "first_name": "...", "last_name": "...", "email": "...", "phone": null },
  "property":  { "name": "...", "address": "...", "slug": "mar-azul" } | null,
  "room":      { "room_type_name": "Suite Panorámica", "room_unit_name": "Suite 301",
                 "room_unit_pending": false } | null,
  //   ⚠️ Mientras la reserva esté pendiente y sin pagar, room_unit_name viene
  //   en null y room_unit_pending en true: la habitación todavía puede
  //   cambiar, porque el hotel se la puede dar a alguien que pague antes.
  //   No le muestres un número al huésped hasta que room_unit_pending sea false.
  "rate_plan": { "name": "Tarifa Estándar" } | null,

  // Cómo pagar lo que falta. Es lo que necesitás para armar tu propia página
  // "mi reserva" sin haber guardado la respuesta de /checkout/confirm.
  "payment": {
    // null si no queda nada por pagar (o la reserva está cancelada/no_show/checked_out).
    "due": {
      "mode":        "deposit",  // deposit (seña) | balance (saldo tras un pago parcial) | total
      "amount":      40500,      // lo que hay que pagar AHORA
      "grand_total": 81000,
      "amount_paid": 40500,
      "balance":     40500,      // todo lo que falta (≥ amount: con seña, el resto va después)
      "currency":    "ARS"
    },
    // Sólo con payment_method "transferencia". Cualquier campo puede ser null.
    "bank_data": { "cbu": "...", "alias": "...", "bank": "...", "holder": "..." },
    // Con "transferencia" (para mandar el comprobante), "manual" y "card_manual".
    "contact_hotel": {
      "email": "...",
      // El primero y su link, por compatibilidad. "phones" trae todos los
      // teléfonos del alojamiento, cada uno con su propio link de WhatsApp.
      "phone": "+54 9 11 ...",
      "whatsapp_url": "https://wa.me/549...?text=...",  // mensaje ya armado
      "phones": [
        { "phone": "+54 9 11 ...", "whatsapp_url": "https://wa.me/549...?text=..." }
      ]
    },
    // Sólo con "card_manual": la tarjeta que dejó, para que la reconozca.
    // null si los datos ya se borraron (a los 7 días, o cuando el hotel marcó
    // el cobro como hecho).
    "card_on_file": { "brand": "Visa", "last4": "3704" }
  },
  "manage_url": "https://app.artechia.com/my-booking?code=...&token=..."
}

// MercadoPago no devuelve link acá (crear una preference es un efecto de lado
// y esto es un GET): pedilo con POST /checkout/payment-link.
// 404 si el booking_code no existe O si el email no coincide.
// Comparación timing-safe: no se revela cuál de los dos falló.
POST
/api/public/review

Enviar review post-estadía

Registra rating (1–5) y feedback opcional para la reserva. Requiere booking_code + access_token (mismo del confirm). Idempotente: una sola review por booking (upsert).

Rate limit: 20 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

code·string (3–40)REQUERIDO

booking_code de la reserva

token·string (64 hex)REQUERIDO

access_token devuelto por /checkout/confirm

rating·number (1–5)REQUERIDO

Puntuación entera de 1 a 5

feedback·string (máx 2000)

Comentario libre del huésped

google_redirected·boolean

true si el huésped fue redirigido a dejar review en Google

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/review\
  -H "Content-Type: application/json" \
  -d '{
    "code": "ART260705A647",
    "token": "<64-char-hex-token>",
    "rating": 5,
    "feedback": "Excelente atención, volveríamos."
  }'

Respuesta 200

{ "ok": true }

// Errores con el formato estándar { ok: false, error: { code, message } }:
//   400 INVALID_BODY / VALIDATION  → body mal formado o rating fuera de 1–5
//   404 NOT_FOUND                  → booking_code inexistente, eliminado, o
//                                    access_token que no coincide (timing-safe)
//   429 RATE_LIMITED
POST
/api/public/marketing/subscribe

Suscribir al newsletter del hotel

Da de alta a una persona en la base de huéspedes del hotel para recibir promociones. Para el típico formulario 'dejanos tu mail y te avisamos de las ofertas'. Exige consentimiento explícito y lo deja registrado con fecha.

Rate limit: 10 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

property_slug·stringREQUERIDO

Slug de la propiedad

org_slug·string

Slug de la organización

email·string (email)REQUERIDO

Email de la persona

first_name·string (1–100)REQUERIDO

Nombre

last_name·string (1–100)REQUERIDO

Apellido

accept_marketing·boolean (true)REQUERIDO

Mandalo solo si la persona tildó la casilla de consentimiento. Sin esto → 400 CONSENT_REQUIRED.

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/marketing/subscribe \
  -H "Content-Type: application/json" \
  -d '{
    "property_slug": "hotel-mar-azul",
    "email": "maria@example.com",
    "first_name": "María",
    "last_name": "González",
    "accept_marketing": true
  }'

Respuesta 200

{ "ok": true, "subscribed": true }

// La respuesta es SIEMPRE la misma, exista o no el email. Es a propósito: si
// contestara distinto, cualquiera podría averiguar quién es cliente del hotel
// probando direcciones.
//
// Tres cosas que este endpoint NO hace, y conviene saberlas:
//
//   1. NO pisa los datos de alguien que ya está en la base. Si el email
//      corresponde a un huésped que ya reservó, su nombre queda como estaba.
//   2. NO revierte una baja. Si la persona se dio de baja de los emails, sigue
//      dada de baja. Para volver a suscribirla, el hotel lo hace desde su panel.
//   3. NO manda ningún email de confirmación. Si querés doble opt-in, mandalo
//      vos desde tu sitio antes de llamar acá.
//
// Errores:
//   400 CONSENT_REQUIRED → falta accept_marketing: true
//   400 VALIDATION       → email/first_name/last_name inválidos o faltantes
//   404 PROPERTY_NOT_FOUND
//   429 RATE_LIMITED

Reservas de grupo (varias unidades en un checkout)

Para vender varias unidades juntas (por ejemplo, 4 cabañas para una familia grande) como UNA reserva: un titular, las mismas fechas, un cupón para el conjunto. En el PMS cada unidad queda como una reserva normal, vinculada a un grupo (código GRP-XXXXXXXXXX). Un cupón se consume UNA vez por grupo; un descuento fijo se descuenta una sola vez; uno porcentual va sobre el subtotal elegible del grupo. Bloquear y confirmar son todo o nada: si falta una unidad no se bloquea ninguna, y si una reserva falla al confirmar no se crea ninguna. Pago: MercadoPago (UN solo pago por la seña —o el total— de todo el grupo, que el PMS reparte entre las reservas), transferencia, coordinar con el alojamiento o tarjeta que cobra el hotel.

Flujo: /group/quote → /group/checkout/start → (datos del huésped) → /group/checkout/confirm. Si el huésped abandona, /group/checkout/release. El resumen para el huésped está en /group.

POST
/api/public/group/quote

Cotizar un grupo

Precio por unidad y del grupo, cupón aplicado al conjunto y disponibilidad real. No bloquea nada ni consume usos del cupón.

Rate limit: 60 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

property_slug·stringREQUERIDO

Slug de la propiedad

org_slug·string

Slug de la organización

check_in·YYYY-MM-DDREQUERIDO

Entrada (la misma para todas las unidades)

check_out·YYYY-MM-DDREQUERIDO

Salida (exclusiva)

units·array (1–10)REQUERIDO

[{ key?, room_type_slug, adults, children?, extras?, rate_plan_id? }]. key identifica la unidad en la respuesta (default: posición "0", "1"…). Dos unidades del mismo tipo = dos elementos con el mismo room_type_slug.

coupon_code·string

Cupón para todo el grupo

email·string (email)

Email del titular: permite validar el límite por huésped del cupón

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/group/quote \
  -H "Content-Type: application/json" \
  -d '{
    "property_slug": "cabanas-franco",
    "check_in": "2026-12-28", "check_out": "2026-12-31",
    "units": [
      { "key": "c7",  "room_type_slug": "cabana-para-7", "adults": 7 },
      { "key": "c4a", "room_type_slug": "cabana-para-4", "adults": 4 },
      { "key": "c4b", "room_type_slug": "cabana-para-4", "adults": 4 },
      { "key": "c6",  "room_type_slug": "cabana-para-6", "adults": 6 }
    ],
    "coupon_code": "FAMILIA",
    "email": "titular@example.com"
  }'

Respuesta 200

{
  "ok": true,
  "quote": {
    "check_in": "2026-12-28", "check_out": "2026-12-31", "nights_count": 3, "currency": "ARS",
    "units": [
      { "key": "c7", "room_type": { "id": "…", "slug": "cabana-para-7", "name": "Cabaña para 7" },
        "adults": 7, "children": 0, "coupon_eligible": true,
        "totals": { "subtotal_base": 300000, "promotion_discount": 0, "coupon_discount": 3225.81,
                    "discount_amount": 3225.81, "extras_total": 0, "taxes_total": 0,
                    "total": 296774.19, "deposit_pct": 30, "deposit_due": 89032.26, "balance_due": 207741.93 },
        "promotion": null, "nights": [ … ], "extras": [ … ], "policy": { … } },
      …
    ],
    "totals": { "subtotal_base": 930000, "promotions_discount": 0, "coupon_discount": 10000,
                "discount_amount": 10000, "extras_total": 0, "taxes_total": 0,
                "total": 920000, "deposit_due": 276000, "balance_due": 644000 },
    "coupon": { "code": "FAMILIA", "type": "fixed", "value": 10000, "amount": 10000,
                "eligible_unit_keys": ["c7","c4a","c4b","c6"], "excluded": [] },
    "coupon_error": null
  }
}
// La suma de las unidades es EXACTAMENTE la del grupo (total, seña, saldo, descuentos).
// coupon_error (si el cupón no aplica): { code, reason, message, excluded? }
//   reason: NOT_FOUND | NOT_STARTED | EXPIRED | MIN_NIGHTS_NOT_MET | ROOM_NOT_ELIGIBLE |
//           RATE_NOT_ELIGIBLE | LIMIT_REACHED | USER_LIMIT_REACHED | NO_DISCOUNT | PROMO_IS_BETTER
// coupon.excluded: unidades que no califican (tipo o tarifa), con su motivo.
//
// Errores (error.code + error.reason + error.unit_errors[{ key, code, reason, message }]):
//   409 NO_AVAILABILITY  reason SOLD_OUT (agotado) | TEMPORARILY_HELD (otro checkout en curso)
//   422 STAY_RESTRICTED  reason MIN_STAY | MAX_STAY | CAPACITY | CLOSED_TO_SALES | NO_RATE
//   422 PROPERTY_CLOSED  (período de cierre) · 422 BOOKINGS_DISABLED (reason HOTEL_PAUSED | NO_PAYMENT_METHOD)
//   422 INVALID_DATES (reason ADVANCE_WINDOW si es por anticipación) · 422 INVALID_UNITS
POST
/api/public/group/checkout/start

Bloquear todas las unidades del grupo

Bloquea TODAS las unidades (o ninguna) por el plazo del alojamiento (15 min por defecto) y devuelve el checkout_token. No consume usos del cupón. Los bloqueos sólo afectan noches que se superponen (la salida es exclusiva).

Rate limit: 20 req / min por IP · 100 req / min por orgCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

…·

Los mismos campos que /group/quote

idempotency_key·string (8–100)

Misma clave = mismo pedido: un reintento (doble clic, red caída) devuelve los mismos bloqueos con idempotent: true. La misma clave con otras fechas/unidades → 409 IDEMPOTENCY_CONFLICT.

ttl_minutes·number (1–30)

Pedir menos tiempo que el del alojamiento (nunca más)

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/group/checkout/start \
  -H "Content-Type: application/json" \
  -d '{ "property_slug": "cabanas-franco", "check_in": "2026-12-28", "check_out": "2026-12-31",
        "units": [ { "key": "c7", "room_type_slug": "cabana-para-7", "adults": 7 } ],
        "coupon_code": "FAMILIA", "email": "titular@example.com",
        "idempotency_key": "web-7f3c9a2e-1" }'

Respuesta 200

{
  "ok": true,
  "checkout_token": "eyJ…",          // opaco; va a /group/checkout/confirm
  "expires_at": "2026-11-02T15:20:00.000Z",
  "ttl_minutes": 15,
  "idempotent": false,
  "quote": { … }                     // mismo formato que /group/quote
}
// 409 NO_AVAILABILITY → no quedó NADA bloqueado; error.unit_errors dice qué unidad y por qué
//     (SOLD_OUT o TEMPORARILY_HELD: otra persona terminando de reservar, reintentar en minutos).
// 409 IDEMPOTENCY_CONFLICT · 409 LOCK_TIMEOUT · 422 como /group/quote
POST
/api/public/group/checkout/confirm

Confirmar el grupo

Vuelve a cotizar con el email real. Si el total cambió, devuelve 409 PRICE_CHANGED con la cotización nueva y NO crea nada. Si no, crea todas las reservas (o ninguna) y consume UN uso del cupón. Reintentable: el mismo token devuelve el mismo grupo.

Rate limit: 20 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

checkout_token·stringREQUERIDO

El de /group/checkout/start

guest·objectREQUERIDO

{ first_name, last_name, email, document_number, phone?, document_type?, country?, city?, address? } — titular del grupo

payment_method·string

mercadopago | transferencia | contact_hotel (o manual) | card_manual. Default: contact_hotel. Con mercadopago la respuesta trae payment_link (un solo pago para todo el grupo).

card·object

Datos de la tarjeta si payment_method = card_manual (mismo formato que /checkout/confirm)

expected_total·number

Total que el huésped vio y aceptó. Mandalo siempre: si difiere del recalculado, 409 PRICE_CHANGED.

coupon_code·string

Cambiar el cupón (se recotiza; si el total cambia → PRICE_CHANGED)

accept_terms·boolean

Obligatorio si el alojamiento tiene términos configurados

custom_fields·object

Campos propios del alojamiento (los obligatorios se validan)

special_requests·string

Pedidos especiales

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/group/checkout/confirm \
  -H "Content-Type: application/json" \
  -d '{ "checkout_token": "eyJ…",
        "guest": { "first_name": "Ana", "last_name": "Pérez", "email": "ana@example.com", "document_number": "30111222" },
        "payment_method": "contact_hotel", "accept_terms": true, "expected_total": 920000 }'

Respuesta 200

{
  "ok": true,
  "group_code": "GRP-037CBA6B4B",
  "group_access_token": "…",
  "summary_url": "https://…/my-booking?code=GRP-037CBA6B4B&token=…",
  "idempotent": false,
  "currency": "ARS",
  "totals": { "total": 494000, "deposit_due": 148199.99, "balance_due": 345800.01, "coupon_discount": 100000 },
  "bookings": [
    { "key": "c7", "booking_code": "ART…", "access_token": "…", "manage_url": "…",
      "room_type_name": "Cabaña para 7", "grand_total": 230631.58, "deposit_due": 69189.47, "status": "pending" },
    …
  ],
  "payment_method": "manual",
  "contact_hotel": { "email": "…", "phone": "…", "phones": ["…"] }   // o bank_data / card_on_file según el método
}
// Con payment_method "mercadopago", en vez de contact_hotel/bank_data viene:
//   "payment_link": { "init_point": "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=…",
//                     "preference_id": "…", "pay_mode": "deposit", "amount": 148199.99 }   // o null si MP falló
//   Redirigí a init_point: es UN pago por la seña (o el total) de TODO el grupo. Al volver, MP lleva al
//   huésped a summary_url. Si vino null, pedilo con GET /api/public/group (lo crea ahí).
// 409 PRICE_CHANGED → { error: { details: { previous_total, new_total, coupon_error } }, quote: { … } }
//     Mostrá el total nuevo y reenviá con expected_total = new_total.
// 409 COUPON_LIMIT_REACHED | COUPON_USER_LIMIT_REACHED → el último uso lo tomó otro grupo: recotizá sin cupón.
// 410 TOKEN_EXPIRED | LOCK_EXPIRED | LOCK_NOT_FOUND → el plazo venció: volver a /group/checkout/start.
// 422 PAYMENT_METHOD_NOT_AVAILABLE | TERMS_NOT_ACCEPTED | VALIDATION_FAILED | GUEST_BLACKLISTED | TOKEN_INVALID
POST
/api/public/group/checkout/release

Soltar los bloqueos del grupo

El huésped abandonó o cambió la selección: libera ya las unidades bloqueadas que no se convirtieron en reserva. Si no se llama, vencen solas al terminar el plazo. Idempotente.

Rate limit: 30 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (body JSON)

checkout_token·stringREQUERIDO

El de /group/checkout/start

Ejemplo de request

curl -X POST https://app.artechia.com/api/public/group/checkout/release \
  -H "Content-Type: application/json" -d '{ "checkout_token": "eyJ…" }'

Respuesta 200

{ "ok": true, "released": 4 }   // 0 si ya estaban sueltos o eran reservas
GET
/api/public/group

Resumen del grupo para el huésped

Estado del grupo, totales vigentes (sin lo cancelado), lo pagado, el saldo y cada reserva con su link de gestión. Requiere el código y el token del grupo.

Rate limit: 60 req / min por IPCORS: Sí — llamable desde el navegadorCache: no-store

Parámetros (query string)

code·stringREQUERIDO

GRP-XXXXXXXXXX

token·stringREQUERIDO

group_access_token

Ejemplo de request

curl "https://app.artechia.com/api/public/group?code=GRP-037CBA6B4B&token=…"

Respuesta 200

{
  "ok": true,
  "group": {
    "group_code": "GRP-037CBA6B4B", "status": "active",   // active | partially_cancelled | cancelled
    "property": { "name": "…", "slug": "…" }, "check_in": "…", "check_out": "…", "currency": "ARS",
    "payment_method": "manual", "coupon": { "code": "FAMILIA", "discount": 10000 },
    "totals": { "total": 920000, "deposit_due": 276000, "paid": 0, "balance_due": 920000 },
    "payment_link": null,   // si el alojamiento cobra con MercadoPago y falta pagar: { init_point, preference_id, pay_mode, amount }
                            // pay_mode "deposit" (seña de todo el grupo) si no pagó nada; "balance" (saldo de TODAS las reservas) si ya pagó la seña
    "bookings": [ { "booking_code": "ART…", "room_type_name": "…", "unit_name": "…", "status": "pending",
                    "grand_total": 296774.19, "coupon_share": 3225.81, "amount_paid": 0,
                    "balance_due": 296774.19, "manage_url": "…" }, … ]
  }
}
// 404 GROUP_NOT_FOUND si el código o el token no son válidos.

Formato de datos

Convenciones para fechas, montos, timestamps y tipos básicos en toda la API.

Fechas y timezone

  • check_in, check_out, from, to: strings YYYY-MM-DD (sin hora).
  • Se interpretan en el timezone de la propiedad (campo property.timezone, ej America/Argentina/Buenos_Aires) — NO en UTC.
  • check_out es exclusiva: una reserva del 10 al 13 julio = 3 noches (10→11, 11→12, 12→13). El huésped se va el 13.
  • availability.to también exclusiva. Para julio completo: from=2025-07-01 to=2025-08-01.
  • Si tu sitio sirve a múltiples zonas, normalizá fechas en el cliente usando el timezone que devuelve /property.

Timestamps ISO 8601

  • expires_at, created_at, cancelled_at: ISO 8601 en UTC, pero el sufijo varía: created_at viene como 2025-07-01T10:00:00.000Z y expires_at como 2026-07-24T23:45:19.605738+00:00. Parsealos siempre con new Date(...); no cortes el string ni asumas la Z.
  • Convertir a TZ local en cliente: new Date(expires_at).toLocaleString().
  • Para diff de countdown: new Date(expires_at).getTime() - Date.now().

Montos y moneda

  • Todos los campos numéricos de precio (total, subtotal_base, discount_amount, deposit_due, amount_paid, balance_due, extras_total, taxes_total) son números enteros en pesos (sin centavos).
  • currency: siempre ARS (ISO 4217). Artechia opera en pesos argentinos — igual leé el campo currency de la respuesta en lugar de hardcodear el símbolo.
  • Formato sugerido para mostrar al usuario:
    new Intl.NumberFormat("es-AR", {
      style: "currency", currency: "ARS",
      maximumFractionDigits: 0,
    }).format(81000)  // → "$ 81.000"

Strings

  • email: se normaliza a lowercase + trim en /booking/lookup. En los demás endpoints, mandalo tal cual.
  • guest.first_name / last_name: el server hace trim(). Largo máx 100.
  • country: ISO 3166-1 alpha-2 en mayúsculas (AR, UY, CL, BR, US, ES…).
  • booking_code: ART + fecha YYMMDD en el timezone de la propiedad + 4 hex, ej. ART260705A647. El prefijo no es configurable por el hotel. Tratalo como identificador opaco: no lo parsees ni valides su forma (las reservas importadas de canales externos usan otro prefijo). En el lookup es case-insensitive y se normaliza a mayúsculas server-side.
  • access_token: hex 64 chars (256 bits). Generado server-side; el cliente nunca lo inventa.

UUIDs y otros tipos

  • room_type.id, extra_id, rate_plan_id, booking_id, promotion.id: UUID v4 lowercase con guiones.
  • checkout_token: token firmado con HMAC, ~600–900 chars (crece con los extras y el cupón). Tratalo como opaco — no parsees el payload en el cliente y no lo pongas en una query string si podés evitarlo.
  • Booleans: true/false JSON estándar. No "true" ni 1.
  • Custom fields del checkout: el server acepta string, number o boolean por valor. Usar el tipo declarado en property.checkout.custom_fields[].type.

Locale de mensajes

Los error.message vienen en español. Si tu sitio es multi-idioma, no los muestres directo al usuario — mapeá error.code (estable y documentado) a tu propio catálogo de strings traducidas.

Códigos de error

Todos los errores tienen el formato { ok: false, error: { code, message, details? } }.details se incluye en errores de validación con un mapa de errores por campo (devuelto por zod). Única excepción: /checkout/lock-status usa { active, reason } y responde 200 incluso con el token inválido. (/checkout/status también, pero no es un alias suyo: devuelve active invertido cuando la reserva ya se creó. No lo uses.)

Mapeá por error.code, no por HTTP status. El mismo status cubre casos muy distintos (409 va de "sin disponibilidad" a "cupón agotado") y un code que no conozcas debe caer a un mensaje genérico + reintento manual, nunca a un crash.

HTTPCódigoCuándo ocurre / Qué hacer
400VALIDATIONParámetros faltantes o formato incorrecto. Revisar details. Incluye ttl_minutes fuera de 1–30 y, en /search y /availability, fechas invertidas.
400INVALID_BODYJSON malformado en el request.
400CONSENT_REQUIREDmarketing/subscribe: falta accept_marketing: true. Mandalo solo si la persona tildó la casilla de consentimiento.
401TOKEN_BAD_SIGNATUREFirma del checkout_token inválida (token manipulado).
401TOKEN_MALFORMED / TOKEN_INVALID_JSONcheckout_token ausente o con formato inválido.
404ORG_NOT_FOUNDOrganización no encontrada, inactiva o suspendida.
404PROPERTY_NOT_FOUNDPropiedad no encontrada.
404ROOM_TYPE_NOT_FOUNDTipo de habitación no encontrado.
404RATE_PLAN_NOT_FOUNDPlan tarifario no encontrado.
404BASE_RATE_NOT_FOUNDNo hay tarifa base configurada para ese rango.
404NOT_FOUNDBooking lookup: código y email no coinciden (timing-safe).
409NO_AVAILABILITYSin disponibilidad → volver al buscador. También en /checkout/confirm si el alojamiento no guarda la habitación durante el checkout (holds_inventory: false) y otra persona confirmó antes.
409LOCK_TIMEOUT/checkout/start: contención alta al adquirir el lock. Reintentar. ⚠️ En /checkout/confirm el MISMO código viene con status 503, no 409 — ramificá por code, no por status.
409LOCK_USEDToken ya fue usado (doble submit) → mostrar confirmación existente.
409LOCK_MISMATCHEl token no corresponde al lock activo.
409GUEST_BLACKLISTEDHuésped en lista negra de la propiedad.
409COUPON_LIMIT_REACHEDCupón alcanzó su límite global de usos.
409COUPON_USER_LIMIT_REACHEDEl huésped ya usó este cupón el máximo de veces.
409WRONG_METHODpayment-link: la reserva no se paga con MercadoPago.
409BOOKING_INACTIVEpayment-link: la reserva está cancelada o marcada no-show.
409ALREADY_PAIDpayment-link: la reserva no tiene saldo pendiente.
409NO_DEPOSIT_CONFIGpayment-link: pay_mode=deposit pero el plan no tiene seña configurada.
410TOKEN_EXPIREDcheckout_token expiró → reiniciar flujo.
410LOCK_EXPIREDLock de inventario expiró → reiniciar desde /start.
410LOCK_NOT_FOUNDEl lock no existe (expirado o nunca creado).
422VALIDATION_FAILEDEstadía mínima/máxima, stop_sell, fecha cerrada, o un custom field obligatorio vacío. Mostrar error.message.
422INVALID_DATEScheck-in en el pasado (hoy en huso argentino) o, en /quote y /checkout/*, check_out ≤ check_in.
422TERMS_NOT_ACCEPTEDconfirm: la propiedad tiene T&C y no se envió accept_terms: true.
422PAYMENT_METHOD_NOT_AVAILABLEconfirm: el método de pago elegido no está habilitado por la propiedad. Releer property.checkout.payment_methods.
422PROPERTY_CLOSEDstart/confirm: las fechas caen en un período cerrado del alojamiento. error.details trae start, end y reason. Chequealo antes con property.closed_periods.
422BOOKINGS_DISABLEDstart/confirm: el hotel no toma reservas online. Dos motivos, en error.details.reason: hotel_paused (apagó el interruptor; error.message trae SU explicación, mostrala tal cual) o no_payment_method (no configuró cómo cobrar). Chequealo antes con property.checkout.bookings_enabled y ni muestres el flujo.
422PLAN_LIMIT_EXCEEDEDconfirm: el hotel no puede tomar reservas online ahora (su cuenta en Artechia está sin plan o llegó al tope mensual). No se reintenta: mostrar los datos de contacto del hotel.
422INVALID_LOCK_KEYconfirm: el lock_key del token no tiene formato válido. No reintentar: reiniciar desde /checkout/start.
422MISSING_QUOTEconfirm: el lock se creó sin cotización guardada. Reiniciar desde /checkout/start.
422MISSING_GUEST_FIELDSconfirm: faltan nombre, apellido, email o documento del huésped. error.details dice cuáles.
422INVALID_PAYMENT_METHODconfirm: payment_method no es mercadopago, transferencia, manual ni card_manual.
422INVALID_QTYstart: la cantidad de unidades pedida no es válida (debe ser ≥ 1).
422INVALID_TARGETstart: el tipo de habitación pedido no existe o no pertenece a esa propiedad.
429RATE_LIMITEDDemasiadas requests. Usar header Retry-After + backoff.
502MP_API_ERRORpayment-link: error al crear preference en MercadoPago. Reintentar.
503MP_NOT_CONFIGUREDMercadoPago no configurado en esta propiedad.
503LOCK_TIMEOUT/checkout/confirm: contención al confirmar → reintentar.
500INTERNAL_ERRORError inesperado del servidor. Reportar a soporte.

Un cupón inválido NO devuelve error. No existe un código COUPON_INVALID: /quote y /checkout/start responden 200 cotizando sin el descuento y explican el rechazo en quote.coupon_error. Los únicos errores de cupón que cortan el flujo son 409 COUPON_LIMIT_REACHED y 409 COUPON_USER_LIMIT_REACHED, y aparecen recién en /checkout/confirm (donde se cuenta el canje real).

Ejemplo end-to-end (curl)

Flujo mínimo desde la búsqueda hasta el link de pago. Reemplazar $ORG, $PROP y $ROOM por los slugs de tu hotel.

# 1. Cargar config de la propiedad
curl https://app.artechia.com/api/public/property?org_slug=$ORG&property_slug=$PROP

# 2. Bloquear inventario y obtener checkout_token
TOKEN=$(curl -sX POST https://app.artechia.com/api/public/checkout/start \
  -H "Content-Type: application/json" \
  -d "{\"org_slug\":\"$ORG\",\"property_slug\":\"$PROP\",\
       \"room_type_slug\":\"$ROOM\",\"check_in\":\"2026-07-10\",\
       \"check_out\":\"2026-07-13\",\"adults\":2}" | jq -r .checkout_token)

# 3. Confirmar reserva
CONFIRM=$(curl -sX POST https://app.artechia.com/api/public/checkout/confirm \
  -H "Content-Type: application/json" \
  -d "{\"checkout_token\":\"$TOKEN\",\
       \"guest\":{\"first_name\":\"María\",\"last_name\":\"García\",\
                  \"email\":\"m@example.com\"},\
       \"payment_method\":\"mercadopago\",\"accept_terms\":true}")
CODE=$(echo $CONFIRM  | jq -r .booking_code)
ACC=$(echo  $CONFIRM  | jq -r .access_token)

# 4. Generar link de pago y redirigir
curl -sX POST https://app.artechia.com/api/public/checkout/payment-link \
  -H "Content-Type: application/json" \
  -d "{\"booking_code\":\"$CODE\",\"access_token\":\"$ACC\",\
       \"pay_mode\":\"deposit\"}" | jq .init_point

Soporte

¿Encontraste un bug, falta un campo en la respuesta, o necesitás un endpoint nuevo? Contactanos en contacto@artechia.com indicando el endpoint y un ejemplo del request.

API pública de Artechia · Motor de reservas para hoteles
API Docs — Artechia · ArtechIA