endpoints.md 4.8 KB


title: Custom Endpoints description: Custom REST API endpoints with authentication and helpers

tags: [payload, endpoints, api, routes, webhooks]

Payload Custom Endpoints

Basic Endpoint Pattern

Custom endpoints are not authenticated by default. Always check req.user.

import { APIError } from 'payload'
import type { Endpoint } from 'payload'

export const protectedEndpoint: Endpoint = {
  path: '/protected',
  method: 'get',
  handler: async (req) => {
    if (!req.user) {
      throw new APIError('Unauthorized', 401)
    }

    // Use req.payload for database operations
    const data = await req.payload.find({
      collection: 'posts',
      where: { author: { equals: req.user.id } },
    })

    return Response.json(data)
  },
}

Route Parameters

export const trackingEndpoint: Endpoint = {
  path: '/:id/tracking',
  method: 'get',
  handler: async (req) => {
    const { id } = req.routeParams

    const tracking = await getTrackingInfo(id)

    if (!tracking) {
      return Response.json({ error: 'not found' }, { status: 404 })
    }

    return Response.json(tracking)
  },
}

Request Body Handling

// Manual JSON parsing
export const createEndpoint: Endpoint = {
  path: '/create',
  method: 'post',
  handler: async (req) => {
    const data = await req.json()

    const result = await req.payload.create({
      collection: 'posts',
      data,
    })

    return Response.json(result)
  },
}

// Using helper (handles JSON + files)
import { addDataAndFileToRequest } from 'payload'

export const uploadEndpoint: Endpoint = {
  path: '/upload',
  method: 'post',
  handler: async (req) => {
    await addDataAndFileToRequest(req)

    // req.data contains parsed body
    // req.file contains uploaded file (if multipart)

    const result = await req.payload.create({
      collection: 'media',
      data: req.data,
      file: req.file,
    })

    return Response.json(result)
  },
}

Query Parameters

export const searchEndpoint: Endpoint = {
  path: '/search',
  method: 'get',
  handler: async (req) => {
    const url = new URL(req.url)
    const query = url.searchParams.get('q')
    const limit = parseInt(url.searchParams.get('limit') || '10')

    const results = await req.payload.find({
      collection: 'posts',
      where: {
        title: {
          contains: query,
        },
      },
      limit,
    })

    return Response.json(results)
  },
}

CORS Headers

import { headersWithCors } from 'payload'

export const corsEndpoint: Endpoint = {
  path: '/public-data',
  method: 'get',
  handler: async (req) => {
    const data = await fetchPublicData()

    return Response.json(data, {
      headers: headersWithCors({
        headers: new Headers(),
        req,
      }),
    })
  },
}

Error Handling

import { APIError } from 'payload'

export const validateEndpoint: Endpoint = {
  path: '/validate',
  method: 'post',
  handler: async (req) => {
    const data = await req.json()

    if (!data.email) {
      throw new APIError('Email is required', 400)
    }

    return Response.json({ valid: true })
  },
}

Endpoint Placement

Collection Endpoints

Mounted at /api/{collection-slug}/{path}.

export const Orders: CollectionConfig = {
  slug: 'orders',
  endpoints: [
    {
      path: '/:id/tracking',
      method: 'get',
      handler: async (req) => {
        // Available at: /api/orders/:id/tracking
        const orderId = req.routeParams.id
        return Response.json({ orderId })
      },
    },
  ],
}

Global Endpoints

Mounted at /api/globals/{global-slug}/{path}.

export const Settings: GlobalConfig = {
  slug: 'settings',
  endpoints: [
    {
      path: '/clear-cache',
      method: 'post',
      handler: async (req) => {
        // Available at: /api/globals/settings/clear-cache
        await clearCache()
        return Response.json({ message: 'Cache cleared' })
      },
    },
  ],
}

Root Endpoints

Mounted at /api/{path}.

export default buildConfig({
  endpoints: [
    {
      path: '/hello',
      method: 'get',
      handler: () => {
        // Available at: /api/hello
        return Response.json({ message: 'Hello!' })
      },
    },
  ],
})

Best Practices

  1. Always check authentication - Custom endpoints are not authenticated by default
  2. Use req.payload for operations - Ensures access control and hooks execute
  3. Use helpers for common tasks - addDataAndFileToRequest, headersWithCors
  4. Throw APIError for errors - Provides consistent error responses
  5. Return Web API Response - Use Response.json() for consistent responses
  6. Validate input - Check required fields, validate types
  7. Log errors - Use req.payload.logger for debugging