Astrocal
Guides

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

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 cancelled and cancelled_at is set.
  • The calendar event is deleted automatically (for all connected calendar providers).
  • Cancellation emails are sent to both the invitee and organizer.
  • A booking.cancelled webhook 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 cancelled regardless 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_time and end_time are 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 .ics attachment are sent to both parties.
  • A booking.rescheduled webhook event is dispatched.
  • You cannot reschedule a cancelled booking. You'll get a 409 Conflict.
  • The new_start_time must be in the future. Past times return a 422 error.

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:

  1. 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

EndpointAuth requiredMethods
POST /v1/bookingsNone (public)-
POST /v1/bookings/:id/cancelCancel token OR API key?token= or Bearer
POST /v1/bookings/:id/rescheduleCancel token OR API key?token= or Bearer
GET /v1/bookingsAPI keyBearer
GET /v1/bookings/:idAPI keyBearer

Error handling

StatusError CodeDescription
401unauthorizedInvalid or missing cancel token / API key
404not_foundBooking does not exist
409conflictTime slot is already booked or no longer available
409booking_cap_reachedEvent type has reached its booking cap for the current period
409already_cancelledCannot reschedule a cancelled booking
422validation_errorInvalid 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

On this page