Profile Reserve 360API para desarrolladores OpenAPI 3.1 (JSON) Crear una clave →

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 + JSONOpenAPI 3.1Claves por negocio con permisosWebhooks HMAC-SHA256Idempotency-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).

Cabecera
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)

AlcanceQué deja hacer
reservas.leerConsultar reservas
reservas.escribirCrear, modificar y cancelar reservas
disponibilidad.leerConsultar huecos libres
clientes.leerConsultar clientes
clientes.escribirCrear y modificar clientes
catalogo.leerConsultar sedes, servicios, profesionales y recursos
eventos.leerConsultar eventos y entradas
eventos.venderVender 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"
    }
}
HTTPCuándo
400 · 413 · 415JSON mal formado, cuerpo de más de 64 KB o sin Content-Type: application/json.
401Sin clave, clave errónea o revocada (sin_clave, clave_no_valida).
403La clave no tiene el permiso de esa ruta (sin_permiso).
404No existe en este negocio (tampoco se dice si existe en otro).
409No hay hueco (sin_hueco con motivo: completo · cerrado · cocina · turno · fuera_de_horario), choque con otra reserva, agotado.
422Un dato no es válido (dato_no_valido + campo).
429Límite de ritmo (limite_de_ritmo). Mira Retry-After.
500Fallo 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.

GET/Quién soy

Datos de la clave usada (prefijo, permisos, límites) y del negocio. Útil para comprobar la conexión.

curl "https://profilereserve360.com/api/v1/" \
  -H "Authorization: Bearer $R360_CLAVE"
Respuesta 200
{
    "negocio": {
        "id": 1,
        "slug": "habana-lounge",
        "nombre": "La Habana Lounge"
    },
    "clave": {
        "prefijo": "r360_ab12cd34",
        "alcances": [
            "reservas.leer"
        ]
    }
}
GET/negocioEl negocio
Permiso catalogo.leer

Datos públicos del negocio, su vocabulario (mesa, sillón, pista…) y sus horarios de reserva.

curl "https://profilereserve360.com/api/v1/negocio" \
  -H "Authorization: Bearer $R360_CLAVE"
GET/sedesSedes 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 "https://profilereserve360.com/api/v1/sedes" \
  -H "Authorization: Bearer $R360_CLAVE"

Servicios, profesionales y recursos (mesas, sillones, pistas…).

GET/serviciosServicios
Permiso catalogo.leer

Los servicios activos (añade todos=1 para ver también los inactivos).

ParámetroDescripción
todosinteger
Incluir inactivos
curl "https://profilereserve360.com/api/v1/servicios" \
  -H "Authorization: Bearer $R360_CLAVE"
GET/servicios/{id}Un servicio
Permiso catalogo.leer
ParámetroDescripción
iden la ruta obligatorio
Identificador

404 No existe.

curl "https://profilereserve360.com/api/v1/servicios/12" \
  -H "Authorization: Bearer $R360_CLAVE"
GET/profesionalesProfesionales
Permiso catalogo.leer

El equipo que atiende citas. servicios = qué servicios hace (null = todos).

ParámetroDescripción
todosinteger
Incluir inactivos
curl "https://profilereserve360.com/api/v1/profesionales" \
  -H "Authorization: Bearer $R360_CLAVE"
GET/profesionales/{id}Un profesional
Permiso catalogo.leer
ParámetroDescripción
iden la ruta obligatorio
Identificador

404 No existe.

curl "https://profilereserve360.com/api/v1/profesionales/12" \
  -H "Authorization: Bearer $R360_CLAVE"
GET/recursosRecursos
Permiso catalogo.leer

Lo reservable: mesas, reservados, sillones, cabinas, pistas… Filtra por clase.

ParámetroDescripción
clasestring
mesa · reservado · sillon · pista · cabina…
curl "https://profilereserve360.com/api/v1/recursos?clase=mesa" \
  -H "Authorization: Bearer $R360_CLAVE"
GET/recursos/{id}Un recurso
Permiso catalogo.leer
ParámetroDescripción
iden la ruta obligatorio
Identificador

404 No existe.

curl "https://profilereserve360.com/api/v1/recursos/12" \
  -H "Authorization: Bearer $R360_CLAVE"

Disponibilidad

Huecos libres con las mismas reglas que la web y el panel: horario, cierres, pacing y asignación de mesa o profesional.

GET/disponibilidadHuecos 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ámetroDescripción
fechastring obligatorio
Día de servicio
paxinteger
Personas (por defecto 2)
servicio_idinteger
Para citas: el servicio
profesional_idinteger
Con un profesional concreto
recurso_idinteger
Sólo si ese recurso concreto está libre
solo_libresinteger
1 = sólo las franjas libres

422 Parámetro no válido.

curl "https://profilereserve360.com/api/v1/disponibilidad?fecha=2026-10-24&pax=4&solo_libres=1" \
  -H "Authorization: Bearer $R360_CLAVE"
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/reservasListar reservas
Permiso reservas.leer

Paginado por cursor, en orden de creación. Para sincronizar, guarda el último actualizado y pide actualizado_desde.

ParámetroDescripción
desdestring
Fecha mínima (día de servicio)
hastastring
Fecha máxima
estadostring
Uno o varios separados por comas: pendiente, confirmada, llegada, sentada, terminada, no_show, cancelada
cliente_idinteger
De un cliente
actualizado_desdestring
ISO 8601 o epoch: sólo lo cambiado desde entonces
limiteinteger
Cuántos por página (1–200, por defecto 50)
cursorinteger
El valor «siguiente» de la página anterior
curl "https://profilereserve360.com/api/v1/reservas?estado=confirmada%2Cpendiente" \
  -H "Authorization: Bearer $R360_CLAVE"
POST/reservasCrear 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ámetroDescripción
Idempotency-Keycabecera
Hace segura la repetición de la petición durante 24 h
Campo del cuerpoTipo
fecha *string
hora *string
paxinteger
servicio_idinteger
profesional_idinteger
recurso_idinteger
cliente_idinteger
clienteClienteNuevo
notasstring
estadostring
confirmada · pendiente
canalstring
telefono · web · whatsapp · instagram · google · walkin · rrpp · qr · empleado · api
duracioninteger
minutos; por defecto la del servicio o la del tamaño de grupo
importenumber
origenobject
Atribución

409 No hay hueco o choca con otra. 422 Datos no válidos.

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"}}'
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ámetroDescripción
iden la ruta obligatorio
Id o código

404 No existe.

curl "https://profilereserve360.com/api/v1/reservas/HL-7K3Q" \
  -H "Authorization: Bearer $R360_CLAVE"
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ámetroDescripción
iden la ruta obligatorio
Id o código
Campo del cuerpoTipo
fechastring
horastring
paxinteger
servicio_idinteger | null
profesional_idinteger | null
recurso_idinteger | null
notasstring
estadostring
pendiente · confirmada · llegada · sentada · terminada · no_show · cancelada
importenumber
motivostring
Si se cancela

404 No existe. 409 Sin hueco.

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}'
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ámetroDescripción
iden la ruta obligatorio
Id o código

409 No se puede cancelar.

curl -X DELETE "https://profilereserve360.com/api/v1/reservas/HL-7K3Q" \
  -H "Authorization: Bearer $R360_CLAVE"
POST/reservas/{id}/cancelarCancelar (con motivo)
Permiso reservas.escribir

Cancelar una reserva con un motivo. Si ya estaba cancelada, devuelve la reserva tal cual (es idempotente).

ParámetroDescripción
iden la ruta obligatorio
Id o código
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"}'

Clientes

Fichas de clientes. Son de la cuenta: las comparten todas sus sedes.

GET/clientesBuscar clientes
Permiso clientes.leer

Por texto (q: nombre, correo o teléfono), por email exacto o por tel exacto.

ParámetroDescripción
qstring
Texto libre
emailstring
Correo exacto
telstring
Teléfono exacto
limiteinteger
Cuántos por página (1–200, por defecto 50)
cursorinteger
El valor «siguiente» de la página anterior
curl "https://profilereserve360.com/api/v1/clientes?q=ana" \
  -H "Authorization: Bearer $R360_CLAVE"
POST/clientesCrear (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 cuerpoTipo
nombrestring
telstring
emailstring
idiomastring
alergiasstring
notasstring
etiquetasarray
nacimientostring
marketingboolean

422 Datos no válidos.

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}'
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ámetroDescripción
iden la ruta obligatorio
Identificador

404 No existe.

curl "https://profilereserve360.com/api/v1/clientes/12" \
  -H "Authorization: Bearer $R360_CLAVE"
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ámetroDescripción
iden la ruta obligatorio
Identificador
Campo del cuerpoTipo
nombrestring
telstring
emailstring
idiomastring
alergiasstring
notasstring
etiquetasarray
nacimientostring
marketingboolean

409 Teléfono ya usado.

curl -X PATCH "https://profilereserve360.com/api/v1/clientes/12" \
  -H "Authorization: Bearer $R360_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{"alergias":"frutos secos"}'

Eventos

Cartelera, aforo y venta de entradas.

GET/eventosCartelera
Permiso eventos.leer

Eventos publicados desde hoy (o desde desde). todos=1 incluye borradores y cancelados.

ParámetroDescripción
desdestring
Fecha mínima
todosinteger
Incluir no publicados
curl "https://profilereserve360.com/api/v1/eventos" \
  -H "Authorization: Bearer $R360_CLAVE"
GET/eventos/{id}Un evento
Permiso eventos.leer

Con sus zonas, precios, capacidad y vendidas.

ParámetroDescripción
iden la ruta obligatorio
Identificador

404 No existe.

curl "https://profilereserve360.com/api/v1/eventos/12" \
  -H "Authorization: Bearer $R360_CLAVE"
GET/eventos/{id}/entradasEntradas vendidas
Permiso eventos.leer

Las entradas de un evento con su estado (reservada, pagada, validada en puerta, anulada).

ParámetroDescripción
iden la ruta obligatorio
Identificador
limiteinteger
Cuántos por página (1–200, por defecto 50)
cursorinteger
El valor «siguiente» de la página anterior
curl "https://profilereserve360.com/api/v1/eventos/12/entradas" \
  -H "Authorization: Bearer $R360_CLAVE"
POST/eventos/{id}/pedidosVender 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ámetroDescripción
iden la ruta obligatorio
Identificador

409 Agotado u ocupado.

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}'
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-EventoEl tipo (reserva.creada…).
X-R360-IdId del evento (evt_…), el mismo en todos los reintentos: guárdalo para no procesar dos veces el mismo aviso.
X-R360-Entrega · X-R360-IntentoNúmero de entrega y de intento (1, 2, 3…).
X-R360-Firmat=<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.

$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);
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.