What you can send
Each partner is approved for one or more scopes. A link to a ward carries the scopes you were approved for, and every request is checked against them.
| Scope | Endpoint | What the family sees |
|---|---|---|
appointments:write | POST /appointments | A read-only event in the calendar of everyone in the family, with a reminder an hour before. |
measurements:write | POST /measurements | Weight, height or temperature on the ward's timeline and health charts. |
activities:write | POST /activities | A timeline entry of any kind: care, health, learning, play, a milestone, an observation. |
The family gets one notification per batch you send, labelled with your name. School staff never see what partners send; it stays with the family.
How linking works
A family member decides which ward you can send data for. There is no way to link to a ward without them.
- A family member makes a code in ScolahIn the app, under Connected apps, they pick your organisation and the ward. Scolah shows a code like
K7QM-3XPD. It works once, for you only, for 10 minutes. - They give you the codeRead out at reception, typed into your app, or entered on your website: whatever suits your service.
- You swap it for tokensYour server sends the code with your client credentials to the token endpoint and gets an access token and a refresh token for that one ward.
- You send dataUse the access token on the data endpoints. Refresh it when it expires, after an hour.
- The family can unlink youFrom the same screen, at any time. Your next request gets
401 invalid_token; stop sending and delete the tokens for that link.
Linking the same ward again replaces the earlier link, so you only ever hold one live link per ward. The token response tells you its link_id; keep it alongside your own patient or customer record.
Quick start
All endpoints live under one base URL. We give you test credentials for our test environment first, and live credentials once your integration works.
| Environment | Base URL |
|---|---|
| Live | https://api.scolah.com/api/v1/core/partner-api |
| Test | https://api.uat.scolah.com/api/v1/core/partner-api |
1. Swap the family member's code for tokens
curl -X POST https://api.scolah.com/api/v1/core/partner-api/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=link_code \
-d code=K7QM-3XPD
{
"access_token": "Hk3v…Q9wA",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "pX7d…2mLc",
"scope": "appointments:write measurements:write",
"link_id": "5b0f6a7e-1c2d-4e3f-9a8b-7c6d5e4f3a2b"
}
2. Send a measurement
curl -X POST https://api.scolah.com/api/v1/core/partner-api/measurements \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "externalId": "visit-8812-weight", "type": "WEIGHT",
"value": 14.2, "unit": "kg", "measuredAt": "2026-10-13T09:40:00+00:00" }
]
}'
{
"accepted": 1,
"rejected": 0,
"results": [ { "externalId": "visit-8812-weight", "status": "ACCEPTED", "error": null } ]
}
The weight appears on the ward's timeline and health chart within seconds.
Tokens
A standard OAuth 2.0 token endpoint. Send the body as application/x-www-form-urlencoded and your client credentials by HTTP Basic. If your client cannot send Basic auth, put client_id and client_secret in the form instead.
| grant_type | Also send | Use it to |
|---|---|---|
link_code | code | Turn a family member's code into a new link and its first tokens. |
refresh_token | refresh_token | Get a new access token for a link you already hold. |
- Access tokens last one hour. Send them as
Authorization: Bearer …. - Refresh tokens rotate: every refresh returns a new access token and a new refresh token, and both old ones stop working at once. Store the new refresh token before you use the access token.
- Each link has one live token pair. If several workers send for the same link, share one token store and refresh in one place.
- Tokens are opaque strings. Don't parse them, and keep them server-side, as you would a password.
- Your client secret is shown to you once. If it leaks, email partners@scolah.com at once: we suspend the credentials, which stops every token issued with them.
Tells you which link a token belongs to and what it allows. Useful as a health check. It never returns anything about the ward.
{ "linkId": "5b0f6a7e-…", "partnerId": "0e9d…", "scope": "appointments:write measurements:write" }
Appointments
Up to 100 appointments per request. Each one becomes a read-only event for everyone in the ward's family, with a reminder an hour before. Send the same externalId again to change an appointment, or with "status": "CANCELLED" to remove it.
| Field | Rules |
|---|---|
externalId | Required. Your own id for the appointment, up to 255 characters. |
title | Required unless cancelling. Shown to the family as is, so write it for them: "Vaccination at Ridge Clinic". Longer than 100 characters is shortened with "…". |
startAt | Required unless cancelling. ISO 8601 with an offset. From 30 days ago to 2 years ahead. |
endAt | Optional. After startAt. |
timeZone | Optional. An IANA zone such as Africa/Accra. |
location, notes | Optional. Shown on the event; the location opens in maps. Shortened after 255 and 2,000 characters. |
status | CONFIRMED (the default) or CANCELLED. |
{
"items": [
{ "externalId": "appt-20931", "title": "Vaccination at Ridge Clinic",
"startAt": "2026-11-04T10:30:00+00:00", "endAt": "2026-11-04T11:00:00+00:00",
"timeZone": "Africa/Accra", "location": "Ridge Clinic, Accra" }
]
}
Each item's status is CREATED, UPDATED, CANCELLED or REJECTED.
Measurements
Up to 100 readings per request. Scolah converts units, checks that each value is plausible for a ward, and adds it to the timeline and health charts.
| type | Units | Accepted range |
|---|---|---|
WEIGHT | kg (default), g, lb | 0.3 to 200 kg |
HEIGHT | cm (default), m, in | 20 to 250 cm |
TEMPERATURE | C (default), F | 30 to 45 °C |
Every reading needs an externalId, a type, a value and a measuredAt from up to a year ago to no more than a day ahead. Sending the same externalId again corrects the reading instead of adding a second one.
Timeline entries
Up to 100 entries per request, for anything else worth a place on the ward's timeline: a check-up summary, a therapy session, a reading level, a first step.
| Field | Rules |
|---|---|
externalId | Required, up to 255 characters. The same id updates the entry. |
type | Required. CARE, HEALTH, LEARNING, PLAY, MILESTONE, OBSERVATION or CUSTOM. |
subtype | Optional. Lower-case letters, digits and underscores, such as doctor_visit. |
title | Required, up to 200 characters. |
description | Optional, up to 5,000 characters. |
occurredAt | Required. ISO 8601 with an offset, from up to a year ago to no more than a day ahead. |
data | Optional. Extra details as a JSON object, up to 16 KB. |
{
"items": [
{ "externalId": "session-4471", "type": "HEALTH", "subtype": "speech_therapy",
"title": "Speech therapy session",
"description": "Worked on 's' and 'sh' sounds. Practise the picture cards at home.",
"occurredAt": "2026-10-12T15:00:00+00:00", "data": { "durationMinutes": 45 } }
]
}
Responses
The data endpoints check each item on its own. One bad item never fails the batch: you get 200 with a result per item, in the order you sent them.
{
"accepted": 2,
"rejected": 1,
"results": [
{ "externalId": "r-1", "status": "ACCEPTED", "error": null },
{ "externalId": "r-2", "status": "ACCEPTED", "error": null },
{ "externalId": "r-3", "status": "REJECTED",
"error": "value is out of the plausible range for a ward" }
]
}
A rejected item is not stored. Fix it and send it again with the same externalId; resending accepted items is safe and never creates duplicates.
Errors
Token and authorisation errors use the OAuth 2.0 shape, so most client libraries read them as they are: {"error": "…", "error_description": "…"}. A malformed request gets a validation body instead: {"title": "Validation Failed", "status": 400, "errors": {…}}.
| Status | error | What to do |
|---|---|---|
| 400 | invalid_grant | The code is wrong, used or expired, or the refresh token was revoked or already used. Ask the family member for a new code. |
| 400 | unsupported_grant_type | Use link_code or refresh_token. |
| 400 | The request itself is malformed, for example an empty list or more than 100 items. | |
| 401 | invalid_client | Check your client id and secret. |
| 401 | invalid_token | The access token expired: refresh it. If refreshing fails with invalid_grant, the family unlinked you: stop sending for that ward and delete its tokens. |
| 403 | insufficient_scope | Your link doesn't allow this kind of data. The needed scope is in the WWW-Authenticate header. |
| 429 | too_many_requests | Over a rate limit: 30 token requests a minute per client id, 120 data requests a minute per access token. Wait for the Retry-After seconds. |
| 503 | temporarily_unavailable | Scolah could not take the batch just now. Send the whole batch again after Retry-After seconds (30 if there is none); items that did get through are updated, not duplicated. |
Good practice
- Batch your data. Send up to 100 items per request rather than one at a time.
- Use stable ids. Your
externalIdis how Scolah recognises an item. Never reuse one for something else. Ids are kept per ward, so the same id for two wards makes two separate items. - Retry safely. On a network error or a
503, send the same batch again. Accepted items are updated in place, not duplicated. - Write for families. Titles and descriptions appear in the family's app as you send them. Avoid internal codes and jargon.
- Respect unlinking. A revoked link is final. Don't ask the family to relink you just to keep sending.
- Send only what the family should keep. Everything you send becomes part of the ward's record, which their family can see, download and delete.
Scolah handles data about children, older adults and other wards. Partners must follow the Scolah terms and privacy policy, and the data protection law that applies to them.
Request access
We review every partner before issuing credentials. Tell us who you are and what you'd like to send, and we'll reply with test credentials or any questions, usually within a few working days.