Staffs
Staffs
Staff endpoints manage human Operator Dashboard accounts.
Staff management and password-change endpoints require a staff-session bearer token; broad API-key scopes cannot replace it. Login needs no existing bearer, and logout has the API-key exception documented below.
Auth paths
POST /api/operator/auth/staff-login
POST /api/operator/auth/staff-change-password
POST /api/operator/auth/logout
staff-login returns { ok: true, token, expires_at, operator, staff }, with a pmost. bearer and a 12-hour expiry. A successful login revokes all previous sessions for that staff account before issuing a new token. Activity does not extend the fixed expiry. Inactive operator, membership, or account status prevents login/session use.
staff-login does not require an existing bearer token. Its JSON body is:
| Field | Type | Requirement | Notes |
|---|---|---|---|
email | string | Required | Valid email address. |
password | string | Required | Non-empty password. |
Login is limited to 5 failed attempts per normalized email and source identifier in a 15-minute window. A blocked attempt returns 429 RATE_LIMIT_EXCEEDED; wait for the window instead of retrying unchanged credentials.
staff-change-password is available only to staff sessions. newPassword is required and must contain at least 8 characters. oldPassword is conditionally required when must_change_password is false; it is optional for a forced first change. Successful change returns a replacement staff token and expiry because all previous sessions are revoked.
Password-change success is { ok: true, token, expires_at }; replace the old bearer immediately. An API-key call returns 400 PASSWORD_CHANGE_NOT_SUPPORTED. Invalid current password returns 400 INVALID_CREDENTIALS here (login uses 401). Login accepts only an unambiguous single active operator membership; otherwise it returns 409 MULTIPLE_OPERATOR_MEMBERSHIPS_UNSUPPORTED.
logout requires a valid bearer token and has no body. It revokes the current staff session when the bearer token is a staff token. An API-key call returns ok: true without revoking the key.
Staff management paths
GET /api/operator/staff
POST /api/operator/staff
PATCH /api/operator/staff/{membership_id}
POST /api/operator/staff/{membership_id}/reset-password
Required scopes:
staffs:readfor liststaffs:writefor create, patch, and reset password
All staff management paths also require auth_type = staff.
List query params
Both list query parameters are optional:
| Parameter | Type | Requirement | Notes |
|---|---|---|---|
search | string | Optional | Case-insensitive match against email or display name. |
status | string | Optional | active or disabled; an omitted or unrecognized value returns all. |
List returns { items: [...] }, newest membership first, without pagination. status=active requires both active membership and active account; status=disabled includes either a disabled membership or inactive account. The returned status is the membership state, while account_status is the separate account state.
Staff fields
membership_idadmin_user_idemaildisplay_namerolestatusaccount_statusscopesmust_change_passwordlast_login_atcreated_atupdated_at
Create body
{
"email": "ops@example.com",
"display_name": "Ops User",
"scopes": ["users:read", "trades:read"]
}
| Field | Type | Requirement | Notes |
|---|---|---|---|
email | string | Required | Valid, globally unused email. |
scopes | string | Required | Non-empty assignable scope list; * is forbidden. |
display_name | string | Optional | Stored display name. |
Rules:
- email must be valid and globally unused
scopesmust be non-empty*is not assignable in staff create/update- write scopes normalize to include the matching read scope when available
- staff can assign only scopes they already hold, unless they have
* - created memberships use
role: "operator_viewer"with the explicit assigned scopes; do not infer permission from role alone - normalization adds matching read scopes before checking the caller's assignable permissions; a caller must hold those added read scopes too
Patch body
All fields are optional, but at least one update is required.
{
"display_name": "Ops Lead",
"status": "disabled",
"scopes": ["users:read", "trades:read", "webhooks:read"]
}
A staff member cannot update their own access status or scopes. The implemented error is SELF_ACCESS_UPDATE_FORBIDDEN. A staff member also cannot reset their own password through the management endpoint; that returns SELF_PASSWORD_RESET_FORBIDDEN.
Patch path membership_id is required and must be a positive integer. display_name, status, and scopes are individually optional, but at least one must be supplied. Reset-password also requires the positive membership_id path parameter and has no body. Validation failures include INVALID_STAFF_ID, INVALID_STATUS, NO_UPDATES, and CANNOT_ASSIGN_SCOPE. Access or password mutations revoke active sessions for the target staff account; changing only the display name does not.
Password reset response
Create and reset responses include onboarding metadata:
{
"ok": true,
"item": {},
"onboarding": {
"email_sent": false,
"email_error": "provider error",
"temp_password": "returned-only-when-email-fails"
}
}
If email sends successfully, temp_password is null.
The item above has the full staff fields listed earlier. On email failure the account/password change has already occurred: securely hand off the returned temporary password instead of repeating create and triggering EMAIL_EXISTS. Never log or place temporary passwords in support tickets. New/reset accounts require a password change; temp_password is not returned by the list route.
Staff-only private market requests use markets:read / markets:write, while support tickets require no extra resource scope. Neither workflow can be enabled by handing an operator API key to a browser.
