I recently wanted to sync events between Guilded and my personal calendar. The simplest solution is to generate an iCalendar file that is served from a personal endpoint for each user. When they subscribe, their calendar app basically acts like an RSS reader and periodically checks the endpoint for the latest list of events.
There are plenty of libraries in your chosen language to generate the
.ics file itself, and the spec is simple enough that you could even generate it by hand. I won’t dive into generating the ICS file itself, but here’s a sample with a single event:
BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:Guilded METHOD:PUBLISH X-WR-CALNAME:Guilded X-PUBLISHED-TTL:PT1H BEGIN:VEVENT UID:email@example.com SUMMARY:My Fun Event DTSTAMP:20200903T063216Z DTSTART:20200826T070000Z DTEND:20200826T080000Z DESCRIPTION:yoooo\n\nhttps://neil.gg URL:https://guilded.gg/teams/... LOCATION:Monkey Island STATUS:CONFIRMED CREATED:20200822T113900Z END:VEVENT END:VCALENDAR
In a nutshell, you can add as many
VEVENT blocks as you need to show more events. The only catch is that this file needs to show all events that you want in the user’s calendar. Unlike an RSS reader, most calendar apps won’t keep a cache of old events around. That means over time, this file can grow very large. I recommend limiting it to a sensible range (i.e., events from 1 month ago to 1 year from now), depending on your application.
There are also a lot of little things you need to know to actually make this calendar work with the variety of calendar services that I couldn’t find documented anywhere. After a lot of investigating (and trial and error), here’s some tips and tricks to help you build your ICS endpoint faster.
Make sure you’re serving your calendar file with the right headers. In particular, they should look like
Content-Type: text/calendar; charset=utf-8 Content-Disposition: attachment; filename="calendar.ics"
This is pretty straightforward, but easy to miss.
Your server needs to respond to
Accept: text/calendar request headers from the client. I discovered the hard way that Node/restify does not by default. It seems iOS is the only one that sends the header, and when the request gets rejected, it shows an unhelpful “Unable to verify account information” error. Since a rejected
Accept header returns a
406 Not Acceptable response and I don’t normally log 4xx errors, I wasn’t catching the problem on my end and went down a rabbit hole of trying to debug why iOS hated my server.
This one was frustrating to discover, but it seems Google Calendar has a max url length of 256 characters. This isn’t documented anywhere and trying to use a longer URL doesn’t return any error, the UI just silently fails. In my case, the user token was going over that limit, so I had to change my hashing algorithm to create a shorter token.
As far as I can tell, query params do work with most calendar apps, but I’d still recommend avoiding them if possible. For example, I changed our calendar URL from
https://guilded.gg/users/blabla/calendar.ics to ensure that we don’t hit issues with some obscure calendar app. It also helps cut down the url length as a bonus.
It turns out the Apple Calendar on macOS is the most forgiving and works exactly like your browser. It’ll happily sync any valid ICS file without any of the setup above. That means you definitely want to test your endpoint with iOS, Google Calendar, and Outlook directly. I recommend using something like ngrok to quickly test your local endpoint. As a bonus, it includes a detailed dashboard at
http://localhost:4040 for viewing request headers and replaying requests to make debugging much easier. This is how I resolved the 406 error above and wish I’d known about the dashboard sooner!