diff --git a/content/api/table-of-contents.json b/content/api/table-of-contents.json index 29480c5b..506efb6d 100644 --- a/content/api/table-of-contents.json +++ b/content/api/table-of-contents.json @@ -42,6 +42,7 @@ "pages": [ "account.apib", "subaccounts.apib", + "users.apib", "data-privacy.apib" ] }, diff --git a/content/api/users.apib b/content/api/users.apib new file mode 100644 index 00000000..13e3407e --- /dev/null +++ b/content/api/users.apib @@ -0,0 +1,560 @@ +FORMAT: 1A +title: Users API +description: Programmatically manage the users on your SparkPost account with an API key. + +# Group Users + +Use the Users API to manage your account's users from code instead of the web app. You can list users, invite new ones, change roles, remove users, and manage each user's subaccount access. + +### Prerequisites + +Every call needs an API key holding one of two grants. Only an admin can attach either grant, and only from the web app's [API Keys page](https://app.sparkpost.com/account/api-keys). No API key can attach a grant, not even a key that already holds one. + +* **`Users: View`** gives read-only access to the three `GET` endpoints: [List Users](#header-list-users), [Retrieve a User](#header-retrieve-a-user), and [List Pending Invites](#header-list-pending-invites). +* **`Users: Manage`** covers the same reads plus every write: invite, update, delete, and the subaccount-mapping changes. + +A key holding neither grant gets `403`, and so does a write attempted with only `Users: View`. + +These grants cover less than the web app's user management. They do not reach two-factor authentication, email verification, SCIM provisioning, or password endpoints, and they cannot create a user directly with a password. New users join by invitation. + +### Roles + +Inviting or updating a user assigns a role through the `access_level` field. The primary-account roles are: + +| Role | Description | +|-------------|-------------------------------------------------------------| +| `admin` | Full access to the account, including user and billing management. | +| `developer` | Access to sending and configuration APIs, without account administration. | +| `reporting` | Read-only access to reporting and analytics. | +| `templates` | Access limited to managing templates. | +| `custom` | A role whose permissions are defined by the `access_policies` you supply. | + +Subaccount-scoped users take one of two access levels instead, `subaccount_reporting` or `subaccount_developer`. These appear only in the `subaccounts` array and in the subaccount-mapping endpoints. + +### Constraints + +These rules apply to every Users API request. They stop a key from escalating its own privileges or locking an account out of its own administration. + +* **No admin through the API.** An API key never assigns the `admin` role, not even a key owned by an admin. Inviting a user as `admin` returns `403`, and so does promoting one. Create admins in the web app. +* **Role ceiling.** Below admin, a key can only grant a role at or under the role held by the user who owns it. A key owned by a `reporting` user invites `reporting` users and nothing higher. +* **Last admin protection.** The API refuses to delete or demote the last `admin` on an account, returning `400`. +* **Self-deletion.** A key cannot delete its own owner. + +### User object + +Returned by [List Users](#header-list-users). + ++ Data Structure: User + + name (string) - The user's display name, derived from their first and last name. + + username (string) - Unique username that identifies the user. + + email (string) - The user's email address. + + access (enum) - The user's primary-account role. Omitted for subaccount-scoped users. + + `admin` + + `developer` + + `reporting` + + `templates` + + `custom` + + access_policies (array) - The policies granted to the user when `access` is `custom`. + + is_sso (boolean) - Whether the user signs in via single sign-on. + + email_verified (boolean) - Whether the user has verified their email address. + + tfa_enabled (boolean) - Whether the user has two-factor authentication enabled. + + last_login (string, nullable) - ISO 8601 timestamp of the user's last sign-in, or `null` if they have never signed in. + + subaccount_id (number) - The subaccount the user is scoped to, if any. + + subaccounts (array) - The subaccounts a subaccount-scoped user has access to, and their access level on each. Present only for subaccount-scoped users. + + (object) + + subaccount_id (number) - The subaccount ID. + + access_level (enum) - The user's access level on the subaccount. + + `subaccount_reporting` + + `subaccount_developer` + + subaccount_name (string) - The subaccount's display name. + + options (object) - User-level options, present only when set. + +### Retrieved user object + +Returned by [Retrieve a User](#header-retrieve-a-user). It differs from the [User object](#header-user-object) that List Users returns. This shape splits `name` into `first_name` and `last_name`, names the primary-account role `access_level` rather than `access`, and adds `created`, `updated`, and a `subaccounts` array that is always present. + ++ Data Structure: RetrievedUser + + username (string) - Unique username that identifies the user. + + first_name (string) - The user's first name. + + last_name (string) - The user's last name. + + email (string) - The user's email address. + + access_level (enum) - The user's primary-account role. Omitted for subaccount-scoped users. + + `admin` + + `developer` + + `reporting` + + `templates` + + `custom` + + access_policies (array) - The policies granted to the user when `access_level` is `custom`. + + subaccount_id (number) - The subaccount the user is scoped to, if any. Omitted when `subaccounts` is non-empty. + + is_sso (boolean) - Whether the user signs in via single sign-on. + + email_verified (boolean) - Whether the user has verified their email address. + + tfa_enabled (boolean) - Whether the user has two-factor authentication enabled. + + options (object) - User-level options, present only when set. + + created (string) - ISO 8601 timestamp of when the user was created. + + updated (string) - ISO 8601 timestamp of when the user was last updated. + + subaccounts (array) - The subaccounts the user has access to, and their access level on each. Always present, and an empty array for a user with no subaccount mappings. When it is non-empty, the top-level `access_level` and `subaccount_id` fields are omitted. + + (object) + + subaccount_id (number) - The subaccount ID. + + subaccount_name (string) - The subaccount's display name. + + access_level (enum) - The user's access level on the subaccount. + + `subaccount_reporting` + + `subaccount_developer` + + status (enum) - The subaccount's status. + + active + + suspended + + terminated + +### Invite object + +Returned by [List Pending Invites](#header-list-pending-invites). + ++ Data Structure: Invite + + id (string) - Unique ID for the pending invite. Use it to revoke the invite. + + email (string) - The email address the invite was sent to. + + from (string) - The email address of the user who created the invite. + + access_level (enum) - The role the user takes on when they register. + + `admin` + + `developer` + + `reporting` + + `templates` + + `custom` + + expires (number) - Unix timestamp, in epoch seconds, at which the invitation expires. + +### Invite lifecycle + +* **Invitations expire after 3 days.** After that the registration link stops working. +* **Expired invitations clean themselves up.** They drop off [List Pending Invites](#header-list-pending-invites) on expiry, so you never have to revoke them. +* **There is no resend.** To give someone another chance to register, [invite the same email address again](#header-invite-a-user). That creates a second, independent invitation with its own expiry. The first one keeps working until it expires or you [revoke it](#header-revoke-a-pending-invite). +* **Invites are rate limited.** Creating them too fast returns `429 Too Many Requests`. Back off and retry. + +### List Users [GET /v1/users] + +Returns the users on your account. + ++ Request + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + Accept: application/json + ++ Response 200 (application/json) + + { + "results": [ + { + "name": "Ada Lovelace", + "username": "ada", + "email": "ada@example.com", + "access": "admin", + "is_sso": false, + "email_verified": true, + "tfa_enabled": false, + "last_login": "2026-07-28T14:22:31.000Z" + }, + { + "name": "Grace Hopper", + "username": "grace", + "email": "grace@example.com", + "access": "reporting", + "is_sso": false, + "email_verified": true, + "tfa_enabled": true, + "last_login": null + } + ] + } + ++ Response 403 (application/json) + + { + "errors": [ + { + "message": "Forbidden" + } + ] + } + +### Retrieve a User [GET /v1/users/{username}] + +Returns one user by username, in the [Retrieved user object](#header-retrieved-user-object) shape rather than the [User object](#header-user-object) shape List Users returns. + ++ Parameters + + username (required, string, `grace`) - The username of the user to retrieve. + ++ Request + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + Accept: application/json + ++ Response 200 (application/json) + + { + "results": { + "username": "grace", + "first_name": "Grace", + "last_name": "Hopper", + "email": "grace@example.com", + "access_level": "reporting", + "is_sso": false, + "email_verified": true, + "tfa_enabled": true, + "created": "2015-01-11T08:00:00.000Z", + "updated": "2018-04-11T08:00:00.000Z", + "subaccounts": [] + } + } + ++ Response 200 (application/json) + + A subaccount-scoped user. + + { + "results": { + "username": "joe", + "first_name": "Joe", + "last_name": "Mechanic", + "email": "joe@example.com", + "is_sso": false, + "email_verified": true, + "tfa_enabled": false, + "created": "2015-01-11T08:00:00.000Z", + "updated": "2018-04-11T08:00:00.000Z", + "subaccounts": [ + { + "subaccount_id": 123, + "subaccount_name": "Joe's Garage", + "access_level": "subaccount_reporting", + "status": "active" + } + ] + } + } + ++ Response 404 (application/json) + + { + "errors": [ + { + "message": "User not found." + } + ] + } + +### Invite a User [POST /v1/users/invite] + +The API has no direct, password-based user creation. New users join by invitation. This endpoint creates the invitation and emails a registration link. The invitee follows the link, sets their own password, and gets the role you named in `access_level`, subject to the [constraints](#header-constraints). + +Send `access_level` to invite a primary-account user, or `subaccounts` to invite a subaccount-scoped one. A `subaccounts` invite does not need a top-level `access_level`. + +The response carries only the invite `id`, which you use to [list](#header-list-pending-invites) and [revoke](#header-revoke-a-pending-invite) the invitation. The API never returns the registration token. It reaches the invitee only in the invitation email, so completing registration requires access to that mailbox. + ++ Data Structure + + email (string, required) - Email address of the person to invite. Maximum 512 characters. + + access_level (enum) - The primary-account role to assign. Required unless `subaccounts` is supplied. `admin` is not available to API keys, see [Constraints](#header-constraints). + + `developer` + + `reporting` + + `templates` + + `custom` + + access_policies (array) - The policies to grant. Only valid when `access_level` is `custom`. + + subaccounts (array) - Invite the user with access to one or more subaccounts instead of the primary account. Between 1 and 25 entries. + + (object) + + subaccount_id (number, required) - The subaccount ID. + + access_level (enum, required) - The subaccount access level. + + `subaccount_reporting` + + `subaccount_developer` + ++ Request (application/json) + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + + + Body + + { + "email": "newuser@example.com", + "access_level": "reporting" + } + ++ Response 200 (application/json) + + { + "results": { + "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301" + } + } + ++ Response 400 (application/json) + + { + "errors": [ + { + "message": "email is a required parameter", + "param": "email", + "value": null + } + ] + } + ++ Response 403 (application/json) + + { + "errors": [ + { + "message": "API keys cannot invite an admin-level user." + } + ] + } + ++ Response 429 (application/json) + + { + "errors": [ + { + "message": "Too many invite requests. Please try again later." + } + ] + } + +### List Pending Invites [GET /v1/users/pending-invites] + +Returns the invitations on your account that nobody has accepted yet. Expired invitations do not appear. + ++ Request + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + Accept: application/json + ++ Response 200 (application/json) + + { + "results": [ + { + "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301", + "email": "newuser@example.com", + "from": "ada@example.com", + "access_level": "reporting", + "expires": 1720656000 + } + ] + } + +### Revoke a Pending Invite [DELETE /v1/users/pending-invites/{id}] + +Revokes a pending invitation. Its registration link stops working immediately. + ++ Parameters + + id (required, string, `3f2504e0-4f89-41d3-9a0c-0305e82c3301`) - The invite ID from [List Pending Invites](#header-list-pending-invites). + ++ Request + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + ++ Response 204 + ++ Response 404 (application/json) + + { + "errors": [ + { + "message": "User invite ID does not exist" + } + ] + } + +### Update a User [PUT /v1/users/{username}] + +Changes a user's role. The body accepts `access_level` and `access_policies` and nothing else. Any other field returns `403`. Names, passwords, email addresses, and user options stay web-app only. + +The [constraints](#header-constraints) apply here too. You cannot set a user to `admin`, and demoting the last `admin` returns `400`. + ++ Data Structure + + access_level (enum) - The primary-account role to assign. + + `developer` + + `reporting` + + `templates` + + `custom` + + access_policies (array) - The policies to grant. Only valid when `access_level` is `custom`. + ++ Parameters + + username (required, string, `grace`) - The username of the user to update. + ++ Request (application/json) + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + + + Body + + { + "access_level": "developer" + } + ++ Response 200 (application/json) + + { + "results": { + "message": "Successfully modified user grace" + } + } + ++ Response 400 (application/json) + + { + "errors": [ + { + "message": "Cannot change the access level of the last admin user on the account." + } + ] + } + ++ Response 403 (application/json) + + { + "errors": [ + { + "message": "API keys may only modify access_level and access_policies." + } + ] + } + +### Delete a User [DELETE /v1/users/{username}] + +Deletes a user from your account. + +You cannot delete the owner of the key you are calling with, a user on a different account, or the account's last `admin`. + ++ Parameters + + username (required, string, `grace`) - The username of the user to delete. + ++ Request + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + ++ Response 204 + ++ Response 400 (application/json) + + { + "errors": [ + { + "message": "Cannot delete the last admin user on the account." + } + ] + } + ++ Response 403 (application/json) + + { + "errors": [ + { + "message": "Cannot delete current user." + } + ] + } + ++ Response 404 (application/json) + + { + "errors": [ + { + "message": "User does not exist" + } + ] + } + +### Add a Subaccount Mapping [POST /v1/users/{username}/subaccounts] + +Grants a user access to a subaccount at the given access level. If the user already has access to that subaccount, the call updates their access level instead. + +This endpoint adds subaccounts to a user who is already subaccount-scoped, so the target needs at least one mapping to begin with. Calling it for a primary-account user returns `400`. To give a new person subaccount access, [invite them](#header-invite-a-user) with a `subaccounts` array. + ++ Data Structure + + subaccount_id (number, required) - The subaccount to grant access to. + + access_level (enum, required) - The access level to grant on the subaccount. + + `subaccount_reporting` + + `subaccount_developer` + ++ Parameters + + username (required, string, `grace`) - The username of the user. + ++ Request (application/json) + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + + + Body + + { + "subaccount_id": 123, + "access_level": "subaccount_reporting" + } + ++ Response 200 (application/json) + + { + "results": { + "message": "Subaccount access granted" + } + } + ++ Response 400 (application/json) + + { + "errors": [ + { + "message": "access_level must be one of: subaccount_reporting, subaccount_developer" + } + ] + } + ++ Response 400 (application/json) + + The user has no subaccount mappings. + + { + "errors": [ + { + "message": "Invalid user" + } + ] + } + +### Remove a Subaccount Mapping [DELETE /v1/users/{username}/subaccounts/{subaccountId}] + +Removes a user's access to a subaccount. + +A subaccount-scoped user must keep at least one subaccount, so removing the last mapping returns `400`. [Delete the user](#header-delete-a-user) instead. + ++ Parameters + + username (required, string, `grace`) - The username of the user. + + subaccountId (required, number, `123`) - The subaccount to revoke access to. + ++ Request + + + Headers + + Authorization: 14ac5499cfdd2bb2859e4476d2e5b1d2bad079bf + ++ Response 204 + ++ Response 400 (application/json) + + { + "errors": [ + { + "message": "Cannot delete last subaccount for user" + } + ] + }