Astrocal
Guides

Waitlist

Let invitees queue for fully-booked slots and get promoted automatically when someone cancels.

A waitlist holds invitees who want a slot that is already taken. When a booking for that slot is cancelled, Astrocal promotes the first matching entry into a real booking automatically.

Prerequisites

Joining and cancelling need no API key.

Enabling the waitlist

Waitlists are configured per event type:

FieldTypeDefaultDescription
waitlist_enabledbooleanfalseWhether invitees may join the waitlist
waitlist_maxnumber50Maximum entries in waiting status, 1-500
waitlist_ttl_daysnumber7Days before an unpromoted entry expires, 1-30
curl -X PATCH https://api.astrocal.dev/v1/event-types/YOUR_EVENT_TYPE_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "waitlist_enabled": true,
    "waitlist_max": 25,
    "waitlist_ttl_days": 14
  }'
const response = await fetch(
  "https://api.astrocal.dev/v1/event-types/YOUR_EVENT_TYPE_ID",
  {
    method: "PATCH",
    headers: {
      Authorization: "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      waitlist_enabled: true,
      waitlist_max: 25,
      waitlist_ttl_days: 14,
    }),
  }
);
const data = await response.json();

Try it in the API playground →

waitlist_max counts every entry in waiting status across the whole event type, not per slot.

Joining a waitlist

Send a POST to /v1/waitlist. This is a public endpoint with no authentication, so your end users can join directly.

curl -X POST https://api.astrocal.dev/v1/waitlist \
  -H "Content-Type: application/json" \
  -d '{
    "event_type_id": "660e8400-e29b-41d4-a716-446655440000",
    "start_time": "2026-03-15T14:00:00Z",
    "invitee_name": "Jane Doe",
    "invitee_email": "jane@example.com",
    "invitee_timezone": "America/Los_Angeles",
    "notes": "Happy to take a short-notice slot"
  }'
const response = await fetch("https://api.astrocal.dev/v1/waitlist", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    event_type_id: "660e8400-e29b-41d4-a716-446655440000",
    start_time: "2026-03-15T14:00:00Z",
    invitee_name: "Jane Doe",
    invitee_email: "jane@example.com",
    invitee_timezone: "America/Los_Angeles",
    notes: "Happy to take a short-notice slot",
  }),
});
const data = await response.json();

Try it in the API playground →

Request fields

FieldTypeRequiredDescription
event_type_idstringYesThe event type to queue for
invitee_namestringYesInvitee name, 1-200 characters
invitee_emailstringYesInvitee email. Stored lowercased.
start_timestringNoISO 8601 slot to wait for. Omit to wait for any slot.
durationnumberNoMeeting length in minutes, 5-480. Use with duration_options.
invitee_timezonestringNoIANA timezone. Defaults to UTC.
notesstringNoFree text, max 1000 characters
metadataobjectNoYour own data. Same limits as booking metadata.

Response

{
  "id": "wl_abc123",
  "event_type_id": "660e8400-e29b-41d4-a716-446655440000",
  "start_time": "2026-03-15T14:00:00.000Z",
  "duration_minutes": null,
  "status": "waiting",
  "position": 3,
  "invitee_name": "Jane Doe",
  "invitee_email": "jane@example.com",
  "invitee_timezone": "America/Los_Angeles",
  "notes": "Happy to take a short-notice slot",
  "metadata": {},
  "cancel_token": "0Vv7yQ...",
  "booking_id": null,
  "promoted_at": null,
  "expires_at": "2026-03-22T09:12:00.000Z",
  "is_test": false,
  "created_at": "2026-03-08T09:12:00.000Z"
}

Store the cancel_token. It lets the invitee cancel without an API key.

expires_at is the creation time plus the event type's waitlist_ttl_days.

Specific slot or any slot

  • Specific slot: pass start_time. The entry is only considered when that exact slot frees up.
  • Any slot: omit start_time. The entry is considered whenever any slot on that event type frees up, but always after the specific-slot entries for it.

Position

position is 1-based and computed on every read:

  • It counts the waiting entries created before this one.
  • For a specific-slot entry, only entries for that same slot and any-slot entries are counted.
  • Entries that are not waiting always report position 0.

Positions shift as entries ahead are promoted, cancelled, or expired. Read the entry again rather than caching the number.

To show a position before anyone joins, call availability. When an event type is capped and has waitlists enabled, the response returns waitlist_available: true and waitlist_position — the position the next entry would take.

Checking an entry

Fetch a single entry by ID with an API key:

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

Try it in the API playground →

Listing entries

GET /v1/waitlist lists entries for one event type. event_type_id is required:

curl "https://api.astrocal.dev/v1/waitlist?event_type_id=660e8400-e29b-41d4-a716-446655440000&status=waiting&limit=20" \
  -H "Authorization: Bearer ac_live_..."
const response = await fetch(
  "https://api.astrocal.dev/v1/waitlist?event_type_id=660e8400-e29b-41d4-a716-446655440000&status=waiting&limit=20",
  {
    headers: {
      Authorization: "Bearer ac_live_...",
    },
  }
);
const data = await response.json();

Try it in the API playground →

ParameterTypeDescription
event_type_idstringRequired. The event type to list entries for.
statusstringwaiting, promoted, expired, or cancelled
start_timestringOnly entries queued for this exact ISO 8601 slot
limitnumberPage size, 1-100. Defaults to 20.
starting_afterstringCursor for the next page

Entries come back oldest first, which is the order they are considered for promotion.

Cancelling an entry

DELETE /v1/waitlist/{id} accepts either a cancel token or an API key, and returns 204 No Content.

With a cancel token (invitee self-service)

curl -X DELETE "https://api.astrocal.dev/v1/waitlist/wl_abc123?token=0Vv7yQ..."
await fetch("https://api.astrocal.dev/v1/waitlist/wl_abc123?token=0Vv7yQ...", {
  method: "DELETE",
});

With an API key (developer)

curl -X DELETE https://api.astrocal.dev/v1/waitlist/wl_abc123 \
  -H "Authorization: Bearer ac_live_..."
await fetch("https://api.astrocal.dev/v1/waitlist/wl_abc123", {
  method: "DELETE",
  headers: {
    Authorization: "Bearer ac_live_...",
  },
});

Try it in the API playground →

Cancellation behaviour:

  • The entry's status becomes cancelled and it stops being considered for promotion.
  • Cancellation is idempotent. Cancelling an already-cancelled entry returns 204.
  • An entry that belongs to another organization returns 404, not 403.
  • An invalid or missing token returns 401.

When a slot frees up

Promotion runs when a booking is cancelled, and only when the event type has waitlist_enabled. Astrocal:

  1. Collects waiting entries for that event type whose start_time matches the cancelled slot, plus any-slot entries.
  2. Orders them specific-slot first, then any-slot, oldest first.
  3. Creates a booking for the first candidate through the normal booking flow — availability checks, calendar sync, confirmation emails, and a booking.created webhook all apply.
  4. Sets the entry's status to promoted, and fills in booking_id and promoted_at.

The entry's duration_minutes, notes, metadata, and invitee_timezone carry over to the new booking. The invitee receives the standard booking confirmation email.

One entry is promoted per cancellation. If the booking fails — for example the slot was taken again in the meantime — Astrocal tries the next candidate instead.

Poll the entry, or watch for the booking.created webhook, to find out that a promotion happened.

Entry statuses

StatusMeaning
waitingIn the queue and eligible for promotion
promotedConverted into a booking. booking_id and promoted_at are set.
expiredPassed expires_at without being promoted
cancelledCancelled by the invitee or by an API key

Expiry is swept by a background worker every five minutes, so an entry can stay waiting for a few minutes past its expires_at.

Authentication summary

EndpointAuth requiredMethods
POST /v1/waitlistNone (public)-
DELETE /v1/waitlist/:idCancel token OR API key?token= or Bearer
GET /v1/waitlistAPI keyBearer
GET /v1/waitlist/:idAPI keyBearer

Error handling

StatusError codedetails.codeDescription
400validation_error-Invalid input, for example a non-UUID event_type_id or a malformed start_time
400bad_request-Waitlist is not enabled for this event type
401unauthorized-Invalid or missing cancel token / API key
404not_found-Event type or waitlist entry does not exist
409conflictalready_on_waitlistThis email is already waiting for the same slot
409conflictwaitlist_fullThe event type already has waitlist_max entries

Both 409s return the outer code conflict, so branch on details.code:

{
  "error": {
    "code": "conflict",
    "message": "Already on waitlist for this slot",
    "details": {
      "code": "already_on_waitlist"
    }
  }
}

A duplicate is the same email, event type, and start_time in waiting status. Cancelled, expired, and promoted entries do not block a rejoin.

Next steps

  • Bookings -- The booking a promotion creates
  • Availability -- Check waitlist_available before offering the queue
  • Event Types -- Booking caps, the usual reason a waitlist opens
  • Errors -- Full error code reference
  • API Reference -- Full endpoint documentation

On this page