obsidian/wiki/payloadcms/rest-api.md
2026-05-15 15:13:56 +01:00

6.3 KiB

tags topic sources created
payloadcms
tech-patterns
payloadcms
https://payloadcms.com/docs/rest-api/overview
2026-05-15

PayloadCMS — REST API

Overview

Payload auto-generates a fully functional REST API from Collection and Global configs. All routes are prefixed with /api (configurable via routes.api).

  • Collection slugs must be kebab-case
  • Supports: depth, locale, fallback-locale, select, populate, limit, page, sort, where, joins
  • Type-safe client available via @payloadcms/sdk (beta)

Collection Endpoints

Method Path Operation
GET /api/{slug} Find (paginated)
GET /api/{slug}/{id} Find by ID
GET /api/{slug}/count Count documents
POST /api/{slug} Create
PATCH /api/{slug} Update many (with where)
PATCH /api/{slug}/{id} Update by ID
DELETE /api/{slug} Delete many (with where)
DELETE /api/{slug}/{id} Delete by ID

Auth Endpoints (auth-enabled collections)

Method Path Operation
POST /api/{slug}/login Login — returns JWT + user
POST /api/{slug}/logout Logout
POST /api/{slug}/refresh-token Refresh JWT
GET /api/{slug}/me Current user
POST /api/{slug}/forgot-password Trigger password reset
POST /api/{slug}/reset-password Apply new password with token
POST /api/{slug}/verify/{token} Verify email
POST /api/{slug}/unlock Unlock locked account

Global Endpoints

Method Path Operation
GET /api/globals/{slug} Get global
POST /api/globals/{slug} Update global

Query Parameters

Param Type Description
depth number Relationship population depth (default: 2)
locale string Return doc in a specific locale
fallback-locale string Fallback locale
select object Field projection (select[title]=true)
populate object Override defaultPopulate for related docs
limit number Docs per page (default: 10)
page number Page number
sort string Field name, prefix - for descending, comma-separated for multi
where object Filter query (see [[wiki/payloadcms/queries
joins object Custom request per join field

Code Examples

Basic fetch

// Find published posts, sorted newest first
const res = await fetch('/api/posts?where[status][equals]=published&sort=-createdAt&limit=10')
const { docs, totalDocs, totalPages } = await res.json()

Complex query with qs-esm

import { stringify } from 'qs-esm'

const query = stringify({
  where: { color: { equals: 'mint' } },
  select: { title: true, slug: true },
  sort: '-createdAt',
  limit: 20,
}, { addQueryPrefix: true })

const res = await fetch(`/api/posts${query}`)

Auth: login and use token

const { token } = await fetch('/api/users/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'dev@example.com', password: 'secret' }),
}).then(r => r.json())

// Authenticated request
const res = await fetch('/api/posts', {
  headers: { Authorization: `JWT ${token}` },
})

PayloadCMS SDK (@payloadcms/sdk)

Type-safe REST client, API similar to Local API:

import { PayloadSDK } from '@payloadcms/sdk'
import type { Config } from './payload-types'

const sdk = new PayloadSDK<Config>({ baseURL: 'https://example.com/api' })

// Operations
await sdk.find({ collection: 'posts', limit: 10, where: { _status: { equals: 'published' } } })
await sdk.findByID({ collection: 'posts', id })
await sdk.create({ collection: 'posts', data: { text: 'hello' } })
await sdk.update({ collection: 'posts', id, data: { text: 'updated' } })
await sdk.delete({ collection: 'posts', id })
await sdk.findGlobal({ slug: 'header' })
await sdk.updateGlobal({ slug: 'header', data: { text: 'new' } })

// Auth operations
await sdk.login({ collection: 'users', data: { email, password } })
await sdk.me({ collection: 'users' }, { headers: { Authorization: `JWT ${token}` } })
await sdk.refreshToken({ collection: 'users' }, { headers: { Authorization: `JWT ${token}` } })

Custom endpoint

await sdk.request({ method: 'POST', path: '/send-data', json: { id: 1 } })

Custom Endpoints

Add endpoints array to collection, global, or root config:

export const Orders: CollectionConfig = {
  slug: 'orders',
  endpoints: [
    {
      path: '/:id/tracking',
      method: 'get',
      handler: async (req) => {
        const tracking = await getTrackingInfo(req.routeParams.id)
        if (!tracking) return Response.json({ error: 'not found' }, { status: 404 })
        return Response.json(tracking)
      },
    },
  ],
}

Custom endpoint helpers

import { addDataAndFileToRequest, addLocalesToRequestFromData, headersWithCors } from 'payload'

// Parse body + file
await addDataAndFileToRequest(req)    // populates req.data and req.file

// Parse locale params
await addLocalesToRequestFromData(req) // populates req.locale and req.fallbackLocale

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

Method Override (long GET queries)

When query strings are too long, use POST with X-Payload-HTTP-Method-Override: GET:

const res = await fetch('/api/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'X-Payload-HTTP-Method-Override': 'GET',
  },
  body: qs.stringify({ depth: 1, locale: 'en' }),
})

Gotchas

  • Custom endpoints are not authenticated by default — add if (!req.user) checks manually
  • req.data is not auto-populated in custom endpoints — call await addDataAndFileToRequest(req) first
  • req.locale / req.fallbackLocale are not populated in custom endpoints — use addLocalesToRequestFromData
  • Custom endpoints with root: true bypass routes.api — only valid on top-level Payload config, not on collections/globals
  • SDK is in beta — may have breaking changes in minor versions
  • limit: 0 automatically disables pagination