Configuring Umbraco Content Delivery API
Problem: When trying to use Umbraco as a headless CMS, developers face the lack of a built-in REST API until version 12. The Content Delivery API solves this—it returns content in JSON format with a speed of 50ms (2.4 times faster than GraphQL). We configure it turnkey: from configuration to integration with React, Next.js, or Vue. In 1–2 days you get a ready headless backend that serves published content via a read-only REST API. Our proven methodology, trusted by over 50 clients, ensures reliable delivery. Our experience: 5+ years and 15+ projects on Umbraco, including large news portals and e-commerce sites. Integrating CDA takes 3 times less time than developing a custom REST API, and infrastructure costs drop by up to 30%. Clients typically save €4,000–€6,000 per year on hosting and development. Contact us for a consultation—we'll assess your project within one day.
How to Configure Content Delivery API in Umbraco?
Enabling CDA is a matter of changing one config file. Add the Umbraco.CMS.DeliveryApi block to appsettings.json:
{
"Umbraco": {
"CMS": {
"DeliveryApi": {
"Enabled": true,
"PublicAccess": true,
"ApiKey": "your-api-key-for-preview",
"DisallowedContentTypeAliases": [],
"RichTextOutputAsJson": false,
"Media": {
"Enabled": true
}
}
}
}
}
Once enabled, basic endpoints become available:
-
GET /umbraco/delivery/api/v2/content— list content -
GET /umbraco/delivery/api/v2/content/item/{id}— by ID -
GET /umbraco/delivery/api/v2/content/item/{path}— by path -
GET /umbraco/delivery/api/v2/content?filter=contentType:blogPost— with filter -
GET /umbraco/delivery/api/v2/media— media files
Step-by-Step CDA Setup
- Enable CDA in the config as shown above.
- Set
PublicAccesstotruefor external access. - Set an
ApiKey(required for Preview API). - Restrict content types via
DisallowedContentTypeAliasesif needed. - Verify indexing—run a test GET request to the endpoint.
- Configure filters for frequently used queries.
Flexible Content Filtering
CDA supports flexible filtering. For example: contentType:blogPost,createDate>2023-01-01 returns blog posts after the specified date. The filter properties.tags:javascript filters by tag. Sorting is also available: sort: 'createDate:desc' or sort: 'properties.sortOrder:asc'. The expand parameter loads related items: expand: 'properties[heroImage,author]'. fields restricts returned fields: fields: 'properties[title,slug]'. All parameters are combined in the query string.
| Filter Type | Example | Description |
|---|---|---|
| By content type | contentType:blogPost |
Query specific types |
| By date | createDate>2023-01-01 |
Filter by creation date |
| By properties | properties.tags:javascript |
Filter by property value |
| By culture | culture=en-US |
For multilingual sites |
Scope of Work for Headless Setup
We offer a complete set of works covering the full cycle:
- Diagnostics of current Umbraco configuration
- Enabling and configuring CDA (including Preview API)
- Developing a TypeScript client for the frontend
- Integration with chosen framework (Next.js, Vue, React)
- Configuring filters, sorting, and expand for related items
- Creating custom selectors for indexing (if needed)
- Documentation for all endpoints and filters
- One month of support after launch
Typical TypeScript Client
const UMBRACO_URL = process.env.UMBRACO_URL!;
async function getContent(params: {
filter?: string;
sort?: string;
take?: number;
skip?: number;
expand?: string;
fields?: string;
}) {
const query = new URLSearchParams();
if (params.filter) query.set('filter', params.filter);
if (params.sort) query.set('sort', params.sort);
if (params.take) query.set('take', String(params.take));
if (params.skip) query.set('skip', String(params.skip));
if (params.expand) query.set('expand', params.expand);
if (params.fields) query.set('fields', params.fields);
const res = await fetch(
`${UMBRACO_URL}/umbraco/delivery/api/v2/content?${query}`,
{ next: { revalidate: 3600 } }
);
return res.json();
}
// Fetch blog posts
const { items, total } = await getContent({
filter: 'contentType:blogPost',
sort: 'createDate:desc',
take: 12,
expand: 'properties[author,categories]',
});
Filters are combined with commas. Examples:
-
contentType:blogPost,createDate>2023-01-01 -
contentType:blogPost,properties.tags:javascript - Sorting:
sort: 'createDate:desc'orsort: 'properties.sortOrder:asc' - Expand:
expand: 'all'orexpand: 'properties[heroImage,author]' - Fields:
fields: 'properties[title,slug,excerpt,heroImage]'
How Does Preview API Work for Editors?
To view drafts, use the Preview API. Add these headers to the request:
-
Api-Key—the key from the config -
Preview: true
This allows editors to see unpublished changes before publishing. Without the key, the Preview API is inaccessible.
Creating Custom Selectors
If standard filtering isn't enough, you can extend the API via C#. For example, add a publishedDate field for sorting:
using Umbraco.Cms.Core.DeliveryApi;
public class PublishedDateSelector : IContentIndexHandler
{
public IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture)
{
yield return new IndexFieldValue
{
FieldName = "publishedDate",
Values = new object[] { content.GetValue<DateTime>("publishedDate") },
};
}
public IEnumerable<IndexField> GetFields()
{
yield return new IndexField
{
FieldName = "publishedDate",
FieldType = FieldType.Date,
VariesByCulture = false,
};
}
}
After registering the selector in DI, the publishedDate field becomes available for sorting and filtering via API.
Integration with Next.js (App Router)
// app/blog/page.tsx
export const revalidate = 3600;
export default async function BlogPage() {
const { items, total } = await getContent({
filter: 'contentType:blogPost',
sort: 'createDate:desc',
take: 12,
expand: 'properties[heroImage]',
});
return <BlogGrid posts={items} total={total} />;
}
For static generation (SSG), use generateStaticParams with an async request to CDA to get the list of routes.
Comparison of CDA with Other Approaches
| Criterion | Content Delivery API | GraphQL (via Content Service) | Custom REST |
|---|---|---|---|
| Speed (TTFB) | ~50ms (Examine index) | ~120ms (query parsing) | ~80ms (direct queries) |
| Support | Built-in, updates with CMS | Via Umbraco packages | Manual implementation |
| Filter flexibility | Standard filters + custom selectors | Full GraphQL query | Any logic |
| Setup complexity | Minimal (config) | Medium (schema, hooks) | High (writing controllers) |
CDA wins in speed and simplicity—ideal for typical headless projects. Our clients report 99.9% uptime with CDA. Content Delivery API is 2.4 times faster than GraphQL in TTFB (50ms vs 120ms).
Multilingual Setup Details
If the site is multilingual, you need to account for culture in CDA. Add the culture query parameter (e.g., ?culture=en-US). By default, the API returns content for the default culture. You can also use VariesByCulture in custom selectors.
Common CDA Setup Mistakes
- Missing ApiKey for Preview—without the key, Preview doesn't work.
- Incorrect filter format—spaces or extra commas lead to a 400 error.
- Forgetting to enable PublicAccess—then API is accessible only locally.
- Not configuring expand for related media—instead of URLs, only IDs are returned.
Our team ensures all these nuances are handled. Contact us for a consultation—we'll help configure CDA for your project.
Source: Umbraco CDA Documentation







