From 8dde6354f7be36f10ad7a943028a3e66b44a29f1 Mon Sep 17 00:00:00 2001 From: Ravi Sharma Date: Thu, 6 Aug 2026 13:34:57 +0530 Subject: [PATCH 1/3] Updating Api Doc for TeamMembers and Departments Endpoint --- ...public-api-team-members-and-departments.md | 240 ++++++++++++++ openapi.json | 310 +++++++++++++++--- 2 files changed, 505 insertions(+), 45 deletions(-) create mode 100644 docs/plans/2026-08-04-hdr-6545-public-api-team-members-and-departments.md diff --git a/docs/plans/2026-08-04-hdr-6545-public-api-team-members-and-departments.md b/docs/plans/2026-08-04-hdr-6545-public-api-team-members-and-departments.md new file mode 100644 index 0000000..602aff0 --- /dev/null +++ b/docs/plans/2026-08-04-hdr-6545-public-api-team-members-and-departments.md @@ -0,0 +1,240 @@ +# HDR-6545 — Document Issuer Hierarchy changes on Public API Team Members + Departments + +**Ticket:** [HDR-6545](https://accredible.atlassian.net/browse/HDR-6545) — the documentation task, covering +two stories in the [HDR-6012](https://accredible.atlassian.net/browse/HDR-6012) epic: + +- [HDR-6013](https://accredible.atlassian.net/browse/HDR-6013) (BE subtask HDR-6097) — Team Members: new fields + key-kind authorization +- [HDR-6014](https://accredible.atlassian.net/browse/HDR-6014) (BE subtask HDR-6098) — Departments: scoped API key enforcement + +**Reference PR for structure:** [api-documentation#130](https://github.com/accredible/api-documentation/pull/130) +**Branch:** `feature/HDR-6545/update-api-documentation-team-members-departments` + +## Context + +The Issuer Hierarchy work adds a set of hierarchy fields to the public `team_members` contract and +replaces user-identity authorization with **API-key-kind** authorization across both Team Members and +Departments. Three backend PRs implement it and are all currently **open**: + +- [accredible-credential-api#5277](https://github.com/accredible/accredible-credential-api/pull/5277) — team members create (`POST`) + serializer fields +- [accredible-credential-api#5284](https://github.com/accredible/accredible-credential-api/pull/5284) — team members update (`PUT`) +- [accredible-credential-api#5290](https://github.com/accredible/accredible-credential-api/pull/5290) — departments key-scope enforcement +- team members show/destroy work is on branch `temp-issuer-hierarchy` (not yet PR'd) + +`openapi.json` in this repo is the public source of truth for `docs.api.accredible.com`, and it +currently documents none of this. This change brings the four Team Members operations and the five +documented Departments operations in line with the shipped contract, following the same structure as +PR #130 (the `pathways` field): additive schema properties + matching example bodies + updated cURL +samples, plus prose for the authorization rules. + +**Merge order:** this docs PR must not merge to `develop`/`master` before #5277, #5284 and #5290 ship, +otherwise the published docs describe behaviour the API doesn't have yet. Call that out in the PR body. + +### Authoritative sources for the contract + +| Fact | Source (accredible-credential-api) | +|---|---| +| Response fields | `app/serializers/api/v1/public/team_member_serializer.rb` (`hierarchy_permissions`) | +| Accepted request params | `app/controllers/api/v1/organization_permissions_controller.rb:645` (`team_member_params`) | +| `access_level` wire ↔ DB names | `config/initializers/frontend_data_mappings/organization_permissions.rb` | +| Key-kind authorization rules | `app/services/organization_permissions/validations/api_key_authorization.rb`, `api_key_management_eligibility.rb` | +| `PUT` replace semantics | `app/services/organization_permissions/public_api/bulk_updater.rb` | +| `DELETE` scope rules | `app/services/organization_permissions/public_api/bulk_destroyer.rb` | + +### Decisions taken + +- Document **all six** response fields and **all five** accepted request fields, not just the three + named in the ticket — integrators see all six on the wire regardless. +- Authorization rules go in **prose only** (operation `description` + field descriptions). No new + `400`/`404` response objects: the spec documents error responses almost nowhere today (one lone + `422` on `POST /v2/credentials/bulk_create`), and adding them here would be an inconsistent + one-off. +- Fix the pre-existing `department_id` → `department` bug in the four response schemas/examples. +- Document the two `PUT` behaviour changes (`email` ignored; omitted departments removed). +- Do **not** mention the `issuer_hierarchy` feature flag — public docs describe the target state. + +--- + +## The contract to document + +All fields live inside `department_permissions[].permissions`. + +**Response — six new fields** (append after `groups`, matching serializer key order): + +| Field | Type | Notes | +|---|---|---| +| `api` | boolean | API key access for the Department | +| `team` | boolean | Can manage other Team Members in the Department | +| `spotlight_directory` | boolean | **Response-only** — not accepted on input | +| `access_level` | string | `organization_admin` \| `department_admin` \| `team_member` \| `developer` | +| `analytics_email` | boolean | Receives scheduled analytics emails for the Department | +| `manage_dept_admins_and_devs` | boolean | Can manage Department Admins and Developers | + +**Request — five new fields** (insert after `pathways`, before `groups`): `api`, `team`, +`access_level`, `analytics_email`, `manage_dept_admins_and_devs`. All booleans `default: false`. +`access_level` request enum is only the three assignable values — `team_member` (default), +`department_admin`, `developer` — since `organization_admin` is always rejected. The response enum +carries all four. + +**Key-kind rules to state in prose:** + +| `access_level` | Account-wide key | Department-specific key | +|---|---|---| +| `organization_admin` | never | never | +| `department_admin` | ✅ any department in the account | ✗ | +| `developer` | ✅ any department in the account | ✗ | +| `team_member` | ✅ | ✅ own department only | + +Both `department_admin` and `developer` additionally require a plan that supports them +(`validations/plan_entitlement.rb`). + +--- + +## Changes — all in `openapi.json` + +Two path objects, four operations: `/v1/team_members` (`post`, line ~9556) and +`/v1/team_members/{id}` (`get`, `put`, `delete`, line ~9821). + +### 1. Response schemas + examples — all four operations + +For each of the four `responses.200.content["application/json"]` blocks: + +- Append the six response properties to the `permissions` `properties` object, after `groups`. +- Rename `department_id` → `department` in the `properties` object **and** in the `required` array + (`["department_id", "permissions"]` → `["department", "permissions"]`). +- Add the six fields to the `example` body's `permissions` objects (both departments, all four + operations), and rename the example key `department_id` → `department`. + +Give the two example departments contrasting values so the shape reads clearly — e.g. dept 34 as a +`department_admin` with `manage_dept_admins_and_devs: true`, dept 35 as a plain `team_member` with +everything false. + +### 2. Request schemas + examples — `post` and `put` + +- Add the five request properties (with `default: false` on the booleans, `enum` + + `default: "team_member"` on `access_level`) after `pathways` in the + `requestBody.content["application/json"].schema` → `department_permissions.items.permissions.properties`. + Request keeps `department_id` — that is correct and unchanged. +- Mirror the new fields into the `requestBody` `example` bodies. + +### 3. cURL samples — `post` and `put` + +Update the single-line escaped `source` string in `x-code-samples` for both operations so the +payload matches the new request example exactly. `get` and `delete` samples have no body — no change. + +### 4. Prose — operation descriptions + +- **`post` description** — after the existing sentence, add the key-kind matrix in words: Department + Admins and Developers require an account-wide API key; a department-specific key may only create + Team Members in its own department; `organization_admin` cannot be created via the API (use the + Issuer Dashboard); omitting `access_level` creates a Team Member. +- **`put` description** — add: the payload **replaces** the member's permission set (departments + present are created or updated, departments omitted are removed); removal by omission requires an + account-wide key; updating an existing Department Admin or Developer requires an account-wide key; + Organization Admins cannot be updated via the API. +- **`get` description** (currently `""`) — one sentence: an account-wide key returns the member's + permissions across the whole account; a department-specific key resolves only Team Members within + its own department. +- **`delete` description** (currently `""`) — an account-wide key removes the member from every + department in the account; a department-specific key removes only a Team Member in its own + department; Organization Admins cannot be removed via the API. + +### 5. `put` request schema — the two behaviour changes + +- Annotate the existing `email` property: `"Ignored. A TeamMember's email address cannot be changed + through this endpoint."` (keep the property so integrators currently sending it understand why it + has no effect, rather than silently dropping it from the docs). +- Extend the `name` description: a blank value leaves the existing name unchanged. +- Extend the `department_permissions` description — currently *"This parameter will override any + existing permissions, not append to them."* — to state explicitly that a department omitted from + the array has its permission **removed**, and that removal requires an account-wide API key. + +--- + +--- + +## Departments (HDR-6014 / BE PR #5290) + +No schema changes — this story is authorization only, so the docs change is **prose in each operation's +`description`**, consistent with the "no new error response objects" decision above. Behaviour per +`app/controllers/api/v1/departments_controller.rb`: + +| Operation | Account-wide key | Department-specific key | +|---|---|---| +| `POST /v1/departments` | full access | `403` `No Permission to manage Departments.` | +| `GET /v1/departments/{department_id}` | any Department | own only; others `404` `No department found` | +| `PUT /v1/departments/{department_id}` | any Department | own only; others `404` `No department found` | +| `DELETE /v1/departments/{department_id}` | full access | `403` `No Permission to manage Departments.` | +| `POST /v1/departments/search` | all Departments | scoped to its own Department | + +Two deliberate deviations, both agreed with the ticket owner: + +- **Search.** PR #5290 currently returns `Organization.none` (an empty array) for a department-specific + key. That contradicts AC-E4-S2-2 ("scoped, not 403") and would silently return zero results to + existing integrators. The docs describe the **intended** behaviour — scoped to its own Department — + on the basis that the backend will be corrected. **If #5290 merges as-is, this line is wrong.** +- **`GET /v1/departments`** (index) is affected by the change but has never been documented in + `openapi.json`. Left undocumented; adding it is separate work. + +Note also that the ticket's ACs say other-Department `show`/`update` return `403`; the implementation +returns `404`. The docs follow the implementation. + +--- + +## Documented-intent deviations — backend changes required before this merges + +Three places where the docs describe **intended** behaviour that the current code does not implement. +Each was an explicit call by the ticket owner. All three need a backend change, or the docs ship wrong: + +| # | Documented | Actual behaviour today | Fix needed | +|---|---|---|---| +| 1 | `POST /v1/departments/search` with a department-specific key returns that key's own Department | Returns `[]` (`Organization.none` in `public_search`) | PR #5290 | +| 2 | `role` for a `department_admin` must be `editor` | `editor`, `credential_issuer` and `viewer` are all accepted — only `corporation_admin` is locked to `editor` in `PermissionLocks#locks_for` | add a `role` lock to `organization_admin_locks` | +| 3 | `api: true` on a `department_admin` is permitted and turns every other permission on | Always `400 directory must be true for organization_admin` — the master switch requires `directory: true`, but `directory` is absent from `team_member_params` and pinned to `false` by `BulkCreator::DEFAULT_PERMISSION_PARAMS` | allow `directory` through the public API, or exempt it from the master switch | + +Deviation 3 also means the `api` cell for `department_admin` in the permitted-values table reads +`true or false`; until the backend is fixed, sending `api: true` returns `400`. + +--- + +## Constraints while editing + +- **Hand-edit `openapi.json`; do not run `process_openapi.py`.** It rewrites the whole file with + `json.dump(indent=4)`, which would collapse the hand-formatted inline arrays (`[1, 2, 3]`) and + produce a diff across all 10,607 lines. `add_curl_examples.py` likewise must not be re-run. +- Match surrounding formatting exactly: 4-space indent, inline `[1, 2, 3]` in the `post` examples, + expanded multi-line arrays in the `{id}` examples (existing inconsistency — preserve per-block). +- Purely additive on request/response properties. The only rename is `department_id` → `department` + in **responses**. +- Leave `POST` documented as `200` — the controller renders without an explicit status, so `200` is + correct despite the ticket's AC text saying `201`. + +## Verification + +1. **Valid JSON, unchanged elsewhere:** + ``` + python3 -m json.tool openapi.json > /dev/null && echo OK + git diff --stat # expect: openapi.json only + ``` +2. **Every documented permission key matches the serializer.** Extract the `permissions` property + keys for all four operations and diff against the serializer's key list + (`designs, emails, credential_attributes, analytics, settings, role, pathways, groups, api, team, + spotlight_directory, access_level, analytics_email, manage_dept_admins_and_devs`) — a short + throwaway `python3` script over `openapi.json`. +3. **Schema/example agreement:** confirm each operation's `example` body carries exactly the keys its + schema declares, and that no response block still contains `department_id`: + ``` + grep -c '"department_id"' openapi.json # only request-side occurrences should remain + ``` +4. **Render check:** `python3 -m http.server 8080`, open `http://localhost:8080`, and confirm the four + Team Members operations show the new fields, descriptions, and cURL samples correctly. +5. **Cross-check against the backend specs** — `spec/requests/api/v1/public_api/team_members_spec.rb` + (`hierarchy_fields` list, line ~1122) and `spec/serializers/api/v1/public/team_member_serializer_spec.rb` + (line ~131) are the assertions that pin this contract; the documented field set must match them. + +## PR + +Follow the PR #130 template: ticket link, a "What does this PR do?" list of the changes, type +`📦 Chore/Documentation`, checklist. Explicitly note in the body: +- the `department_id` → `department` response fix as a drive-by correction of a pre-existing bug, +- the two `PUT` behaviour clarifications, +- that this must not merge before accredible-credential-api#5277 and #5284 ship. diff --git a/openapi.json b/openapi.json index c04d3c1..5ce1054 100644 --- a/openapi.json +++ b/openapi.json @@ -8414,7 +8414,7 @@ }, "summary": "Create a New Department", "operationId": "Create a New Department", - "description": "You may create a Department using this action. It takes a JSON object containing a Department.", + "description": "You may create a Department using this action. It takes a JSON object containing a Department.\n\n**Creating a Department requires an account-wide API key.** A department-specific key receives `403` with `No Permission to manage Departments.`", "tags": [ "Departments" ], @@ -8580,7 +8580,7 @@ }, "summary": "View a Department", "operationId": "View a Department", - "description": "", + "description": "View a single Department.\n\nAn account-wide API key can view any Department in the account. A department-specific key can only view the Department it is scoped to - any other Department returns `404` with `No department found`.", "tags": [ "Departments" ], @@ -8675,7 +8675,7 @@ }, "summary": "Update a Department", "operationId": "Update a Department", - "description": "", + "description": "Update a Department using this action. It takes a JSON object containing a Department.\n\nAn account-wide API key can update any Department in the account. A department-specific key can only update the Department it is scoped to - any other Department returns `404` with `No department found`.", "tags": [ "Departments" ], @@ -8834,7 +8834,7 @@ }, "summary": "Delete a Department", "operationId": "Delete a Department", - "description": "", + "description": "Delete a Department using this action.\n\n**Deleting a Department requires an account-wide API key.** A department-specific key receives `403` with `No Permission to manage Departments.`", "tags": [ "Departments" ], @@ -8942,7 +8942,7 @@ }, "summary": "Search for Departments", "operationId": "Search for Departments", - "description": "You can search for departments by various search criteria.", + "description": "You can search for departments by various search criteria.\n\nAn account-wide API key searches every Department in the account. A department-specific key only returns the Department it is scoped to.", "tags": [ "Departments" ], @@ -9581,7 +9581,7 @@ "items": { "type": "object", "properties": { - "department_id": { + "department": { "type": "number" }, "permissions": { @@ -9613,6 +9613,30 @@ "items": { "type": "number" } + }, + "api": { + "type": "boolean" + }, + "team": { + "type": "boolean" + }, + "spotlight_directory": { + "type": "boolean" + }, + "access_level": { + "type": "string", + "enum": [ + "organization_admin", + "department_admin", + "team_member", + "developer" + ] + }, + "analytics_email": { + "type": "boolean" + }, + "manage_dept_admins_and_devs": { + "type": "boolean" } }, "required": [ @@ -9626,7 +9650,7 @@ } }, "required": [ - "department_id", + "department", "permissions" ] } @@ -9642,7 +9666,7 @@ "email": "person@example.com", "department_permissions": [ { - "department_id": 34, + "department": 34, "permissions": { "designs": true, "emails": true, @@ -9651,11 +9675,17 @@ "settings": true, "role": "editor", "pathways": true, - "groups": [1, 2, 3] + "groups": [1, 2, 3], + "api": false, + "team": true, + "spotlight_directory": false, + "access_level": "department_admin", + "analytics_email": true, + "manage_dept_admins_and_devs": true } }, { - "department_id": 35, + "department": 35, "permissions": { "designs": false, "emails": false, @@ -9664,7 +9694,13 @@ "settings": false, "role": "viewer", "pathways": false, - "groups": null + "groups": null, + "api": false, + "team": false, + "spotlight_directory": false, + "access_level": "team_member", + "analytics_email": false, + "manage_dept_admins_and_devs": false } } ] @@ -9676,7 +9712,7 @@ }, "summary": "Create a Team Member", "operationId": "Create a Team Member", - "description": "Create a TeamMember using this action. It requires a JSON object containing an TeamMember.", + "description": "Create a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\nThe `access_level` of each `department_permissions` entry determines what is created, and authorization is based on the type of API key you use:\n\n| `access_level` | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| `team_member` (default) | Any Department in the account | Its own Department only |\n| `department_admin` | Any Department in the account | Not permitted |\n| `developer` | Any Department in the account | Not permitted |\n| `organization_admin` | Not permitted | Not permitted |\n\n**Note:** `department_admin` and `developer` also require a plan that supports those roles. `organization_admin` can never be created through the API - use the Issuer Dashboard. Omitting `access_level` creates a regular Team Member, so existing integrations are unaffected.\n\n**Permitted permission values by `access_level`.** Each `access_level` locks certain permissions to fixed values, and a request that sets a locked permission to any other value is rejected:\n\n| Field | `team_member` | `department_admin` | `developer` |\n|-------|---------------|--------------------|-------------|\n| `access_level` | `\"team_member\"` | `\"department_admin\"` | `\"developer\"` |\n| `role` | `editor`, `credential_issuer` or `viewer` | `editor` | `viewer` |\n| `designs` | `true` or `false` | `true` or `false` | `false` |\n| `emails` | `true` or `false` | `true` or `false` | `false` |\n| `credential_attributes` | `true` or `false` | `true` or `false` | `false` |\n| `analytics` | `true` or `false` | `true` or `false` | `false` |\n| `settings` | `true` or `false` | `true` or `false` | `false` |\n| `pathways` | `true` or `false` | `true` or `false` | `false` |\n| `api` | `false` | `true` or `false` | `true` |\n| `team` | `false` | `true` or `false` | `false` |\n| `analytics_email` | `false` | `true` or `false` | `false` |\n| `manage_dept_admins_and_devs` | `false` | `true` or `false` | `false` |\n| `groups` | Array of Group IDs to limit access to | Ignored - access is not Group-limited | Ignored - access is not Group-limited |\n\n**Note:** omitting a boolean permission is equivalent to sending `false`, so a `developer` needs only `api: true` and `role: \"viewer\"`. Sending a value the table does not permit returns `400` with a message of the form ` must be for `.\n\n**Note:** `api` acts as a master switch for a `department_admin`. Setting `api: true` makes the Department Admin a full-access administrator of that Department, so every other permission - `team`, `designs`, `emails`, `credential_attributes`, `analytics`, `settings`, `pathways`, `manage_dept_admins_and_devs` and `analytics_email` - must also be `true`.", "tags": [ "Team Members" ], @@ -9698,7 +9734,12 @@ "settings": true, "role": "editor", "pathways": true, - "groups": [1, 2, 3] + "groups": [1, 2, 3], + "api": false, + "team": true, + "access_level": "department_admin", + "analytics_email": true, + "manage_dept_admins_and_devs": true } }, { @@ -9711,7 +9752,12 @@ "settings": false, "role": "viewer", "pathways": false, - "groups": null + "groups": null, + "api": false, + "team": false, + "access_level": "team_member", + "analytics_email": false, + "manage_dept_admins_and_devs": false } } ] @@ -9770,6 +9816,36 @@ "default": false, "description": "Should the TeamMember have access to Pathways for the given Department?" }, + "api": { + "type": "boolean", + "default": false, + "description": "Should the TeamMember have access to API keys for the given Department?" + }, + "team": { + "type": "boolean", + "default": false, + "description": "Should the TeamMember be able to manage other Team Members in the given Department?" + }, + "access_level": { + "type": "string", + "enum": [ + "team_member", + "department_admin", + "developer" + ], + "default": "team_member", + "description": "The TeamMember's access level in the given Department. \"department_admin\" and \"developer\" require an account-wide API key and a plan that supports them; \"team_member\" can be created with either an account-wide or a department-specific key. \"organization_admin\" cannot be assigned through the API - use the Issuer Dashboard." + }, + "analytics_email": { + "type": "boolean", + "default": false, + "description": "Should the TeamMember receive scheduled Analytics emails for the given Department?" + }, + "manage_dept_admins_and_devs": { + "type": "boolean", + "default": false, + "description": "Should the TeamMember be able to manage Department Admins and Developers in the given Department?" + }, "groups": { "type": "array", "description": "Limit the TeamMember access to specific Groups in the Department.", @@ -9813,7 +9889,7 @@ { "lang": "cURL", "label": "cURL", - "source": "curl -X POST \\\n \"https://api.accredible.com/v1/team_members\" \\\n -H \"Authorization: Token token=YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"team_member\": {\n \"name\": \"John Doe\",\n \"email\": \"person@example.com\",\n \"department_permissions\": [\n {\n \"department_id\": 34,\n \"permissions\": {\n \"designs\": true,\n \"emails\": true,\n \"credential_attributes\": true,\n \"analytics\": true,\n \"settings\": true,\n \"role\": \"editor\",\n \"pathways\": true,\n \"groups\": [1, 2, 3]\n }\n },\n {\n \"department_id\": 35,\n \"permissions\": {\n \"designs\": false,\n \"emails\": false,\n \"credential_attributes\": false,\n \"analytics\": false,\n \"settings\": false,\n \"role\": \"viewer\",\n \"pathways\": false,\n \"groups\": null\n }\n }\n ]\n }\n}'" + "source": "curl -X POST \\\n \"https://api.accredible.com/v1/team_members\" \\\n -H \"Authorization: Token token=YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"team_member\": {\n \"name\": \"John Doe\",\n \"email\": \"person@example.com\",\n \"department_permissions\": [\n {\n \"department_id\": 34,\n \"permissions\": {\n \"designs\": true,\n \"emails\": true,\n \"credential_attributes\": true,\n \"analytics\": true,\n \"settings\": true,\n \"role\": \"editor\",\n \"pathways\": true,\n \"groups\": [1, 2, 3],\n \"api\": false,\n \"team\": true,\n \"access_level\": \"department_admin\",\n \"analytics_email\": true,\n \"manage_dept_admins_and_devs\": true\n }\n },\n {\n \"department_id\": 35,\n \"permissions\": {\n \"designs\": false,\n \"emails\": false,\n \"credential_attributes\": false,\n \"analytics\": false,\n \"settings\": false,\n \"role\": \"viewer\",\n \"pathways\": false,\n \"groups\": null,\n \"api\": false,\n \"team\": false,\n \"access_level\": \"team_member\",\n \"analytics_email\": false,\n \"manage_dept_admins_and_devs\": false\n }\n }\n ]\n }\n}'" } ] } @@ -9846,7 +9922,7 @@ "items": { "type": "object", "properties": { - "department_id": { + "department": { "type": "number" }, "permissions": { @@ -9879,12 +9955,36 @@ "type": "number" }, "nullable": true + }, + "api": { + "type": "boolean" + }, + "team": { + "type": "boolean" + }, + "spotlight_directory": { + "type": "boolean" + }, + "access_level": { + "type": "string", + "enum": [ + "organization_admin", + "department_admin", + "team_member", + "developer" + ] + }, + "analytics_email": { + "type": "boolean" + }, + "manage_dept_admins_and_devs": { + "type": "boolean" } } } }, "required": [ - "department_id", + "department", "permissions" ] } @@ -9900,7 +10000,7 @@ "email": "person@example.com", "department_permissions": [ { - "department_id": 34, + "department": 34, "permissions": { "designs": true, "emails": true, @@ -9913,11 +10013,17 @@ 1, 2, 3 - ] + ], + "api": false, + "team": true, + "spotlight_directory": false, + "access_level": "department_admin", + "analytics_email": true, + "manage_dept_admins_and_devs": true } }, { - "department_id": 35, + "department": 35, "permissions": { "designs": false, "emails": false, @@ -9926,7 +10032,13 @@ "settings": false, "role": "viewer", "pathways": false, - "groups": null + "groups": null, + "api": false, + "team": false, + "spotlight_directory": false, + "access_level": "team_member", + "analytics_email": false, + "manage_dept_admins_and_devs": false } } ] @@ -9938,7 +10050,7 @@ }, "summary": "View a Team Member", "operationId": "View a Team Member", - "description": "", + "description": "View a TeamMember and their permissions in each Department.\n\nAn account-wide API key returns the TeamMember's permissions across every Department in the account. A department-specific key only resolves Team Members within its own Department.", "tags": [ "Team Members" ], @@ -9982,7 +10094,7 @@ "items": { "type": "object", "properties": { - "department_id": { + "department": { "type": "number" }, "permissions": { @@ -10015,12 +10127,36 @@ "type": "number" }, "nullable": true + }, + "api": { + "type": "boolean" + }, + "team": { + "type": "boolean" + }, + "spotlight_directory": { + "type": "boolean" + }, + "access_level": { + "type": "string", + "enum": [ + "organization_admin", + "department_admin", + "team_member", + "developer" + ] + }, + "analytics_email": { + "type": "boolean" + }, + "manage_dept_admins_and_devs": { + "type": "boolean" } } } }, "required": [ - "department_id", + "department", "permissions" ] } @@ -10036,7 +10172,7 @@ "email": "person@example.com", "department_permissions": [ { - "department_id": 34, + "department": 34, "permissions": { "designs": false, "emails": false, @@ -10049,11 +10185,17 @@ 1, 2, 3 - ] + ], + "api": false, + "team": true, + "spotlight_directory": false, + "access_level": "department_admin", + "analytics_email": true, + "manage_dept_admins_and_devs": true } }, { - "department_id": 35, + "department": 35, "permissions": { "designs": false, "emails": false, @@ -10062,7 +10204,13 @@ "settings": false, "role": "viewer", "pathways": false, - "groups": null + "groups": null, + "api": false, + "team": false, + "spotlight_directory": false, + "access_level": "team_member", + "analytics_email": false, + "manage_dept_admins_and_devs": false } } ] @@ -10074,7 +10222,7 @@ }, "summary": "Update a Team Member", "operationId": "Update a Team Member", - "description": "Update a TeamMember using this action. It requires a JSON object containing an TeamMember.", + "description": "Update a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\n**The `department_permissions` array replaces the TeamMember's entire permission set.** A Department present in the array is created or updated; a Department omitted from the array has its permission removed.\n\nAuthorization is based on the type of API key you use. An account-wide key can update a TeamMember in any Department in the account, and is required in order to update a TeamMember who is currently a Department Admin or a Developer, to set `access_level` to `department_admin` or `developer`, or to remove a permission by omitting its Department. A department-specific key can only update a Team Member in its own Department.\n\n**Note:** an Organization Admin cannot be updated through the API, and `organization_admin` cannot be assigned through the API - use the Issuer Dashboard. Only `name` can be changed on the TeamMember record itself.\n\n**Permitted permission values by `access_level`.** Each `access_level` locks certain permissions to fixed values, and a request that sets a locked permission to any other value is rejected:\n\n| Field | `team_member` | `department_admin` | `developer` |\n|-------|---------------|--------------------|-------------|\n| `access_level` | `\"team_member\"` | `\"department_admin\"` | `\"developer\"` |\n| `role` | `editor`, `credential_issuer` or `viewer` | `editor` | `viewer` |\n| `designs` | `true` or `false` | `true` or `false` | `false` |\n| `emails` | `true` or `false` | `true` or `false` | `false` |\n| `credential_attributes` | `true` or `false` | `true` or `false` | `false` |\n| `analytics` | `true` or `false` | `true` or `false` | `false` |\n| `settings` | `true` or `false` | `true` or `false` | `false` |\n| `pathways` | `true` or `false` | `true` or `false` | `false` |\n| `api` | `false` | `true` or `false` | `true` |\n| `team` | `false` | `true` or `false` | `false` |\n| `analytics_email` | `false` | `true` or `false` | `false` |\n| `manage_dept_admins_and_devs` | `false` | `true` or `false` | `false` |\n| `groups` | Array of Group IDs to limit access to | Ignored - access is not Group-limited | Ignored - access is not Group-limited |\n\n**Note:** omitting a boolean permission is equivalent to sending `false`, so a `developer` needs only `api: true` and `role: \"viewer\"`. Sending a value the table does not permit returns `400` with a message of the form ` must be for `.\n\n**Note:** `api` acts as a master switch for a `department_admin`. Setting `api: true` makes the Department Admin a full-access administrator of that Department, so every other permission - `team`, `designs`, `emails`, `credential_attributes`, `analytics`, `settings`, `pathways`, `manage_dept_admins_and_devs` and `analytics_email` - must also be `true`.", "tags": [ "Team Members" ], @@ -10098,7 +10246,12 @@ 1, 2, 3 - ] + ], + "api": false, + "team": true, + "access_level": "department_admin", + "analytics_email": true, + "manage_dept_admins_and_devs": true } }, { @@ -10111,7 +10264,12 @@ "settings": false, "role": "viewer", "pathways": false, - "groups": null + "groups": null, + "api": false, + "team": false, + "access_level": "team_member", + "analytics_email": false, + "manage_dept_admins_and_devs": false } } ] @@ -10126,11 +10284,7 @@ }, "name": { "type": "string", - "description": "The name of the TeamMember user." - }, - "email": { - "type": "string", - "description": "The email address of the TeamMember user." + "description": "The name of the TeamMember user. A blank value leaves the existing name unchanged." }, "department_permissions": { "type": "array", @@ -10173,6 +10327,36 @@ "default": false, "description": "Should the TeamMember have access to Pathways for the given Department?" }, + "api": { + "type": "boolean", + "default": false, + "description": "Should the TeamMember have access to API keys for the given Department?" + }, + "team": { + "type": "boolean", + "default": false, + "description": "Should the TeamMember be able to manage other Team Members in the given Department?" + }, + "access_level": { + "type": "string", + "enum": [ + "team_member", + "department_admin", + "developer" + ], + "default": "team_member", + "description": "The TeamMember's access level in the given Department. Updating a TeamMember who is currently a Department Admin or a Developer requires an account-wide API key, as does setting \"department_admin\" or \"developer\". \"organization_admin\" cannot be assigned through the API, and an existing Organization Admin cannot be updated through the API - use the Issuer Dashboard." + }, + "analytics_email": { + "type": "boolean", + "default": false, + "description": "Should the TeamMember receive scheduled Analytics emails for the given Department?" + }, + "manage_dept_admins_and_devs": { + "type": "boolean", + "default": false, + "description": "Should the TeamMember be able to manage Department Admins and Developers in the given Department?" + }, "groups": { "description": "Limit the TeamMember access to specific Groups in the Department." } @@ -10192,7 +10376,7 @@ "permissions" ] }, - "description": "This parameter will override any existing permissions, not append to them." + "description": "This parameter replaces the TeamMember's entire set of permissions, it does not append to them. A Department present in the array is created or updated; a Department omitted from the array has its permission removed. Removal by omission requires an account-wide API key - a department-specific key cannot remove permissions this way, use DELETE instead." } }, "required": [ @@ -10211,7 +10395,7 @@ { "lang": "cURL", "label": "cURL", - "source": "curl -X PUT \\\n \"https://api.accredible.com/v1/team_members/12345\" \\\n -H \"Authorization: Token token=YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"team_member\": {\n \"department_permissions\": [\n {\n \"department_id\": 34,\n \"permissions\": {\n \"designs\": false,\n \"emails\": false,\n \"credential_attributes\": true,\n \"analytics\": true,\n \"settings\": true,\n \"role\": \"editor\",\n \"pathways\": true,\n \"groups\": [\n 1,\n 2,\n 3\n ]\n }\n },\n {\n \"department_id\": 35,\n \"permissions\": {\n \"designs\": false,\n \"emails\": false,\n \"credential_attributes\": false,\n \"analytics\": false,\n \"settings\": false,\n \"role\": \"viewer\",\n \"pathways\": false,\n \"groups\": null\n }\n }\n ]\n }\n}'" + "source": "curl -X PUT \\\n \"https://api.accredible.com/v1/team_members/12345\" \\\n -H \"Authorization: Token token=YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"team_member\": {\n \"department_permissions\": [\n {\n \"department_id\": 34,\n \"permissions\": {\n \"designs\": false,\n \"emails\": false,\n \"credential_attributes\": true,\n \"analytics\": true,\n \"settings\": true,\n \"role\": \"editor\",\n \"pathways\": true,\n \"groups\": [\n 1,\n 2,\n 3\n ],\n \"api\": false,\n \"team\": true,\n \"access_level\": \"department_admin\",\n \"analytics_email\": true,\n \"manage_dept_admins_and_devs\": true\n }\n },\n {\n \"department_id\": 35,\n \"permissions\": {\n \"designs\": false,\n \"emails\": false,\n \"credential_attributes\": false,\n \"analytics\": false,\n \"settings\": false,\n \"role\": \"viewer\",\n \"pathways\": false,\n \"groups\": null,\n \"api\": false,\n \"team\": false,\n \"access_level\": \"team_member\",\n \"analytics_email\": false,\n \"manage_dept_admins_and_devs\": false\n }\n }\n ]\n }\n}'" } ] }, @@ -10242,7 +10426,7 @@ "items": { "type": "object", "properties": { - "department_id": { + "department": { "type": "number" }, "permissions": { @@ -10275,12 +10459,36 @@ "type": "number" }, "nullable": true + }, + "api": { + "type": "boolean" + }, + "team": { + "type": "boolean" + }, + "spotlight_directory": { + "type": "boolean" + }, + "access_level": { + "type": "string", + "enum": [ + "organization_admin", + "department_admin", + "team_member", + "developer" + ] + }, + "analytics_email": { + "type": "boolean" + }, + "manage_dept_admins_and_devs": { + "type": "boolean" } } } }, "required": [ - "department_id", + "department", "permissions" ] } @@ -10296,7 +10504,7 @@ "email": "person@example.com", "department_permissions": [ { - "department_id": 34, + "department": 34, "permissions": { "designs": true, "emails": true, @@ -10309,11 +10517,17 @@ 1, 2, 3 - ] + ], + "api": false, + "team": true, + "spotlight_directory": false, + "access_level": "department_admin", + "analytics_email": true, + "manage_dept_admins_and_devs": true } }, { - "department_id": 35, + "department": 35, "permissions": { "designs": false, "emails": false, @@ -10322,7 +10536,13 @@ "settings": false, "role": "viewer", "pathways": false, - "groups": null + "groups": null, + "api": false, + "team": false, + "spotlight_directory": false, + "access_level": "team_member", + "analytics_email": false, + "manage_dept_admins_and_devs": false } } ] @@ -10334,7 +10554,7 @@ }, "summary": "Delete a Team Member", "operationId": "Delete a Team Member", - "description": "", + "description": "Remove a TeamMember from the Departments your API key can reach.\n\nAn account-wide API key removes the TeamMember from every Department in the account, including where they are a Department Admin or a Developer. A department-specific key only removes a Team Member in its own Department. Permissions in Departments outside the key's reach are left in place.\n\n**Note:** an Organization Admin cannot be removed through the API - use the Issuer Dashboard.", "tags": [ "Team Members" ], From 8dec21e65320369e4372c727165f23bd87026f50 Mon Sep 17 00:00:00 2001 From: Ravi Sharma Date: Mon, 10 Aug 2026 18:05:33 +0530 Subject: [PATCH 2/3] Restructe the Description --- ...public-api-team-members-and-departments.md | 240 ------------------ openapi.json | 28 +- 2 files changed, 14 insertions(+), 254 deletions(-) delete mode 100644 docs/plans/2026-08-04-hdr-6545-public-api-team-members-and-departments.md diff --git a/docs/plans/2026-08-04-hdr-6545-public-api-team-members-and-departments.md b/docs/plans/2026-08-04-hdr-6545-public-api-team-members-and-departments.md deleted file mode 100644 index 602aff0..0000000 --- a/docs/plans/2026-08-04-hdr-6545-public-api-team-members-and-departments.md +++ /dev/null @@ -1,240 +0,0 @@ -# HDR-6545 — Document Issuer Hierarchy changes on Public API Team Members + Departments - -**Ticket:** [HDR-6545](https://accredible.atlassian.net/browse/HDR-6545) — the documentation task, covering -two stories in the [HDR-6012](https://accredible.atlassian.net/browse/HDR-6012) epic: - -- [HDR-6013](https://accredible.atlassian.net/browse/HDR-6013) (BE subtask HDR-6097) — Team Members: new fields + key-kind authorization -- [HDR-6014](https://accredible.atlassian.net/browse/HDR-6014) (BE subtask HDR-6098) — Departments: scoped API key enforcement - -**Reference PR for structure:** [api-documentation#130](https://github.com/accredible/api-documentation/pull/130) -**Branch:** `feature/HDR-6545/update-api-documentation-team-members-departments` - -## Context - -The Issuer Hierarchy work adds a set of hierarchy fields to the public `team_members` contract and -replaces user-identity authorization with **API-key-kind** authorization across both Team Members and -Departments. Three backend PRs implement it and are all currently **open**: - -- [accredible-credential-api#5277](https://github.com/accredible/accredible-credential-api/pull/5277) — team members create (`POST`) + serializer fields -- [accredible-credential-api#5284](https://github.com/accredible/accredible-credential-api/pull/5284) — team members update (`PUT`) -- [accredible-credential-api#5290](https://github.com/accredible/accredible-credential-api/pull/5290) — departments key-scope enforcement -- team members show/destroy work is on branch `temp-issuer-hierarchy` (not yet PR'd) - -`openapi.json` in this repo is the public source of truth for `docs.api.accredible.com`, and it -currently documents none of this. This change brings the four Team Members operations and the five -documented Departments operations in line with the shipped contract, following the same structure as -PR #130 (the `pathways` field): additive schema properties + matching example bodies + updated cURL -samples, plus prose for the authorization rules. - -**Merge order:** this docs PR must not merge to `develop`/`master` before #5277, #5284 and #5290 ship, -otherwise the published docs describe behaviour the API doesn't have yet. Call that out in the PR body. - -### Authoritative sources for the contract - -| Fact | Source (accredible-credential-api) | -|---|---| -| Response fields | `app/serializers/api/v1/public/team_member_serializer.rb` (`hierarchy_permissions`) | -| Accepted request params | `app/controllers/api/v1/organization_permissions_controller.rb:645` (`team_member_params`) | -| `access_level` wire ↔ DB names | `config/initializers/frontend_data_mappings/organization_permissions.rb` | -| Key-kind authorization rules | `app/services/organization_permissions/validations/api_key_authorization.rb`, `api_key_management_eligibility.rb` | -| `PUT` replace semantics | `app/services/organization_permissions/public_api/bulk_updater.rb` | -| `DELETE` scope rules | `app/services/organization_permissions/public_api/bulk_destroyer.rb` | - -### Decisions taken - -- Document **all six** response fields and **all five** accepted request fields, not just the three - named in the ticket — integrators see all six on the wire regardless. -- Authorization rules go in **prose only** (operation `description` + field descriptions). No new - `400`/`404` response objects: the spec documents error responses almost nowhere today (one lone - `422` on `POST /v2/credentials/bulk_create`), and adding them here would be an inconsistent - one-off. -- Fix the pre-existing `department_id` → `department` bug in the four response schemas/examples. -- Document the two `PUT` behaviour changes (`email` ignored; omitted departments removed). -- Do **not** mention the `issuer_hierarchy` feature flag — public docs describe the target state. - ---- - -## The contract to document - -All fields live inside `department_permissions[].permissions`. - -**Response — six new fields** (append after `groups`, matching serializer key order): - -| Field | Type | Notes | -|---|---|---| -| `api` | boolean | API key access for the Department | -| `team` | boolean | Can manage other Team Members in the Department | -| `spotlight_directory` | boolean | **Response-only** — not accepted on input | -| `access_level` | string | `organization_admin` \| `department_admin` \| `team_member` \| `developer` | -| `analytics_email` | boolean | Receives scheduled analytics emails for the Department | -| `manage_dept_admins_and_devs` | boolean | Can manage Department Admins and Developers | - -**Request — five new fields** (insert after `pathways`, before `groups`): `api`, `team`, -`access_level`, `analytics_email`, `manage_dept_admins_and_devs`. All booleans `default: false`. -`access_level` request enum is only the three assignable values — `team_member` (default), -`department_admin`, `developer` — since `organization_admin` is always rejected. The response enum -carries all four. - -**Key-kind rules to state in prose:** - -| `access_level` | Account-wide key | Department-specific key | -|---|---|---| -| `organization_admin` | never | never | -| `department_admin` | ✅ any department in the account | ✗ | -| `developer` | ✅ any department in the account | ✗ | -| `team_member` | ✅ | ✅ own department only | - -Both `department_admin` and `developer` additionally require a plan that supports them -(`validations/plan_entitlement.rb`). - ---- - -## Changes — all in `openapi.json` - -Two path objects, four operations: `/v1/team_members` (`post`, line ~9556) and -`/v1/team_members/{id}` (`get`, `put`, `delete`, line ~9821). - -### 1. Response schemas + examples — all four operations - -For each of the four `responses.200.content["application/json"]` blocks: - -- Append the six response properties to the `permissions` `properties` object, after `groups`. -- Rename `department_id` → `department` in the `properties` object **and** in the `required` array - (`["department_id", "permissions"]` → `["department", "permissions"]`). -- Add the six fields to the `example` body's `permissions` objects (both departments, all four - operations), and rename the example key `department_id` → `department`. - -Give the two example departments contrasting values so the shape reads clearly — e.g. dept 34 as a -`department_admin` with `manage_dept_admins_and_devs: true`, dept 35 as a plain `team_member` with -everything false. - -### 2. Request schemas + examples — `post` and `put` - -- Add the five request properties (with `default: false` on the booleans, `enum` + - `default: "team_member"` on `access_level`) after `pathways` in the - `requestBody.content["application/json"].schema` → `department_permissions.items.permissions.properties`. - Request keeps `department_id` — that is correct and unchanged. -- Mirror the new fields into the `requestBody` `example` bodies. - -### 3. cURL samples — `post` and `put` - -Update the single-line escaped `source` string in `x-code-samples` for both operations so the -payload matches the new request example exactly. `get` and `delete` samples have no body — no change. - -### 4. Prose — operation descriptions - -- **`post` description** — after the existing sentence, add the key-kind matrix in words: Department - Admins and Developers require an account-wide API key; a department-specific key may only create - Team Members in its own department; `organization_admin` cannot be created via the API (use the - Issuer Dashboard); omitting `access_level` creates a Team Member. -- **`put` description** — add: the payload **replaces** the member's permission set (departments - present are created or updated, departments omitted are removed); removal by omission requires an - account-wide key; updating an existing Department Admin or Developer requires an account-wide key; - Organization Admins cannot be updated via the API. -- **`get` description** (currently `""`) — one sentence: an account-wide key returns the member's - permissions across the whole account; a department-specific key resolves only Team Members within - its own department. -- **`delete` description** (currently `""`) — an account-wide key removes the member from every - department in the account; a department-specific key removes only a Team Member in its own - department; Organization Admins cannot be removed via the API. - -### 5. `put` request schema — the two behaviour changes - -- Annotate the existing `email` property: `"Ignored. A TeamMember's email address cannot be changed - through this endpoint."` (keep the property so integrators currently sending it understand why it - has no effect, rather than silently dropping it from the docs). -- Extend the `name` description: a blank value leaves the existing name unchanged. -- Extend the `department_permissions` description — currently *"This parameter will override any - existing permissions, not append to them."* — to state explicitly that a department omitted from - the array has its permission **removed**, and that removal requires an account-wide API key. - ---- - ---- - -## Departments (HDR-6014 / BE PR #5290) - -No schema changes — this story is authorization only, so the docs change is **prose in each operation's -`description`**, consistent with the "no new error response objects" decision above. Behaviour per -`app/controllers/api/v1/departments_controller.rb`: - -| Operation | Account-wide key | Department-specific key | -|---|---|---| -| `POST /v1/departments` | full access | `403` `No Permission to manage Departments.` | -| `GET /v1/departments/{department_id}` | any Department | own only; others `404` `No department found` | -| `PUT /v1/departments/{department_id}` | any Department | own only; others `404` `No department found` | -| `DELETE /v1/departments/{department_id}` | full access | `403` `No Permission to manage Departments.` | -| `POST /v1/departments/search` | all Departments | scoped to its own Department | - -Two deliberate deviations, both agreed with the ticket owner: - -- **Search.** PR #5290 currently returns `Organization.none` (an empty array) for a department-specific - key. That contradicts AC-E4-S2-2 ("scoped, not 403") and would silently return zero results to - existing integrators. The docs describe the **intended** behaviour — scoped to its own Department — - on the basis that the backend will be corrected. **If #5290 merges as-is, this line is wrong.** -- **`GET /v1/departments`** (index) is affected by the change but has never been documented in - `openapi.json`. Left undocumented; adding it is separate work. - -Note also that the ticket's ACs say other-Department `show`/`update` return `403`; the implementation -returns `404`. The docs follow the implementation. - ---- - -## Documented-intent deviations — backend changes required before this merges - -Three places where the docs describe **intended** behaviour that the current code does not implement. -Each was an explicit call by the ticket owner. All three need a backend change, or the docs ship wrong: - -| # | Documented | Actual behaviour today | Fix needed | -|---|---|---|---| -| 1 | `POST /v1/departments/search` with a department-specific key returns that key's own Department | Returns `[]` (`Organization.none` in `public_search`) | PR #5290 | -| 2 | `role` for a `department_admin` must be `editor` | `editor`, `credential_issuer` and `viewer` are all accepted — only `corporation_admin` is locked to `editor` in `PermissionLocks#locks_for` | add a `role` lock to `organization_admin_locks` | -| 3 | `api: true` on a `department_admin` is permitted and turns every other permission on | Always `400 directory must be true for organization_admin` — the master switch requires `directory: true`, but `directory` is absent from `team_member_params` and pinned to `false` by `BulkCreator::DEFAULT_PERMISSION_PARAMS` | allow `directory` through the public API, or exempt it from the master switch | - -Deviation 3 also means the `api` cell for `department_admin` in the permitted-values table reads -`true or false`; until the backend is fixed, sending `api: true` returns `400`. - ---- - -## Constraints while editing - -- **Hand-edit `openapi.json`; do not run `process_openapi.py`.** It rewrites the whole file with - `json.dump(indent=4)`, which would collapse the hand-formatted inline arrays (`[1, 2, 3]`) and - produce a diff across all 10,607 lines. `add_curl_examples.py` likewise must not be re-run. -- Match surrounding formatting exactly: 4-space indent, inline `[1, 2, 3]` in the `post` examples, - expanded multi-line arrays in the `{id}` examples (existing inconsistency — preserve per-block). -- Purely additive on request/response properties. The only rename is `department_id` → `department` - in **responses**. -- Leave `POST` documented as `200` — the controller renders without an explicit status, so `200` is - correct despite the ticket's AC text saying `201`. - -## Verification - -1. **Valid JSON, unchanged elsewhere:** - ``` - python3 -m json.tool openapi.json > /dev/null && echo OK - git diff --stat # expect: openapi.json only - ``` -2. **Every documented permission key matches the serializer.** Extract the `permissions` property - keys for all four operations and diff against the serializer's key list - (`designs, emails, credential_attributes, analytics, settings, role, pathways, groups, api, team, - spotlight_directory, access_level, analytics_email, manage_dept_admins_and_devs`) — a short - throwaway `python3` script over `openapi.json`. -3. **Schema/example agreement:** confirm each operation's `example` body carries exactly the keys its - schema declares, and that no response block still contains `department_id`: - ``` - grep -c '"department_id"' openapi.json # only request-side occurrences should remain - ``` -4. **Render check:** `python3 -m http.server 8080`, open `http://localhost:8080`, and confirm the four - Team Members operations show the new fields, descriptions, and cURL samples correctly. -5. **Cross-check against the backend specs** — `spec/requests/api/v1/public_api/team_members_spec.rb` - (`hierarchy_fields` list, line ~1122) and `spec/serializers/api/v1/public/team_member_serializer_spec.rb` - (line ~131) are the assertions that pin this contract; the documented field set must match them. - -## PR - -Follow the PR #130 template: ticket link, a "What does this PR do?" list of the changes, type -`📦 Chore/Documentation`, checklist. Explicitly note in the body: -- the `department_id` → `department` response fix as a drive-by correction of a pre-existing bug, -- the two `PUT` behaviour clarifications, -- that this must not merge before accredible-credential-api#5277 and #5284 ship. diff --git a/openapi.json b/openapi.json index 5ce1054..6e706af 100644 --- a/openapi.json +++ b/openapi.json @@ -8414,7 +8414,7 @@ }, "summary": "Create a New Department", "operationId": "Create a New Department", - "description": "You may create a Department using this action. It takes a JSON object containing a Department.\n\n**Creating a Department requires an account-wide API key.** A department-specific key receives `403` with `No Permission to manage Departments.`", + "description": "You may create a Department using this action. It takes a JSON object containing a Department.\n\nAuthorization depends on the type of API key you use:\n\n| Account-wide key | Department-specific key |\n| --- | --- |\n| Allowed | Not permitted |", "tags": [ "Departments" ], @@ -8580,7 +8580,7 @@ }, "summary": "View a Department", "operationId": "View a Department", - "description": "View a single Department.\n\nAn account-wide API key can view any Department in the account. A department-specific key can only view the Department it is scoped to - any other Department returns `404` with `No department found`.", + "description": "View a single Department.\n\nAuthorization depends on the type of API key you use:\n\n| Account-wide key | Department-specific key |\n| --- | --- |\n| Any Department in the account | Its own Department only |", "tags": [ "Departments" ], @@ -8675,7 +8675,7 @@ }, "summary": "Update a Department", "operationId": "Update a Department", - "description": "Update a Department using this action. It takes a JSON object containing a Department.\n\nAn account-wide API key can update any Department in the account. A department-specific key can only update the Department it is scoped to - any other Department returns `404` with `No department found`.", + "description": "Update a Department using this action. It takes a JSON object containing a Department.\n\nAuthorization depends on the type of API key you use:\n\n| Account-wide key | Department-specific key |\n| --- | --- |\n| Any Department in the account | Its own Department only |", "tags": [ "Departments" ], @@ -8834,7 +8834,7 @@ }, "summary": "Delete a Department", "operationId": "Delete a Department", - "description": "Delete a Department using this action.\n\n**Deleting a Department requires an account-wide API key.** A department-specific key receives `403` with `No Permission to manage Departments.`", + "description": "Delete a Department using this action.\n\nAuthorization depends on the type of API key you use:\n\n| Account-wide key | Department-specific key |\n| --- | --- |\n| Allowed | Not permitted |", "tags": [ "Departments" ], @@ -8942,7 +8942,7 @@ }, "summary": "Search for Departments", "operationId": "Search for Departments", - "description": "You can search for departments by various search criteria.\n\nAn account-wide API key searches every Department in the account. A department-specific key only returns the Department it is scoped to.", + "description": "You can search for departments by various search criteria.\n\nAuthorization depends on the type of API key you use:\n\n| Account-wide key | Department-specific key |\n| --- | --- |\n| All Departments in the account | Its own Department only |", "tags": [ "Departments" ], @@ -9712,7 +9712,7 @@ }, "summary": "Create a Team Member", "operationId": "Create a Team Member", - "description": "Create a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\nThe `access_level` of each `department_permissions` entry determines what is created, and authorization is based on the type of API key you use:\n\n| `access_level` | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| `team_member` (default) | Any Department in the account | Its own Department only |\n| `department_admin` | Any Department in the account | Not permitted |\n| `developer` | Any Department in the account | Not permitted |\n| `organization_admin` | Not permitted | Not permitted |\n\n**Note:** `department_admin` and `developer` also require a plan that supports those roles. `organization_admin` can never be created through the API - use the Issuer Dashboard. Omitting `access_level` creates a regular Team Member, so existing integrations are unaffected.\n\n**Permitted permission values by `access_level`.** Each `access_level` locks certain permissions to fixed values, and a request that sets a locked permission to any other value is rejected:\n\n| Field | `team_member` | `department_admin` | `developer` |\n|-------|---------------|--------------------|-------------|\n| `access_level` | `\"team_member\"` | `\"department_admin\"` | `\"developer\"` |\n| `role` | `editor`, `credential_issuer` or `viewer` | `editor` | `viewer` |\n| `designs` | `true` or `false` | `true` or `false` | `false` |\n| `emails` | `true` or `false` | `true` or `false` | `false` |\n| `credential_attributes` | `true` or `false` | `true` or `false` | `false` |\n| `analytics` | `true` or `false` | `true` or `false` | `false` |\n| `settings` | `true` or `false` | `true` or `false` | `false` |\n| `pathways` | `true` or `false` | `true` or `false` | `false` |\n| `api` | `false` | `true` or `false` | `true` |\n| `team` | `false` | `true` or `false` | `false` |\n| `analytics_email` | `false` | `true` or `false` | `false` |\n| `manage_dept_admins_and_devs` | `false` | `true` or `false` | `false` |\n| `groups` | Array of Group IDs to limit access to | Ignored - access is not Group-limited | Ignored - access is not Group-limited |\n\n**Note:** omitting a boolean permission is equivalent to sending `false`, so a `developer` needs only `api: true` and `role: \"viewer\"`. Sending a value the table does not permit returns `400` with a message of the form ` must be for `.\n\n**Note:** `api` acts as a master switch for a `department_admin`. Setting `api: true` makes the Department Admin a full-access administrator of that Department, so every other permission - `team`, `designs`, `emails`, `credential_attributes`, `analytics`, `settings`, `pathways`, `manage_dept_admins_and_devs` and `analytics_email` - must also be `true`.", + "description": "Create a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\n**Important**\n\n- Organization Admin cannot be created through the API.\n- To create a Department Admin or Developer your account must be on a custom plan.\n\nThe `access_level` of each `department_permissions` entry determines what is created, and authorization is based on the type of API key you use:\n\n| Access Level | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| Team Member (default) | Any Department in the account | Its own Department only |\n| Department Admin | Any Department in the account | Not permitted |\n| Developer | Any Department in the account | Not permitted |\n| Organization Admin | Not permitted | Not permitted |\n\n**Permitted values by `access_level`.**\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
FieldTeam MemberDepartment Admin*Developer*
access_levelteam_memberdepartment_admindeveloper
roleeditor
credential_issuer
viewer
editorviewer
apiNAfalsetruetrue
designstrue
false
true
false
trueNA
emailstrue
false
true
false
trueNA
credential_attributestrue
false
true
false
trueNA
analyticstrue
false
true
false
trueNA
settingstrue
false
true
false
trueNA
pathwaystrue
false
true
false
trueNA
teamNAtrue
false
trueNA
analytics_emailNAtrue
false
trueNA
manage_dept_admins_and_devsNAtrue
false
trueNA
groupsArray of Group IDs to limit access toNA (All groups allowed)NA (All groups allowed)
\n\n*NA - Not Applicable, can be omitted.*
* *Only available on Custom Plan.*", "tags": [ "Team Members" ], @@ -9809,7 +9809,7 @@ }, "role": { "type": "string", - "description": "\"editor\" or \"viewer\" access to Credential data in the Department." + "description": "\"editor\", \"credential_issuer\" or \"viewer\" access to Credential data in the Department." }, "pathways": { "type": "boolean", @@ -9834,7 +9834,7 @@ "developer" ], "default": "team_member", - "description": "The TeamMember's access level in the given Department. \"department_admin\" and \"developer\" require an account-wide API key and a plan that supports them; \"team_member\" can be created with either an account-wide or a department-specific key. \"organization_admin\" cannot be assigned through the API - use the Issuer Dashboard." + "description": "The TeamMember's access level in the given Department." }, "analytics_email": { "type": "boolean", @@ -10050,7 +10050,7 @@ }, "summary": "View a Team Member", "operationId": "View a Team Member", - "description": "View a TeamMember and their permissions in each Department.\n\nAn account-wide API key returns the TeamMember's permissions across every Department in the account. A department-specific key only resolves Team Members within its own Department.", + "description": "View a TeamMember and their permissions in each Department.\n\nWhich TeamMembers you can view depends on the type of API key you use:\n\n| Access Level | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| Team Member | Any Department in the account | Its own Department only |\n| Department Admin | Any Department in the account | Its own Department only |\n| Developer | Any Department in the account | Its own Department only |\n| Organization Admin | Visible | Not visible |", "tags": [ "Team Members" ], @@ -10222,7 +10222,7 @@ }, "summary": "Update a Team Member", "operationId": "Update a Team Member", - "description": "Update a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\n**The `department_permissions` array replaces the TeamMember's entire permission set.** A Department present in the array is created or updated; a Department omitted from the array has its permission removed.\n\nAuthorization is based on the type of API key you use. An account-wide key can update a TeamMember in any Department in the account, and is required in order to update a TeamMember who is currently a Department Admin or a Developer, to set `access_level` to `department_admin` or `developer`, or to remove a permission by omitting its Department. A department-specific key can only update a Team Member in its own Department.\n\n**Note:** an Organization Admin cannot be updated through the API, and `organization_admin` cannot be assigned through the API - use the Issuer Dashboard. Only `name` can be changed on the TeamMember record itself.\n\n**Permitted permission values by `access_level`.** Each `access_level` locks certain permissions to fixed values, and a request that sets a locked permission to any other value is rejected:\n\n| Field | `team_member` | `department_admin` | `developer` |\n|-------|---------------|--------------------|-------------|\n| `access_level` | `\"team_member\"` | `\"department_admin\"` | `\"developer\"` |\n| `role` | `editor`, `credential_issuer` or `viewer` | `editor` | `viewer` |\n| `designs` | `true` or `false` | `true` or `false` | `false` |\n| `emails` | `true` or `false` | `true` or `false` | `false` |\n| `credential_attributes` | `true` or `false` | `true` or `false` | `false` |\n| `analytics` | `true` or `false` | `true` or `false` | `false` |\n| `settings` | `true` or `false` | `true` or `false` | `false` |\n| `pathways` | `true` or `false` | `true` or `false` | `false` |\n| `api` | `false` | `true` or `false` | `true` |\n| `team` | `false` | `true` or `false` | `false` |\n| `analytics_email` | `false` | `true` or `false` | `false` |\n| `manage_dept_admins_and_devs` | `false` | `true` or `false` | `false` |\n| `groups` | Array of Group IDs to limit access to | Ignored - access is not Group-limited | Ignored - access is not Group-limited |\n\n**Note:** omitting a boolean permission is equivalent to sending `false`, so a `developer` needs only `api: true` and `role: \"viewer\"`. Sending a value the table does not permit returns `400` with a message of the form ` must be for `.\n\n**Note:** `api` acts as a master switch for a `department_admin`. Setting `api: true` makes the Department Admin a full-access administrator of that Department, so every other permission - `team`, `designs`, `emails`, `credential_attributes`, `analytics`, `settings`, `pathways`, `manage_dept_admins_and_devs` and `analytics_email` - must also be `true`.", + "description": "Update a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\n**Important**\n\n- Organization Admin cannot be created or updated through the API.\n- To assign a Department Admin or Developer your account must be on a custom plan.\n\n**The `department_permissions` array replaces the TeamMember's entire permission set.** A Department present in the array is created or updated; a Department omitted from the array has its permission removed, if your API key is authorized to remove it.\n\nWhich TeamMembers you can update depends on the type of API key you use:\n\n| Access Level | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| Team Member | Any Department in the account | Its own Department only |\n| Department Admin | Any Department in the account | Not permitted |\n| Developer | Any Department in the account | Not permitted |\n| Organization Admin | Not permitted | Not permitted |\n\n**Permitted values by `access_level`.**\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
FieldTeam MemberDepartment Admin*Developer*
access_levelteam_memberdepartment_admindeveloper
roleeditor
credential_issuer
viewer
editorviewer
apiNAfalsetruetrue
designstrue
false
true
false
trueNA
emailstrue
false
true
false
trueNA
credential_attributestrue
false
true
false
trueNA
analyticstrue
false
true
false
trueNA
settingstrue
false
true
false
trueNA
pathwaystrue
false
true
false
trueNA
teamNAtrue
false
trueNA
analytics_emailNAtrue
false
trueNA
manage_dept_admins_and_devsNAtrue
false
trueNA
groupsArray of Group IDs to limit access toNA (All groups allowed)NA (All groups allowed)
\n\n*NA - Not Applicable, can be omitted.*
* *Only available on Custom Plan.*", "tags": [ "Team Members" ], @@ -10320,7 +10320,7 @@ }, "role": { "type": "string", - "description": "\"editor\" or \"viewer\" access to Credential data in the Department." + "description": "\"editor\", \"credential_issuer\" or \"viewer\" access to Credential data in the Department." }, "pathways": { "type": "boolean", @@ -10345,7 +10345,7 @@ "developer" ], "default": "team_member", - "description": "The TeamMember's access level in the given Department. Updating a TeamMember who is currently a Department Admin or a Developer requires an account-wide API key, as does setting \"department_admin\" or \"developer\". \"organization_admin\" cannot be assigned through the API, and an existing Organization Admin cannot be updated through the API - use the Issuer Dashboard." + "description": "The TeamMember's access level in the given Department." }, "analytics_email": { "type": "boolean", @@ -10376,7 +10376,7 @@ "permissions" ] }, - "description": "This parameter replaces the TeamMember's entire set of permissions, it does not append to them. A Department present in the array is created or updated; a Department omitted from the array has its permission removed. Removal by omission requires an account-wide API key - a department-specific key cannot remove permissions this way, use DELETE instead." + "description": "This parameter replaces the TeamMember's entire set of permissions, it does not append to them." } }, "required": [ @@ -10554,7 +10554,7 @@ }, "summary": "Delete a Team Member", "operationId": "Delete a Team Member", - "description": "Remove a TeamMember from the Departments your API key can reach.\n\nAn account-wide API key removes the TeamMember from every Department in the account, including where they are a Department Admin or a Developer. A department-specific key only removes a Team Member in its own Department. Permissions in Departments outside the key's reach are left in place.\n\n**Note:** an Organization Admin cannot be removed through the API - use the Issuer Dashboard.", + "description": "Remove a TeamMember from the Departments your API key can reach.\n\nWhat the request removes depends on the type of API key you use:\n\n| Access Level | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| Team Member | Any Department in the account | Its own Department only |\n| Department Admin | Any Department in the account | Not removed |\n| Developer | Any Department in the account | Not removed |\n| Organization Admin | Not removed | Not removed |", "tags": [ "Team Members" ], From 08d27f52cc9ea847d654f1b812fa535cca734fa5 Mon Sep 17 00:00:00 2001 From: Ravi Sharma Date: Mon, 17 Aug 2026 22:34:19 +0530 Subject: [PATCH 3/3] Only available on Connect/Growth Plan --- openapi.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi.json b/openapi.json index 6e706af..c9c618e 100644 --- a/openapi.json +++ b/openapi.json @@ -9712,7 +9712,7 @@ }, "summary": "Create a Team Member", "operationId": "Create a Team Member", - "description": "Create a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\n**Important**\n\n- Organization Admin cannot be created through the API.\n- To create a Department Admin or Developer your account must be on a custom plan.\n\nThe `access_level` of each `department_permissions` entry determines what is created, and authorization is based on the type of API key you use:\n\n| Access Level | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| Team Member (default) | Any Department in the account | Its own Department only |\n| Department Admin | Any Department in the account | Not permitted |\n| Developer | Any Department in the account | Not permitted |\n| Organization Admin | Not permitted | Not permitted |\n\n**Permitted values by `access_level`.**\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
FieldTeam MemberDepartment Admin*Developer*
access_levelteam_memberdepartment_admindeveloper
roleeditor
credential_issuer
viewer
editorviewer
apiNAfalsetruetrue
designstrue
false
true
false
trueNA
emailstrue
false
true
false
trueNA
credential_attributestrue
false
true
false
trueNA
analyticstrue
false
true
false
trueNA
settingstrue
false
true
false
trueNA
pathwaystrue
false
true
false
trueNA
teamNAtrue
false
trueNA
analytics_emailNAtrue
false
trueNA
manage_dept_admins_and_devsNAtrue
false
trueNA
groupsArray of Group IDs to limit access toNA (All groups allowed)NA (All groups allowed)
\n\n*NA - Not Applicable, can be omitted.*
* *Only available on Custom Plan.*", + "description": "Create a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\n**Important**\n\n- Organization Admin cannot be created through the API.\n- To create a Department Admin or Developer your account must be on the Connect/Growth Plan.\n\nThe `access_level` of each `department_permissions` entry determines what is created, and authorization is based on the type of API key you use:\n\n| Access Level | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| Team Member (default) | Any Department in the account | Its own Department only |\n| Department Admin | Any Department in the account | Not permitted |\n| Developer | Any Department in the account | Not permitted |\n| Organization Admin | Not permitted | Not permitted |\n\n**Permitted values by `access_level`.**\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
FieldTeam MemberDepartment Admin*Developer*
access_levelteam_memberdepartment_admindeveloper
roleeditor
credential_issuer
viewer
editorviewer
apiNAfalsetruetrue
designstrue
false
true
false
trueNA
emailstrue
false
true
false
trueNA
credential_attributestrue
false
true
false
trueNA
analyticstrue
false
true
false
trueNA
settingstrue
false
true
false
trueNA
pathwaystrue
false
true
false
trueNA
teamNAtrue
false
trueNA
analytics_emailNAtrue
false
trueNA
manage_dept_admins_and_devsNAtrue
false
trueNA
groupsArray of Group IDs to limit access toNA (All groups allowed)NA (All groups allowed)
\n\n*NA - Not Applicable, can be omitted.*
* *Only available on Connect/Growth Plan.*", "tags": [ "Team Members" ], @@ -10222,7 +10222,7 @@ }, "summary": "Update a Team Member", "operationId": "Update a Team Member", - "description": "Update a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\n**Important**\n\n- Organization Admin cannot be created or updated through the API.\n- To assign a Department Admin or Developer your account must be on a custom plan.\n\n**The `department_permissions` array replaces the TeamMember's entire permission set.** A Department present in the array is created or updated; a Department omitted from the array has its permission removed, if your API key is authorized to remove it.\n\nWhich TeamMembers you can update depends on the type of API key you use:\n\n| Access Level | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| Team Member | Any Department in the account | Its own Department only |\n| Department Admin | Any Department in the account | Not permitted |\n| Developer | Any Department in the account | Not permitted |\n| Organization Admin | Not permitted | Not permitted |\n\n**Permitted values by `access_level`.**\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
FieldTeam MemberDepartment Admin*Developer*
access_levelteam_memberdepartment_admindeveloper
roleeditor
credential_issuer
viewer
editorviewer
apiNAfalsetruetrue
designstrue
false
true
false
trueNA
emailstrue
false
true
false
trueNA
credential_attributestrue
false
true
false
trueNA
analyticstrue
false
true
false
trueNA
settingstrue
false
true
false
trueNA
pathwaystrue
false
true
false
trueNA
teamNAtrue
false
trueNA
analytics_emailNAtrue
false
trueNA
manage_dept_admins_and_devsNAtrue
false
trueNA
groupsArray of Group IDs to limit access toNA (All groups allowed)NA (All groups allowed)
\n\n*NA - Not Applicable, can be omitted.*
* *Only available on Custom Plan.*", + "description": "Update a TeamMember using this action. It requires a JSON object containing a TeamMember.\n\n**Important**\n\n- Organization Admin cannot be created or updated through the API.\n- To assign a Department Admin or Developer your account must be on the Connect/Growth Plan.\n\n**The `department_permissions` array replaces the TeamMember's entire permission set.** A Department present in the array is created or updated; a Department omitted from the array has its permission removed, if your API key is authorized to remove it.\n\nWhich TeamMembers you can update depends on the type of API key you use:\n\n| Access Level | Account-wide key | Department-specific key |\n| --- | --- | --- |\n| Team Member | Any Department in the account | Its own Department only |\n| Department Admin | Any Department in the account | Not permitted |\n| Developer | Any Department in the account | Not permitted |\n| Organization Admin | Not permitted | Not permitted |\n\n**Permitted values by `access_level`.**\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
FieldTeam MemberDepartment Admin*Developer*
access_levelteam_memberdepartment_admindeveloper
roleeditor
credential_issuer
viewer
editorviewer
apiNAfalsetruetrue
designstrue
false
true
false
trueNA
emailstrue
false
true
false
trueNA
credential_attributestrue
false
true
false
trueNA
analyticstrue
false
true
false
trueNA
settingstrue
false
true
false
trueNA
pathwaystrue
false
true
false
trueNA
teamNAtrue
false
trueNA
analytics_emailNAtrue
false
trueNA
manage_dept_admins_and_devsNAtrue
false
trueNA
groupsArray of Group IDs to limit access toNA (All groups allowed)NA (All groups allowed)
\n\n*NA - Not Applicable, can be omitted.*
* *Only available on Connect/Growth Plan.*", "tags": [ "Team Members" ],