Documentación
Empezar
La API de yaReserva para quien integra un sistema con un hotel: cómo se firma cada petición, cómo se evita cobrar dos veces y qué devuelve cuando algo sale mal.
Qué es esta API
yaReserva es el sistema con el que un hotel opera y vende. Esta API es la puerta para que otro programa haga una parte de ese trabajo: consultar qué hay libre y reservar desde el sitio web del propio hotel, cargar la cena del restaurante a la habitación, o emitir la llave digital al hacer check-in.
No es una API pública de catálogo: cada credencial pertenece a un hotel y a una propiedad suya, y solo ve lo de esa propiedad. El hotel la crea desde su panel y la puede apagar cuando quiera.
| Integración | Para quién | Quién llama a quién |
|---|---|---|
| Motor de reservas propio | El hotel que ya tiene su buscador en su web | Tu servidor llama a yaReserva |
| POS de restaurante | Soft Restaurant, Parrot, Loyverse u otro punto de venta | El POS llama a yaReserva |
| Cerraduras | Salto, TTLock, Dormakaba, Assa Abloy y el puente del hotel | yaReserva llama a tu puente |
Dato: Todo lo de esta página aplica a las tres
Cambia el prefijo de los headers y el de la llave, no la mecánica. Lee esta página una vez y después ve directo al apartado de tu integración.
Ambientes
Hay dos ambientes y son dos mundos separados. Las credenciales de uno no sirven en el otro: una llave de pruebas en producción responde 401, y al revés también. El hotel pide las de prueba en su panel y con ellas recibe la URL base contra la que apuntar.
| Ambiente | URL base | Qué pasa ahí |
|---|---|---|
| Pruebas | La URL base llega con tus credenciales de prueba | Datos de prueba. Ningún cobro real, ninguna cerradura real. Es donde se construye. |
| Producción | https://api.yareserva.com | Huéspedes reales y dinero real. El hotel la habilita cuando su integración ya funciona en pruebas. |
Guarda la URL base en tu configuración, nunca en el código. Pasar de pruebas a producción tiene que ser cambiar dos valores: la URL y el par de credenciales.
Autenticación por firma
No hay tokens ni sesiones. Cada petición se firma por separado con el secreto de la conexión, y el secreto nunca viaja: lo que viaja es la firma, que es lo único que se puede calcular teniéndolo.
Cada integración tiene su terna de headers. Cambia el nombre, no la forma: llave pública, hora y firma.
| Integración | Prefijo de la llave | Headers |
|---|---|---|
| Motor de reservas | yreng_ | X-YR-ENG-Key, X-YR-ENG-Timestamp, X-YR-ENG-Signature |
| POS de restaurante | yrpos_ | X-YR-POS-Key, X-YR-POS-Timestamp, X-YR-POS-Signature |
| Cerraduras | yrlock_ | X-YR-Lock-Key, X-YR-Lock-Timestamp, X-YR-Lock-Signature |
- Serializa el cuerpo a JSON y quédate con esos bytes exactos. A partir de aquí no lo vuelvas a formatear: un espacio de más cambia la firma.
- Toma la hora actual en segundos Unix, en UTC. Ese número es el timestamp.
- Arma la cadena a firmar concatenando el timestamp, un punto y el cuerpo: «timestamp + "." + cuerpo». En un GET el cuerpo es vacío, así que la cadena termina en el punto.
- Calcula HMAC-SHA256 de esa cadena usando el secreto como llave, y pásalo a hexadecimal en minúsculas.
- Manda los tres headers: la llave pública, el timestamp que usaste y la firma con el prefijo «sha256=».
X-YR-ENG-Key: yreng_3f9a1c0b7d2e4a6f8b0c1d2e
X-YR-ENG-Timestamp: 1789459391
X-YR-ENG-Signature: sha256=9c1f0a7d4b2e6c8a1f3d5b7e9c0a2d4f6b8e0c2a4d6f8b0c2e4a6d8f0b2c4e6aKEY=yreng_3f9a1c0b7d2e4a6f8b0c1d2e
SECRET=yreng_sk_...
BODY='{"offers":["eyJ2IjoxLCJyIjoi..."],"guest":{"firstName":"Ana","lastName":"Ruiz","email":"ana@correo.mx"},"acceptedTerms":true,"acceptedPrivacy":true}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://api.yareserva.com/api/v1/engine/bookings \
-H "Content-Type: application/json" \
-H "X-YR-ENG-Key: $KEY" \
-H "X-YR-ENG-Timestamp: $TS" \
-H "X-YR-ENG-Signature: sha256=$SIG" \
-H "X-Idempotency-Key: 8c2e1f04-5a6b-4c7d-8e9f-0a1b2c3d4e5f" \
--data-binary "$BODY"const crypto = require('crypto');
function firmar(payload, secret) {
const body = JSON.stringify(payload);
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex');
return { body, ts, sig: `sha256=${sig}` };
}
// Manda `body` tal cual, sin volver a serializar el objeto.import hashlib, hmac, json, time
def firmar(payload, secret):
body = json.dumps(payload, separators=(",", ":")).encode()
ts = str(int(time.time()))
sig = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return body, ts, "sha256=" + sigRegla: El secreto se ve una sola vez
Al crear la conexión o al rotarla, el panel enseña el secreto una vez y no lo vuelve a enseñar: se guarda cifrado y ni el hotel ni nosotros lo podemos leer después. Guárdalo en tu gestor de secretos en ese momento. Si se pierde, la salida es rotar, y el secreto anterior deja de valer en el acto.
El timestamp caduca
Una firma válida sigue siendo válida para siempre si nadie mira la hora. Por eso la miramos: el timestamp que mandas no puede diferir del nuestro en más de cinco minutos. Fuera de ese margen la petición se rechaza aunque la firma esté bien.
- Sincroniza el reloj del servidor con NTP. El 401 más común no es una firma mal calculada, es un reloj atrasado.
- Genera un timestamp y una firma nuevos en cada reintento. Reenviar la petición completa tal cual falla en cuanto pasan cinco minutos.
- La hora va en segundos, no en milisegundos. Un valor en milisegundos queda miles de años en el futuro y se rechaza.
Idempotencia
Una red que se corta no te dice si la petición llegó. Reintentar a ciegas es lo que duplica una reserva o cobra dos veces la misma cena. Por eso todo lo que crea algo lleva una llave de idempotencia.
X-Idempotency-Key: 8c2e1f04-5a6b-4c7d-8e9f-0a1b2c3d4e5f- Genera la llave una vez, antes del primer intento, y reúsala en todos los reintentos de esa misma operación. Un UUID v4 sirve.
- La misma llave devuelve el mismo resultado: el recurso que ya se creó, no uno nuevo y no un error.
- La llave queda anotada junto al recurso que creó. Reusarla con datos distintos no crea nada nuevo ni da error: devuelve el primero. Si cambian los datos, cambia la llave.
- La llave admite letras, dígitos y . _ : - y cabe en 100 caracteres. Un UUID v4 sirve.
- La comprobación de la llave y la venta ocurren dentro de un lock, así que dos peticiones simultáneas con la misma llave no pueden crear dos recursos: se serializan.
Dato: El POS usa el folio del ticket
En los cargos a la habitación el papel de la llave de idempotencia lo hace el campo «externalId», que es el folio del ticket del restaurante. Es la misma idea con otro nombre: reintenta con el mismo folio y el cargo no se duplica.
Formato de las respuestas
Toda respuesta es JSON con la misma envoltura. Cuando salió bien viene «data»; cuando no, viene «error». Nunca los dos. «meta» aparece solo en las listas paginadas.
{
"data": {
"id": "5d0c3f1e-2b7a-4c1d-9e8f-0a1b2c3d4e5f",
"totalCents": 372000,
"currency": "MXN",
"createdAt": "2026-11-20T21:03:11Z"
},
"meta": { "page": 1, "limit": 20, "total": 37 }
}{
"error": {
"code": "VALIDATION_ERROR",
"message": "checkOut must be after checkIn",
"details": { "field": "checkOut" }
}
}| Qué | Cómo viaja | Ejemplo |
|---|---|---|
| Identificadores | UUID v4 en texto. Nunca enteros. | 5d0c3f1e-2b7a-4c1d-9e8f-0a1b2c3d4e5f |
| Momentos | ISO 8601 en UTC, con la Z al final. | 2026-11-20T21:03:11Z |
| Días | AAAA-MM-DD, sin hora. Una entrada es un día, no un instante. | 2026-11-20 |
| Importes | Centavos, siempre entero y siempre con su moneda. | 372000 MXN son $3,720.00 |
| Errores | El código es estable y en inglés; el mensaje va en español, para que lo puedas enseñar tal cual. | code: OFFER_EXPIRED |
Cuidado: Los centavos no se convierten a decimal por el camino
Divide entre cien solo al pintar en pantalla. Si guardas el importe como número con punto decimal, tarde o temprano un redondeo deja un peso de diferencia entre tu ticket y el folio del huésped.
Errores
El código HTTP dice de qué tipo es el problema y «error.code» dice cuál exactamente. Programa contra el código, no contra el texto del mensaje: el mensaje está para leerlo una persona y puede cambiar.
Errores
| HTTP | Código | Cuándo | Qué hacer |
|---|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | Falta la llave de idempotencia en algo que crea un recurso. | Mándala. No se inventa por ti. |
| 401 | INVALID_SIGNATURE | Llave desconocida o revocada, firma que no cuadra, u hora fuera del margen de cinco minutos. | Revisa el reloj primero. Después, que firmes los bytes exactos que mandas. |
| 403 | ORIGIN_NOT_ALLOWED | La petición llegó desde un navegador con un dominio que la llave no tiene autorizado. | No reintentes. O llamas desde tu servidor, o el hotel declara ese dominio en su panel. |
| 404 | NOT_FOUND | El recurso no existe, o no es de esta propiedad. | Comprueba el identificador. Una credencial solo ve su propiedad. |
| 409 | (varios) | La operación choca con el estado actual: ya se canceló, ya se cobró, ya no admite cargos. Cada integración tiene sus códigos, y están en su apartado. | Lee el recurso para ver cómo quedó antes de decidir. Reintentar igual vuelve a chocar. |
| 413 | PAYLOAD_TOO_LARGE | El cuerpo pasa de 64 KB. | Manda menos líneas o menos detalle. |
| 422 | VALIDATION_ERROR | Falta un campo o trae un valor imposible. | «details.field» dice cuál. Corrígelo y vuelve a mandar. |
| 429 | RATE_LIMIT_EXCEEDED | Más de 300 peticiones por minuto desde la misma IP. | Espera lo que diga «Retry-After» y reintenta. Es el único 4xx que se reintenta sin cambiar nada. |
| 500 | INTERNAL_ERROR | Algo se rompió de nuestro lado. | Reintenta con la misma llave de idempotencia, espaciando los intentos. |
| 503 | PAYMENTS_NOT_CONFIGURED | El hotel no tiene el cobro en línea configurado. | No reintentes. Todo lo demás sigue funcionando; lo único que no se puede es cobrar. |
Regla: Nunca guardes datos de tarjeta
Ningún endpoint de esta API recibe un número de tarjeta, un CVV ni una fecha de vencimiento, y ninguno los devuelve. El dato de la tarjeta lo toma Stripe en el navegador del huésped y nunca pasa por tu sistema ni por el nuestro.
¿Vas a integrar un hotel?
Las credenciales salen del panel del hotel, y las de sandbox se crean el mismo día. Si el hotel todavía no tiene cuenta, empieza por registrarlo.