Events and recurrence
An event is a single event or a whole series. You address it by your own external_id, for example the post ID from WordPress. You don’t need to store any id from us.
Create or update an event
Section titled “Create or update an event”PUT /v1/publisher/calendars/{calendar_id}/events/{external_id}curl -X PUT https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events/wp-1482 \ -H "Authorization: Bearer $KS_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Sunset Yoga in the Park", "description": "<p>Bring your own <strong>mat</strong>. Free for members.</p>", "location": "Riverside Park, Pier 2, New York", "url": "https://example.org/classes/sunset-yoga/", "start": "2026-11-12T18:00:00", "end": "2026-11-12T19:30:00", "timezone": "America/New_York", "status": "confirmed", "categories": ["Outdoor"], "metadata": { "post_id": 1482 } }'external_id may be 1 to 128 characters: letters, digits and . _ : -. A prefix such as wp-1482 works well.
Response: 201 Created the first time, 200 OK on a change.
{ "external_id": "wp-1482", "calendar_id": "cal_XXXXXXXXXXXX", "sequence": 0, "created_at": "2026-11-03T14:05:40.000Z", "updated_at": "2026-11-03T14:05:40.000Z", "event": { "title": "Sunset Yoga in the Park", "all_day": false, "start": "2026-11-12T18:00:00", "end": "2026-11-12T19:30:00", "timezone": "America/New_York", "status": "confirmed", "show_as": "free", "categories": ["Outdoor"], "description": "Bring your own mat. Free for members.", "location": "Riverside Park, Pier 2, New York", "url": "https://example.org/classes/sunset-yoga/", "metadata": { "post_id": 1482 } }, "changed": true}eventis the event as we stored it: times as local time intimezone, HTML as text.sequencecounts up with every real change.- Send the same event again and the API answers
200with"changed": false. Nothing is written, the feed stays unchanged. So you can simply callPUTon every save in your system. PUTreplaces the whole event. Fields you leave out are empty or back to their default afterwards.
Optimistic locking with If-Match
Section titled “Optimistic locking with If-Match”Optionally send the last sequence you saw:
If-Match: "3"If the event changed in the meantime (or does not exist yet), you get 409 sequence_mismatch instead of an overwrite.
Fields
Section titled “Fields”| Field | Rules | Default |
|---|---|---|
title | Required, 1 to 255 characters | |
start | Required. Date or date-time, see times | |
end | Optional. For timed events not before start; for all-day events exclusive | start + the calendar’s default_duration, or one day |
timezone | IANA name, e.g. America/New_York | The calendar’s time zone |
all_day | true or false | true if start is a plain date |
description | Text or HTML, up to 65,536 characters. HTML becomes text; links are kept as text (URL). At most 8,000 characters are stored. | |
location | up to 500 characters | |
url | https:// URL, up to 2,000 characters. Shown as a link in apps and also appended to the description. | |
status | confirmed, tentative or cancelled | confirmed |
show_as | free or busy: whether the event counts as busy for subscribers | free |
categories | up to 10 entries of 1 to 50 characters | [] |
recurrence | Series, see recurrence | |
metadata | Any JSON object up to 2 KB. Returned in API responses, but never shown in the feed or on the subscribe page. |
show_as defaults to free: public events should not mark your subscribers’ calendars as busy.
A cancelled event ("status": "cancelled") stays in the feed and is marked as cancelled there; how that looks depends on the app. To remove it completely, delete it.
Times and time zones
Section titled “Times and time zones”start / end | Meaning |
|---|---|
"2026-11-12T18:00:00" | Local time in timezone (or the calendar’s time zone). Recommended. |
"2026-11-12T23:00:00Z" or "2026-11-12T18:00:00-05:00" | Instant with offset. Converted to timezone and stored that way. |
"2026-11-12" | Plain date: all-day event. |
Local time plus an IANA time zone is the safest choice: daylight saving time stays right, even for series across the clock change.
All-day events: end is exclusive. An event on 12 November only has "start": "2026-11-12" and "end": "2026-11-13", or no end at all. A three-day festival from 12 to 14 November has "end": "2026-11-15".
Recurrence
Section titled “Recurrence”{ "title": "Morning Flow", "start": "2026-11-03T07:30:00", "end": "2026-11-03T08:30:00", "timezone": "America/New_York", "recurrence": { "rules": ["FREQ=WEEKLY;BYDAY=TU;UNTIL=20270330"], "except_dates": ["2026-12-22", "2026-12-29"], "overrides": [ { "original_start": "2026-11-17T07:30:00", "start": "2026-11-17T08:00:00", "end": "2026-11-17T09:00:00", "location": "Studio 2" }, { "original_start": "2026-12-01T07:30:00", "status": "cancelled" } ] }}| Field | Rules |
|---|---|
rules | 1 to 5 RRULE strings per RFC 5545, up to 500 characters each. The RRULE: prefix is optional. Several rules become separate series in the feed. |
except_dates | up to 1,000 occurrences that are skipped without a trace. A date is enough; for timed series it means the occurrence on that day. |
overrides | up to 500 changed occurrences, see below. |
Allowed RRULE parts: FREQ (DAILY, WEEKLY, MONTHLY, YEARLY), INTERVAL (1 to 1,000), COUNT (1 to 1,000), UNTIL, BYDAY, BYMONTHDAY, BYMONTH, BYSETPOS, WKST. COUNT and UNTIL exclude each other. Other parts such as BYHOUR or FREQ=HOURLY are refused with unsupported_rrule.
UNTIL may be YYYYMMDD, YYYYMMDDTHHMMSS (local time in the event’s time zone) or YYYYMMDDTHHMMSSZ (UTC). We convert it the way RFC 5545 requires. A plain date on a timed event means “through the end of that day”.
Change single occurrences: overrides
Section titled “Change single occurrences: overrides”original_startnames the occurrence as the rule generates it, in the event’s time zone (a date for all-day series).- You can change
start,end,title,description,location,urlandstatus. Fields you leave out keep the series value. "status": "cancelled"cancels just that occurrence. To remove it without a trace, useexcept_datesinstead.- Overrides are part of the event: every
PUTsends the full list. An override you leave out reverts to the series. - Each occurrence may be overridden only once (otherwise
duplicate_override).
Mapping typical plugin fields
Section titled “Mapping typical plugin fields”How common fields of an event plugin (WordPress and similar) map:
| In the plugin | In the API |
|---|---|
| Post ID | external_id, e.g. "wp-1482" |
| Title | title |
| Content / description (HTML) | description. HTML works as is; a short excerpt plus url works better. |
| Event permalink | url |
| Venue, address | location, e.g. "Studio by the River, 5 Main Street, New York" |
| Start date + start time | start: "2026-11-12T18:00:00" |
| End date + end time | end: "2026-11-12T19:30:00" |
Site time zone (timezone_string) | timezone on the calendar, on the event only if different |
| “All day” checked | start/end as plain dates, end + 1 day |
| Categories, tags | categories |
| Status “cancelled” | "status": "cancelled" |
| Status “tentative” | "status": "tentative" |
| Post trashed, draft, private | DELETE on the event |
| Custom fields (price, ticket id …) | metadata (only for you) or as text in description (for subscribers) |
Recurrence settings:
| Setting in the plugin | RRULE |
|---|---|
| Daily / weekly / monthly / yearly | FREQ=DAILY / WEEKLY / MONTHLY / YEARLY |
| Every 2 weeks | FREQ=WEEKLY;INTERVAL=2 |
| Tuesdays and Thursdays | FREQ=WEEKLY;BYDAY=TU,TH |
| Second Monday of the month | FREQ=MONTHLY;BYDAY=2MO |
| Last Friday of the month | FREQ=MONTHLY;BYDAY=-1FR |
| On the 15th of every month | FREQ=MONTHLY;BYMONTHDAY=15 |
| Ends after 10 occurrences | ;COUNT=10 |
| Ends on 25 March 2027 | ;UNTIL=20270325 |
| Blackout dates (holidays) | recurrence.except_dates |
| Single occurrence moved | recurrence.overrides |
For PHP, the MIT-licensed library rlanvin/php-rrule builds and validates these rules.
Get an event
Section titled “Get an event”curl https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events/wp-1482 -H "Authorization: Bearer $KS_KEY"If the event does not exist: 404 event_not_found.
List events
Section titled “List events”curl "https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events?limit=100" -H "Authorization: Bearer $KS_KEY"| Parameter | Meaning |
|---|---|
limit | 1 to 100, default 100 |
cursor | next_cursor of the previous page |
updated_since | ISO instant with offset, e.g. 2026-11-01T00:00:00Z: only events changed after it |
The response contains data (events as above) and next_cursor (null on the last page). Events are sorted by last change, so updated_since plus the cursor returns every change exactly once. An invalid cursor answers 400 invalid_cursor.
Delete an event
Section titled “Delete an event”curl -X DELETE https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events/wp-1482 -H "Authorization: Bearer $KS_KEY"Always 204 No Content, also when the event was already gone. Retries are safe.
Validation errors
Section titled “Validation errors”Invalid events are refused as a whole, with 400 invalid_request and every problem at once in errors:
{ "type": "https://docs.kalender-sync.de/en/api/errors/invalid_request", "title": "The request is invalid", "status": 400, "code": "invalid_request", "request_id": "req_…", "errors": [ { "field": "end", "code": "end_before_start", "message": "end must not be before start." }, { "field": "recurrence.rules[0]", "code": "unsupported_rrule", "message": "BYHOUR is not supported." } ]}errors[].code | Meaning |
|---|---|
invalid_timezone | Unknown time zone |
invalid_date | Unreadable date (all-day events, except_dates) |
invalid_datetime | Unreadable date-time, or an override that does not fit the series |
end_before_start | end is before start (all-day: not after it) |
invalid_rrule | Rule is malformed, e.g. COUNT and UNTIL together |
unsupported_rrule | Rule part or frequency is not supported |
duplicate_override | An occurrence was overridden twice |
too_large | metadata larger than 2 KB |
Schema violations (missing required field, unknown field, text too long) carry the validator’s code, e.g. invalid_type, too_big or unrecognized_keys.
Other possible errors when writing: event_limit_reached, calendar_not_found, plan_required, calendar_blocked.