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

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

🚨 invite and remind send 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 ReportDatathe 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:

  1. Write contracts for POST/PATCH /campaigns, PATCH /reports/{id}, POST /reports, POST /reports/summary, POST /reports/import/{variant} — required fields and value ranges.
  2. POST /campaigns/{campaignid}/members — the bulk-add body shape.
  3. POST /members/import — payload and upsert semantics.
  4. GET /members/{memberid}/campaigns and GET /groups/members — response shapes.
  5. GET /reports/verbatims — the query parameter that selects reports.
  6. GET /reports/filtered/{variant}/{entitytype}/{entityid} — which entitytype values are valid, and which scope lifts the 403.
  7. Settings family — response shapes, and the canonical way to discover the variants and each variant's languages.
  8. Error catalogue — the data message strings per failure mode.
  9. extra / memberextra — the custom-field sub-schema.
  10. 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.

Was this helpful? Get in touch if something is missing or unclear.