> ## Documentation Index
> Fetch the complete documentation index at: https://help.skillsdb.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Changelog

> Dated, additive-only history of the SkillsDB REST API

<Info>
  **Quick Summary:** Every change to the public REST API lands here with a date. Inside v1 every entry is additive — nothing here ever breaks an existing integration.
</Info>

## 2026-09-02 — Pre-launch fixes, person skills, and skill retirement (pre-release)

Still ahead of the first public release, so the corrections below land before
any integration can depend on the old shapes.

**Added**

* **Additional skills**: `GET/POST/DELETE /people/{personId}/skills` — assign
  existing catalogue skills directly to a person, list them, remove them. This
  closes the gap where recording a training required an assignment the API
  could not create. Carries `skills:read` / `skills:write`.
* **Skill retirement and delete**: `POST /skills/{id}/archive` and
  `/unarchive` (reversible; `isArchived` now on the Skill object), and
  `DELETE /skills/{id}` — a soft delete guarded by a 409 with dependency
  counts and a `?confirm=true` remedy, matching the app's library semantics.
* **Org value delete**: `DELETE /org/{dimension}/{id}` — removes a dimension
  value outright, completing the create/deactivate/delete story for all
  eleven dimensions. Guarded the same way: a 409 with dependency counts when
  people hold the value and a `?confirm=true` remedy that removes the
  memberships; a value referenced by records outside the API (assessments,
  surveys, saved views) stays undeletable — deactivate it instead.
* **Scopes in the specification**: every operation now declares its required
  scopes machine-readably (`security` array and `x-required-scopes`), and the
  security scheme lists the full catalogue — an admin can see exactly which
  scopes a key needs per endpoint.
* A new [`method-not-allowed`](/api-docs/errors/method-not-allowed) (405)
  problem type: a wrong verb on a real path is now distinguishable from a
  wrong path, and lists the allowed methods.

**Changed (pre-release corrections)**

* One name per concept across request bodies: `PATCH /people/{id}/access-level`
  takes `accessLevel` (previously `level`, while `POST /people` already said
  `accessLevel`); career sections and skill groups use `name` on both create
  and read (previously `title` — while `POST /careers` said `name`).
* Flag names are capped at 100 characters (previously the contract said 200
  while storage allowed 100 — the create appeared to work and then failed).
* Every response from any `/api/v*` path is now an RFC 9457 problem document —
  including URL typos, unknown versions (410 `version-gone`), and wrong verbs
  (405). No raw framework error body is reachable.
* Three scopes with no endpoints behind them yet (`assessments:read`,
  `certifications:read`, `certifications:write`) are no longer grantable; they
  return when their resources ship.

## 2026-08-31 — Contract corrections and security hardening (pre-release)

The API has not yet had its first public release, so the field renames below
land before any integration can depend on the old names. This is the one window
in which the contract gets corrected rather than versioned.

**Changed (pre-release corrections)**

* One column, one public name: the career-assignment person rows now use
  `externalId` and `email` (previously `employeeId` / `emailAddress`) — the same
  field names as the [Person object](/api-docs/resources/person). The bulk sync
  request field is likewise `externalId` (previously `employeeId`), now capped
  at 20 characters to match what is actually stored.
* `requirement.scope` is `all` or `going_forward` (previously `goingForward`) —
  the one enum value that had shipped outside the lowercase-snake convention.
* `GET /people/{personId}/careers` now rejects `limit` over 200 and `offset`
  over 10,000 with a validation error, as the reference always stated.
* Malformed ids in career-assignment request bodies are now a `400`
  validation error naming the field, instead of a generic `404`.
* Every create operation is documented as returning `201` (what it always did).
* The reference now documents the `{data, meta}` envelope on every response,
  every error status with the `application/problem+json` shape, the standard
  response headers (`X-Request-Id`, `RateLimit-*`, `Idempotency-Replayed`), the
  `Idempotency-Key` request header on every write, and — on each operation —
  **the scope it requires**.

**Hardened**

* `Idempotency-Key` storage is now scoped **per credential**, not per company:
  no credential can replay, collide with, or occupy a key another credential
  used. A retried write is only ever replayed to the credential that made it.
* Deactivating a person disables every API credential acting as them on the
  very next request — and that `401` is now byte-identical to a revoked key's,
  disclosing nothing about the acting person's state.
* Person-scoped training, learning-plan, flag and org reads are bounded by the
  acting identity's span of control, exactly like the people directory:
  a manager-bound credential reads its span, an out-of-span person is a `404`.
* `POST /people` and the email-change operation return a clean `409` when the
  address is already registered elsewhere in SkillsDB (the address is globally
  unique; the conflict names no holder).
* `GET /org/manager-tree` is bounded by the acting identity like every other
  person-scoped read: admin tiers get the full hierarchy, a manager-bound
  credential gets its own subtree, an employee-bound credential is refused.
* `GET /skill-groups/{id}/trainer-count` and both `renew-training` operations
  return the uniform `404` for an id outside your company (previously a `200`
  with zero counts).

## 2026-08-27 — Postman collection

**Added**

* A generated [Postman collection](/api-docs/skillsdb-public-api.postman_collection.json) covering every endpoint (154 requests across 14 folders). Authentication is Bearer at the **collection level** (`{{apiKey}}` variable) and the common headers are applied by a collection pre-request script, so every request inherits both — import, paste your key once, and play. Bodies come prefilled with working examples; writes carry a one-click optional `Idempotency-Key`. Regenerated with the OpenAPI artifact on every contract change.

## 2026-08-27 — Org structure and hardening

**Added**

* **Org structure** ([objects](/api-docs/resources/org-structure)): the eleven dimensions at `GET|POST /api/v1/org/{dimension}` and `PATCH /org/{dimension}/{id}` (rename or deactivate — values never delete), `GET /org/{dimension}/{id}/usage` and `/people`, `GET /org/dimensions`, and `GET /org/manager-tree` — the company's reporting hierarchy as depth-ordered, company-scoped rows. New scopes: `org:read`, `org:write`.

**Hardened**

* The system-admin bypass header (`nest-admin-secret`) is now **rejected outright** on every `/api/v1` request — including alongside a valid API key — instead of being ignored. The public API authenticates by API key only.

## 2026-08-27 — Training data and flags

**Added**

* **Training records** ([object](/api-docs/resources/training)): `GET /api/v1/trainings` (filter by person, skill, status, or expiry — `expiredOnly=true` / `expiresWithinDays=N`), `GET /trainings/{id}`, `GET /trainings/{id}/evidence` (time-limited links), `POST /trainings` (with `preApproved` and `notApplicable`), `PATCH /trainings/{id}`.
* **Training approval**: `GET /training-approvals?status=` across every decidable state, `POST /trainings/{id}/approve|reject|cancel` (reasons required to reject/cancel), `GET /trainings/{id}/potential-approvers?expertsOnly=`, and per person `GET /people/{id}/approval-activity` (what they decided — spans superseded records) and `/approval-requests` (what they submitted). Approver entitlement is evaluated point-in-time and not retained on past decisions.
* **Renewal**: `POST /skills/{id}/renew-training` (fans out — **bulk bucket**) with `GET /skills/{id}/renew-training/preview`, and the merged `GET /people/{personId}/skills/{skillId}/training-log` — live and superseded records together, each flagged `isSuperseded`.
* **Learning plans** ([object](/api-docs/resources/learning-plan)): `GET /people/{id}/learning-plans`, `GET|PATCH /learning-plans/{id}` (due-date and state rules enforced), `PUT /learning-plans/{id}/rating` (three values, **null clears** — and only the plan's owner can rate), and `GET|POST|DELETE /learning-items/{id}/people`.
* **Flags** ([objects](/api-docs/resources/flag)) — two independent systems: people flags (CRUD, first-class enable/disable, reorder, dependencies, guarded delete, per-person assignment, the visibility setting) and skill flags (the same plus trainer designation, four-scope assignment at `/skill-flag-assignments/{scope}`, and `GET /skill-groups/{id}/trainer-count`). Disabling a flag hides every existing assignment from reads without deleting anything — stated on the operations because reconciliation breaks otherwise.
* New scopes: `training:read`, `training:write`, `flags:read`, `flags:write`.

## 2026-08-27 — People management

**Added**

* **Person** resource ([object](/api-docs/resources/person)):
  * Directory: `GET /api/v1/people` (bounded by the acting identity's access level, filterable by name/email/externalId), `GET /people/count`, `GET /people/{id}`, `GET /people/{id}/avatar` (a resolved short-lived URL).
  * Record: `POST /people` (creates the authentication identity too; duplicate email → conflict), `PATCH /people/{id}` — a **true partial update**: absent means unchanged, explicit null clears. Email, access level, manager, job title and people type each move through their own operation.
  * Access levels are **slugs, never integers** (`basic`, `manager`, `admin`, `fullaccess`, plus three read-only legacy levels), and a grant can never exceed the acting identity's own level (403).
  * `PATCH /people/{id}/manager` refuses a reporting loop with a conflict naming who and at what depth.
  * Lifecycle: `POST /people/{id}/deactivate` (stamps the inactive date) and `/activate`. **There is no delete** — the product's delete is an irreversible hard delete, so deactivation is the lifecycle operation.
  * Reporting structure: `GET /people/{id}/direct-reports`, `GET /people/{id}/span-of-control` (subject included at level 0), `GET /people/managers?excludeSubtreeOf=`, `GET /people/distribution`.
  * Org schema: `GET`/`POST` (additive)/`DELETE /people/{id}/org-schema`, and `PUT /people/{id}/org-schema/{dimension}` to **replace one dimension exactly** — the shape an HRIS sync needs.
* **Person attributes**: `GET|POST|PATCH|DELETE /api/v1/job-titles` and `/api/v1/people-types` (deletes refuse with a conflict and the holder count while people still hold them), `GET /api/v1/access-levels`, `GET /api/v1/countries`.

## 2026-08-27 — Skills management

**Added**

* **Skill** resource ([object](/api-docs/resources/skill)):
  * `GET /api/v1/skills` — the company skill library, filterable by `name` and `description`. Each entry carries its `usage`: every career that **includes** it (attached directly or under a section, Required or not), how many people hold it, and whether any career including it has training dates enabled.
  * `GET /api/v1/skills/{id}` — one skill: name, description, and whether it is a certification.
  * `POST /api/v1/skills` / `PATCH /api/v1/skills/{id}` — create and update map exactly `name` and `description`. There is deliberately **no delete** — no such operation exists in the product.
  * `GET /api/v1/skills/{id}/people` — everyone who holds the skill, directly or through a career that includes it. Respects span of control.
  * `GET /api/v1/skills/{id}/versions` — name/description snapshots with editor, date, change note and the baseline marker.
  * `GET /api/v1/skills/{id}/activity` — a **version log, not a change feed**: exactly three event types (`baseline`, `version_created`, `renew_training`).
* **Skill Request** workflow ([object](/api-docs/resources/skill-request)): `GET`/`POST /api/v1/skill-requests`, `POST /api/v1/skill-requests/{id}/approve` and `/reject`. Approving creates the library skill and assigns it to the requester; deciding an already-decided request returns a [conflict](/api-docs/errors/conflict).

**Changed (pre-release)**

* The career definition no longer carries certifications (`GET /api/v1/careers/{id}/certifications` and the `certifications` array were removed) — certification data gets its own resource in a later release.
* A career skill's requirement fields were restructured into a single stored-configuration object: `requirement: { isRequired, scope, effectiveFrom, setBy }`. `scope` is `all` or `going_forward`. See [the Career object](/api-docs/resources/career) for why this is a configuration, never a resolved boolean.

<Note>
  Both changes land before the API's first general release, while no integration can depend on the old shape. After GA, changes of this kind would ship in a new major version instead.
</Note>

## 2026-08-25 — Bulk people sync

**Added**

* `POST /api/v1/bulk/people` — create-or-update up to **500** people in one call, matched by email within your company. Set `isActive: false` to deactivate (a soft delete: the person and their history are retained).

The batch returns `200` whenever it was processed — read each record's `status` (`created`, `updated`, `deactivated`, `failed`) and the `summary`, not the HTTP code. One bad record never fails the rest of the batch.

<Note>
  Two things to design your sync around. **Send an `Idempotency-Key`** — this is the endpoint where a retry matters most; a replayed request returns the original response instead of double-creating. And **email addresses are unique across SkillsDB**, not per company: an address already registered to another organization is reported as a failed record rather than silently attaching to it.
</Note>

Bulk sync counts against the **bulk** rate-limit bucket, which is deliberately far lower than the read allowance — a workforce sync is a small number of very large calls.

## 2026-08-25 — Career assignment

**Added**

* **Career assignment**: who holds a career, and the operations that change it.
  * `GET /api/v1/careers/{id}/people` — everyone holding the career, with their level, the server-stamped `assignedAt` and the caller-supplied `initiationDate`.
  * `GET /api/v1/careers/{id}/distribution` — headcount grouped by level and by people-flag, aggregated server-side. A manager sees only their span of control.
  * `GET /api/v1/people/{personId}/careers` — every career a person holds or previously held, each marked `isPrevious`. Previous assignments are included by default; pass `?includePrevious=false` to exclude them.
  * `POST /api/v1/careers/{id}/people` — assign people, each at their own level, with an optional `initiationDate`. **Assigning is never an update**: anyone already holding the career is skipped and their level is left unchanged.
  * `PATCH /api/v1/careers/{id}/people/{personId}` — the only way to move someone between levels of a career they already hold.
  * `DELETE /api/v1/careers/{id}/people` — unassign people. Career history is recorded before the live assignment is removed, so the career reappears on the person marked `isPrevious`.
  * `DELETE /api/v1/people/{personId}/careers` — clear every career one person holds, in a single call. Scoped to the named person.

<Note>
  Two behaviours worth designing around. **Assigning fans out**: beyond the assignment row it grants the learnings attached to the target level and attaches the career's skills, so one call against many people is a large write — budget it against your rate limit accordingly. **Re-assigning will not correct a level**: an integration that assumes it does will silently do nothing; use the change-level operation.
</Note>

## 2026-08-20 — v1 initial surface

**Added**

* **Authentication and platform**: API keys (`Authorization: Bearer sdb_live_...`) with scopes, acting-person binding, rotation with overlap, immediate revocation. `GET /api/v1/ping` connectivity check. RFC 9457 problem+json errors with the [error catalogue](/api-docs/errors/index). `X-Request-Id` on every response. `RateLimit-*` headers (measurement mode). `Idempotency-Key` support on writes. Unauthenticated spec downloads at `/api/v1/openapi.json` and `/api/v1/openapi.yaml`.
* **Career** resource: list/get/create/update/delete (dependency-guarded with `?confirm=true`), full definition read, levels, sections, skill groups, skills with per-level activation and resolved benchmarks, learnings, certifications, settings (with the assessments-requires-proficiency rule), and the activity feed.
* **Career Type** resource: list/get/create/update/delete (delete refuses while in use).
* **Grading Scale**: read-only company scale — the rungs all benchmarks reference.
