Convenciones
Todos los endpoints siguen las mismas convenciones de códigos de estado, errores, reintentos y paginación. Apréndelas una vez y valen en todas partes.
| Estado | Significado |
|---|---|
200 | Correcto. |
201 | Se ha creado un recurso (un huésped, un bloqueo o un cargo). |
400 | Petición inválida: JSON mal formado o un parámetro que no pasó la validación. |
401 | Fallo de autenticación: falta la clave de API o no es válida. |
403 | La clave es válida pero no tiene el ámbito necesario. |
404 | La ruta o el recurso no existe (o no es tuyo). |
405 | La ruta existe, pero no para este método HTTP. |
409 | Conflicto: sin disponibilidad, reutilización de una clave de idempotencia o saldo de fidelidad insuficiente. |
429 | Límite de uso alcanzado. Reintenta después del tiempo de la cabecera Retry-After. |
500 | Error interno del servidor. |
503 | Mantenimiento. La plataforma no está disponible temporalmente. Reintenta después del tiempo de la cabecera Retry-After. |
Cada fallo devuelve el estado correspondiente y un único objeto error:
{
"error": {
"type": "permission",
"code": "insufficient_scope",
"message": "Missing required scope: reservations:write.",
"request_id": "req_a1b2c3d4e5"
}
}type: la categoría:invalid_request,authentication,permission,not_found,conflict,rate_limitoserver.code: un identificador estable y legible por máquina. Bifurca por este valor, no pormessage.message: una explicación legible por personas. Puedes registrarla en el log; no la analices.param: el campo de la petición que provocó el error. Solo aparece en los errores400de validación de campos, y se omite en los demás.estimated_end: una marca de tiempo ISO con la hora prevista de fin de una ventana de mantenimiento. Solo aparece en los503, y solo cuando se ha fijado una hora de fin. Es más precisa que la cabeceraRetry-After, que se deriva de ella, así que úsala para mostrar un estado del tipo "volvemos a las".request_id: identifica la petición de forma única. Inclúyelo cuando contactes con soporte.
Antes el código que el mensaje
El texto de message puede cambiar; code no. Gestiona los errores bifurcando por error.code (por ejemplo, no_availability, rate_plan_not_found, insufficient_points).
Códigos habituales con los que te encontrarás:
| Código | Tipo | Cuándo |
|---|---|---|
missing_api_key / invalid_api_key | authentication | Falta la cabecera Authorization, o la clave es desconocida, está desactivada o ha caducado. |
insufficient_scope | permission | La clave no tiene el ámbito del endpoint. |
invalid_parameters / invalid_json | invalid_request | Un campo no pasó la validación, o el cuerpo no era JSON válido. |
unknown_route | not_found | Ningún endpoint coincide con la ruta. |
no_availability | conflict | El tipo de habitación está agotado para las fechas pedidas. |
idempotency_key_reuse | conflict | Se reutilizó una Idempotency-Key con un cuerpo de petición distinto. |
idempotency_in_progress | conflict | Una petición anterior con la misma Idempotency-Key sigue en curso. Reintenta en breve. |
insufficient_points | conflict | Un canje de fidelidad supera el saldo disponible. |
rate_limited | rate_limit | Se superó el límite de peticiones por clave. |
maintenance_mode | server | La plataforma está en una ventana de mantenimiento. Reintenta después del tiempo de la cabecera Retry-After. |
Todos los identificadores de recursos de Veridien son UUID, por ejemplo 7d5dc2ea-1f4e-4bfa-9a52-6c3f08b41e27. La referencia de reserva que ve el huésped es otra cosa: confirmation_code es un código corto y legible (por ejemplo B7XKQ4M2NZ) que se muestra a huéspedes y personal, y nunca se usa como identificador de recurso. Los identificadores de petición llevan el prefijo req_. Los campos que aportas tú, como payment_reference, son opacos para Veridien y conservan el formato que use tu sistema.
Las peticiones POST y PATCH son las que cambian el estado: crear un huésped, bloquear inventario, confirmar una reserva, canjear puntos, anotar un cargo. Para que los reintentos sean seguros, envía una cabecera Idempotency-Key con un valor único (un UUID funciona bien):
curl -X POST https://veridien.app/api/v1/holds \
-H "Authorization: Bearer vrdn_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1d2c9a-8b3e-4a17-9c2f-1e5b7d0a4c83" \
-d '{ "room_type_id": "9b2f6c3e-8d1a-4f5b-a7c9-4e0d2b8f1a63", "rate_plan_id": "5c1e8f4a-2b7d-4a9c-8e6f-1d3b9a5c7e02", "check_in": "2026-08-01", "check_out": "2026-08-04", "guest_id": "3f8a2c1b-9d4e-4c6a-8b7f-5e2d1a9c0b34", "adults": 2 }'- La primera petición con una clave dada se ejecuta con normalidad y su respuesta se guarda.
- Un reintento con la misma clave y el mismo cuerpo reproduce la respuesta guardada: ni un segundo bloqueo, ni un segundo cargo.
- Reutilizar la clave con un cuerpo distinto devuelve
409 idempotency_key_reuse. - Enviar la misma clave en paralelo, antes de que haya terminado la primera petición, devuelve
409 idempotency_in_progress. Reintenta un momento después y obtendrás la respuesta guardada. La clave se reserva antes de ejecutar la operación, así que la segunda petición nunca llega a ejecutarla. - Una petición que falla libera su clave, así que un reintento honesto vuelve a ejecutarse en lugar de reproducir el fallo. Solo se guardan las respuestas correctas: un
409 no_availabilitypor un agotamiento puntual no se reproduce eternamente cuando la habitación vuelve a estar libre.
Las claves están acotadas a cada clave de API. Usa una Idempotency-Key nueva por cada operación lógica.
Confirmar es doblemente idempotente
Confirmar un bloqueo (POST /holds/{id}/confirm) es además idempotente sobre su payment_reference: volver a confirmar una reserva ya confirmada, o repetir la misma referencia de pago, devuelve el resultado confirmado existente con "already_confirmed": true en lugar de volver a cobrar.
GET /reservations pagina con limit y offset. Los demás endpoints de listado (/room-types, /services, /rate-plans) devuelven el conjunto completo del establecimiento y no paginan.
| Parámetro | Valor por defecto | Máximo | Significado |
|---|---|---|---|
limit | 25 | 100 | Cuántos registros devolver. |
offset | 0 | 10000 | Cuántos registros saltar. |
La respuesta envuelve los resultados en data e indica si quedan más:
{
"has_more": true,
"data": [ /* ... */ ]
}Para pedir la página siguiente, suma limit a tu offset anterior. Detente cuando has_more sea false.
Todo valor monetario es una cadena decimal, nunca un número en coma flotante: "1310.40", no 1310.4. Analízalos con un tipo decimal, no con uno binario de coma flotante.
Un establecimiento tiene una moneda base, y una cuenta puede llevar líneas en varias monedas a la vez (un cargo de habitación en USD con una línea de restaurante en MVR es lo normal). De ahí salen dos reglas:
Los importes en monedas distintas nunca se suman entre sí. Allí donde se informa de un saldo, un array balances da los importes reales por moneda, y esa es la cifra fiable:
"balances": [
{ "currency": "USD", "charges": "1310.40", "payments": "0.00", "balance": "1310.40" },
{ "currency": "MVR", "charges": "1500.00", "payments": "1500.00", "balance": "0.00" }
]Una cuenta está settled solo cuando no se debe nada en ninguna moneda. Una deuda en USD nunca se cancela con un crédito en MVR.
Los totales planos son una valoración, y pueden ser null. Campos como balance, total_charges y folio_balance son cifras de conveniencia valoradas en la moneda base del establecimiento, construidas a partir del tipo de cambio congelado en cada línea en el momento en que se escribió. Como el tipo está congelado, un cambio posterior nunca altera el valor declarado de un cargo existente. Si una línea está en una moneda para la que el establecimiento no tiene tipo configurado, no se puede valorar, y esos campos son null en lugar de un número equivocado. Trata el caso null antes de hacer cuentas.
Los límites de uso funcionan con un cubo con fugas (token bucket), por clave de API. Tu cubo tiene capacidad para 600 fichas y se rellena a un ritmo constante de 10 fichas por segundo (600 por minuto). Cada petición gasta una ficha:
- Una ráfaga puede gastar el cubo entero de golpe, hasta 600 peticiones seguidas.
- El rendimiento sostenido es el ritmo de recarga, 10 peticiones por segundo.
Cuando el cubo está vacío, las peticiones devuelven 429 con una cabecera Retry-After. Espera al menos los segundos indicados y reintenta. Es mejor cachear las lecturas (/property, /room-types y /services llevan un Cache-Control corto; /availability y /rates son inventario en vivo y a propósito no se cachean, así que no los caches tú tampoco) y evitar los bucles de sondeo agresivos.
Cada respuesta, correcta o con error, lleva asociado un request_id (que también se devuelve en los cuerpos de error y en /me). Guárdalo en el log. Cuando comuniques una incidencia, el identificador de petición permite a soporte rastrear la llamada exacta.