Developers
Developer documentation for the Tolvi API
Everything in Tolvi is available through a versioned REST API. The Tolvi web app uses the same API, so there are no private endpoints. This page covers access, limits, webhooks and versioning. The reference lists every operation.
Last updated
What are the Tolvi API and the Tolvi MCP server?
The Tolvi API is a REST API over HTTPS with JSON bodies, version 1, at /api/v1. It has 606 operations covering the work queue, clients, engagements, trial balances, journals, lead schedules, review points, workflow, tax, fixed assets, sync health, Tolvi Meetings (transcripts and their write-ups), Tolvi AI, settings and webhooks. Its OpenAPI 3.1 specification is generated from the API's own code, so the reference can't drift from what the API does.
The Tolvi MCP server lets an AI assistant read your work queue, engagements, review points, meeting write-ups and your clients' Tolvi Books, and raise review points once you confirm. Every tool calls the same API with the same permissions. Connecting an assistant is coming next: the MCP page lists the tools and what's left to do.
Tolvi is in early access, so API credentials come with an early access account. Email us if you want to build against Tolvi.
Base URLs
Use https://api.tolvi.co.uk/api/v1 for your own scripts and integrations. Every request and response uses UTF-8 JSON unless the reference says otherwise.
| What | URL | Use it for |
|---|---|---|
| API | https://api.tolvi.co.uk/api/v1 | Your scripts, integrations and apps. |
| The app's own API | https://app.tolvi.co.uk/api/v1 | The same API under the web app's address, for the Tolvi web app itself. Don't build against it. |
| MCP server | https://mcp.tolvi.co.uk/mcp | Coming AI assistants, once MCP sign-in is set up. Connecting is coming next. |
| OpenAPI specification | tolvi.co.uk/openapi.json | Generating a client or importing into your API tools. The API also serves it at /api/v1/openapi.json. |
How do I authenticate?
Send Authorization: Bearer <token> on every request. Tolvi accepts three kinds of token. Each one acts with the permissions of a person or an API client in your firm, never more. If you belong to several firms, choose one with the x-firm-id header.
| Token | For | How you get it |
|---|---|---|
Personal access tokentwp_… | Scripts and tools acting as you. | Create it in Settings → API credentials. Choose its scopes and expiry (90 days by default, up to 366). Tolvi shows it once and stores only a hash. |
Client credentialstwat_… | Server-to-server integrations, such as a practice management system. | A firm admin creates an API client in Settings → API credentials, with scopes and a role ceiling. Exchange its id and secret at POST /api/v1/oauth/token for a token that lasts an hour. |
| OAuth 2.1 authorisation code with PKCE | Apps acting for a signed-in person. | Tolvi's identity provider issues the token. The API's /.well-known/oauth-protected-resource document names it and lists the scopes. To register an app, email support@tolvi.co.uk. |
A request with a personal access token
curl "https://api.tolvi.co.uk/api/v1/clients?limit=20" \
-H "Authorization: Bearer twp_…"Exchanging client credentials for a token
curl -X POST "https://api.tolvi.co.uk/api/v1/oauth/token" \
-u "<client id>:<client secret>" \
-d grant_type=client_credentials \
-d scope="work:read clients:read"The token endpoint answers with access_token, token_type, expires_in and scope, and its errors use the OAuth shape (error, error_description). Creating, listing and revoking credentials is only possible from a signed-in Tolvi session, never with an API token.
How do scopes and roles work together?
A token can do what both its scopes and its role allow. Tolvi works this out on every request: the effective permission is the role's permissions intersected with the token's scopes. A personal access token is capped by its owner's current role, so it loses access the moment their role changes. An API client is capped by the role ceiling a firm admin set.
Each operation in the reference lists the scopes it needs. A missing scope gets a 403 with WWW-Authenticate: Bearer error="insufficient_scope". Operations with no scope, such as creating credentials or signing up, are for a signed-in Tolvi session only.
| Scope | Allows |
|---|---|
work:read | Read the work queue |
clients:read | Read clients |
clients:write | Create and edit clients |
engagements:read | Read year-end files and working papers |
engagements:write | Prepare working papers, sign sections off and assign work |
review:write | Raise, clear and reopen review points; approve, return and reopen year-end files |
integrations:manage | Connect and disconnect clients’ bookkeeping software |
settings:read | Read firm settings and the team (names, emails, roles and capacity) |
settings:admin | Change everyday firm settings and see invitations (security, firm details, workflows, AI and people need a signed-in person) |
webhooks:manage | Manage webhook subscriptions and see their deliveries |
meetings:read | Read meetings: transcripts and their write-ups (summary, takeaways, next steps, open questions, draft follow-up email) |
meetings:write | Send note-takers to meetings and file them against clients |
client_books:read | See which businesses have linked their Tolvi Books to the firm, and the state of each link |
books:read | Read a business’s books in Tolvi Books (as the business, or as its linked accountant) |
books:write | Keep a business’s books: categorise, reconcile, invoices, receipts and journals (an accountant also needs the link’s access) |
books:admin | Change a business’s Tolvi Books set-up: its details, rules that post without asking, connections and who has access (some of it needs a signed-in person) |
self_assessment:read | Read clients’ self assessment returns and calculations, without their National Insurance numbers or dates of birth |
self_assessment:write | Prepare clients’ self assessment returns. Approving and filing stay with signed-in people |
accounts:read | Read clients’ statutory year end accounts: figures, notes and PDFs |
accounts:write | Prepare clients’ statutory year end accounts. Approving them stays with signed-in people |
corporation_tax:read | Read clients’ Company Tax Returns: boxes, computations and PDFs |
corporation_tax:write | Prepare clients’ Company Tax Returns. Approving and filing them stay with signed-in people |
Sign-off and approval need a person: the API checks the same workflow guards as the app, and the MCP server never offers them.
What are the rate limits?
Each API credential or connected app in a firm (a personal access token, an API client, an app signed in with OAuth or the MCP server) gets a fixed number of requests a minute, set by the firm's plan. People using the Tolvi web app have limits of their own, so an integration can't use up anyone's allowance and the other way round: each person gets 600 requests a minute, and the whole firm a higher cap across everyone signed in, which grows with the plan.
| Plans | Tier | Requests a minute, per credential or app | Requests a minute, whole firm in the web app |
|---|---|---|---|
| Starter, Solo, Practice, Growth, Firm, Scale | Standard | 600 | 3,000 |
| Group, Group Plus | Higher | 3,000 | 6,000 |
| Enterprise | Agreed per firm | 10,000 | 15,000 |
Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy, for the limit closest to running out. Over a limit, the API answers 429 with a Retry-After header, and the problem detail's rateLimitScope says which limit was hit: client (this credential or app), person, firm or ip.
Requests without a valid token are also limited per address: 600 a minute, and 20 a minute on the token, sign-up and invitation endpoints. Signed-in requests don't count towards the per-address limit, so an office sharing one address isn't slowed down together. See pricing for what each plan includes.
How do webhooks work?
Webhooks tell your systems when something happens in Tolvi. A firm admin subscribes an HTTPS URL to event types in Settings → Webhooks, or through the API with the webhooks:manage scope. Each delivery is a signed POST with a JSON body. The body carries ids only, so fetch the full record through the API.
| Event type | When |
|---|---|
engagement.stage_changed | An engagement moved stage: submitted, approved, returned for amendments or reopened. |
section.signed_off | A section (W/P ref) was signed off by its preparer or a reviewer. |
review_point.raised | A review point was raised on a section or the engagement. |
review_point.cleared | A review point was cleared. |
tb.imported | A new trial balance version was imported. |
sync.change_detected | The bookkeeping software changed after sign-off: the figures in Tolvi are out of date. |
self_assessment.filed | HMRC accepted a client’s self assessment return, filed live. |
accounts.approved | People recorded the directors’ approval of a client’s statutory accounts. |
corporation_tax.filed | HMRC accepted a company’s Company Tax Return, filed live. |
A delivery body
{
"id": "6b0c…",
"type": "review_point.raised",
"apiVersion": "v1",
"createdAt": "2026-10-18T09:12:44.120Z",
"firmId": "3e57…",
"data": { "engagementId": "…", "clientId": "…", "reviewPointId": "…", "sectionCode": "C", "category": "query", "raisedBy": "…" }
}Each delivery has the headers Tolvi-Signature, Tolvi-Event-Id, Tolvi-Event-Type and Tolvi-Delivery-Id. The event id stays the same on every retry and replay.
How to verify the signature
Tolvi-Signature is t=<unix seconds>,v1=<hex>. The v1 value is HMAC-SHA256, keyed with the subscription's whsec_… secret, of <t>.<raw body>. To accept a delivery:
- Compute the HMAC over the raw body, before parsing the JSON.
- Compare it with each
v1value in constant time. - Reject the delivery if
tis more than 5 minutes from your clock. - Ignore an event id you have already processed.
Node.js
import crypto from 'node:crypto';
// header: the Tolvi-Signature value. rawBody: the request body exactly as received.
export function verifyTolvi(secret, header, rawBody, toleranceSeconds = 300) {
const parts = header.split(',').map((p) => p.split('='));
const t = Number(parts.find(([k]) => k === 't')?.[1]);
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}Python
import hashlib, hmac, time
def verify_tolvi(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
items = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in items if k == "t"), None)
sigs = [v for k, v in items if k == "v1"]
if not t or not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)Retries and the delivery log
- A delivery succeeds when your endpoint answers 2xx within 10 seconds. Redirects aren't followed and count as a failure.
- A failed attempt is retried after 30 seconds, then the wait doubles up to 6 hours. After 12 attempts the delivery is marked failed.
- After 20 failed attempts in a row, the subscription is disabled and the firm's admins are told. Re-enable it in Settings → Webhooks or with
PATCH /api/v1/webhooks/{id}. - Every attempt is logged with its status code. Any delivery can be replayed from the log.
- Destinations must be HTTPS URLs on public addresses. Private, loopback and link-local addresses are refused when the URL is saved and again on every connection.
- Order isn't guaranteed. Use
createdAt, and fetch the current state from the API when order matters.
How is the API versioned?
The version is in the path: /api/v1. Changes within version 1 are additive only, such as new endpoints, new optional fields and new event types. Your code should ignore fields it doesn't know.
Removing or renaming anything is a breaking change. Before anything is removed, the affected operations carry deprecation headers for six months' notice. Every change to the API is checked by an automated breaking-change test against the current specification, which fails the build if version 1 would break.
Where do I get help with the API?
Email support@tolvi.co.uk. You'll be emailing the people who build Tolvi. Include the request path, the time and the response's instance and detail, but never your token or secret. To report a security issue, put "Security" in the subject line.