Skip to content
Koltrix

Calendar invites by email: ICS files, time zones and client quirks

Sending a meeting invite from your app sounds simple until Outlook shows an attachment and Gmail shows the wrong hour. How to build ICS invites that behave.

Koltrix Team6 min read
White wall covered with many clocks
Photo by Donald Wu on Unsplash
On this page(9 sections)
  1. What an invite is made of
  2. The anatomy of an ICS file
  3. Time zones: the source of most trouble
  4. Updates and cancellations
  5. How the big clients behave
  6. Practical advice for building it
  7. What to test
  8. For recurring events
  9. Key takeaways

Your app books a demo, a class or an appointment, and you want the customer to get a calendar entry, not just a line of text with a time in it. The standard way is to send an invitation as part of an email, using a small text format called iCalendar, usually carried in a file ending in .ics.

It works well when it is built carefully. When it is not, one person sees a proper invitation with Accept and Decline buttons, another sees an unexplained attachment, and a third sees the meeting an hour off. This guide explains how the pieces fit, the mistakes that cause most of the trouble, and how to test the result.

What an invite is made of

An invitation is a normal email with an extra MIME part that holds the calendar data. If MIME is new to you, MIME for developers explains how parts nest. For an invite you typically want:

  • A plain-text and HTML body that tells the reader the details in words: what, when, where, and how to join.
  • A calendar part with the content type text/calendar, with a method parameter such as REQUEST.
  • The same calendar data also as a named attachment, invite.ics, because some clients only look for a file.

Do not rely on the calendar data alone. Some clients, mailing tools and phones show only the message body, and the words need to stand on their own.

The anatomy of an ICS file

An ICS file is plain text with one property per line:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Co//Booking//EN
CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20261014T093000Z
SEQUENCE:0
DTSTART:20261021T140000Z
DTEND:20261021T143000Z
SUMMARY:Product demo with Maya
LOCATION:https://example.com/join/84213
ORGANIZER;CN=Example Co:mailto:[email protected]
ATTENDEE;CN=Sam Lee;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:[email protected]
DESCRIPTION:Join here: https://example.com/join/84213
END:VEVENT
END:VCALENDAR

The properties that matter most:

  • METHOD. REQUEST sends or updates an invitation, CANCEL withdraws one, and REPLY is what an attendee's client sends back. The method on the MIME part must match the METHOD inside the file.
  • UID. A permanent, unique identifier for the event. Every update and cancellation must reuse the same UID, or clients treat each message as a new meeting.
  • SEQUENCE. A number that starts at 0 and increases with every change. A client ignores an update whose sequence is not higher than the one it already has.
  • DTSTAMP. When this version of the invite was created, in UTC.
  • ORGANIZER and ATTENDEE. Who runs the meeting and who is invited. RSVP=TRUE asks the attendee to respond.
  • Line endings. The format requires CRLF line endings, and lines longer than 75 octets are folded, with the continuation starting with a single space. Build files with a library that does this for you.

Use a maintained iCalendar library for your language instead of string concatenation. Escaping commas, semicolons and newlines in text fields is easy to get wrong by hand.

Time zones: the source of most trouble

A calendar event is a moment in time and, often, a place on a clock. Getting this wrong is why people see the "right" time an hour off.

Option 1: UTC. Write the start and end with a trailing Z, as in the example above. Every client converts to the viewer's own time zone. For one-off events this is the simplest and most reliable choice.

Option 2: A named zone. Write DTSTART;TZID=Europe/Berlin:20261021T160000, and include a VTIMEZONE block that defines the zone's rules. Without that block, some clients cannot interpret the zone name and guess. Use this form for recurring events that should stay at the same local hour across daylight saving changes, such as a weekly 9:00 class.

Never use floating time. A time with no Z and no TZID means "whatever the viewer's local time is". The same invitation then lands at different real moments for people in different places, which is almost never what you want.

Store the event's time in UTC along with the intended zone in your own database, and generate the file from that. Scheduling emails across time zones in your app covers the underlying habits, and it applies to the words in the body too: show the time with its zone, such as "Wednesday 21 October, 14:00 UTC (16:00 in Berlin)".

Updates and cancellations

People move meetings. Handle that on purpose.

  • Reschedule: send a new REQUEST with the same UID, a higher SEQUENCE and the new times.
  • Cancel: send METHOD:CANCEL with the same UID, a higher SEQUENCE and STATUS:CANCELLED.
  • Keep the thread. Use the same subject root, or reply in the thread, so the change is visible next to the original.
  • Do not send a fresh UID for a change, or the attendee will have two entries.

Also decide who is allowed to change an event, because the ORGANIZER address is part of what a client trusts.

How the big clients behave

Clients differ, and your testing should include at least these. The details below are general behavior and can change, so confirm with your own test messages.

  • Gmail. It recognizes an invitation and shows it with an RSVP area and an "Add to calendar" option, when the text/calendar part is correctly formed. It is stricter when the message looks suspicious: authentication matters, so make sure SPF, DKIM and DMARC align for your sending domain. See DMARC alignment.
  • Outlook and Microsoft 365. Outlook looks for method information and the attachment. A mismatch between the MIME method and the file's METHOD is a common reason it shows only a plain attachment.
  • Apple Mail and iOS. It handles .ics attachments well and offers to add the event. It is more forgiving of format details than some desktop clients.
  • Webmail and other clients. Some show only the attachment. That is why the body must contain the time, place and link in words.

If a client offers Accept and Decline buttons, the response goes back to the ORGANIZER address as an email with METHOD:REPLY. Decide where that mailbox is read, and whether your system parses replies. A shared address that nobody monitors is a quiet failure. Meeting and scheduling requests in a shared inbox covers the people side of it.

Practical advice for building it

  1. Generate, do not template. Use a library to build the calendar object. Include PRODID, UID, DTSTAMP and SEQUENCE every time.
  2. Send both forms. Include the calendar as an alternative part with the method set, and as an invite.ics attachment. Transactional attachments covers sending files through an API.
  3. Keep the organizer address real. It should be an address that can receive replies and that matches your sending identity.
  4. Put the join link in three places: the LOCATION field, the DESCRIPTION and the email body.
  5. Escape and fold correctly. Let the library do it. Test with names that contain commas, accents and quotes.
  6. Keep the event small. Avoid long descriptions and attachments inside the event. A short description and a link are enough.
  7. Make updates idempotent. If your system retries a send, it must not create a second event. Use the UID and sequence from your own records.

What to test

Send a test invitation to at least one account in each major client and check:

  • The event appears with the right start and end in the viewer's own zone
  • Accept, Decline and Maybe buttons or an equivalent are visible where the client supports them
  • A reschedule updates the existing entry and does not create a second one
  • A cancellation removes or marks the entry
  • The body text shows the time with its zone, the place and the link
  • Names with accents and commas render correctly
  • The message passes authentication and does not land in spam
  • A daylight saving change does not move a recurring event, if you use them
  • Replies arrive at a mailbox someone reads

Save the test messages as fixtures. Email tests in CI explains how to keep this kind of check running.

For recurring events

Recurrence adds rules (RRULE), exceptions and per-instance changes. If you only need a handful of dates, sending separate events is simpler and less error-prone. If you need real recurrence, use a library with full support, define the time zone with TZID and VTIMEZONE, and test across a daylight saving change. School and class schedules, as described in email for edtech and school calendars, are a typical case.

Key takeaways

  • An invite is an email with a text/calendar part and an .ics attachment, and the body must also state the details in words.
  • Keep UID fixed and raise SEQUENCE for every update or cancellation, with the MIME method matching the file's METHOD.
  • Use UTC for one-off events, TZID with a VTIMEZONE for recurring local-time events, and never floating time.
  • Build the file with a library, authenticate your sending domain and test in Gmail, Outlook and Apple Mail.
  • Make sure RSVP replies reach a mailbox that someone reads.

Start with Koltrix

Your domain, one inbox, and an API that sends.

A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.

SharePost on XLinkedIn