Skip to main content
Was this helpful?

Event API

Events and Event Comments

Create events, manage recurring series, and collaborate through comments

This page documents the event CRUD surface, recurring-event handling, calendar-scoped event reads, and the comment thread endpoints attached to events.

JWT or user API keyRecurring updatesCalendar range queriesComment threads

Authentication and Permissions​

  • All routes on this page are intended to be authenticated.
  • Event comments use JwtAuthGuard at the controller level.
  • Every event CRUD route explicitly uses JwtAuthGuard.
  • Historical note: GET /api/events/calendar/:calendarId was previously missing its guard, and was also declared after GET /api/events/:id, so Nest matched the literal segment calendar as an :id and ParseIntPipe rejected the request - the route was unreachable, which is why the missing guard went unnoticed. Both are fixed as of 1.4.62: the route is guarded, declared before :id, and bounded by a date range plus a hard row cap.
  • Access to events and comments is enforced by event and calendar ownership or share permissions in the service layer.

Endpoint Reference​

Events​

MethodPathPurposeRequest or queryAuth
POST/api/eventsCreate one event.Body: event fieldsJWT or user API key
POST/api/events/recurringCreate a recurring event series.Body: recurring event fieldsJWT or user API key
GET/api/eventsList accessible events overlapping an optional date range.Query: startDate,endDate,calendarIdsJWT or user API key
GET/api/events/conflictsList events overlapping a time window.Query: start,end,calendarIds,excludeEventIdJWT or user API key
GET/api/events/:idGet one event.Path: idJWT or user API key
PATCH/api/events/:idUpdate one event or one recurring occurrence.Path: id, body: partial event fields plus updateModeJWT or user API key
DELETE/api/events/:idDelete one event.Path: idJWT or user API key
PATCH/api/events/:id/moveReschedule an event, preserving its duration.Path: id, body: move fieldsJWT or user API key
PATCH/api/events/:id/recurringUpdate a recurring series with explicit scope.Path: id, body: recurring update fields plus updateScopeJWT or user API key
GET/api/events/calendar/:calendarIdList events for one calendar, within a bounded range.Path: calendarId, query: startDate,endDateJWT or user API key

Event Comments​

MethodPathPurposeRequest or queryAuth
GET/api/events/:eventId/commentsList comments for an event.Path: eventIdJWT or user API key
POST/api/events/:eventId/commentsCreate a comment.Path: eventId, body: content,templateKey,parentCommentId,isFlaggedJWT or user API key
POST/api/events/:eventId/comments/track-openTrack that a user opened an event.Path: eventId, body: noteJWT or user API key
PATCH/api/events/:eventId/comments/:commentIdUpdate a comment.Path: eventId,commentId, body: contentJWT or user API key
PATCH/api/events/:eventId/comments/:commentId/flagFlag or unflag a comment.Path: eventId,commentId, body: isFlaggedJWT or user API key
POST/api/events/:eventId/comments/:commentId/repliesReply to a comment.Path: eventId,commentId, body: comment create fieldsJWT or user API key

Request Shapes​

Create and update event​

CreateEventDto and UpdateEventDto

  • title: required on create, string
  • description: optional string
  • startDate: required on create, ISO date
  • startTime: optional string
  • endDate: optional ISO date
  • endTime: optional string
  • isAllDay: optional boolean
  • location: optional string
  • status: optional enum confirmed|tentative|cancelled
  • recurrenceType: optional enum none|daily|weekly|monthly|yearly
  • recurrenceRule: optional JSON payload
  • color: optional string
  • icon: optional string
  • notes: optional string
  • tags: optional string array, max 64 chars each
  • labels: optional alias for tags
  • reminders: optional array of { minutesBefore, channel } (see below)
  • calendarId: optional number
  • updateMode: update-only enum single|all|future

Validation limits:

  • title length: 300
  • location length: 200
  • icon length: 10
  • color length: 7

Reminders​

EventReminderDto (each entry in reminders[]):

  • minutesBefore: required number, 0-1440 (24 hours)
  • channel: required enum inapp|email|webpush|mobilepush

Reminders are stored on the event itself (Event.reminders, a JSON column) rather than as a global user preference, so different events can have different lead times. EventReminderSchedulerService checks every minute for events whose startsAtUtc - minutesBefore falls due, and publishes an event.reminder notification (see Notifications API) for each one — no separate "create reminder" endpoint exists; reminders are just part of the event payload. Sending an empty reminders: [] array on update clears all reminders for that event.

Recurring series​

CreateRecurringEventDto and UpdateRecurringEventDto

  • calendarId: required on create
  • recurrence.type: required enum none|daily|weekly|monthly|yearly
  • recurrence.interval: optional number, default 1
  • recurrence.daysOfWeek: optional enum array SU|MO|TU|WE|TH|FR|SA
  • recurrence.dayOfMonth: optional number
  • recurrence.monthOfYear: optional number
  • recurrence.endType: optional never|count|date
  • recurrence.count: optional number
  • recurrence.endDate: optional ISO date
  • recurrence.timezone: optional string
  • updateScope: update-only enum this|future|all

List query​

  • ListEventsQueryDto.startDate: optional ISO date
  • ListEventsQueryDto.endDate: optional ISO date
  • ListEventsQueryDto.calendarIds: optional, repeated (?calendarIds=1&calendarIds=2) or comma-separated (?calendarIds=1,2), maximum 100

Overlap, not containment. The range matches events that overlap the window (startDate <= end AND COALESCE(endDate, startDate) >= start), not only those whose start falls inside it. A three-day event beginning the day before the window opens is returned, because it is visible on the grid.

Always bounded. Any missing bound falls back to the default window rather than being omitted: EVENTS_LIST_DEFAULT_LOOKBACK_DAYS (default 180) to EVENTS_LIST_DEFAULT_LOOKAHEAD_DAYS (default 365) days from now. Supplying only one bound previously skipped date filtering entirely, which meant a single query parameter could trigger an unbounded scan.

Maximum span. Windows wider than 400 days (EVENTS_LIST_MAX_RANGE_DAYS) are rejected with 400 VALIDATION_FAILED and a details.maxRangeDays field.

calendarIds is a filter, never a grant. It narrows the result within the calendars the caller can already access. Ids the caller cannot see are silently dropped rather than rejected, so the parameter cannot be used to probe whether a given calendar exists.

Move​

MoveEventDto - PATCH /api/events/:id/move

  • startDate: required YYYY-MM-DD destination date
  • startTime: optional HH:mm. Ignored for all-day events; omit to keep the current time
  • calendarId: optional, moves the event to another calendar (write access required on both)
  • keepDuration: optional, default true. When false, only the start moves
  • rejectOnConflict: optional, default false. When true, a conflicting destination returns 409 EVENT_SLOT_CONFLICT
  • scope: optional this|future|all for recurring events

Returns { event, conflicts }. Use this rather than PATCH /api/events/:id for rescheduling: it recomputes the end from the original duration, preserves an all-day event's day-span, and reports what the destination overlaps. A keepDuration: false move that would end the event before it starts is rejected with 400 EVENT_MOVE_INVALID_RANGE.

Conflicts​

EventConflictsQueryDto - GET /api/events/conflicts

  • start, end: required ISO-8601 instants (not dates - a conflict check is time-of-day sensitive)
  • calendarIds: optional filter, same semantics as the list query
  • excludeEventId: optional, typically the event being dragged so it does not conflict with itself

Overlap is half-open: an event ending exactly when the window opens is adjacent, not conflicting. The frontend's local drag preview and the primecal_calendars_events_conflicts MCP tool apply the identical rule, so all three agree.

Comments​

CreateEventCommentDto

  • content: optional string
  • templateKey: optional enum CommentTemplateKey
  • parentCommentId: optional number
  • isFlagged: optional boolean

Other comment DTOs:

  • UpdateEventCommentDto.content: required string
  • FlagCommentDto.isFlagged: required boolean
  • TrackEventOpenDto.note: optional string

Example Calls​

Create a calendar event​

curl -X POST "$PRIMECAL_API/api/events" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "School pickup",
"startDate": "2026-03-30",
"startTime": "15:30",
"endDate": "2026-03-30",
"endTime": "16:00",
"calendarId": 5,
"tags": ["family", "kids"]
}'

Create a recurring event series​

curl -X POST "$PRIMECAL_API/api/events/recurring" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Soccer practice",
"startDate": "2026-04-01",
"startTime": "17:00",
"endDate": "2026-04-01",
"endTime": "18:30",
"calendarId": 5,
"recurrence": {
"type": "weekly",
"interval": 1,
"daysOfWeek": ["WE"],
"endType": "date",
"endDate": "2026-06-30"
}
}'

Update a single occurrence in a recurring series​

curl -X PATCH "$PRIMECAL_API/api/events/42" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startTime": "17:30",
"endTime": "19:00",
"updateMode": "single"
}'

Add a comment​

curl -X POST "$PRIMECAL_API/api/events/42/comments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Running 10 minutes late."
}'

Response and Behavior Notes​

  • Event responses include a calendar summary and a createdBy summary.
  • tags and labels are parallel inputs; clients should pick one convention and stay consistent.
  • Recurring-series updates have two distinct models:
    • PATCH /api/events/:id uses updateMode with single|all|future
    • PATCH /api/events/:id/recurring uses updateScope with this|future|all
  • Comment responses include nested replies, reporter metadata, visibility, and flag state.

Performance​

  • (calendarId, startDate, endDate) is indexed as IDX_events_calendar_date_range. The pre-existing indexes covered startsAtUtc, which the calendar range query does not filter on, so that query was previously unindexed.
  • Request only the window you render. The calendar page fetches roughly the visible period plus a week of padding, caches each window separately, and prefetches the adjacent periods.

Best Practices​

  • Send date and time fields separately; the backend models them as separate properties.
  • Prefer GET /api/events?startDate=...&endDate=... for calendar views and exports, and pass calendarIds so the filtering happens in SQL rather than in the browser.
  • Use PATCH /api/events/:id/move for rescheduling rather than a field-level update.
  • Keep recurring edits explicit. Do not assume the client default matches the user's intent.
  • Normalize event labels on the client if you also expose reusable labels through the user settings flow.
  • Use comments for collaboration metadata and visible discussion, not as a hidden machine-state channel.