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.
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”.
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:
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.
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.
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:
/propertyy/room-type→public, max-age=60. El origen manda ademásstale-while-revalidate=300, pero el edge lo recorta: lo que llega al cliente espublic, 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:
| Endpoints | Qué exige |
|---|---|
| /property · /room-type · /availability · /search · /quote | Nada: es la información que ya mostrás en tu web. Solo rate limit. |
| /checkout/start | Nada — 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-preview | El checkout_token que devuelve /checkout/start, firmado con HMAC y con vencimiento. No se puede fabricar. |
| /checkout/payment-link · /review | El access_token de 64 caracteres que devuelve /checkout/confirm. |
| /booking/lookup | El 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
GET /api/public/propertyCargar 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
GET /api/public/availabilityPintar el calendario: disponible / pocas unidades / no disponible / oferta
- 3
POST /api/public/searchListar habitaciones disponibles con precio total, fotos, amenities y validación
- 4
GET /api/public/room-type(Opcional) Detalle estático de una habitación si no se viene desde /search
- 5
POST /api/public/quote(Opcional) Cotización detallada noche por noche + política de cancelación de una habitación
- 6
POST /api/public/checkout/startBloquear inventario (TTL configurable 1–30 min, default 15) → checkout_token
- 7
GET /api/public/checkout/extras(Opcional) Listar extras opcionales para la habitación bloqueada
- 8
PATCH /api/public/checkout/guest-preview(Opcional) Preview en vivo en el PMS mientras el huésped escribe
- 9
GET /api/public/checkout/lock-status(Opcional) Polling cada ~10 s para detectar expiración del lock o cancelación admin
- 10
POST /api/public/checkout/confirmEnviar 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
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
GET /api/public/booking/lookupConsultar estado, datos completos y a dónde pagar (payment): código + email, o código + access_token (el del manage_url)
- 13
POST /api/public/review(Opcional) Recibir rating y feedback post-estadía
/api/public/propertyConfiguració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.
Parámetros (query string)
Slug de la organización (recomendado, multi-tenant)
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)./api/public/availabilityDisponibilidad 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.
Parámetros (query string)
Slug de la organización
Slug de la propiedad
Fecha inicio (inclusiva)
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./api/public/searchBuscar habitaciones con precio
Devuelve todos los tipos de habitación con su cotización y sus unidades libres para el período. Una habitación es reservable si quote !== null, available_units > 0 y todos los campos de validation son favorables.
Parámetros (body JSON)
Slug de la organización
Slug de la propiedad
Fecha de entrada
Fecha de salida
Adultos (default: 2, máx: 20)
Niños (default: 0, máx: 20)
Ejemplo de request
curl -X POST https://app.artechia.com/api/public/search\
-H "Content-Type: application/json" \
-d '{
"org_slug": "hotel-costa",
"property_slug": "mar-azul",
"check_in": "2025-07-10",
"check_out": "2025-07-13",
"adults": 2
}'Respuesta 200
{
"ok": true,
"currency": "ARS",
"check_in": "2025-07-10",
"check_out": "2025-07-13",
"adults": 2,
"children": 0,
// Una sola frase para mostrar cuando NADA se puede reservar, ya escrita para
// un huésped: "Esta habitación no admite niños", "La estadía mínima en estas
// fechas es de 3 noches", "No quedan habitaciones libres para esas fechas".
// null si alguna habitación sí se puede reservar, o si los motivos difieren
// entre sí (ahí mostrá el de cada results[].error).
"notice": null,
"results": [
{
"room_type": {
"id": "uuid",
"name": "Suite Panorámica",
"slug": "suite-panoramica",
"description": "Vista al mar con jacuzzi.",
"tagline": "Lujo frente al mar",
"size_m2": 38,
"max_occupancy": 3,
"capacity_children": 1,
"photo_url": "https://...", // primera foto (thumbnail)
"photos": [ // galería completa — para slider/lightbox
{ "url": "https://foto1.jpg", "alt": "Vista principal",
"thumb_url": "https://foto1-thumb.webp" }, // ~640px, null en fotos viejas
{ "url": "https://foto2.jpg", "alt": "Baño", "thumb_url": null }
],
"amenities": ["WiFi", "Aire acondicionado", "TV", "Jacuzzi"]
},
"available_units": 3, // unidades LIBRES reales del tipo en ese rango
"quote": {
"nights_count": 3,
"totals": {
"subtotal_base": 90000, // suma de noches sin descuentos
"discount_amount": 9000, // descuento aplicado (promo + cupón)
"extras_total": 0, // extras obligatorios pre-incluidos
"taxes_total": 0, // impuestos si aplica
"tax_pct": 0,
"total": 81000, // total final a pagar
"deposit_pct": 50, // % de seña (0 si el plan no pide seña)
"deposit_due": 40500, // seña a pagar con MP
"currency": "ARS"
},
// ⚠️ OJO CON EL NOMBRE: acá el total se llama "total", pero la respuesta
// de /checkout/confirm y la de /booking/lookup lo llaman "grand_total".
// Es el mismo número. Leer el campo equivocado devuelve undefined y
// termina mostrando "$undefined" en pantalla.
"validation": {
"has_rate": true,
"stop_sell": false,
"closed": false, // fecha cerrada a la venta por el hotel
"min_stay_ok": true,
"max_stay_ok": true,
"occupancy_ok": true,
"min_stay": 1,
"max_stay": 30
},
"promotion": { "id": "uuid", "description": "10% off temporada alta", "discount_amount": 9000 } | null
},
"error": null
},
{
"room_type": { ... },
"quote": null,
"error": { "code": "BASE_RATE_NOT_FOUND", "message": "..." } // no cotizable
}
]
}
// ── REGLA PARA MOSTRAR "Reservar" ──────────────────────────────────────────
// const v = r.quote?.validation;
// const reservable = !!r.quote && r.available_units > 0 &&
// v.has_rate && !v.stop_sell && !v.closed &&
// v.min_stay_ok && v.max_stay_ok && v.occupancy_ok;
//
// available_units es la disponibilidad REAL (reservas + holds + cupo manual +
// unidades en mantenimiento) del tipo en ese rango. "validation" NO la mira:
// una habitación agotada puede tener validation impecable. Sin este chequeo
// mostrarías como reservable algo que revienta con 409 NO_AVAILABILITY recién
// en /checkout/start, después de que el huésped completó todo el formulario.
// Con available_units === 1 podés mostrar "¡Última disponible!".
// NOTA: /search devuelve un quote REDUCIDO (sin policy / nights / coupon /
// coupon_error / extras). Para la política de cancelación o el desglose noche
// por noche, usá /quote.
//
// /search NUNCA falla por una habitación: devuelve TODOS los tipos activos y
// pone el motivo en results[].error (quote: null). Los códigos posibles ahí son
// los mismos del loader/pricing: BASE_RATE_NOT_FOUND, RATE_PLAN_NOT_FOUND,
// VALIDATION_FAILED, INVALID_DATES. El HTTP sigue siendo 200.
//
// results[].error.message ya viene escrito para el huésped y dice QUÉ cambiar:
// "Esta habitación no admite niños", "Esta habitación admite hasta 2 adultos",
// "La estadía mínima en estas fechas es de 3 noches. Elegiste 2 noches".
// Mostralo tal cual en la tarjeta en vez de un "no disponible" genérico: es la
// diferencia entre que la persona corrija la búsqueda o se vaya del sitio.
//
// check_out <= check_in → 400 VALIDATION (acá, a diferencia de /quote, que
// devuelve 422 INVALID_DATES). check_in en el pasado → 422 INVALID_DATES./api/public/room-typeDetalle 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.
Parámetros (query string)
Slug de la organización
Slug de la propiedad
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"]
}
}/api/public/quoteCotizar habitación específica
Desglose detallado noche por noche. Útil para mostrar el breakdown de precio antes del checkout.
Parámetros (body JSON)
Slug de la organización
Slug de la propiedad
Slug del tipo de habitación
Fecha de entrada
Fecha de salida
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.
Adultos (default: 2)
Niños (default: 0)
Extras a incluir: [{ extra_id: uuid, qty: number }]
Código de descuento
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./api/public/checkout/startIniciar 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.
Parámetros (body JSON)
Slug de la organización
Slug de la propiedad
Slug del tipo de habitación
Fecha de entrada
Fecha de salida
Adultos (default: 2, máx: 20)
Niños (default: 0, máx: 20)
TTL del lock (default: 15, máx: 30). Tiempo que se bloquea el inventario. Fuera de rango → 400 VALIDATION.
NO usar desde un sitio público: el plan tarifario es único y lo resuelve el server (por fechas/prioridad). Omitir siempre.
Extras seleccionados: [{ extra_id: uuid, qty: number }]
Código de descuento
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./api/public/checkout/extrasListar 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á.
Parámetros (query string)
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/api/public/checkout/guest-previewPreview 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.
Parámetros (body JSON)
Token de /checkout/start
Nombre parcial
Apellido parcial
Email parcial
Teléfono parcial
Documento parcial
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./api/public/checkout/lock-statusPolling 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.
Parámetros (query string)
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./api/public/checkout/confirmConfirmar 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.
Parámetros (body JSON)
Token de /checkout/start
Nombre
Apellido
Email (recibe confirmación)
Teléfono
Tipo doc (ej: 'DNI', 'Pasaporte')
Número de documento (DNI/pasaporte) — obligatorio
País (ej: AR, UY, CL, BR)
Ciudad
Dirección
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.
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 seleccionados: [{ extra_id: uuid, qty: number }]
Código de descuento (si no se envió en /start)
Pedidos especiales (si special_requests_enabled)
{ [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.
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./api/public/checkout/payment-linkGenerar link de pago MercadoPago (opcional/fallback)
En el flujo normal NO es necesario: /checkout/confirm ya devuelve payment_link.init_point inline. Usar este endpoint solo si confirm devolvió payment_link: null (MP falló) o para cobrar el saldo pendiente (pay_mode=balance). Crea (o reutiliza) una preference de MercadoPago. Si el booking ya tiene preference cacheada con mismo pay_mode + amount, no re-llama a MP.
Parámetros (body JSON)
Código de la reserva
Token de /checkout/confirm
Qué cobrar: seña / monto total / saldo pendiente. Si lo omitís el server elige solo: 'balance' si ya hay pagos, 'deposit' si el plan tiene seña, si no 'total'. Omitirlo es lo recomendado.
Ejemplo de request
curl -X POST https://app.artechia.com/api/public/checkout/payment-link\
-H "Content-Type: application/json" \
-d '{
"booking_code": "ART260705A647",
"access_token": "<64-char-hex-token>",
"pay_mode": "deposit"
}'Respuesta 200
// La respuesta tiene SIEMPRE la misma forma (cambie o no reused):
{
"ok": true,
"init_point": "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=...",
"preference_id": "1234567-abc",
"amount": 40500,
"pay_mode": "deposit",
"currency": "ARS",
"reused": false // true = se reusó la preference cacheada del booking
}
// reused: true significa que ya existía una preference con el MISMO pay_mode y
// el MISMO amount, así que no se volvió a llamar a MercadoPago. El link es
// igual de válido. No cambia nada de tu lado.
//
// Redirigir al usuario: window.location.href = init_point
// El webhook de MP confirma la reserva automáticamente cuando se paga.
//
// Una reserva eliminada por el hotel devuelve 404 NOT_FOUND (no se puede
// seguir cobrando algo que el hotel dio de baja)./api/public/booking/lookupConsultar 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.
Parámetros (query string)
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 del huésped (case-insensitive). Obligatorio si no mandás access_token.
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ó./api/public/reviewEnviar 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).
Parámetros (body JSON)
booking_code de la reserva
access_token devuelto por /checkout/confirm
Puntuación entera de 1 a 5
Comentario libre del huésped
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/api/public/marketing/subscribeSuscribir 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.
Parámetros (body JSON)
Slug de la propiedad
Slug de la organización
Email de la persona
Nombre
Apellido
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_LIMITEDReservas 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.
/api/public/group/quoteCotizar 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.
Parámetros (body JSON)
Slug de la propiedad
Slug de la organización
Entrada (la misma para todas las unidades)
Salida (exclusiva)
[{ 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.
Cupón para todo el grupo
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/api/public/group/checkout/startBloquear 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).
Parámetros (body JSON)
Los mismos campos que /group/quote
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.
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/api/public/group/checkout/confirmConfirmar 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.
Parámetros (body JSON)
El de /group/checkout/start
{ first_name, last_name, email, document_number, phone?, document_type?, country?, city?, address? } — titular del grupo
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).
Datos de la tarjeta si payment_method = card_manual (mismo formato que /checkout/confirm)
Total que el huésped vio y aceptó. Mandalo siempre: si difiere del recalculado, 409 PRICE_CHANGED.
Cambiar el cupón (se recotiza; si el total cambia → PRICE_CHANGED)
Obligatorio si el alojamiento tiene términos configurados
Campos propios del alojamiento (los obligatorios se validan)
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/api/public/group/checkout/releaseSoltar 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.
Parámetros (body JSON)
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/api/public/groupResumen 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.
Parámetros (query string)
GRP-XXXXXXXXXX
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: stringsYYYY-MM-DD(sin hora).- Se interpretan en el timezone de la propiedad (campo
property.timezone, ejAmerica/Argentina/Buenos_Aires) — NO en UTC. check_outes exclusiva: una reserva del 10 al 13 julio = 3 noches (10→11, 11→12, 12→13). El huésped se va el 13.availability.totambién exclusiva. Para julio completo:from=2025-07-01to=2025-08-01.- Si tu sitio sirve a múltiples zonas, normalizá fechas en el cliente usando el
timezoneque devuelve/property.
Timestamps ISO 8601
expires_at,created_at,cancelled_at: ISO 8601 en UTC, pero el sufijo varía:created_atviene como2025-07-01T10:00:00.000Zyexpires_atcomo2026-07-24T23:45:19.605738+00:00. Parsealos siempre connew Date(...); no cortes el string ni asumas laZ.- 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: siempreARS(ISO 4217). Artechia opera en pesos argentinos — igual leé el campocurrencyde 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 hacetrim(). Largo máx 100.country: ISO 3166-1 alpha-2 en mayúsculas (AR,UY,CL,BR,US,ES…).booking_code:ART+ fechaYYMMDDen 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/falseJSON estándar. No"true"ni1. - Custom fields del checkout: el server acepta
string,numberobooleanpor valor. Usar el tipo declarado enproperty.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.
| HTTP | Código | Cuándo ocurre / Qué hacer |
|---|---|---|
| 400 | VALIDATION | Parámetros faltantes o formato incorrecto. Revisar details. Incluye ttl_minutes fuera de 1–30 y, en /search y /availability, fechas invertidas. |
| 400 | INVALID_BODY | JSON malformado en el request. |
| 400 | CONSENT_REQUIRED | marketing/subscribe: falta accept_marketing: true. Mandalo solo si la persona tildó la casilla de consentimiento. |
| 401 | TOKEN_BAD_SIGNATURE | Firma del checkout_token inválida (token manipulado). |
| 401 | TOKEN_MALFORMED / TOKEN_INVALID_JSON | checkout_token ausente o con formato inválido. |
| 404 | ORG_NOT_FOUND | Organización no encontrada, inactiva o suspendida. |
| 404 | PROPERTY_NOT_FOUND | Propiedad no encontrada. |
| 404 | ROOM_TYPE_NOT_FOUND | Tipo de habitación no encontrado. |
| 404 | RATE_PLAN_NOT_FOUND | Plan tarifario no encontrado. |
| 404 | BASE_RATE_NOT_FOUND | No hay tarifa base configurada para ese rango. |
| 404 | NOT_FOUND | Booking lookup: código y email no coinciden (timing-safe). |
| 409 | NO_AVAILABILITY | Sin 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. |
| 409 | LOCK_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. |
| 409 | LOCK_USED | Token ya fue usado (doble submit) → mostrar confirmación existente. |
| 409 | LOCK_MISMATCH | El token no corresponde al lock activo. |
| 409 | GUEST_BLACKLISTED | Huésped en lista negra de la propiedad. |
| 409 | COUPON_LIMIT_REACHED | Cupón alcanzó su límite global de usos. |
| 409 | COUPON_USER_LIMIT_REACHED | El huésped ya usó este cupón el máximo de veces. |
| 409 | WRONG_METHOD | payment-link: la reserva no se paga con MercadoPago. |
| 409 | BOOKING_INACTIVE | payment-link: la reserva está cancelada o marcada no-show. |
| 409 | ALREADY_PAID | payment-link: la reserva no tiene saldo pendiente. |
| 409 | NO_DEPOSIT_CONFIG | payment-link: pay_mode=deposit pero el plan no tiene seña configurada. |
| 410 | TOKEN_EXPIRED | checkout_token expiró → reiniciar flujo. |
| 410 | LOCK_EXPIRED | Lock de inventario expiró → reiniciar desde /start. |
| 410 | LOCK_NOT_FOUND | El lock no existe (expirado o nunca creado). |
| 422 | VALIDATION_FAILED | Estadía mínima/máxima, stop_sell, fecha cerrada, o un custom field obligatorio vacío. Mostrar error.message. |
| 422 | INVALID_DATES | check-in en el pasado (hoy en huso argentino) o, en /quote y /checkout/*, check_out ≤ check_in. |
| 422 | TERMS_NOT_ACCEPTED | confirm: la propiedad tiene T&C y no se envió accept_terms: true. |
| 422 | PAYMENT_METHOD_NOT_AVAILABLE | confirm: el método de pago elegido no está habilitado por la propiedad. Releer property.checkout.payment_methods. |
| 422 | PROPERTY_CLOSED | start/confirm: las fechas caen en un período cerrado del alojamiento. error.details trae start, end y reason. Chequealo antes con property.closed_periods. |
| 422 | BOOKINGS_DISABLED | start/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. |
| 422 | PLAN_LIMIT_EXCEEDED | confirm: 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. |
| 422 | INVALID_LOCK_KEY | confirm: el lock_key del token no tiene formato válido. No reintentar: reiniciar desde /checkout/start. |
| 422 | MISSING_QUOTE | confirm: el lock se creó sin cotización guardada. Reiniciar desde /checkout/start. |
| 422 | MISSING_GUEST_FIELDS | confirm: faltan nombre, apellido, email o documento del huésped. error.details dice cuáles. |
| 422 | INVALID_PAYMENT_METHOD | confirm: payment_method no es mercadopago, transferencia, manual ni card_manual. |
| 422 | INVALID_QTY | start: la cantidad de unidades pedida no es válida (debe ser ≥ 1). |
| 422 | INVALID_TARGET | start: el tipo de habitación pedido no existe o no pertenece a esa propiedad. |
| 429 | RATE_LIMITED | Demasiadas requests. Usar header Retry-After + backoff. |
| 502 | MP_API_ERROR | payment-link: error al crear preference en MercadoPago. Reintentar. |
| 503 | MP_NOT_CONFIGURED | MercadoPago no configurado en esta propiedad. |
| 503 | LOCK_TIMEOUT | /checkout/confirm: contención al confirmar → reintentar. |
| 500 | INTERNAL_ERROR | Error 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_pointSoporte
¿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.
