# Migrating from Cronofy

CalMonkey’s v1 API follows Cronofy’s calendar API: the same paths, parameter names, response shapes, status codes and error shapes for connecting calendars, free/busy, events with attendees, recurring events and conferencing, and push notifications. An existing Cronofy client usually switches with a change of hostnames and credentials, and its users reconnecting.

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

## What is compatible

These endpoints have Cronofy’s paths, parameters, response fields, status codes and error shapes. A Cronofy 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 Cronofy’s: `google`, `office365`, `live_connect`, `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 Cronofy’s fields and return Cronofy’s 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`, as Cronofy.
- `recurrence: { rules: [{ frequency, interval, count | until, by_day: [{ day, nth_of_period }] }] }`, with one rule, a date-only `until` and Cronofy’s 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

Cronofy’s other products and endpoints have no counterpart on this API: the Availability and Scheduler APIs, Smart Invites, `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>

## Cronofy compatibility mode

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

| Behaviour | Compatibility on (as Cronofy) | Off (CalMonkey) |
| --- | --- | --- |
| All-day event written without transparency | transparent (free), as Cronofy | 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 | cronofy.type / cronofy.data; 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 | Cronofy-HMAC-SHA256 as well | Calmonkey-\* headers only |
| Application calendars’ provider_name / provider_service | cronofy | 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 `Cronofy-HMAC-SHA256` is computed exactly as Cronofy computes it (base64 HMAC-SHA256 of the body, keyed with your client secret), 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. Cronofy also accepts http and other ports.
- **Channels** have no `scheduling_conversations` key (that belongs to Cronofy’s Scheduler).
- **Profiles** carry two added keys, which Cronofy 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, which Cronofy’s application calendars refuse; 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); Cronofy accepts a start that is not. 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` (Cronofy’s `exclude` among them) are `errors.not_supported`, as on Cronofy.
- **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; Cronofy shows `pending` there. `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`, as Cronofy.
- **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 Cronofy compatibility mode.
3. **Point your client at CalMonkey.** If it reads the hosts from settings, that is four values:

   ```sh
   CRONOFY_CLIENT_ID=<your CalMonkey client id>
   CRONOFY_CLIENT_SECRET=<your CalMonkey client secret>
   CRONOFY_API_HOST=https://api.calmonkey.com
   CRONOFY_SITE_HOST=https://app.calmonkey.com
   ```

   The API host replaces `api.cronofy.com` (or your data centre’s API host); the app host replaces `app.cronofy.com` for `/oauth/authorize`. If your code checks that callback or API URLs end in `cronofy.com`, 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 Cronofy: each user connects their calendar again through CalMonkey. Before switching, for each connection: delete the events you wrote through Cronofy, close its channels and revoke its Cronofy token; 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 Sydney, Australia. There is no choice of data centre, so there is no data centre in the hostnames either.
