Calendar Feeds
Calendar Feeds API
Secure iCalendar (.ics) feeds that publish scheduled activities for subscription in Google Calendar, Apple Calendar, Outlook, and any other RFC 5545βcompatible client.
Endpoints
| Method | URL | Description |
|---|---|---|
| GET | /calendar/feed.ics |
Global feed β every scheduled activity in the system |
| GET | /calendar/feed/{user}.ics |
Per-user feed β only activities owned by {user} |
Both endpoints return Content-Type: text/calendar; charset=utf-8 with a Content-Disposition: inline; filename="signals-calendar.ics" header.
Authentication
These feeds are not authenticated with a Sanctum bearer token. Calendar clients cannot send custom headers when polling a subscription, so the feeds are protected with Laravel signed URLs instead. Each URL carries a signature query parameter that is verified by the signed middleware on every request.
- A valid signature returns
200with the iCalendar body. - A missing or tampered signature returns
403 Forbidden. - The request is served without a logged-in session β the signature is the only credential.
Signed URLs have no expiry, so a subscribed client keeps polling indefinitely.
Feed Window
Both feeds include activities whose start time falls within the window:
starts_at >= now() - 1 year (no upper bound)
That is, everything from one year ago up to any point in the future. Activities with no start time are excluded β they are not schedulable and never appear in a feed. The per-user feed additionally filters to activities owned by the requested user.
Feed Content
The body is a single VCALENDAR (VERSION:2.0, CALSCALE:GREGORIAN) containing one VEVENT per scheduled activity.
| Field | Source |
|---|---|
UID |
activity-{id}@{host} β stable per activity |
DTSTAMP |
Generation time (UTC) |
DTSTART / DTEND |
Activity start/end in UTC (Ymd\THis\Z). All-day activities use ;VALUE=DATE with a date-only value. For all-day events, DTEND is the RFC 5545 exclusive end date: a single-day all-day event emits DTEND as start + 1 day; a multi-day all-day event emits DTEND as the actual exclusive end date, preserving the full span. |
SUMMARY |
Activity subject |
LOCATION |
Activity location |
DESCRIPTION |
Activity description |
STATUS |
CONFIRMED for Scheduled, Held, and Completed activities (Completed also carries X-COMPLETED:TRUE); CANCELLED for cancelled activities |
TRANSP |
TRANSPARENT when the activity's time status is Free; OPAQUE when Busy |
ORGANIZER |
Owner's email as a mailto: CAL-ADDRESS; display name in the double-quoted CN parameter (e.g. ORGANIZER;CN="Jane Operator":mailto:jane@example.com). Omitted when the owner has no email address. |
Text values are escaped per RFC 5545 (commas, semicolons, backslashes, newlines), and long lines are folded at 75 octets.
Example
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Signals//Calendar//EN
CALSCALE:GREGORIAN
X-WR-CALNAME:Signals Calendar
BEGIN:VEVENT
UID:activity-42@signals.test
DTSTAMP:20260613T090000Z
DTSTART:20260615T090000Z
DTEND:20260615T093000Z
SUMMARY:Follow up on rental quote
LOCATION:Phone
DESCRIPTION:Call to discuss pricing
STATUS:CONFIRMED
TRANSP:OPAQUE
ORGANIZER;CN="Jane Operator":mailto:jane@example.com
END:VEVENT
END:VCALENDAR
Minting & Subscribing
Feed URLs are generated as signed routes:
- Global feed β
URL::signedRoute('calendar.feed.global') - Per-user feed β
URL::signedRoute('calendar.feed.user', ['user' => $userId])
In the application, copyable subscribe URLs are surfaced in two places:
- The feed modal on the Calendar page β every user sees their own per-user feed URL; administrators and owners additionally see the global feed URL and the full per-user list for all staff.
- Settings β Calendar (
/settings/calendar) β the current user's subscribe URL with a copy button and a Download .ics button; administrators also see the global feed URL.
To subscribe, copy a feed URL and paste it into your calendar client's "subscribe by URL" / "add calendar from URL" option. The client polls the URL on its own schedule and reflects new and changed activities automatically.
Security Model & Tradeoff
The feeds rely entirely on the unguessable signature embedded in the URL. This keeps subscription frictionless (no token management, no expiry) but carries a deliberate tradeoff:
- Not individually revocable β because the signature is derived from the route and the application key rather than a stored per-feed secret, a single leaked feed URL cannot be revoked on its own.
- Global invalidation only β rotating the application's
APP_KEYchanges the signing secret and immediately invalidates every existing feed URL (all users, global and per-user). Subscribers must then be issued fresh URLs.
Treat feed URLs as sensitive: anyone with a feed URL can read the activities it exposes without logging in.