An ICS file is a plain text file describing one or more calendar entries, written in a format called iCalendar. It is the reason an appointment booked in one system can be added to a calendar app that knows nothing about that system. Open one in a text editor and you can read every line.
You meet ICS files in two quite different roles. As an attachment, one arrives on a confirmation email and represents a single appointment at the moment the email was sent. As a subscribed feed, a URL serves an ICS file that a calendar app fetches repeatedly, so the calendar tracks a list that changes over time.
Getting the difference right, and getting two properties right inside the file, is most of what separates a booking confirmation that updates cleanly from one that leaves three stale copies of the same appointment on your client's phone.
What An ICS File Is
The format is defined in RFC 5545. A few mechanical facts that explain most oddities you will see:
- It is line-based text, with properties written as
NAME;PARAM=value:value. - Lines are limited in length and are folded by breaking them and starting the continuation with a single space. A description that appears mangled in a text editor is usually just folded.
- Lines end with a carriage return and line feed pair. Some strict parsers reject files that use bare newlines.
- Commas, semicolons and backslashes inside text values are escaped with a backslash, and a line break inside a description is written as the two characters backslash and n.
- The file is served as the media type
text/calendar.
Everything sits inside a VCALENDAR container, which holds components. VEVENT is an appointment. VTODO is a task, VJOURNAL a note, VFREEBUSY a block of availability, VTIMEZONE a definition of a timezone including its daylight saving rules, and VALARM a reminder attached to an event.
A Real Annotated File
This is a complete, valid invitation for a 45 minute consultation:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Ltd//Booking 1.0//EN
CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VTIMEZONE
TZID:Europe/London
BEGIN:STANDARD
DTSTART:19701025T020000
TZOFFSETFROM:+0100
TZOFFSETTO:+0000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19700329T010000
TZOFFSETFROM:+0000
TZOFFSETTO:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20260714T093000Z
DTSTART;TZID=Europe/London:20260721T140000
DTEND;TZID=Europe/London:20260721T144500
SEQUENCE:0
STATUS:CONFIRMED
TRANSP:OPAQUE
SUMMARY:Consultation with Dana Whitfield
DESCRIPTION:Booked online. Reschedule or cancel:\n
https://book.example.com/b/8f21c4a9
LOCATION:https://meet.example.com/dana-consult
ORGANIZER;CN=Dana Whitfield:mailto:[email protected]
ATTENDEE;CN=Sam Okoro;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
RSVP=TRUE:mailto:[email protected]
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Consultation in 15 minutes
TRIGGER:-PT15M
END:VALARM
END:VEVENT
END:VCALENDAR
Reading it in order:
PRODIDidentifies the software that produced the file. It is required and is never shown to a user, but it appears in support tickets.METHOD:REQUESTsays this is an invitation asking for a response. More on methods below.- The
VTIMEZONEblock spells out what Europe/London means, including when the clocks change. Including it is what lets a client interpret the event correctly rather than guessing. UIDis the permanent identity of this appointment. It must be globally unique and must never change for the life of the booking.DTSTAMPis when this version of the object was produced, not when the meeting is.DTSTARTandDTENDcarry a timezone identifier, so the time means 14:00 local in London whatever the recipient is doing. AZsuffix instead would mean UTC. For an all-day event you would instead writeDTSTART;VALUE=DATE:20260721with the end date set to the day after the last day covered.SEQUENCEis the revision number, starting at zero.STATUS:CONFIRMEDdistinguishes a firm booking from a tentative or cancelled one.TRANSP:OPAQUEmeans the event consumes your time, which is what makes it show as busy. The alternative, transparent, shows as free and is invisible to availability checks.ORGANIZERandATTENDEEcarry the participants, with the attendee status set to needs action and a request for a reply.VALARMwith a trigger of minus 15 minutes is a client-side reminder. It is not a substitute for a real reminder email, since the client may ignore it entirely. See appointment reminder timing.- Notice the folded lines: the description and attendee values continue on a following line beginning with a single space.
Why UID And SEQUENCE Decide Update Or Duplicate
This is the part worth internalising, because it is the difference between a professional confirmation flow and a mess.
When a calendar client receives an event, it looks for an existing entry with the same UID. If it finds none, it creates a new one. If it finds one, it compares revisions: a higher SEQUENCE, or an equal sequence with a later DTSTAMP, means the incoming copy supersedes what is stored, and the client replaces it in place.
So a reschedule of the appointment above is not a new event. It is the same file with the same UID, a new DTSTART and DTEND, and SEQUENCE:1. The entry moves in the client's calendar and no second copy appears.
A cancellation is the same identity again, with METHOD:CANCEL, STATUS:CANCELLED and SEQUENCE:2. Clients that honour it will remove or strike through the entry.
The classic bug is generating a fresh UID for every email that goes out. Confirmation, reminder and reschedule then arrive as three unrelated appointments, and the client ends up with a calendar showing an appointment at the old time and the new one. If you build this yourself, derive the UID from your own booking identifier so it is stable by construction, and never from a timestamp or a random value generated at send time.
Two related properties: RECURRENCE-ID identifies a single occurrence within a repeating series so you can move just that one, and it keeps the same UID as the series. RRULE describes the repetition itself, for example FREQ=WEEKLY;BYDAY=TU;COUNT=8 for eight Tuesdays. The operational side of repeating bookings is in recurring appointments setup.
METHOD, And Why An Invitation Is Not A Download
The METHOD property is what turns a calendar file into a message about a meeting. The values you will meet:
| Method | Meaning | Typical use |
|---|---|---|
PUBLISH | Here is an event, no reply expected | An Add To Calendar download or a feed |
REQUEST | You are invited, please respond | An email invitation with Accept and Decline |
REPLY | My response to your invitation | Sent back by the attendee client |
CANCEL | This event is cancelled | A cancellation notice |
This is why some calendar attachments show Accept and Decline buttons in the email client and some just offer to save the event. An email client will generally only render the invitation interface when the method is REQUEST and the recipient appears in the attendee list. If your confirmation email attaches a file with no attendees and no method, expect a plain download, which is often exactly what you want. The wider design of these emails is covered in appointment confirmation emails.
Attachments Versus Subscribed Feeds
The two delivery models solve different problems and are not interchangeable.
| ICS attachment | Subscribed feed | |
|---|---|---|
| What it is | A file sent once | A URL fetched repeatedly |
| Contains | Usually one event | A whole list of events |
| Updates | Only when you send another file | On the client refresh cycle |
| Recipient action | Open the file once | Subscribe once, then nothing |
| Good for | Confirming one appointment to a client | Showing a staff member their whole schedule |
A feed is usually offered as a link starting webcal://, which is the same URL over HTTPS with a scheme that persuades the operating system to open a calendar app rather than a browser. Because most clients cannot log in to a feed, the URL itself is the credential: it should contain a long unguessable token, and there should be a way to rotate it if someone forwards it to the wrong person.
Why A Subscribed Feed Can Be Slow To Refresh
Subscribing is not syncing. The client decides when to fetch, and you have no control over it.
Apple Calendar exposes a refresh interval per subscription, with options ranging from every few minutes to daily, so a user who cares can set it themselves. Google Calendar does not offer an interval at all and refreshes external calendar URLs on its own schedule, and Google states that changes can take a number of hours to appear. Outlook on the web similarly refreshes when it chooses.
Cache headers and an entity tag help a well behaved client avoid refetching unchanged data, but nothing makes a slow client fetch more often. So the rule is simple: never use a subscribed feed for anything time-critical. A booking two hours from now must reach the person by email or a notification. The feed is for the shape of the week.
Serving An ICS File Correctly
If you are generating these yourself, the details that cause support tickets:
- Send
Content-Type: text/calendar; charset=utf-8. For an invitation, add the method as a parameter on that header as well as inside the file. - For a download, add a content disposition header with a sensible filename such as
appointment.ics. - Use carriage return and line feed line endings, and fold long lines properly.
- Escape commas and semicolons in summaries and descriptions. An unescaped comma in a location is a surprisingly common cause of a rejected file.
- Include a
VTIMEZONEblock for any timezone you reference, or use UTC times throughout. - Keep
DTSTAMPcurrent on each send and incrementSEQUENCEonly when something meaningful changed.
Common Reasons An ICS File Does Not Import
- The end is before the start, usually an all-day event whose end date was set to the same day rather than the next one.
- A missing UID or DTSTAMP. Both are required, and strict clients refuse the file rather than guessing.
- An unescaped character in a text value, which truncates the property.
- A timezone identifier with no matching VTIMEZONE block, which leaves the client to guess and often lands the event an hour out.
- A duplicated appointment after a change, which is the UID problem described above rather than a parsing error.
How This Works In appntmnts
appntmnts attaches a calendar file to booking confirmations with a stable UID derived from the booking, so rescheduling moves the client's calendar entry instead of adding a second one, and cancelling removes it. Staff schedules are also available as a subscribed feed.
For anything that needs to be current rather than eventually current, connect the calendar properly: two-way sync writes and updates events through the provider rather than waiting for a client to refresh a file. The options are on the features page, and if a synced event misbehaves, why calendar sync fails covers the usual causes.