Astrocal
Guides

Calendars

Connect Google, Microsoft, and CalDAV calendars for automatic busy-time blocking.

Calendar connections let Astrocal read your existing events and block busy times from availability. When a booking is created, cancelled, or rescheduled, Astrocal automatically syncs the calendar event.

Prerequisites

  • An Astrocal account with an API key (Authentication guide)
  • For Google: a Google account with Google Calendar
  • For Microsoft: a Microsoft 365 or Outlook.com account
  • For CalDAV: an app-specific password from your provider (Apple, Fastmail, Nextcloud, or any CalDAV server)

How calendar sync works

  1. Busy-time blocking -- When checking availability, Astrocal reads your connected calendars and removes any slots that overlap with existing events.
  2. Booking sync -- When a booking is created, a calendar event is automatically added. When a booking is cancelled or rescheduled, the calendar event is updated or removed.
  3. One-way write -- Astrocal is the source of truth. It writes booking events to your calendar, but external calendar changes are not synced back to Astrocal.

Per-member calendars

Every calendar connection belongs to an organization member, and each member can hold one connection per provider — so a team can connect their own Google, Microsoft, or CalDAV calendars side by side. Google and Microsoft connections are made from the dashboard for your own member record. CalDAV connects via the API take a member_id field — list your members with GET /v1/organizations/{orgId}/members to find it.

Bookings resolve their calendar in this order:

  1. The assigned host's connection (for event types with host assignment)
  2. The organization's default calendar (see Default calendar)
  3. Any active connection in the organization

For event types with a round-robin assignment strategy, availability is computed per host: a slot is offered when at least one assigned host is free.

Connecting Google Calendar

Google Calendar uses an OAuth redirect flow. Start it from the Astrocal dashboard: the dashboard binds the authorization request to your browser session, which stops anyone else from completing the flow into their own organisation.

  1. Open Dashboard → Calendars.
  2. Click Connect Google Calendar.
  3. Approve the requested calendar access on Google's consent screen. Keep the calendar permission ticked — Google lets you untick individual permissions, and Astrocal cannot sync without calendar access.
  4. Google returns you to the dashboard with the connection active.

If you untick calendar access, the dashboard shows "Calendar access wasn't granted" and no connection is stored. Reconnect and approve the calendar permission.

The connect endpoint cannot be driven with an API key. A request without a dashboard session is rejected with 400 bad_request. The connection is created for your own member record.

The connection is then used for busy-time blocking and booking sync.

Connecting Microsoft Calendar

Microsoft Calendar also uses an OAuth redirect flow with PKCE (Proof Key for Code Exchange), and is likewise started from the dashboard.

  1. Open Dashboard → Calendars.
  2. Click Connect Microsoft Calendar.
  3. Approve the requested calendar access on Microsoft's consent screen.
  4. Microsoft returns you to the dashboard with the connection active.

Connecting CalDAV calendars

CalDAV connections use a username and password instead of OAuth. This supports Apple iCloud Calendar, Fastmail, Nextcloud, and any standard CalDAV server.

CalDAV providers require an app-specific password, not your regular account password. See your provider's documentation for how to generate one.

Supported presets

PresetServer URLNotes
appleAuto-configuredRequires an Apple app-specific password
fastmailAuto-configuredRequires a Fastmail app password
nextcloudRequires server_urle.g., https://cloud.example.com/remote.php/dav
customRequires server_urlAny CalDAV-compatible server

Connecting with a preset

Apple iCloud Calendar:

curl -X POST https://api.astrocal.dev/v1/calendars/caldav/connect \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "member_id": "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
    "preset": "apple",
    "username": "user@icloud.com",
    "password": "xxxx-xxxx-xxxx-xxxx"
  }'
const response = await fetch(
  "https://api.astrocal.dev/v1/calendars/caldav/connect",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      member_id: "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
      preset: "apple",
      username: "user@icloud.com",
      password: "xxxx-xxxx-xxxx-xxxx",
    }),
  }
);
const data = await response.json();

Fastmail:

curl -X POST https://api.astrocal.dev/v1/calendars/caldav/connect \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "member_id": "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
    "preset": "fastmail",
    "username": "user@fastmail.com",
    "password": "your-app-password"
  }'
const response = await fetch(
  "https://api.astrocal.dev/v1/calendars/caldav/connect",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      member_id: "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
      preset: "fastmail",
      username: "user@fastmail.com",
      password: "your-app-password",
    }),
  }
);
const data = await response.json();

Nextcloud (requires server_url):

curl -X POST https://api.astrocal.dev/v1/calendars/caldav/connect \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "member_id": "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
    "preset": "nextcloud",
    "username": "admin",
    "password": "your-app-password",
    "server_url": "https://cloud.example.com/remote.php/dav"
  }'
const response = await fetch(
  "https://api.astrocal.dev/v1/calendars/caldav/connect",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      member_id: "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
      preset: "nextcloud",
      username: "admin",
      password: "your-app-password",
      server_url: "https://cloud.example.com/remote.php/dav",
    }),
  }
);
const data = await response.json();

Custom CalDAV server:

curl -X POST https://api.astrocal.dev/v1/calendars/caldav/connect \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "member_id": "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
    "preset": "custom",
    "username": "user",
    "password": "password",
    "server_url": "https://caldav.example.com/dav"
  }'
const response = await fetch(
  "https://api.astrocal.dev/v1/calendars/caldav/connect",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      member_id: "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
      preset: "custom",
      username: "user",
      password: "password",
      server_url: "https://caldav.example.com/dav",
    }),
  }
);
const data = await response.json();

Try it in the API playground →

Response (201 Created):

{
  "id": "880e8400-e29b-41d4-a716-446655440000",
  "provider": "caldav",
  "calendar_id": "user@icloud.com",
  "account_email": "user@icloud.com",
  "member_id": "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
  "member_name": "user@icloud.com",
  "connected_at": "2026-03-15T10:00:00Z",
  "status": "active",
  "last_sync_at": null,
  "sync_status": "connected",
  "sync_error": null
}

Listing connections

List all calendar connections for your organization:

curl https://api.astrocal.dev/v1/calendars \
  -H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch("https://api.astrocal.dev/v1/calendars", {
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
  },
});
const data = await response.json();

Try it in the API playground →

Response:

{
  "data": [
    {
      "id": "880e8400-e29b-41d4-a716-446655440000",
      "provider": "google",
      "calendar_id": "primary",
      "account_email": "user@gmail.com",
      "member_id": "9b2f8c4e-1234-4abc-9def-1a2b3c4d5e6f",
      "member_name": "user@gmail.com",
      "connected_at": "2026-03-01T10:00:00Z",
      "status": "active",
      "last_sync_at": "2026-03-15T14:30:00Z",
      "sync_status": "connected",
      "sync_error": null
    }
  ],
  "has_more": false
}

Connection status fields

FieldValuesDescription
statusactive, needs_reauthWhether the connection is usable
sync_statusconnected, expired, errorCurrent sync health
sync_errorstring or nullError message if sync has failed
last_sync_atstring or nullLast successful sync timestamp
member_idUUIDThe org member this connection belongs to
member_namestring or nullThe owning member's email, when known

A needs_reauth status means the OAuth tokens have expired and the user needs to reconnect. For CalDAV, this typically means the app-specific password was revoked.

Disconnect alerts

When a connection first transitions to needs_reauth, Astrocal alerts you automatically:

  • A calendar.disconnected webhook event is dispatched to subscribed endpoints.
  • An email is sent to every organization admin and owner with a link to reconnect.
  • A warning banner appears in the dashboard until the calendar is reconnected.

Alerts fire once per disconnect — repeated sync failures while the connection is already in needs_reauth do not re-alert.

Disconnecting a calendar

curl -X DELETE https://api.astrocal.dev/v1/calendars/880e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch(
  "https://api.astrocal.dev/v1/calendars/880e8400-e29b-41d4-a716-446655440000",
  {
    method: "DELETE",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
    },
  }
);

Try it in the API playground →

Response:

{
  "id": "880e8400-e29b-41d4-a716-446655440000",
  "deleted": true
}

After disconnecting, busy-time blocking stops immediately. Existing booking events that were already written to the calendar remain (they are not deleted).

Authorization: API keys carry organization-wide authority and can disconnect any connection. Dashboard users can disconnect their own calendar; owners and admins can disconnect anyone's. A member attempting to disconnect another member's calendar receives a 403.

Default calendar

Set an organization-wide default calendar for bookings that have no assigned host. When set, those bookings are created on the default connection instead of an arbitrary one.

curl -X PATCH https://api.astrocal.dev/v1/organizations/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "default_calendar_connection_id": "880e8400-e29b-41d4-a716-446655440000"
  }'
const response = await fetch(
  "https://api.astrocal.dev/v1/organizations/550e8400-e29b-41d4-a716-446655440000",
  {
    method: "PATCH",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      default_calendar_connection_id: "880e8400-e29b-41d4-a716-446655440000",
    }),
  }
);

The connection must be active and belong to your organization (422 otherwise). Pass null to clear the default. You can also set it from the dashboard under Settings → Organization → Default Calendar.

Error handling

StatusError CodeDescription
400validation_errorMissing or invalid fields (e.g., no member_id, or no server_url for nextcloud)
401unauthorizedInvalid API key
403forbiddenDashboard user tried to disconnect another member's calendar
404not_foundCalendar connection or member does not exist in this organization
409conflictCalendar account is already connected
502provider_errorCalendar provider returned an error (e.g., invalid credentials for CalDAV)

Next steps

  • Availability -- Configure availability rules and query bookable slots
  • Bookings -- Create bookings that automatically sync to connected calendars
  • Webhooks -- Get notified when bookings are created, cancelled, or rescheduled
  • API Reference -- Full endpoint documentation

On this page