Documentation

Wolovy API

Read clients, projects and hours from your own systems and publish reports to the Client Portal with a company API key.

Base URL
https://app.wolovy.com/api
Authentication
X-Company-Token
Permissions
Read + 5 writes
Rate limit
200 requests per minute

Getting started

The Wolovy API lets you read your clients, projects and hours from other systems, keep each project’s maintenance block up to date and publish reports to the Client Portal. It works with a company API key.

  1. Create an API key. In Wolovy, open Company tools (/company/tools) and go to the API keys tab. Only the super_admin and admin roles can do this.
  2. Send it with every request in the X-Company-Token header.
  3. Call the API from your server, not from the browser: the API does not accept requests from web origins that haven’t been allowed.
curl "https://app.wolovy.com/api/clients?companyId=<COMPANY_ID>&page=1&pageSize=20" \
  -H "X-Company-Token: <TOKEN>"

Authentication

Each company can have up to 20 API keys, each with a label saying which application uses it. They are all valid at the same time and are revoked separately. Regenerating or revoking a key invalidates its previous value immediately.

  • The key always goes in the X-Company-Token: <TOKEN> header.
  • Authorization: Bearer is for user sessions and does not work with API keys.
  • Every key belongs to one company. If you send a companyId (in the query) or an X-Company-Id header for another company, the response is 403 api_token_company_denied.

Treat every key like a password: one per integration, stored on your server and never in the code of a website or an app.

What a key can do

API keys are read-only: they accept GET, HEAD and OPTIONS. There are only five allowed writes:

Method Path What for
PATCH /projects/{id}/maintenance Keep a project’s maintenance block up to date
POST /client-portals/{id}/entries Create a report in a Client Portal
PATCH /client-portals/{id}/entries/reorder Reorder the reports
PATCH /client-portals/{id}/entries/{entryId} Edit a report
POST /client-portals/{id}/entries/{entryId}/publish Publish a report

Any other write returns 403 api_token_read_only.

These are not available with a key either, not even for reading: Project Management (/pm), Tickets, Economic Health, API key management and Client Portal reads. They all need a user session.

Conventions

  • Base URL: https://app.wolovy.com/api
  • Format: JSON. For writes, send Content-Type: application/json.
  • Dates: ISO 8601. YYYY-MM-DD in filters and YYYY-MM-DDTHH:mm:ss.sssZ in timestamps.
  • Pagination: page and pageSize. Paginated lists return { total, items, page, pageSize }.
  • Parameters: only send the documented ones.

Rate limit

Up to 200 requests per minute per IP address. Every response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset. Go over it and the response is a 429 with a retry-after header telling you when to try again.

Errors

Application errors arrive as { "message": "…", "code": "…" } (code isn’t always present). Server errors, such as a 429 or malformed JSON, arrive as { "statusCode", "error", "message" }.

Code When
400 Invalid body or parameters. For example, api_token_user_id_required.
401 The key is missing or invalid.
403 api_token_read_only, api_token_company_denied, api_token_not_allowed or api_token_user_denied.
404 The resource doesn’t exist.
413 A report’s HTML is larger than 512 KB.
422 A report component isn’t valid. The response includes issues.
429 Rate limit exceeded.

Clients

List clients

GET /clients?companyId={uuid}&page=1&pageSize=20&search=

Paginated list of the company’s clients. Response: { total, items, page, pageSize }.

curl "https://app.wolovy.com/api/clients?companyId=<COMPANY_ID>&search=brava" \
  -H "X-Company-Token: <TOKEN>"

Get a client

GET /clients/{id}

The client’s detail, with its notes.

Projects

List projects

GET /projects?companyId={uuid}&page=1&pageSize=20&search=&status=&priority=&clientId=&maintenance=&dateRange=&from=&to=

Lists and filters the company’s projects.

Get a project

GET /projects/{id}

The full detail: client, assignments, times and subprojects. It includes durationBreakdown: { direct, subtasks, projectManagement } with the hours logged directly on the project, on its jobs (subtasks in the API) and on its Project Management tasks. totalDurationSeconds adds them all up.

Subprojects

GET /projects/{id}/subprojects

Update the maintenance block

PATCH /projects/{id}/maintenance

Updates only the project’s maintenance block. It is one of the five writes allowed with an API key. It takes a partial body with any of these fields: maintenance (boolean), maintenanceService and one field per month, from maintenanceJanuary to maintenanceDecember.

  • Only the keys you send are written; the rest stay as they were.
  • null or an empty string clears that field.
  • Any other project field, or an empty body, returns 400.
  • Response: { applied: string[], serverValues: {…}, updatedAt }.

This example only touches January and March; the other ten months and maintenanceService stay the same:

curl -X PATCH "https://app.wolovy.com/api/projects/<PROJECT_ID>/maintenance" \
  -H "Content-Type: application/json" \
  -H "X-Company-Token: <TOKEN>" \
  -d '{"maintenanceJanuary":"Annual review\n12 - Filter change","maintenanceMarch":null}'

And this one marks a project as a maintenance project:

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":"Annual support contract"}'

Time

GET /time/my?userId={uuid}&date=YYYY-MM-DD
GET /time/my?userId={uuid}&from=YYYY-MM-DD&to=YYYY-MM-DD

A person’s timeline, for one day or a date range. With an API key, userId is required (if missing: 400 api_token_user_id_required) and must belong to the key’s company (if not: 403 api_token_user_denied). You can get the userId values from the assignments returned by GET /projects/{id}.

Statistics

GET /stats/dedication?companyId={uuid}&month=1-12&year=YYYY

Returns a summary with the total hours, the people and the projects for that month. companyId, month and year are required.

curl "https://app.wolovy.com/api/stats/dedication?companyId=<COMPANY_ID>&month=9&year=2026" \
  -H "X-Company-Token: <TOKEN>"

Client Portal

With an API key you can create, edit, reorder and publish reports in the Client Portal. You can’t read portals: copy the portal’s ID from Wolovy, from the address of its page (/client-portals/{id}).

Method Path Body
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 returns { entry, report, componentCount }. Keep entry.id: the other endpoints use it.

A report created through the API always stays a draft unless you send publish: true or call the publish endpoint. Every version created this way is labelled as created by the 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":"September 2026","html":"<h1>September</h1><p>This month we launched the new website.</p>"}'

Allowed HTML

HTML is cleaned when it’s saved and only the clean copy is served. It is also shown inside an isolated iframe, with no cookies, no storage and no network.

  • Allowed tags: 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.
  • Removed with their content: 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.
  • Attributes: class id title lang dir role style aria-*. Classes are only kept if they start with wv-.
  • Links: only https: and mailto:. They always open in a new tab, with rel="noopener noreferrer nofollow ugc".
  • Images: only data:image/… or images uploaded to the portal itself from Wolovy. External https:// images are removed.
  • Inline styles: a closed list of properties (colour, background, typography, spacing, borders, flex and grid). Any value containing url(, expression(, @import, \, < or /* is rejected.
  • JavaScript from the content never runs, and neither do attributes such as onclick or onload.
  • Limits: 512 KB per document, 5,000 elements, 64 levels of nesting and 40 components.

Components

Content never runs JavaScript, so a chart is declared as an empty element with a single JSON attribute, and Wolovy draws it. Only div, section and figure can carry a component.

<div data-chart='{"type":"bar","title":"Visits","categories":["Jun","Jul","Aug","Sep"],
  "series":[{"name":"Sessions","data":[1200,1500,1800,2200]}]}'></div>

<div data-counter='{"label":"Conversions","value":1234,"suffix":" units"}'></div>

<div data-progress='{"label":"Progress","value":72}'></div>

<div data-kpi='{"columns":3,"items":[
  {"label":"Visits","value":18400},
  {"label":"Conversions","value":312},
  {"label":"Revenue","value":9450,"prefix":"EUR "}]}'></div>

<div data-comparison='{"aLabel":"2025","bLabel":"2026","rows":[
  {"label":"Sessions","a":14000,"b":18400},
  {"label":"Bounce (%)","a":58,"b":47,"higherIsBetter":false}]}'></div>

The JSON is validated on save: an unknown key is a 422 error, not ignored, so a typo shows up as an error instead of an empty chart. At most 8 KB of JSON per component and 40 components per report.

data-chart

Field Type Notes
type bar, stacked-bar, line, area or donut Required
title Text, up to 80 characters
unit Text, up to 12 characters Shown in the tooltips
decimals 0 to 4, default 0
legend Boolean, default true
categories 1 to 48 strings of up to 32 characters The X axis
series 1 to 6 { name, color?, data: number[] }

Every series[].data must have exactly as many values as categories. A donut takes a single series and its legend shows the categories.

data-counter

Field Type Notes
label Text, 1 to 48 characters Required
value Number Required
prefix Text, up to 4 characters Spaces are kept: "EUR " gives EUR 9,450
suffix Text, up to 8 characters Same: " units" gives 128 units
decimals 0 to 4, default 0
animate Boolean, default true Respects the reduced-motion preference
tone See colours

data-progress

Field Type Notes
value Number Required
max Number above 0, default 100
label Text, up to 48 characters
showValue Boolean, default true Shows the percentage
tone See colours

data-kpi

Field Type Notes
columns 2, 3 or 4 From 640 px wide; below that the grid adapts
items 1 to 8 objects shaped like data-counter Required

data-comparison

Field Type Notes
title Text, up to 120 characters
aLabel / bLabel Text, 1 to 24 characters Required, they head the two columns
format number, percent or currency, default number
currency 3-letter code Only with format: "currency"
decimals 0 to 4, default 0
unit Text, up to 12 characters
higherIsBetter Boolean, default true Can be set row by row
rows 1 to 24 { label, a, b, higherIsBetter? }

The Δ column turns green when the change is for the better and red when it’s for the worse. higherIsBetter is set per row because a report mixes metrics where going up is good (sessions) with others where going down is (bounce rate, load time).

Colours

tone and color only accept primary, success, warning, danger, muted or series. The actual colours come from the company’s settings, never from the HTML: that keeps every report consistent, and AI-generated content can’t break the visual identity.

AI-generated reports

  1. Copy the portal’s ID from its page in Wolovy.
  2. Read whatever data the report needs through the API: GET /projects/{id}, GET /time/my, GET /stats/dedication.
  3. Create the report with POST /client-portals/{id}/entries. It stays a draft.
  4. Someone at the agency reviews it, publishes it and sends the notice to the client from Wolovy. If you trust your pipeline, you can also publish through the API.

Nothing gets published just because an AI wrote it.

Embedding the portal

Every portal also has an embed link: dashboard and reports, read-only and without tickets. It is generated on the portal’s page and only works on the domains you allow.

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

Good practice

  • Create one key per integration, with a label saying which one it is.
  • Keep it on your server. Never in the code of a website, an app or a repository.
  • If you think it has leaked, regenerate it and update the integration.
  • Always send an explicit companyId, especially if your account runs several companies.
  • Stay within 200 requests per minute and, after a 429, wait for as long as retry-after says.

No account yet? Try it with your team.

Get started

30 days, no card. Then €6 per person per month + VAT.