Skills

Available. Job roles, a person’s job roles, a person’s skills and the team skills matrix are readable (and job roles writable) through the API. These endpoints follow your plan: job roles and a person’s skills need Skills management (Starter and up); the matrix needs Skills planning (Growth and up) and your organization’s skills-planning switch turned on. An API key alone is not enough: a plan without the feature answers 403.

A job role (for example Site supervisor) lists the skills that work needs and the level needed for each. People hold job roles; the level a person holds in each skill is compared with the level their roles require. The same computation drives the app, the person’s skills record and these endpoints, so they always agree.

Levels, lowest to highest: awareness, developing, proficient, expert.


The Job Role Object

{
  "id": "2c1b0a9f-8e7d-4c6b-9a5f-3e2d1c0b9a8f",
  "name": "Site supervisor",
  "description": "Leads a shift on site",
  "key": null,
  "is_active": true,
  "required_skills": [
    {
      "skill_id": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2918",
      "skill_name": "Forklift operation",
      "required_level": "proficient",
      "reviewed_at": "2026-10-01T14:00:00.000Z"
    }
  ],
  "holder_count": 12,
  "created_at": "2026-09-20T09:00:00.000Z",
  "updated_at": "2026-10-01T14:00:00.000Z"
}
FieldDescription
keySet for roles managed by imported content (the Embay Academy); null for roles you create
required_skillsThe active skills the role requires and the level for each. reviewed_at is when the level was last set
holder_countLive people holding the role (a person holding it through two sources counts once)

Endpoints

List Job Roles

GET /api/v1/job-roles

Authentication required: read scope

ParameterTypeRequiredDescription
searchstringNoSearch on the role name
include_inactivebooleanNotrue lists inactive roles too. Default: false
pageintegerNoDefault: 1
per_pageintegerNoDefault: 50, max: 200

Ordered by name.

Create a Job Role

POST /api/v1/job-roles

Authentication required: write scope

{
  "name": "Site supervisor",
  "description": "Leads a shift on site",
  "required_skills": [
    { "skill_id": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2918", "required_level": "proficient" }
  ]
}
FieldRules
nameRequired, at most 150 characters, unique among live roles (ignoring case)
descriptionOptional, at most 1,000 characters
required_skillsOptional, at most 100; each skill once, active skills only

Returns 201 with the job role. 409 when the name is taken; 400 for an unknown or inactive skill.

Get a Job Role

GET /api/v1/job-roles/{jobRoleId}

Authentication required: read scope. 404 when the role does not exist or was deleted.

Update a Job Role

PATCH /api/v1/job-roles/{jobRoleId}

Authentication required: write scope

Send any of name, description, is_active and required_skills. When required_skills is sent, it replaces the role’s required levels: skills not listed stop being required, and [] removes them all. A requirement on a deactivated skill is not visible through the API and is never removed by this call. There is no delete endpoint: set is_active to false to stop a role from counting.

Replace a Person’s Imported Job Roles

PUT /api/v1/users/{userId}/job-roles

Authentication required: write scope

Use it to sync job titles from your HR system. The body lists the job roles the person should hold from this integration:

{ "job_role_ids": ["2c1b0a9f-8e7d-4c6b-9a5f-3e2d1c0b9a8f"] }
  • The call replaces only the person’s imported roles (the same source as the CSV import). A role an administrator assigned by hand, or one that follows a profile field, is never touched, so a sync cannot undo a person’s decision.
  • [] removes every imported role. At most 50 ids; every role must exist and be active.
  • Each role the person did not already hold fires the job_role.assigned webhook.

The response lists what was added and removed, and every role the person holds, with its sources:

{
  "data": {
    "user_id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789",
    "added": ["2c1b0a9f-8e7d-4c6b-9a5f-3e2d1c0b9a8f"],
    "removed": [],
    "job_roles": [
      { "job_role_id": "2c1b0a9f-8e7d-4c6b-9a5f-3e2d1c0b9a8f", "name": "Site supervisor", "is_active": true, "sources": ["import", "manual"] }
    ]
  },
  "meta": null,
  "error": null
}

Get a Person’s Skills

GET /api/v1/users/{userId}/skills

Authentication required: read scope. locale (en or fr-CA) chooses the language of skill and role names.

{
  "data": {
    "user_id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789",
    "requirements": "job_roles",
    "job_roles": [{ "id": "2c1b0a9f-8e7d-4c6b-9a5f-3e2d1c0b9a8f", "name": "Site supervisor" }],
    "summary": { "required_skills": 3, "satisfied": 2, "to_develop": 1, "counts": { "below_1": 1, "meets": 2 } },
    "skills": [
      {
        "skill_id": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2918",
        "name": "Forklift operation",
        "domain": { "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Safety" },
        "status": "below_1",
        "required_level": "proficient",
        "required_by": { "kind": "job_role", "id": "2c1b0a9f-8e7d-4c6b-9a5f-3e2d1c0b9a8f", "name": "Site supervisor" },
        "attained_level": "developing",
        "source": "completion",
        "earned_at": "2026-09-01T00:00:00.000Z",
        "valid_until": null,
        "expiring": false,
        "pending_claim_level": null
      }
    ]
  },
  "meta": null,
  "error": null
}
requirementsMeaning
job_rolesThe person holds roles that require skills
noneNo role, or roles that require nothing yet: no level is expected
statusMeaning
below_2Two or more levels below the requirement
below_1One level below
not_assessedRequired, but no level held
meets / exceedsAt or above the requirement
not_requiredHeld, but no role requires it

The list holds the skills the person holds or is required to hold, required skills first, most urgent first. Evidence notes and self-assessment notes are not included: they stay in the app.

Get the Team Skills Matrix

GET /api/v1/skills/matrix

Authentication required: read scope. Needs Skills planning (Growth and up) and the organization’s skills-planning switch on (Settings → Skills planning). The API key sees the whole organization. Every pull is recorded in the audit log (the filters and the row count, never the content).

ParameterTypeDescription
group_idUUID listPeople in these groups
job_role_idUUID listPeople holding these roles; with no skill_id, the columns are the skills these roles require
domain_idUUID listSkills in these domains
skill_idUUID listThe columns (at most 40)
statuslistKeep people with at least one cell in these statuses
searchstringFirst or last name
localestringen or fr-CA
page, per_pageintegerDefault 1 and 50, max 200

Lists accept comma-separated values (?status=below_1,below_2) or a repeated parameter.

The app’s matrix can also filter on a custom user field. The API does not offer that filter: a field an administrator keeps private would leak through who matches it. Filter by group or job role instead.

Like every list, data is the page of rows (one per person). The columns and the matrix-level flags are in meta, beside the pagination:

{
  "data": [
    {
      "id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789",
      "user": { "id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789", "first_name": "Ada", "last_name": "Roy" },
      "job_roles": ["Site supervisor"],
      "cells": [
        { "skill_id": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2918", "status": "below_1", "required_level": "proficient", "attained_level": "developing", "valid_until": null, "expiring": false, "pending_claim_level": null }
      ]
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "total": 1,
    "columns": [
      { "skill_id": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2918", "name": "Forklift operation", "domain": { "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Safety" }, "is_critical": true }
    ],
    "columns_truncated": false,
    "available_columns": 1,
    "has_requirements": true,
    "population_truncated": false
  },
  "error": null
}

Each row’s cells follow the order of meta.columns. columns_truncated is true when more than 40 skills matched (narrow with domain_id or skill_id); population_truncated is true when a status filter scanned the first 10,000 people only.


Plan errors

A plan without the feature answers 403 with an entitlement_blocked block, so you can tell a plan limit from a key problem:

{
  "data": null,
  "meta": null,
  "error": {
    "code": 403,
    "message": "Skills planning is not included in your plan",
    "entitlement_blocked": { "code": "ENTITLEMENT_BLOCKED", "flag_or_limit": "skills_planning", "current_tier": "starter", "required_tier": "growth" }
  }
}

When the plan includes skills planning but your organization turned it off, the matrix answers 403 with "reason": "SKILLS_PLANNING_OFF".


Webhooks

  • job_role.assigned: a person received a job role on their user page or through this API.
  • skill.attained: a person’s held level in a skill rose.

See Webhooks for the payloads.


Troubleshooting

ProblemCauseFix
403 with entitlement_blockedYour plan does not include the featureUpgrade, or ask your account manager
403 with reason: SKILLS_PLANNING_OFFSkills planning is turned offAn Owner or Admin turns it on in Settings → Skills planning
400 Unknown or inactive skillA skill_id does not exist or was deactivatedList the skills in the app’s Skills Library
409 on create or renameAnother live role has that nameChoose another name, or update the existing role