payload-reservepayload-reserve

Examples

Real-world use case configurations — salon, hotel, restaurant, event venue — plus Stripe and email integration patterns.

Real-world use case configurations and integration patterns.

Use Cases

Salon / Barbershop

Staff are resources, services are treatments. Each stylist has their own recurring weekly schedule.

payloadReserve({
  adminGroup: 'Salon',
  defaultBufferTime: 0,
  cancellationNoticePeriod: 24,
  slugs: {
    resources: 'stylists',
    services: 'treatments',
    reservations: 'appointments',
  },
})

Typical services: duration: 30, durationType: 'fixed', bufferTimeAfter: 10


Hotel

Rooms are resources with quantity equal to the number of identical rooms of that type. Each guest stays for a full calendar day — use full-day duration.

payloadReserve({
  adminGroup: 'Hotel',
  cancellationNoticePeriod: 48,
  slugs: {
    resources: 'rooms',
    services: 'room-types',
    reservations: 'bookings',
  },
})
 
// Room type service
await payload.create({
  collection: 'room-types',
  data: {
    name: 'Standard Double',
    duration: 1440,
    durationType: 'full-day',
    price: 149.00,
  },
})
 
// Room resource (10 identical standard doubles)
await payload.create({
  collection: 'rooms',
  data: {
    name: 'Standard Double',
    services: [standardDoubleId],
    quantity: 10,
    capacityMode: 'per-reservation',
  },
})

Restaurant / Event Space

Group bookings where total guest count matters. Use per-guest capacity mode with a maximum party size enforced via guestCount.

payloadReserve({
  adminGroup: 'Restaurant',
  cancellationNoticePeriod: 2,
})
 
// Dining room resource (max 60 guests total)
await payload.create({
  collection: 'resources',
  data: {
    name: 'Main Dining Room',
    services: [diningServiceId],
    quantity: 60,
    capacityMode: 'per-guest',
  },
})

Bookings with guestCount: 4 occupy 4 of the 60 total seats. The room is full when total booked guests reach 60.


Event Venue (Custom Status Machine)

Events go through an approval workflow before being confirmed. Use a custom status machine.

payloadReserve({
  adminGroup: 'Events',
  statusMachine: {
    statuses: ['enquiry', 'quote-sent', 'deposit-paid', 'confirmed', 'completed', 'cancelled'],
    defaultStatus: 'enquiry',
    terminalStatuses: ['completed', 'cancelled'],
    blockingStatuses: ['deposit-paid', 'confirmed'],
    transitions: {
      enquiry: ['quote-sent', 'cancelled'],
      'quote-sent': ['deposit-paid', 'cancelled'],
      'deposit-paid': ['confirmed', 'cancelled'],
      confirmed: ['completed', 'cancelled'],
      completed: [],
      cancelled: [],
    },
  },
  hooks: {
    afterStatusChange: [
      async ({ doc, newStatus }) => {
        if (newStatus === 'quote-sent') {
          await sendQuoteEmail(doc)
        }
        if (newStatus === 'confirmed') {
          await sendContractEmail(doc)
        }
      },
    ],
  },
})

Integration Patterns

Stripe Payment Gate

Hold the time slot with a pending reservation while the customer pays. Confirm on successful payment.

// 1. Create reservation on your booking page (slot is held, conflict detection runs)
const reservation = await payload.create({
  collection: 'reservations',
  data: {
    service: serviceId,
    resource: resourceId,
    customer: customerId,
    startTime: selectedSlot,
  },
  // status defaults to 'pending'
})
 
// 2. Create a Stripe Checkout Session with the reservation ID in metadata
const session = await stripe.checkout.sessions.create({
  line_items: [{ price: stripePriceId, quantity: 1 }],
  metadata: { reservationId: String(reservation.id) },
  mode: 'payment',
  success_url: `${process.env.NEXT_PUBLIC_URL}/booking/success`,
  cancel_url: `${process.env.NEXT_PUBLIC_URL}/booking/cancel`,
})
 
// 3. In your Stripe webhook handler, confirm the reservation
// app/api/stripe-webhook/route.ts
export async function POST(req: Request) {
  const body = await req.text()
  const sig = req.headers.get('stripe-signature')!
  const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
 
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.Checkout.Session
    const reservationId = session.metadata?.reservationId
 
    if (reservationId) {
      const payload = await getPayload({ config })
      await payload.update({
        collection: 'reservations',
        id: reservationId,
        data: { status: 'confirmed' },
        context: { skipReservationHooks: false }, // hooks run — validates transition
      })
    }
  }
 
  return new Response('OK', { status: 200 })
}

Email Notifications

Use afterBookingCreate and afterStatusChange hooks to send transactional emails:

payloadReserve({
  hooks: {
    afterBookingCreate: [
      async ({ doc, req }) => {
        const customer = await req.payload.findByID({
          collection: 'customers',
          id: doc.customer as string,
          depth: 0,
          req,
        })
        await sendEmail({
          subject: 'Booking received',
          template: 'booking-created',
          to: customer.email as string,
          variables: { bookingId: doc.id, startTime: doc.startTime },
        })
      },
    ],
    afterStatusChange: [
      async ({ doc, newStatus, req }) => {
        if (newStatus === 'confirmed') {
          await sendEmail({ template: 'booking-confirmed', variables: doc })
        }
        if (newStatus === 'cancelled') {
          await sendEmail({ template: 'booking-cancelled', variables: doc })
        }
      },
    ],
  },
})

Multi-Tenant Deployments

Scope all queries to a tenant using beforeBookingCreate to inject tenant metadata, and access control functions to filter by tenant:

payloadReserve({
  access: {
    reservations: {
      read: ({ req }) => {
        if (!req.user) {return false}
        return { tenant: { equals: req.user.tenant } }
      },
      create: ({ req }) => !!req.user,
    },
  },
  hooks: {
    beforeBookingCreate: [
      async ({ data, req }) => {
        // Inject tenant ID from the authenticated user
        return { ...data, tenant: req.user?.tenant }
      },
    ],
  },
})

On this page