Event Standard

Calendar events: appointments, all-day entries, recurring series and the calendars that hold them. Designed to round-trip with iCalendar (RFC 5545), so events can be imported from and exported to Android, desktop and web calendars. Dates follow the date rules, people are contacts, places are locations.

Appointment

A timed event. $start/$end are local wall-clock times in $timezone.

{
	$type: 'event',
	$version: 1,
	$kind: 'appointment',
	$id: '7f3c2a1e-5b8d-4c1a-9e2f-0a6b3d4c5e6f',
	$title: 'Dentist',
	$description: 'Bring the insurance card',
	$start: '2026-10-02T18:00:00',
	$end: '2026-10-02T19:00:00',
	$timezone: 'Europe/Vienna',
	$location: {
		$type: 'location',
		$kind: 'point',
		$name: 'Dr. Huber',
		$lat: 48.20012,
		$lng: 16.36921,
		$address: { $street: 'Mariahilfer Straße 12', $city: 'Vienna', $postal: '1070', $country: 'AT' }
	},
	$status: 'confirmed',
	$visibility: 'private',
	$reminders: [
		{ $before: 'PT1H' },
		{ $days_before: 1, $time: '20:00' }
	],
	$created: '2026-09-25T09:12:00Z',
	$updated: '2026-09-25T09:12:00Z'
}

Recurring Appointment with Attendees

{
	$type: 'event',
	$version: 1,
	$kind: 'appointment',
	$id: 'standup-3b9d',
	$title: 'Team Standup',
	$start: '2026-10-05T09:30:00',
	$duration: 'PT15M',
	$timezone: 'Europe/Vienna',
	$location: 'Room 4',
	$url: 'https://meet.example.com/standup',
	$organizer: { $type: 'contact', $name: 'John', $email: '[email protected]' },
	$attendees: [
		{ $type: 'contact', $name: 'Jane', $email: '[email protected]', $role: 'required', $response: 'accepted' },
		{ $type: 'contact', $name: 'Tom', $email: '[email protected]', $role: 'optional', $response: 'pending' }
	],
	$recurrence: {
		$rule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20261231T225959Z',
		$exclude: ['2026-10-26T09:30:00']
	},
	$overrides: [
		{ $recurrence_id: '2026-10-14T09:30:00', $start: '2026-10-14T11:00:00', $location: 'Room 1' },
		{ $recurrence_id: '2026-11-02T09:30:00', $status: 'cancelled' }
	],
	$busy: true
}

All-Day Event

Dates without time. A yearly birthday with the default reminders replaced by custom ones:

{
	$type: 'event',
	$version: 1,
	$kind: 'allday',
	$id: 'bday-jane',
	$title: 'Jane\'s Birthday',
	$start: '2026-11-03',
	$recurrence: 'FREQ=YEARLY',
	$reminders: [
		{ $days_before: 1, $time: '20:00' },
		{ $days_before: 0, $time: '08:00' }
	],
	$busy: false
}

// multi-day: $end is exclusive -- this is 20 to 24 December
{ $type: 'event', $version: 1, $kind: 'allday', $title: 'Vacation', $start: '2026-12-20', $end: '2026-12-25' }

Calendar

A named collection of events. Carries the defaults its events inherit.

{
	$type: 'event',
	$version: 1,
	$kind: 'calendar',
	$id: 'cal-private',
	$name: 'Private',
	$color: '#3b82f6',
	$timezone: 'Europe/Vienna',
	$visibility: 'private',
	$owner: { $type: 'contact', $name: 'John', $platforms: [{ $url: 'https://ident.worldapi.org', $identifier: 'john' }] },
	$reminders: [
		{ $before: 'PT1H' },
		{ $days_before: 1, $time: '20:00' }
	],
	$events: [ ... ]
}

Object Kinds

$kind Use case
appointment Timed event: $start/$end are datetimes
allday Whole days: $start/$end are dates (YYYY-MM-DD)
calendar A collection of events with shared defaults

Standard Properties

Property Type Required Description
$type string yes Always 'event'
$version number yes Standard version
$kind string yes appointment, allday, or calendar
$id string yes Globally unique, stable ID (becomes iCal UID)
$title string yes* Short summary (events)
$description string no Longer text
$start string yes* Local datetime, UTC instant, or date (allday)
$end string no Exclusive end, same format as $start
$duration string no ISO 8601 duration, alternative to $end
$timezone string no IANA zone, e.g. Europe/Vienna
$location string | object no Free text or a location object
$url string no Link, e.g. online meeting
$organizer object no Contact
$attendees array no Contacts with $role and $response
$recurrence string | object no RRULE string, or { $rule, $exclude, $include }
$overrides array no Changed instances of a recurring event
$reminders array no Reminders; omitted = inherit defaults, [] = none
$status string no confirmed (default), tentative, cancelled
$visibility string no private (default), busy, public
$busy boolean no Blocks time. Default true for appointment, false for allday
$color string no CSS color
$calendar string no $id of the calendar the event belongs to
$name string yes* Calendar name
$owner object no Calendar owner (contact)
$events array no Events in a calendar
$created string no UTC instant
$updated string no UTC instant of last change

* Required depends on $kind: events need $title and $start, calendars need $name.

Time Rules

  • $start without offset + $timezone -- wall-clock time in that zone (recommended). The event stays at 18:00 local across DST changes.
  • $start without offset and no $timezone -- floating: same clock time wherever the viewer is. $timezone falls back to the calendar's.
  • $start ending in Z -- an absolute instant. Use for events whose start and end are in different zones (flights).
  • allday uses dates only and never shifts with timezones.
  • $end is exclusive (as in iCal). Missing $end and $duration: appointment = zero length, allday = one day.

Recurrence

$recurrence uses iCalendar RRULE syntax verbatim (RFC 5545 §3.3.10), evaluated in the event's $timezone. The short form is the rule string alone.

Property Description
$rule RRULE value, e.g. FREQ=MONTHLY;BYMONTHDAY=1;COUNT=12
$exclude Instances removed from the series (their original $start)
$include Extra instances outside the rule

$overrides entries hold $recurrence_id (the original $start of the instance) plus only the properties that differ. $status: 'cancelled' in an override equals an $exclude. To change "this and all following", end the series with UNTIL and start a new event.

Reminders

Property Type Description
$before string ISO 8601 duration before $start, e.g. PT1H, P1D. PT0S = at start
$days_before number Calendar days before the start date (0 = same day)
$time string Wall-clock HH:MM on that day, in the event's timezone. Use with $days_before
$at string Absolute UTC instant (one-off events only)
$kind string notification (default) or email

Each reminder uses exactly one of $before, $days_before+$time, or $at. For all-day events, $before counts back from 00:00 of the start date. For recurring events, reminders apply to every instance. Resolution order: the event's $reminders, then the calendar's $reminders, then the app settings. A list that is present replaces the defaults. It does not merge with them.

Attendees

Attendees are contact objects with two extra properties:

Property Values
$role required (default), optional, chair
$response pending (default), accepted, declined, tentative

Privacy

$visibility controls what others see when a calendar is shared: private = nothing, busy = only the time slot, public = everything. Single private fields (notes, internal IDs) use the _ prefix, e.g. _notes, _android_id, and are never exported to shared or public views.

Mapping to iCalendar (RFC 5545)

WorldAPI iCalendar
calendar VCALENDAR (X-WR-CALNAME, X-WR-TIMEZONE)
appointment / allday VEVENT
$id UID
$title / $description SUMMARY / DESCRIPTION
$start + $timezone DTSTART;TZID=Europe/Vienna:20261002T180000
$start UTC / floating DTSTART:20261002T160000Z / DTSTART:20261002T180000
$start allday DTSTART;VALUE=DATE:20261103
$end / $duration DTEND (exclusive in both) / DURATION
$location LOCATION (name + address as text), GEO:lat;lng
$url / $color URL / COLOR (RFC 7986)
$organizer ORGANIZER;CN=John:mailto:[email protected]
$attendees ATTENDEE;CN=..;ROLE=REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR;PARTSTAT=NEEDS-ACTION|ACCEPTED|DECLINED|TENTATIVE
$recurrence RRULE, EXDATE, RDATE
$overrides[] Extra VEVENT with same UID + RECURRENCE-ID
$status STATUS:CONFIRMED|TENTATIVE|CANCELLED
$visibility CLASS:PRIVATE|CONFIDENTIAL|PUBLIC
$busy TRANSP:OPAQUE|TRANSPARENT
$reminders $before VALARM TRIGGER:-PT1H
$reminders $days_before+$time VALARM TRIGGER computed against DTSTART (18:00 event, day before 20:00 = -PT22H; allday = -PT4H)
$reminders $at VALARM TRIGGER;VALUE=DATE-TIME:20261001T180000Z
reminder $kind ACTION:DISPLAY / ACTION:EMAIL
$created / $updated CREATED / LAST-MODIFIED

Exports only reach inherited default reminders if they are resolved and written as explicit VALARMs. On import, a TRIGGER becomes $before. Android stores reminders as minutes before the start. A $days_before+$time reminder has to be converted to minutes for each event, and it can be off by an hour across a DST change.