Bookings API
Endpoints for creating and managing bookings.
All booking endpoints require authentication.
Create Booking
Create a new booking for a listing.
POST /api/bookingsRequest 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
| Field | Type | Required | Description |
|---|---|---|---|
listingId | string | Yes | Listing CUID |
checkIn | string | Yes | Check-in date (ISO format) |
checkOut | string | Yes | Check-out date (ISO format) |
adults | number | Yes | Number of adults (1-20) |
children | number | No | Number of children (0-10) |
guestName | string | Yes | Guest full name |
guestEmail | string | Yes | Guest email |
guestPhone | string | Yes | Phone with +233 prefix |
specialRequests | string | No | Special 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
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid input data |
| 401 | UNAUTHORIZED | Authentication required |
| 404 | NOT_FOUND | Listing not found |
| 409 | DATES_UNAVAILABLE | Selected dates not available |
List User Bookings
Get all bookings for the authenticated user.
GET /api/bookingsQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | - | Filter by status |
page | number | 1 | Page number |
limit | number | 10 | Items 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
| Parameter | Type | Description |
|---|---|---|
reference | string | Booking 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
| Status | Code | Description |
|---|---|---|
| 404 | NOT_FOUND | Booking not found |
| 403 | FORBIDDEN | Booking belongs to another user |
Cancel Booking
Cancel an existing booking.
POST /api/bookings/{reference}/cancelRequest 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-in | Refund |
|---|---|
| 7+ days | Full subtotal refund |
| 3-6 days | 50% of subtotal |
| < 3 days | No refund |
⚠️
Service fees are non-refundable. Taxes may be refunded proportionally.
Errors
| Status | Code | Description |
|---|---|---|
| 400 | ALREADY_CANCELLED | Booking already cancelled |
| 400 | CANNOT_CANCEL | Booking status prevents cancellation |
| 403 | FORBIDDEN | Not your booking |
Booking Statuses
| Status | Description |
|---|---|
PENDING | Booking created, awaiting payment |
CONFIRMED | Payment successful, booking active |
CANCELLED | Booking cancelled by user or admin |
COMPLETED | Check-out date has passed |
NO_SHOW | Guest 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>
}