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
- Busy-time blocking -- When checking availability, Astrocal reads your connected calendars and removes any slots that overlap with existing events.
- 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.
- 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:
- The assigned host's connection (for event types with host assignment)
- The organization's default calendar (see Default calendar)
- 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.
- Open Dashboard → Calendars.
- Click Connect Google Calendar.
- 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.
- 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.
- Open Dashboard → Calendars.
- Click Connect Microsoft Calendar.
- Approve the requested calendar access on Microsoft's consent screen.
- 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
| Preset | Server URL | Notes |
|---|---|---|
apple | Auto-configured | Requires an Apple app-specific password |
fastmail | Auto-configured | Requires a Fastmail app password |
nextcloud | Requires server_url | e.g., https://cloud.example.com/remote.php/dav |
custom | Requires server_url | Any 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
| Field | Values | Description |
|---|---|---|
status | active, needs_reauth | Whether the connection is usable |
sync_status | connected, expired, error | Current sync health |
sync_error | string or null | Error message if sync has failed |
last_sync_at | string or null | Last successful sync timestamp |
member_id | UUID | The org member this connection belongs to |
member_name | string or null | The 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.disconnectedwebhook 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
| Status | Error Code | Description |
|---|---|---|
| 400 | validation_error | Missing or invalid fields (e.g., no member_id, or no server_url for nextcloud) |
| 401 | unauthorized | Invalid API key |
| 403 | forbidden | Dashboard user tried to disconnect another member's calendar |
| 404 | not_found | Calendar connection or member does not exist in this organization |
| 409 | conflict | Calendar account is already connected |
| 502 | provider_error | Calendar 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