# Quick Start Guide

Scheduling sounds simple until you build it. Calendars are fragmented across providers, availability shifts in real time, and adding conferencing means wiring up yet another integration.

This guide walks through a complete integration, end to end. Starting from a fresh set of API credentials, you'll connect a user's calendar, find when they're free, book a meeting, sync events back to your application, and handle real-time notifications when anything changes.

The examples work with any language or framework. If you'd rather use an [SDK](/developers/api-libraries/index.md), the same steps apply.

- [What we'll build](#what-we-ll-build)

- [Before you start](#before-you-start)

- [Step 1. Authorize a user](#step-1-authorize-a-user)

- [Step 2. List calendars](#step-2-list-calendars)

- [Step 3. Query availability](#step-3-query-availability)

- [Step 4. Create an event](#step-4-create-an-event)

- [Step 5. Read events](#step-5-read-events)

- [Step 6. Push notifications](#step-6-push-notifications)

- [Next steps](#next-steps)

## What we’ll build
By the end of this guide you'll have a working scheduling integration. A user will connect their calendar to your application, you'll query when they're free, book a meeting with a conferencing link provisioned automatically, sync their events back into your system, and receive real-time notifications whenever their calendar changes - all without writing separate code for Google, Outlook, Exchange, or iCloud.

The six steps cover each phase of that workflow in order:

- **Authorize** - connect a user's calendar to your application via OAuth 2.0.

- **List calendars** - see which calendars are available on their account.

- **Query availability** - find the open slots in their schedule.

- **Create an event** - book the slot and attach a conferencing link automatically.

- **Read events** - sync events back into your application within a time-bounded window.

- **Push notifications** - receive real-time webhooks when a user's calendar changes.

## Before you start
### Create a Cronofy developer account and app
If you haven't already, [create a free Cronofy Developer Account](https://app.cronofy.com/sign_up/developer). From your developer dashboard, create a new application. This gives you:

- A **Client ID** - the public identifier for your app.

- A **Client Secret** - keep this private; never expose it in client-side code.

- An **SDK Identifier** - used to route requests to the correct [data center](/developers/data-centers/index.md).

> **INFO:** All API requests are made to your data center's API host. The default (USA) host is `api.cronofy.com`. For other regions, use the corresponding host listed on the [Data Centers](/developers/data-centers/index.md) page - for example, `api-de.cronofy.com` for Germany.

This guide uses `api.cronofy.com` throughout. Replace it with your own host if you're on a different data center.

### Configure a redirect URI
During the OAuth flow, Cronofy redirects the user back to a URI you control after they grant access. This should be an endpoint in your own application that can receive the `code` query parameter and exchange it for tokens.

While your app is in developer mode you can use any redirect URI without registering it - useful for testing locally or trying things out quickly. When you're ready to move to production, you'll need to provide your redirect URIs to our support team as part of the [application verification process](/developers/faqs/application-management/verify-application/index.md).

Keep your **Client ID**, **Client Secret**, and **redirect URI** handy for the next step.

<hr>
## Step 1. Authorize a user
Cronofy uses the OAuth 2.0 Authorization Code flow ([RFC 6749 §4.1](https://www.rfc-editor.org/rfc/rfc6749#section-4.1)). Your application sits between Cronofy and the user's calendar provider - Cronofy handles all the provider-specific OAuth complexity.

> **INFO:** The authorization URL uses `app.cronofy.com`, not `api.cronofy.com`.

### Step 1a - Redirect the user to the authorization URL
Construct the authorization URL with the parameters below and redirect the user to it. The parameters are URL-encoded in the querystring as described in [RFC 6749 appendix B](https://www.rfc-editor.org/rfc/rfc6749#appendix-B).

```text
https://app.cronofy.com/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope=read_write&state={STATE}
```

**Parameters**

<table>
  <thead>
      <tr>
          <th>Parameter</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>`response_type`</td>
          <td>Always `code`.</td>
      </tr>
      <tr>
          <td>`client_id`</td>
          <td>Your application's Client ID from the developer dashboard.</td>
      </tr>
      <tr>
          <td>`redirect_uri`</td>
          <td>The endpoint in your application that will receive the `code`. In developer mode any value is accepted; in production this must be pre-registered with Cronofy support.</td>
      </tr>
      <tr>
          <td>`scope`</td>
          <td>The permissions your application needs. See the scopes table below.</td>
      </tr>
      <tr>
          <td>`state`</td>
          <td>A random value you generate. Cronofy returns it unchanged so you can verify it and prevent CSRF attacks.</td>
      </tr>
  </tbody>
</table>
**Scopes**

<table>
  <thead>
      <tr>
          <th>Scope</th>
          <th>What it allows</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>`read_write`</td>
          <td>Read event details and free/busy status; create, update, and delete events. Use this for most integrations.</td>
      </tr>
      <tr>
          <td>`free_busy_write`</td>
          <td>Read free/busy status only (no event details); create, update, and delete events. Use this when you need availability and booking but not the content of existing events.</td>
      </tr>
  </tbody>
</table>
This guide uses `read_write` throughout. For more granular control, see the [Permissions reference](/developers/authorization/permissions/index.md).

To test this, either wire the authorization URL up in your application or make a call to it in Postman.

### Step 1b - Handle the redirect and exchange the code
Cronofy redirects the user to your `redirect_uri` with a `code` on the query string. Exchange it immediately - it is single-use and short-lived:

```http
POST /oauth/token HTTP/1.1
Host: api.cronofy.com
Content-Type: application/json; charset=utf-8

{
  "client_id": "{CLIENT_ID}",
  "client_secret": "{CLIENT_SECRET}",
  "grant_type": "authorization_code",
  "code": "{CODE}",
  "redirect_uri": "{REDIRECT_URI}"
}
```

```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "token_type": "bearer",
  "access_token": "P531x88i05Ld2yXHIQ7WjiEyqlmOHsgI",
  "expires_in": 3600,
  "refresh_token": "3gBYG1XamYDUEXUyybbummQWEe5YqPmf",
  "scope": "read_write",
  "account_id": "acc_5ba21743f408617d1269ea1e",
  "sub": "acc_5ba21743f408617d1269ea1e",
  "linking_profile": {
    "provider_name": "google",
    "profile_id": "pro_n23kjnwrw2",
    "profile_name": "example@cronofy.com"
  }
}
```

Store `access_token`, `refresh_token`, and `sub` against the user record:

- `access_token` - passed as `Authorization: Bearer {ACCESS_TOKEN}` on all subsequent API requests.

- `refresh_token` - use this to obtain a new `access_token` when the current one expires; do not discard it.

- `sub` - the stable Cronofy account identifier for this user; use it to look up their tokens on future requests.

> **INFO:** When a request returns `401 Unauthorized`, use the `refresh_token` to obtain a new `access_token`. See [Refreshing an Access Token](/developers/api/authorization/refresh-token/index.md) for the request format. Most OAuth2 libraries handle this automatically.

<hr>
## Step 2. List calendars
Now that you have an `access_token` from Step 1, you can start making authenticated API calls on behalf of the user. The first call retrieves all calendars on their account - you'll need a `calendar_id` from the response to query availability and create events in the steps ahead.

```http
GET /v1/calendars HTTP/1.1
Host: api.cronofy.com
Authorization: Bearer {ACCESS_TOKEN}
```

```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "calendars": [
    {
      "provider_name": "google",
      "profile_name": "example@gmail.com",
      "calendar_id": "cal_n23kjnwrw2_jsdfjksn234",
      "calendar_name": "example@gmail.com",
      "calendar_readonly": false,
      "calendar_deleted": false,
      "calendar_primary": true,
      "calendar_integrated_conferencing_available": true
    }
  ]
}
```

Note the `calendar_id` - you'll use it in the next two steps. The `calendar_integrated_conferencing_available` field indicates whether that calendar can automatically generate a conferencing link (for example, Google Meet on Google Calendar, or Microsoft Teams on a Microsoft 365 calendar connected via the Graph API).

> **INFO:** A user may have multiple calendars across multiple providers. For scheduling, use the calendar with `calendar_primary: true` unless your use case requires a specific one.

<hr>
## Step 3. Query availability
With the `access_token` from Step 1 and the `calendar_id` and `sub` from Step 2, you can now query when the user is free. The [Availability API](/developers/api/scheduling/availability/index.md) inspects their calendar and returns open slots within the time windows you specify.

The request body defines three things:

- **participants** - the users to check, identified by `sub`.

- **required_duration** - the minimum slot length in minutes.

- **query_periods** - the time windows to search within.

```http
POST /v1/availability HTTP/1.1
Host: api.cronofy.com
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json; charset=utf-8

{
  "participants": [
    {
      "required": "all",
      "members": [
        {
          "sub": "{SUB}",
          "calendar_ids": ["{CALENDAR_ID}"]
        }
      ]
    }
  ],
  "required_duration": { "minutes": 60 },
  "response_format": "slots",
  "query_periods": [
    {
      "start": "2026-06-27T09:00:00Z",
      "end": "2026-06-27T17:00:00Z"
    }
  ]
}
```

Using `response_format: &quot;slots&quot;` returns non-overlapping slots of exactly `required_duration` minutes.

```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "available_slots": [
    {
      "start": "2026-06-27T09:00:00Z",
      "end": "2026-06-27T10:00:00Z",
      "participants": [
        { "sub": "acc_5700a00eb0ccd07000000000" }
      ]
    },
    {
      "start": "2026-06-27T10:00:00Z",
      "end": "2026-06-27T11:00:00Z",
      "participants": [
        { "sub": "acc_5700a00eb0ccd07000000000" }
      ]
    },
    {
      "start": "2026-06-27T14:00:00Z",
      "end": "2026-06-27T15:00:00Z",
      "participants": [
        { "sub": "acc_5700a00eb0ccd07000000000" }
      ]
    }
  ]
}
```

Each entry in `available_slots` is a time where all participants are free for the required duration. Pick a slot's `start` and `end` to use in the next step.

> **WARNING:** `query_periods` values must be in UTC and must represent future times. The window must fall within 35 days of the earliest `start`.

<hr>
## Step 4. Create an event
Now that you have an available time slot from Step 3, you can schedule the event. Using the `access_token` from Step 1, the `calendar_id` from Step 2, and the `start` and `end` from Step 3, you can create the event in the user's calendar. Including `&quot;conferencing&quot;: { &quot;profile_id&quot;: &quot;default&quot; }` instructs Cronofy to automatically provision a conferencing link.

The `default` profile resolves in this order:

- The user's default authorized conferencing service (such as Zoom), if they have connected one.

- The calendar's integrated conferencing provider - Google Meet for Google Calendar, Microsoft Teams for eligible Microsoft 365 calendars.

- No conferencing, if neither is available.

```http
POST /v1/calendars/{CALENDAR_ID}/events HTTP/1.1
Host: api.cronofy.com
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json; charset=utf-8

{
  "event_id": "scheduling_demo_01",
  "summary": "Demo meeting",
  "description": "Scheduled via the Cronofy API.",
  "start": "2026-06-27T09:00:00",
  "end": "2026-06-27T10:00:00",
  "tzid": "America/New_York",
  "conferencing": {
    "profile_id": "default"
  }
}
```

`tzid` is an IANA timezone identifier. It tells Cronofy which timezone to interpret the `start` and `end` values in - important for events near DST transitions and for displaying the correct local time to attendees. When `tzid` is set, omit the trailing `Z` from the time strings (a trailing `Z` forces UTC interpretation and overrides `tzid`). For more detail, see the [Time Zones guide](/developers/calendars-events/time-zones/index.md) and the [Daylight Saving Time guide](/developers/faqs/event-management/daylight-saving/index.md).

A successful response is `HTTP 202 Accepted`. The event is created immediately; the conferencing link is provisioned asynchronously. If you read the event before the link is ready, the `conferencing` field will be `{ &quot;pending&quot;: true }`. Once generated, it resolves to:

```json
{
  "conferencing": {
    "provider_name": "google_meet",
    "join_url": "https://meet.google.com/abc-defg-hij"
  }
}
```

> **INFO:** The `event_id` you supply is your own stable identifier for the event. Sending the same `event_id` in a future request updates the existing event rather than creating a duplicate - useful for rescheduling.

For the widest conferencing compatibility, leave the `location` field empty. Cronofy will populate it with the join URL on calendars that do not have a dedicated conferencing field.

<hr>
## Step 5. Read events
With an event created, you can now read events back from the user's calendar. You'll use this both for the initial load and as the response to push notifications (covered in the next step).

`tzid` is **required** for read events - it tells Cronofy which timezone to express event times in.

```http
GET /v1/events?tzid={TZID}&from={FROM}&to={TO}&include_deleted=true&include_managed=true HTTP/1.1
Host: api.cronofy.com
Authorization: Bearer {ACCESS_TOKEN}
```

**Key parameters**

<table>
  <thead>
      <tr>
          <th>Parameter</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>`tzid`</td>
          <td>**Required.** IANA timezone identifier for the returned event times - for example `America/New_York`, `Europe/London`, or `Etc/UTC`.</td>
      </tr>
      <tr>
          <td>`from`</td>
          <td>Start of the sync window. Defaults to 42 days in the past.</td>
      </tr>
      <tr>
          <td>`to`</td>
          <td>End of the sync window. Defaults to 201 days in the future.</td>
      </tr>
      <tr>
          <td>`last_modified`</td>
          <td>ISO 8601 timestamp. Returns only events modified after this time. Use for incremental sync - pass the `changes_since` value from a push notification (see Step 6).</td>
      </tr>
      <tr>
          <td>`include_deleted`</td>
          <td>Set to `true` to include deleted events, so you can remove them from your database. Defaults to `false`.</td>
      </tr>
      <tr>
          <td>`include_managed`</td>
          <td>Set to `true` to include events created by your application via the Cronofy API. Defaults to `false`.</td>
      </tr>
  </tbody>
</table>
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "pages": {
    "current": 1,
    "total": 1,
    "next_page": "https://api.cronofy.com/v1/events/pages/08a07b034306679e"
  },
  "events": [
    {
      "calendar_id": "cal_n23kjnwrw2_jsdfjksn234",
      "event_uid": "evt_external_539e00003f5e",
      "summary": "Demo meeting",
      "description": "Scheduled via the Cronofy API.",
      "start": "2026-06-27T09:00:00",
      "end": "2026-06-27T10:00:00",
      "deleted": false,
      "participation_status": "accepted",
      "transparency": "opaque",
      "event_status": "confirmed",
      "categories": [],
      "attendees": [],
      "conferencing": {
        "provider_name": "google_meet",
        "join_url": "https://meet.google.com/abc-defg-hij"
      },
      "created": "2026-06-27T08:00:00Z",
      "updated": "2026-06-27T08:00:00Z"
    }
  ]
}
```

**Handling pagination**

When `pages.total` is greater than `pages.current`, follow the `pages.next_page` URL to retrieve the next page. Keep fetching until you've processed the last page.

**Incremental sync with `last_modified`**

For ongoing sync, pass `last_modified` set to the `changes_since` timestamp from the most recent push notification. This returns only the events that have changed since that point, keeping the response small. See [Incremental Synchronization](/developers/api/events/read-events/index.md) for full details.

> **INFO:** The default sync window covers 42 days in the past to 201 days in the future, relative to the time of the request. Events that fall outside this window - for example, a far-future booking - won't be returned unless you explicitly widen `from` and `to`.

<hr>
## Step 6. Push notifications
Register a notification channel to receive a webhook the moment a user's calendar changes. Your server then calls Read Events with the `changes_since` timestamp from the notification to fetch only what changed.

**Testing locally**

Your `callback_url` must be publicly reachable. For local development, you can use ngrok to expose your local server - see [Push Notifications](/developers/push-notifications/index.md) for details.

### Register a notification channel
```http
POST /v1/channels HTTP/1.1
Host: api.cronofy.com
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json; charset=utf-8

{
  "callback_url": "https://your-app.com/webhooks/cronofy",
  "filters": {}
}
```

```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "channel": {
    "channel_id": "chn_54cf7c7cb4ad4c1027000001",
    "callback_url": "https://your-app.com/webhooks/cronofy",
    "filters": {}
  }
}
```

### Handle the verification notification
Immediately after creating a channel, Cronofy sends a `verification` notification to your `callback_url` to confirm it is reachable. Your endpoint must respond `HTTP 200` within 5 seconds, or the channel will be closed.

```json
{
  "notification": {
    "type": "verification"
  },
  "channel": {
    "channel_id": "chn_54cf7c7cb4ad4c1027000001",
    "callback_url": "https://your-app.com/webhooks/cronofy",
    "filters": {}
  }
}
```

### Handle change notifications
When a user's calendar changes, Cronofy sends a `change` notification. The `changes_since` value is the timestamp to pass as `last_modified` in your Read Events call.

```json
{
  "notification": {
    "type": "change",
    "changes_since": "2024-06-23T09:24:16Z"
  },
  "channel": {
    "channel_id": "chn_54cf7c7cb4ad4c1027000001",
    "callback_url": "https://your-app.com/webhooks/cronofy",
    "filters": {}
  }
}
```

On receiving a `change` notification:

- Read the `changes_since` timestamp from the notification body.

- Call `GET /v1/events` with `last_modified={CHANGES_SINCE}`, `include_deleted=true`, and `include_managed=true`.

For more information about staying up to date with a user's calendar, see [Recommended Event Sync](/developers/faqs/event-management/recommended-sync/index.md).

**Testing change notifications**

You can trigger test notifications directly from your Cronofy developer dashboard without needing to make real calendar changes. Go to your application dashboard, select **Channels** from the left navigation, and choose an active channel. Under the **Trigger Push Notifications** heading you'll find buttons to send a dummy version of each notification type to your channel's callback URL.

> **INFO:** Verify the `Cronofy-HMAC-SHA256` header on every incoming notification. It is a Base64-encoded HMAC-SHA256 of the raw request body, signed with your Client Secret. Reject any request where the signature does not match. See [Push Notifications](/developers/api/push-notifications/index.md) for the full verification algorithm.

<hr>
You've now built a complete calendar integration: authorized a user, queried availability, created a meeting, synced events, and wired up real-time notifications. From here you can explore more advanced capabilities like [Availability Rules](/developers/api/scheduling/availability-rules/index.md) for setting working hours, [UI Elements](/developers/ui-elements/index.md) to add pre-built scheduling components to your interface, or [Meeting Agents](/developers/meeting-agents/index.md) to automatically record, transcribe, and summarise the meetings you've just scheduled.

## Next steps
- [API Libraries *Official SDKs for Node.js, Ruby, Python, and more.*](/developers/api-libraries/index.md)

- [Authorization *Understand token refresh, scopes, and account types.*](/developers/authorization/index.md)

- [Availability API *Advanced options: multi-participant queries, buffers, and availability rules.*](/developers/api/scheduling/availability/index.md)

- [Conferencing Services *Learn about Zoom, Teams, and Google Meet integration.*](/developers/api/conferencing-services/index.md)

- [Create or Update Event *Full reference for event creation, including all supported fields.*](/developers/api/events/upsert-event/index.md)

- [Push Notifications *Complete reference including security, filters, and notification types.*](/developers/api/push-notifications/index.md)

- [Read Events *Full reference for sync parameters, pagination, and incremental sync.*](/developers/api/events/read-events/index.md)

- [Verify your application *Steps to complete before going live with real users.*](/developers/faqs/application-management/verify-application/index.md)



---
[Read in HTML](/developers/getting-started/quick-start-guide/api-quick-start-guide/)