
Contentstack Vibe Docs
- 48 installs
- 1 repo stars
- Updated August 2, 2026
- contentstack/contentstack-vibe-docs
Look up Contentstack implementation docs for REST/GraphQL/CMA APIs, SDKs, Live Preview, and Next.js/Nuxt/Gatsby patterns via a routing table.
About
Bundles comprehensive Contentstack CMS documentation across REST, GraphQL, CMA, and Image APIs, SDKs, Live Preview, and framework patterns with a routing table. A developer uses it as a reference when implementing any Contentstack feature.
- ~13,500 lines of Contentstack reference across 30+ files with a routing table
- Covers REST/GraphQL/CMA/Image APIs, SDKs, Live Preview, and framework patterns
Contentstack Vibe Docs by the numbers
- 48 all-time installs (skills.sh)
- Ranked #819 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/contentstack/contentstack-vibe-docs --skill contentstack-vibe-docsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 48 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 2, 2026 |
| Repository | contentstack/contentstack-vibe-docs ↗ |
What it does
Look up Contentstack implementation docs for REST/GraphQL/CMA APIs, SDKs, Live Preview, and Next.js/Nuxt/Gatsby patterns via a routing table.
Files
Contentstack Documentation for AI Agents
This skill contains ~13,500 lines across 30+ reference files. Read the routing table, pick the 1-3 files you actually need, and stop. Never read everything.
Routing table
| Task | File |
|---|---|
| Quick code pattern lookup | QUICK_REFERENCE.md |
| Contentstack basics | concepts/base-concepts.md |
| Design content models, choose references vs modular blocks vs global fields, taxonomy | concepts/data-modeling-best-practices.md |
| Localization, fallback chains, non-localizable fields | concepts/localization.md |
| Regions, endpoints, region-aware hosts | concepts/regions.md |
| Fetch content (REST) | api/rest-api.md |
| Fetch content (GraphQL) | api/graphql-api.md |
| Create/update/delete/publish, modular block schema, CMA headers | api/content-management-api.md |
| Transform images, asset folders, asset limits, file_uid | api/image-delivery-api.md |
| TypeScript Delivery SDK | sdk/delivery-sdk.md |
| Live Preview overview | live-preview/concepts.md |
Live Preview CSR (ssr: false) | live-preview/csr-mode.md |
Live Preview SSR (ssr: true), per-request factory, hash isolation | live-preview/ssr-mode.md |
Visual Builder, edit tags, addEditableTags, VB_EmptyBlockParentClass | live-preview/visual-builder.md |
| Debug Live Preview / Visual Builder failures | live-preview/debugging.md |
Next.js patterns, Draft Mode, revalidateTag | frameworks/nextjs.md |
| Nuxt patterns | frameworks/nuxt.md |
| Gatsby patterns | frameworks/gatsby.md |
| Pick the right token (delivery/preview/management/authtoken/OAuth) | security/tokens-authentication.md |
| Roles, custom permissions, teams | security/roles-permissions.md |
| OAuth login with Auth.js v5 (Next.js) | authentication/oauth.md |
| Webhooks: signatures, event channels, release storms | workflows/webhooks.md |
| Releases: atomic coordinated deploys | workflows/releases.md |
| Workflows & publish rules | workflows/content-workflows.md |
| Branches & aliases: zero-downtime deploys | workflows/branches-aliases.md |
| Environments, publishing, Sync API, rate limits | workflows/environments-publishing.md |
| Variants & Personalize | personalization/variants-and-personalize.md |
| CLI plugins — overview & quickstart | extensions/cli-plugins/overview.md |
| CLI plugins — commands, flags, arguments | extensions/cli-plugins/commands.md |
| CLI plugins — publishing, testing, troubleshooting | extensions/cli-plugins/publishing.md |
| Developer Hub apps (App SDK, UI locations, API proxy) | extensions/devhub-apps.md |
| Contentstack Launch: deployments, env sync | extensions/launch.md |
| Real-world code patterns | examples/practical-examples.md |
| Package versions | VERSIONS.md |
Common task combinations
| Scenario | Files (in order) |
|---|---|
| New Next.js project | base-concepts → delivery-sdk → nextjs |
| New Nuxt project | base-concepts → delivery-sdk → nuxt |
| Add Live Preview to Next.js | live-preview/concepts → live-preview/ssr-mode → nextjs |
| Add Visual Builder to existing site | live-preview/visual-builder |
| Debug broken preview | live-preview/debugging |
| Build a CRUD/migration script | content-management-api → security/tokens-authentication |
| Full-stack with user login | delivery-sdk → nextjs → oauth |
| Webhook-driven rebuild | workflows/webhooks → workflows/environments-publishing |
| Zero-downtime content deploy | workflows/branches-aliases → workflows/releases |
| Multi-locale rollout | concepts/localization → workflows/environments-publishing |
| Deploy to Launch from CI | extensions/launch → workflows/webhooks |
| Responsive image optimization | api/image-delivery-api |
| Quick snippet | QUICK_REFERENCE.md |
Decision helpers
Which API? Read published content → REST / GraphQL / Delivery SDK. Write content → Content Management API. Transform images → Image Delivery API.
Which SDK? @contentstack/delivery-sdk for reads (frontend/backend). @contentstack/management for writes (server-only, never frontend).
Which Live Preview mode? The ssr flag controls how the CMS iframe updates, not your app's rendering strategy.
ssr: false— postMessage. CMS sends data to iframe, client re-fetches and updates without reload.ssr: true— iframe reload with?live_preview=<hash>&entry_uid=.... Server reads params per request.
For ssr: true, create a fresh Contentstack client per request (factory pattern). Sharing one global client leaks preview state between concurrent editors. See live-preview/ssr-mode.md.
Which token? Frontend reads → Delivery Token (safe). Preview reads → Preview Token (safe). Server writes → Management Token (NEVER frontend). User sessions → Authtoken or OAuth. Full decision tree in security/tokens-authentication.md.
Ask before coding
Before implementing, confirm with the developer:
- Region (US, EU, AU, Azure NA/EU, GCP NA/EU) — affects every endpoint.
- Framework (Next.js, Nuxt, Gatsby, etc.) — determines Live Preview mode.
- Environment (dev/staging/production) — scopes the delivery token.
- Credentials in env vars? — never ask for the values themselves.
Security (summary)
Never ask for, log, output, or hardcode API keys, tokens, or secrets. Always use process.env.* references. Never use Management Tokens in frontend code. If a developer pastes a real token, warn them and recommend rotating it. Full rules: security/tokens-authentication.md.
Red flags
- Reading all reference files instead of routing to 1-3.
- Hardcoding credentials or exposing management tokens to the browser.
- Hardcoding region hosts instead of using
@timbenniks/contentstack-endpoints. - Mixing Delivery SDK patterns with Management SDK patterns.
- Mixing REST and GraphQL patterns in one query.
- Sharing a module-level Contentstack client across SSR preview requests.
- Forgetting
api_version: 3.2for reference publishing. - Forgetting
.includeReference()then wondering why references are undefined. - Ignoring
X-RateLimit-Resetand busy-looping on 429s.
Contentstack Content Management API
Complete guide to using Contentstack's Content Management API (CMA) for programmatic content operations — create, update, delete, and publish entries, content types, assets, and more.
Base URL
https://api.contentstack.io/v3/{resource}Regional Base URLs:
| Region | Base URL |
|---|---|
us | https://api.contentstack.io/v3/ |
eu | https://eu-api.contentstack.com/v3/ |
au | https://au-api.contentstack.com/v3/ |
azure-na | https://azure-na-api.contentstack.com/v3/ |
azure-eu | https://azure-eu-api.contentstack.com/v3/ |
gcp-na | https://gcp-na-api.contentstack.com/v3/ |
gcp-eu | https://gcp-eu-api.contentstack.com/v3/ |
See Regions Guide for finding your stack's region.
---
Authentication
The CMA supports three authentication methods. All require the api_key header. Header name for the credential differs between methods — a common copy-paste bug.
| Method | Credential header | Use case |
|---|---|---|
| User login | authtoken: <authtoken> | Interactive user sessions |
| Management Token | authorization: <mgmt_token> | Server-side automation, CI/CD, migrations |
| OAuth | authorization: Bearer <access_token> | Third-party apps on behalf of a user |
Note that Management Tokens useauthorization: <token>without aBearerprefix, while OAuth usesauthorization: Bearer <token>. They are different headers despite sharing a name.
Option 1: Authtoken (User-Specific)
api_key: YOUR_API_KEY
authtoken: YOUR_AUTHTOKEN
Content-Type: application/json- Tied to user roles and permissions
- Maximum 20 valid tokens per user
- No time expiration
- Obtain via login:
POST /v3/user-session
Option 2: Management Token (Stack-Level)
api_key: YOUR_API_KEY
authorization: YOUR_MANAGEMENT_TOKEN
Content-Type: application/json- Not tied to a specific user
- Maximum 10 tokens per stack
- Can be read-only or read-write
- Assignable to specific branches
Management token limitations: Cannot manage organizations, stack operations, user sessions, token management, workflow stage changes, or publish rules requiring user/role approval.
Option 3: OAuth Bearer Token
api_key: YOUR_API_KEY
authorization: Bearer YOUR_OAUTH_TOKEN
Content-Type: application/jsonSee OAuth Guide for implementation details.
---
Entries
Entries are the content items within a content type. This is the most commonly used CMA resource.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/content_types/{ct_uid}/entries | Get all entries |
GET | /v3/content_types/{ct_uid}/entries/{entry_uid} | Get a single entry |
POST | /v3/content_types/{ct_uid}/entries | Create an entry |
PUT | /v3/content_types/{ct_uid}/entries/{entry_uid} | Update an entry |
DELETE | /v3/content_types/{ct_uid}/entries/{entry_uid} | Delete an entry |
POST | /v3/content_types/{ct_uid}/entries/{entry_uid}/publish | Publish an entry |
POST | /v3/content_types/{ct_uid}/entries/{entry_uid}/unpublish | Unpublish an entry |
Create an Entry
curl -X POST 'https://api.contentstack.io/v3/content_types/blog_post/entries' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"entry": {
"title": "My First Blog Post",
"url": "/blog/my-first-post",
"body": "<p>Hello world</p>",
"author": [{ "uid": "blt1234567890", "_content_type_uid": "author" }],
"tags": ["tutorial", "getting-started"]
}
}'With locale:
curl -X POST 'https://api.contentstack.io/v3/content_types/blog_post/entries?locale=fr-fr' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"entry": {
"title": "Mon Premier Article"
}
}'Update an Entry
curl -X PUT 'https://api.contentstack.io/v3/content_types/blog_post/entries/blt1234567890' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"entry": {
"title": "Updated Title",
"body": "<p>Updated content</p>"
}
}'Publish an Entry
curl -X POST 'https://api.contentstack.io/v3/content_types/blog_post/entries/blt1234567890/publish' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"entry": {
"environments": ["production"],
"locales": ["en-us"]
}
}'Scheduled publish:
curl -X POST 'https://api.contentstack.io/v3/content_types/blog_post/entries/blt1234567890/publish' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"entry": {
"environments": ["production"],
"locales": ["en-us"],
"scheduled_at": "2025-06-15T09:00:00.000Z"
}
}'Unpublish an Entry
curl -X POST 'https://api.contentstack.io/v3/content_types/blog_post/entries/blt1234567890/unpublish' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"entry": {
"environments": ["production"],
"locales": ["en-us"]
}
}'Query Entries
Query parameters:
| Parameter | Description |
|---|---|
locale | Locale code (e.g., en-us, fr-fr) |
include_publish_details | Returns publish details per environment |
include_workflow | Include workflow stage information |
include_count | Include total count in response |
skip | Number of entries to skip (pagination) |
limit | Number of entries to return (default: 25) |
asc | Sort ascending by field |
desc | Sort descending by field |
query | JSON query for filtering (MongoDB-style operators) |
Example — fetch entries with filtering:
curl -G 'https://api.contentstack.io/v3/content_types/blog_post/entries' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
--data-urlencode 'query={"status": "published"}' \
--data-urlencode 'limit=10' \
--data-urlencode 'skip=0' \
--data-urlencode 'include_count=true' \
--data-urlencode 'desc=created_at'Additional Entry Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/content_types/{ct_uid}/entries/{entry_uid}/references | Get entry references |
GET | /v3/content_types/{ct_uid}/entries/{entry_uid}/languages | Get entry languages |
GET | /v3/content_types/{ct_uid}/entries/{entry_uid}/versions | Get version history |
GET | /v3/content_types/{ct_uid}/entries/{entry_uid}/export | Export an entry |
POST | /v3/content_types/{ct_uid}/entries/import | Import an entry |
POST | /v3/content_types/{ct_uid}/entries/{entry_uid}/localize | Localize an entry |
POST | /v3/content_types/{ct_uid}/entries/{entry_uid}/workflow | Set workflow stage |
---
Content Types
Content types define the schema for your entries.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/content_types | Get all content types |
GET | /v3/content_types/{uid} | Get a single content type |
POST | /v3/content_types | Create a content type |
PUT | /v3/content_types/{uid} | Update a content type |
DELETE | /v3/content_types/{uid} | Delete a content type |
POST | /v3/content_types/import | Import a content type |
GET | /v3/content_types/{uid}/export | Export a content type |
Create a Content Type
curl -X POST 'https://api.contentstack.io/v3/content_types' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"content_type": {
"title": "Blog Post",
"uid": "blog_post",
"schema": [
{
"display_name": "Title",
"uid": "title",
"data_type": "text",
"mandatory": true,
"unique": true,
"field_metadata": { "description": "The post title" }
},
{
"display_name": "URL",
"uid": "url",
"data_type": "text",
"mandatory": true
},
{
"display_name": "Body",
"uid": "body",
"data_type": "json",
"field_metadata": { "allow_json_rte": true }
},
{
"display_name": "Published Date",
"uid": "published_date",
"data_type": "isodate"
},
{
"display_name": "Author",
"uid": "author",
"data_type": "reference",
"reference_to": ["author"],
"multiple": false
},
{
"display_name": "Featured Image",
"uid": "featured_image",
"data_type": "file"
},
{
"display_name": "Tags",
"uid": "tags",
"data_type": "text",
"multiple": true
},
{
"display_name": "Is Featured",
"uid": "is_featured",
"data_type": "boolean"
}
],
"options": {
"is_page": true,
"singleton": false,
"url_pattern": "/:title",
"url_prefix": "/blog/"
}
}
}'Field Data Types Reference
| Field Type | data_type | Key Properties |
|---|---|---|
| Single Line Text | text | format (regex validation) |
| Multi Line Text | text | field_metadata.multiline: true |
| Markdown | text | field_metadata.markdown: true |
| Rich Text (HTML) | text | allow_rich_text: true |
| JSON Rich Text | json | field_metadata.allow_json_rte: true |
| Number | number | -- |
| Boolean | boolean | -- |
| Date | isodate | startDate, endDate |
| Select/Dropdown | text | enum with choices array, requires `display_type: "dropdown"` |
| File | file | extensions array for allowed types |
| Link | link | -- |
| Reference | reference | reference_to (array of content type UIDs) |
| Group | group | Nested schema array |
| Modular Blocks | blocks | Nested schema arrays per block |
| Global Field | global_field | reference_to (global field UID) |
Universal Field Properties
| Property | Type | Description |
|---|---|---|
display_name | string | Visible label (required) |
uid | string | Unique identifier (required) |
data_type | string | Field data type (required) |
mandatory | boolean | Required field |
unique | boolean | Unique value constraint |
multiple | boolean | Allow multiple values (array) |
field_metadata.description | string | Help text |
field_metadata.default_value | any | Default value |
field_metadata.instruction | string | Editor instruction |
field_metadata.placeholder | string | Input placeholder |
Modular Blocks field schema
blocks data type lets editors choose which block to insert from a set of nested schemas. Each block has its own uid, title, and a nested schema array.
{
"display_name": "Page Components",
"uid": "page_components",
"data_type": "blocks",
"multiple": true,
"mandatory": false,
"blocks": [
{
"title": "Hero",
"uid": "hero",
"schema": [
{ "display_name": "Headline", "uid": "headline", "data_type": "text", "mandatory": true },
{ "display_name": "Subheading", "uid": "subheading", "data_type": "text" },
{ "display_name": "Image", "uid": "image", "data_type": "file" },
{
"display_name": "CTA",
"uid": "cta",
"data_type": "group",
"schema": [
{ "display_name": "Label", "uid": "label", "data_type": "text" },
{ "display_name": "URL", "uid": "url", "data_type": "text" }
]
}
]
},
{
"title": "Content",
"uid": "content",
"schema": [
{ "display_name": "Title", "uid": "title", "data_type": "text" },
{ "display_name": "Body", "uid": "body", "data_type": "text", "allow_rich_text": true }
]
}
]
}At query time, each selected block appears in the entry's page_components array keyed by its block uid:
{
"page_components": [
{ "hero": { "headline": "Welcome", "subheading": "...", "image": { "uid": "..." }, "cta": { "label": "...", "url": "..." } } },
{ "content": { "title": "About", "body": "<p>...</p>" } }
]
}See ../frameworks/nextjs.md → Modular Blocks for rendering patterns, and ../live-preview/visual-builder.md for edit-tag handling.
Important Notes for Content Type Creation
Enum/Dropdown fields require `display_type`: When using enum with choices, you must include display_type: "dropdown" on the field or the API will reject the request.
{
"display_name": "Status",
"uid": "status",
"data_type": "text",
"enum": {
"advanced": false,
"choices": [
{ "value": "available" },
{ "value": "pending" },
{ "value": "adopted" }
]
},
"display_type": "dropdown"
}`is_page: true` requires a `url` field: If you set options.is_page to true, the schema must include a field with "uid": "url". Without it, the API returns a validation error. This url field is also required for Live Preview and Visual Builder to route correctly to the entry in the preview iframe.
{
"display_name": "URL",
"uid": "url",
"data_type": "text",
"mandatory": true
}---
Assets
Assets are files (images, documents, videos) managed in your stack.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/assets | Get all assets |
GET | /v3/assets/{asset_uid} | Get a single asset |
POST | /v3/assets | Upload an asset |
PUT | /v3/assets/{asset_uid} | Replace an asset |
DELETE | /v3/assets/{asset_uid} | Delete an asset |
POST | /v3/assets/{asset_uid}/publish | Publish an asset |
POST | /v3/assets/{asset_uid}/unpublish | Unpublish an asset |
Important: Asset uploads use multipart/form-data, not JSON.
Upload an Asset
curl -X POST 'https://api.contentstack.io/v3/assets' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-F 'asset[upload]=@/path/to/image.jpg' \
-F 'asset[parent_uid]=folder_uid' \
-F 'asset[title]=Hero Image' \
-F 'asset[description]=Homepage hero banner' \
-F 'asset[tags]=hero,homepage'Replace an Asset
curl -X PUT 'https://api.contentstack.io/v3/assets/blt1234567890' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-F 'asset[upload]=@/path/to/new-image.jpg'Publish an Asset
curl -X POST 'https://api.contentstack.io/v3/assets/blt1234567890/publish' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"asset": {
"environments": ["production"],
"locales": ["en-us"]
}
}'Asset Constraints
- Maximum 10 assets uploaded at once
- Maximum file size: 700 MB per asset
- Upload uses
multipart/form-datacontent type
Asset Folders
| Method | Path | Description |
|---|---|---|
GET | /v3/assets/folders/{folder_uid} | Get a folder |
POST | /v3/assets/folders | Create a folder |
PUT | /v3/assets/folders/{folder_uid} | Update a folder |
DELETE | /v3/assets/folders/{folder_uid} | Delete a folder |
---
Environments
Environments define where content is published (e.g., production, staging).
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/environments | Get all environments |
GET | /v3/environments/{environment_uid} | Get a single environment |
POST | /v3/environments | Create an environment |
PUT | /v3/environments/{environment_uid} | Update an environment |
DELETE | /v3/environments/{environment_uid} | Delete an environment |
Create an Environment
curl -X POST 'https://api.contentstack.io/v3/environments' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"environment": {
"name": "staging",
"urls": [
{ "locale": "en-us", "url": "https://staging.example.com" }
]
}
}'---
Locales
Locales enable multilingual content.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/locales | Get all locales |
GET | /v3/locales/{locale_code} | Get a single locale |
POST | /v3/locales | Add a locale |
PUT | /v3/locales/{locale_code} | Update a locale |
DELETE | /v3/locales/{locale_code} | Delete a locale |
Add a Locale
curl -X POST 'https://api.contentstack.io/v3/locales' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"locale": {
"code": "fr-fr",
"name": "French - France",
"fallback_locale": "en-us"
}
}'Important: A fallback locale is required when adding a new locale.
---
Global Fields
Reusable field groups shared across content types.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/global_fields | Get all global fields |
GET | /v3/global_fields/{uid} | Get a single global field |
POST | /v3/global_fields | Create a global field |
PUT | /v3/global_fields/{uid} | Update a global field |
DELETE | /v3/global_fields/{uid} | Delete a global field |
Important: Pass api_version: '3.2' header for nested global fields support.
Create a Global Field
curl -X POST 'https://api.contentstack.io/v3/global_fields' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"global_field": {
"title": "SEO Fields",
"uid": "seo_fields",
"schema": [
{
"display_name": "Meta Title",
"uid": "meta_title",
"data_type": "text"
},
{
"display_name": "Meta Description",
"uid": "meta_description",
"data_type": "text",
"field_metadata": { "multiline": true }
},
{
"display_name": "OG Image",
"uid": "og_image",
"data_type": "file"
}
]
}
}'---
Webhooks
Webhooks notify external services when content changes.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/webhooks | Get all webhooks |
GET | /v3/webhooks/{webhook_uid} | Get a single webhook |
POST | /v3/webhooks | Create a webhook |
PUT | /v3/webhooks/{webhook_uid} | Update a webhook |
DELETE | /v3/webhooks/{webhook_uid} | Delete a webhook |
GET | /v3/webhooks/{webhook_uid}/logs | Get webhook execution logs |
Create a Webhook
curl -X POST 'https://api.contentstack.io/v3/webhooks' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"webhook": {
"name": "Entry Published",
"destinations": [
{
"target_url": "https://your-api.example.com/webhook",
"http_basic_auth": "username:password",
"http_basic_password": "secret",
"custom_header": [
{ "header_name": "X-Custom-Header", "value": "custom-value" }
]
}
],
"channels": [
"content_types.blog_post.entries.publish.success"
],
"retry_policy": "manual"
}
}'Common Webhook Event Channels
| Channel Pattern | Description |
|---|---|
content_types.entries.create | Any entry created |
content_types.entries.update | Any entry updated |
content_types.entries.publish.success | Any entry published |
content_types.entries.unpublish.success | Any entry unpublished |
content_types.entries.delete | Any entry deleted |
content_types.{ct_uid}.entries.create | Entry created in specific content type |
content_types.{ct_uid}.entries.publish.success | Entry published in specific content type |
assets.delete | Asset deleted |
assets.publish.success | Asset published |
content_types.create | Content type created |
content_types.update | Content type updated |
Webhook Payload Format
Contentstack sends a POST request with a JSON body:
{
"event": "publish",
"module": "entry",
"api_key": "your_stack_api_key",
"data": {
"entry": { "uid": "blt1234567890", "title": "My Post" },
"content_type": { "uid": "blog_post" },
"environment": { "name": "production" },
"locale": "en-us"
},
"triggered_at": "2025-01-15T10:30:00.000Z"
}Security headers sent: Content-Type: application/json, X-Contentstack-Request-Signature
---
Workflows
Workflows define content review and approval stages.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/workflows | Get all workflows |
GET | /v3/workflows/{workflow_uid} | Get a single workflow |
POST | /v3/workflows | Create a workflow |
PUT | /v3/workflows/{workflow_uid} | Update a workflow |
DELETE | /v3/workflows/{workflow_uid} | Delete a workflow |
Set Entry Workflow Stage
curl -X POST 'https://api.contentstack.io/v3/content_types/blog_post/entries/blt1234567890/workflow' \
-H 'api_key: YOUR_API_KEY' \
-H 'authtoken: YOUR_AUTHTOKEN' \
-H 'Content-Type: application/json' \
-d '{
"workflow": {
"workflow_stage": {
"uid": "blt_workflow_stage_uid"
}
}
}'Note: Only Stack Owner, Administrator, and Developer roles can create workflows. Management tokens cannot change workflow stages — use an authtoken instead.
---
Releases
Releases group entries and assets for batch publishing.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/releases | Get all releases |
GET | /v3/releases/{release_uid} | Get a single release |
POST | /v3/releases | Create a release |
PUT | /v3/releases/{release_uid} | Update a release |
DELETE | /v3/releases/{release_uid} | Delete a release |
POST | /v3/releases/{release_uid}/deploy | Deploy a release |
GET | /v3/releases/{release_uid}/items | Get release items |
POST | /v3/releases/{release_uid}/items | Add items to release |
DELETE | /v3/releases/{release_uid}/items | Remove items from release |
Create and Deploy a Release
# 1. Create the release
curl -X POST 'https://api.contentstack.io/v3/releases' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"release": {
"name": "Q1 Product Launch",
"description": "All content for the product launch"
}
}'
# 2. Add items to the release
curl -X POST 'https://api.contentstack.io/v3/releases/{release_uid}/items' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"items": [
{
"uid": "entry_uid_1",
"locale": "en-us",
"version": 1,
"content_type_uid": "blog_post",
"action": "publish"
},
{
"uid": "asset_uid_1",
"action": "publish"
}
]
}'
# 3. Deploy the release
curl -X POST 'https://api.contentstack.io/v3/releases/{release_uid}/deploy' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"release": {
"environments": ["production"],
"locales": ["en-us"],
"action": "publish"
}
}'---
Bulk Operations
Perform operations on multiple entries or assets at once.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /v3/bulk/publish | Bulk publish |
POST | /v3/bulk/unpublish | Bulk unpublish |
POST | /v3/bulk/delete | Bulk delete |
POST | /v3/bulk/workflow | Bulk update workflow stage |
GET | /v3/bulk/{job_id} | Check job status |
Bulk Publish
curl -X POST 'https://api.contentstack.io/v3/bulk/publish' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"entries": [
{ "uid": "entry_uid_1", "content_type": "blog_post", "locale": "en-us" },
{ "uid": "entry_uid_2", "content_type": "blog_post", "locale": "en-us" }
],
"assets": [
{ "uid": "asset_uid_1" }
],
"locales": ["en-us"],
"environments": ["production"],
"publish_with_reference": true
}'Important: Bulk operations are rate-limited to 1 request per second.
---
Branches
Branches allow content versioning and parallel development.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v3/stacks/branches | Get all branches |
GET | /v3/stacks/branches/{branch_uid} | Get a branch |
POST | /v3/stacks/branches | Create a branch |
DELETE | /v3/stacks/branches/{branch_uid} | Delete a branch |
GET | /v3/stacks/branches_compare | Compare branches |
Create a Branch
curl -X POST 'https://api.contentstack.io/v3/stacks/branches' \
-H 'api_key: YOUR_API_KEY' \
-H 'authorization: YOUR_MANAGEMENT_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"branch": {
"uid": "feature-redesign",
"source": "main"
}
}'Note: To target a specific branch in any API call, add the branch header:
branch: feature-redesign---
Tokens
Management Tokens
| Method | Path | Description |
|---|---|---|
GET | /v3/stacks/management_tokens | Get all tokens |
POST | /v3/stacks/management_tokens | Create a token |
PUT | /v3/stacks/management_tokens/{token_uid} | Update a token |
DELETE | /v3/stacks/management_tokens/{token_uid} | Delete a token |
Delivery Tokens
| Method | Path | Description |
|---|---|---|
GET | /v3/stacks/delivery_tokens | Get all tokens |
POST | /v3/stacks/delivery_tokens | Create a token |
PUT | /v3/stacks/delivery_tokens/{token_uid} | Update a token |
DELETE | /v3/stacks/delivery_tokens/{token_uid} | Delete a token |
---
JavaScript CMA SDK
The @contentstack/management SDK provides a typed JavaScript interface for all CMA operations.
Installation
npm install @contentstack/managementInitialization
import * as contentstack from "@contentstack/management";
// With Management Token (recommended for automation)
const client = contentstack.client();
const stack = client.stack({
api_key: process.env.CONTENTSTACK_API_KEY!,
management_token: process.env.CONTENTSTACK_MANAGEMENT_TOKEN!,
});
// With Authtoken (user-specific)
const client = contentstack.client({
authtoken: process.env.CONTENTSTACK_AUTHTOKEN!,
});
const stack = client.stack({
api_key: process.env.CONTENTSTACK_API_KEY!,
});SDK Configuration Options
| Option | Default | Description |
|---|---|---|
host | api.contentstack.io | API host (set per region) |
timeout | 30000 | Request timeout in ms |
retryOnError | true | Enable automatic retries |
retryLimit | 5 | Maximum retry attempts |
retryDelay | 300 | Delay between retries in ms |
Content Type Operations
// Get all content types
const contentTypes = await stack.contentType().query().find();
// Get a single content type
const blogType = await stack.contentType("blog_post").fetch();
// Create a content type
const newType = await stack.contentType().create({
content_type: {
title: "Blog Post",
uid: "blog_post",
schema: [
{ display_name: "Title", uid: "title", data_type: "text", mandatory: true },
{ display_name: "Body", uid: "body", data_type: "json", field_metadata: { allow_json_rte: true } },
],
options: { is_page: true, url_pattern: "/:title", url_prefix: "/blog/" },
},
});
// Delete a content type
await stack.contentType("blog_post").delete();Entry Operations
// Create an entry
const entry = await stack.contentType("blog_post").entry().create({
entry: {
title: "My Post",
body: "<p>Hello world</p>",
tags: ["tutorial"],
},
});
// Update an entry
const entry = await stack.contentType("blog_post").entry("blt1234567890").fetch();
entry.title = "Updated Title";
await entry.update();
// Publish an entry
await stack.contentType("blog_post").entry("blt1234567890").publish({
entry: {
environments: ["production"],
locales: ["en-us"],
},
});
// Unpublish an entry
await stack.contentType("blog_post").entry("blt1234567890").unpublish({
entry: {
environments: ["production"],
locales: ["en-us"],
},
});
// Delete an entry
await stack.contentType("blog_post").entry("blt1234567890").delete();
// Query entries
const entries = await stack
.contentType("blog_post")
.entry()
.query({ query: { title: "My Post" } })
.find();Asset Operations
import * as fs from "fs";
// Upload an asset
const asset = await stack.asset().create({
asset: {
upload: fs.createReadStream("/path/to/image.jpg"),
title: "Hero Image",
description: "Homepage hero banner",
parent_uid: "folder_uid", // optional
tags: "hero,homepage",
},
});
// Fetch an asset
const asset = await stack.asset("blt1234567890").fetch();
// Publish an asset
await stack.asset("blt1234567890").publish({
asset: {
environments: ["production"],
locales: ["en-us"],
},
});
// Delete an asset
await stack.asset("blt1234567890").delete();
// Create a folder
const folder = await stack.asset().folder().create({
asset: {
name: "Product Images",
parent_uid: "parent_folder_uid", // optional
},
});Global Field Operations
// Create a global field
await stack.globalField().create({
global_field: {
title: "SEO Fields",
uid: "seo_fields",
schema: [
{ display_name: "Meta Title", uid: "meta_title", data_type: "text" },
{ display_name: "Meta Description", uid: "meta_description", data_type: "text" },
],
},
});
// Fetch a global field
const seoFields = await stack.globalField("seo_fields").fetch();
// Important: pass api_version for nested global fields
const client = contentstack.client({ api_version: "3.2" });Release Operations
// Create a release
const release = await stack.release().create({
release: {
name: "Q1 Product Launch",
description: "All content for the product launch",
},
});
// Add items to a release
await stack.release("release_uid").item().create({
items: [
{ uid: "entry_uid_1", locale: "en-us", version: 1, content_type_uid: "blog_post", action: "publish" },
],
});
// Deploy a release
await stack.release("release_uid").deploy({
release: {
environments: ["production"],
locales: ["en-us"],
action: "publish",
},
});Bulk Operations
// Bulk publish
await stack.bulkOperation().publish({
entries: [
{ uid: "entry_uid_1", content_type: "blog_post", locale: "en-us" },
{ uid: "entry_uid_2", content_type: "blog_post", locale: "en-us" },
],
locales: ["en-us"],
environments: ["production"],
});
// Bulk delete
await stack.bulkOperation().delete({
entries: [
{ uid: "entry_uid_1", content_type: "blog_post", locale: "en-us" },
],
});---
Error Handling
HTTP Status Codes
| Status | Meaning |
|---|---|
2xx | Success |
400 | Malformed request |
401 | Invalid credentials |
403 | Access forbidden |
404 | Resource not found |
412 | Invalid API key |
422 | Validation error |
429 | Rate limit exceeded |
5xx | Server error |
Error Response Format
{
"error_code": 141,
"error_message": "Entry not found",
"errors": {
"entry_uid": ["Entry does not exist"]
}
}SDK Error Handling
try {
const entry = await stack.contentType("blog_post").entry().create({
entry: { title: "My Post" },
});
console.log("Created:", entry.uid);
} catch (error: any) {
console.error("CMA Error:", error.message);
console.error("Status:", error.status);
console.error("Details:", error.errors);
}---
Rate Limiting
| Operation Type | Limit |
|---|---|
Read (GET) | 10 requests/second/organization |
Write (POST/PUT/DELETE) | 10 requests/second/organization |
| Bulk operations | 1 request/second |
| Stack creation | 1 per minute |
Rate limit response headers:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7When exceeded, the API returns HTTP 429. The SDK automatically retries with backoff when retryOnError: true.
---
Best Practices
1. Use management tokens for automation — prefer stack-scoped tokens over user authtokens for CI/CD and scripts 2. Use the SDK for JavaScript projects — handles retries, pagination, and type safety 3. Implement retry logic — respect rate limits and use exponential backoff 4. Use bulk operations for batch tasks — publish, unpublish, or delete multiple items in one call 5. Use releases for coordinated publishes — group related content for atomic deployment 6. Use branches for parallel development — create branches for features, merge when ready 7. Use global fields for reusable schemas — avoid duplicating field definitions across content types 8. Never expose management tokens in frontend code — use server-side only 9. Use webhooks for event-driven integrations — avoid polling for changes 10. Store credentials in environment variables — never hardcode tokens
---
Environment Variables
# Management API
CONTENTSTACK_API_KEY=your_api_key
CONTENTSTACK_MANAGEMENT_TOKEN=your_management_token
CONTENTSTACK_REGION=us
# If using authtoken instead
CONTENTSTACK_AUTHTOKEN=your_authtoken---
See REST API for content delivery, GraphQL API for GraphQL delivery, and Delivery SDK for the TypeScript delivery SDK.
Contentstack GraphQL API
Complete guide to using Contentstack's GraphQL API for content delivery.
Base URL
https://{region}-graphql.contentstack.com/graphqlPreview URL:
https://{region}-graphql-preview.contentstack.com/graphqlAuthentication
api_key: YOUR_API_KEY
access_token: YOUR_DELIVERY_TOKENFor Preview:
api_key: YOUR_API_KEY
access_token: YOUR_PREVIEW_TOKEN
live_preview: PREVIEW_HASH
preview_token: YOUR_PREVIEW_TOKEN
include_applied_variants: "true"Query Structure
Basic Query
query {
allContentstackPage(where: { url: "/" }) {
nodes {
uid
title
url
}
}
}Fetch Single Entry
query GetEntry($uid: String!) {
contentstackPage(uid: $uid) {
uid
title
url
description
image {
url
title
}
}
}Variables:
{ "uid": "blt123" }Fetch Multiple Entries
query GetEntries {
allContentstackBlogPost(
where: { title: { regex: "tutorial", options: "i" } }
skip: 0
limit: 10
sort: [{ published_date: ASC }]
) {
nodes {
uid
title
url
published_date
}
total
}
}Query Filters
query {
allContentstackBlogPost(
where: {
and: [
{ category: { in: ["tech", "design"] } }
{ published_date: { gte: "2024-01-01" } }
]
}
) {
nodes {
title
category
}
}
}Filter Operators:
| Operator | Description |
|---|---|
eq | Equal to |
ne | Not equal to |
gt | Greater than |
gte | Greater than or equal |
lt | Less than |
lte | Less than or equal |
in | Matches any in array |
nin | Matches none in array |
regex | Regular expression |
exists | Field exists |
and | Logical AND |
or | Logical OR |
Including References
References are included by querying nested fields:
query {
contentstackBlogPost(uid: "blt123") {
title
author {
uid
title
bio
}
category {
uid
title
}
}
}Fragments
fragment BlogPostFields on ContentstackBlogPost {
uid
title
url
published_date
excerpt
}
query {
allContentstackBlogPost {
nodes {
...BlogPostFields
author {
title
}
}
}
}Assets
query {
contentstackAsset(uid: "blt123") {
url
title
filename
content_type
file_size
dimension {
width
height
}
}
}Search Assets
query {
allContentstackAsset(
where: { title: { regex: "logo", options: "i" } }
limit: 20
) {
nodes {
uid
url
title
}
total
}
}Error Handling
GraphQL returns 200 with errors in response:
{
"data": null,
"errors": [
{
"message": "Entry not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["contentstackPage"]
}
]
}JavaScript Example
import { request } from "graphql-request";
const GRAPHQL_HOST = "graphql.contentstack.com";
const query = `
query GetPage($url: String!) {
allContentstackPage(where: { url: $url }) {
nodes {
uid
title
url
}
}
}
`;
const headers = {
api_key: process.env.CONTENTSTACK_API_KEY!,
access_token: process.env.CONTENTSTACK_DELIVERY_TOKEN!,
};
const data = await request(
`https://${GRAPHQL_HOST}/graphql`,
query,
{ url: "/" },
headers
);GraphQL vs REST
| Feature | GraphQL | REST |
|---|---|---|
| Fetch specific fields | ✅ Built-in | Use only[] param |
| References | Auto-included when queried | Use include[] param |
| Multiple queries | Single request | Multiple requests |
| Response size | Only requested data | Full entry data |
| Learning curve | Higher | Lower |
Performance Optimization
1. Keep Payloads Under 5 MB
Large payloads slow down API responses and can cause timeouts. GraphQL helps by only fetching requested fields, but still monitor response sizes.
If payload exceeds 5 MB:
- Query only needed fields (don't fetch entire entries)
- Reduce depth of nested references
- Implement pagination
- Split queries into smaller requests
2. Query Only Needed Fields
GraphQL's main advantage is fetching only what you need. Don't query entire entries:
Bad - Fetching Everything:
query {
allContentstackBlogPost {
nodes {
# Fetching all fields unnecessarily
uid
title
url
description
content
author { uid title bio email phone address }
category { uid title description metadata }
tags { uid title description }
# ... many more fields
}
}
}Good - Only Needed Fields:
query {
allContentstackBlogPost {
nodes {
uid
title
url
author {
title
}
}
}
}3. Limit Reference Depth
Avoid deep nesting of references:
Avoid:
query {
contentstackBlogPost(uid: "blt123") {
author {
company {
employees {
department {
manager {
# Too deep!
}
}
}
}
}
}
}Prefer:
query {
contentstackBlogPost(uid: "blt123") {
author {
title
}
}
}Then fetch nested references separately if needed.
4. Use Fragments for Reusability
Fragments help organize queries and reduce duplication:
fragment BlogPostFields on ContentstackBlogPost {
uid
title
url
published_date
excerpt
}
fragment AuthorFields on ContentstackAuthor {
uid
title
bio
}
query {
allContentstackBlogPost {
nodes {
...BlogPostFields
author {
...AuthorFields
}
}
}
}5. Implement Pagination
Always implement pagination for large datasets:
query GetEntries($skip: Int!, $limit: Int!) {
allContentstackBlogPost(skip: $skip, limit: $limit) {
nodes {
title
url
}
total
}
}Best Practices:
- Use reasonable page sizes (10-50 items)
- Always query
totalfor pagination UI - Implement infinite scroll or page-based navigation
6. Use Modular Blocks Instead of Multiple References
When building pages with multiple content blocks, use Modular Blocks instead of multiple content type references:
Why Modular Blocks:
- Reduces query complexity
- All blocks fetched in single query
- More efficient than multiple references
- Better performance
Example Structure:
query {
contentstackPage(uid: "page_uid") {
title
blocks {
# All block types automatically included
... on ContentstackHeroBlock {
title
image { url }
}
... on ContentstackContentBlock {
content
}
... on ContentstackCTABlock {
button_text
button_url
}
}
}
}7. Implement Caching Strategy
CDN Caching:
- Contentstack automatically caches GraphQL responses via CDN
- Cache is invalidated when content is published
- Leverage CDN for better performance
Application-Level Caching:
import { request } from "graphql-request";
const cache = new Map();
async function getCachedQuery(query: string, variables: any) {
const cacheKey = `${query}_${JSON.stringify(variables)}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const data = await request(endpoint, query, variables, headers);
cache.set(cacheKey, data);
// Set TTL
setTimeout(() => cache.delete(cacheKey), 3600000); // 1 hour
return data;
}Cache Frequently-Used Queries:
- Navigation menus
- Footer content
- FAQ sections
- Content that doesn't change often
8. Implement Lazy Loading
Load content progressively instead of all at once:
Example - Lazy Load Sections:
// Load critical content first
const hero = await queryHero();
const mainContent = await queryMainContent();
// Load secondary content later
setTimeout(async () => {
const sidebar = await querySidebar();
const footer = await queryFooter();
}, 100);Framework-Specific:
- React: Use
React.lazy()andSuspense - Vue: Use dynamic imports
- Next.js: Use dynamic imports with
ssr: false
9. Optimize Your Code
Eliminate Redundancies:
- Remove duplicate queries
- Check if data is already fetched before requesting
- Avoid making queries unique with random numbers/timestamps
- Cache responses in your application
Example - Avoid Duplicate Queries:
// BAD: Queries same data multiple times
async function renderPage() {
const entry = await queryEntry(uid);
const author = await queryAuthor(entry.author_uid);
const entry2 = await queryEntry(uid); // Duplicate!
}
// GOOD: Query once, reuse
async function renderPage() {
const entry = await queryEntry(uid);
const author = await queryAuthor(entry.author_uid);
// Reuse entry object
}10. Batch Operations When Needed
If you need multiple references that exceed limits, batch them:
// Split queries into batches
async function fetchEntryWithBatchedReferences(uid: string) {
// Batch 1: Core data
const entry = await request(endpoint, `
query {
contentstackBlogPost(uid: "${uid}") {
title
author { title }
category { title }
}
}
`);
// Batch 2: Additional references
const tags = await request(endpoint, `
query {
allContentstackTag(where: { entries: { in: ["${uid}"] } }) {
nodes { title }
}
}
`);
// Merge results
return { ...entry.contentstackBlogPost, tags: tags.allContentstackTag.nodes };
}11. Use Variables for Dynamic Queries
Always use variables instead of string interpolation:
Bad:
query {
contentstackPage(uid: "hardcoded_uid") {
title
}
}Good:
query GetPage($uid: String!) {
contentstackPage(uid: $uid) {
title
}
}This enables query caching and prevents injection issues.
12. Monitor API Usage
- Check rate limit headers in responses
- Implement retry logic with exponential backoff
- Monitor response times and payload sizes
- Set up alerts for API errors
Performance Checklist
When making GraphQL queries, ensure:
- ✅ Response payload < 5 MB
- ✅ Querying only needed fields
- ✅ Limiting reference depth
- ✅ Using fragments for reusability
- ✅ Implementing pagination
- ✅ Caching frequently-used queries
- ✅ Using lazy loading for non-critical content
- ✅ Eliminating duplicate queries
- ✅ Handling errors gracefully
- ✅ Using variables for dynamic queries
Best Practices Summary
1. Query only needed fields - Don't fetch entire entries 2. Use fragments - For reusable query parts 3. Implement pagination - Use skip and limit 4. Handle errors - Check errors array in response 5. Cache queries - GraphQL responses are cacheable 6. Use variables - For dynamic queries instead of string interpolation 7. Limit reference depth - Avoid deep nesting
Regional Considerations
Always specify the correct region in your GraphQL endpoint:
- Use region-specific endpoints (e.g.,
eu-graphql.contentstack.comfor EU) - Ensure tokens are valid for the region
- Consider latency when choosing region
- See Regions Guide for complete configuration details
See QUICK_REFERENCE.md for more patterns.
Contentstack Image Delivery API
Complete guide to transforming and optimizing images on-the-fly using Contentstack's Image Delivery API. Resize, crop, convert formats, add overlays, and optimize quality via URL parameters.
How It Works
Append query parameters to any Contentstack image asset URL to transform it. The source image is never modified — transformations are applied to the delivered version and cached on the CDN.
{image_url}?width=800&height=600&format=webp&quality=80Important: The environment query parameter is required on all asset requests.
---
Base URL
Image delivery uses a separate domain from the REST API.
Regional Image URLs:
| Region | Base URL |
|---|---|
us | https://images.contentstack.io/ |
eu | https://eu-images.contentstack.com/ |
au | https://au-images.contentstack.com/ |
azure-na | https://azure-na-images.contentstack.com/ |
azure-eu | https://azure-eu-images.contentstack.com/ |
gcp-na | https://gcp-na-images.contentstack.com/ |
gcp-eu | https://gcp-eu-images.contentstack.com/ |
Full URL structure:
https://images.contentstack.io/v3/assets/{stack_api_key}/{asset_uid}/{file_uid}/{filename}?environment=production&{parameters}Placeholders explained:
| Placeholder | What it is | Where to get it |
|---|---|---|
stack_api_key | Your Stack API key (the api_key env var). Safe in URLs. | Stack Settings → Keys. Same value as api_key in REST calls. |
asset_uid | The asset's unique ID, prefixed blt... | In the asset response: asset.uid |
file_uid | Internal version/file ID for this asset version | In the asset response: asset.file_uid (returned by the Assets endpoint). Present for every published asset. |
filename | Original uploaded filename, URL-encoded | asset.filename. Cannot be changed after upload. |
Non-image assets (PDFs, videos, etc.) serve from assets.contentstack.io (and regional equivalents), not images.contentstack.io. Transforms only apply to images.
Typical flow:
1. Fetch the entry or asset via CDA → receive asset object with url, uid, file_uid, filename, dimension. 2. Use asset.url as the base and append Image Delivery transform params. The CDA asset url already contains the correct stack / asset / file / filename structure.
// asset comes from an entry's asset reference
const src = `${asset.url}?width=800&format=webp&quality=80`;See Regions Guide for finding your stack's region.
RTE-inline images don't transform
Images inserted directly into Rich Text Editor fields serve as-is and do not honor Image Delivery transform params. For transform-ready images, reference assets via asset fields on the content type, not via RTE embeds.
---
Supported Formats and Limits
Input formats: JPEG, PNG, WEBP, GIF, AVIF
Output formats: JPEG, Progressive JPEG, PNG, WebP (lossy), WebP (lossless), GIF, AVIF
Constraints:
| Limit | Value |
|---|---|
| Maximum input file size | 50 MB |
| Maximum input dimensions | 12,000 x 12,000 px |
| Maximum output dimensions | 8,192 x 8,192 px |
| Maximum AVIF output | 4,096 x 4,096 px |
| Maximum animated GIF frames | 1,000 |
---
Resize
Control the output dimensions of an image.
| Parameter | Values | Description |
|---|---|---|
width | 1–8192 (pixels), 0.0–0.99 (percentage), or Np (e.g., 200p = 200%) | Output width |
height | 1–8192 (pixels), 0.0–0.99 (percentage), or Np | Output height |
disable | upscale | Prevent enlargement beyond original dimensions |
Examples:
// Fixed pixel dimensions
?width=300&height=200
// Percentage of original (50%)
?width=0.5
// Scale up to 200%
?width=200p
// Prevent upscaling — image won't exceed original size
?width=1200&disable=upscaleWhen only width or height is specified, the other dimension scales proportionally.
Resize Filter
| Parameter | Values | Description |
|---|---|---|
resize-filter | nearest, bilinear, bicubic, lanczos2, lanczos3 | Algorithm for resizing. Default: lanczos3 |
| Filter | Best For |
|---|---|
nearest | Speed, pixel art |
bilinear | Enlarging images |
bicubic | Shrinking images |
lanczos2 | Preserving edges |
lanczos3 | Best quality (default) |
?width=500&height=550&resize-filter=nearest---
Fit
Controls how the image fits within specified dimensions. Requires both width and height.
| Value | Behavior |
|---|---|
bounds | Constrains image within dimensions (no exceeding). Preserves aspect ratio. |
crop | Crops to exact dimensions. |
cover | Fills dimensions completely, cropping centrally if needed. |
Examples:
// Fit within 400x300 box, preserving aspect ratio
?width=400&height=300&fit=bounds
// Crop to exact 400x300
?width=400&height=300&fit=crop
// Cover the full 400x300 area
?width=400&height=300&fit=cover---
Crop
Four crop syntaxes are available.
Region Crop (by dimensions)
?crop={width},{height}Crops from center. Values in pixels or percentage (0.0–0.99).
Aspect Ratio Crop
?crop={width}:{height}Crops to specific aspect ratio (note the colon separator).
Sub-Region Crop (x,y positioning)
?crop={width},{height},x{value},y{value}Defines top-left corner of the crop region in pixels.
Offset Crop (center-point positioning)
?crop={width},{height},offset-x{value},offset-y{value}Defines center point of the crop. Offset values are percentages.
Crop Modifiers
| Modifier | Description |
|---|---|
safe | Prevents out-of-bounds errors. Returns intersection of source and crop area. |
smart | Enables content-aware cropping. |
Examples:
// Center crop 300x400
?crop=300,400
// 16:9 aspect ratio
?crop=16:9
// Crop starting at x=50, y=100
?crop=300,400,x50,y100
// Crop centered at 50%, 50%
?crop=300,400,offset-x50,offset-y50
// Safe crop (won't error if dimensions exceed source)
?crop=1000,1000,safe
// Content-aware smart crop
?crop=300,400,smart---
Trim
Trims pixels from image edges. Follows CSS shorthand conventions.
| Values | Behavior |
|---|---|
trim=25 | 25px from all edges |
trim=25,50 | 25px top/bottom, 50px left/right |
trim=25,50,75 | 25px top, 50px left/right, 75px bottom |
trim=25,50,75,100 | Top, right, bottom, left (clockwise) |
?trim=50
?trim=25,50,75,100---
Orient
Rotate and flip images.
| Value | Transformation |
|---|---|
1 | No change (default) |
2 | Horizontal flip |
3 | 180 degree rotation |
4 | Vertical flip |
5 | Horizontal flip + 90 degree left rotation |
6 | 90 degree clockwise rotation |
7 | Horizontal flip + 90 degree right rotation |
8 | 90 degree counter-clockwise rotation |
?orient=6 // Rotate 90° clockwise
?orient=2 // Mirror horizontally---
Format Conversion
| Parameter | Value | Description |
|---|---|---|
format | jpg | Baseline JPEG |
format | pjpg | Progressive JPEG |
format | png | PNG |
format | webp | WebP |
format | webpll | WebP Lossless |
format | webply | WebP Lossy |
format | gif | GIF |
format | avif | AVIF |
?format=webp
?format=pjpg&quality=80Auto Format
Automatically serve modern formats to supporting browsers.
| Parameter | Behavior |
|---|---|
auto=webp | Serves WebP to browsers that support it |
auto=avif | Serves AVIF to browsers that support it |
When combined with format, auto takes precedence for supporting browsers. Non-supporting browsers fall back to the format value.
// AVIF for modern browsers, JPG fallback
?auto=avif&format=jpg---
Quality
Controls compression level for lossy formats only (jpg, pjpg, avif, webply).
| Parameter | Values | Description |
|---|---|---|
quality | 1–100 | Compression quality. Higher = better quality, larger file. |
?format=pjpg&quality=75
?format=webply&quality=80---
Blur
| Parameter | Values | Description |
|---|---|---|
blur | 1–1000 | Blur intensity. Higher = more blur. Decimals supported. |
?blur=20
?blur=5.5---
Sharpen
| Parameter | Syntax | Description |
|---|---|---|
sharpen | a{amount},r{radius},t{threshold} | Increase edge definition |
| Sub-parameter | Range | Description |
|---|---|---|
a (amount) | 0–10 | Edge contrast intensity |
r (radius) | 1–1000 | Area affected. Lower = edges only. |
t (threshold) | 0–255 | Minimum brightness difference for sharpening |
?sharpen=a5,r2,t0---
Brightness, Contrast, Saturation
| Parameter | Range | Description |
|---|---|---|
brightness | -100 to 100 | Light intensity. 0 = unchanged, 100 = white, -100 = black. |
contrast | -100 to 100 | Tone difference. 0 = unchanged, -100 = neutral gray. |
saturation | -100 to 100 | Color intensity. 0 = unchanged, -100 = grayscale. |
// Slightly brighter and more vivid
?brightness=10&saturation=20
// Convert to grayscale
?saturation=-100
// Increase contrast
?contrast=30---
Overlay (Watermark)
Place one image on top of another.
| Parameter | Values | Description |
|---|---|---|
overlay | Relative URL path (from /v3/assets/...) | The overlay image |
overlay-align | top, bottom, left, right, middle, center | Position. Default: middle,center. Combine vertical + horizontal. |
overlay-repeat | x, y, both | Repeat the overlay |
overlay-width | Pixels or percentage | Overlay width |
overlay-height | Pixels or percentage | Overlay height |
overlay-pad | Pixels (CSS shorthand) | Padding around overlay |
Example:
// Watermark in bottom-right corner
?overlay=/v3/assets/{stack_api_key}/{asset_uid}/{file_uid}/watermark.png&overlay-align=bottom,right&overlay-width=100&overlay-pad=10
// Repeating watermark
?overlay=/v3/assets/.../logo.png&overlay-repeat=both---
Background Color
Sets the background color for transparent images or padding.
| Format | Syntax |
|---|---|
| Hex (3 or 6 digit) | bg-color=ff0000 (no # prefix) |
| RGB | bg-color=140,220,123 |
| RGBA | bg-color=140,220,123,0.5 (alpha: 0.0–1.0) |
?bg-color=cccccc
?bg-color=140,220,123,0.5---
Canvas
Expand the canvas around an image (adds space around it).
| Syntax | Description |
|---|---|
canvas={width},{height} | Fixed dimensions |
canvas={width}:{height} | Aspect ratio |
canvas={width},{height},x{val},y{val} | With top-left positioning |
canvas={width},{height},offset-x{val},offset-y{val} | With center-point positioning |
Canvas dimensions must be greater than or equal to the image dimensions. Image is centered by default.
?canvas=800,600
?canvas=16:9
?canvas=800,600,x100,y50Note: If canvas and pad are used together, pad is ignored.
---
Padding
| Parameter | Values | Description |
|---|---|---|
pad | Pixels (CSS shorthand: 1-4 values) | Add extra pixels to image edges |
// 20px all sides
?pad=20
// 10px top/bottom, 20px left/right
?pad=10,20---
Device Pixel Ratio
Deliver resolution-appropriate images for high-DPI displays. Requires width or height.
| Parameter | Values | Description |
|---|---|---|
dpr | 1–10000 | Device pixel ratio multiplier |
// Deliver 800px wide image for 2x Retina displays
?width=400&dpr=2---
Animated GIF: Extract Frame
| Parameter | Description |
|---|---|
frame | Extracts the first frame from an animated GIF |
?frameNote: Width and height parameters are not recommended for GIF files.
---
Common Patterns
Responsive Image with Modern Format
// Serve optimized WebP with AVIF for supporting browsers
const heroUrl = `${asset.url}?width=1200&auto=avif&format=webp&quality=80&environment=production`;Thumbnail Generation
const thumbnailUrl = `${asset.url}?width=200&height=200&fit=cover&format=webp&quality=75&environment=production`;Retina-Ready Images
// Standard and 2x versions
const imgSrc = `${asset.url}?width=400&format=webp&quality=80&environment=production`;
const imgSrcSet = `${asset.url}?width=400&dpr=2&format=webp&quality=80&environment=production 2x`;Grayscale Effect
const grayscaleUrl = `${asset.url}?saturation=-100&environment=production`;Blurred Placeholder (LQIP)
const placeholderUrl = `${asset.url}?width=40&blur=20&quality=30&format=webp&environment=production`;Watermarked Image
const watermarkedUrl = `${asset.url}?overlay=/v3/assets/${stackApiKey}/${watermarkUid}/${watermarkFileUid}/watermark.png&overlay-align=bottom,right&overlay-width=150&overlay-pad=20&environment=production`;React/Next.js srcSet Pattern
function ContentstackImage({ asset, widths = [400, 800, 1200] }: { asset: Asset; widths?: number[] }) {
const baseParams = "format=webp&quality=80&environment=production";
const srcSet = widths
.map((w) => `${asset.url}?width=${w}&${baseParams} ${w}w`)
.join(", ");
const sizes = "(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px";
return (
<img
src={`${asset.url}?width=${widths[widths.length - 1]}&${baseParams}`}
srcSet={srcSet}
sizes={sizes}
alt={asset.title}
loading="lazy"
/>
);
}Vue/Nuxt Responsive Image
function getImageUrl(asset: Asset, width: number, options: Record<string, string | number> = {}) {
const params = new URLSearchParams({
width: String(width),
format: "webp",
quality: "80",
environment: process.env.CONTENTSTACK_ENVIRONMENT || "production",
...Object.fromEntries(Object.entries(options).map(([k, v]) => [k, String(v)])),
});
return `${asset.url}?${params.toString()}`;
}---
Parameter Interaction Rules
1. fit requires both width and height 2. dpr requires width or height 3. resize-filter requires width or height 4. quality only works with lossy formats: jpg, pjpg, avif, webply 5. auto takes precedence over format for supporting browsers 6. canvas and pad together: pad is ignored 7. disable=upscale prevents enlargement beyond original dimensions 8. frame only works with animated GIFs 9. environment is mandatory on all requests
---
Best Practices
1. Always specify `format=webp` or `auto=webp` — reduces file size by 25-35% compared to JPEG 2. Set `quality` between 75-85 — optimal balance of quality and file size 3. Use `disable=upscale` — prevents blurry enlargements 4. Use `fit=cover` for thumbnails — ensures consistent dimensions 5. Implement `srcSet` for responsive images — serve appropriate sizes per viewport 6. Use `dpr=2` for Retina displays — deliver crisp images on high-DPI screens 7. Generate LQIP placeholders — small, blurred images for progressive loading 8. Chain parameters efficiently — combine all transformations in a single URL 9. Use `auto=avif` with `format=webp` fallback — best compression with broad compatibility 10. Always include the `environment` parameter — required for all asset delivery
---
Asset organization & limits
Folder structure
Plan a folder hierarchy early. Moving assets between folders preserves UIDs and references — you can always reorganize later, but it's cheaper to do once.
A workable default:
/images
/heroes ← page heroes, banners
/products ← product shots
/og ← social share images
/team ← people photos
/icons ← UI icons
/documents
/legal
/whitepapers
/videoAsset constraints
| Limit | Value |
|---|---|
| Max file size (UI upload) | 700 MB |
| Max file size (API upload) | 100 MB |
| Max assets per batch upload | 10 |
| Max assets per stack (default) | 10,000 |
| Max assets per organization (default) | 500,000 |
| Filename | Cannot be changed after upload |
Replacing vs deleting
- Replacing an asset creates a new version but keeps the same
asset.uid— all entry references stay intact. Use this to update an image in place. - Deleting an asset breaks every reference to it in entries. Check references before deletion.
Asset versioning: GET /v3/assets/{asset_uid}/versions returns version history. Retrieve a specific version by appending ?version={n} on the asset endpoint.
Rate limits
Image delivery requests count against the delivery rate limits on your plan. On 429, back off — don't busy-retry. For build-time image pipelines that generate many variants, cache aggressively on your side and don't treat Image Delivery as a per-request transform service for every user.
---
See REST API for content delivery endpoints, Delivery SDK for SDK-based asset fetching, and Practical Examples for more patterns.
Contentstack REST API
Complete guide to using Contentstack's REST API for content delivery.
Base URL
https://{region}-cdn.contentstack.io/v3/{resource}Preview URL (for unpublished content):
https://{region}-rest-preview.contentstack.com/v3/{resource}Authentication
All requests require headers:
api_key: YOUR_API_KEY
access_token: YOUR_DELIVERY_TOKENFor Preview:
api_key: YOUR_API_KEY
access_token: YOUR_PREVIEW_TOKEN
live_preview: PREVIEW_HASHCommon Endpoints
Fetch Single Entry
GET /v3/content_types/{content_type_uid}/entries/{entry_uid}?environment=productionExample:
curl -X GET \
'https://cdn.contentstack.io/v3/content_types/blog_post/entries/blt123?environment=production' \
-H 'api_key: YOUR_API_KEY' \
-H 'access_token: YOUR_DELIVERY_TOKEN'Fetch Multiple Entries
GET /v3/content_types/{content_type_uid}/entries?environment=productionQuery Parameters:
| Parameter | Description |
|---|---|
environment | Environment name (required) |
locale | Locale code (optional, defaults to default locale) |
query | JSON query for filtering (optional) |
skip | Number of entries to skip (pagination) |
limit | Number of entries to return (pagination, max 100) |
include_count | Include total count in response |
asc | Field to sort ascending |
desc | Field to sort descending |
Example Query:
{
"query": {
"title": { "$regex": "tutorial", "$options": "i" },
"published_date": { "$lte": "2024-01-01" }
},
"skip": 0,
"limit": 10,
"asc": "published_date"
}Query Operators
{
"query": {
"$or": [
{ "category": { "$in": ["tech", "design"] } },
{ "tags": { "$regex": "javascript" } }
],
"published_date": { "$gte": "2024-01-01" }
}
}| Operator | Description |
|---|---|
$eq | Equal to |
$ne | Not equal to |
$gt | Greater than |
$gte | Greater than or equal |
$lt | Less than |
$lte | Less than or equal |
$in | Matches any in array |
$nin | Matches none in array |
$exists | Field exists |
$regex | Regular expression |
$and | Logical AND |
$or | Logical OR |
Including References
Single Reference:
GET /v3/content_types/blog_post/entries/blt123?include[]=authorMultiple References:
GET /v3/content_types/blog_post/entries/blt123?include[]=author&include[]=category&include[]=tagsDeep Includes (include references within references):
GET /v3/content_types/blog_post/entries/blt123?include[]=author.published_dateNote: Keep total includes to 10 or fewer for optimal performance.
Field Selection
Only specific fields:
?only[BASE][]=title&only[BASE][]=urlExclude fields:
?except[BASE][]=internal_notesFetch Assets
Single Asset:
GET /v3/assets/{asset_uid}?environment=productionQuery Parameters:
environment: Environment name (required)version: Asset version (optional)
Search Assets:
GET /v3/assets?environment=productionQuery Parameters:
environment: Environment name (required)query: JSON query for filtering (optional)skip: Pagination offsetlimit: Max assets (max 100)
Example Query:
{
"query": {
"title": { "$regex": "logo", "$options": "i" }
},
"skip": 0,
"limit": 20
}Response Format
Single Entry:
{
"entry": {
"uid": "blt123",
"title": "Entry Title",
"url": "/entry-url",
"publish_details": {
"environment": "production",
"locale": "en-us",
"time": "2024-01-01T00:00:00.000Z"
},
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z",
"created_by": "user_uid",
"updated_by": "user_uid",
"ACL": {},
"_version": 1,
"field_name": "field_value"
}
}Multiple Entries:
{
"entries": [
{ "uid": "blt123", "title": "..." },
{ "uid": "blt456", "title": "..." }
],
"count": 2
}Error Handling
| Status | Description |
|---|---|
200 | Success |
400 | Bad Request |
401 | Unauthorized |
404 | Not Found |
422 | Validation Error |
500 | Server Error |
Error Response:
{
"error_code": 141,
"error_message": "Entry not found",
"errors": { "entry_uid": ["Entry does not exist"] }
}Rate Limiting
Check headers:
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9950
X-RateLimit-Reset: 1640995200On 429, back off with exponential delay + jitter. Never busy-loop.
Pagination
- Default page size: 25 entries.
- Maximum `limit`: 100 entries per request.
- No cursor pagination on REST. Use
skip+limit, andinclude_count=trueto know when to stop.
GET /v3/content_types/blog_post/entries?limit=100&skip=0&include_count=true
→ { entries: [...], count: 347 }
GET /v3/content_types/blog_post/entries?limit=100&skip=100
GET /v3/content_types/blog_post/entries?limit=100&skip=200
GET /v3/content_types/blog_post/entries?limit=100&skip=300For deltas (site rebuilds, incremental caches), prefer the Sync API over polling — see ../workflows/environments-publishing.md → Sync API.
Region aliases
Some responses and tooling accept region codes via aliases (e.g. na ↔ us, AWS-NA ↔ us). The canonical region codes and hosts are in ../concepts/regions.md. Stick to the canonical codes in code (us, eu, au, azure-na, azure-eu, gcp-na, gcp-eu) to avoid edge cases.
Query operator gotchas
regexaccepts a case-insensitivity modifier viaoptions: "i":
{ "title": { "$regex": "tutorial", "$options": "i" } }Without options, matching is case-sensitive.
- GraphQL's
regexargument name may vary — check the GraphQL schema you're querying.
Performance Optimization
1. Keep Payloads Under 5 MB
Large payloads slow down API responses and can cause timeouts. Monitor response sizes:
# Check response size
curl -I 'https://cdn.contentstack.io/v3/content_types/blog_post/entries?environment=production' \
-H 'api_key: YOUR_API_KEY' \
-H 'access_token: YOUR_DELIVERY_TOKEN' | grep -i content-lengthIf payload exceeds 5 MB:
- Use projection queries to limit fields
- Reduce number of includes
- Implement pagination
- Split requests into smaller batches
2. Limit Includes to 10 or Fewer
Each include adds overhead. Keep total includes to 10 or fewer:
Example - Too Many Includes (Avoid):
GET /v3/content_types/blog_post/entries/entry_uid?include[]=author&include[]=category&include[]=tags&include[]=related_posts&include[]=comments&include[]=likes&include[]=shares&include[]=metadata&include[]=translations&include[]=variantsExample - Optimized Includes:
GET /v3/content_types/blog_post/entries/entry_uid?include[]=author&include[]=categorySplit Large Includes: If you need many references, split them into multiple calls:
// Instead of one call with 10 includes
const entry = await fetchEntryWithManyIncludes(uid);
// Split into multiple calls
const entry = await fetchEntry(uid);
const author = await fetchAuthor(entry.author_uid);
const category = await fetchCategory(entry.category_uid);
// ... fetch other references as needed3. Optimize Includes and Reference Depth
Limit Includes:
- Keep total number of includes to 10 or fewer
- Avoid deep nesting of references
- Only include references you actually need
Avoid Deep Reference Nesting:
# Avoid
GET /v3/content_types/blog_post/entries/entry_uid?include[]=author.company.employees.department.manager
# Prefer
GET /v3/content_types/blog_post/entries/entry_uid?include[]=authorThen fetch nested references separately if needed.
4. Use Projection Queries
Use only and except parameters to limit response size:
Only Specific Fields:
GET /v3/content_types/blog_post/entries?only[BASE][]=title&only[BASE][]=url&only[BASE][]=published_dateExclude Fields:
GET /v3/content_types/blog_post/entries?except[BASE][]=internal_notes&except[BASE][]=draft_content5. Implement Pagination
Always implement pagination for large datasets:
GET /v3/content_types/blog_post/entries?skip=0&limit=10&include_count=trueBest Practices:
- Use reasonable page sizes (10-50 items)
- Always include
include_count=truefor pagination UI - Implement infinite scroll or page-based navigation
6. Use Modular Blocks Instead of Multiple References
When building pages with multiple content blocks, use Modular Blocks instead of multiple content type references:
Why Modular Blocks:
- Reduces number of includes needed
- All blocks fetched in single call
- More efficient than multiple references
- Better performance
Example Structure:
// Content Type: Page
// Field: blocks (Modular Block)
// Blocks: HeroBlock, ContentBlock, CTABlock, GalleryBlock
// Single call fetches all blocks
const page = await fetchPage("page_uid");
// All blocks are included automatically, no includes needed7. Implement Caching Strategy
CDN Caching:
- Contentstack automatically caches API responses via CDN
- Cache is invalidated when content is published
- Leverage CDN for better performance
Application-Level Caching:
// Cache frequently-used data
const cache = new Map();
async function getCachedEntry(uid: string) {
const cacheKey = `entry_${uid}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const entry = await fetchEntry(uid);
cache.set(cacheKey, entry);
// Set TTL (time to live)
setTimeout(() => cache.delete(cacheKey), 3600000); // 1 hour
return entry;
}Cache Frequently-Used Data:
- User groups, titles, metadata
- FAQ sections
- Navigation menus
- Content that doesn't change often
8. Implement Lazy Loading
Load content progressively instead of all at once:
Example - Lazy Load Sections:
// Load critical content first
const hero = await fetchHero();
const mainContent = await fetchMainContent();
// Load secondary content later
setTimeout(async () => {
const sidebar = await fetchSidebar();
const footer = await fetchFooter();
}, 100);9. Optimize Your Code
Eliminate Redundancies:
- Remove duplicate API calls
- Check if data is already fetched before requesting
- Avoid making queries unique with random numbers/timestamps
- Cache responses in your application
Example - Avoid Duplicate Calls:
// BAD: Fetches same data multiple times
function renderPage() {
const entry = await fetchEntry(uid);
const author = await fetchAuthor(entry.author_uid);
const entry2 = await fetchEntry(uid); // Duplicate!
}
// GOOD: Fetch once, reuse
function renderPage() {
const entry = await fetchEntry(uid);
const author = await fetchAuthor(entry.author_uid);
// Reuse entry object
}10. Batch Operations When Needed
If you need multiple includes that exceed limits, batch them:
// Split includes into batches
async function fetchEntryWithBatchedIncludes(uid: string) {
// Batch 1: Core references
const entry = await fetchEntry(uid, ["author", "category"]);
// Batch 2: Additional references
const tags = await fetchTags(entry.tag_uids);
// Merge results
return { ...entry, tags };
}11. Monitor API Usage
- Check rate limit headers in responses
- Implement retry logic with exponential backoff
- Monitor response times and payload sizes
- Set up alerts for API errors
Performance Checklist
When making API calls, ensure:
- ✅ Response payload < 5 MB
- ✅ Total includes ≤ 10
- ✅ Using projection queries (
only/except) - ✅ Implementing pagination
- ✅ Avoiding deep reference nesting
- ✅ Caching frequently-used data
- ✅ Using lazy loading for non-critical content
- ✅ Eliminating duplicate calls
- ✅ Handling errors gracefully
- ✅ Using SDK when possible
Best Practices Summary
1. Keep payloads under 5 MB 2. Limit includes to 10 or fewer 3. Use projection queries (only/except) 4. Always implement pagination 5. Cache frequently-used data 6. Handle errors gracefully 7. Use SDK when possible - Prefer @contentstack/delivery-sdk over direct API calls
Regional Considerations
Always specify the correct region in your API calls:
- Use region-specific endpoints (e.g.,
eu-cdn.contentstack.iofor EU) - Ensure tokens are valid for the region
- Consider latency when choosing region
- See Regions Guide for complete configuration details
See QUICK_REFERENCE.md for code patterns.
Contentstack OAuth with Auth.js v5 (NextAuth)
A clean, production-ready integration pattern for Contentstack OAuth with Auth.js v5 in Next.js applications. Includes critical lessons learned from deploying to serverless environments (Vercel, Contentstack Launch).
---
Quick Reference (for AI Agents)
Files to create:
1. lib/auth.ts — Auth.js configuration with Contentstack provider 2. app/api/auth/[...nextauth]/route.ts — Route handler
Required packages:
npm install next-auth@5 @timbenniks/contentstack-endpointsRequired environment variables:
AUTH_SECRET— Random string for JWT encryptionAUTH_TRUST_HOST=true— Required for serverless deployments (Vercel, Contentstack Launch)CONTENTSTACK_REGION— Region code (na, eu, azure-na, azure-eu, gcp-na, gcp-eu)CONTENTSTACK_APP_ID— App UID from Developer HubCONTENTSTACK_CLIENT_ID— OAuth client IDCONTENTSTACK_CLIENT_SECRET— OAuth client secretNEXTAUTH_URL— App URL for callbacks (use HTTPS for production)
Contentstack setup:
- Redirect URL:
{NEXTAUTH_URL}/api/auth/callback/contentstack - Required scope:
user:read(add CMA scopes as needed)
Critical implementation details:
- Authorization URL:
{appUrl}/apps/{APP_ID}/authorize— NOT#!/apps/... - Token URL:
{appUrl}/apps-api/token - Userinfo URL:
{apiUrl}/v3/user— response is nested underuserkey - Use
checks: ["state"](or["pkce", "state"]for PKCE) - Always use
session: { strategy: "jwt" } - On serverless: use
auth()for session/token access — nevergetToken()withoutsecureCookie - Auth route handler must export
dynamic = "force-dynamic"
---
Prerequisites
- Next.js 14+ with App Router
- Auth.js v5 (
next-auth@5) - A Contentstack app with OAuth enabled in Developer Hub
Environment Variables
# Auth.js secret (generate with: openssl rand -base64 32)
AUTH_SECRET="your-secret"
# Required for serverless deployments (Vercel, Contentstack Launch)
AUTH_TRUST_HOST=true
# Contentstack OAuth (from Developer Hub > Your App > OAuth)
CONTENTSTACK_REGION="eu" # na, eu, azure-na, azure-eu, gcp-na, gcp-eu
CONTENTSTACK_APP_ID="your-app-uid"
CONTENTSTACK_CLIENT_ID="your-client-id"
CONTENTSTACK_CLIENT_SECRET="your-client-secret"
# App URL (for OAuth callbacks)
# IMPORTANT: Use HTTPS for production/serverless deployments
NEXTAUTH_URL="http://localhost:3000"Environment Variable Notes
- `AUTH_SECRET` is the v5 name. Auth.js also reads
NEXTAUTH_SECRETas a fallback. - `AUTH_TRUST_HOST=true` tells Auth.js to trust the
Host/X-Forwarded-Hostheaders. Required when running behind a reverse proxy or on serverless platforms. - `NEXTAUTH_URL` must use
https://in production. On Vercel, Auth.js auto-detects the URL viaVERCEL_URL, but setting it explicitly avoids edge cases. - Do not set both
AUTH_URLandNEXTAUTH_URLto different values — Auth.js v5 prefersAUTH_URLwhen both are present.
Contentstack App Configuration
In Contentstack Developer Hub → Your App → OAuth:
1. Redirect URL: {NEXTAUTH_URL}/api/auth/callback/contentstack 2. User Token Scopes: Select the scopes your app needs. At minimum: user:read
Common scope combinations:
| Use case | Scopes |
|---|---|
| Read-only | user:read |
| CMS authoring | user:read cm.content-types.management:read cm.entries.management:read cm.entries.management:write cm.assets.management:read cm.assets.management:write |
| CMS + taxonomies | Add: cm.taxonomies.management:read cm.taxonomies.management:write cm.taxonomy.terms:read cm.taxonomy.terms:write |
| Full management | All available scopes |
Installation
npm install next-auth@5 @timbenniks/contentstack-endpoints---
Option 1: Cookie-Based Sessions (Recommended)
The simplest approach — no database required. User sessions are stored as encrypted JWTs in HTTP-only cookies.
Auth Configuration
File: `lib/auth.ts`
import NextAuth from "next-auth";
import type { JWT } from "next-auth/jwt";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";
const region = process.env.CONTENTSTACK_REGION ?? "na";
const endpoints = getContentstackEndpoints(region);
const appUrl = endpoints.application!;
const apiUrl = endpoints.contentManagement!;
type ContentstackProfile = {
uid: string;
email: string;
first_name?: string;
last_name?: string;
username?: string;
profile_image?: string;
};
export const { handlers, auth, signIn, signOut } = NextAuth({
session: { strategy: "jwt" },
pages: { signIn: "/login", error: "/login" },
providers: [
{
id: "contentstack",
name: "Contentstack",
type: "oauth",
checks: ["state"],
authorization: {
url: `${appUrl}/apps/${process.env.CONTENTSTACK_APP_ID}/authorize`,
params: { response_type: "code", scope: "user:read" },
},
token: `${appUrl}/apps-api/token`,
userinfo: {
url: `${apiUrl}/v3/user`,
async request({
tokens,
}: {
tokens: { access_token?: string };
}) {
const res = await fetch(`${apiUrl}/v3/user`, {
headers: {
Authorization: `Bearer ${tokens.access_token ?? ""}`,
},
});
const { user } = (await res.json()) as {
user: ContentstackProfile;
};
return user;
},
},
profile: (profile: ContentstackProfile) => ({
id: profile.uid,
name:
[profile.first_name, profile.last_name]
.filter(Boolean)
.join(" ") ||
profile.username ||
profile.email,
email: profile.email,
image: profile.profile_image ?? null,
}),
clientId: process.env.CONTENTSTACK_CLIENT_ID,
clientSecret: process.env.CONTENTSTACK_CLIENT_SECRET,
},
],
callbacks: {
jwt({ token, user }) {
if (user) token.sub = user.id;
return token;
},
session({ session, token }) {
if (token.sub) session.user.id = token.sub;
return session;
},
},
});Route Handler
File: `app/api/auth/[...nextauth]/route.ts`
import { handlers } from "@/lib/auth";
export const dynamic = "force-dynamic";
export const { GET, POST } = handlers;`export const dynamic = "force-dynamic"` is critical. Without it, serverless platforms may statically optimize or cache this route, breaking the OAuth callback flow.
That's it for basic auth! No database setup required.
---
Storing the Contentstack Access Token (CMA Access)
If your app needs to call the Contentstack Content Management API on behalf of the user, you must persist the OAuth access token and refresh token in the JWT.
Extended Auth Configuration with Token Storage
File: `lib/auth.ts`
import NextAuth from "next-auth";
import type { JWT } from "next-auth/jwt";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";
import { redirect } from "next/navigation";
const region = process.env.CONTENTSTACK_REGION ?? "na";
const endpoints = getContentstackEndpoints(region);
const appUrl = endpoints.application!;
const apiUrl = endpoints.contentManagement!;
type ContentstackProfile = {
uid: string;
email: string;
first_name?: string;
last_name?: string;
username?: string;
profile_image?: string;
};
type ExtendedToken = JWT & {
accessToken?: string;
refreshToken?: string;
accessTokenExpiresAt?: number;
error?: "RefreshAccessTokenError";
};
async function refreshAccessToken(
token: ExtendedToken
): Promise<ExtendedToken> {
try {
const body = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: token.refreshToken ?? "",
client_id: process.env.CONTENTSTACK_CLIENT_ID ?? "",
client_secret: process.env.CONTENTSTACK_CLIENT_SECRET ?? "",
});
const res = await fetch(`${appUrl}/apps-api/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!res.ok) {
return { ...token, error: "RefreshAccessTokenError" };
}
const data = await res.json();
return {
...token,
accessToken: data.access_token ?? token.accessToken,
refreshToken: data.refresh_token ?? token.refreshToken,
accessTokenExpiresAt: data.expires_in
? Date.now() + data.expires_in * 1000
: token.accessTokenExpiresAt,
error: undefined,
};
} catch {
return { ...token, error: "RefreshAccessTokenError" };
}
}
export const { handlers, auth, signIn, signOut } = NextAuth({
session: { strategy: "jwt" },
pages: { signIn: "/login", error: "/login" },
providers: [
{
id: "contentstack",
name: "Contentstack",
type: "oauth",
checks: ["state"],
authorization: {
url: `${appUrl}/apps/${process.env.CONTENTSTACK_APP_ID}/authorize`,
params: {
response_type: "code",
scope:
"user:read cm.content-types.management:read cm.entries.management:read cm.entries.management:write cm.assets.management:read cm.assets.management:write cm.taxonomies.management:read cm.taxonomies.management:write cm.taxonomy.terms:read cm.taxonomy.terms:write",
},
},
token: `${appUrl}/apps-api/token`,
userinfo: {
url: `${apiUrl}/v3/user`,
async request({
tokens,
}: {
tokens: { access_token?: string };
}) {
const res = await fetch(`${apiUrl}/v3/user`, {
headers: {
Authorization: `Bearer ${tokens.access_token ?? ""}`,
},
});
const { user } = (await res.json()) as {
user: ContentstackProfile;
};
return user;
},
},
profile: (profile: ContentstackProfile) => ({
id: profile.uid,
name:
[profile.first_name, profile.last_name]
.filter(Boolean)
.join(" ") ||
profile.username ||
profile.email,
email: profile.email,
image: profile.profile_image ?? null,
}),
clientId: process.env.CONTENTSTACK_CLIENT_ID,
clientSecret: process.env.CONTENTSTACK_CLIENT_SECRET,
},
],
callbacks: {
async jwt({ token, user, account }) {
// Initial sign-in: persist the OAuth tokens
if (user && account) {
const expiresAt =
typeof account.expires_at === "number"
? account.expires_at
: typeof account.expires_at === "string"
? Number(account.expires_at)
: undefined;
const expiresIn =
typeof account.expires_in === "number"
? account.expires_in
: typeof account.expires_in === "string"
? Number(account.expires_in)
: undefined;
return {
...token,
sub: user.id,
name: user.name ?? null,
email: user.email ?? null,
picture: user.image ?? null,
accessToken: account.access_token,
refreshToken: account.refresh_token,
accessTokenExpiresAt: Number.isFinite(expiresAt)
? expiresAt! * 1000
: Number.isFinite(expiresIn)
? Date.now() + expiresIn! * 1000
: undefined,
} as ExtendedToken;
}
// Subsequent requests: check if token needs refresh
const current = token as ExtendedToken;
const safetyWindowMs = 60_000; // refresh 60s before expiry
if (
current.accessToken &&
(!current.accessTokenExpiresAt ||
Date.now() < current.accessTokenExpiresAt - safetyWindowMs)
) {
return current; // token still valid
}
if (current.refreshToken) {
return refreshAccessToken(current);
}
// No refresh token available
return {
...current,
accessToken: undefined,
refreshToken: undefined,
accessTokenExpiresAt: undefined,
error: "RefreshAccessTokenError",
};
},
session({ session, token }) {
const ext = token as ExtendedToken;
if (ext.sub) {
session.user.id = ext.sub;
session.user.name = ext.name ?? session.user.name;
session.user.email = ext.email ?? session.user.email;
session.user.image = ext.picture ?? session.user.image;
}
// Expose the access token to server components
session.accessToken = ext.accessToken;
session.error = ext.error;
return session;
},
},
});
// Helper: require authenticated session or redirect to login
export async function requireSession() {
const session = await auth();
if (!session) redirect("/login");
return session;
}Type Augmentation
File: `types/next-auth.d.ts`
import "next-auth";
declare module "next-auth" {
interface Session {
accessToken?: string;
error?: "RefreshAccessTokenError";
}
}Using the Access Token in Server Components
import { requireSession } from "@/lib/auth";
export default async function DashboardPage() {
const session = await requireSession();
// Use session.accessToken to call Contentstack CMA
const res = await fetch("https://eu-api.contentstack.com/v3/content_types", {
headers: {
api_key: process.env.CONTENTSTACK_API_KEY!,
authorization: `Bearer ${session.accessToken}`,
},
});
const data = await res.json();
// ...
}---
Option 2: Database-Persisted Sessions
Use this approach if you need to:
- Store additional user data
- Track user activity
- Link users to other models in your database
- Revoke sessions server-side
Additional Installation
npm install @auth/prisma-adapter @prisma/client prismaPrisma Schema
File: `prisma/schema.prisma`
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite" // or "postgresql", "mysql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}Database Client
File: `lib/db.ts`
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;Auth Configuration with Prisma
Add the adapter to any of the auth configurations above:
import { PrismaAdapter } from "@auth/prisma-adapter";
import { db } from "./db";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(db),
session: { strategy: "jwt" }, // Still use JWT strategy
// ... rest of config
});Initialize Database
npx prisma db push---
Usage Patterns
Server Component
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function ProtectedPage() {
const session = await auth();
if (!session) redirect("/login");
return <div>Welcome, {session.user.name}</div>;
}Client Component (Sign In)
"use client";
import { signIn } from "next-auth/react";
export function SignInButton() {
return (
<button
onClick={() => signIn("contentstack", { callbackUrl: "/dashboard" })}
>
Sign in with Contentstack
</button>
);
}Server Action (Sign In)
import { signIn } from "@/lib/auth";
export default function LoginPage() {
return (
<form
action={async () => {
"use server";
await signIn("contentstack", { redirectTo: "/dashboard" });
}}
>
<button type="submit">Sign in with Contentstack</button>
</form>
);
}Middleware (Route Protection)
File: `middleware.ts` (project root)
export { auth as middleware } from "@/lib/auth";
export const config = {
matcher: ["/dashboard/:path*", "/app/:path*"],
};---
Serverless Deployment: Critical Gotchas
Deploying to Vercel, Contentstack Launch, or any serverless platform introduces issues that don't exist in local development. This section documents confirmed bugs and their fixes.
1. The __Secure- Cookie Prefix Problem
Symptom: Auth works locally but returns "Missing access token" on production, even though cookies are clearly set in the browser.
Root cause: On HTTPS, Auth.js sets the session cookie as __Secure-authjs.session-token (with the __Secure- prefix). But if you use getToken() from next-auth/jwt without passing secureCookie: true, it defaults to looking for authjs.session-token (no prefix). The cookie names don't match, so the token is never found.
How Auth.js decides the cookie name:
| Context | Cookie name |
|---|---|
| HTTP (localhost) | authjs.session-token |
| HTTPS (production) | __Secure-authjs.session-token |
The auth handler detects the protocol from the request URL. But getToken() is a standalone helper — it has no request context and defaults to the non-secure name unless you tell it otherwise.
Fix: Don't use getToken() directly. Use auth() instead, which handles cookie name detection internally:
// BAD: getToken() without secureCookie — breaks on HTTPS
import { getToken } from "next-auth/jwt";
import { cookies } from "next/headers";
async function getAccessToken() {
const cookieStore = await cookies();
const cookieHeader = cookieStore
.getAll()
.map(({ name, value }) => `${name}=${value}`)
.join("; ");
const token = await getToken({
req: { headers: { cookie: cookieHeader } },
secret: process.env.AUTH_SECRET,
// Missing secureCookie! Defaults to false → looks for wrong cookie name on HTTPS
});
return token?.accessToken;
}// GOOD: Use auth() which handles secure cookies automatically
import { auth } from "@/lib/auth";
async function getAccessToken() {
const session = await auth();
return session?.accessToken;
}If you absolutely must use getToken() (e.g., in diagnostic tooling), always pass secureCookie:
import { getToken } from "next-auth/jwt";
const isSecure =
process.env.NODE_ENV === "production" ||
(process.env.AUTH_URL ?? process.env.NEXTAUTH_URL ?? "").startsWith("https");
const token = await getToken({
req: { headers: { cookie: cookieHeader } },
secret: process.env.AUTH_SECRET,
secureCookie: isSecure,
});2. Auth Route Must Be Dynamic
Symptom: OAuth callback returns errors or stale responses on serverless.
Fix: Always mark the auth route handler as dynamic:
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
export const dynamic = "force-dynamic";
export const { GET, POST } = handlers;Without this, serverless platforms may statically optimize or cache the route, breaking the stateful OAuth redirect flow.
3. AUTH_TRUST_HOST Is Required
Symptom: Auth callbacks fail with URL mismatch or redirect errors on serverless.
Fix: Set AUTH_TRUST_HOST=true in your production environment variables. This tells Auth.js to trust the Host and X-Forwarded-Host headers, which are set by reverse proxies on Vercel and Contentstack Launch.
4. Don't Duplicate Token Refresh Logic
The JWT callback in Auth.js already handles token refresh on every auth() call. If you also implement manual refresh logic elsewhere (e.g., in your CMA client), you'll have two competing refresh paths that can cause race conditions.
Pattern: Let the JWT callback own all token refresh logic. Server components and server actions should simply read session.accessToken from auth():
// The JWT callback handles refresh automatically
const session = await requireSession();
if (!session.accessToken) {
// Token refresh failed — redirect to login
redirect("/login");
}
// Use session.accessToken for CMA calls5. Environment Variable Checklist for Production
| Variable | Required | Notes |
|---|---|---|
AUTH_SECRET | Yes | openssl rand -base64 32 |
AUTH_TRUST_HOST | Yes (serverless) | Set to true |
NEXTAUTH_URL | Recommended | Must be https:// for production |
CONTENTSTACK_APP_ID | Yes | From Developer Hub |
CONTENTSTACK_CLIENT_ID | Yes | From Developer Hub > OAuth |
CONTENTSTACK_CLIENT_SECRET | Yes | From Developer Hub > OAuth |
CONTENTSTACK_REGION | Yes | Determines API endpoints |
---
URL Construction (Important!)
The authorization URL format is critical and easy to get wrong.
Correct Format
Authorization: {appUrl}/apps/{APP_ID}/authorize
Token: {appUrl}/apps-api/token
Userinfo: {apiUrl}/v3/userCommon Mistakes
❌ {appUrl}/#!/apps/{APP_ID}/authorize # Hash routing doesn't work
❌ {appUrl}/apps/authorize # Missing APP_ID
❌ {appUrl}/apps/{CLIENT_ID}/authorize # Wrong ID (use APP_ID, not CLIENT_ID)URL Resolution with @timbenniks/contentstack-endpoints
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";
const endpoints = getContentstackEndpoints("eu");
// endpoints.application → "https://eu-app.contentstack.com"
// endpoints.contentManagement → "https://eu-api.contentstack.com"Full URL Examples (EU Region)
| Endpoint | URL |
|---|---|
| Authorization | https://eu-app.contentstack.com/apps/{APP_ID}/authorize |
| Token | https://eu-app.contentstack.com/apps-api/token |
| Userinfo | https://eu-api.contentstack.com/v3/user |
| Callback | https://your-app.vercel.app/api/auth/callback/contentstack |
---
Key Points
| Aspect | Detail |
|---|---|
| PKCE support | Contentstack supports PKCE — see note below |
| Custom userinfo | Contentstack returns data nested under user key |
| Region URLs | Use @timbenniks/contentstack-endpoints for correct endpoints |
| JWT required | Always use session: { strategy: "jwt" } |
| Serverless | Use auth() not getToken() — set AUTH_TRUST_HOST=true — mark auth route as dynamic |
PKCE (Proof Key for Code Exchange)
Contentstack supports PKCE for enhanced security. This example uses checks: ["state"] for simplicity, but you can enable PKCE:
{
id: "contentstack",
type: "oauth",
checks: ["pkce", "state"], // Enable PKCE
// ... rest of config
}PKCE is recommended for:
- Public clients (SPAs, mobile apps)
- Enhanced protection against authorization code interception attacks
For server-side Next.js apps with a client secret, checks: ["state"] is sufficient.
Contentstack Profile Fields
The userinfo endpoint returns:
interface ContentstackUser {
uid: string;
email: string;
first_name?: string;
last_name?: string;
username?: string;
profile_image?: string;
}Debugging Auth Issues
When auth fails on production, check these in order:
1. Browser DevTools → Application → Cookies: Is the session cookie present? What's its name (authjs.session-token vs __Secure-authjs.session-token)? 2. Environment variables: Is AUTH_SECRET set? Is AUTH_TRUST_HOST=true? Is NEXTAUTH_URL using https://? 3. Network tab: Does the /api/auth/callback/contentstack request succeed? Check for redirect loops or error responses. 4. Server logs: Look for "RefreshAccessTokenError" — this means the refresh token is expired or invalid, and the user needs to re-authenticate.
Diagnostic Report Pattern
For production debugging, you can build a diagnostic report that checks cookie state and token readability:
import { cookies } from "next/headers";
import { getToken } from "next-auth/jwt";
export async function buildAuthDiagnostics() {
const cookieStore = await cookies();
const cookieNames = cookieStore.getAll().map((c) => c.name);
const sessionCookieName = [
"__Secure-authjs.session-token",
"authjs.session-token",
].find((name) => cookieNames.includes(name));
const isSecure =
process.env.NODE_ENV === "production" ||
(process.env.AUTH_URL ?? process.env.NEXTAUTH_URL ?? "").startsWith("https");
const cookieHeader = cookieStore
.getAll()
.map(({ name, value }) => `${name}=${value}`)
.join("; ");
let tokenReadable = false;
try {
const token = await getToken({
req: { headers: { cookie: cookieHeader } },
secret: process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET,
secureCookie: isSecure,
});
tokenReadable = Boolean(token);
} catch {}
return {
sessionCookieName: sessionCookieName ?? "none",
isSecure,
tokenReadable,
authTrustHost: process.env.AUTH_TRUST_HOST ?? "unset",
authSecretSet: Boolean(process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET),
};
}References
Contentstack Base Concepts
Core concepts every AI agent needs to understand when working with Contentstack.
What is Contentstack?
Contentstack is a headless CMS that separates content management from content presentation. It provides REST and GraphQL APIs to deliver content to any platform.
Core Concepts
Stack
The top-level container for all content. Contains content types, entries, assets, environments, and configurations.
- API Key: Unique identifier (required for all API calls)
- Region: Data center location (US, EU, AU, Azure, GCP)
- Access Token: Authentication token (Delivery or Management)
Content Types
Schema definitions for content structure. Like a database table schema.
- UID: Unique identifier (e.g.,
blog_post,page) - Fields: Individual data fields with types and validation
Common Field Types:
| Type | Description |
|---|---|
text | Single-line text |
textarea | Multi-line text |
richtext | Rich text editor |
number | Numeric values |
date | Date and time |
file | File uploads |
reference | Link to other entries |
modular_block | Reusable content blocks |
group | Group of related fields |
json | Raw JSON data |
Entries
Instances of content types containing actual content data.
- UID: Unique identifier
- Title: Display name
- Locale: Language variant (e.g.,
en-us,fr-fr) - Published: Whether entry is live
Environments
Stages in content lifecycle:
- Development: Testing and development
- Staging: Pre-production testing
- Production: Live environment
Each environment can have different content versions and separate delivery tokens.
Locales
Language or regional variations:
- Each entry can exist in multiple locales
- Codes follow ISO format (e.g.,
en-us,de-de) - Default locale set at stack level
Assets
Files stored in Contentstack:
- Images, videos, documents
- Automatically served via CDN
- Support for image transformations
- Versioning and metadata
References
Links between entries:
- Single reference: Links to one entry
- Multiple references: Links to multiple entries
- Cross-content-type references supported
Modular Blocks
Reusable content components:
- Can be added/removed dynamically
- Each block has its own schema
- Enable flexible page building
API Types
Delivery API (CDA)
- Purpose: Read-only access to published content
- Token: Delivery Token
- Speed: Fast, cached, CDN-delivered
- Use: Production applications
Preview API
- Purpose: Access unpublished content
- Token: Preview Token
- Use: Live Preview, development
Management API (CMA)
- Purpose: Full CRUD operations
- Token: Management Token
- Security: NEVER expose in frontend
- Use: Backend/server-side only
Tokens Summary
| Token Type | Purpose | Client-safe? |
|---|---|---|
| Delivery Token | Read published content | ✅ yes |
| Preview Token | Read draft content (Live Preview) | ✅ yes (preview only) |
| Management Token | Full CRUD | ❌ server only |
For the full decision tree (including Authtoken and OAuth), see security/tokens-authentication.md.
Regions & Endpoints
Contentstack operates in multiple regions. Each region has specific API endpoints.
| Region | REST CDN | GraphQL |
|---|---|---|
| US (AWS) | cdn.contentstack.io | graphql.contentstack.com |
| EU (AWS) | eu-cdn.contentstack.com | eu-graphql.contentstack.com |
| AU (AWS) | au-cdn.contentstack.io | au-graphql.contentstack.com |
| Azure NA | azure-na-cdn.contentstack.io | azure-na-graphql.contentstack.com |
| Azure EU | azure-eu-cdn.contentstack.io | azure-eu-graphql.contentstack.com |
| GCP NA | gcp-na-cdn.contentstack.io | gcp-na-graphql.contentstack.com |
| GCP EU | gcp-eu-cdn.contentstack.io | gcp-eu-graphql.contentstack.com |
Important: You must configure the correct region for your stack. See the Regions Guide for complete configuration details across all SDKs and tools.
Content Workflow
Content Manager creates entry in CMS
↓
Entry saved as draft
↓
Entry previewed (Live Preview)
↓
Entry published to environment
↓
Content available via Delivery API
↓
Frontend fetches and displaysKey Terminology
| Term | Description |
|---|---|
| Stack | Top-level container for all content |
| Content Type | Schema definition for content structure |
| Entry | Instance of content type with data |
| Environment | Stage of content lifecycle |
| Locale | Language or regional variation |
| Asset | File stored in Contentstack |
| Reference | Link between entries |
| Modular Block | Reusable content component |
Localization
Contentstack models multilingual content as a tree rooted at a master language, with each language optionally falling back to another. Fields can be localized per language or shared. Entries exist per locale once localized, otherwise inherit from the fallback chain.
Master language — a permanent choice
The master language is set at stack creation and cannot be changed. It is the root of every fallback chain.
Pick it deliberately:
- The language of your primary market, OR
- The language in which content is authored first, OR
en-usif in doubt and you're an English-first org
Everything else (add/remove locales, reconfigure fallbacks) is reversible. The master is not.
Fallback chains
Each non-master language can have one fallback language. Inheritance chains follow the fallback pointer until they hit a language with content or reach the master.
Example:
fr-ca → fr-fr → en-us (master)
de-at → de-de → en-us (master)
ja-jp → en-us (master) # direct fallbackAuthoring in fr-fr automatically flows to fr-ca unless fr-ca has its own localized version. Changing a fallback relationship later affects inheritance for existing content — do it carefully.
Localized vs unlocalized entries
This distinction causes most localization confusion.
| Localized entry | Unlocalized entry | |
|---|---|---|
| Exists as | Independent copy with its own fields, version history, publishing status, workflow state | Lives only in the fallback chain |
| Editors | Can edit per locale independently | Edit in the fallback source; changes propagate |
| CDA response | Returns the localized entry directly | Returns the fallback content with _in_progress or locale metadata indicating inheritance |
| Locked in? | Yes — localizing is one-way per (entry, locale) pair | No — you can always localize later |
Rule of thumb: only localize when the content genuinely differs. Localizing "just because" creates drift — a change in the master won't reach translated copies anymore.
Non-localizable fields
Mark these non-localizable at the content-type level:
- Identifiers (SKUs, ISBNs, product codes)
- Numeric/boolean data (prices, quantities, flags)
- Dates and timestamps
- Coordinates
- Shared assets (logos, icons that are language-agnostic)
- URLs and slugs that shouldn't differ per locale
- References to shared entities
Keep these localizable (default):
- Titles, headlines, body copy
- Descriptions, summaries, meta tags
- Alt text on translated images
- CTAs, button labels
- Language-specific URLs/slugs
Content Management — locale APIs
# List locales configured on the stack
GET /v3/locales
# Create a new locale with a fallback
POST /v3/locales
Body: { "locale": { "code": "fr-ca", "name": "French (Canada)", "fallback_locale": "fr-fr" } }Headers: standard CMA auth (api_key + authorization: <management_token>).
Delivery — querying by locale
Pass locale explicitly in every CDA call. Default behavior without `include_fallback=true`: only the exact locale is checked. A missing localized entry returns nothing.
GET /v3/content_types/blog_post/entries?locale=fr-caTo get fallback behavior (the whole chain, first hit wins):
GET /v3/content_types/blog_post/entries?locale=fr-ca&include_fallback=trueWith the TypeScript SDK:
const entry = await stack
.contentType("blog_post")
.entry(entryUid)
.locale("fr-ca")
.includeFallback()
.fetch();GraphQL: pass locale as an argument; fallback behavior is controlled per query — check the GraphQL schema for the include_fallback/fallback argument name on your schema version.
Multi-locale publishing
From the master-language entry, editors can publish to multiple locales at once:
// CMA publish payload
{
"entry": {
"environments": ["production"],
"locales": ["en-us", "fr-fr", "de-de"]
}
}Notes:
- Only the latest version of each localized entry publishes.
- Plan limits cap simultaneous locale counts — check your plan.
- Scheduled publishing works per-locale.
Editorial gotchas
- Localized entry versions can only be deleted from the master-language entry's delete modal. If you need to remove a stale translation, open the master entry, not the localized one.
- Re-localizing after master changes requires re-copying fields manually (or via a CMA script) — Contentstack does not auto-propagate once localized.
- Workflow state is per-localized-entry — a page can be
Approvedinen-usandDraftinfr-frsimultaneously.
Strategy patterns
Global brand, many markets, shared visuals, translated text:
- Most fields localizable (text).
- Assets, prices, SKUs non-localizable.
- Translation agencies localize entries after master approval.
Regional sites with divergent layouts:
- Separate entries per region — not locale-localization.
- Use references and a region field instead.
Multi-channel (web + mobile + in-store):
- Don't model channels as locales. Use separate content types or variants (see
../personalization/variants-and-personalize.md).
Red flags
- Changing master language post-launch — you can't.
- Localizing entries preemptively before content actually differs — creates maintenance drift.
- Forgetting
include_fallback=trueon CDA calls and wondering why some locales return empty. - Translating SKUs, prices, or coordinates — mark those non-localizable.
- Expecting master-language edits to propagate to already-localized entries — they don't.
Practical Examples
Real-world implementation patterns for Contentstack. All examples assume you've created a shared stack instance.
Shared Stack Instance
// lib/contentstack.ts - Import this in all examples
import contentstack from "@contentstack/delivery-sdk";
export const stack = contentstack.stack({
apiKey: process.env.CONTENTSTACK_API_KEY!,
deliveryToken: process.env.CONTENTSTACK_DELIVERY_TOKEN!,
environment: process.env.CONTENTSTACK_ENVIRONMENT!,
region: process.env.CONTENTSTACK_REGION || "us",
});---
Rendering Rich Text
React
import DOMPurify from "isomorphic-dompurify";
function RichText({ content }: { content: string }) {
return (
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(content),
}}
/>
);
}Vue
<script setup>
import DOMPurify from "dompurify";
const props = defineProps<{ content: string }>();
const sanitized = computed(() => DOMPurify.sanitize(props.content));
</script>
<template>
<div v-html="sanitized"></div>
</template>---
Working with References
Single Reference
import { stack } from "@/lib/contentstack";
const entry = await stack
.contentType("blog_post")
.entry("entry_uid")
.includeReference(["author"])
.fetch();
// Access
console.log(entry.author.name);Multiple References
const entry = await stack
.contentType("blog_post")
.entry("entry_uid")
.includeReference(["author", "category", "tags"])
.fetch();Render Reference Array
function RelatedPosts({ posts }: { posts: Post[] }) {
return (
<ul>
{posts?.map((post) => (
<li key={post.uid}>
<a href={post.url}>{post.title}</a>
</li>
))}
</ul>
);
}---
Handling Modular Blocks
Block Structure
Contentstack returns modular blocks as:
{
blocks: [
{
hero_block: { title: "...", image: {...} }
},
{
content_block: { title: "...", body: "..." }
}
]
}React Block Renderer
function BlockRenderer({ blocks }: { blocks: any[] }) {
return (
<>
{blocks?.map((item, index) => {
if (item.hero_block) {
return <HeroBlock key={index} data={item.hero_block} />;
}
if (item.content_block) {
return <ContentBlock key={index} data={item.content_block} />;
}
if (item.cta_block) {
return <CTABlock key={index} data={item.cta_block} />;
}
return null;
})}
</>
);
}Block Components
function HeroBlock({ data }: { data: HeroBlockData }) {
return (
<section className="hero">
<h1>{data.title}</h1>
{data.image && <img src={data.image.url} alt={data.title} />}
{data.description && <p>{data.description}</p>}
</section>
);
}
function ContentBlock({ data }: { data: ContentBlockData }) {
return (
<section className="content">
<h2>{data.title}</h2>
<RichText content={data.body} />
</section>
);
}---
Asset Transformations
Transform Function
interface TransformOptions {
width?: number;
height?: number;
quality?: number;
format?: "auto" | "webp" | "jpeg" | "png";
fit?: "bounds" | "crop" | "pad";
}
function transformImage(url: string, options: TransformOptions): string {
const params = new URLSearchParams();
if (options.width) params.append("width", options.width.toString());
if (options.height) params.append("height", options.height.toString());
if (options.quality) params.append("quality", options.quality.toString());
if (options.format) params.append("format", options.format);
if (options.fit) params.append("fit", options.fit);
return params.toString() ? `${url}?${params.toString()}` : url;
}
// Usage
const thumbnail = transformImage(asset.url, { width: 400, height: 300, fit: "crop" });
const optimized = transformImage(asset.url, { width: 800, format: "webp", quality: 85 });Responsive Image Component
function ResponsiveImage({ asset }: { asset: Asset }) {
const srcSet = [
`${asset.url}?width=400 400w`,
`${asset.url}?width=800 800w`,
`${asset.url}?width=1200 1200w`,
].join(", ");
return (
<img
src={asset.url}
srcSet={srcSet}
sizes="(max-width: 768px) 400px, (max-width: 1200px) 800px, 1200px"
alt={asset.title || asset.filename}
/>
);
}---
Common Patterns
Blog Listing with Pagination
import { stack } from "@/lib/contentstack";
async function getBlogPosts(page = 1, pageSize = 10) {
const skip = (page - 1) * pageSize;
const result = await stack
.contentType("blog_post")
.entry()
.includeReference(["author"])
.query()
.skip(skip)
.limit(pageSize)
.includeCount()
.orderByDescending("published_date")
.find();
return {
posts: result.entries,
pagination: {
currentPage: page,
totalPages: Math.ceil(result.count / pageSize),
totalEntries: result.count,
},
};
}Get Entry by URL
import { QueryOperation } from "@contentstack/delivery-sdk";
import { stack } from "@/lib/contentstack";
async function getPageByUrl(url: string) {
const result = await stack
.contentType("page")
.entry()
.query()
.where("url", QueryOperation.EQUALS, url)
.find();
return result.entries[0] || null;
}Navigation Menu
async function getNavigation() {
const result = await stack
.contentType("navigation_item")
.entry()
.query()
.orderByAscending("order")
.find();
return result.entries;
}Related Posts
async function getRelatedPosts(currentUid: string, category: string) {
const result = await stack
.contentType("blog_post")
.entry()
.query()
.where("category", QueryOperation.EQUALS, category)
.addQuery({ uid: { $ne: currentUid } })
.limit(3)
.find();
return result.entries;
}---
TypeScript Types
import { Entry } from "@contentstack/delivery-sdk";
interface Asset {
uid: string;
url: string;
title: string;
filename: string;
dimension?: { width: number; height: number };
}
interface BlogPost extends Entry {
title: string;
url: string;
excerpt: string;
content: string;
published_date: string;
featured_image?: Asset;
author?: Author;
category?: Category;
tags?: Tag[];
}
interface Author extends Entry {
name: string;
bio: string;
avatar?: Asset;
}
interface Page extends Entry {
title: string;
url: string;
blocks?: ModularBlock[];
}
interface ModularBlock {
hero_block?: HeroBlockData;
content_block?: ContentBlockData;
cta_block?: CTABlockData;
}---
Locale Handling
Fetch in Specific Locale
const entry = await stack
.contentType("page")
.entry("entry_uid")
.language("fr-fr")
.fetch();Locale Switcher
const LOCALES = ["en-us", "fr-fr", "de-de"];
function LocaleSwitcher({ current }: { current: string }) {
return (
<select value={current} onChange={(e) => switchLocale(e.target.value)}>
{LOCALES.map((locale) => (
<option key={locale} value={locale}>
{locale.toUpperCase()}
</option>
))}
</select>
);
}---
Error Handling
Safe Fetch Pattern
async function safeFetch<T>(
fetcher: () => Promise<T>
): Promise<{ data: T | null; error: string | null }> {
try {
const data = await fetcher();
return { data, error: null };
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error("Contentstack fetch error:", message);
return { data: null, error: message };
}
}
// Usage
const { data: post, error } = await safeFetch(() =>
stack.contentType("blog_post").entry("uid").fetch()
);
if (error) {
return <ErrorMessage message={error} />;
}