La API de Profile Reserve 360
Conecta el TPV, la web de la cadena, un CRM o tu propia app con las reservas del negocio: huecos libres con las mismas reglas que el panel, reservas que se crean, se mueven y se cancelan, clientes sin duplicados, entradas de eventos y avisos firmados cuando algo cambia.
URL base · https://profilereserve360.com/api/v1
REST + JSON OpenAPI 3.1 Claves por negocio con permisos Webhooks HMAC-SHA256 Idempotency-Key
Autenticación
Cada clave pertenece a un negocio (una sede) y se crea en el panel, en Integraciones › API y webhooks , eligiendo sus permisos . La clave completa se enseña una sola vez; guárdala como un secreto de servidor (nunca en el JavaScript de una web: para eso está el widget).
CabeceraCopiar
Authorization: Bearer r360_ab12cd34_0f1e2d3c4b5a69788796a5b4c3d2e1f0
También vale X-Api-Key: r360_…. Comprueba la conexión con GET /, que devuelve el negocio, el prefijo de la clave y sus permisos.
Permisos (alcances)
Alcance Qué deja hacer
reservas.leer Consultar reservas reservas.escribir Crear, modificar y cancelar reservas disponibilidad.leer Consultar huecos libres clientes.leer Consultar clientes clientes.escribir Crear y modificar clientes catalogo.leer Consultar sedes, servicios, profesionales y recursos eventos.leer Consultar eventos y entradas eventos.vender Vender entradas
Límites de ritmo
Cada clave tiene un límite por minuto (60 por defecto) y otro por día (10.000), ajustables en el panel. Todas las respuestas llevan X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset (epoch). Al pasarte recibes un 429 con Retry-After en segundos: espera ese tiempo y reintenta.
Errores
Los errores usan los códigos HTTP de siempre y un cuerpo con un codigo estable (para programar contra él) y un mensaje en español (para enseñarlo). Si el problema es un campo, viene en campo.
409 Conflict
{
"error": {
"codigo": "sin_hueco",
"mensaje": "Esa franja ya tiene el máximo de entradas (pacing).",
"motivo": "cocina"
}
}
HTTP Cuándo
400 · 413 · 415 JSON mal formado, cuerpo de más de 64 KB o sin Content-Type: application/json.
401 Sin clave, clave errónea o revocada (sin_clave, clave_no_valida).
403 La clave no tiene el permiso de esa ruta (sin_permiso).
404 No existe en este negocio (tampoco se dice si existe en otro).
409 No hay hueco (sin_hueco con motivo: completo · cerrado · cocina · turno · fuera_de_horario), choque con otra reserva, agotado.
422 Un dato no es válido (dato_no_valido + campo).
429 Límite de ritmo (limite_de_ritmo). Mira Retry-After.
500 Fallo nuestro. Se registra; si se repite, escríbenos con la hora.
Paginación
Las listas largas (/reservas, /clientes, entradas) devuelven {"datos": […], "siguiente": 123}. Para la página siguiente, pide lo mismo con cursor=123; cuando siguiente es null ya no hay más. limite va de 1 a 200 (50 por defecto).
Para sincronizar reservas con otro sistema, guarda la hora de tu última lectura y pide GET /reservas?actualizado_desde=<esa hora>, o mejor: suscríbete a los webhooks .
Idempotencia
Si una petición POST /reservas se corta y no sabes si llegó, repítela con la misma cabecera Idempotency-Key: durante 24 horas devuelve la misma reserva (con Idempotent-Replayed: true) en lugar de crear otra.
Negocio
Quién es el negocio de la clave, sus sedes y sus horarios.
Datos de la clave usada (prefijo, permisos, límites) y del negocio. Útil para comprobar la conexión.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
Respuesta 200
{
"negocio": {
"id": 1,
"slug": "habana-lounge",
"nombre": "La Habana Lounge"
},
"clave": {
"prefijo": "r360_ab12cd34",
"alcances": [
"reservas.leer"
]
}
}
Permiso catalogo.leer
Datos públicos del negocio, su vocabulario (mesa, sillón, pista…) y sus horarios de reserva.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/negocio" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/negocio", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/negocio");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
GET /sedes Sedes de la cuenta
Permiso catalogo.leer
Todos los locales de la misma cuenta (grupo). La clave sólo reserva en el suyo; las demás se listan para enlazarlas.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/sedes" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/sedes", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/sedes");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
Catálogo
Servicios, profesionales y recursos (mesas, sillones, pistas…).
Permiso catalogo.leer
Los servicios activos (añade todos=1 para ver también los inactivos).
Parámetro Descripción
todos integer Incluir inactivos
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/servicios" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/servicios", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/servicios");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
GET /servicios/{id} Un servicio
Permiso catalogo.leer
Parámetro Descripción
id en la ruta obligatorio Identificador
404 No existe.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/servicios/12" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/servicios/12", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/servicios/12");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
GET /profesionales Profesionales
Permiso catalogo.leer
El equipo que atiende citas. servicios = qué servicios hace (null = todos).
Parámetro Descripción
todos integer Incluir inactivos
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/profesionales" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/profesionales", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/profesionales");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
GET /profesionales/{id} Un profesional
Permiso catalogo.leer
Parámetro Descripción
id en la ruta obligatorio Identificador
404 No existe.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/profesionales/12" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/profesionales/12", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/profesionales/12");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
Permiso catalogo.leer
Lo reservable: mesas, reservados, sillones, cabinas, pistas… Filtra por clase.
Parámetro Descripción
clase string mesa · reservado · sillon · pista · cabina…
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/recursos?clase=mesa" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/recursos?clase=mesa", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/recursos?clase=mesa");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
GET /recursos/{id} Un recurso
Permiso catalogo.leer
Parámetro Descripción
id en la ruta obligatorio Identificador
404 No existe.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/recursos/12" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/recursos/12", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/recursos/12");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
Disponibilidad
Huecos libres con las mismas reglas que la web y el panel: horario, cierres, pacing y asignación de mesa o profesional.
GET /disponibilidad Huecos de un día
Permiso disponibilidad.leer
Franjas del día agrupadas por turno, cada una libre o con su motivo:
completo: no queda mesa, profesional o recurso;cerrado: cierre o festivo;cocina: la franja ya tiene el máximo de entradas (pacing);turno: el turno entero está lleno.No aplica la antelación mínima de la web: la API es del propio negocio.
Parámetro Descripción
fecha string obligatorio Día de serviciopax integer Personas (por defecto 2)servicio_id integer Para citas: el servicioprofesional_id integer Con un profesional concretorecurso_id integer Sólo si ese recurso concreto está libresolo_libres integer 1 = sólo las franjas libres
422 Parámetro no válido.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/disponibilidad?fecha=2026-10-24&pax=4&solo_libres=1" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/disponibilidad?fecha=2026-10-24&pax=4&solo_libres=1", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/disponibilidad?fecha=2026-10-24&pax=4&solo_libres=1");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
Respuesta 200
{
"fecha": "2026-10-24",
"pax": 4,
"servicio_id": null,
"profesional_id": null,
"recurso_id": null,
"libres": 2,
"turnos": [
{
"turno": "Cenas",
"franjas": [
{
"hora": "21:00",
"libre": true,
"motivo": null,
"profesional_id": null
},
{
"hora": "21:15",
"libre": false,
"motivo": "cocina",
"profesional_id": null
},
{
"hora": "21:30",
"libre": true,
"motivo": null,
"profesional_id": null
}
]
}
]
}
Reservas
Crear, consultar, modificar y cancelar reservas.
GET /reservas Listar reservas
Permiso reservas.leer
Paginado por cursor, en orden de creación. Para sincronizar, guarda el último actualizado y pide actualizado_desde.
Parámetro Descripción
desde string Fecha mínima (día de servicio)hasta string Fecha máximaestado string Uno o varios separados por comas: pendiente, confirmada, llegada, sentada, terminada, no_show, canceladacliente_id integer De un clienteactualizado_desde string ISO 8601 o epoch: sólo lo cambiado desde entonceslimite integer Cuántos por página (1–200, por defecto 50)cursor integer El valor «siguiente» de la página anterior
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/reservas?estado=confirmada%2Cpendiente" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/reservas?estado=confirmada%2Cpendiente", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/reservas?estado=confirmada%2Cpendiente");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
POST /reservas Crear una reserva
Permiso reservas.escribir
Comprueba el hueco con las mismas reglas que la web (horario, cierres, pacing, mesas, profesional) y asigna la mesa o el profesional si no se dan.
El cliente se busca por teléfono o correo en la cuenta antes de crearlo: no se duplican fichas.
Manda la cabecera Idempotency-Key (cualquier texto único, hasta 100 caracteres) para que un reintento de red devuelva la misma reserva en vez de crear dos.
Dispara el webhook reserva.creada.
Parámetro Descripción
Idempotency-Key cabecera Hace segura la repetición de la petición durante 24 h
Campo del cuerpo Tipo
fecha * string hora * string pax integer servicio_id integer profesional_id integer recurso_id integer cliente_id integer cliente ClienteNuevo notas string estado string confirmada · pendiente canal string telefono · web · whatsapp · instagram · google · walkin · rrpp · qr · empleado · api duracion integer minutos; por defecto la del servicio o la del tamaño de grupoimporte number origen object Atribución
409 No hay hueco o choca con otra. 422 Datos no válidos.
curl JavaScript PHP Copiar
curl -X POST "https://profilereserve360.com/api/v1/reservas" \
-H "Authorization: Bearer $R360_CLAVE" \
-H "Idempotency-Key: pedido-tpv-99812" \
-H "Content-Type: application/json" \
-d '{"fecha":"2026-10-24","hora":"21:30","pax":4,"cliente":{"nombre":"Ana Ejemplo","tel":"600000001","email":"ana@example.com"},"notas":"Cumpleaños","origen":{"fuente":"tpv","campana":"otono"}}' const r = await fetch("https://profilereserve360.com/api/v1/reservas", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`, "Content-Type": "application/json"},
body: JSON.stringify({"fecha":"2026-10-24","hora":"21:30","pax":4,"cliente":{"nombre":"Ana Ejemplo","tel":"600000001","email":"ana@example.com"},"notas":"Cumpleaños","origen":{"fuente":"tpv","campana":"otono"}})
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/reservas");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(array (
'fecha' => '2026-10-24',
'hora' => '21:30',
'pax' => 4,
'cliente' =>
array (
'nombre' => 'Ana Ejemplo',
'tel' => '600000001',
'email' => 'ana@example.com',
),
'notas' => 'Cumpleaños',
'origen' =>
array (
'fuente' => 'tpv',
'campana' => 'otono',
),
))]);
$datos = json_decode(curl_exec($c), true);
Respuesta 201
{
"id": 1843,
"codigo": "HL-7K3Q",
"negocio_id": 1,
"fecha": "2026-10-24",
"hora": "21:30",
"inicio": "2026-10-24T21:30:00+02:00",
"duracion": 105,
"pax": 4,
"estado": "confirmada",
"canal": "api",
"notas": "Cumpleaños",
"importe": 0.0,
"pagado": 0.0,
"motivo_cancelacion": null,
"cliente": {
"id": 311,
"nombre": "Ana Ejemplo",
"tel": "600000001",
"email": "ana@example.com"
},
"servicio": null,
"profesional": null,
"recursos": [
{
"id": 12,
"nombre": "M4",
"clase": "mesa"
}
],
"evento_id": null,
"origen": {
"fuente": "api",
"medio": "",
"campana": "",
"via": "api"
},
"creado": "2026-10-20T10:02:11+02:00",
"actualizado": "2026-10-20T10:02:11+02:00"
}
GET /reservas/{id} Una reserva
Permiso reservas.leer
Por id numérico o por código (HL-7K3Q).
Parámetro Descripción
id en la ruta obligatorio Id o código
404 No existe.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/reservas/HL-7K3Q" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/reservas/HL-7K3Q", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/reservas/HL-7K3Q");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
Respuesta 200
{
"id": 1843,
"codigo": "HL-7K3Q",
"negocio_id": 1,
"fecha": "2026-10-24",
"hora": "21:30",
"inicio": "2026-10-24T21:30:00+02:00",
"duracion": 105,
"pax": 4,
"estado": "confirmada",
"canal": "api",
"notas": "Cumpleaños",
"importe": 0.0,
"pagado": 0.0,
"motivo_cancelacion": null,
"cliente": {
"id": 311,
"nombre": "Ana Ejemplo",
"tel": "600000001",
"email": "ana@example.com"
},
"servicio": null,
"profesional": null,
"recursos": [
{
"id": 12,
"nombre": "M4",
"clase": "mesa"
}
],
"evento_id": null,
"origen": {
"fuente": "api",
"medio": "",
"campana": "",
"via": "api"
},
"creado": "2026-10-20T10:02:11+02:00",
"actualizado": "2026-10-20T10:02:11+02:00"
}
PATCH /reservas/{id} Modificar una reserva
Permiso reservas.escribir
Manda sólo lo que cambia. Si cambia la fecha, la hora, las personas, el servicio o el profesional, se vuelve a comprobar el hueco (sin contarse a sí misma) y se reasigna la mesa.
Cambiar estado sirve para confirmar, marcar llegada, no-show o terminada. Dispara reserva.editada y, si cambia el estado, reserva.estado.
Parámetro Descripción
id en la ruta obligatorio Id o código
Campo del cuerpo Tipo
fecha string hora string pax integer servicio_id integer | null profesional_id integer | null recurso_id integer | null notas string estado string pendiente · confirmada · llegada · sentada · terminada · no_show · cancelada importe number motivo string Si se cancela
404 No existe. 409 Sin hueco.
curl JavaScript PHP Copiar
curl -X PATCH "https://profilereserve360.com/api/v1/reservas/HL-7K3Q" \
-H "Authorization: Bearer $R360_CLAVE" \
-H "Content-Type: application/json" \
-d '{"hora":"22:00","pax":5}' const r = await fetch("https://profilereserve360.com/api/v1/reservas/HL-7K3Q", {
method: "PATCH",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`, "Content-Type": "application/json"},
body: JSON.stringify({"hora":"22:00","pax":5})
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/reservas/HL-7K3Q");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(array (
'hora' => '22:00',
'pax' => 5,
))]);
$datos = json_decode(curl_exec($c), true);
DELETE /reservas/{id} Cancelar una reserva
Permiso reservas.escribir
Igual que POST /reservas/{id}/cancelar. No borra nada: la reserva queda «cancelada» con su motivo (el historial del cliente lo necesita).
Parámetro Descripción
id en la ruta obligatorio Id o código
409 No se puede cancelar.
curl JavaScript PHP Copiar
curl -X DELETE "https://profilereserve360.com/api/v1/reservas/HL-7K3Q" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/reservas/HL-7K3Q", {
method: "DELETE",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/reservas/HL-7K3Q");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
POST /reservas/{id}/cancelar Cancelar (con motivo)
Permiso reservas.escribir
Cancelar una reserva con un motivo. Si ya estaba cancelada, devuelve la reserva tal cual (es idempotente).
Parámetro Descripción
id en la ruta obligatorio Id o código
curl JavaScript PHP Copiar
curl -X POST "https://profilereserve360.com/api/v1/reservas/HL-7K3Q/cancelar" \
-H "Authorization: Bearer $R360_CLAVE" \
-H "Content-Type: application/json" \
-d '{"motivo":"El cliente llamó para anular"}' const r = await fetch("https://profilereserve360.com/api/v1/reservas/HL-7K3Q/cancelar", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`, "Content-Type": "application/json"},
body: JSON.stringify({"motivo":"El cliente llamó para anular"})
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/reservas/HL-7K3Q/cancelar");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(array (
'motivo' => 'El cliente llamó para anular',
))]);
$datos = json_decode(curl_exec($c), true);
Clientes
Fichas de clientes. Son de la cuenta: las comparten todas sus sedes.
GET /clientes Buscar clientes
Permiso clientes.leer
Por texto (q: nombre, correo o teléfono), por email exacto o por tel exacto.
Parámetro Descripción
q string Texto libreemail string Correo exactotel string Teléfono exactolimite integer Cuántos por página (1–200, por defecto 50)cursor integer El valor «siguiente» de la página anterior
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/clientes?q=ana" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/clientes?q=ana", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/clientes?q=ana");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
POST /clientes Crear (o encontrar) un cliente
Permiso clientes.escribir
Si ya hay un cliente con ese teléfono o correo en la cuenta, se completa y se devuelve ese (200, existente: true); si no, se crea (201) y se dispara cliente.creado.
Campo del cuerpo Tipo
nombre string tel string email string idioma string alergias string notas string etiquetas array nacimiento string marketing boolean
422 Datos no válidos.
curl JavaScript PHP Copiar
curl -X POST "https://profilereserve360.com/api/v1/clientes" \
-H "Authorization: Bearer $R360_CLAVE" \
-H "Content-Type: application/json" \
-d '{"nombre":"Ana Ejemplo","tel":"600000001","email":"ana@example.com","etiquetas":["vip"],"marketing":true}' const r = await fetch("https://profilereserve360.com/api/v1/clientes", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`, "Content-Type": "application/json"},
body: JSON.stringify({"nombre":"Ana Ejemplo","tel":"600000001","email":"ana@example.com","etiquetas":["vip"],"marketing":true})
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/clientes");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(array (
'nombre' => 'Ana Ejemplo',
'tel' => '600000001',
'email' => 'ana@example.com',
'etiquetas' =>
array (
0 => 'vip',
),
'marketing' => true,
))]);
$datos = json_decode(curl_exec($c), true);
Respuesta 201
{
"id": 311,
"nombre": "Ana Ejemplo",
"tel": "600000001",
"email": "ana@example.com",
"idioma": "es",
"nacimiento": null,
"etiquetas": [
"vip"
],
"alergias": "gluten",
"notas": "",
"marketing": true,
"bloqueado": false,
"creado": "2026-03-02T18:40:00+01:00"
}
GET /clientes/{id} Un cliente
Permiso clientes.leer
Con su historial en este negocio (reservas, visitas, no-shows, última visita).
Parámetro Descripción
id en la ruta obligatorio Identificador
404 No existe.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/clientes/12" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/clientes/12", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/clientes/12");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
PATCH /clientes/{id} Modificar un cliente
Permiso clientes.escribir
Manda sólo lo que cambia. marketing apunta cuándo se dio o se retiró el consentimiento.
Parámetro Descripción
id en la ruta obligatorio Identificador
Campo del cuerpo Tipo
nombre string tel string email string idioma string alergias string notas string etiquetas array nacimiento string marketing boolean
409 Teléfono ya usado.
curl JavaScript PHP Copiar
curl -X PATCH "https://profilereserve360.com/api/v1/clientes/12" \
-H "Authorization: Bearer $R360_CLAVE" \
-H "Content-Type: application/json" \
-d '{"alergias":"frutos secos"}' const r = await fetch("https://profilereserve360.com/api/v1/clientes/12", {
method: "PATCH",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`, "Content-Type": "application/json"},
body: JSON.stringify({"alergias":"frutos secos"})
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/clientes/12");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(array (
'alergias' => 'frutos secos',
))]);
$datos = json_decode(curl_exec($c), true);
Eventos
Cartelera, aforo y venta de entradas.
Permiso eventos.leer
Eventos publicados desde hoy (o desde desde). todos=1 incluye borradores y cancelados.
Parámetro Descripción
desde string Fecha mínimatodos integer Incluir no publicados
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/eventos" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/eventos", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/eventos");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
GET /eventos/{id} Un evento
Permiso eventos.leer
Con sus zonas, precios, capacidad y vendidas.
Parámetro Descripción
id en la ruta obligatorio Identificador
404 No existe.
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/eventos/12" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/eventos/12", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/eventos/12");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
GET /eventos/{id}/entradas Entradas vendidas
Permiso eventos.leer
Las entradas de un evento con su estado (reservada, pagada, validada en puerta, anulada).
Parámetro Descripción
id en la ruta obligatorio Identificadorlimite integer Cuántos por página (1–200, por defecto 50)cursor integer El valor «siguiente» de la página anterior
curl JavaScript PHP Copiar
curl "https://profilereserve360.com/api/v1/eventos/12/entradas" \
-H "Authorization: Bearer $R360_CLAVE" const r = await fetch("https://profilereserve360.com/api/v1/eventos/12/entradas", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`}
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/eventos/12/entradas");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE')]]);
$datos = json_decode(curl_exec($c), true);
POST /eventos/{id}/pedidos Vender entradas
Permiso eventos.vender
Un pedido con una o varias líneas: {tipo:"pie", zona, cantidad} (entrada general), {tipo:"butaca", id} o {tipo:"mesa", id, pax}. El precio lo pone el servidor, nunca el que venga en la petición. Hoy el pago queda pendiente en el local; gestion_url es el enlace del comprador con sus QR.
Parámetro Descripción
id en la ruta obligatorio Identificador
409 Agotado u ocupado.
curl JavaScript PHP Copiar
curl -X POST "https://profilereserve360.com/api/v1/eventos/12/pedidos" \
-H "Authorization: Bearer $R360_CLAVE" \
-H "Content-Type: application/json" \
-d '{"items":[{"tipo":"pie","zona":5,"cantidad":2}],"cliente":{"nombre":"Ana Ejemplo","tel":"600000001"},"rgpd":true}' const r = await fetch("https://profilereserve360.com/api/v1/eventos/12/pedidos", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.R360_CLAVE}`, "Content-Type": "application/json"},
body: JSON.stringify({"items":[{"tipo":"pie","zona":5,"cantidad":2}],"cliente":{"nombre":"Ana Ejemplo","tel":"600000001"},"rgpd":true})
});
const datos = await r.json(); $c = curl_init("https://profilereserve360.com/api/v1/eventos/12/pedidos");
curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('R360_CLAVE'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(array (
'items' =>
array (
0 =>
array (
'tipo' => 'pie',
'zona' => 5,
'cantidad' => 2,
),
),
'cliente' =>
array (
'nombre' => 'Ana Ejemplo',
'tel' => '600000001',
),
'rgpd' => true,
))]);
$datos = json_decode(curl_exec($c), true);
Respuesta 201
{
"id": 77,
"localizador": "HE-9QX2",
"total": 30.0,
"estado": "reservado",
"pago": "en_local",
"gestion_url": "https://profilereserve360.com/r/hemingway?g=…",
"entradas": [
{
"codigo": "7K3Q-X9M2-PHAB",
"zona": "Pista",
"sitio": "Entrada general",
"precio": 15.0,
"estado": "reservada",
"pagado": false
}
]
}
Webhooks
Avisos a tu servidor cuando pasa algo, para no tener que preguntar cada minuto.
Da de alta la dirección (https) en Integraciones › API y webhooks y elige los eventos. Recibirás un POST con JSON como éste:
reserva.creada
{
"id": "evt_4f0c9a1be27d8c3f5a6b7e90",
"tipo": "reserva.creada",
"creado": "2026-10-20T10:02:11+02:00",
"negocio": {
"id": 1,
"slug": "habana-lounge",
"nombre": "La Habana Lounge"
},
"datos": {
"id": 1843,
"negocio_id": 1,
"estado": "confirmada",
"canal": "web",
"reserva": {
"id": 1843,
"codigo": "HL-7K3Q",
"fecha": "2026-10-24",
"hora": "21:30",
"pax": 4,
"estado": "confirmada",
"…": "…"
}
}
}
Eventos
reserva.creadaReserva creada
reserva.editadaReserva modificada
reserva.estadoCambio de estado (confirmada, llegada, no-show, cancelada…)
cliente.creadoCliente nuevo
pago.cobradoPago cobrado
pago.fallidoPago fallido
pago.devueltoPago devuelto
com.respuestaRespuesta de un cliente (WhatsApp/SMS)
pingLa prueba del botón «Probar»
Con «Todos» (*) recibes también los eventos que añadan otros módulos. Si el evento va de una reserva, datos.reserva la trae entera (la misma forma que GET /reservas/{id}).
Cabeceras
X-R360-Evento El tipo (reserva.creada…).
X-R360-Id Id del evento (evt_…), el mismo en todos los reintentos : guárdalo para no procesar dos veces el mismo aviso.
X-R360-Entrega · X-R360-Intento Número de entrega y de intento (1, 2, 3…).
X-R360-Firma t=<epoch>,v1=<hex>: ver abajo.
Reintentos
Contesta con un 2xx en menos de 8 segundos (procesa después, en segundo plano). Si no, se reintenta a los 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h. Las redirecciones no se siguen. Tras 10 avisos fallidos seguidos el webhook se desactiva solo y se avisa en el panel. Desde el registro del panel se puede ver cada intento y reenviar cualquier aviso.
Mientras el negocio no encienda «Salida real», los avisos quedan registrados como retenidos (con su carga y su firma) pero no se envían. Así se puede montar el receptor sin prisas.
Verificar la firma
Cada webhook tiene su secreto (whsec_…, en el panel). Calcula HMAC-SHA256(secreto, t + "." + cuerpo_crudo) y compáralo en tiempo constante con v1. Rechaza lo que tenga más de 5 minutos: así nadie puede reenviarte un aviso capturado.
PHP Node.js Python Copiar
$cuerpo = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_R360_FIRMA'] ?? ''), $f);
$esperada = hash_hmac('sha256', $f['t'] . '.' . $cuerpo, getenv('R360_WHSEC'));
if (!hash_equals($esperada, $f['v1'] ?? '') || abs(time() - (int)$f['t']) > 300) {
http_response_code(401); exit;
}
$aviso = json_decode($cuerpo, true); // $aviso['tipo'], $aviso['datos']['reserva']…
http_response_code(200);
import crypto from "node:crypto";
// Express: app.post("/webhooks/r360", express.raw({type: "application/json"}), (req, res) => {…})
const f = Object.fromEntries(req.get("X-R360-Firma").split(",").map(p => p.split("=")));
const esperada = crypto.createHmac("sha256", process.env.R360_WHSEC).update(f.t + "." + req.body).digest("hex");
const ok = f.v1 && crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(f.v1))
&& Math.abs(Date.now() / 1000 - Number(f.t)) < 300;
if (!ok) return res.sendStatus(401);
const aviso = JSON.parse(req.body);
res.sendStatus(200);
import hmac, hashlib, time, json, os
f = dict(p.split("=", 1) for p in request.headers["X-R360-Firma"].split(","))
cuerpo = request.get_data() # los bytes tal cual llegaron
esperada = hmac.new(os.environ["R360_WHSEC"].encode(), f["t"].encode() + b"." + cuerpo, hashlib.sha256).hexdigest()
if not hmac.compare_digest(esperada, f.get("v1", "")) or abs(time.time() - int(f["t"])) > 300:
abort(401)
aviso = json.loads(cuerpo)
Firma sobre el cuerpo crudo : si tu framework ya lo ha convertido a objeto y lo vuelves a serializar, los espacios cambian y la firma no cuadra.
Calendarios (ICS)
Desde Integraciones › Calendarios cada negocio, profesional o recurso tiene un enlace .ics (iCalendar) que Google Calendar, Outlook y Apple leen y refrescan solos: reservas de 30 días atrás a 6 meses adelante, con estado (confirmada / por confirmar / cancelada) y la hora real (la noche del viernes incluye la madrugada del sábado). El token del enlace es la llave: se revoca en un clic.
Origen de las reservas
Al crear una reserva por la API puedes mandar origen: {fuente, medio, campana}; si no, cuenta como fuente api. Las reservas de la web y el widget guardan solas sus UTM (utm_source, utm_medium, utm_campaign), el QR y la web de procedencia. Todo sale en reserva.origen y en el informe «Origen y embudo» del panel.