Skip to content

Authentication, Users & Permissions API

Base paths: /core/api/auth, /core/api/permissions, /core/api/public and the membership routes under /core/api/communities/{community_id}.

This page covers login and token handling (Keycloak-backed), the user administration endpoints, the per-user feature permissions, the community membership endpoints and the public landing statistics. For the Keycloak token flow itself and how to call the API with a Bearer token, see Authentication. For the role model (SUPER_USER, ADMIN, MEMBER) and the difference between a user and a member, see Members. The community configuration endpoints are in the Communities API.

Endpoint summary

Method Path Purpose
POST /core/api/auth/login Exchange email and password for Keycloak tokens
POST /core/api/auth/refresh Obtain a new access token from a refresh token
POST /core/api/auth/logout Invalidate a refresh token
POST /core/api/auth/signup Create a user in Keycloak and in the database (internal, super user)
GET /core/api/auth/me Current user profile
GET /core/api/auth/userinfo Current user profile (same payload as /me)
POST /core/api/auth/password_reset_request Send a password reset email
GET /core/api/auth/users List users
GET /core/api/auth/roles Current user's platform role
PUT /core/api/auth Update the caller's own profile
DELETE /core/api/auth/users/{user_id} Delete a user (super user)
PATCH /core/api/auth/users/{user_id}/role Change a user's platform role (super user)
PATCH /core/api/auth/users/{user_id} Change a user's name or email (super user)
POST /core/api/auth/users/{user_id}/communities/{community_id} Link an existing user to a community (super user)
DELETE /core/api/auth/users/{user_id}/communities/{community_id} End a user's membership of a community (super user)
GET /core/api/auth/admin/communities List every community (super user)
GET /core/api/permissions List feature permissions of all users
GET /core/api/permissions/me Feature permissions of the current user
PUT /core/api/permissions Activate or deactivate a feature permission (super user)
GET /core/api/permissions/ems-modules List registered energy management module types
GET /core/api/public/landing-stats Public platform totals
POST /core/api/communities/{community_id}/users Create a user and add them to a community
GET /core/api/communities/{community_id}/members List community members
GET /core/api/communities/{community_id}/members/by-nif Find a member by NIF
GET /core/api/communities/{community_id}/members/by-name Find a member by name
GET /core/api/communities/{community_id}/members/metrics/kpis Savings per member over a period
PUT /core/api/communities/{community_id}/users/{user_id} Update a member and the dated membership
DELETE /core/api/communities/{community_id}/users/{user_id} End a membership
POST /core/api/communities/{community_id}/users/{user_id}/invite Send or resend the dashboard invite

Who can call what

Authorization is enforced by two dependencies (is_allowed and get_current_user) plus explicit checks inside some handlers.

  • Authenticated means a valid Keycloak Bearer token (401 if missing, invalid or expired) that maps to a user in the database (404 if the user is not found).
  • Verified means the user's is_verified flag is true. is_allowed rejects unverified users with 404 Please verify your account. Users become verified on their first successful login or token refresh.
  • Global endpoints (no community_id in the path, for example /core/api/permissions): SUPER_USER may call everything; any verified user may call GET; every other method is SUPER_USER only (403).
  • Community-scoped endpoints (community_id in the path): the caller must belong to that community (403 You are not a member of this community). Community ADMIN may use every method; community MEMBER may only use GET (403 Community members can only perform read operations). SUPER_USER bypasses these checks.
  • An unknown community_id returns 403 (the platform's NotFound error uses status 403), not 404.
  • Some handlers add a stricter check on top (for example "SUPER_USER only", or "own data only" through assert_self_or_privileged). Those are stated on each endpoint below.
Role Where it lives Meaning
SUPER_USER user.role (platform) Unrestricted
USER user.role (platform) Default platform role, no implicit community access
ADMIN community membership Full access inside that community
MEMBER community membership Read access inside that community

Authentication

Login

POST /core/api/auth/login

Exchanges an email and password for Keycloak tokens using the Resource Owner Password (direct grant) flow, and returns the matching database user. On the first successful login the user is linked to its Keycloak ID and marked as verified; pending admin memberships (created by an invitation) are activated at that moment.

Auth / permissions: public (no token).

Parameter In Type Required Description
username body (form) string Yes The user's email address
password body (form) string Yes The user's password

The body is application/x-www-form-urlencoded (OAuth2 password form), not JSON.

Example

curl -X POST https://<host>/core/api/auth/login \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=admin@example.com" \
  -d "password=your_password"

Response (abridged)

{
  "access_token": "eyJhbGci...",
  "refresh_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 300,
  "user": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "name": "Ana Costa",
    "email": "admin@example.com",
    "role": { "id": "8c1f0d3e-5a41-4f0b-9a52-0e7f3c2b6d10", "name": "ADMIN" },
    "is_verified": true,
    "managed_communities": [
      { "id": "6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44", "name": "REC Matosinhos" }
    ]
  },
  "role": "ADMIN"
}

Tip

  • Invalid credentials return 401 Invalid email or password. A user that exists in Keycloak but not in the RECreation database returns 404 User not found in database. Please contact administrator. Any other failure returns 500.
  • The user is matched by email. Direct Keycloak token requests (see Authentication) are also accepted by the rest of the API; this endpoint is the convenient variant that also returns the user and role.
  • Access tokens are short-lived (about 5 minutes); renew them with POST /core/api/auth/refresh.

Refresh Access Token

POST /core/api/auth/refresh

Uses a refresh token to obtain a new access token. Returns the same payload as login (tokens, user, role).

Auth / permissions: public (the refresh token is the credential).

Parameter In Type Required Description
refresh_token body string Yes The refresh token issued by login

Example

curl -X POST https://<host>/core/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "eyJhbGci..."}'

Response (abridged)

{
  "access_token": "eyJhbGci...",
  "refresh_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 300,
  "user": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "admin@example.com", "is_verified": true },
  "role": "ADMIN"
}

Tip

  • If Keycloak does not rotate the refresh token, the response echoes back the one you sent.
  • An invalid or expired refresh token returns 401 Invalid or expired refresh token. A user missing from the database returns 404 User not found in database.
  • When both tokens have expired the user must log in again.

Logout

POST /core/api/auth/logout

Invalidates the refresh token in Keycloak (the Keycloak session is ended).

Auth / permissions: public.

Parameter In Type Required Description
refresh_token body string Yes The refresh token to invalidate

Example

curl -X POST https://<host>/core/api/auth/logout \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "eyJhbGci..."}'

Response

{ "message": "Successfully logged out" }

Tip

The endpoint always answers 200 with this message, even if Keycloak is unreachable or rejects the token (the failure is only logged). Clients should discard their tokens regardless.

Register User (internal)

POST /core/api/auth/signup

Creates a user in Keycloak, assigns the matching Keycloak realm role, creates the database user linked through keycloak_id, and creates the user's feature permissions (all inactive).

Internal endpoint

Declared with include_in_schema=False: it does not appear in the generated OpenAPI documentation. To add members to a community use POST /core/api/communities/{community_id}/users instead.

Auth / permissions: SUPER_USER only (403 Super users only otherwise).

Parameter In Type Required Description
name body string Yes Full name. The first word becomes the Keycloak first name, the rest the last name
email body string No Email; also used as the Keycloak username
password body string Yes At least 6 characters
role body enum No SUPER_USER, ADMIN (default), MEMBER or USER. Mapped to Keycloak roles SUPER_USER, COMMUNITY_ADMIN, COMMUNITY_MEMBER (anything else maps to COMMUNITY_MEMBER)
phone body string No Phone number
nif body string No Tax identification number, validated
address body string No Address

Example

curl -X POST https://<host>/core/api/auth/signup \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ana Costa",
    "email": "ana.costa@example.com",
    "password": "S3cret-pass",
    "role": "ADMIN"
  }'

Response 201 Created (abridged)

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "Ana Costa",
  "email": "ana.costa@example.com",
  "is_verified": false,
  "role": { "id": "8c1f0d3e-5a41-4f0b-9a52-0e7f3c2b6d10", "name": "ADMIN" },
  "permissions": [ { "name": "OPTIMIZATION_MODULE", "is_active": false } ]
}

Tip

  • If a verified user with that email already exists the call fails with 404 User with this email already exists. An existing unverified user is re-linked to a new Keycloak account instead.
  • Any failure during creation rolls back the database and returns 400 Failed to create user: ....
  • The new user is unverified until the first login (POST /core/api/auth/login).

Get Current User

GET /core/api/auth/me

Returns the profile of the authenticated user, including the platform role, feature permissions, managed communities and community roles.

Auth / permissions: any authenticated user (the verified check is not applied here, so unverified users can also call it).

Example

curl https://<host>/core/api/auth/me \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "Ana Costa",
  "email": "ana.costa@example.com",
  "phone": "912345678",
  "address": "Rua do Porto 123",
  "nif": "123456789",
  "is_verified": true,
  "role": { "id": "8c1f0d3e-5a41-4f0b-9a52-0e7f3c2b6d10", "name": "ADMIN" },
  "created_at": "2026-01-15T10:30:00Z",
  "ownerships": [],
  "permissions": [
    { "id": "0d5a7c34-72a1-4e0e-9f61-6f4b1c2e8a90", "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "OPTIMIZATION_MODULE", "is_active": true }
  ],
  "managed_communities": [
    { "id": "6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44", "name": "REC Matosinhos" }
  ],
  "community_roles": [
    { "id": "6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44", "name": "REC Matosinhos", "role": "ADMIN" }
  ]
}

Tip

managed_communities lists the communities where the user is ADMIN; community_roles lists every non-deleted community association with the role in each. Use these to decide which community selector to show in a client. Two routers register GET /core/api/auth/me (authentication and users); both return the same payload.

Get User Info

GET /core/api/auth/userinfo

Returns the database user matching the Keycloak token. In the current implementation the payload is identical to GET /core/api/auth/me.

Auth / permissions: any authenticated user.

Example

curl https://<host>/core/api/auth/userinfo \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "Ana Costa",
  "email": "ana.costa@example.com",
  "is_verified": true,
  "role": { "id": "8c1f0d3e-5a41-4f0b-9a52-0e7f3c2b6d10", "name": "ADMIN" }
}

Tip

The user is looked up by the token's Keycloak subject first and by email as a fallback; in the fallback case the account is linked to the Keycloak ID on the fly.

Request Password Reset

POST /core/api/auth/password_reset_request

Asks Keycloak to email the user a link to set a new password.

Auth / permissions: public. Rate limited to 20 requests per hour per client IP (429 when exceeded).

Parameter In Type Required Description
email body string Yes The account email

Example

curl -X POST https://<host>/core/api/auth/password_reset_request \
  -H "Content-Type: application/json" \
  -d '{"email": "ana.costa@example.com"}'

Response

{ "message": "Email enviado para o endereço de email solicitado!" }

Tip

The response is always the same, whether or not the email exists or the email could be sent, so the endpoint cannot be used to discover registered addresses. Only users that already have a Keycloak account (keycloak_id set) actually receive an email. The reset link redirects to the platform frontend.


Users

List Users

GET /core/api/auth/users

Returns users of the platform with their role, permissions and community roles.

Auth / permissions: any verified user (global endpoint, GET).

Example

curl https://<host>/core/api/auth/users \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

[
  {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "name": "Ana Costa",
    "email": "ana.costa@example.com",
    "role": { "id": "8c1f0d3e-5a41-4f0b-9a52-0e7f3c2b6d10", "name": "ADMIN" },
    "is_verified": true,
    "community_roles": [
      { "id": "6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44", "name": "REC Matosinhos", "role": "ADMIN" }
    ]
  }
]

Tip

The handler's description says it excludes SUPER_USER accounts, but the query reads every user in the table, so super users are included. To list the members of one community use GET /core/api/communities/{community_id}/members.

Get Current User Role

GET /core/api/auth/roles

Returns the platform role of the authenticated user.

Auth / permissions: any authenticated user.

Example

curl https://<host>/core/api/auth/roles \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

{ "id": "8c1f0d3e-5a41-4f0b-9a52-0e7f3c2b6d10", "name": "ADMIN" }

Tip

The handler returns a single role object while the response model is declared as a list of roles. If your client has trouble with this route, read role from GET /core/api/auth/me instead, which is the same information in a stable shape.

Update Own Profile

PUT /core/api/auth

Updates profile fields of the authenticated user. Only the fields you send are changed.

Auth / permissions: the route applies is_allowed on a global endpoint with an unsafe method, so in practice only SUPER_USER is accepted (403 Only super-users can perform this action for anyone else). It always updates the caller's own record, never another user's.

Parameter In Type Required Description
name body string No Full name
email body string No Email
password body string No New password (see tip)
nif body string No Tax identification number, validated
address body string No Address
phone body string No Phone number

Example

curl -X PUT https://<host>/core/api/auth \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Ana M. Costa", "phone": "912345678"}'

Response (abridged)

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "Ana M. Costa",
  "phone": "912345678",
  "email": "ana.costa@example.com"
}

Tip

  • The change is applied to the RECreation database only. It is not synchronised to Keycloak (name, email and password in Keycloak are not touched). Users who forgot their password should use POST /core/api/auth/password_reset_request.
  • To change a user's name or email in both places, a super user can use PATCH /core/api/auth/users/{user_id}.
  • To edit a community member's contact details together with the dated membership, use PUT /core/api/communities/{community_id}/users/{user_id}.

Delete User

DELETE /core/api/auth/users/{user_id}

Permanently deletes a user from the database (permissions, community associations and membership history included) and then removes the Keycloak account.

Auth / permissions: SUPER_USER only (403 Only SUPER_USER can delete users.).

Parameter In Type Required Description
user_id path UUID Yes The user to delete

Example

curl -X DELETE https://<host>/core/api/auth/users/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
  -H "Authorization: Bearer $TOKEN"

Response

{ "deleted": true }

Tip

  • Deletion is irreversible and invalidates the cached results of every community period the user belonged to. To end a membership while keeping the user and the history, use DELETE /core/api/communities/{community_id}/users/{user_id}?effective_at=....
  • If the Keycloak removal fails, the database user is already gone; the failure is logged and the call still returns 200.
  • Unknown user_id returns 404 User not found.

Change User Role

PATCH /core/api/auth/users/{user_id}/role

Changes the platform role of a user, in the database and in Keycloak.

Auth / permissions: SUPER_USER only (403 Only SUPER_USER can change roles.).

Parameter In Type Required Description
user_id path UUID Yes The user to change
role body enum Yes SUPER_USER, USER, ADMIN or MEMBER

Example

curl -X PATCH https://<host>/core/api/auth/users/3fa85f64-5717-4562-b3fc-2c963f66afa6/role \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role": "ADMIN"}'

Response (abridged)

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "Ana Costa",
  "role": { "id": "8c1f0d3e-5a41-4f0b-9a52-0e7f3c2b6d10", "name": "ADMIN" }
}

Tip

  • This changes the user's platform role (user.role). Per-community roles are changed through the membership endpoints.
  • A failed Keycloak synchronisation does not roll back the database change; it is only logged, so the two can diverge until the next change.
  • Unknown user returns 404; an unknown role name returns 400 Invalid role.

Update User (admin)

PATCH /core/api/auth/users/{user_id}

Changes the name and/or email of any user, in the database and in Keycloak.

Auth / permissions: SUPER_USER only (403 Only SUPER_USER can update users.).

Parameter In Type Required Description
user_id path UUID Yes The user to change
name body string No New full name
email body string No New email. Must not be used by another user

Example

curl -X PATCH https://<host>/core/api/auth/users/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email": "ana.new@example.com"}'

Response (abridged)

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "Ana Costa",
  "email": "ana.new@example.com"
}

Tip

  • Sending neither name nor email is a no-op that returns the user unchanged.
  • Email already used by another user returns 409 Email already in use.; unknown user returns 404 User not found.
  • A failed Keycloak synchronisation is logged and does not roll back the database change.
POST /core/api/auth/users/{user_id}/communities/{community_id}

Links an existing user to a community as MEMBER or ADMIN. The membership is created ACTIVE for the given period. If the user already has a membership in that community the requested period is replaced with the new role and status.

Auth / permissions: SUPER_USER only (403 Only SUPER_USER can assign communities.).

Parameter In Type Required Description
user_id path UUID Yes The user to link
community_id path UUID Yes The community
role query enum No MEMBER (default) or ADMIN. Other values return 400 Role must be MEMBER or ADMIN.
start_date query date Yes First day (inclusive, Europe/Lisbon) of the membership
end_date query date No Last day (inclusive). Omit for an open-ended membership

Example

curl -X POST "https://<host>/core/api/auth/users/3fa85f64-5717-4562-b3fc-2c963f66afa6/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44?role=ADMIN&start_date=2026-01-01" \
  -H "Authorization: Bearer $TOKEN"

Response

{ "linked": true, "role": "ADMIN" }

Tip

  • Membership is dated; see Business Validity. Results of the affected period are invalidated and must be recomputed.
  • is_host and is_promoter are kept from the covering version when one exists, otherwise false.
  • Unknown user returns 404 User not found.; other errors from the membership write are returned as 400 with the error message.
  • This does not create a Keycloak account or send an invitation; use POST /core/api/communities/{community_id}/users/{user_id}/invite.
DELETE /core/api/auth/users/{user_id}/communities/{community_id}

Ends a user's membership of a community from effective_at. History before that day is kept, and the user account is not deleted.

Auth / permissions: SUPER_USER only (403 Only SUPER_USER can remove community management.).

Parameter In Type Required Description
user_id path UUID Yes The user
community_id path UUID Yes The community
effective_at query date Yes First day with no membership; the membership is closed on the previous day

Example

curl -X DELETE "https://<host>/core/api/auth/users/3fa85f64-5717-4562-b3fc-2c963f66afa6/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44?effective_at=2026-07-01" \
  -H "Authorization: Bearer $TOKEN"

Response 204 No Content (empty body).

Tip

  • Versions that start on or after effective_at are removed; a version spanning that day is truncated to the day before.
  • The association is marked INACTIVE and the community's results from effective_at onwards are invalidated.
  • Returns 404 Association not found. if the user has no membership in the community.

List All Communities (admin)

GET /core/api/auth/admin/communities

Returns every community in the platform, for the administration screen. This differs from GET /core/api/communities, which only lists communities the caller belongs to.

Auth / permissions: SUPER_USER only (403 Only SUPER_USER can list all communities.).

Example

curl https://<host>/core/api/auth/admin/communities \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

[
  {
    "id": "6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44",
    "name": "REC Matosinhos",
    "identifier": "rec-matosinhos",
    "status": "ACTIVE",
    "type": "REC",
    "market_rate": "VANILLA_IMR",
    "version_id": null,
    "start_date": null,
    "end_date": null,
    "total_members": 0
  }
]

Tip

total_members is always 0 in this response. Use GET /core/api/communities/{community_id} for aggregated figures. When no community exists at all the platform's NotFound error is returned, with status 403.


Permissions

Feature permissions switch optional modules on or off per user. Each user has one row per PermissionType, created inactive when the user is created. Permissions are separate from roles.

Permission Feature
OPTIMIZATION_MODULE, SIMULATION_MODULE, SIZING_MODULE, PRICING_MODULE Platform modules
FLEXIBILITY_MODULE Umbrella permission for energy management route access. Managed automatically (see the tip on PUT /core/api/permissions)
EMS_GDBN_BEFLEXIBLE, EMS_OSTEC_SIMPLE, EMS_OSTEC_AC, EMS_OSTEC_FLEX, EMS_OSTEC_FLEX_HEMS, EMS_OSTEC_PROMOTOR, EMS_OSTEC_ARBITRAGEM One per registered energy management module, see Energy Management

List All Permissions

GET /core/api/permissions

Returns the permission rows of all users, except rows belonging to SUPER_USER accounts. Used by the administration screen.

Auth / permissions: any verified user (global endpoint, GET).

Example

curl https://<host>/core/api/permissions \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

[
  {
    "id": "0d5a7c34-72a1-4e0e-9f61-6f4b1c2e8a90",
    "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "name": "OPTIMIZATION_MODULE",
    "is_active": true
  }
]

Tip

The result is not paginated and not filtered by community: it contains every row of every non-super user (one row per permission type per user). Filter client-side by user_id.

Get My Permissions

GET /core/api/permissions/me

Returns the permission rows of the authenticated user, active and inactive.

Auth / permissions: any verified user.

Example

curl https://<host>/core/api/permissions/me \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

[
  { "id": "0d5a7c34-72a1-4e0e-9f61-6f4b1c2e8a90", "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "OPTIMIZATION_MODULE", "is_active": true },
  { "id": "5e1b9a02-3c77-4d18-a4f0-7b6c8d9e0f12", "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "SIZING_MODULE", "is_active": false }
]

Tip

Despite the endpoint description ("active permissions"), inactive rows are returned too. Check is_active before showing a feature.

Update a Permission

PUT /core/api/permissions

Activates or deactivates one permission row. The row is located by id; name and is_active of that row are overwritten with the values sent.

Auth / permissions: SUPER_USER only (global endpoint, unsafe method; 403 Only super-users can perform this action otherwise).

Parameter In Type Required Description
id body UUID Yes The permission row to change
user_id body UUID Yes Owner of the row (required by the schema; the row is located by id)
name body enum Yes Permission type (see the table above)
is_active body boolean Yes New state

Example

curl -X PUT https://<host>/core/api/permissions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "0d5a7c34-72a1-4e0e-9f61-6f4b1c2e8a90",
    "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "name": "EMS_OSTEC_FLEX",
    "is_active": true
  }'

Response

{
  "id": "0d5a7c34-72a1-4e0e-9f61-6f4b1c2e8a90",
  "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "EMS_OSTEC_FLEX",
  "is_active": true
}

Tip

  • Changing any EMS_* permission also updates that user's FLEXIBILITY_MODULE row: it becomes active if at least one EMS_* permission is active and inactive when none is. Do not set FLEXIBILITY_MODULE by hand.
  • An unknown id is not handled explicitly and results in a server error rather than 404.

List EMS Modules

GET /core/api/permissions/ems-modules

Returns the registered energy management module types with their display names. The module_type values correspond to the EMS_<module_type> permission names.

Auth / permissions: any verified user.

Example

curl https://<host>/core/api/permissions/ems-modules \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

[
  { "module_type": "OSTEC_FLEX", "display_name": "OSTEC Flexibility" },
  { "module_type": "OSTEC_ARBITRAGEM", "display_name": "OSTEC Battery Arbitrage" },
  { "module_type": "GDBN_BEFLEXIBLE", "display_name": "GDBN BeFlexible" }
]

Tip

The list comes from the module registry in code, so it reflects what the deployed version supports. Use it to build the per-user permission screen without hard-coding module names.


Public

Landing Statistics

GET /core/api/public/landing-stats

Returns platform-wide totals, used by the login screen.

Auth / permissions: public (no token, no permission checks).

Example

curl https://<host>/core/api/public/landing-stats

Response

{
  "total_communities": 12,
  "total_members": 348,
  "total_resources": 95
}

Tip

Only aggregate counts are exposed. total_members counts every user in the platform (all roles), not only active community members, and the figures are not scoped to the caller.


Community membership

These endpoints sit under /core/api/communities/{community_id} and follow the community-scoped rules in Who can call what: ADMIN of that community (or SUPER_USER) may write, any member of the community may read. Membership is dated, see Business Validity and Members.

Add Member to a Community

POST /core/api/communities/{community_id}/users

Creates a new user and adds them to the community with a dated membership. The field-by-field description, examples and response are in Communities API: Add Member to Community.

Auth / permissions: community ADMIN or SUPER_USER.

Parameter In Type Required Description
community_id path UUID Yes The community
name, email, phone, nif, address body string name required Profile of the new user
status body enum No ACTIVE, INACTIVE or DELETED
role body enum No MEMBER (default) or ADMIN
has_dashboard_access body boolean No Create a Keycloak account and send the invitation (needs email)
is_host, is_promoter body boolean No Anchor and Promoter flags
start_date body date Yes First day of the membership
end_date body date No Last day of the membership

Example

curl -X POST https://<host>/core/api/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Joao Silva",
    "email": "joao.silva@example.com",
    "role": "MEMBER",
    "status": "ACTIVE",
    "has_dashboard_access": true,
    "start_date": "2026-01-01"
  }'

Response 201 Created (abridged)

{
  "id": "9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23",
  "name": "Joao Silva",
  "email": "joao.silva@example.com",
  "status": "ACTIVE",
  "has_dashboard_access": true,
  "is_verified": false
}

Tip

  • A password is auto-generated when none is sent; the invited user sets their own through the Keycloak invitation email.
  • The Keycloak account and invitation are best effort: if they fail the member is still created and the invitation can be sent again with the invite endpoint below. has_dashboard_access is reported as true when the user has a Keycloak ID.
  • The role sent is also stored as the new user's platform role. Use MEMBER or ADMIN.
  • This response carries status and has_dashboard_access only; the dated fields (membership_version_id, start_date, end_date, is_host, is_promoter) come back from GET .../members and PUT .../users/{user_id}.
  • When status is omitted the membership version is stored as INACTIVE, so send status: "ACTIVE" for the member to take part in market and settlement.

List Community Members

GET /core/api/communities/{community_id}/members

Returns the users whose MEMBER membership covers effective_at, ordered by name, with their ownerships and the membership version fields. See also Communities API: List Community Members.

Auth / permissions: any member of the community (ADMIN or MEMBER), or SUPER_USER.

Parameter In Type Required Description
community_id path UUID Yes The community
effective_at query date No Day to evaluate (default: today, Europe/Lisbon)
exclude_deleted query string No Accepted for compatibility (true/false)

Example

curl "https://<host>/core/api/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44/members?effective_at=2026-03-01" \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

[
  {
    "id": "9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23",
    "name": "Joao Silva",
    "email": "joao.silva@example.com",
    "status": "ACTIVE",
    "is_host": false,
    "is_promoter": false,
    "has_dashboard_access": true,
    "membership_version_id": "9b2e6c1a-0d4f-4c5e-8a7b-2f1e3d4c5b6a",
    "start_date": "2026-01-01",
    "end_date": null,
    "ownerships": []
  }
]

Tip

  • Only memberships with the role MEMBER are returned; community admins are not listed here.
  • exclude_deleted does not change the result: membership validity (effective_at) decides who appears. Members whose membership ended before effective_at are absent.
  • ownerships are evaluated at effective_at and restricted to this community.
  • Returns an empty list when nothing is found.

Get Member by NIF

GET /core/api/communities/{community_id}/members/by-nif

Finds a member of the community by tax identification number.

Auth / permissions: community ADMIN or SUPER_USER may look up anyone; a MEMBER may only look up themselves (the nif must equal their own, otherwise 403 Access denied).

Parameter In Type Required Description
community_id path UUID Yes The community
nif query string Yes Exact NIF to search for

Example

curl "https://<host>/core/api/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44/members/by-nif?nif=123456789" \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

{
  "id": "9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23",
  "name": "Joao Silva",
  "nif": "123456789",
  "status": "ACTIVE",
  "is_host": false,
  "is_promoter": false,
  "has_dashboard_access": true
}

Tip

  • No match returns 404 Member not found.
  • The values reflect the association's current status and flags; the dated fields (membership_version_id, start_date, end_date) are not populated by this endpoint.

Get Member by Name

GET /core/api/communities/{community_id}/members/by-name

Finds a member of the community by full name.

Auth / permissions: community ADMIN or SUPER_USER may look up anyone; a MEMBER may only look up themselves (the name must equal their own, otherwise 403 Access denied).

Parameter In Type Required Description
community_id path UUID Yes The community
name query string Yes Name to search for

Example

curl -G "https://<host>/core/api/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44/members/by-name" \
  --data-urlencode "name=Joao Silva" \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

{
  "id": "9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23",
  "name": "Joao Silva",
  "status": "ACTIVE",
  "is_host": false,
  "is_promoter": false,
  "has_dashboard_access": true
}

Tip

  • No match returns 404 Member not found.
  • Names are not unique; prefer the user id or the NIF when you have them. URL-encode names with spaces or accents.

Get Members KPIs

GET /core/api/communities/{community_id}/members/metrics/kpis

Returns the savings of each member over a period, computed from the settlement results as the revenue with the community minus the revenue without it (rev_total - rev_total_no_csc). See Settlement.

Auth / permissions: any member of the community (ADMIN or MEMBER), or SUPER_USER.

Parameter In Type Required Description
community_id path UUID Yes The community
start_date query date Yes Start of the period
end_date query date Yes End of the period

Example

curl "https://<host>/core/api/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44/members/metrics/kpis?start_date=2026-01-01&end_date=2026-03-31" \
  -H "Authorization: Bearer $TOKEN"

Response (abridged)

{
  "9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23": {
    "savings": 41.27,
    "net_consumption": 0,
    "co2_reduction": 0
  }
}

Tip

  • The object is keyed by member (user) ID. Savings are in EUR, summed over the settled days in the period.
  • net_consumption and co2_reduction are placeholders and are always 0.
  • Any community member can read the savings of every member through this endpoint, not only their own.
  • Only periods that have settlement results contribute; run the settlement first.

Update a Member

PUT /core/api/communities/{community_id}/users/{user_id}

Updates the member's profile and the dated membership values. Full description in Communities API: Update a Membership.

Auth / permissions: community ADMIN or SUPER_USER.

Parameter In Type Required Description
community_id path UUID Yes The community
user_id path UUID Yes The member
name, email, nif, address, phone body string No Profile fields (not dated)
status body enum No ACTIVE, INACTIVE or DELETED (dated)
is_host, is_promoter body boolean No Anchor and Promoter flags (dated)
start_date body date Yes First day the dated values apply
end_date body date No Last day (inclusive). null = open-ended

Example

curl -X PUT https://<host>/core/api/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44/users/9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "INACTIVE", "start_date": "2026-07-01"}'

Response (abridged)

{
  "id": "9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23",
  "name": "Joao Silva",
  "status": "INACTIVE",
  "is_host": false,
  "is_promoter": false,
  "membership_version_id": "4c8e2a19-7b30-4f56-9d01-3a5b7c9e1f68",
  "start_date": "2026-07-01",
  "end_date": null
}

Tip

  • An email used by another user returns 409 (message in Portuguese: "Este email ja esta a ser usado por outro utilizador.").
  • Sending only profile fields (no status, is_host or is_promoter) updates the profile without creating a new membership boundary.
  • Changing dated fields rewrites the period [start_date, end_date] and invalidates the community's results for that period. If no membership version covers start_date the call fails with the platform NotFound error (status 403).
  • A member that does not belong to the community also returns the platform NotFound error (status 403).

End a Membership

DELETE /core/api/communities/{community_id}/users/{user_id}

Ends the membership from effective_at; the user account is kept. See Communities API: End a Membership.

Auth / permissions: community ADMIN or SUPER_USER.

Parameter In Type Required Description
community_id path UUID Yes The community
user_id path UUID Yes The member
effective_at query date Yes First day without membership

Example

curl -X DELETE "https://<host>/core/api/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44/users/9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23?effective_at=2026-07-01" \
  -H "Authorization: Bearer $TOKEN"

Response 204 No Content (empty body), or 200 with a message when the member still owns resources:

{
  "member_id": "9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23",
  "message": "Membro tem recursos alocados. Por favor, remova-os antes de apagar o membro"
}

Tip

  • Ownerships active on effective_at block the operation: nothing is changed and the 200 body above is returned. Remove or end the ownerships first, then repeat the call. Check the status code, not only the absence of an error.
  • On success the membership versions are closed on the day before effective_at, the association is set to INACTIVE and the community results from effective_at onwards are invalidated.

Send Dashboard Invite

POST /core/api/communities/{community_id}/users/{user_id}/invite

Gives a community member access to the dashboard: creates the Keycloak account if the member has none (assigning the realm role and the community group) and sends an email with the link to set the password and verify the email. Calling it again resends the email.

Auth / permissions: community ADMIN or SUPER_USER.

Parameter In Type Required Description
community_id path UUID Yes The community
user_id path UUID Yes The member to invite

Example

curl -X POST https://<host>/core/api/communities/6b2d9c10-1f4e-4a8b-b7c3-9d5e2a1f0c44/users/9a3c1f7e-2b64-4d85-8e10-5c7d9b0a1e23/invite \
  -H "Authorization: Bearer $TOKEN"

Response

{ "message": "Invite sent" }

Tip

  • The member's email is used as the Keycloak username. The Keycloak role is derived from the membership role (ADMIN gives COMMUNITY_ADMIN, otherwise COMMUNITY_MEMBER).
  • The Keycloak ID is saved before the email is sent, so a retry after a mail failure does not create a duplicate account.
  • No membership in this community returns 404 Member not found in this community; Keycloak or mail failures return 502 Failed to send invite: ....
  • After accepting the invite the member can sign in with POST /core/api/auth/login or directly against Keycloak.