Documentation is currently in beta. Report issues →

Admin API

Administrative endpoints for managing the platform. All endpoints require admin authentication.

⚠️

Admin endpoints require ADMIN or SUPER_ADMIN role. Unauthorized access returns 403 Forbidden.

Listings Management

List All Listings (Admin)

GET /api/admin/listings

Query Parameters

ParameterTypeDefaultDescription
statusstring-Filter by status
typestring-Filter by type
featuredboolean-Filter featured only
pagenumber1Page number
limitnumber20Items per page

Response

{
  "data": [
    {
      "id": "clx123...",
      "title": "Luxury Beach Villa",
      "status": "PUBLISHED",
      "type": "ACCOMMODATION",
      "region": "Greater Accra",
      "priceGHS": "850.00",
      "featured": true,
      "viewCount": 1250,
      "bookingCount": 45,
      "createdAt": "2024-01-15T10:30:00Z"
    }
  ],
  "pagination": { ... },
  "stats": {
    "total": 156,
    "published": 142,
    "draft": 10,
    "pending": 4
  }
}

Create Listing

POST /api/admin/listings

Request Body

{
  "type": "ACCOMMODATION",
  "title": "Mountain Lodge Retreat",
  "description": "A peaceful mountain getaway...",
  "highlights": ["Mountain views", "Private pool", "Chef service"],
  "region": "Ashanti",
  "city": "Kumasi",
  "area": "Lake Bosomtwe",
  "address": "Lake Bosomtwe Road",
  "latitude": 6.5091,
  "longitude": -1.4093,
  "priceGHS": 650,
  "propertyType": "VILLA",
  "bedrooms": 4,
  "bathrooms": 3,
  "maxGuests": 8,
  "checkInTime": "14:00",
  "checkOutTime": "11:00",
  "amenityIds": ["clxABC...", "clxDEF..."],
  "featured": false
}

Response

{
  "data": {
    "id": "clx456...",
    "slug": "mountain-lodge-retreat-kumasi",
    "status": "DRAFT",
    ...
  }
}

Update Listing

PATCH /api/admin/listings/{id}

Request Body

{
  "title": "Updated Title",
  "priceGHS": 750,
  "status": "PUBLISHED"
}

Delete Listing

DELETE /api/admin/listings/{id}
⚠️

Deleting a listing with active bookings will fail. Cancel or complete bookings first.


Upload Listing Images

POST /api/admin/listings/{id}/images
Content-Type: multipart/form-data

Request

files[]: (binary)
files[]: (binary)

Response

{
  "data": [
    {
      "id": "clxIMG1...",
      "url": "https://res.cloudinary.com/...",
      "publicId": "y3nko/listings/abc123",
      "isPrimary": true,
      "order": 0
    }
  ]
}

Bookings Management

List All Bookings (Admin)

GET /api/admin/bookings

Query Parameters

ParameterTypeDefaultDescription
statusstring-Filter by status
startDatestring-Start of date range
endDatestring-End of date range
listingIdstring-Filter by listing
pagenumber1Page number
limitnumber20Items per page

Response

{
  "data": [
    {
      "id": "clx789...",
      "reference": "Y3K-ABC123",
      "status": "CONFIRMED",
      "checkIn": "2024-03-15",
      "checkOut": "2024-03-18",
      "total": "3123.75",
      "user": {
        "id": "clxUSR...",
        "email": "guest@example.com",
        "profile": { "firstName": "Kwame", "lastName": "Asante" }
      },
      "listing": {
        "id": "clx123...",
        "title": "Luxury Beach Villa"
      },
      "createdAt": "2024-02-20T10:30:00Z"
    }
  ],
  "pagination": { ... },
  "stats": {
    "total": 250,
    "pending": 12,
    "confirmed": 180,
    "completed": 45,
    "cancelled": 13,
    "revenue": "485250.00"
  }
}

Update Booking Status

PATCH /api/admin/bookings/{reference}

Request Body

{
  "status": "CONFIRMED",
  "note": "Manually confirmed after verification"
}

Admin Cancel Booking

POST /api/admin/bookings/{reference}/cancel

Request Body

{
  "reason": "Property unavailable due to maintenance",
  "refundFull": true
}

Users Management

List Users

GET /api/admin/users

Query Parameters

ParameterTypeDefaultDescription
rolestring-Filter by role
searchstring-Search by name/email
pagenumber1Page number
limitnumber20Items per page

Response

{
  "data": [
    {
      "id": "clxUSR...",
      "email": "user@example.com",
      "role": "USER",
      "createdAt": "2024-01-10T08:00:00Z",
      "profile": {
        "firstName": "Kwame",
        "lastName": "Asante",
        "avatarUrl": "..."
      },
      "_count": {
        "bookings": 5,
        "favorites": 12
      }
    }
  ],
  "pagination": { ... }
}

Update User Role

PATCH /api/admin/users/{id}/role

Request Body

{
  "role": "HOST"
}

Only SUPER_ADMIN can promote users to ADMIN role.


Analytics

Dashboard Stats

GET /api/admin/stats

Response

{
  "data": {
    "users": {
      "total": 1250,
      "newThisMonth": 85,
      "growth": 7.2
    },
    "bookings": {
      "total": 450,
      "thisMonth": 42,
      "revenue": "125000.00",
      "avgBookingValue": "2778.00"
    },
    "listings": {
      "total": 156,
      "published": 142,
      "avgOccupancy": 68.5
    },
    "recentBookings": [ ... ],
    "topListings": [ ... ]
  }
}

Server Action Implementation

// lib/actions/admin.ts
"use server"
 
import { requireRole } from '@/lib/auth'
import { prisma } from '@/lib/db'
import { revalidatePath } from 'next/cache'
 
export async function createListing(data: CreateListingInput) {
  await requireRole(['ADMIN', 'SUPER_ADMIN'])
 
  const slug = generateSlug(data.title, data.city)
 
  const listing = await prisma.listing.create({
    data: {
      ...data,
      slug,
      status: 'DRAFT',
      amenities: {
        create: data.amenityIds.map(id => ({
          amenityId: id
        }))
      }
    }
  })
 
  revalidatePath('/admin/listings')
  return listing
}
 
export async function updateListingStatus(
  listingId: string,
  status: ListingStatus
) {
  await requireRole(['ADMIN', 'SUPER_ADMIN'])
 
  const listing = await prisma.listing.update({
    where: { id: listingId },
    data: {
      status,
      publishedAt: status === 'PUBLISHED' ? new Date() : undefined
    }
  })
 
  revalidatePath('/admin/listings')
  revalidatePath(`/accommodations/${listing.slug}`)
  return listing
}
 
export async function getAdminStats() {
  await requireRole(['ADMIN', 'SUPER_ADMIN'])
 
  const [users, bookings, listings, revenue] = await prisma.$transaction([
    prisma.user.count(),
    prisma.booking.count(),
    prisma.listing.count({ where: { status: 'PUBLISHED' } }),
    prisma.booking.aggregate({
      _sum: { total: true },
      where: { status: 'CONFIRMED' }
    })
  ])
 
  return {
    users,
    bookings,
    listings,
    revenue: revenue._sum.total || 0
  }
}