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.
Authentication and Permissions
- All routes on this page are intended to be authenticated.
- Event comments use
JwtAuthGuardat the controller level. - Every event CRUD route explicitly uses
JwtAuthGuard. - Historical note:
GET /api/events/calendar/:calendarIdwas previously missing its guard, and was also declared afterGET /api/events/:id, so Nest matched the literal segmentcalendaras an:idandParseIntPiperejected 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
| Method | Path | Purpose | Request or query | Auth |
|---|---|---|---|---|
POST | /api/events | Create one event. | Body: event fields | JWT or user API key |
POST | /api/events/recurring | Create a recurring event series. | Body: recurring event fields | JWT or user API key |
GET | /api/events | List accessible events overlapping an optional date range. | Query: startDate,endDate,calendarIds | JWT or user API key |
GET | /api/events/conflicts | List events overlapping a time window. | Query: start,end,calendarIds,excludeEventId | JWT or user API key |
GET | /api/events/:id | Get one event. | Path: id | JWT or user API key |
PATCH | /api/events/:id | Update one event or one recurring occurrence. | Path: id, body: partial event fields plus updateMode | JWT or user API key |
DELETE | /api/events/:id | Delete one event. | Path: id | JWT or user API key |
PATCH | /api/events/:id/move | Reschedule an event, preserving its duration. | Path: id, body: move fields | JWT or user API key |
PATCH | /api/events/:id/recurring | Update a recurring series with explicit scope. | Path: id, body: recurring update fields plus updateScope | JWT or user API key |
GET | /api/events/calendar/:calendarId | List events for one calendar, within a bounded range. | Path: calendarId, query: startDate,endDate | JWT or user API key |
Event Comments
| Method | Path | Purpose | Request or query | Auth |
|---|---|---|---|---|
GET | /api/events/:eventId/comments | List comments for an event. | Path: eventId | JWT or user API key |
POST | /api/events/:eventId/comments | Create a comment. | Path: eventId, body: content,templateKey,parentCommentId,isFlagged | JWT or user API key |
POST | /api/events/:eventId/comments/track-open | Track that a user opened an event. | Path: eventId, body: note | JWT or user API key |
PATCH | /api/events/:eventId/comments/:commentId | Update a comment. | Path: eventId,commentId, body: content | JWT or user API key |
PATCH | /api/events/:eventId/comments/:commentId/flag | Flag or unflag a comment. | Path: eventId,commentId, body: isFlagged | JWT or user API key |
POST | /api/events/:eventId/comments/:commentId/replies | Reply to a comment. | Path: eventId,commentId, body: comment create fields | JWT or user API key |
Request Shapes
Create and update event
CreateEventDto and UpdateEventDto
title: required on create, stringdescription: optional stringstartDate: required on create, ISO datestartTime: optional stringendDate: optional ISO dateendTime: optional stringisAllDay: optional booleanlocation: optional stringstatus: optional enumconfirmed|tentative|cancelledrecurrenceType: optional enumnone|daily|weekly|monthly|yearlyrecurrenceRule: optional JSON payloadcolor: optional stringicon: optional stringnotes: optional stringtags: optional string array, max 64 chars eachlabels: optional alias fortagsreminders: optional array of{ minutesBefore, channel }(see below)calendarId: optional numberupdateMode: update-only enumsingle|all|future
Validation limits:
titlelength: 300locationlength: 200iconlength: 10colorlength: 7
Reminders
EventReminderDto (each entry in reminders[]):
minutesBefore: required number,0-1440(24 hours)channel: required enuminapp|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 createrecurrence.type: required enumnone|daily|weekly|monthly|yearlyrecurrence.interval: optional number, default1recurrence.daysOfWeek: optional enum arraySU|MO|TU|WE|TH|FR|SArecurrence.dayOfMonth: optional numberrecurrence.monthOfYear: optional numberrecurrence.endType: optionalnever|count|daterecurrence.count: optional numberrecurrence.endDate: optional ISO daterecurrence.timezone: optional stringupdateScope: update-only enumthis|future|all
List query
ListEventsQueryDto.startDate: optional ISO dateListEventsQueryDto.endDate: optional ISO dateListEventsQueryDto.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: requiredYYYY-MM-DDdestination datestartTime: optionalHH:mm. Ignored for all-day events; omit to keep the current timecalendarId: optional, moves the event to another calendar (write access required on both)keepDuration: optional, defaulttrue. Whenfalse, only the start movesrejectOnConflict: optional, defaultfalse. Whentrue, a conflicting destination returns409 EVENT_SLOT_CONFLICTscope: optionalthis|future|allfor 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 queryexcludeEventId: 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 stringtemplateKey: optional enumCommentTemplateKeyparentCommentId: optional numberisFlagged: optional boolean
Other comment DTOs:
UpdateEventCommentDto.content: required stringFlagCommentDto.isFlagged: required booleanTrackEventOpenDto.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
calendarsummary and acreatedBysummary. tagsandlabelsare parallel inputs; clients should pick one convention and stay consistent.- Recurring-series updates have two distinct models:
PATCH /api/events/:idusesupdateModewithsingle|all|futurePATCH /api/events/:id/recurringusesupdateScopewiththis|future|all
- Comment responses include nested replies, reporter metadata, visibility, and flag state.
Performance
(calendarId, startDate, endDate)is indexed asIDX_events_calendar_date_range. The pre-existing indexes coveredstartsAtUtc, 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 passcalendarIdsso the filtering happens in SQL rather than in the browser. - Use
PATCH /api/events/:id/movefor 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.