API invite with group_names param returns 403 for group-owner user unless they are admin

Summary

A non-admin user who is an owner of a Discourse group can successfully invite new members with that group assigned through the web UI invite dialog. The identical action via the REST API (/invites.json, Global-scope API key, correct Api-Username) returns a 403 invalid_access — but only when group_names/group_ids is included in the payload. Critically, granting the user Moderator status does not fix the API call — only granting full Admin does, even though Moderator is sufficient for every other tested scenario (web UI, non-grouped API invite).

Environment

  • Discourse version: 2026.7.1
  • API key type: Global scope, CheriArchives = the test user’s exact username

Steps to reproduce

  1. Create non-admin, non-moderator user (<userid>), make them owner of group <group_name>.
  2. Web UI, as <userid>: invite with no group → succeeds. Invite with <group_name> assigned → succeeds.
  3. API, as <userid> (Global-scope key, Api-Username=<userid>):
    • POST /invites.json with no group_names → succeeds.
    • POST /invites.json with group_names=<group_name> (same group they own) → 403 invalid_access.
  4. Promote <userid> to Moderator. Repeat step 3’s grouped API call → still 403 invalid_access.
  5. Promote <userid> to Admin. Repeat step 3’s grouped API call, same key, same payload → succeeds.

Expected behavior

Since group ownership alone is sufficient permission for a non-staff user to assign that group on invite via the web UI, the API — authenticated as the same user via Api-Username — should honor the same permission. At minimum, Moderator (which is staff) should not behave identically to a non-privileged user here; the fact that Moderator fails identically to no-role, and only Admin succeeds, suggests the API code path checks user.admin? specifically rather than group ownership or even general staff status (user.staff?, which is true for moderators).

Actual behavior

The API 403s on any grouped invite unless the acting user is a full Admin. Group ownership (verified working via web UI) and Moderator/staff status (verified via direct promotion test) are both insufficient for the API path, despite either being sufficient through the web UI or for the ungrouped API case.

Question for the Discourse team

Does the API-authenticated invite-with-group code path require user.admin? explicitly, rather than checking group ownership or user.staff? the way the web UI / session-authenticated path does? If so, is this intentional (undocumented API-specific restriction) or a bug where the API guardian check diverges from the web guardian check for this action?

Happy to provide full request/response logs (key redacted) on request.