# Crear una reserva de uno a ocho turnos

> POST /api/v1/reservas

`POST /api/v1/reservas`

Scope `reservas:crear`. Exige `Idempotency-Key`: misma clave y mismo cuerpo devuelven la misma reserva (200, `operacion.repetida: true`); otro cuerpo con la misma clave da 409 `CONFLICTO_INTENCION`. Con seña, la reserva queda GUARDADO con `pago: PENDIENTE` y `datos.sena.checkoutUrl` (Mercado Pago); se confirma solo por el webhook de MP.

> Autenticación: `Authorization: Bearer <clave>`. Ver [Autenticación](https://turnovapp.com/docs/autenticacion.md).

## Parámetros

| Nombre | En | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | Sí | Formato `^[A-Za-z0-9._:-]{8,100}$`. |

## Cuerpo

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `turnos` | array de object | Sí | Entre 1 y 8 elementos. |
| `turnos[].servicioId` | string | Sí |  |
| `turnos[].varianteId` | string o null | No |  |
| `turnos[].profesionalId` | string o null | No |  |
| `turnos[].fecha` | string (date) | Sí |  |
| `turnos[].hora` | string | Sí | Hora del negocio. Formato `^(?:[01]\d\|2[0-3]):[0-5]\d$`. |
| `turnos[].addOnIds` | array de string | No | Entre 0 y 50 elementos. |
| `sucursalId` | string o null | No |  |
| `modoCadena` | `"uno_tras_otro"` | No |  |
| `cliente` | object | Sí |  |
| `cliente.nombre` | string | Sí | Hasta 120 caracteres. |
| `cliente.telefono` | string | Sí | Hasta 32 caracteres. |
| `cliente.codigoPais` | string | No | Hasta 6 caracteres. Por defecto `+54`. |
| `cliente.email` | string | No | Hasta 254 caracteres. |
| `cliente.consintioContacto` | boolean | No |  |
| `notas` | string | No | Hasta 4000 caracteres. |
| `cuponCodigo` | string | No | Hasta 64 caracteres. |

## Ejemplo

curl:

```bash
curl -X POST "https://turnovapp.com/api/v1/reservas" \
  -H "Authorization: Bearer $TURNOVA_CLAVE_SECRETA" \
  -H "Idempotency-Key: reserva-7f3c2a9e-01" \
  -H "Content-Type: application/json" \
  -d '{ "turnos": [ { "servicioId": "cmh3s0abc0012", "fecha": "2030-01-10", "hora": "10:00" } ], "cliente": { "nombre": "Ana Pérez", "telefono": "+5493515550101", "email": "ana@example.com" } }'
```

JavaScript:

```js
// Desde el navegador, en un origen cargado en la clave pública.
const res = await fetch("https://turnovapp.com/api/v1/reservas", {
  method: "POST",
  headers: {
    Authorization: "Bearer tnv_pk_TU_CLAVE_PUBLICA",
    "Idempotency-Key": claveDeEstaReserva, // la misma en cada reintento
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "turnos": [
      {
        "servicioId": "cmh3s0abc0012",
        "fecha": "2030-01-10",
        "hora": "10:00"
      }
    ],
    "cliente": {
      "nombre": "Ana Pérez",
      "telefono": "+5493515550101",
      "email": "ana@example.com"
    }
  }),
});
const r = await res.json();
if (!r.ok) throw new Error(`${r.error.codigo}: ${r.error.mensaje}`);
```

## Respuestas

| HTTP | Qué significa |
| --- | --- |
| 200 | Reintento de una reserva ya hecha con la misma clave |
| 201 | Reserva nueva |
| 400 | Datos inválidos: mirá `error.campos` (`ENTRADA_INVALIDA`, `IDEMPOTENCY_KEY_FALTANTE`…). |
| 401 | Clave faltante, inválida o revocada. |
| 403 | Origen no permitido, permiso faltante, clave secreta desde un navegador o plan sin la API (`PLAN_SIN_API`). |
| 404 | No existe o no es de este negocio. |
| 409 | Conflicto: el horario ya no está libre, otra intención con la misma `Idempotency-Key` o el servicio se reserva por WhatsApp. |
| 429 | Demasiados pedidos (`LIMITE_USO`): esperá `Retry-After` segundos. |
| 500 | Error técnico: reintentá con los mismos datos (y la misma `Idempotency-Key`). |

### Respuesta 201

```json
{
  "version": 1,
  "ok": true,
  "progreso": {
    "principal": "GUARDADO",
    "pago": "PENDIENTE"
  },
  "operacion": {
    "referencia": "cmh4k2n9x0001",
    "repetida": false
  },
  "requiereRevision": false,
  "datos": {
    "compraId": "cmh4k2n9x0001",
    "total": 25000,
    "sena": {
      "monto": 6250,
      "checkoutUrl": "https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=123456-abcd"
    },
    "turnos": [
      {
        "turnoId": "cmh4k2pa10003",
        "servicio": "Masaje descontracturante",
        "profesional": "Lucía",
        "fecha": "2030-01-10",
        "hora": "10:00",
        "duracionMin": 60,
        "extras": [],
        "sesionesTotal": null
      }
    ]
  }
}
```

| Campo | Tipo | Obligatorio | Descripción |
| --- | --- | --- | --- |
| `datos.compraId` | string | No |  |
| `datos.total` | number | No |  |
| `datos.sena` | object o null | No |  |
| `datos.sena.monto` | number | No |  |
| `datos.sena.checkoutUrl` | string o null | No | Link de Mercado Pago. MP vuelve a la página de resultado de Turnova. |
| `datos.turnos` | array de object | No |  |
| `datos.turnos[].turnoId` | string | No |  |
| `datos.turnos[].servicio` | string | No |  |
| `datos.turnos[].profesional` | string | No |  |
| `datos.turnos[].fecha` | string (date) | No |  |
| `datos.turnos[].hora` | string | No |  |
| `datos.turnos[].duracionMin` | integer | No |  |
| `datos.turnos[].extras` | array de string | No |  |
| `datos.turnos[].sesionesTotal` | integer o null | No |  |

Los errores siguen el formato común: ver [Errores](https://turnovapp.com/docs/errores.md).
