Bookings
Create, cancel, and reschedule bookings via the Astrocal API.
Bookings represent scheduled meetings between your users and invitees. The booking lifecycle covers creation, confirmation, cancellation, and rescheduling -- with automatic calendar sync and email notifications at each step.
Prerequisites
- An Astrocal account with an API key (Authentication guide)
- At least one event type with availability configured (Event Types, Availability)
- Optional: a connected calendar for busy-time blocking and event sync (Calendars)
Creating a booking
Create a booking by sending a POST request to /v1/bookings. This is a public endpoint with no authentication required, so your end users can book directly.
curl -X POST https://api.astrocal.dev/v1/bookings \
-H "Content-Type: application/json" \
-d '{
"event_type_id": "evt_abc123",
"start_time": "2026-03-15T14:00:00Z",
"invitee_name": "Jane Doe",
"invitee_email": "jane@example.com",
"invitee_timezone": "America/Los_Angeles"
}'const response = await fetch("https://api.astrocal.dev/v1/bookings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
event_type_id: "evt_abc123",
start_time: "2026-03-15T14:00:00Z",
invitee_name: "Jane Doe",
invitee_email: "jane@example.com",
invitee_timezone: "America/Los_Angeles",
}),
});
const data = await response.json();For event types with duration_options, pass duration to select a specific meeting length:
curl -X POST https://api.astrocal.dev/v1/bookings \
-H "Content-Type: application/json" \
-d '{
"event_type_id": "evt_abc123",
"start_time": "2026-03-15T14:00:00Z",
"invitee_name": "Jane Doe",
"invitee_email": "jane@example.com",
"invitee_timezone": "America/Los_Angeles",
"duration": 60
}'const response = await fetch("https://api.astrocal.dev/v1/bookings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
event_type_id: "evt_abc123",
start_time: "2026-03-15T14:00:00Z",
invitee_name: "Jane Doe",
invitee_email: "jane@example.com",
invitee_timezone: "America/Los_Angeles",
duration: 60,
}),
});
const data = await response.json();Try it in the API playground →
If duration is omitted, the event type's default duration_minutes is used. The value must be one of the event type's duration_options.
The API validates the requested slot against availability rules and existing bookings. If the slot is unavailable, you'll get a 409 Conflict response.
If the event type has a booking cap configured and it has been reached for the current period, you'll get a 409 Conflict with error code booking_cap_reached. Check availability first -- a capped: true flag in the response indicates the cap has been reached.
The response includes a cancel_token that enables self-service cancellation and rescheduling without authentication. Store this token if you want to let invitees manage their own bookings.
Cancelling a booking
Cancel a booking by sending a POST request to /v1/bookings/{id}/cancel. Both invitees and developers can cancel, using different authentication methods.
With a cancel token (invitee self-service)
The cancel token is included in the booking response and in confirmation emails. Invitees can cancel without logging in:
curl -X POST "https://api.astrocal.dev/v1/bookings/bk_123/cancel?token=abc123" \
-H "Content-Type: application/json" \
-d '{"reason": "Schedule conflict"}'const response = await fetch(
"https://api.astrocal.dev/v1/bookings/bk_123/cancel?token=abc123",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ reason: "Schedule conflict" }),
}
);
const data = await response.json();The reason field is optional.
With an API key (developer)
Developers can cancel any booking in their organization using an API key:
curl -X POST https://api.astrocal.dev/v1/bookings/bk_123/cancel \
-H "Authorization: Bearer ac_live_..." \
-H "Content-Type: application/json" \
-d '{"reason": "Customer requested cancellation"}'const response = await fetch(
"https://api.astrocal.dev/v1/bookings/bk_123/cancel",
{
method: "POST",
headers: {
Authorization: "Bearer ac_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ reason: "Customer requested cancellation" }),
}
);
const data = await response.json();Try it in the API playground →
Cancellation behavior
- The booking status changes to
cancelledandcancelled_atis set. - The calendar event is deleted automatically (for all connected calendar providers).
- Cancellation emails are sent to both the invitee and organizer.
- A
booking.cancelledwebhook event is dispatched. - Cancellation is idempotent: cancelling an already-cancelled booking returns success.
Refunds for paid bookings
When a paid booking is cancelled:
- If the booking's start time is 24 hours or more in the future, a full Stripe refund is issued automatically.
- If the booking starts in less than 24 hours, no automatic refund is issued. You can issue a manual refund through the Stripe dashboard.
- The booking status changes to
cancelledregardless of refund outcome.
See the Payments guide for more on paid bookings.
Rescheduling a booking
Reschedule a booking by sending a POST request to /v1/bookings/{id}/reschedule with the new start time. Like cancellation, this supports both cancel token and API key authentication.
With a cancel token (invitee self-service)
curl -X POST "https://api.astrocal.dev/v1/bookings/bk_123/reschedule?token=abc123" \
-H "Content-Type: application/json" \
-d '{
"new_start_time": "2026-03-16T10:00:00Z",
"reason": "Need to move to Tuesday"
}'const response = await fetch(
"https://api.astrocal.dev/v1/bookings/bk_123/reschedule?token=abc123",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
new_start_time: "2026-03-16T10:00:00Z",
reason: "Need to move to Tuesday",
}),
}
);
const data = await response.json();With an API key (developer)
curl -X POST https://api.astrocal.dev/v1/bookings/bk_123/reschedule \
-H "Authorization: Bearer ac_live_..." \
-H "Content-Type: application/json" \
-d '{"new_start_time": "2026-03-16T10:00:00Z"}'const response = await fetch(
"https://api.astrocal.dev/v1/bookings/bk_123/reschedule",
{
method: "POST",
headers: {
Authorization: "Bearer ac_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ new_start_time: "2026-03-16T10:00:00Z" }),
}
);
const data = await response.json();Try it in the API playground →
Reschedule behavior
- The new time slot is validated against availability rules and existing bookings. The current booking's slot is excluded from conflict checking (since it's being replaced).
- The booking's
start_timeandend_timeare updated. The booking ID and cancel token remain the same. - The calendar event is updated with the new times (for all connected calendar providers).
- Reschedule emails with an updated
.icsattachment are sent to both parties. - A
booking.rescheduledwebhook event is dispatched. - You cannot reschedule a cancelled booking. You'll get a
409 Conflict. - The
new_start_timemust be in the future. Past times return a422error.
Listing bookings
List bookings for your organization with optional filters:
curl https://api.astrocal.dev/v1/bookings?status=confirmed&limit=10 \
-H "Authorization: Bearer ac_live_..."const response = await fetch(
"https://api.astrocal.dev/v1/bookings?status=confirmed&limit=10",
{
headers: {
Authorization: "Bearer ac_live_...",
},
}
);
const data = await response.json();Supports filtering by event_type_id, status, start_date, end_date, and assigned_host_id. Uses cursor-based pagination with starting_after.
Round-robin bookings
When an event type uses a round-robin assignment strategy, each booking is automatically assigned to a host. The booking response includes an assigned_host object:
{
"assigned_host": {
"member_id": "mem_abc123",
"name": null,
"email": "host@company.com"
}
}Filter bookings by host using the assigned_host_id query parameter:
curl "https://api.astrocal.dev/v1/bookings?assigned_host_id=mem_abc123" \
-H "Authorization: Bearer ac_live_..."const response = await fetch(
"https://api.astrocal.dev/v1/bookings?assigned_host_id=mem_abc123",
{
headers: {
Authorization: "Bearer ac_live_...",
},
}
);
const data = await response.json();Try it in the API playground →
For event types without round-robin, assigned_host is null.
Common patterns
Check availability before booking
Always query availability before presenting slots to the user. This prevents 409 errors from race conditions:
- Check available slots
"https://api.astrocal.dev/v1/availability?event_type_id=evt_abc123&start=2026-03-15&end=2026-03-21&timezone=America/New_York""https://api.astrocal.dev/v1/availability?event_type_id=evt_abc123&start=2026-03-15&end=2026-03-21&timezone=America/New_York"
); const data = await response.json(); ```
</Tab>
</Tabs>
_[Try it in the API playground →](/docs/api-reference/availability/v1/availability/get)_
2. Book one of the returned slots
<Tabs items={["curl", "TypeScript"]}>
<Tab value="curl">
```bash
curl -X POST https://api.astrocal.dev/v1/bookings \
-H "Content-Type: application/json" \
-d '{
"event_type_id": "evt_abc123",
"start_time": "2026-03-17T14:00:00Z",
"invitee_name": "Jane Doe",
"invitee_email": "jane@example.com",
"invitee_timezone": "America/New_York"
}'const response = await fetch("https://api.astrocal.dev/v1/bookings", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
event_type_id: "evt_abc123",
start_time: "2026-03-17T14:00:00Z",
invitee_name: "Jane Doe",
invitee_email: "jane@example.com",
invitee_timezone: "America/New_York",
}),
});
const data = await response.json();Try it in the API playground →
Handle double-booking gracefully
Even with availability checks, race conditions can occur. Always handle 409 Conflict responses by refreshing slots and asking the user to pick again.
Store the cancel token
The cancel_token in the booking response lets invitees manage their own bookings. Store it alongside the booking ID if you're building a custom UI:
{
"id": "bk_abc123",
"cancel_token": "ctk_xyz789",
"status": "confirmed",
...
}Use the token for cancel and reschedule requests without needing an API key.
Authentication summary
| Endpoint | Auth required | Methods |
|---|---|---|
POST /v1/bookings | None (public) | - |
POST /v1/bookings/:id/cancel | Cancel token OR API key | ?token= or Bearer |
POST /v1/bookings/:id/reschedule | Cancel token OR API key | ?token= or Bearer |
GET /v1/bookings | API key | Bearer |
GET /v1/bookings/:id | API key | Bearer |
Error handling
| Status | Error Code | Description |
|---|---|---|
| 401 | unauthorized | Invalid or missing cancel token / API key |
| 404 | not_found | Booking does not exist |
| 409 | conflict | Time slot is already booked or no longer available |
| 409 | booking_cap_reached | Event type has reached its booking cap for the current period |
| 409 | already_cancelled | Cannot reschedule a cancelled booking |
| 422 | validation_error | Invalid input (e.g., new_start_time is in the past) |
Next steps
- Availability -- Configure availability rules and query bookable slots
- Payments -- Collect payments with Stripe when bookings are created
- Webhooks -- Get notified on booking lifecycle events
- Calendars -- Connect calendars for automatic busy-time blocking and event sync
- API Reference -- Full endpoint documentation