Company roles
A company role is a named position in your organization’s org chart (for
example “Service Manager”). Each role can have a training curriculum — the
lessons and topics everyone in the role is expected to complete — and a set of
members who currently hold it. Creating a role, changing its name or
description, moving it, merging it, deleting it, and choosing who holds it
need company-role:manage. Adding or removing a curriculum item needs
curriculum:manage. What an external role can read is
content-grant:manage. Changing a curriculum item after it is added stays
in the app. Duties are on
Duties.
List company roles
Section titled “List company roles”GET /v1/company-roles
Lists the organization’s company roles, sorted alphabetically by name. Each result carries how many people hold the role and the role it reports to.
Query parameters
Section titled “Query parameters”| Parameter | Type | Notes |
|---|---|---|
query |
string | Case-insensitive match on the role name. |
cursor |
string | Pagination cursor from a previous response. |
limit |
number | 1–100. Default 50. |
Request
Section titled “Request”curl "https://api.mereon.ai/v1/company-roles?query=manager" \ -H "Authorization: Bearer $MEREON_TOKEN"Response
Section titled “Response”{ "data": [ { "id": "695690de-1c49-42c7-a416-b0d3bea29102", "name": "Service Manager", "description": "Supervises the field technician team and schedules work.", "memberCount": 2, "parentRoleId": "1c0f8b2a-7d34-4e91-bf02-3a5d9c6e8f10", "createdAt": "2026-02-01T10:00:00.000Z", "updatedAt": "2026-05-20T10:00:00.000Z" } ], "nextCursor": null}| Field | Type | Notes |
|---|---|---|
description |
string | null | The role’s description, if set. |
memberCount |
number | How many people currently hold the role. |
parentRoleId |
string | null | The role this one reports to; null at the top of the chart. |
Get a company role
Section titled “Get a company role”GET /v1/company-roles/{id}
Role detail: the summary fields plus the role’s training curriculum (the
lessons and topics everyone in the role must complete) and its current
members. An external role also lists the resources it can open in
contentGrants. An internal role returns that list empty. Returns 404 if
the role is not visible to you.
Response
Section titled “Response”{ "id": "695690de-1c49-42c7-a416-b0d3bea29102", "name": "Service Manager", "description": "Supervises the field technician team and schedules work.", "memberCount": 2, "parentRoleId": "1c0f8b2a-7d34-4e91-bf02-3a5d9c6e8f10", "createdAt": "2026-02-01T10:00:00.000Z", "updatedAt": "2026-05-20T10:00:00.000Z", "curriculum": [ { "type": "lesson", "id": "lsn_6e3f...", "name": "Scheduling and dispatch", "dueDate": { "type": "relative", "fixedDate": null, "relativeDays": 30, "anchor": "role_assignment_date" }, "retrainingOverride": { "type": "recurring", "intervalDays": 365, "completionMode": "changed_material" } }, { "type": "topic", "id": "t_91ab...", "name": "Maintenance plans", "dueDate": { "type": "none", "fixedDate": null, "relativeDays": null, "anchor": null }, "retrainingOverride": null } ], "members": [ { "id": "usr_44de...", "name": "Carla Field" } ], "contentGrants": []}| Field | Type | Notes |
|---|---|---|
curriculum[].type |
string | lesson or topic. |
curriculum[].id |
string | The lesson or topic id. Resolve a lesson with /v1/lessons/{id}, a topic with /v1/topics/{id}. |
curriculum[].dueDate |
object | When the item is due for a person in the role. type is none, fixed or relative. A fixed deadline carries fixedDate. A relative one carries relativeDays and an anchor (assignment_date, enrollment_date, employment_start, role_assignment_date or first_login), and has no single date until it is applied to a person. |
curriculum[].retrainingOverride |
object | null | The role’s own retraining rule for the item, which replaces the item’s default completely. null when the role defers to the item’s default policy. An object with type: "none" means the role has said never to retake it, which is different from deferring. |
members[] |
object | Each holder’s id and name. |
contentGrants[] |
object | Empty for an internal role. For an external role, each resource it can open: contentId, title, and type (video, audio, document, or guide). |
List a role’s duties
Section titled “List a role’s duties”GET /v1/company-roles/{id}/duties
Everything this role answers for, from the holder’s side. The
duties endpoint answers the opposite question: who answers for a
given topic. Needs raci-assignment.read as well as access to the org chart,
as the duty list on the role’s page does.
Query parameters
Section titled “Query parameters”| Parameter | Type | Notes |
|---|---|---|
entityType |
string | TOPIC, ITEM, SPACE, or CATEGORY. Worth using: a duty on a topic reaches every item on it, so an unfiltered list is mostly items. |
cursor |
string | Pagination cursor from a previous response. |
limit |
number | 1–100. Default 50. |
Request
Section titled “Request”curl "https://api.mereon.ai/v1/company-roles/695690de/duties?entityType=TOPIC" \ -H "Authorization: Bearer $MEREON_TOKEN"Response
Section titled “Response”{ "data": [ { "role": "ACCOUNTABLE", "entityType": "TOPIC", "entityId": "top_9f2a...", "entityName": "Expense approval", "inheritedFrom": { "entityType": "SPACE", "entityId": "spc_4c81..." }, "tierQualifier": "MIN_TIER", "advancementTierId": "tier_7a12...", "advancementTierLabel": "Senior", "assignmentId": "ras_71b3...", "source": "MANUAL", "confidence": null, "learningPriorityOverride": null, "url": "https://mereon.ai/topics/expense-approval" } ], "nextCursor": null}| Field | Type | Notes |
|---|---|---|
role |
string | RESPONSIBLE, ACCOUNTABLE, CONSULTED, or INFORMED. |
entityName |
string | Resolved so the row stands alone. Duties on entities no longer in the live graph are omitted, so this is always present. |
inheritedFrom |
object | null | The ancestor the duty reaches this entity through, or null when assigned directly. A duty on a space is reported against each topic in it. |
tierQualifier |
string | How far the duty reaches across the role’s advancement path: ALL, MIN_TIER, MAX_TIER, or EXACT_TIER. ALL when the role has no path. |
advancementTierLabel |
string | null | The tier the qualifier refers to, when one applies. |
assignmentId |
string | The assignment behind this duty. For an inherited duty it is the parent’s assignment, not one on the entity named. |
source |
string | Who set the duty: MANUAL for a person’s own assignment, or AI_ENHANCED or AI_CLASSIFIED for the system’s suggestion. |
confidence |
number | null | How sure the suggestion was, from 0 to 1. null for a person’s own assignment. |
learningPriorityOverride |
string | null | This duty’s own learning priority (CRITICAL, HIGH, MEDIUM, or LOW). null means the topic’s priority applies. |
Rows are ordered by entity type then name, and the list is limited to entities the
caller can read. A duty held at the space or category level expands to one row per
descendant, so it can be long — which is what entityType is for.
Get the org chart
Section titled “Get the org chart”GET /v1/org-chart
The whole chart in one call: every role with the role it reports to, the people who currently hold it, and whether it is vacant. Use it to answer “who holds this role?” or “which roles are unfilled?” without reading each role in turn.
Rebuild the tree by matching each role’s reportsToRoleId to an id in the same
list. A role at the top has reportsToRoleId: null. Someone who has left the
organization does not count as holding a role, so a seat only they held reads as
vacant.
Request
Section titled “Request”curl "https://api.mereon.ai/v1/org-chart" \ -H "Authorization: Bearer $MEREON_TOKEN"Response
Section titled “Response”{ "data": [ { "id": "695690de-1c49-42c7-a416-b0d3bea29102", "name": "Service Manager", "reportsToRoleId": "1c0f8b2a-7d34-4e91-bf02-3a5d9c6e8f10", "holders": [ { "id": "usr_2d9e...", "name": "Ada Admin", "email": "ada@example.com" } ], "vacant": false } ], "nextCursor": null}The chart is not paginated, so nextCursor is always null. This is the chart of
roles. Who answers to whom is a separate question, answered for the connected user
below.
Get your reporting line
Section titled “Get your reporting line”GET /v1/me/reporting-line
The roles the connected user holds, the person they report to, and the people who report to them.
Two people who share a title are not each other’s managers, because the people
come from the manager each person has been given and not from the role tree.
managers has at most one entry. It is a list so the shape does not change if
dotted lines are ever supported. Only people who are currently employed are listed.
Response
Section titled “Response”{ "myRoles": [{ "id": "695690de-1c49-42c7-a416-b0d3bea29102", "name": "Service Manager" }], "managers": [ { "employeeId": "emp_81c2...", "userId": "usr_44aa...", "name": "Grace Hopper", "email": "grace@example.com", "roleNames": ["Operations Director"] } ], "directReports": []}| Field | Type | Notes |
|---|---|---|
employeeId |
string | Always present. A manager need not have a login. |
userId |
string | null | null for someone who has never signed in. |
roleNames |
string[] | May be empty. A role says what someone does, not who manages them. |
Create a company role
Section titled “Create a company role”POST /v1/company-roles
Creates a company role. Leave parentRoleId out and the role sits at the top
of the chart. kind defaults to INTERNAL. EXTERNAL is a limited-access
role for people outside the company. A role with the same name already in the
organization is refused with 409. A parent that cannot be used leaves the new
role at the top. Limited to 10 calls a minute and 60 an hour.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | One line, up to 255 characters. |
kind |
string | no | INTERNAL or EXTERNAL. Defaults to INTERNAL. |
parentRoleId |
string | null | no | The role it reports to. null or omitted places it at the top. |
Request
Section titled “Request”curl -X POST "https://api.mereon.ai/v1/company-roles" \ -H "Authorization: Bearer $MEREON_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Service Manager" }'Response
Section titled “Response”201 Created. The role, and memberCount 0.
Save a draft
Section titled “Save a draft”PUT /v1/company-roles/{id}
Saves a new name or description as a draft. Readers keep seeing the live name
until you publish it. Send at least one of name and description. A field
you leave out keeps the value already on the draft, or the live role when
there is no draft. The same limit as creating a role.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | no | One line, up to 255 characters. |
description |
string | no | Up to 20,000 characters. Empty is allowed. |
Response
Section titled “Response”200 OK. The draft name and description, and liveName, the name a list still returns.
Publish a draft
Section titled “Publish a draft”POST /v1/company-roles/{id}/publish
Makes the draft name and description the ones readers see. Refused when the role has no draft. The same limit as creating a role.
Response
Section titled “Response”200 OK. The role as it reads after publishing.
Discard a draft
Section titled “Discard a draft”DELETE /v1/company-roles/{id}/draft
Throws away a draft name or description. The live role is unchanged. Refused when the role has no draft. The same limit as creating a role.
Response
Section titled “Response”200 OK. The live role.
Move a company role
Section titled “Move a company role”PUT /v1/company-roles/{id}/parent
Sets the role this one reports to. parentRoleId null places it at the top
of the chart. A move that would put a role under itself, or an internal role
under an external role, is refused with 400. The same limit as creating a role.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
parentRoleId |
string | null | yes | The role it should report to, or null for the top. |
Delete a company role
Section titled “Delete a company role”DELETE /v1/company-roles/{id}
Deletes the role. The response includes how many people held it. The same limit as creating a role.
Merge a company role
Section titled “Merge a company role”POST /v1/company-roles/{id}/merge
Moves this role’s people, curriculum, duties and child roles onto the target role, then deletes this role. The two roles have to be the same kind. Returns the role that remains. The same limit as creating a role.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
targetRoleId |
string | yes | The role that remains. |
Assign a person
Section titled “Assign a person”POST /v1/company-roles/{id}/members
Assigns someone who already belongs to the organization. An internal person on
an external role, or an external person on an internal role, is refused with
400. Someone who cannot open the role’s training is refused with 400.
Assigning someone who already holds the role refreshes that assignment. A
person outside the organization is refused with 400. The same limit as
creating a role.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
userId |
string | yes | From Find people. |
Response
Section titled “Response”201 Created. { "roleId", "userId" }.
Remove a person
Section titled “Remove a person”DELETE /v1/company-roles/{id}/members/{userId}
Removes someone from the role and cancels the training that role assigned them.
Someone who does not hold the role is refused with 404. The same limit as creating a role.
Response
Section titled “Response”200 OK. The membership that was removed.
Replace an advancement path
Section titled “Replace an advancement path”PUT /v1/company-roles/{id}/advancement-path
Replaces the role’s advancement path. The first tier is the most junior. An
empty tiers list removes the path. Send a tier’s id to keep that step when
its label changes. The same limit as creating a role.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
tiers |
object[] | yes | At most 100. Each has label (one line, up to 100 characters) and an optional id. |
Response
Section titled “Response”200 OK. The role id and the tiers, each with id, label and rank.
Set a person’s standing
Section titled “Set a person’s standing”PUT /v1/company-roles/{id}/members/{userId}/standing
Sets or clears where someone stands on the role’s advancement path.
standingTierId null clears an explicit standing so the lowest tier applies.
The tier has to be on this role’s path. Someone who does not hold the role is
refused with 404. The same limit as creating a role.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
standingTierId |
string | null | yes | A tier on this role’s path, or null. |
Response
Section titled “Response”200 OK. { "roleId", "userId", "standingTierId" }.
Add a curriculum item
Section titled “Add a curriculum item”POST /v1/company-roles/{id}/curriculum
Adds a lesson or a topic to the role’s training curriculum. People who already
hold the role are not given it unless applyToExisting is true. A topic can
be added only when the role already has a duty on that topic. Send only the
fields for the due date you chose. Needs curriculum:manage and permission to
change that role, which owners and admins have. Limited to 10 calls a minute
and 60 an hour.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
itemType |
string | yes | lesson or topic. |
itemId |
string | yes | The lesson id, or the topic id. |
applyToExisting |
boolean | no | When true, people who already hold the role are given this item. Defaults to false. |
dueDateType |
string | yes | none, fixed, or relative. Send only the fields that type uses. |
dueDateFixed |
string | when fixed | ISO date-time. |
dueDateRelativeDays |
number | when relative | At least 1. |
dueDateRelativeAnchor |
string | when relative | employment_start, role_assignment_date, or first_login. |
retraining |
object | null | no | Omit, or null, to keep the item’s own rule. retrainingType none turns retraining off for this role. recurring also names intervalDays and completionMode (full_retake or changed_material). |
tierQualifier |
string | no | ALL, MIN, MAX, or EXACT. Defaults to ALL. |
advancementTierId |
string | null | when not ALL | A tier on this role’s path. Omit it for ALL. |
Response
Section titled “Response”201 Created. The curriculum item, in the same shape the role detail returns
inside curriculum. id is the lesson or topic id.
Remove a curriculum item
Section titled “Remove a curriculum item”DELETE /v1/company-roles/{id}/curriculum/{itemType}/{itemId}
Removes a lesson or a topic. itemType and itemId are what the role detail
returns for that item. Needs curriculum:manage and permission to change that
role. The same limit as adding an item.
Response
Section titled “Response”200 OK. The item that was removed.
Changing a curriculum item’s due date, retraining, or how far it reaches on the advancement path, after it is added, stays in the app.
Add a content grant
Section titled “Add a content grant”POST /v1/company-roles/{id}/content-grants
Gives an external company role access to one video, audio file, document, or
guide. Granting a resource that is already granted leaves it in place. An
internal role is refused. Needs content-grant:manage and permission to
change that role, which owners and admins have. Limited to 10 calls a minute
and 60 an hour.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
contentId |
string | yes | The video, audio, document, or guide id. |
Response
Section titled “Response”201 Created. The grant: contentId, title, and type (video, audio,
document, or guide).
Remove a content grant
Section titled “Remove a content grant”DELETE /v1/company-roles/{id}/content-grants/{contentId}
Removes one resource from an external company role. contentId is what the
role detail returns in contentGrants. A grant that is not on the role is
refused with 404. Needs content-grant:manage and permission to change
that role. The same limit as adding a grant.
Response
Section titled “Response”200 OK. The grant that was removed.
A lesson stays on the role’s curriculum.
Errors
Section titled “Errors”The company-role:manage routes answer with these statuses.
| Status | When |
|---|---|
400 |
The body is not the schema’s, the move is not allowed, the person cannot hold this kind of role, or they cannot open the role’s training. |
403 |
The token lacks company-role:manage, or the caller may not take that action on the role. |
404 |
The role is not in this organization, or the person does not hold it. |
409 |
A role with that name already exists. |
429 |
More than 10 calls a minute, or 60 an hour, on that route. Retry-After says when to try again. |
A curriculum route answers with these.
| Status | When |
|---|---|
400 |
The body is not the schema’s, itemType is not lesson or topic, the lesson or topic is not in this organization, the topic is not one of the role’s duties, or someone who holds the role cannot open the item. |
403 |
The token lacks curriculum:manage, or the caller may not change that role’s curriculum. |
404 |
The role is not in this organization, or that item is not on its curriculum. |
409 |
That item is already on the curriculum. |
429 |
More than 10 calls a minute, or 60 an hour, on that route. Retry-After says when to try again. |
A content-grant route answers with these.
| Status | When |
|---|---|
400 |
The body is not the schema’s, the id is not valid, or the role is not external. |
403 |
The token lacks content-grant:manage, or the caller may not change what that role can read. |
404 |
The role is not in this organization, the resource is not in this organization, or that grant is not on the role. |
429 |
More than 10 calls a minute, or 60 an hour, on that route. Retry-After says when to try again. |