# Compatibility and migrating

CalMonkey’s v1 API uses the paths, parameter names, response shapes, status codes and error shapes that many existing calendar integrations already call, for connecting calendars, free/busy, events with attendees, recurring events and conferencing, and push notifications. Such an integration usually switches with a change of hostnames and credentials, and its users reconnecting.

<a id="compatible"></a>

## What is compatible

These endpoints have the paths, parameters, response fields, status codes and error shapes that many existing calendar integrations already call. A client that uses only them switches by changing hostnames and credentials.

- `GET /oauth/authorize`
- `POST /oauth/token`
- `POST /oauth/token/revoke`
- `POST /v1/application_calendars`
- `GET /v1/account`
- `GET /v1/userinfo`
- `GET /v1/profiles`
- `GET /v1/calendars`
- `GET /v1/free_busy`
- `GET /v1/events`
- `POST /v1/calendars/{id}/events`
- `DELETE /v1/calendars/{id}/events`
- `POST /v1/calendars/{id}/events/occurrences`
- `POST /v1/channels`
- `GET /v1/channels`
- `DELETE /v1/channels/{id}`
- `POST /v1/link_tokens`

Provider names are `google`, `office365`, `live_connect` and `apple`. Every response also carries a `Calmonkey-Request-Id` header, and authenticated responses the `RateLimit-*` headers.

<a id="events-fields"></a>

### Attendees, recurring events and conferencing

Event writes and reads take these fields and return these shapes:

- `attendees: { invite: [{ email, display_name }], remove: [{ email }] }` on writes; `attendees[]` with `email`, `display_name` and `status`, `organizer` and `participation_status` on reads. A wrong address is `errors.invalid_email` on `attendees.invite[0].email`.
- `recurrence: { rules: [{ frequency, interval, count | until, by_day: [{ day, nth_of_period }] }] }`, with one rule, a date-only `until` and these keys for what a rule cannot say: `errors.not_supported`, `errors.not_recognized`, `errors.must_be_date`, `errors.must_be_integer_greater_than_zero`, `errors.not_supported_count_until_combination`, `errors.max_recurrence_rules_exceeded`, `errors.invalid_format`. Leaving `recurrence` out keeps the series and `null` removes it. `exceptions.add` skips dates.
- `conferencing: { profile_id }` with `default`, `integrated` and `none`; reads show `{ pending: true }`, then the `join_url`.

<a id="other-products"></a>

### Outside the compatible set

These have no counterpart on this API: availability-search and scheduling products, `conferencing.profile_id: "explicit"` with a join URL of your own, changing a participation status through the API, on-premises Exchange, and per-profile revoke (`POST /v1/profiles/{id}/revoke`). Revoke the whole grant with `/oauth/token/revoke` instead.

<a id="mode"></a>

## Compatibility mode

Where CalMonkey’s own behaviour differs from what existing integrations expect, an application can ask for the behaviour they expect. Compatibility mode is a setting of each application, off by default. Turn it on for the application an existing client will use; leave it off for new integrations. You switch it in the application’s page in the dashboard.

| Behaviour | Compatibility on | Off (CalMonkey) |
| --- | --- | --- |
| All-day event written without transparency | transparent (free) | opaque (busy) |
| free_busy periods | event_uid only with include_ids=true | always event_uid |
| 422 wording | “\<field> must be specified”; errors.tzid_unrecognized “TZID not recognized”; errors.invalid_time_pair “end must be after start” | errors.required “required”; errors.invalid |
| POST /v1/application_calendars | no account_id; linking_profile.profile_name null; the same token pair again while it is valid | account_id; the calendar’s name; a new pair on every call |
| Refreshing a token | the same access token while it has at least 10 minutes left (expires_in = what is left) | a new access token |
| GET /v1/account for an application calendar | 403, plain text | 200 |
| GET /v1/userinfo | the type and data under the key names existing integrations read; an application calendar has application_calendar_id and no name or zoneinfo | calmonkey.type / calmonkey.data |
| Creating a channel | no signing_secret in the response | signing_secret, once |
| Webhook headers | a second HMAC header as well, under the name existing integrations verify | Calmonkey-\* headers only |
| Application calendars’ provider_name / provider_service | the name existing integrations expect | calmonkey |
| Reading a recurring event your application wrote | the first occurrence carries event_id and recurrence; later ones carry series_identifier and series_master, no event_id, and options.update false | every occurrence carries event_id, series_identifier and series_master; the first also recurrence |
| A timed recurring event written without tzid | accepted; it repeats at the same UTC time | 422 on tzid: a series repeats in local time and needs its zone |
| recurrence with an empty rules list | accepted: the event stops repeating | 422; send recurrence: null |
| attendees on a calendar that sends no invitations | errors.event.cannot_set_attendees | errors.attendees_unsupported |
| conferencing.profile_id integrated on a calendar without a link of its own | errors.integrated_conferencing_not_available | errors.conferencing_unavailable |
| An unknown conferencing.profile_id | errors.not_recognized | errors.invalid_value |

Webhook signatures: in compatibility mode a second header carries the same value as `Calmonkey-HMAC-SHA256` (base64 HMAC-SHA256 of the body, keyed with your client secret), under the header name existing integrations verify, so your existing verification keeps working once it uses the new client secret.

<a id="differences"></a>

## Differences in every mode

- **Callback URLs** must be https on port 443 and on a public host. A test-mode application may also register `http://localhost…` for [webhooks on your own machine](https://calmonkey.com/docs/ai.md#listen).
- **Channels** have no `scheduling_conversations` key.
- **Profiles** carry two added keys, which existing clients ignore: `profile_calendars` (each profile’s calendars, so one call is enough) and `profile_relink_url` (always null: a profile is reconnected through `/oauth/authorize`).
- **Events** always carry `event_private` (false), `extended_transparency` (the same as `transparency`), `organizer`, and `url` when one was written. **Calendars** carry `calendar_attachments_available` (false). **Profiles** carry `provider_service`.
- **Attendees.** `notify_attendees` (on writes and deletes) is CalMonkey’s own field. With `notify_attendees: false` an application calendar stores guests; taking guests off with `remove` alone is always accepted. A body with a mistake in it answers with the mistake first, and with the calendar’s refusal once the body is valid.
- **Recurring events.** The event’s start must itself be an occurrence of the rule (a rule for Tuesdays needs a start on a Tuesday, in the event’s time zone). A monthly rule without `by_day` starts on the 1st to the 28th, a yearly one on any date but 29 February. `count` goes up to 730 and `interval` to 99. `start` and `end` are also accepted as `{ time, tzid }` objects.
- **Single occurrences.** `recurrence.exceptions.change`, `DELETE` with `occurrence_date` and `POST /v1/calendars/{id}/events/occurrences` are CalMonkey’s own, and every occurrence carries an added `occurrence_date`. Other keys under `exceptions` (`exclude` among them) are `errors.not_supported`.
- **Conferencing.** A made link reads `{ provider_name, join_url }`: `provider_name` is added. `profile_id: "default"` on a calendar without a link of its own (iCloud, application calendars) writes the event without one and reads show no `conferencing` key. `profile_id: "explicit"` is refused with `422` on `conferencing.profile_id`.
- **Wrong client credentials** are `400 {"error":"invalid_client"}` on `/oauth/*` and `401` with an empty body on `POST /v1/application_calendars`.
- **Webhooks** are retried for about 23 hours ([schedule](https://calmonkey.com/docs/errors.md#webhook-retries)); `410 Gone` closes the channel.

<a id="steps"></a>

## Switching, step by step

1. **Create an application** in the [dashboard](https://app.calmonkey.com/dashboard). Copy the client id and the client secret (shown once).
2. **Register your redirect URIs**, exactly as your code sends them, and turn on compatibility mode.
3. **Point your client at CalMonkey.** If it reads the hosts from settings, that is four values (under the names your code already reads):

   ```sh
   CALENDAR_CLIENT_ID=<your CalMonkey client id>
   CALENDAR_CLIENT_SECRET=<your CalMonkey client secret>
   CALENDAR_API_HOST=https://api.calmonkey.com
   CALENDAR_SITE_HOST=https://app.calmonkey.com
   ```

   The API host replaces your current API host; the app host replaces the host your code sends people to for `/oauth/authorize`. If your code checks that callback or API URLs end in your current provider’s domain, change that check to compare with the CalMonkey hosts exactly.
4. **Check webhook verification** with the new client secret, and that your callback URL is https on port 443.
5. **Move your users.** Tokens cannot be carried over from another service: each user connects their calendar again through CalMonkey. Before switching, for each connection: delete the events you wrote through the old service, close its channels and revoke its token there; then mark the connection as needing to reconnect, and show the user a short message (“Calendar sync was upgraded. Connect your calendar again.”). When they reconnect, write their upcoming events again: writes are idempotent by `event_id`.
6. **Test first** with an [application calendar](https://calmonkey.com/docs/quickstart.md#application-calendars), which needs no browser, then with one real account per provider you offer.

> CalMonkey is hosted in Australia. There is no choice of data centre, so there is no data centre in the hostnames.
