Documentation is currently in beta. Report issues →

Bookings API

Endpoints for creating and managing bookings.

All booking endpoints require authentication.

Create Booking

Create a new booking for a listing.

POST /api/bookings

Request Body

{
  "listingId": "clx123abc...",
  "checkIn": "2024-03-15",
  "checkOut": "2024-03-18",
  "adults": 2,
  "children": 1,
  "guestName": "Kwame Asante",
  "guestEmail": "kwame@example.com",
  "guestPhone": "+233551234567",
  "specialRequests": "Late check-in around 8pm"
}

Request Schema

FieldTypeRequiredDescription
listingIdstringYesListing CUID
checkInstringYesCheck-in date (ISO format)
checkOutstringYesCheck-out date (ISO format)
adultsnumberYesNumber of adults (1-20)
childrennumberNoNumber of children (0-10)
guestNamestringYesGuest full name
guestEmailstringYesGuest email
guestPhonestringYesPhone with +233 prefix
specialRequestsstringNoSpecial requests (max 1000 chars)

Response

{
  "data": {
    "booking": {
      "id": "clx789xyz...",
      "reference": "Y3K-ABC123",
      "status": "PENDING",
      "listingId": "clx123abc...",
      "checkIn": "2024-03-15",
      "checkOut": "2024-03-18",
      "nights": 3,
      "adults": 2,
      "children": 1,
      "pricePerNight": "850.00",
      "subtotal": "2550.00",
      "serviceFee": "255.00",
      "taxes": "318.75",
      "total": "3123.75",
      "currency": "GHS",
      "createdAt": "2024-02-20T10:30:00Z"
    },
    "paymentUrl": "https://checkout.paystack.com/abc123"
  }
}

Errors

StatusCodeDescription
400VALIDATION_ERRORInvalid input data
401UNAUTHORIZEDAuthentication required
404NOT_FOUNDListing not found
409DATES_UNAVAILABLESelected dates not available

List User Bookings

Get all bookings for the authenticated user.

GET /api/bookings

Query Parameters

ParameterTypeDefaultDescription
statusstring-Filter by status
pagenumber1Page number
limitnumber10Items per page

Response

{
  "data": [
    {
      "id": "clx789xyz...",
      "reference": "Y3K-ABC123",
      "status": "CONFIRMED",
      "checkIn": "2024-03-15",
      "checkOut": "2024-03-18",
      "nights": 3,
      "total": "3123.75",
      "listing": {
        "id": "clx123abc...",
        "title": "Luxury Beach Villa",
        "slug": "luxury-beach-villa-accra",
        "images": [{ "url": "...", "isPrimary": true }]
      },
      "createdAt": "2024-02-20T10:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 5,
    "totalPages": 1,
    "hasMore": false
  }
}

Get Booking Details

Get complete details for a specific booking.

GET /api/bookings/{reference}

Path Parameters

ParameterTypeDescription
referencestringBooking reference (e.g., “Y3K-ABC123”)

Response

{
  "data": {
    "id": "clx789xyz...",
    "reference": "Y3K-ABC123",
    "status": "CONFIRMED",
    "checkIn": "2024-03-15",
    "checkOut": "2024-03-18",
    "nights": 3,
    "adults": 2,
    "children": 1,
    "pricePerNight": "850.00",
    "subtotal": "2550.00",
    "serviceFee": "255.00",
    "taxes": "318.75",
    "total": "3123.75",
    "currency": "GHS",
    "guestName": "Kwame Asante",
    "guestEmail": "kwame@example.com",
    "guestPhone": "+233551234567",
    "specialRequests": "Late check-in around 8pm",
    "confirmedAt": "2024-02-20T10:35:00Z",
    "listing": {
      "id": "clx123abc...",
      "title": "Luxury Beach Villa",
      "slug": "luxury-beach-villa-accra",
      "address": "123 Beach Road, Labadi",
      "checkInTime": "14:00",
      "checkOutTime": "11:00",
      "images": [{ "url": "...", "isPrimary": true }]
    },
    "payments": [
      {
        "id": "clx999...",
        "status": "COMPLETED",
        "amount": "3123.75",
        "method": "MOBILE_MONEY",
        "paidAt": "2024-02-20T10:35:00Z"
      }
    ]
  }
}

Errors

StatusCodeDescription
404NOT_FOUNDBooking not found
403FORBIDDENBooking belongs to another user

Cancel Booking

Cancel an existing booking.

POST /api/bookings/{reference}/cancel

Request Body

{
  "reason": "Change of travel plans"
}

Response

{
  "data": {
    "success": true,
    "booking": {
      "id": "clx789xyz...",
      "reference": "Y3K-ABC123",
      "status": "CANCELLED",
      "cancelledAt": "2024-02-25T14:00:00Z",
      "cancelReason": "Change of travel plans"
    },
    "refund": {
      "eligible": true,
      "amount": "2550.00",
      "currency": "GHS",
      "status": "PROCESSING"
    }
  }
}

Refund Policy

Refunds are calculated based on days until check-in:

Days Before Check-inRefund
7+ daysFull subtotal refund
3-6 days50% of subtotal
< 3 daysNo refund
⚠️

Service fees are non-refundable. Taxes may be refunded proportionally.

Errors

StatusCodeDescription
400ALREADY_CANCELLEDBooking already cancelled
400CANNOT_CANCELBooking status prevents cancellation
403FORBIDDENNot your booking

Booking Statuses

StatusDescription
PENDINGBooking created, awaiting payment
CONFIRMEDPayment successful, booking active
CANCELLEDBooking cancelled by user or admin
COMPLETEDCheck-out date has passed
NO_SHOWGuest did not arrive

Server Action Usage

For form submissions, prefer server actions:

// lib/actions/bookings.ts
"use server"
 
import { createClient } from '@/lib/supabase/server'
import { prisma } from '@/lib/db'
import { createBookingSchema } from '@/lib/validators'
import { initializePayment } from '@/lib/paystack'
 
export async function createBooking(data: CreateBookingInput) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
 
  if (!user) {
    throw new Error('Please log in to book')
  }
 
  // Validate
  const validated = createBookingSchema.parse(data)
 
  // Check availability
  const listing = await prisma.listing.findUnique({
    where: { id: validated.listingId }
  })
 
  if (!listing) {
    throw new Error('Listing not found')
  }
 
  // Calculate pricing
  const pricing = calculatePricing({
    pricePerNight: Number(listing.priceGHS),
    nights: calculateNights(validated.checkIn, validated.checkOut),
    currency: 'GHS'
  })
 
  // Create booking
  const booking = await prisma.booking.create({
    data: {
      reference: generateReference('Y3K'),
      userId: user.id,
      listingId: listing.id,
      checkIn: new Date(validated.checkIn),
      checkOut: new Date(validated.checkOut),
      nights: pricing.nights,
      adults: validated.adults,
      children: validated.children || 0,
      pricePerNight: listing.priceGHS,
      subtotal: pricing.subtotal,
      serviceFee: pricing.serviceFee,
      taxes: pricing.taxes,
      total: pricing.total,
      guestName: validated.guestName,
      guestEmail: validated.guestEmail,
      guestPhone: validated.guestPhone,
      specialRequests: validated.specialRequests
    }
  })
 
  // Initialize payment
  const payment = await initializePayment({
    email: validated.guestEmail,
    amount: Math.round(pricing.total * 100),
    reference: booking.reference,
    callbackUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/bookings/${booking.reference}`,
    metadata: {
      bookingId: booking.id,
      userId: user.id,
      listingId: listing.id
    }
  })
 
  return {
    booking,
    paymentUrl: payment.authorizationUrl
  }
}

Usage in Component

"use client"
 
import { createBooking } from '@/lib/actions/bookings'
import { useRouter } from 'next/navigation'
 
function BookingForm({ listing }) {
  const router = useRouter()
 
  async function handleSubmit(formData: FormData) {
    const result = await createBooking({
      listingId: listing.id,
      checkIn: formData.get('checkIn'),
      checkOut: formData.get('checkOut'),
      adults: Number(formData.get('adults')),
      guestName: formData.get('guestName'),
      guestEmail: formData.get('guestEmail'),
      guestPhone: formData.get('guestPhone')
    })
 
    // Redirect to Paystack
    window.location.href = result.paymentUrl
  }
 
  return <form action={handleSubmit}>...</form>
}