Aller au contenu principal
Cette page vous a-t-elle été utile ?

User Group API

People Groups and Calendar Sharing

Manage reusable people groups, invites, and group-shared calendars

User groups are people groups, not calendar sidebar groups. They let a user share one or more calendars with a stable set of members while preserving the existing calendar color, icon, visibility, and event behavior.

JWT or user API keyPrivacy-preserving invitesCalendarShare principal: groupOwner/admin controls

Authentication and Permissions​

  • All endpoints require authentication.
  • Plan gating is per route, not per controller. Creating a group and everything that manages one - update, delete, add/update/remove members, create/list/withdraw invites, attach/detach calendars - needs the user_groups plan feature (@RequireFeature('user_groups'); a plan without it gets 403 FEATURE_NOT_ENTITLED). Reading the groups you belong to (GET /user-groups, GET /user-groups/:id, GET /user-groups/:id/members), accepting or declining an invitation and leaving a group are open to every plan: a Free account must be able to be invited, say yes, see the group and leave it. A controller-wide gate once locked all of that.
  • Users can list groups they own or belong to.
  • Group metadata, membership, invite, and calendar-link writes require owner or admin role.
  • Deleting a group requires the group owner. The owner cannot leave their group (403 GROUP_OWNER_CANNOT_LEAVE); everyone else can.
  • Calendar attachment also checks the actor's calendar rights before creating the group share.
  • Invite creation is privacy-preserving: the response shape does not reveal whether the email belongs to an existing PrimeCal user.
  • Invite accept and decline use an opaque token and authenticated user context. They answer only for the account the invitation is addressed to (403 INVITATION_NOT_FOR_YOU otherwise); invalid, expired and already-used tokens answer 404 INVITATION_NOT_FOUND / INVITATION_EXPIRED. The unified Invitations API lists and answers group and seat invitations together.

Endpoint Reference​

Groups​

MethodPathPurposeRequest or queryAuth
GET/api/user-groupsList groups owned by or shared with the current user.NoneJWT or user API key
POST/api/user-groupsCreate a people group.Body: name,slug,description,kindJWT or user API key
GET/api/user-groups/:idGet one group with members and shared calendar details where visible.Path: idJWT or user API key
PATCH/api/user-groups/:idUpdate group metadata.Path: id, body: partial group fieldsJWT or user API key
DELETE/api/user-groups/:idDelete a people group.Path: idJWT or user API key

Members​

MethodPathPurposeRequest or queryAuth
GET/api/user-groups/:id/membersList group members.Path: idJWT or user API key
POST/api/user-groups/:id/membersAdd or update a member directly by user ID.Path: id, body: userId,roleJWT or user API key
PATCH/api/user-groups/:id/members/:userIdChange a member role.Path: id,userId, body: roleJWT or user API key
DELETE/api/user-groups/:id/members/:userIdRemove a member from the group.Path: id,userIdJWT or user API key
POST/api/user-groups/:id/leaveLeave a group you belong to (open to every plan; 204). The owner cannot.Path: idJWT or user API key

Invites​

MethodPathPurposeRequest or queryAuth
POST/api/user-groups/:id/invitesCreate an invite by email, optionally with a seat on the inviter's plan. Inviting the same address to the same group again, by the same sender, re-sends their pending invitation; another admin starts one of their own (a seat held for an invitation is on its sender's plan).Path: id, body: email,message,addToMyAccountJWT or user API key
GET/api/user-groups/:id/invitesList pending group invites for owner/admin review.Path: idJWT or user API key
DELETE/api/user-groups/:id/invites/:inviteIdWithdraw a pending invite and release the seat held for it (204).Path: id,inviteIdJWT or user API key
POST/api/user-groups/invites/:token/acceptAccept an invite by opaque token. Returns the group plus seatActivated. Open to every plan.Path: tokenJWT or user API key
POST/api/user-groups/invites/:token/declineDecline an invite by opaque token; releases its seat. Open to every plan.Path: tokenJWT or user API key

Calendars​

MethodPathPurposeRequest or queryAuth
POST/api/user-groups/:groupId/calendarsAttach one or more existing calendars to a people group.Path: groupId, body: calendarIds,calendarId,permissionJWT or user API key
POST/api/user-groups/:groupId/calendars/createCreate a calendar primarily attached to a people group.Path: groupId, body: calendar fields plus optional groupPermissionJWT or user API key
DELETE/api/user-groups/:groupId/calendarsDetach one or more calendars from a people group.Path: groupId, body: calendarIds or calendarIdJWT or user API key
DELETE/api/user-groups/:groupId/calendars/:calendarIdDetach one calendar from a people group.Path: groupId,calendarIdJWT or user API key

DTO Shapes and Validation​

CreateUserGroupDto​

  • name: required string, 2 to 200 chars.
  • slug: optional stable key, max 120 chars, pattern ^[a-z0-9][a-z0-9_-]*$.
  • description: optional string, max 500 chars.
  • kind: optional enum family|friends|team|resource|custom.

UpdateUserGroupDto​

  • Same group metadata fields as create, all optional.
  • slug and description may be cleared with null where service behavior allows it.

AddUserGroupMemberDto and UpdateUserGroupMemberDto​

  • userId: required integer greater than 0 for direct member additions.
  • role: enum admin|member for API callers. owner is reserved for the group owner record.

CreateUserGroupInviteDto​

  • email: required email string, max 320 chars, trimmed and lowercased server-side.
  • message: optional string, max 500 chars, trimmed server-side.
  • addToMyAccount: optional boolean. Also give the invitee a seat on the inviter's own subscription (never anyone else's - a group admin who is not the plan owner gives a seat on their own plan). The seat is reserved first: if the plan has no seats (403 SEAT_MANAGEMENT_NOT_AVAILABLE), every seat is taken (400 SEAT_LIMIT_REACHED), the address already has one (409 SEAT_ALREADY_ASSIGNED) or is the inviter's own (400 CANNOT_INVITE_OWNER), nothing is created. The seat expires with the invitation (14 days), is activated when the invitation is accepted, declined when it is declined and released when it is withdrawn.

The response is { id, status, expiresAt, emailDeliveryPlanned, emailSent, seatReserved } - none of it depends on whether the address has an account. emailSent is false when no email provider is configured or sending failed; the invitation is saved either way.

Invite storage includes token, expiresAt, seatId (the seat held for it, ON DELETE SET NULL), emailSentAt and emailSendAttempts. UserGroupInviteStatus is pending, accepted, declined, expired or revoked (withdrawn by the sender).

GroupCalendarsDto​

  • calendarIds: array of unique integers, 1 to 100 items.
  • calendarId: legacy single-calendar field, accepted when calendarIds is absent.
  • permission: optional enum read|write|admin, defaults to read for attach operations.

Example Calls​

Create a group​

curl -X POST "$PRIMECAL_API/api/user-groups" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Family Planning",
"slug": "family-planning",
"description": "Calendars shared with the household",
"kind": "family"
}'

Invite a user by email​

curl -X POST "$PRIMECAL_API/api/user-groups/9/invites" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "teammate@example.com",
"message": "Join the shared planning calendars."
}'

The response is intentionally generic. Do not use it to infer whether teammate@example.com has a PrimeCal account.

Invite a user and add them to your own plan​

curl -X POST "$PRIMECAL_API/api/user-groups/9/invites" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "email": "wife@example.com", "addToMyAccount": true }'

Needs a plan with seats and one free - see GET /api/seats/capacity in the Invitations API.

Accept an invite​

curl -X POST "$PRIMECAL_API/api/user-groups/invites/$INVITE_TOKEN/accept" \
-H "Authorization: Bearer $TOKEN"

The response is the group (as GET /api/user-groups/:id returns it) plus seatActivated: true|false. When it is true the account has the inviter's plan features from the next request.

Attach calendars to a group​

curl -X POST "$PRIMECAL_API/api/user-groups/9/calendars" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"calendarIds": [5, 7],
"permission": "write"
}'

Response and Behavior Notes​

  • Group-shared calendars are represented by CalendarShare rows with principalType: "group".
  • Direct user sharing uses the same CalendarShare model with principalType: "user".
  • Effective calendar access includes owned calendars, direct user shares, and people-group shares.
  • If multiple paths grant access to the same calendar, the highest permission applies: admin > write > read.
  • Accepted group members see group-shared calendars through GET /api/calendars without calling this API directly.
  • Every invitation is emailed to its address (an account or not) with Accept and Decline links to the app's Invitations page; an existing account also gets an in-app notification whose data.deepLink is /app/invitations. An address with no account yet finds the invitation waiting once it registers with that address (invites are matched by targetUserId, else by the lower-cased email).

Best Practices​

  • Use people groups when several calendars should stay aligned under one member list and permission model.
  • Keep direct user shares for one-off access.
  • Build invite UIs around generic success states.
  • Refresh both /api/user-groups and /api/calendars after accepting an invite.
  • Prefer GET /api/notifications/catalog when building rule or preference editors for invite-related events.