Members
Members are the internal Ketendo users - your own staff, as opposed to the people and organisations you trade with. Save a member here before any record that has to reference one by id. The two read endpoints below let you check what already exists before you create anything.
SaveMember lives under /v1/integration/member/, and the two read endpoints under plain /v1/Member/ - not beside the records that reference them.POST/v1/integration/member/SaveMemberCreate/update an internal user
| Field | Type | Required? | Notes |
|---|---|---|---|
ExternalId | string | Required | Your unique key for this member, e.g. STAFF-4471 |
CompanyId | int | Required | Which company this member belongs to - see the CompanyId note. Also accepted as Member.CompanyId; send it in either position |
Member | object | Required | |
↳ UserName | string | Required | The member's email address. This is the match key - see the callout below |
↳ FirstName, LastName | string | Required | Both are mandatory on the Ketendo member screen - please send them so records created via the API match what a user would have had to capture |
↳ OtherName | string | Optional | |
↳ PhoneNumber | string | Required | Send it in international E.164 format, e.g. +27821234567. Ketendo validates numbers against the company's country, so E.164 is the safe form. A leading + is stripped before saving |
↳ RoleIds | array of guid | Required | Roles to grant this member for CompanyId. Send exactly one role id - the Ketendo screen limits selection to one. On update, the roles for this company are replaced by what you send |
↳ ReportsToUserId | guid? | Optional | Internal id of the member this person reports to - there is no email-based alternative for this field yet |
↳ UsesExternalAuthProvider | bool | Optional | Default false. Set true if they sign in via SSO |
↳ SendInviteNotification | bool | Optional | Default false. We verified this only fires the first time a member is created - re-sending it for an existing member does nothing |
↳ Deleted | bool | Optional | Default false. Set true to deactivate; their linked Employee record is deactivated with them |
↳ Employee | object | Optional | Employee record to create/update alongside the member - MemberId and Deleted are stamped for you |
↳ userCommunicationChannels | array | Required | Which channels this member is contacted on. Ketendo expects at least one entry per category - see the callout below for the exact values to send |
↳ ↳ communicationChannelTypeId | int | Required | Email = 1, SMS = 2, Platform (in-app) = 3. Email is what's normally selected, so send 1 unless you specifically need the others |
↳ ↳ communicationChannelTypeCategoryId | int | Required | Verification = 1, In App = 2 |
↳ ↳ memberId | - | Auto-set | Don't send - stamped for you once the member id is known |
↳ userCompanyAccess | array | Optional | Cross-company role access; only saved when supplied |
↳ ExternalIdentifier | - | Auto-set | Don't send - set from the ExternalId you sent |
UserName (the email), not on your ExternalId. Sending a different UserName for the same ExternalId creates a second member rather than renaming the first, and the call then fails with "External member mapping mismatch" to stop us silently repointing your ExternalId at a different person. Treat UserName as fixed once a member exists.PhoneNumber, the name fields, ReportsToUserId, RoleIds, Deleted, UsesExternalAuthProvider and ExternalIdentifier. UserName/email cannot be changed through this endpoint because it is the match key."userCommunicationChannels": [
{ "communicationChannelTypeId": 1, "communicationChannelTypeCategoryId": 1 },
{ "communicationChannelTypeId": 1, "communicationChannelTypeCategoryId": 2 }
]
That is Email (1) for both Verification (1) and In App (2). Note there is no address field on these entries - the email used is the member's UserName, and the number is their PhoneNumber.FirstName, LastName, PhoneNumber, RoleIds and userCommunicationChannels are marked required here because the Ketendo member screen enforces them. The API itself is more lenient and will technically accept a member without them, but doing so creates a record a Ketendo user could not have created - typically one with no role, so the person cannot be assigned work or sign in meaningfully. Please treat them as mandatory.SendInviteNotification: true on first creation to have Ketendo email the member an invitation, or leave it false and onboard them separately.Request
POST /v1/integration/member/SaveMember
{
"ExternalId": "STAFF-4471",
"CompanyId": 101,
"Member": {
"UserName": "jessica@example.com",
"FirstName": "Jessica",
"LastName": "Naidoo",
"PhoneNumber": "+27821234567",
"RoleIds": ["3f2a1b4c-1111-4a2b-9c3d-00000000000e"],
"userCommunicationChannels": [
{ "communicationChannelTypeId": 1, "communicationChannelTypeCategoryId": 1 },
{ "communicationChannelTypeId": 1, "communicationChannelTypeCategoryId": 2 }
],
"UsesExternalAuthProvider": false,
"SendInviteNotification": false,
"Deleted": false
}
}
Result
{
"externalId": "STAFF-4471",
"internalId": "7a8b9c0d-2222-4a2b-9c3d-00000000000f",
"mappingCreated": true
}
Keep the returned internalId if a record needs it. Many endpoints will also accept the member's email or your own ExternalId instead, so check the field before storing ids you may not need.
GET/v1/Member/ValidateUsername/{username}Check whether a member already exists
Use this before SaveMember if you need to know whether an email is already taken - for example to decide between creating a member and just referencing the existing one.
| Endpoint | Use for |
|---|---|
GET /v1/Member/ValidateUsername/{username} | Checking a brand-new member's email |
GET /v1/Member/ValidateUsername/{memberId}/{username} | Checking while editing an existing member - excludes that member from the check, so re-sending their own email doesn't report a clash |
true means the username is already in use (so don't create it again); false means it is available.UserName across the whole platform, not just your company - the same person cannot exist twice under different companies with the same email. Only Authorization: Bearer <token> is needed; X-Source-System is not required.Request
GET /v1/Member/ValidateUsername/jessica@example.com
Authorization: Bearer <token>
Result
true
Wrapped in the standard response envelope. true = already taken, false = free to use.
POST/v1/Member/MembersList/search the members on a company
A second way to confirm what Ketendo already has - useful for reconciling your list of staff against ours, or for finding a member's internal id. It is a POST because the search/paging options go in the body.
| Field | Type | Required? | Notes |
|---|---|---|---|
Identifier | int | Required | The CompanyId whose members you want |
Identifier2 | guid? | Optional | Leave this out. It applies role-level visibility filtering for a specific portal user and isn't useful for an integration |
SearchCriteria | string | Optional | Free-text search, e.g. an email or surname |
PageNumber, PageSize | int | Optional | Standard paging |
SortBy | string | Optional | |
SortDirection | int | Optional | 0 ascending, 1 descending |
IncludeDeleted | bool | Optional | Defaults to true - send false if you only want active members |
ExternalId you assigned, use GET /v1/EntityLookup/member/external/{externalId} from the Retrieve by ExternalId section instead - that's usually the simpler option once you've saved them at least once.POST /v1/Member/MembersList, taking the same body.Request
POST /v1/Member/Members
{
"Identifier": 101,
"SearchCriteria": "jessica@example.com",
"PageNumber": 1,
"PageSize": 25,
"IncludeDeleted": false
}
Result
{
"result": [
{
"id": "7a8b9c0d-2222-4a2b-9c3d-00000000000f",
"userName": "jessica@example.com",
"phoneNumber": "27821234567",
"personalInformation": { "firstName": "Jessica", "lastName": "Naidoo" }
}
],
"totalRecords": 1
}
Trimmed for brevity - each member also carries their roles and company details.