Partner API
The Improval Partner API lets your system add CPD entries to a user's Improval learning diary and keep them in sync. Most partners have their own CPD tracker, learning modules or events, and use the API to send each completed learning event to Improval, where the user can reflect on it and use it in their appraisal.
Your system only ever sees the entries it created. It can't list or read anything else in the user's account.
Already integrated with FourteenFish?
The Improval Partner API is built to match the FourteenFish Partner API v2. It uses the same endpoints, the same entry fields, the same status codes and the same webhook. Code you've written for FourteenFish should work with Improval after these changes:
- Base URL:
https://app.improval.com/partnerapi/v1/ - Link URL:
https://app.improval.com/linkto/{integrationId} - One extra step when linking. We send the user back to you with a one-time
codeinstead of putting their API key in the URL. Your server swaps the code for theuserIdanduserApiKeywith one call (see Linking a user). This keeps keys out of browser history and server logs.
Everything else, including HTTP Basic authentication with userId and userApiKey, is the same. We've also added a few optional extras: credits on entries, ?updatedSince= on the entry list, GET /me and signed webhooks.
Getting started
Ask us to register your integration. We'll need:
- Your name and logo, shown to users when they link their account.
- Return URLs: where we send users back to after they've linked. They must be https (http://localhost is fine for testing).
- A webhook URL (optional, https) to hear about changes users make to your entries.
- A technical contact.
- Your organisation's Improval account. We'll set one up if you don't have one. The integration belongs to it, so you manage who can look after it, just like the rest of your team.
Anyone with system setup access at your organisation then opens app.improval.com/Integrations. There they accept the Partner API terms, find your integration ID, make your integration secret, and manage your return URLs, webhook URL and logo. The secret is shown once, only to the person who makes it, and we don't keep a copy, so nobody needs to email it. Keep it on your server: it's only used to swap link codes for users' keys. If it's ever exposed, make a new one there.
New integrations start in sandbox, where only Improval test accounts can link, so you can build and test against the real service. When you're ready, ask us and we'll make your integration live. Accepting the terms is all that's needed: you don't have to sign anything. If your organisation wants a signed copy, we can send the same terms as an agreement to sign in Improval.
Linking a user
Each user links their Improval account to your system once. You then store a userId and userApiKey for them, which you use for every request.
1. Send the user to Improval
https://app.improval.com/linkto/{integrationId}?state={your-value}
state is optional, and we send it back unchanged (use it to match the user up, and to protect against cross-site request forgery). If you've registered more than one return URL, add &returnUrl= with the one you want.
The user signs in to Improval (with two-factor authentication), or creates a free account, and confirms that your system can add entries to their learning diary.
2. We send them back with a code
https://example.com/account/improval?code=Xs9...&state={your-value}
If the user cancels, you get ?error=access_denied&state=… instead. A code works once, for 5 minutes.
3. Swap the code for the user's key
From your server, authenticated with HTTP Basic using your integration ID and secret:
POST https://app.improval.com/partnerapi/v1/link/token
Authorization: Basic base64(integrationId:integrationSecret)
Content-Type: application/json
{ "code": "Xs9..." }
200 OK
{ "userId": 12345, "userApiKey": "bjqy526symvnwkvd3aq6mkwdcdur92dk4n7p0qz1" }
Store both against the user. The key doesn't expire. If the user links again, they get a new key and the old one stops working.
Authenticating requests
Use HTTP Basic authentication: the username is the userId and the password is the userApiKey. In .NET with RestSharp:
client.Authenticator = new HttpBasicAuthenticator(userId, userApiKey);
Or with Axios in Node:
await axios.get(url, { auth: { username: userId, password: userApiKey } });
A 401 Unauthorized usually means the user has unlinked your system (in Improval under Settings > Connected apps). Ask them to link again. You can check a key with GET https://app.improval.com/partnerapi/v1/me.
Entries
An entry is a record of a learning event: a webinar, a module, an event, an article, a conversation. It's added to the user's learning diary as a CPD entry, with your system's name as its source.
Create an entry
POST https://app.improval.com/partnerapi/v1/entries
{
"title": "Influenza and Covid webinar",
"date": "2026-04-12",
"learning": "The webinar provided a great insight into planning flu clinics...",
"reflection": "It made me think about how we invite housebound patients...",
"duration": 90,
"sourceUrl": "https://www.example.com/webinars/covid-and-influenza"
}
| Field | Type | Description | |
|---|---|---|---|
title | string | Required | A short title, up to 255 characters. |
date | string | Required | The date of the learning event, yyyy-mm-dd. |
learning | string | Optional | What the user learned, or what was covered. |
reflection | string | Optional | The user's reflection on what they learned. |
duration | integer | Optional | Minutes spent. Recorded as CPD credits in hours (90 minutes = 1.5). |
sourceUrl | string | Optional | A link back to the learning, for online learning. |
credits | number | Optional | Improval only: CPD credits, when they aren't simply the hours spent. Overrides duration. |
Send whatever you have. A 200 OK response gives the full entry, the same as a GET. Store the entryId, so you can update the entry and match up webhooks.
The entry
{
"entryId": 4054148,
"title": "Influenza and Covid webinar",
"date": "2026-04-12",
"learning": "The webinar provided a great insight into planning flu clinics...",
"reflection": "It made me think about how we invite housebound patients...",
"duration": 90,
"hours": 1.5,
"credits": 1.5,
"sourceUrl": "https://www.example.com/webinars/covid-and-influenza",
"locked": false,
"createdAt": "2026-04-15T11:12:21Z",
"updatedAt": "2026-04-15T15:54:37Z",
"attachments": [
{
"attachmentId": 2507151,
"fileName": "Attendance certificate.pdf",
"sizeKb": 392,
"deepLink": "https://app.improval.com/Attachment/Download/2507151",
"attachmentUri": "https://app.improval.com/partnerapi/v1/entries/4054148/attachments/2507151"
}
],
"deepLink": "https://app.improval.com/Entries/View/4054148",
"entryUri": "https://app.improval.com/partnerapi/v1/entries/4054148"
}
deepLink opens the entry in Improval. A "View in Improval" link next to the entry in your system is a nice touch for users. hours and credits are the CPD credits recorded, and duration is the same in minutes.
Get, list, update and delete
GET /entries/{entryId} | One entry, with its attachments. |
GET /entries | Every entry your system created for this user: { "entries": [ … ] }, newest first, with attachments. Optional ?updatedSince=2026-04-15T00:00:00Z. |
PATCH /entries/{entryId} | Send only the fields that have changed. Returns the updated entry. |
DELETE /entries/{entryId} | Deletes the entry. 204 No Content. |
| Status | What it means |
|---|---|
400 Bad Request | Something in the request isn't right. detail says what. |
403 Forbidden | The entry belongs to another user, or your system didn't create it. |
404 Not Found | There's no entry with this ID. |
409 Conflict | The entry is locked and can't be changed (see below). |
410 Gone | The user has deleted the entry in Improval. Mark it as no longer synced, or delete it in your system. |
Locked entries
Once a user submits an entry for review, for example in their appraisal, it's locked: "locked": true, and any change you try returns 409 Conflict. You'll get a webhook when an entry becomes locked, so you can stop users editing it in your system too.
Attachments
To add an attendance certificate or any other file, POST it to the entry Base64-encoded:
POST https://app.improval.com/partnerapi/v1/entries/{entryId}/attachments
{ "fileName": "Attendance certificate.pdf", "base64data": "JVBERi0xLjMKJcTl8uXrp/Og0MTGCjMgMCBvYmoK..." }
Files can be up to 2MB: PDF, Word (.doc, .docx), text, or images (.jpg, .png, .gif). The response gives the attachment's attachmentId, fileName, sizeKb, deepLink and attachmentUri.
Attachments can't be changed. To replace one, delete it and add the new file:
DELETE https://app.improval.com/partnerapi/v1/entries/{entryId}/attachments/{attachmentId}
Webhooks
When a user changes an entry your system created (edits it, deletes it, adds or removes an attachment, or it becomes locked), we POST to your webhook URL:
{ "userId": 12345, "entryId": 4054148, "entryUri": "https://app.improval.com/partnerapi/v1/entries/4054148" }
Look up the user by userId and GET the entryUri for the latest details. A 410 Gone means the user deleted the entry. We don't send webhooks for changes your own system makes through the API.
Webhooks usually go out within seconds of a change. Reply with any 2xx status. Anything else, or no reply within 10 seconds, and we try again with increasing gaps for about a day.
Each webhook carries an X-Improval-Signature header: sha256= followed by the hex HMAC-SHA256 of the request body, using your webhook secret. Checking it is optional, but recommended:
var expected = "sha256=" + Convert.ToHexString(
HMACSHA256.HashData(Encoding.UTF8.GetBytes(webhookSecret), Encoding.UTF8.GetBytes(body))).ToLowerInvariant();
Errors and limits
- Errors come back as JSON problem details (RFC 7807), with the reason in
detail. - Up to 120 requests a minute per integration. Above that you'll get
429 Too Many Requestswith aRetry-Afterheader. - All requests must use https.
- This is version 1 of the Improval Partner API. We'll keep v1 backwards compatible, and announce any changes here first.
Using the API
When you use the Partner API, you agree to:
- only add entries the user has asked you to, about learning they took part in;
- keep users' keys and your integration secret secure, and tell us straight away if you think one has been exposed;
- have a privacy policy that explains how you share data with Improval;
- keep within the rate limits.
We can suspend an integration that puts users or the service at risk. Using the API means accepting the Partner API terms, which include how data is shared between us as independent controllers.
FourteenFish is a trademark of FourteenFish Ltd. Improval isn't affiliated with FourteenFish.
Want to integrate with Improval?
Tell us about your system and we'll set up a sandbox integration for you.