Endpoints & Data Models
Every end-point, with what you send and what you get back. Field-level data models are at the bottom of this page.
Base URL
https://scalinguptoolkit.com/api/v1
Each instance has its own base URL. Growth Institute uses the one above.
On this page
- Conventions — envelope, errors, pagination, gotchas
- Members · Groups · Campaigns · Reports · Samples · Settings
- Data models — every field, type and whether it is writable
- Enumerations — all allowed values
- Still to be documented
Conventions
Read this first — it applies to every end-point on this page.
| Topic | Rule |
|---|---|
| Auth | Authorization: Bearer <access_token> on every request. See Authentication. Tokens live max 1 hour and there is no refresh token. |
| Request format | JSON. Send Content-Type: application/json on any request with a body. (The token end-point is the exception — that one is form-urlencoded.) |
| Response envelope | Every JSON end-point returns JSend: { "status": "OK" | "ERROR", "data": … }. |
⚠️ Read status, not the HTTP code |
A business failure still comes back as HTTP 200 with status: "ERROR". Always branch on the body's status, never on response.ok. |
| When you do get a non-200 | 401 = missing/expired token, 403 = the end-point is not in your client's scopes, 404 = unknown id or an unregistered route, 405 = wrong method on that path. |
data on error |
Either "" (empty string) or a short message string. It is not an object. |
| Ids | 10-character case-sensitive strings, e.g. VFVoErejQy. Server-generated; never construct them. |
| Field naming | API JSON fields are all lowercase, no separators: memberid, groupid, campaignid, reportid, parentgroup, firstname. (The MCP tools use camelCase parameter names — that is an MCP-layer convention only.) |
| Dates | ISO-8601 with offset, e.g. 2026-07-08T10:08:18-04:00. ⚠️ Older records may use a space instead of T (2018-11-07 21:33:19+01:00). Parse tolerantly. |
| Booleans vs. strings | ⚠️ Not consistent across resources. testuser is a real boolean on /members but the string "0"/"1" on campaign members and reports. countreminded is a numeric string. Coerce, don't compare strictly. |
| Empty objects | ⚠️ An empty object is often serialised as an empty array [] instead of {} (PHP). config: [], extra: [], tags: [] all mean "empty". Check Array.isArray() before treating it as an object. |
| Pagination | ⚠️ Not supported. limit / offset query parameters are accepted but ignored — collection end-points return the full set (verified 2026-08-10: GET /members?limit=2 returned all 610 members). Page client-side. |
| Sorting / filtering | Not supported on collection end-points. GET /reports/search/{term} is the only server-side search. |
Legend used in the tables
| Mark | Meaning |
|---|---|
| ✅ | Verified against the live API on 2026-08-10 — the shape below is what you actually get. |
| ⚠️ | Works, but has a gotcha. Read the note. |
| ❓ | Not yet documented. Shape unconfirmed — contact us if you need it. |
About the scope list
The admin Scopes tab lists every available end-point. Access for a given client is always limited to the end-points opened for it — anything else returns 403, even though the route exists. Ask your Esperto contact to open extra scopes; note that changing scopes regenerates the client secret.
Members
Individual members — everyone who has ever participated in assessments. A member belongs to at most one group at a time (see Groups).
| Method | Path | Request body | Returns | |
|---|---|---|---|---|
| GET | /members |
— | Member[] (list shape) |
✅ |
| POST | /members |
MemberWrite |
created Member |
⚠️ |
| POST | /members/import |
❓ | ❓ | ❓ |
| POST | /members/dupecheck |
{ "value": "<email>" } |
""; status: "ERROR" = duplicate exists, "OK" = free |
⚠️ |
| GET | /members/{memberid} |
— | Member (detail shape — differs from the list!) |
⚠️ |
| PATCH | /members/{memberid} |
partial MemberWrite |
updated Member |
⚠️ |
| DELETE | /members/{memberid} |
— | "" |
✅ |
| GET | /members/{memberid}/reports |
— | MemberReport[] |
✅ |
| GET | /members/{memberid}/campaigns |
— | ❓ | ❓ |
⚠️ POST /members/dupecheck — the key must be value. Any other key (e.g.
email, or a { members: [...] } array) is silently accepted and returns
status: "OK" — a false negative. Always send exactly:
{ "value": "someone@example.com" }
The API rejects duplicate member e-mail addresses on create, so run this first.
⚠️ GET /members and GET /members/{memberid} return different fields.
The list carries phone, company, jobtitle and a human-readable group
name; the detail carries groupid, createdon, extra, activeusersession
and useraccount — but not phone/company/jobtitle. Both models are
below.
⚠️ You cannot move a member between groups via PATCH /members/{memberid}.
Sending a group / groupid field returns Access denied. Use the group
routes instead — DELETE /groups/{old}/members/{memberid} then
POST /groups/{new}/members/{memberid}.
⚠️ No bulk update. Updating N members is N PATCH calls.
POST /members/import is for import (create/upsert), not for toggling fields on
existing members.
Groups
Folders in the Members tree. A group is a node with a parentgroup, so groups
nest arbitrarily deep; the root node is the literal string root.
| Method | Path | Request body | Returns | |
|---|---|---|---|---|
| GET | /groups |
— | Group[] (flat list — build the tree from parentgroup) |
✅ |
| POST | /groups |
GroupWrite |
created Group |
✅ |
| GET | /groups/members |
— | ❓ | ❓ |
| POST | /groups/dupecheck |
{ "value": "<alias>" } |
""; status: "ERROR" = duplicate exists |
⚠️ |
| GET | /groups/{groupid} |
— | Group |
✅ |
| PATCH | /groups/{groupid} |
partial GroupWrite |
updated Group |
❓ |
| DELETE | /groups/{groupid} |
— | "" |
❓ |
| GET | /groups/nogroup/members |
— | GroupMember[] — members not in any group |
✅ |
| GET | /groups/{groupid}/members |
— | GroupMember[] |
✅ |
| POST | /groups/{groupid}/members/{memberid} |
none (ids in the path) | "" |
⚠️ |
| DELETE | /groups/{groupid}/members/{memberid} |
— | "" |
✅ |
| GET | /groups/{groupid}/reports |
— | Report[] |
❓ |
| GET | /groups/{groupid}/reports/counts |
— | counts per variant (see /reports/counts) |
❓ |
⚠️ Adding a member to a group takes no body. Both ids go in the path and the
request body must be empty. Both …/members/{memberid} (plural) and
…/member/{memberid} (singular) are registered and do the same thing; prefer
the plural form.
⚠️ GET /groups/{groupid}/members returns a leaner member shape than
GET /members — no phone, company, jobtitle or group, but it does carry
extra. Model below as GroupMember.
Dupecheck matches on the alias, not the display name. Group names and aliases must both be unique; the alias must have no spaces or special characters.
Campaigns
A campaign invites a set of people to take one assessment, and drives the mails and reminders. See the Campaigns guide for the concepts.
| Method | Path | Request body | Returns | |
|---|---|---|---|---|
| GET | /campaigns |
— | Campaign[] (list shape) |
✅ |
| POST | /campaigns |
CampaignWrite ❓ |
created Campaign |
❓ |
| POST | /campaigns/dupecheck |
{ "value": "<alias>" } |
""; status: "ERROR" = duplicate exists |
⚠️ |
| GET | /campaigns/{campaignid} |
— | Campaign (detail shape — more fields than the list) |
✅ |
| PATCH | /campaigns/{campaignid} |
partial CampaignWrite ❓ |
updated Campaign |
❓ |
| DELETE | /campaigns/{campaignid} |
— | "" |
✅ |
| GET | /campaigns/{campaignid}/reports |
— | CampaignReport[] |
✅ |
| GET | /campaigns/{campaignid}/members |
— | CampaignMember[] |
✅ |
| POST | /campaigns/{campaignid}/members |
❓ (bulk add — array of ids, or { members: [...] }?) |
❓ | ❓ |
| DELETE | /campaigns/{campaignid}/members/all |
— | "" |
✅ |
| POST | /campaigns/{campaignid}/members/invite |
optional ❓ | "" |
🚨 |
| POST | /campaigns/{campaignid}/members/remind |
optional ❓ | "" |
🚨 |
| GET | /campaigns/{campaignid}/members/{memberid} |
— | CampaignMember |
✅ |
| POST | /campaigns/{campaignid}/members/{memberid} |
none (ids in the path) | "" |
✅ |
| PATCH | /campaigns/{campaignid}/members/{memberid} |
❓ | updated CampaignMember |
❓ |
| DELETE | /campaigns/{campaignid}/members/{memberid} |
— | "" |
✅ |
| PATCH | /campaigns/{campaignid}/members/{memberid}/reset |
none | "" |
✅ |
| GET | /campaigns/{campaignid}/members/{memberid}/reports |
— | MemberReport[] |
✅ |
🚨
inviteandremindsend real e-mail to every eligible member of the campaign, immediately. There is no dry-run. Only call them against a campaign whose members are addresses you control.
⚠️ GET /campaigns and GET /campaigns/{campaignid} return different fields.
The detail call adds creator, creatordetails, extraParams, access and
relations; the list adds batchrelation and campaigngroupid. Both below.
Campaign dupecheck matches on the alias, not on name — two campaigns may
share a display name.
Reports
The result of an assessment. One completed questionnaire = one personal
report; a group report aggregates several personal reports.
| Method | Path | Request body | Returns | |
|---|---|---|---|---|
| GET | /reports |
— | Report[] |
✅ |
| GET | /reports/search/{term} |
— | ⚠️ not reports — { campaigns[], members[], groups[] } matching the term |
⚠️ |
| GET | /reports/filtered/{variant}/{entitytype}/{entityid} |
— | Report[] |
⚠️ |
| GET | /reports/counts |
— | { [variant]: { [reporttype]: "<count>" } } — counts are strings |
✅ |
| GET | /reports/verbatims |
— | free-text answers | ⚠️ |
| GET | /reports/summary/{variant} |
— | the summary-report definition for that variant (not report data) | ✅ |
| GET | /reports/{reportid} |
— | 🚨 the rendered PDF, as binary — not JSON | 🚨 |
| GET | /reports/{reportid}/short |
— | ReportShort — metadata only |
✅ |
| GET | /reports/{reportid}/data |
— | ReportData — the answers; use this for analysis |
✅ |
| GET | /reports/{reportid}/usage |
— | Report[] — the group reports this report feeds into |
✅ |
| PATCH | /reports/{reportid} |
❓ | updated report | ❓ |
| DELETE | /reports/{reportid} |
— | "" |
✅ |
| POST | /reports/{reportid}/recalc |
none | "" |
✅ |
| POST | /reports |
❓ | ❓ | ❓ |
| POST | /reports/summary |
❓ | ❓ | ❓ |
| POST | /reports/import/{variant} |
❓ | ❓ | ❓ |
🚨 GET /reports/{reportid} returns a PDF byte stream, not JSON. Do not try
to JSON.parse() it. For metadata use /short; for the answers use /data.
⚠️ GET /reports/search/{term} is a global lookup that returns matching
campaigns, members and groups — an object with three arrays, not a report
list. Use it to resolve a name to an id.
⚠️ GET /reports/filtered/{variant}/{entitytype}/{entityid} returned 403
on 2026-08-10 for entitytype campaign and member, even with the full
reports scope enabled. Treat it as unavailable for now and use
/campaigns/{campaignid}/reports or /members/{memberid}/reports instead.
⚠️ GET /reports/verbatims returns status: "ERROR", data: "no reports selected" when called bare — it needs a report selection, whose query-parameter
name is not yet documented.
Samples
| Method | Path | Request body | Returns | |
|---|---|---|---|---|
| GET | /samples |
— | ❓ | ❓ |
Settings
System configuration, report variants and their texts. Use these to discover
which variant and language values a campaign may reference.
| Method | Path | Request body | Returns | |
|---|---|---|---|---|
| GET | /settings/config |
— | full client config | ❓ |
| GET | /settings/config/short |
— | shortened client config | ❓ |
| GET | /settings/reports |
— | the available report variants | ❓ |
| GET | /settings/reports/{reportalias}/datastructure |
— | question/answer structure of a variant | ❓ |
| GET | /settings/reports/{reportalias}/textkeys |
— | text keys of a variant | ❓ |
| GET | /settings/reports/{reportalias}/{lang}/texts |
— | texts for a variant + language | ❓ |
These are not enabled on our own integration client, so the payloads have not been verified. Ask to have the settings scope opened if you need variant discovery.
Data models
Types below: string, int, bool, datetime (ISO-8601 with offset),
object, array. "R" = returned, "W" = writable.
Member — list shape (GET /members)
| Field | Type | R/W | Notes |
|---|---|---|---|
memberid |
string | R | Server-generated, 10 chars. |
title |
string|null | RW | Free text ("CEO", "Mr."). Often "" or null. |
firstname |
string | RW | |
middlename |
string|null | RW | Dutch-style tussenvoegsel; may be "". |
lastname |
string | RW | |
email |
string | RW | Unique across the instance — dupecheck before create. |
phone |
string | RW | ⚠️ Present here, absent from the detail call. |
company |
string | RW | ⚠️ Present here, absent from the detail call. |
jobtitle |
string | RW | ⚠️ Present here, absent from the detail call. |
status |
string|null | RW | active | inactive | null. |
level |
string|null | RW | See Enumerations. |
testuser |
bool | RW | Real boolean here. |
group |
string|null | R | The group's display name, not its id. Derived. |
Member — detail shape (GET /members/{memberid})
| Field | Type | R/W | Notes |
|---|---|---|---|
memberid |
string | R | |
title |
string | RW | |
firstname |
string | RW | |
middlename |
string | RW | |
lastname |
string | RW | |
email |
string | RW | |
status |
string | RW | active | inactive. |
level |
string|null | RW | |
groupid |
string|null | R | ⚠️ Read-only in practice — see the note under Members. |
createdon |
datetime | R | |
testuser |
bool | RW | |
extra |
object|[] |
RW | Instance-specific custom fields. [] when empty. |
activeusersession |
bool | R | Whether the member currently has a live questionnaire session. |
useraccount |
bool | R | Whether the member also has a platform login. |
GroupMember (GET /groups/{groupid}/members, GET /groups/nogroup/members)
memberid, title, firstname, middlename, lastname, email, status,
level, testuser (bool), extra. No phone / company / jobtitle /
group.
MemberWrite (POST / PATCH /members)
Send any subset of the writable fields above. email is required on create and
must be unique. ❓ The full required-field list and the extra sub-schema are
not yet confirmed.
Group
| Field | Type | R/W | Notes |
|---|---|---|---|
groupid |
string | R | |
alias |
string | W (create) | Unique, no spaces or special characters. Often prefixed with the creator id, e.g. WYwQ7PN20r_Bambuu. |
name |
string | RW | Display name. |
description |
string|null | RW | |
parentgroup |
string | RW | Parent groupid, or the literal "root". |
type |
string|null | RW | company | team | folder | null. |
status |
string | RW | active | inactive. |
config |
object|[] |
RW | { "selfregistration": { "signup": bool, "register": bool } }. [] when unset. |
created |
datetime | R | |
creator |
string|null | R | User id of the creator. |
accesstype |
string | R | Your access: owner | editor. |
extra |
object|[] |
RW | |
membercount |
int | R | Direct members only — not including sub-groups. |
GroupWrite (POST /groups) — verified working:
{
"name": "Claude company",
"alias": "Claudecompany",
"type": "company",
"parentgroup": "root",
"description": "optional"
}
Campaign — list shape (GET /campaigns)
| Field | Type | R/W | Notes |
|---|---|---|---|
campaignid |
string | R | |
alias |
string | R/W? | Unique; auto-generated for wizard campaigns, e.g. WYwQ7PN20r_QuartSessPrep_nlNL_260630187391. |
name |
string | RW | Display name. Not unique. |
description |
string | RW | |
start |
datetime | RW | |
end |
datetime|null | RW | Set when closingtype: "date"; null for open / auto. |
admins |
string[] | RW | Member ids. May contain null entries in legacy rows. |
variant |
string | W (create) | Reference to a report-variant alias. See Enumerations. |
language |
string | W (create) | Reference to a language the chosen variant supports. |
status |
string | R | active | inactive | ended. |
created |
datetime | R | |
mailconfig |
object|[] |
RW | Template id per mail stage — see below. |
config |
object|[] |
RW | See CampaignConfig below. |
closingtype |
string|null | RW | date | auto | open | null. |
autoinvite |
string | RW | none | fixed | instant. |
inviteoffset |
object|null | RW | Same { amount, unit } shape as reminderoffset. |
autoreminder |
bool | RW | |
reminderoffset |
object|null | RW | ⚠️ An object, not an int: { "amount": 2, "unit": "day" }. unit seen: day, week. |
reminderrepeat |
int|null | RW | How many reminders at most (seen: 3, 5). |
sendmemberconfirmation |
bool | RW | |
sendreportdelivery |
bool | RW | |
autogenerated |
bool | R | |
membercount |
int | R | Derived. |
productid |
string|null | R | |
memberstatuscount |
object | R | { new, invited, started, completed, specialtotal, specialcompleted } — ints. |
accesstype |
string | R | owner | editor. |
batchrelation |
any|null | R | ⚠️ List call only. |
campaigngroupid |
string|null | R | ⚠️ List call only. |
Campaign — extra fields on the detail call (GET /campaigns/{campaignid})
| Field | Type | Notes |
|---|---|---|
creator |
string | User id. |
creatordetails |
object | { firstname, lastname, email, color }. |
extraParams |
object[] | Participant sub-roles, e.g. a "Buyer" track. Each: { label, value, mandatory, canaddparticipants, showintitle, hideinwizard, mailconfig }. value is a query string like "role=buyer". |
access |
object | { "owner": Access[], "share": Access[] }. Each Access: { id, campaignid, userid, type, created, firstname, lastname, email, agencyname, agencyfooter }. |
relations |
array | Links to other campaigns. Empty in all observed cases. |
batchrelation and campaigngroupid are not returned here.
CampaignConfig (config)
| Key | Type | Notes |
|---|---|---|
selfregistration.signup |
bool | Allow open sign-up via the campaign link. |
selfregistration.register |
bool | Allow self-registration of new members. |
selfregistration.group |
string | Target groupid for self-registered members, or "root" / "". |
selfregistration.skipvalidation |
bool | |
minparticipants |
int | |
maxparticipants |
int | -1 = unlimited. |
selfadmin.addmembers |
array | |
custommailsubject |
object | Subject override per mail stage, e.g. { "campaign_invitation": "…" }. |
mailconfig
Object keyed by mail stage; each value is a base64-ish template identifier.
Stages observed: campaign_invitation, campaign_reminder,
confirmation_admin, confirmation_member, closing_notification,
campaign_reportdelivery. [] means "instance defaults".
CampaignWrite (POST / PATCH /campaigns) ❓
Not yet confirmed. Practically you will set name, variant, language,
start, closingtype (+ end when date), and the mail settings. Which of
these are strictly required, and whether alias must be supplied, is open.
CampaignMember (GET /campaigns/{campaignid}/members)
| Field | Type | Notes |
|---|---|---|
token |
string | The participation token for this member in this campaign. Also the report accesstoken. |
memberid |
string | |
title, firstname, middlename, lastname, email |
string | Copied from the member record. |
groupalias |
string | ⚠️ List call only. |
groupname |
string | ⚠️ List call only. On the single-member call this is a single field named group instead. |
status |
string | Participation status: new | invited | started | completed. |
testuser |
string | ⚠️ "0" / "1" — a string here, unlike /members. |
memberstatus |
string | The member record's own status (active / inactive). |
created |
datetime | When the member was added to the campaign. |
dateinvited |
datetime|null | |
datestarted |
datetime|null | |
datecompleted |
datetime|null | |
datereminded |
datetime|null | Last reminder. |
countreminded |
string|null | ⚠️ Numeric string, e.g. "3". |
extra |
object|null | |
tags |
array | |
results |
object|[] |
{ "status": "active", "reportid": "6XN79X1zKr" } once completed; [] while there is no report. ⚠️ Type changes — check Array.isArray(). |
mailconfig |
object|null | Per-member override. |
sessionurl |
string | Personal questionnaire link (https://q.esperto.one/q/…). "" until the member starts. |
memberextra |
array | ⚠️ List call only. |
Report (GET /reports, /campaigns/{campaignid}/reports)
| Field | Type | Notes |
|---|---|---|
reportid |
string | |
date |
datetime | Completion / generation moment. |
name |
string|null | Only set for group reports. |
campaignid |
string|null | |
type |
string | personal | group | groupanon | condensed | complete. |
variant |
string | Report-variant alias. |
language |
string | |
config |
object | Personal: { extra, campaignparams }. Group: { variant, language, reportType, selectedReports, selectedReportsMaster, selectedMember, excludedVerbatims } — the ids are comma-separated strings, not arrays. |
lastopened |
datetime|null | |
status |
string | active. |
tags |
array | |
testcase |
string|null | "0" / "1". |
memberid |
string | Subject of the report. |
title, firstname, middlename, lastname, email |
string | Joined member fields. |
memberstatus |
string |
MemberReport (GET /members/{memberid}/reports)
Same core fields plus id (numeric string — the internal row id), token,
groupid, campaignalias, campaignname. ⚠️ Here tags is a JSON string
("[]"), not an array.
ReportShort (GET /reports/{reportid}/short)
reportid, type, name, variant, language, memberid, firstname,
middlename, lastname, title, testcase, groupid, groupname,
accesstoken, campaignid, campaignname, config, date, status, tags.
ReportData (GET /reports/{reportid}/data)
Everything in ReportShort (minus campaignname, plus token, thedate,
alias, groupname) plus the answers:
| Field | Type | Notes |
|---|---|---|
raw |
string | 🚨 A JSON-encoded string, not an object. JSON.parse() it. Keys are question codes: {"Q1":8,"Q2":"…","Q3_1":8,…,"Remarks1":""}. Values are ints (scores) or strings (free text). |
processed |
string | 🚨 Also a JSON-encoded string. Same keys after scoring/cleanup; some raw keys may be dropped. |
extra |
string | 🚨 Also a JSON-encoded string: {"member":{"basic":[],"extras":[],"xparam":null}}. |
thedate |
datetime | Note the field name — thedate, not date. |
The question codes (Q1, Q3_1, Remarks1, …) are defined per variant.
Use GET /settings/reports/{reportalias}/datastructure to map them to question
texts.
Report counts (GET /reports/counts)
{ "status": "OK",
"data": { "ScaleUp2": { "personal": "130", "group": "95", "groupanon": "12", "condensed": "2" } } }
⚠️ The counts are strings.
Enumerations
Observed values across the live instance (2026-08-10). Instance-specific — do not hard-code without checking your own.
| Field | Values |
|---|---|
Member status |
active, inactive, null |
Member level |
ceofounder, ceofounderwithteam, ceofounderalone, teamleader, employee, guest, null |
Group type |
company, team, folder, null |
Group / campaign accesstype |
owner, editor |
Campaign status |
active, inactive, ended |
Campaign closingtype |
date (fixed end date, requires end), auto (closes when everyone finished), open (never closes), null |
Campaign autoinvite |
none, fixed, instant |
Campaign-member status |
new, invited, started, completed |
Report type |
personal, group, groupanon, condensed, complete |
variant |
ScaleUp, ScaleUp2, LeadVision, QuartSessPrep, RockHabits, Pathfinderv2, 5States360v2, ENNEAGRAM |
language |
enUS, nlNL, esES, frFR — ⚠️ per variant; each variant supports its own subset with one default. |
reminderoffset.unit |
day, week |
Still to be documented
Open items, tracked so you know what is genuinely missing rather than merely absent:
- Write contracts for
POST/PATCH /campaigns,PATCH /reports/{id},POST /reports,POST /reports/summary,POST /reports/import/{variant}— required fields and value ranges. POST /campaigns/{campaignid}/members— the bulk-add body shape.POST /members/import— payload and upsert semantics.GET /members/{memberid}/campaignsandGET /groups/members— response shapes.GET /reports/verbatims— the query parameter that selects reports.GET /reports/filtered/{variant}/{entitytype}/{entityid}— whichentitytypevalues are valid, and which scope lifts the 403.- Settings family — response shapes, and the canonical way to discover the variants and each variant's languages.
- Error catalogue — the
datamessage strings per failure mode. extra/memberextra— the custom-field sub-schema.- Invite/remind — whether an optional body can target a subset of members.
Need one of these before it lands here? Contact us.
Verified live against https://scalinguptoolkit.com/api/v1 on 2026-08-10:
members, groups, campaigns, campaign members, campaign reports, member reports,
report short/data/usage/counts, summary-report definitions, search and the three
dupecheck end-points.