Skip to content

Working with Time Slots

With externalBooking: true the calendar stops after the visitor picks a time slot: the built-in form does not open, the "Confirm" button under the slot list disappears, and the component emits time-slot-changed. Your app renders its own call to action and submits the booking with the exported functions.

vue
<template>
  <ZaptimeCalendar :config="config" @time-slot-changed="(s) => (slot = s)" />

  <form v-if="slot" @submit.prevent="submit">
    <p>{{ slot.start }} – {{ slot.end }}</p>
    <input v-model="form.firstName" placeholder="First name" />
    <input v-model="form.lastName" placeholder="Last name" />
    <input v-model="form.email" type="email" required placeholder="Email" />
    <input v-model="form.phone" type="tel" placeholder="Phone" />
    <p v-if="error" role="alert">{{ error }}</p>
    <button type="submit" :disabled="submitting">Book</button>
  </form>

  <p v-if="booked">Booked. Confirmation sent to {{ booked.data.userEmail }}.</p>
</template>

<script setup lang="ts">
import { reactive, ref } from "vue";
import { ZaptimeCalendar, book } from "@zaptime/vue3";
import type { ZaptimeConfig } from "@zaptime/vue3";
import { SlotNoLongerAvailableError, slotNoLongerAvailableText, useCalendar } from "@zaptime/core";
import type { ReservationResponse, TimeSlot } from "@zaptime/core";

const config: ZaptimeConfig = {
  token: "<API_TOKEN>",
  externalBooking: true,
};

const slot = ref<TimeSlot | undefined>();
const form = reactive({ firstName: "", lastName: "", email: "", phone: "" });
const submitting = ref(false);
const error = ref<string | undefined>();
const booked = ref<ReservationResponse | undefined>();

const { getDays } = useCalendar();

async function submit() {
  submitting.value = true;
  error.value = undefined;
  try {
    const res = await book({ ...form });
    if (res.success) {
      booked.value = res;
      slot.value = undefined;
    } else {
      error.value = "Please check the entered details.";
    }
  } catch (e) {
    if (e instanceof SlotNoLongerAvailableError) {
      error.value = slotNoLongerAvailableText(config.locale);
      slot.value = undefined;
      await getDays(); // refresh availability
    } else {
      error.value = "Booking failed. Please try again.";
    }
  } finally {
    submitting.value = false;
  }
}
</script>

All functions read the token, API base URL, selected time slot and timezone from the calendar state. Pass calendarId when the calendar was rendered with a calendar-id.

Book

Books the selected time slot immediately.

ts
book({
  email: string,
  firstName?: string,
  lastName?: string,
  phone?: string,
  seats?: number,             // defaults to 1
  location?: Location,        // defaults to the event type's first location
  customFields?: CustomFieldCollected[],
  guests?: string[],          // guest email addresses
  calendarId?: string,
}): Promise<ReservationResponse>

Throws SlotNoLongerAvailableError when the API answers 409 (the slot was taken in the meantime). Throws a generic Error when no time slot is selected or the request fails. When the event type has redirectAfterBookingUrl set, book navigates to it after a successful call.

Reserve

Holds the selected time slot without booking it. The hold is refreshed automatically every 15 minutes while the page stays open. Use it for multi-step flows: reserve the slot, collect payment or more details, then confirm.

ts
reserve({
  email: string,
  firstName?: string,
  lastName?: string,
  phone?: string,
  seats?: number,
  location?: Location,
  customFields?: CustomFieldCollected[],
  guests?: string[],
  calendarId?: string,
}): Promise<ReservationResponse>

Same errors as book.

Confirm

Confirms a previously reserved time slot and stops the refresh. Details passed here are merged into the reservation.

ts
confirm({
  calendarId?: string,
  firstName?: string,
  lastName?: string,
  phone?: string,
  customFields?: CustomFieldCollected[],
  guests?: string[],
}): Promise<ReservationResponse>

Throws when nothing was reserved for the given calendarId.

Cancel

Cancels a previously reserved time slot and stops the refresh. Resolves to false when nothing was reserved.

ts
cancel(calendarId?: string): Promise<boolean>

Reschedule

Moves an existing reservation to the selected time slot. Requires the calendar to be initialized with reservationUuid in the config. Imported from @zaptime/core.

ts
import { reschedule, RescheduleNotAllowedError } from "@zaptime/core";

reschedule(calendarId?: string): Promise<ReservationResponse>

Throws RescheduleNotAllowedError when the API answers 403 (notice period violated, reservation already started, or rescheduling disabled). See Rescheduling.

Stop the reservation refresh

Call this on unmount when a reservation may still be in flight, so the interval does not keep running in the background.

ts
import { stopReservationRefresh } from "@zaptime/core";

onUnmounted(() => stopReservationRefresh());

Reserve and confirm flow

ts
import { reserve, confirm, cancel } from "@zaptime/vue3";

const reservation = await reserve({ email: "john@doe.test" });

try {
  await chargeCustomer(reservation.data.uuid);
  await confirm({ firstName: "John", lastName: "Doe" });
} catch {
  await cancel();
}

Custom fields

Custom fields are identified by the UUIDs of the booking form fields defined in the dashboard. See Custom Fields for where to find them. To render the dashboard-defined form yourself, use the useBookingForm composable, which knows every field's type, label, required flag and options.

Error messages

@zaptime/core exports two helpers that return the localized message for the given locale, with an English fallback:

ts
import { slotNoLongerAvailableText, rescheduleNotAllowedText } from "@zaptime/core";

slotNoLongerAvailableText(config.locale);
rescheduleNotAllowedText(config.locale);

The texts can be overridden in locale.confirmationForm.slotNoLongerAvailable and locale.confirmationForm.rescheduleNotAllowed.