Mastering Payload CMS API: Authentication & Queries Explained
Unlock the power of Payload CMS with our guide to REST API authentication and efficient data querying techniques.

📚 Comprehensive Payload CMS Guides
Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.
I was setting up an n8n automation to generate delivery reports from our Payload CMS database when I hit a wall. The API kept returning "Nimate dovoljenja za izvedbo tega dejanja" (You don't have permission to perform this action), even though I was using a valid API key from a super admin account. After digging through logs and documentation, I discovered the issue wasn't my permissions—it was how I was authenticating.
Payload CMS provides REST API endpoints for every collection automatically, but the authentication format is specific and easy to get wrong. This guide shows you exactly how to authenticate with Payload's API and use its powerful query parameters to fetch exactly the data you need.
Understanding Payload's Built-In REST API
When you create a collection in Payload, the CMS automatically generates REST endpoints for you. No custom endpoint creation needed. For a collection with slug delivery-dates, you immediately get:
GET /api/delivery-dates- List all documentsGET /api/delivery-dates/:id- Get single documentPOST /api/delivery-dates- Create documentPATCH /api/delivery-dates/:id- Update documentDELETE /api/delivery-dates/:id- Delete document
The challenge isn't the endpoints themselves—they're already there. The challenge is understanding how to authenticate properly and how to filter data using Payload's query parameters.
Setting Up API Key Authentication
Before you can fetch data externally, you need to enable API keys in your Payload configuration. In your collection config where authentication is enabled (typically your users collection), you need this setting:
// File: src/collections/Users.ts
export const Users: CollectionConfig = {
slug: 'users',
auth: {
useAPIKey: true, // This enables API key authentication
// ... other auth settings
},
// ... rest of configuration
}
Once enabled, navigate to your Payload admin panel, go to Users, and edit a super admin user. You'll see an API Key field where you can generate a new key. This is a long string like 77c9b8b0-1b81-46a1-ab62-86f76f427eed. Copy this key—you'll need it for every API request.
The critical piece that took me hours to figure out is the authentication header format. Payload requires a very specific structure that's different from typical Bearer token authentication. The format is:
Authorization: {collection-slug} API-Key {your-api-key}
Notice the space between "API-Key" and your key, and the exact capitalization. For a users collection, your header looks like this:
Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed
This tells Payload to authenticate the request using the API key from the users collection, and Payload's middleware will populate req.user with your user document, allowing access control to work properly.
Understanding Access Control Impact
Access control in Payload directly affects what your API requests can retrieve. Here's an example from an orders collection:
// File: src/collections/Orders.ts
access: {
read: ({ req }) => {
if (req.user) {
// Super admin can read all
if (isSuperAdmin(req.user)) return true;
// Customer can only read their own orders
if (req.user.collection === 'customers') {
return {
customer: {
equals: req.user.id,
},
};
}
}
return false;
},
create: () => true, // Public order creation for checkout
update: superAdminOrTenantAdminAccess,
delete: superAdminOrTenantAdminAccess,
}
This access control means unauthenticated requests will fail. A customer's API key will only return their own orders. Only a super admin's API key can retrieve all orders. This is why getting authentication right is crucial—without req.user being populated, Payload returns permission errors even for valid requests.
Compare this with a publicly readable collection:
// File: src/collections/DeliveryDates.ts
access: {
read: () => true, // Anyone can read delivery dates
create: superAdminOrTenantAdminAccess,
update: superAdminOrTenantAdminAccess,
delete: superAdminOrTenantAdminAccess,
}
Delivery dates can be fetched without authentication because read returns true for everyone. But you still need authentication to create, update, or delete records.
Making Your First Authenticated Request
Let's fetch delivery dates with proper authentication. Using cURL:
curl -g 'https://www.kmetijamehak.si/api/delivery-dates' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
The -g flag tells cURL to disable URL globbing, which prevents issues with square brackets in query parameters we'll use later.
In Postman, set up the request like this:
- Method: GET
- URL:
https://www.kmetijamehak.si/api/delivery-dates - Headers tab:
- Key:
Authorization - Value:
users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed
- Key:
For n8n or other automation tools, use the HTTP Request node with a custom header. The header name is Authorization and the value follows the same format: users API-Key {your-key}.
Using the Where Parameter for Filtering
Payload's where parameter lets you filter documents using a MongoDB-style query syntax. The key is understanding how to structure these queries in URL parameters.
Let's say you want to fetch orders for a specific delivery date. Your delivery date has an ID of 5. The query looks like this:
curl -g 'https://www.kmetijamehak.si/api/orders?where[deliveryDate][equals]=5' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
The where[deliveryDate][equals]=5 syntax tells Payload to filter orders where the deliveryDate relationship equals the ID 5. This returns only orders associated with that specific delivery date.
Payload supports multiple operators for different field types:
For relationships and basic equality:
where[fieldName][equals]=value
where[fieldName][not_equals]=value
For numeric and date fields:
where[price][greater_than]=100
where[price][less_than_equal]=500
where[date][greater_than_equal]=2025-11-05T00:00:00.000Z
For text fields:
where[status][in]=confirmed,completed
where[region][not_in]=cancelled
You can combine multiple where conditions. To fetch confirmed orders for a specific delivery date:
curl -g 'https://www.kmetijamehak.si/api/orders?where[deliveryDate][equals]=5&where[status][equals]=confirmed' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
Each additional where condition is added with an ampersand. Payload treats multiple conditions as AND operations by default.
Filtering Date Fields Correctly
Date fields require special attention because they store full timestamps, not just dates. If your delivery date field contains 2025-11-05T12:00:00.000Z, a query for where[date][equals]=2025-11-05 will return nothing because it's not an exact match.
The solution is using range queries. To get all records for November 5th, 2025:
curl -g 'https://www.kmetijamehak.si/api/delivery-dates?where[date][greater_than_equal]=2025-11-05T00:00:00.000Z&where[date][less_than]=2025-11-06T00:00:00.000Z' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
This query finds all documents where the date is greater than or equal to the start of November 5th and less than the start of November 6th. This captures any time during that day, regardless of the specific timestamp stored.
Alternatively, if you know the exact timestamp stored in your database, you can match it precisely:
curl -g 'https://www.kmetijamehak.si/api/delivery-dates?where[date][equals]=2025-11-05T12:00:00.000Z' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
The range approach is more flexible when you're filtering by day rather than exact time.
Populating Relationships with Depth
By default, Payload returns relationship fields as just IDs. If your order has a deliveryDate field that references the delivery-dates collection, you'll receive:
{
"id": 123,
"orderNumber": "ORD-001",
"deliveryDate": 5,
"customer": 42
}
To get the full related documents, use the depth parameter:
curl -g 'https://www.kmetijamehak.si/api/orders?where[deliveryDate][equals]=5&depth=2' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
With depth=2, you get nested relationship data:
{
"id": 123,
"orderNumber": "ORD-001",
"deliveryDate": {
"id": 5,
"date": "2025-11-05T12:00:00.000Z",
"region": "primorska",
"locationTimeSlots": [
{
"location": {
"id": 7,
"name": "Ljubljana - parkirišče Barje p+r",
"address": "Parkirišče Barje p+r, Ljubljana"
},
"timeStart": "2025-10-31T13:00:00.000Z",
"timeEnd": "2025-10-31T13:30:00.000Z"
}
]
},
"customer": {
"id": 42,
"email": "customer@example.com",
"firstName": "John"
}
}
The depth value determines how many levels deep to populate. depth=1 populates immediate relationships. depth=2 populates relationships within those relationships. Higher depth values can slow your queries, so use the minimum needed for your use case.
Selecting Specific Fields with Select
When you're building reports or integrations, you often don't need every field from a document. Fetching unnecessary data wastes bandwidth and slows down your queries. Payload's select parameter lets you specify exactly which fields to return.
The select syntax uses bracket notation similar to where clauses. For top-level fields, the format is straightforward:
select[fieldName]=true
To fetch only the order number and status from orders:
curl -g 'https://www.kmetijamehak.si/api/orders?select[orderNumber]=true&select[status]=true' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
For nested fields within groups, you extend the bracket notation to specify the path:
select[groupName][fieldName]=true
Here's a real example from an orders collection. The customerData field is a group containing firstName, lastName, email, and other customer information. To select only specific fields from this group along with other top-level fields:
curl -g 'https://www.kmetijamehak.si/api/orders?where[deliveryDate][equals]=33&select[customerData][firstName]=true&select[customerData][email]=true&select[orderNumber]=true&select[deliveryDate][id]=true&select[deliveryDate][date]=true&select[pickupLocation]=true' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
This query returns orders for delivery date 33, but only includes:
- The customer's first name from the customerData group
- The customer's email from the customerData group
- The order number
- The delivery date's ID and date fields
- The pickup location
The response will look like:
{
"docs": [
{
"orderNumber": "ORD-001",
"customerData": {
"firstName": "John",
"email": "john@example.com"
},
"deliveryDate": {
"id": 33,
"date": "2025-11-05T12:00:00.000Z"
},
"pickupLocation": 7
}
]
}
Notice that other fields from customerData like lastName, phone, and address aren't included. Only the fields you explicitly selected are returned. This is particularly valuable when working with large collections or when building specific reports that need a subset of data.
When selecting fields from relationship documents like deliveryDate, you're choosing which fields from that related document to include. If you want the full related document, use the depth parameter instead. If you want specific fields from the relationship, combine select with depth:
curl -g 'https://www.kmetijamehak.si/api/orders?where[deliveryDate][equals]=33&depth=1&select[orderNumber]=true&select[deliveryDate][date]=true&select[deliveryDate][region]=true' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
This populates the deliveryDate relationship but only includes the date and region fields from it, along with the order number from the order itself.
Other Useful Query Parameters
Beyond where, depth, and select, Payload provides several other query parameters to shape your response:
Limit the number of results:
limit=50
Paginate through results:
limit=50&page=2
Sort by field (ascending or descending):
sort=createdAt
sort=-createdAt
The minus sign indicates descending order. You can sort by any field in your collection. Combine all these parameters together for precise queries:
curl -g 'https://www.kmetijamehak.si/api/orders?where[status][equals]=confirmed&depth=1&limit=20&sort=-createdAt&select[orderNumber]=true&select[customer]=true&select[grandTotal]=true' \
-H 'Authorization: users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed'
This query fetches the 20 most recent confirmed orders with customer details populated, returning only the order number, customer data, and grand total.
Implementing in n8n
To use this in n8n for automated reports or cron jobs, add an HTTP Request node with these settings:
- Method: GET
- URL:
https://www.kmetijamehak.si/api/orders - Add Query Parameters:
- Name:
where[deliveryDate][equals], Value:33 - Name:
select[orderNumber], Value:true - Name:
select[customerData][firstName], Value:true - Name:
select[customerData][email], Value:true - Name:
select[deliveryDate][date], Value:true - Name:
depth, Value:1
- Name:
- Add Header:
- Name:
Authorization - Value:
users API-Key 77c9b8b0-1b81-46a1-ab62-86f76f427eed
- Name:
The response will be JSON that you can process in subsequent n8n nodes. Parse the docs array to access individual orders, then transform or export the data as needed for your reports. The select parameters ensure you're only fetching the exact data you need, making your automation faster and more efficient.
Conclusion
Fetching data from Payload CMS externally requires understanding two key pieces: the specific authentication header format and how to structure query parameters. Once you know that authentication follows the {collection-slug} API-Key {key} pattern and that where clauses and select statements use bracket notation for nested filtering, you can query any Payload collection from n8n, cron jobs, or any external service.
You now have the foundation to build automated reports, sync data with external systems, or create custom integrations that leverage Payload's built-in REST API without writing a single custom endpoint. The access control you define in your collections will automatically apply to these API requests, keeping your data secure while giving you the flexibility to access it programmatically.
Let me know in the comments if you have questions, and subscribe for more practical development guides.
Thanks, Matija