{"openapi":"3.1.0","info":{"title":"Parceros Restaurant Reservations API","version":"1.0.0","summary":"Consultar horas reservables y apartar una mesa, sin llave y sin cuenta.","description":"Public API for checking accepted reservation times and creating confirmed reservations at Parceros, a Colombian restaurant in Mérida, Yucatán, Mexico. Timezone: America/Merida. The availability operation returns times the restaurant accepts; it does not claim that a physical table is free because no capacity system exists.\n\nLO QUE NO SE PUEDE HACER AQUÍ, para no intentarlo:\n- No se puede mandar un pedido de comida (hay dinero, una dirección y un repartidor detrás, y un pedido que nadie miró llega a una puerta equivocada). En su lugar: usar POST https://parceros.mx/api/pedido/borrador si aparece en este contrato, o armarlo en https://parceros.mx/menu y confirmarlo ahí.\n- No se puede consultar ni modificar una reserva ya hecha (haría falta demostrar quién la hizo, y este contrato no pide identidad a nadie). En su lugar: escribir por WhatsApp a 999 251 8262 con el folio.\n- No se puede saber si hay mesas libres (el restaurante no lleva aforo: no existe ese dato, ni para nosotros). En su lugar: reservar la hora, que es lo que sí queda registrado.\n- No hay datos de clientes, ni historial, ni el panel del turno (son datos personales y no salen de aquí por ninguna puerta). En su lugar: ninguna.\n\nDocumentación legible: https://parceros.mx/api","contact":{"name":"Parceros","url":"https://parceros.mx/api","email":"contacto@parceros.mx"}},"servers":[{"url":"https://parceros.mx"}],"externalDocs":{"url":"https://parceros.mx/api","description":"Guía breve para usar esta API"},"paths":{"/api/info":{"get":{"operationId":"info","summary":"Horarios, si está abierto ahora, envío y preguntas frecuentes","description":"Todo lo que hace falta para contestar una duda sin abrir el sitio. IMPORTANTE: «abierto» y «cocina_abierta» son dos cosas distintas — la cocina cierra una hora antes que el local (media hora el domingo), así que hay una franja en la que el local está abierto y ya no se sirve comida. Si alguien pregunta si puede ir a cenar, la respuesta es «cocina_abierta».","parameters":[{"name":"lang","in":"query","required":false,"description":"Idioma del FAQ y de las etiquetas. Por defecto español.","schema":{"type":"string","description":"Idioma del FAQ y de las etiquetas. Por defecto español.","enum":["es","en"],"examples":["es"]}}],"responses":{"200":{"description":"Datos del negocio, estado ahora mismo, tarifa de envío y el FAQ entero.","content":{"application/json":{"schema":{"type":"object","properties":{"nombre":{"type":"string"},"ahora":{"type":"object","properties":{"dia":{"type":"string"},"hora":{"type":"string"},"abierto":{"type":"boolean"},"cocina_abierta":{"type":"boolean"},"cocina_cierra":{"type":"string"},"valet":{"type":"boolean"}},"required":["dia","hora","abierto","cocina_abierta","cocina_cierra","valet"]},"envio":{"type":"object","properties":{"base_km":{"type":"number"},"base_costo":{"type":"number"},"costo_km_extra":{"type":"number"},"radio_maximo_km":{"type":"number"}},"required":["base_km","base_costo","costo_km_extra","radio_maximo_km"]},"calificacion":{"type":"object","properties":{"puntaje":{"type":"number"},"total":{"type":"number"},"fuente":{"type":"string"}},"required":["puntaje","total","fuente"]}},"required":["nombre","ahora","envio","calificacion"]},"example":{"nombre":"Parceros","ahora":{"dia":"2026-08-21","hora":"22:30","abierto":true,"cocina_abierta":false,"cocina_cierra":"22:00","valet":true},"envio":{"base_km":4,"base_costo":45,"costo_km_extra":10,"radio_maximo_km":20},"calificacion":{"puntaje":4.8,"total":3600,"fuente":"Google"}}}}}}}},"/api/menu":{"get":{"operationId":"menu","summary":"La carta entera con precios","description":"Las siete secciones, sus platillos y las adiciones, con el id de cada línea pedible. Los precios llevan IVA incluido, que es como se cobran. Los nombres de platillo no se traducen —«Bandeja Paisa» es como se pide en la mesa y como se busca— así que «lang» solo cambia la descripción.","parameters":[{"name":"lang","in":"query","required":false,"description":"Idioma de las descripciones. Por defecto español.","schema":{"type":"string","description":"Idioma de las descripciones. Por defecto español.","enum":["es","en"],"examples":["es"]}}],"responses":{"200":{"description":"La carta completa, con ids que sirven para armar un pedido.","content":{"application/json":{"schema":{"type":"object","properties":{"restaurante":{"type":"string"},"moneda":{"type":"string"},"iva_incluido":{"type":"boolean"},"total_platillos":{"type":"number"},"secciones":{"type":"array","items":{"type":"object","properties":{"nombre":{"type":"string"},"platillos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"nombre":{"type":"string"},"presentaciones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"etiqueta":{"type":"null"},"precio":{"type":"number"}},"required":["id","etiqueta","precio"]}},"favorito":{"type":"boolean"},"destacado":{"type":"boolean"}},"required":["id","nombre","presentaciones","favorito","destacado"]}}},"required":["nombre","platillos"]}},"adiciones":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"nombre":{"type":"string"},"precio":{"type":"number"}},"required":["id","nombre","precio"]}}},"required":["restaurante","moneda","iva_incluido","total_platillos","secciones","adiciones"]},"example":{"restaurante":"Parceros","moneda":"MXN","iva_incluido":true,"total_platillos":71,"secciones":[{"nombre":"Entradas","platillos":[{"id":"p-entradas-hero-chicharron-trancao","nombre":"Chicharrón Trancao","presentaciones":[{"id":"p-entradas-hero-chicharron-trancao","etiqueta":null,"precio":330}],"favorito":false,"destacado":true}]}],"adiciones":[{"id":"a-arepa-de-queso","nombre":"Arepa de queso","precio":60}]}}}}}}},"/api/disponibilidad":{"get":{"operationId":"getAvailableReservationTimes","summary":"Las horas a las que se puede reservar","description":"Devuelve, para cada día consultado, las horas a las que este restaurante ACEPTA una reserva. No es un mapa de mesas libres: el negocio no lleva aforo, así que una hora que aparece aquí se puede reservar, y eso es todo lo que significa. Las horas salen del horario de COCINA (que cierra antes que el local) menos un margen para llegar, y van de 30 en 30 minutos. Esta operación solo consulta: NO crea ni modifica una reserva. Llama a esto antes de reservar: el campo `hora` de POST /api/reserva se rechaza si no es una de estas.","parameters":[{"name":"dia","in":"query","required":false,"description":"Un día concreto, AAAA-MM-DD, en el huso del restaurante. Si se omite se devuelven los próximos días.","schema":{"type":"string","description":"Un día concreto, AAAA-MM-DD, en el huso del restaurante. Si se omite se devuelven los próximos días.","pattern":"^\\d{4}-\\d{2}-\\d{2}$","examples":["2026-08-23"]}},{"name":"dias","in":"query","required":false,"description":"Cuántos días devolver a partir de hoy. Por defecto 7, máximo 90. Se ignora si se manda \"dia\".","schema":{"type":"integer","description":"Cuántos días devolver a partir de hoy. Por defecto 7, máximo 90. Se ignora si se manda \"dia\".","minimum":1,"maximum":90,"examples":[7]}}],"responses":{"200":{"description":"El huso, el instante en que se calculó, si las reservas están activas y un objeto por día con sus horas reservables.","content":{"application/json":{"schema":{"type":"object","properties":{"zona":{"type":"string"},"calculado":{"type":"string"},"reservas_activas":{"type":"boolean"},"paso_minutos":{"type":"number"},"margen_minutos":{"type":"number"},"dias":{"type":"array","items":{"type":"object","properties":{"dia":{"type":"string"},"abre":{"type":"boolean"},"local":{"type":"object","properties":{"abre":{"type":"string"},"cierra":{"type":"string"}},"required":["abre","cierra"]},"cocina_cierra":{"type":"string"},"horas_reservables":{"type":"array","items":{"type":"string"}}},"required":["dia","abre","local","cocina_cierra","horas_reservables"]}}},"required":["zona","calculado","reservas_activas","paso_minutos","margen_minutos","dias"]},"example":{"zona":"America/Merida","calculado":"2026-08-21T19:12","reservas_activas":true,"paso_minutos":30,"margen_minutos":45,"dias":[{"dia":"2026-08-21","abre":true,"local":{"abre":"13:00","cierra":"23:00"},"cocina_cierra":"22:00","horas_reservables":["20:00","20:30","21:00","21:30"]}]}}}},"400":{"description":"«dia»: «dia» no tiene forma AAAA-MM-DD · «dias»: «dias» no es un entero entre 1 y 90 · «rango»: el día pedido es anterior a hoy o va más allá de 90 días","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}}}}},"/api/envio":{"get":{"operationId":"envio","summary":"Cuánto cuesta el envío a un punto, y cuánto tarda","description":"Traza la ruta real por calles y devuelve el precio firme del envío más el tiempo total —cocina incluida—. El precio que sale de aquí ES EL QUE SE COBRA: no es una estimación y no lleva asterisco. Si no se puede trazar la ruta, «cotizado» es false y no hay número: no se inventa uno. Está limitado por IP porque cada consulta cuesta dinero.","parameters":[{"name":"lat","in":"query","required":true,"description":"Latitud del destino, en grados decimales.","schema":{"type":"string","description":"Latitud del destino, en grados decimales.","examples":[20.9721]}},{"name":"lng","in":"query","required":true,"description":"Longitud del destino, en grados decimales.","schema":{"type":"string","description":"Longitud del destino, en grados decimales.","examples":[-89.6215]}}],"responses":{"200":{"description":"Kilómetros de recorrido, costo firme y la ventana de entrega.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"cubierto":{"type":"boolean"},"cotizado":{"type":"boolean"},"km":{"type":"number"},"costo":{"type":"number"},"moneda":{"type":"string"},"iva_incluido":{"type":"boolean"},"entrega_min":{"type":"number"},"entrega_max":{"type":"number"}},"required":["ok","cubierto","cotizado","km","costo","moneda","iva_incluido","entrega_min","entrega_max"]},"example":{"ok":true,"cubierto":true,"cotizado":true,"km":6.4,"costo":75,"moneda":"MXN","iva_incluido":true,"entrega_min":40,"entrega_max":45}}}},"400":{"description":"«coordenadas»: «lat» o «lng» no son números válidos","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}},"429":{"description":"«demasiadas»: se pasó el límite por IP. La respuesta trae «Retry-After» con los segundos que hay que esperar","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}}}}},"/api/contacto":{"get":{"operationId":"contacto","summary":"Abrir WhatsApp con un mensaje listo","description":"Devuelve un enlace de WhatsApp al restaurante con un mensaje inicial ya escrito. Sirve para la duda que no cabe en los campos de una reserva o de un pedido. No manda el mensaje: la persona lo ve, lo puede completar y decide si lo envía.","parameters":[{"name":"motivo","in":"query","required":false,"description":"El contexto del mensaje. Por defecto «duda».","schema":{"type":"string","description":"El contexto del mensaje. Por defecto «duda».","enum":["duda","pedido","reserva"],"examples":["pedido"]}}],"responses":{"200":{"description":"Un enlace de WhatsApp con el mensaje inicial y el teléfono del restaurante.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"whatsapp":{"type":"string"}},"required":["ok","whatsapp"]},"example":{"ok":true,"whatsapp":"https://wa.me/529991234567?text=..."}}}}}}},"/api/pedido/borrador":{"post":{"operationId":"pedidoBorrador","summary":"Armar un pedido para que la persona lo confirme","description":"Resuelve los ids contra la carta vigente, cotiza el envío por ruta real y devuelve un enlace efímero al carrito ya armado. NO crea un pedido, NO escribe en la base y NO manda WhatsApp: la persona abre el enlace, revisa el total y confirma el mensaje ella misma.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lineas":{"type":"array","description":"Platillos de GET /api/menu: id y cantidad. Máximo 60 líneas y 30 piezas por línea.","items":{"type":"object","properties":{"id":{"type":"string","description":"Id exacto de la carta.","examples":["p-entradas-hero-chicharron-trancao"]},"cantidad":{"type":"integer","description":"Piezas de esa línea.","minimum":1,"maximum":30,"examples":[1]}},"required":["id","cantidad"]},"examples":[[{"id":"p-entradas-hero-chicharron-trancao","cantidad":1}]]},"entrega":{"type":"object","description":"Cómo se entrega. Para domicilio requiere dirección y coordenadas del destino.","properties":{"modo":{"type":"string","description":"«domicilio» o «recoger».","enum":["domicilio","recoger"],"examples":["domicilio"]},"direccion":{"type":"string","description":"Dirección que verá la persona antes de confirmar.","maxLength":300,"examples":["Calle 60 498, Centro"]},"referencias":{"type":"string","description":"Indicaciones para encontrar la puerta.","maxLength":300,"examples":["Portón negro"]},"lat":{"type":"number","description":"Latitud del destino para cotizar domicilio.","examples":[20.9721]},"lng":{"type":"number","description":"Longitud del destino para cotizar domicilio.","examples":[-89.6215]}},"required":["modo"],"examples":[{"modo":"domicilio","direccion":"Calle 60 498, Centro","lat":20.9721,"lng":-89.6215}]}},"required":["lineas","entrega"]}}}},"responses":{"200":{"description":"El enlace efímero que la persona abre para revisar y confirmar el pedido.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"confirmar_en":{"type":"string"},"expira_en_minutos":{"type":"number"}},"required":["ok","confirmar_en","expira_en_minutos"]},"example":{"ok":true,"confirmar_en":"https://parceros.mx/api/pedido/borrador/…","expira_en_minutos":15}}}},"400":{"description":"«forma»: faltan líneas, entrega o coordenadas válidas","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}},"422":{"description":"«lineas»: algún id ya no existe en la carta o la cantidad no vale · «fuera_de_cobertura»: el punto queda fuera del radio; ofrece recoger","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}},"429":{"description":"«demasiadas»: se pasó el límite por IP; respeta Retry-After","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}},"503":{"description":"«sin_ruta»: no se pudo cotizar una ruta firme; ofrece WhatsApp","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}}}}},"/api/reserva":{"post":{"operationId":"createRestaurantReservation","summary":"Apartar una mesa","description":"Escribe la reserva en el sistema del restaurante y devuelve un folio. Si la respuesta es 200, la mesa QUEDA APARTADA: no es una solicitud ni queda pendiente de confirmación, y una persona del equipo la ve en el acto. Por eso no se llama con datos inventados — el nombre y el teléfono tienen que ser los de quien va a venir, porque es como se le avisa si algo cambia. No hace falta cuenta, ni llave, ni token.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dia":{"type":"string","description":"AAAA-MM-DD, en el huso del restaurante. Desde hoy y hasta 90 días.","pattern":"^\\d{4}-\\d{2}-\\d{2}$","examples":["2026-08-23"]},"hora":{"type":"string","description":"HH:MM. Tiene que ser una de las que devuelve GET /api/disponibilidad para ese día; cualquier otra se rechaza.","pattern":"^\\d{2}:\\d{2}$","examples":["20:30"]},"personas":{"type":"integer","description":"Cuántos van a comer. De 1 a 40. No hay barrera para un grupo grande.","minimum":1,"maximum":40,"examples":[4]},"nombre":{"type":"string","description":"A nombre de quién queda la mesa. De 2 a 80 caracteres.","maxLength":80,"examples":["Ana Restrepo"]},"telefono":{"type":"string","description":"En formato internacional E.164, con «+» y código de país. IMPORTANTE: diez dígitos sin prefijo se interpretan como mexicanos, así que un número de Colombia o de Estados Unidos mandado sin «+» queda guardado como mexicano y el restaurante no puede avisar.","examples":["+529991234567"]},"ocasion":{"type":"string","description":"«cumpleanos» si se celebra un cumpleaños —el equipo prepara la mesa— o «general». Por defecto «general».","enum":["general","cumpleanos"],"examples":["general"]},"comentarios":{"type":"string","description":"Peticiones que el salón tiene que preparar: alergias, silla de bebé, terraza, algo que celebrar. Lo lee una persona. Hasta 500 caracteres.","maxLength":500,"examples":["Uno del grupo es celíaco"]},"landing":{"type":"string","description":"De dónde viene la reserva. Un agente manda aquí su nombre —«chatgpt», «claude», «gemini»— para que el equipo sepa que la escribió un asistente y no una persona en el sitio.","examples":["agente:chatgpt"]}},"required":["dia","hora","personas","nombre","telefono"]}}}},"responses":{"200":{"description":"La reserva confirmada: folio para identificarla y los datos que el agente envió. Un 200 significa que ya existe en el sistema del restaurante.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"folio":{"type":"string"},"status":{"type":"string"},"reservation":{"type":"object","properties":{"date":{"type":"string"},"time":{"type":"string"},"party_size":{"type":"number"}},"required":["date","time","party_size"]}},"required":["ok","folio","status","reservation"]},"example":{"ok":true,"folio":"PR-4K7M9X","status":"confirmed","reservation":{"date":"2026-08-23","time":"20:30","party_size":4}}}}},"400":{"description":"«json»: el cuerpo no es JSON","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}},"413":{"description":"«grande»: el cuerpo pasa de 8 KB","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}},"422":{"description":"«datos»: algún campo no vale. La respuesta trae «campo» con cuál es, para poder corregir ese y solo ese","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}},"502":{"description":"«base»: el sistema del restaurante no contestó. LA MESA NO QUEDÓ APARTADA: hay que decírselo a la persona y ofrecerle el WhatsApp","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}},"503":{"description":"«apagado»: las reservas están desactivadas","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"motivo":{"type":"string"},"error":{"type":"string","description":"Stable machine-readable error code."},"campo":{"type":"string"},"detalle":{"type":"string"}},"required":["ok","motivo"]}}}}}}}}}