Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

REST API

The REST API is a fully functional HTTP client that allows you to interact with your Documents in a RESTful manner. It supports all CRUD operations and is equipped with automatic pagination, depth, and sorting. All Payload API routes are mounted and prefixed to your config's routes.api URL segment (default: /api).

For example, if you have a Collection called pages, you can fetch its documents directly from the browser or any HTTP client:

1
fetch('https://localhost:3000/api/pages')
2
.then((res) => res.json())
3
.then((data) => console.log(data))

To enhance DX, you can use Payload SDK to query your REST API.

REST query parameters:

  • depth - automatically populates relationships and uploads
  • locale - retrieves document(s) in a specific locale
  • fallback-locale - specifies a fallback locale if no locale value exists
  • select - specifies which fields to include in the result
  • populate - specifies which fields to include in the result from populated documents
  • limit - limits the number of documents returned
  • page - specifies which page to get documents from when used with a limit
  • sort - specifies the field(s) to use to sort the returned documents by
  • where - specifies advanced filters to use to query documents
  • joins - specifies the custom request for each join field by name of the field

Collections

Each collection is mounted using its slug value. For example, if a collection's slug is users, all corresponding routes will be mounted on /api/users.

Note: Collection slugs must be formatted in kebab-case

All CRUD operations are exposed as follows:

OperationMethodPathView
FindGET/api/{collection-slug}
Find By IDGET/api/{collection-slug}/{id}
CountGET/api/{collection-slug}/count
CreatePOST/api/{collection-slug}
UpdatePATCH/api/{collection-slug}
Update By IDPATCH/api/{collection-slug}/{id}
DeleteDELETE/api/{collection-slug}
Delete by IDDELETE/api/{collection-slug}/{id}

Auth Operations

Auth enabled collections are also given the following endpoints:

OperationMethodPathView
LoginPOST/api/{user-collection}/login
LogoutPOST/api/{user-collection}/logout
UnlockPOST/api/{user-collection}/unlock
RefreshPOST/api/{user-collection}/refresh-token
Verify UserPOST/api/{user-collection}/verify/{token}
Current UserGET/api/{user-collection}/me
Forgot PasswordPOST/api/{user-collection}/forgot-password
Reset PasswordPOST/api/{user-collection}/reset-password

Globals

Globals cannot be created or deleted, so there are only two REST endpoints opened:

OperationMethodPathView
Get GlobalGET/api/globals/{global-slug}
Update GlobalPOST/api/globals/{global-slug}

Preferences

In addition to the dynamically generated endpoints above Payload also has REST endpoints to manage the admin user preferences for data specific to the authenticated user.

OperationMethodPathView
Get PreferenceGET/api/payload-preferences/{key}
Create PreferencePOST/api/payload-preferences/{key}
Delete PreferenceDELETE/api/payload-preferences/{key}

Custom Endpoints

Additional REST API endpoints can be added to your application by providing an array of endpoints in various places within a Payload Config. Custom endpoints are useful for adding additional middleware on existing routes or for building custom functionality into Payload apps and plugins. Endpoints can be added at the top of the Payload Config, collections, and globals and accessed respective of the api and slugs you have configured.

Each endpoint object needs to have:

Property

Description

path

A string for the endpoint route after the collection or globals slug

method

The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options'

handler

A function that accepts req - PayloadRequest object which contains Web Request properties, currently authenticated user and the Local API instance payload.

custom

Extension point for adding custom data (e.g. for plugins)

Example:

1
import type { CollectionConfig } from 'payload'
2
3
// a collection of 'orders' with an additional route for tracking details, reachable at /api/orders/:id/tracking
4
export const Orders: CollectionConfig = {
5
slug: 'orders',
6
fields: [
7
/* ... */
8
],
9
endpoints: [
10
{
11
path: '/:id/tracking',
12
method: 'get',
13
handler: async (req) => {
14
const tracking = await getTrackingInfo(req.routeParams.id)
15
16
if (!tracking) {
17
return Response.json({ error: 'not found' }, { status: 404 })
18
}
19
20
return Response.json({
21
message: `Hello ${req.routeParams.name as string} @ ${req.routeParams.group as string}`,
22
})
23
},
24
},
25
{
26
path: '/:id/tracking',
27
method: 'post',
28
handler: async (req) => {
29
// `data` is not automatically appended to the request
30
// if you would like to read the body of the request
31
// you can use `data = await req.json()`
32
const data = await req.json()
33
await req.payload.update({
34
collection: 'tracking',
35
data: {
36
// data to update the document with
37
},
38
})
39
return Response.json({
40
message: 'successfully updated tracking info',
41
})
42
},
43
},
44
{
45
path: '/:id/forbidden',
46
method: 'post',
47
handler: async (req) => {
48
// this is an example of an authenticated endpoint
49
if (!req.user) {
50
return Response.json({ error: 'forbidden' }, { status: 403 })
51
}
52
53
// do something
54
55
return Response.json({
56
message: 'successfully updated tracking info',
57
})
58
},
59
},
60
],
61
}

Root-level routes

Custom endpoints defined in your Payload Config are always mounted under your configured routes.api path (default: /api). To define a route that is not prefixed by this path, add a Next.js Route Handler at the desired location in your app directory:

1
// src/app/my-route/route.ts
2
3
export const GET = async () => {
4
return Response.json({
5
message: 'This is an example of a custom route.',
6
})
7
}

Helpful tips

req.data

Data is not automatically appended to the request. You can read the body data by calling await req.json().

Or you could use our helper function that mutates the request and appends data and file if found.

1
import { addDataAndFileToRequest } from 'payload'
2
3
// custom endpoint example
4
{
5
path: '/:id/tracking',
6
method: 'post',
7
handler: async (req) => {
8
await addDataAndFileToRequest(req)
9
await req.payload.update({
10
collection: 'tracking',
11
data: {
12
// data to update the document with
13
}
14
})
15
return Response.json({
16
message: 'successfully updated tracking info'
17
})
18
}
19
}

req.locale & req.fallbackLocale

The locale and the fallback locale are not automatically appended to custom endpoint requests. If you would like to add them you can use this helper function.

1
import { addLocalesToRequestFromData } from 'payload'
2
3
// custom endpoint example
4
{
5
path: '/:id/tracking',
6
method: 'post',
7
handler: async (req) => {
8
await addLocalesToRequestFromData(req)
9
// you now can access req.locale & req.fallbackLocale
10
return Response.json({ message: 'success' })
11
}
12
}

headersWithCors

By default, custom endpoints don't handle CORS headers in responses. The headersWithCors function checks the Payload config and sets the appropriate CORS headers in the response accordingly.

1
import { headersWithCors } from 'payload'
2
3
// custom endpoint example
4
{
5
path: '/:id/tracking',
6
method: 'post',
7
handler: async (req) => {
8
return Response.json(
9
{ message: 'success' },
10
{
11
headers: headersWithCors({
12
headers: new Headers(),
13
req,
14
})
15
},
16
)
17
}
18
}

Method Override for GET Requests

Payload supports a method override feature that allows you to send GET requests using the HTTP POST method. This can be particularly useful in scenarios when the query string in a regular GET request is too long.

How to Use

To use this feature, include the X-Payload-HTTP-Method-Override header set to GET in your POST request. The parameters should be sent in the body of the request with the Content-Type set to application/x-www-form-urlencoded.

Example

Here is an example of how to use the method override to perform a GET request:

Using Method Override (POST)

1
const res = await fetch(`${api}/${collectionSlug}`, {
2
method: 'POST',
3
credentials: 'include',
4
headers: {
5
'Accept-Language': i18n.language,
6
'Content-Type': 'application/x-www-form-urlencoded',
7
'X-Payload-HTTP-Method-Override': 'GET',
8
},
9
body: qs.stringify({
10
depth: 1,
11
locale: 'en',
12
}),
13
})

Equivalent Regular GET Request

1
const res = await fetch(`${api}/${collectionSlug}?depth=1&locale=en`, {
2
method: 'GET',
3
credentials: 'include',
4
headers: {
5
'Accept-Language': i18n.language,
6
},
7
})

Passing as JSON

When using X-Payload-HTTP-Method-Override, it expects the body to be a query string. If you want to pass JSON instead, you can set the Content-Type to application/json and include the JSON body in the request.

Example

1
const res = await fetch(`${api}/${collectionSlug}/${id}`, {
2
// Only the findByID endpoint supports HTTP method overrides with JSON data
3
method: 'POST',
4
credentials: 'include',
5
headers: {
6
'Accept-Language': i18n.language,
7
'Content-Type': 'application/json',
8
'X-Payload-HTTP-Method-Override': 'GET',
9
},
10
body: JSON.stringify({
11
depth: 1,
12
locale: 'en',
13
}),
14
})

This can be more efficient for large JSON payloads, as you avoid converting data to and from query strings. However, only certain endpoints support this. Supported endpoints will read the parsed body under a data property, instead of reading from query parameters as with standard GET requests.

Payload REST API SDK

The best, fully type-safe way to query Payload REST API is to use the SDK package, which can be installed with:

1
pnpm add @payloadcms/sdk

Its usage is very similar to the Local API.

Example:

1
import { PayloadSDK } from '@payloadcms/sdk'
2
3
const sdk = new PayloadSDK({
4
baseURL: 'https://example.com/api',
5
})

For projects without a payload-types.ts file, or when working with multiple Payload configs, you can manually pass the types as a generic:

1
import { PayloadSDK } from '@payloadcms/sdk'
2
import type { Config } from './payload-types'
3
4
const sdk = new PayloadSDK<Config>({
5
baseURL: 'https://example.com/api',
6
})

Operations

1
// Find operation
2
const posts = await sdk.find({
3
collection: 'posts',
4
draft: true,
5
limit: 10,
6
locale: 'en',
7
page: 1,
8
where: { _status: { equals: 'published' } },
9
})
10
11
// Find by ID operation
12
const posts = await sdk.findByID({
13
id,
14
collection: 'posts',
15
draft: true,
16
locale: 'en',
17
})
18
19
// Auth login operation
20
const result = await sdk.login({
21
collection: 'users',
22
data: {
23
email: 'dev@payloadcms.com',
24
password: '12345',
25
},
26
})
27
28
// Create operation
29
const result = await sdk.create({
30
collection: 'posts',
31
data: { text: 'text' },
32
})
33
34
// Create operation with a file
35
// `file` can be either a Blob | File object or a string URL
36
const result = await sdk.create({ collection: 'media', file, data: {} })
37
38
// Count operation
39
const result = await sdk.count({
40
collection: 'posts',
41
where: { id: { equals: post.id } },
42
})
43
44
// Update (by ID) operation
45
const result = await sdk.update({
46
collection: 'posts',
47
id: post.id,
48
data: {
49
text: 'updated-text',
50
},
51
})
52
53
// Update (bulk) operation
54
const result = await sdk.update({
55
collection: 'posts',
56
where: {
57
id: {
58
equals: post.id,
59
},
60
},
61
data: { text: 'updated-text-bulk' },
62
})
63
64
// Delete (by ID) operation
65
const result = await sdk.delete({ id: post.id, collection: 'posts' })
66
67
// Delete (bulk) operation
68
const result = await sdk.delete({
69
where: { id: { equals: post.id } },
70
collection: 'posts',
71
})
72
73
// Find Global operation
74
const result = await sdk.findGlobal({ slug: 'global' })
75
76
// Update Global operation
77
const result = await sdk.updateGlobal({
78
slug: 'global',
79
data: { text: 'some-updated-global' },
80
})
81
82
// Auth Login operation
83
const result = await sdk.login({
84
collection: 'users',
85
data: { email: 'dev@payloadcms.com', password: '123456' },
86
})
87
88
// Auth Me operation
89
const result = await sdk.me(
90
{ collection: 'users' },
91
{
92
headers: {
93
Authorization: `JWT ${user.token}`,
94
},
95
},
96
)
97
98
// Auth Refresh Token operation
99
const result = await sdk.refreshToken(
100
{ collection: 'users' },
101
{ headers: { Authorization: `JWT ${user.token}` } },
102
)
103
104
// Auth Forgot Password operation
105
const result = await sdk.forgotPassword({
106
collection: 'users',
107
data: { email: user.email },
108
})
109
110
// Auth Reset Password operation
111
const result = await sdk.resetPassword({
112
collection: 'users',
113
data: { password: '1234567', token: resetPasswordToken },
114
})
115
116
// Find Versions operation
117
const result = await sdk.findVersions({
118
collection: 'posts',
119
where: { parent: { equals: post.id } },
120
})
121
122
// Find Version by ID operation
123
const result = await sdk.findVersionByID({
124
collection: 'posts',
125
id: version.id,
126
})
127
128
// Restore Version operation
129
const result = await sdk.restoreVersion({
130
collection: 'posts',
131
id,
132
})
133
134
// Find Global Versions operation
135
const result = await sdk.findGlobalVersions({
136
slug: 'global',
137
})
138
139
// Find Global Version by ID operation
140
const result = await sdk.findGlobalVersionByID({
141
id: version.id,
142
slug: 'global',
143
})
144
145
// Restore Global Version operation
146
const result = await sdk.restoreGlobalVersion({
147
slug: 'global',
148
id,
149
})

Every operation has optional 3rd parameter which is used to add additional data to the RequestInit object (like headers):

1
await sdk.me(
2
{
3
collection: 'users',
4
},
5
{
6
// RequestInit object
7
headers: {
8
Authorization: `JWT ${token}`,
9
},
10
},
11
)

To query custom endpoints, you can use the request method, which is used internally for all other methods:

1
await sdk.request({
2
method: 'POST',
3
path: '/send-data',
4
json: {
5
id: 1,
6
},
7
})

Custom fetch implementation and baseInit for shared RequestInit properties:

1
const sdk = new PayloadSDK<Config>({
2
baseInit: { credentials: 'include' },
3
baseURL: 'https://example.com/api',
4
fetch: async (url, init) => {
5
console.log('before req')
6
const response = await fetch(url, init)
7
console.log('after req')
8
return response
9
},
10
})

Example of a custom fetch implementation for testing the REST API without needing to spin up a next development server:

1
import config from '@payload-config'
2
import {
3
REST_DELETE,
4
REST_GET,
5
REST_PATCH,
6
REST_POST,
7
REST_PUT,
8
} from '@payloadcms/next/routes'
9
import { PayloadSDK } from '@payloadcms/sdk'
10
11
const api = {
12
GET: REST_GET(config),
13
POST: REST_POST(config),
14
PATCH: REST_PATCH(config),
15
DELETE: REST_DELETE(config),
16
PUT: REST_PUT(config),
17
}
18
19
const awaitedConfig = await config
20
21
export const sdk = new PayloadSDK({
22
baseURL: ``,
23
fetch: (path: string, init: RequestInit) => {
24
const [slugs, search] = path.slice(1).split('?')
25
const url = `${awaitedConfig.serverURL || 'http://localhost:3000'}${awaitedConfig.routes.api}/${slugs}${search ? `?${search}` : ''}`
26
27
if (init.body instanceof FormData) {
28
const file = init.body.get('file') as Blob
29
if (file && init.headers instanceof Headers) {
30
init.headers.set('Content-Length', file.size.toString())
31
}
32
}
33
const request = new Request(url, init)
34
35
const params = {
36
params: Promise.resolve({
37
slug: slugs.split('/'),
38
}),
39
}
40
41
return api[init.method.toUpperCase()](request, params)
42
},
43
})

Was this page helpful?

Next

GraphQL Overview