Primeros pasos
La API de Wolovy te deja leer tus clientes, proyectos y horas desde otros sistemas, mantener el bloque de mantenimiento de cada proyecto y publicar informes en el Portal del Cliente. Funciona con una API key de empresa.
- Crea una API key. En Wolovy, entra en Herramientas de empresa (
/company/tools) y abre la pestaña API keys. Solo pueden hacerlo los rolessuper_adminyadmin. - Envíala en cada petición con la cabecera
X-Company-Token. - Llama a la API desde tu servidor, no desde el navegador: la API no acepta peticiones de orígenes web que no estén autorizados.
curl "https://app.wolovy.com/api/clients?companyId=<COMPANY_ID>&page=1&pageSize=20" \
-H "X-Company-Token: <TOKEN>"
Autenticación
Cada empresa puede tener hasta 20 API keys, cada una con su etiqueta para saber qué aplicación la usa. Todas son válidas a la vez y se revocan por separado. Regenerar o revocar una key invalida su valor anterior al momento.
- La key va siempre en la cabecera
X-Company-Token: <TOKEN>. Authorization: Beareres para las sesiones de usuario y no sirve para las API keys.- Cada key pertenece a una empresa. Si envías un
companyId(en la query) o una cabeceraX-Company-Idde otra empresa, la respuesta es403 api_token_company_denied.
Trata cada key como una contraseña: una por integración, guardada en el servidor y nunca en el código de una web o una app.
Qué puede hacer una key
Las API keys son de solo lectura: aceptan GET, HEAD y OPTIONS. Solo hay cinco escrituras permitidas:
| Método | Ruta | Para qué |
|---|---|---|
PATCH |
/projects/{id}/maintenance |
Mantener el bloque de mantenimiento de un proyecto |
POST |
/client-portals/{id}/entries |
Crear un informe en un Portal del Cliente |
PATCH |
/client-portals/{id}/entries/reorder |
Reordenar los informes |
PATCH |
/client-portals/{id}/entries/{entryId} |
Editar un informe |
POST |
/client-portals/{id}/entries/{entryId}/publish |
Publicar un informe |
Cualquier otra escritura responde 403 api_token_read_only.
Tampoco están disponibles con una key, ni para leer: Project Management (/pm), Tickets, Salud económica, la gestión de API keys y las lecturas del Portal del Cliente. Todo eso necesita una sesión de usuario.
Convenciones
- URL base:
https://app.wolovy.com/api - Formato: JSON. En las escrituras, envía
Content-Type: application/json. - Fechas: ISO 8601.
YYYY-MM-DDen los filtros yYYYY-MM-DDTHH:mm:ss.sssZen las marcas de tiempo. - Paginación:
pageypageSize. Las listas paginadas devuelven{ total, items, page, pageSize }. - Parámetros: envía solo los que están documentados.
Límite de peticiones
Hasta 200 peticiones por minuto por dirección IP. Cada respuesta lleva x-ratelimit-limit, x-ratelimit-remaining y x-ratelimit-reset. Si te pasas, la respuesta es 429 con una cabecera retry-after que indica cuándo volver a intentarlo.
Errores
Los errores de la aplicación llegan como { "message": "…", "code": "…" } (code no siempre está). Los del servidor, como el 429 o un JSON mal formado, llegan como { "statusCode", "error", "message" }.
| Código | Cuándo |
|---|---|
400 |
Cuerpo o parámetros no válidos. Por ejemplo, api_token_user_id_required. |
401 |
Falta la key o no es válida. |
403 |
api_token_read_only, api_token_company_denied, api_token_not_allowed o api_token_user_denied. |
404 |
El recurso no existe. |
413 |
El HTML de un informe pasa de 512 KB. |
422 |
Un componente de un informe no es válido. La respuesta incluye issues. |
429 |
Límite de peticiones superado. |
Clientes
Listar clientes
GET /clients?companyId={uuid}&page=1&pageSize=20&search=
Lista paginada de los clientes de la empresa. Respuesta: { total, items, page, pageSize }.
curl "https://app.wolovy.com/api/clients?companyId=<COMPANY_ID>&search=brava" \
-H "X-Company-Token: <TOKEN>"
Ver un cliente
GET /clients/{id}
Detalle del cliente, con sus notas.
Proyectos
Listar proyectos
GET /projects?companyId={uuid}&page=1&pageSize=20&search=&status=&priority=&clientId=&maintenance=&dateRange=&from=&to=
Lista y filtra los proyectos de la empresa.
Ver un proyecto
GET /projects/{id}
Detalle completo: cliente, asignaciones, tiempos y subproyectos. Incluye durationBreakdown: { direct, subtasks, projectManagement } con las horas imputadas directamente al proyecto, a sus trabajos (subtasks en la API) y a sus tareas de Project Management. totalDurationSeconds las suma todas.
Subproyectos
GET /projects/{id}/subprojects
Actualizar el mantenimiento
PATCH /projects/{id}/maintenance
Actualiza solo el bloque de mantenimiento del proyecto. Es una de las cinco escrituras permitidas con API key. Acepta un cuerpo parcial con cualquiera de estos campos: maintenance (booleano), maintenanceService y un campo por mes, de maintenanceJanuary a maintenanceDecember.
- Solo se escriben las claves que envías; las demás se quedan como estaban.
nullo una cadena vacía borran el contenido de ese campo.- Cualquier otro campo del proyecto, o un cuerpo vacío, responde
400. - Respuesta:
{ applied: string[], serverValues: {…}, updatedAt }.
Este ejemplo solo toca enero y marzo; los otros diez meses y maintenanceService se quedan igual:
curl -X PATCH "https://app.wolovy.com/api/projects/<PROJECT_ID>/maintenance" \
-H "Content-Type: application/json" \
-H "X-Company-Token: <TOKEN>" \
-d '{"maintenanceJanuary":"Revisión anual\n12 - Cambio de filtros","maintenanceMarch":null}'
Y este marca un proyecto como de mantenimiento:
curl -X PATCH "https://app.wolovy.com/api/projects/<PROJECT_ID>/maintenance" \
-H "Content-Type: application/json" \
-H "X-Company-Token: <TOKEN>" \
-d '{"maintenance":true,"maintenanceService":"Contrato anual de soporte"}'
Tiempo
GET /time/my?userId={uuid}&date=YYYY-MM-DD
GET /time/my?userId={uuid}&from=YYYY-MM-DD&to=YYYY-MM-DD
La línea de tiempo de una persona, de un día o de un rango de fechas. Con una API key, userId es obligatorio (si falta: 400 api_token_user_id_required) y tiene que ser de la empresa de la key (si no: 403 api_token_user_denied). Puedes sacar los userId de las asignaciones que devuelve GET /projects/{id}.
Estadísticas
GET /stats/dedication?companyId={uuid}&month=1-12&year=YYYY
Devuelve un summary con las horas totales, las personas y los proyectos del mes indicado. companyId, month y year son obligatorios.
curl "https://app.wolovy.com/api/stats/dedication?companyId=<COMPANY_ID>&month=9&year=2026" \
-H "X-Company-Token: <TOKEN>"
Portal del Cliente
Con una API key puedes crear, editar, ordenar y publicar informes en el Portal del Cliente. No puedes leer los portales: el identificador del portal lo copias de Wolovy, de la dirección de su ficha (/client-portals/{id}).
| Método | Ruta | Cuerpo |
|---|---|---|
POST |
/client-portals/{id}/entries |
{ title, html?, publish? } |
PATCH |
/client-portals/{id}/entries/reorder |
{ orderedIds: [uuid] } |
PATCH |
/client-portals/{id}/entries/{entryId} |
{ title?, html?, status? } |
POST |
/client-portals/{id}/entries/{entryId}/publish |
{ note? } |
POST /entries responde { entry, report, componentCount }. Guarda el entry.id: es el que usan los demás endpoints.
Un informe creado por API queda siempre en borrador, salvo que envíes publish: true o llames al endpoint de publicación. Cada versión creada así queda marcada como creada por API.
curl -X POST "https://app.wolovy.com/api/client-portals/<PORTAL_ID>/entries" \
-H "Content-Type: application/json" \
-H "X-Company-Token: <TOKEN>" \
-d '{"title":"Septiembre 2026","html":"<h1>Septiembre</h1><p>Este mes hemos publicado la nueva web.</p>"}'
HTML permitido
El HTML se limpia al guardarlo y solo se sirve la copia limpia. Además, se muestra dentro de un iframe aislado, sin cookies, sin almacenamiento y sin red.
- Etiquetas permitidas:
p br hr h1…h6 strong b em i u s small sub sup mark code pre blockquote abbr time del ins ul ol li dl dt dd table thead tbody tfoot tr th td caption colgroup col figure figcaption img a div span section article header footer main aside. - Se eliminan con su contenido:
script style noscript template textarea option title iframe object embed applet param base link meta form input button select label svg math canvas audio video source track slot portal marquee frame frameset dialog details summary. - Atributos:
class id title lang dir role style aria-*. Las clases solo se conservan si empiezan porwv-. - Enlaces: solo
https:ymailto:. Siempre se abren en otra pestaña, conrel="noopener noreferrer nofollow ugc". - Imágenes: solo
data:image/…o imágenes subidas al propio portal desde Wolovy. Las imágenes externashttps://se eliminan. - Estilos en línea: una lista cerrada de propiedades (color, fondo, tipografía, espaciado, bordes, flex y grid). Se rechaza cualquier valor con
url(,expression(,@import,\,<o/*. - Nunca se ejecuta JavaScript del contenido, ni atributos como
onclickuonload. - Límites: 512 KB por documento, 5.000 elementos, 64 niveles de anidamiento y 40 componentes.
Componentes
El contenido nunca ejecuta JavaScript, así que un gráfico se declara como un elemento vacío con un único atributo JSON, y Wolovy lo dibuja. Solo div, section y figure pueden llevar un componente.
<div data-chart='{"type":"bar","title":"Visitas","categories":["Jun","Jul","Ago","Sep"],
"series":[{"name":"Sesiones","data":[1200,1500,1800,2200]}]}'></div>
<div data-counter='{"label":"Conversiones","value":1234,"suffix":" uds"}'></div>
<div data-progress='{"label":"Avance","value":72}'></div>
<div data-kpi='{"columns":3,"items":[
{"label":"Visitas","value":18400},
{"label":"Conversiones","value":312},
{"label":"Ingresos","value":9450,"prefix":"EUR "}]}'></div>
<div data-comparison='{"aLabel":"2025","bLabel":"2026","rows":[
{"label":"Sesiones","a":14000,"b":18400},
{"label":"Rebote (%)","a":58,"b":47,"higherIsBetter":false}]}'></div>
El JSON se valida al guardar: una clave desconocida es un error 422, no se ignora, así que una errata sale como error y no como un gráfico vacío. Máximo 8 KB de JSON por componente y 40 componentes por informe.
data-chart
| Campo | Tipo | Notas |
|---|---|---|
type |
bar, stacked-bar, line, area o donut |
Obligatorio |
title |
Texto de hasta 80 caracteres | |
unit |
Texto de hasta 12 caracteres | Se muestra en los tooltips |
decimals |
De 0 a 4, por defecto 0 |
|
legend |
Booleano, por defecto true |
|
categories |
De 1 a 48 textos de hasta 32 caracteres | El eje X |
series |
De 1 a 6 { name, color?, data: number[] } |
Cada series[].data debe tener exactamente tantos valores como categories. Un donut admite una sola serie y su leyenda muestra las categorías.
data-counter
| Campo | Tipo | Notas |
|---|---|---|
label |
Texto de 1 a 48 caracteres | Obligatorio |
value |
Número | Obligatorio |
prefix |
Texto de hasta 4 caracteres | Respeta los espacios: "EUR " da EUR 9.450 |
suffix |
Texto de hasta 8 caracteres | Igual: " uds" da 128 uds |
decimals |
De 0 a 4, por defecto 0 |
|
animate |
Booleano, por defecto true |
Respeta la preferencia de movimiento reducido |
tone |
Ver colores |
data-progress
| Campo | Tipo | Notas |
|---|---|---|
value |
Número | Obligatorio |
max |
Número mayor que 0, por defecto 100 |
|
label |
Texto de hasta 48 caracteres | |
showValue |
Booleano, por defecto true |
Muestra el porcentaje |
tone |
Ver colores |
data-kpi
| Campo | Tipo | Notas |
|---|---|---|
columns |
2, 3 o 4 |
A partir de 640 px de ancho; por debajo, la rejilla se adapta |
items |
De 1 a 8 objetos con la forma de data-counter |
Obligatorio |
data-comparison
| Campo | Tipo | Notas |
|---|---|---|
title |
Texto de hasta 120 caracteres | |
aLabel / bLabel |
Texto de 1 a 24 caracteres | Obligatorios, encabezan las dos columnas |
format |
number, percent o currency, por defecto number |
|
currency |
Código de 3 letras | Solo con format: "currency" |
decimals |
De 0 a 4, por defecto 0 |
|
unit |
Texto de hasta 12 caracteres | |
higherIsBetter |
Booleano, por defecto true |
Se puede fijar fila a fila |
rows |
De 1 a 24 { label, a, b, higherIsBetter? } |
La columna Δ se pinta en verde cuando el cambio es a mejor y en rojo cuando es a peor. higherIsBetter se fija por fila porque un informe mezcla métricas donde subir es bueno (sesiones) con otras donde lo bueno es bajar (rebote, tiempo de carga).
Colores
tone y color solo aceptan primary, success, warning, danger, muted o series. Los colores reales salen de la configuración de la empresa, nunca del HTML: así todos los informes son coherentes y un contenido generado por IA no puede romper la identidad visual.
Informes generados con IA
- Copia el identificador del portal desde su ficha en Wolovy.
- Lee con la API los datos que necesite el informe:
GET /projects/{id},GET /time/my,GET /stats/dedication. - Crea el informe con
POST /client-portals/{id}/entries. Queda en borrador. - Alguien de la agencia lo revisa, lo publica y envía el aviso al cliente desde Wolovy. Si confías en tu proceso, también puedes publicarlo por API.
Nada se publica solo por haberlo generado una IA.
Incrustar el portal
Cada portal tiene también un enlace para incrustar: el dashboard y los informes, de solo lectura y sin tickets. Se genera en la ficha del portal, que solo funciona en los dominios que autorices.
<iframe src="https://app.wolovy.com/api/client-portal/e/<EMBED_TOKEN>"
style="width:100%;min-height:800px;border:0"
loading="lazy" referrerpolicy="no-referrer"></iframe>
Buenas prácticas
- Crea una key por integración, con una etiqueta que diga cuál es.
- Guárdala en el servidor. Nunca en el código de una web, de una app o de un repositorio.
- Si sospechas que se ha filtrado, regenérala y actualiza la integración.
- Envía siempre
companyIdexplícito, sobre todo si tu cuenta lleva varias empresas. - Respeta el límite de 200 peticiones por minuto y, ante un
429, espera lo que indiqueretry-after.