Skip to content

docs: add Users API reference page#586

Draft
danielfsousa wants to merge 3 commits into
mainfrom
feat/users-api-docs
Draft

docs: add Users API reference page#586
danielfsousa wants to merge 3 commits into
mainfrom
feat/users-api-docs

Conversation

@danielfsousa

@danielfsousa danielfsousa commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a public API reference page for the Users API — the endpoints exposed by the option-gated, api_key-role user-management grants that let an API key manage the users on an account.

New page content/api/users.apib, registered under the Accounts category in content/api/table-of-contents.json (after subaccounts.apib).

Everything in the .apib is public. Implementation-internal details are intentionally kept out of the page and captured in Reviewer notes below instead.

Endpoints documented

  • GET /api/v1/users — list users
  • GET /api/v1/users/:username — retrieve a user
  • POST /api/v1/users/invite — invite a user (creation is invite-based only)
  • GET /api/v1/users/pending-invites — list pending invites (canonical path)
  • DELETE /api/v1/users/pending-invites/:id — revoke a pending invite
  • PUT /api/v1/users/:username — update a user
  • DELETE /api/v1/users/:username — delete a user
  • GET /api/v1/users/:username/subaccounts — list a user's subaccount mappings
  • POST /api/v1/users/:username/subaccounts / DELETE /api/v1/users/:username/subaccounts/:subaccountId — manage subaccount mappings

Also covered

  • Prerequisites / grants: an API key needs one of two api_key-role grants, attached by an admin through the web app (never by another API key): Users: View for read-only access (the GET endpoints) or Users: Manage for full management (required by every write endpoint). No account-option / support-enablement step is documented — see the GA rollout note under Reviewer notes.
  • Invite semantics and lifecycle: creation is invite-based only (no password-based create via API); the response returns the invite token, documented as a credential. Invitations expire after 3 days; expired invites drop off the pending list automatically; there is no resend (re-invite the same email to create a new independent invitation); invite creation is rate limited (429).
  • Update endpoint documented positively: only first_name, last_name, access_level, access_policies, and options are updatable.
  • Constraints: role ceiling bounded by the key owner's role; the last admin cannot be deleted or demoted; keys holding a grant are revoked if their owning admin is deleted; two-factor, email-verification, SCIM, and password endpoints are out of scope.

Reviewer notes (not in public docs)

These are the implementation-internal details behind the public wording, for reviewer context only. They are deliberately not in the .apib:

  • GA rollout / no option-gate wording. These docs are slated to publish at GA, after the account-option gate has been removed. So at publish time there is no "must be enabled by SparkPost support" step, and the page intentionally omits any account-option or contact-support mention. Until GA, the feature is gated by the account option allow_user_management_via_api (set only by SparkPost support) — that gate is expected to be gone by the time these docs go live.
  • PUT restricted-field behavior. The page documents updatable fields positively rather than enumerating forbidden ones. The underlying behavior for programmatic (API-key) callers: password, tfa_enabled, and is_sso are rejected with 403; email is currently silently ignored (generateUpdateFields, "Don't update email address, see AC-297") and is slated to also become a 403 via the deny-list once the accusers-api change lands (see verification note below).
  • Invite rate-limit internals. The 429 is driven by inviteThrottle (config.emailThrottling.invite): maxPerRecipient = 3, maxPerRequester = 20, over a ttl of 3600s (1-hour window). The public docs intentionally omit these numbers.
  • Invite expiry source. The documented 3-day expiry is config.emailInvite.ttl = 259200s. Expired invites are filtered out of the listing (invite-dynamo.getInvites isExpired check) and DynamoDB TTL reaps the records.

Verification status

Request/response shapes and error bodies were verified against the accusers-api source (resources/user-controller.js, resources/user-endpoints.js, lib/validator/*, lib/models/*, config/default.json) and the hardening PR SparkPost/accusers-api#1294, and the grant patterns/labels against @sparkpost/access (lib/token-access.js) and access PR SparkPost/access#124.

Verified against current code: list users, invite (request/response incl. token, 400, 429), revoke invite, update user (updatable fields, Successfully modified user <username>, last-admin 400), delete user, add/remove subaccount mapping, the invite lifecycle facts (3-day TTL, expired-invite filtering, throttle → 429), and the existing role-ceiling / last-admin / key-revocation constraints. The Users: View grant (label "Users: View", read-only GET surface: list, single-user, subaccount mappings, pending invites) is present in the access users-manage-programmatic working branch.

Could NOT yet verify against the pushed PR #1294 / access #124 — documented per the agreed spec and flagged as forthcoming. As of their latest pushes, the pushed PRs don't yet contain these; they line up with the accusers-api work items still in progress (retrieve-user hardening, new subaccounts-list endpoint, pending-invites enrichment + canonical alias, email deny-list) and the access grant additions:

  • Users: View grant — defined in the access working branch but not yet in the pushed access Add redirects for subaccounts docs changes #124 diff. Confirm it ships in the release.
  • GET /api/v1/users/:username grant coverage and a grant-scoped response shape. The page documents it returning the list-item shape; the current getUser handler returns a richer object (including tokens and auth_connection) that must not be exposed under these grants. Confirm the grant-scoped response matches the documented list-item shape.
  • GET /api/v1/users/:username/subaccounts — no such route exists yet; response shape (subaccount_id, subaccount_name, access_level, status) is modeled on the existing per-user subaccounts array. Confirm route + shape.
  • GET /api/v1/users/pending-invites canonical path — only /pending-invites/all exists today. Confirm the canonical path is added to the route and both grants.
  • Invite object access_level + expires (epoch seconds) — the current listing returns only {id, email, from}. Confirm the enriched fields.
  • email on PUT → 403 — current deny-list is password/tfa_enabled/is_sso only; email is silently ignored. Confirm email is added to the deny-list.
  • Users: Manage GET coverage — the manage grant's current get_grants cover only /users and /pending-invites/all; single-user and subaccounts-list GETs are covered by Users: View. Confirm manage keys can also reach those read endpoints (or that the docs should scope those reads to Users: View).

⚠️ Do not merge/publish until the feature ships

This documents an unreleased feature. Hold until the implementation ships:

  • SparkPost/access#124 (grants, @sparkpost/access 4.13.0)
  • SparkPost/accusers-api#1294 (endpoint hardening)
  • SparkPost/auth-api#319 (grant enforcement)

Closes #584
Part of SparkPost/access#121

https://claude.ai/code/session_01AE1PyieVr2SdnCwF5A4xxU

Document the option-gated Users API exposed by the api_key-role
`users/manage-programmatic` grant (label "Users: Manage"): list users,
invite a user, list/revoke pending invites, update and delete a user,
and manage a user's subaccount mappings.

Covers the prerequisites (the `allow_user_management_via_api` account
option enabled by support, plus an admin attaching the grant via the web
app), the invite-based-only creation flow and treating the invite token
as a credential, the update field deny-list (password, tfa_enabled,
is_sso, email), and the role-ceiling / last-admin / key-revocation
constraints. Registered under the Accounts category in the table of
contents.

Closes #584
Part of SparkPost/access#121

Do not merge/publish until the feature ships. Implementation PRs:
SparkPost/access#124, SparkPost/accusers-api#1294, SparkPost/auth-api#319.

Claude-Session: https://claude.ai/code/session_01AE1PyieVr2SdnCwF5A4xxU
Remove non-public details: the account option key, the internal
auth_migrated user field, and the PUT restricted-fields enumeration.
Prerequisites and the update endpoint are now worded positively (what
is enabled / what is updatable) rather than naming internal config or
forbidden fields.

Add the invite lifecycle (3-day expiry, expired invites drop off the
pending list automatically, no resend — re-invite instead, 429 when
rate limited), the Retrieve a User and List a User's Subaccounts
endpoints, and the canonical GET /v1/users/pending-invites listing
(replacing the legacy /all variant). The Invite object now carries
access_level and expires.

Claude-Session: https://claude.ai/code/session_01AE1PyieVr2SdnCwF5A4xxU
These docs publish at GA, after the account-option gate is removed, so
there is no "enabled by SparkPost support" prerequisite at publish time;
remove that wording and the contact-support banner.

Document both grants: `Users: View` (read-only, GET endpoints) and
`Users: Manage` (full management; required by every write endpoint). The
only remaining prerequisite is that an admin attaches the grant to the
key via the web app.

Claude-Session: https://claude.ai/code/session_01AE1PyieVr2SdnCwF5A4xxU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Document the Users API (User Management via API Keys)

1 participant