# APIs and Developer Tooling

### About this export

| Field | Value |
| --- | --- |
| **content_type** | course |
| **platform** | contentstack-academy |
| **source_url** | https://www.contentstack.com/academy/courses/apis-and-developer-tooling |
| **language** | en |
| **product_area** | Contentstack Academy |
| **learning_path** | cms-developer-certification |
| **course_id** | apis-and-developer-tooling |
| **slug** | apis-and-developer-tooling |
| **version** | 2026-07-31 |
| **last_updated** | 2026-08-03 |
| **status** | published |
| **keywords** | ["Contentstack Academy"] |
| **summary_one_line** | APIs and Developer Tooling Turn Contentstack concepts into implementation muscle memory through API design, SDK usage, rendering patterns, environment strategy, and CLI-driven workflows. Who This Course Is For This cours… |
| **total_duration_minutes** | 96 |
| **lessons_count** | 15 |
| **video_lessons_count** | 0 |
| **text_lessons_count** | 15 |
| **linked_learning_path** | cms-developer-certification |
| **linked_assessment_ref** | LMS_UNCONFIGURED_COURSE_ASSESSMENT |
| **markdown_file_url** | /academy/md/courses/apis-and-developer-tooling.md |
| **generated_at** | 2026-08-03T11:49:29.583Z |
| **intended_audience** | [] |
| **prerequisites** | [] |
| **related_courses** | [] |

> **Academy MD v3** — companion `.md` for Ask AI. Quizzes and graded assessments are **LMS-only**; this file never contains answer keys.

## Course Overview

| Metadata | Value |
| --- | --- |
| Catalog duration | 1h 36m 25s |
| Released (if known) | 2026-07-31 |
| Product area | Contentstack Academy |

### Description

# APIs and Developer Tooling

Turn Contentstack concepts into implementation muscle memory through API design, SDK usage, rendering patterns, environment strategy, and CLI-driven workflows.

## Who This Course Is For

This course is for developers who are ready to write code, call APIs, and connect Contentstack to a real application or deployment pipeline.

## You Will Be Able To

*   choose the right API surface and credential model for each responsibility
*   initialize SDKs, compose queries, and render entries safely in code
*   align environments, CI/CD, and migrations with your content delivery strategy

## Recommended Preparation

Complete Courses 0-2 first. You should be comfortable with TypeScript, Node.js, async/await, and basic HTTP requests.

## Estimated Effort

1.5 - hours

## Build Thread

You will fetch and render Veda storefront content, then carry that same project through environment setup, promotion strategy, and CLI migration flows.

## Suggested Next Step

Begin with [Delivery API vs Management API responsibilities](https://contentstack-developer-certification.eu-contentstackapps.com/course-3-apis-and-developer-tooling/module-3-1-api-architecture-and-authentication/01-delivery-api-vs-management-api-responsibilities).

### Learning objectives

1. Follow each lesson in order.
2. Practice in a training stack using placeholders **YOUR_STACK_API_KEY** and **YOUR_DELIVERY_TOKEN** in local `.env` files only.
3. Validate API responses against the official documentation.

### Topics covered

Contentstack Academy

## Course structure

```text
apis-and-developer-tooling/
├── 01-api-architecture-and-authentication-overview · text · 3 min
├── 02-delivery-api-vs-management-api-responsibilities · text · 1 min
├── 03-regions-clouds-and-api-endpoints · text · 1 min
├── 04-rest-vs-graphql-choosing-the-right-query-surface · text · 1 min
├── 05-authentication-and-access-control-concepts · text · 1 min
├── 06-rate-limiting-error-codes-and-retry-patterns · text · 1 min
├── 07-fetching-and-rendering-content-overview · text · 1 min
├── 08-sdk-initialization-and-query-patterns · text · 1 min
├── 09-references-includes-and-localized-content-retrieval · text · 1 min
├── 10-image-delivery-and-transformation-apis · text · 1 min
├── 11-performance-caching-and-frontend-integration · text · 1 min
├── 12-environments-and-deployment-overview · text · 1 min
├── 13-environments-publishing-and-promotion-strategies · text · 1 min
├── 14-aligning-cms-workflows-with-ci-cd · text · 1 min
├── 15-contentstack-cli-and-content-migration · text · 1 min
```

## Lessons

### Lesson 01 — API Architecture and Authentication : Overview

<!-- ai_metadata: {"lesson_id":"01","type":"text","duration_minutes":3,"topics":["API","Architecture","and","Authentication","Overview"]} -->

#### Lesson text

# API Architecture and Authentication

This module gives you the decision framework for choosing the right API surface, runtime boundary, and authentication strategy before you write implementation code.

## Why This Module Matters

API mistakes in Contentstack are rarely syntax problems. They are boundary problems: wrong plane, wrong token, wrong runtime, or wrong assumption about ownership.

## You Will Be Able To

*   distinguish delivery, preview, and management responsibilities clearly
*   choose between REST and GraphQL based on use case rather than trend
*   apply safer authentication and credential-scoping practices

## Recommended Preparation

Complete Courses 1 and 2 first, then begin Course 3 when you are ready to connect concepts to code.

## Estimated Effort

75-90 minutes

## Practice Focus

Use the Veda storefront to map which services read published content, which services need privileged write access, and which credentials belong where.

## Suggested Next Step

Start with lesson 1 in this module and trace every example back to a concrete runtime or ownership boundary.

#### Key takeaways

- Connect **API Architecture and Authentication : Overview** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 02 — Delivery API vs management API responsibilities

<!-- ai_metadata: {"lesson_id":"02","type":"text","duration_minutes":1,"topics":["Delivery","API","management","API","responsibilities"]} -->

#### Lesson text

# Delivery API vs management API responsibilities

> **TL;DR**
> 
> *   Use the Content Delivery API (CDA) for all frontend/runtime content reads and the Content Management API (CMA) for back-office operations only.
> *   Separate your codebase into a content-read layer (CDA) and a content-admin layer (CMA), each with its own token lifecycle and failure policy.
> *   Choose the API by intent and data state, not by what returns results quickest during local development.

Most integration mistakes in headless projects are not caused by missing features. They are caused by blurry boundaries. This lesson exists to make one boundary unambiguous: the API used to deliver published content to experiences is not the API used to manage content operations.

In Contentstack terms, that means understanding when to use Content Delivery APIs (REST CDA or GraphQL CDA) versus the Content Management API (CMA), even though CMA also exposes GET operations. If you internalize this distinction early, you avoid a long list of production problems: token leaks, poor cache behavior, brittle frontend coupling, and unnecessary operational risk.

## Why this boundary exists

Think in terms of system responsibilities:

*   Delivery APIs are optimized for reading published content for runtime experiences.
*   CMA is optimized for content operations: creating, updating, deleting, workflow-related actions, environment management, branch-level administration, and other control-plane tasks.

Contentstack’s own docs make this explicit in two directions:

1.  CMA docs state that while CMA includes GET endpoints, teams should use Content Delivery API to deliver content to web/mobile properties.
2.  CDA docs emphasize CDA is for content retrieval and warn against using it for management-style activity.

That is not just documentation preference. It is architecture guidance: separate your **delivery plane** from your **management plane**.

## The two APIs solve different reliability problems

A useful mental model is: these APIs solve different classes of reliability and security requirements.

### Delivery API reliability profile

Delivery APIs are designed for high-volume read traffic and content delivery semantics:

*   Environment-scoped delivery tokens
*   CDN-backed retrieval behavior
*   Query patterns designed for runtime reads
*   Published-content focus

This aligns with frontend traffic patterns: many reads, low tolerance for latency spikes, predictable query contracts, aggressive caching where appropriate.

### CMA reliability profile

CMA is designed for controlled mutation and administrative operations:

*   Read-write capabilities
*   Token models tied to management permissions (authtoken, management token, OAuth token)
*   Operational endpoints beyond content retrieval
*   Stronger coupling to governance and workflow state

This aligns with back-office traffic patterns: fewer requests, higher privilege, explicit auditability, stricter change control.

Trying to collapse both planes into one endpoint strategy usually creates a system that is neither secure enough for management nor efficient enough for delivery.

## Decision framework: choose the API by intent, not convenience

Use this decision sequence when implementing any new integration:

1.  What is the intent of this call?
    *   Render user-facing experience with published content: CDA/GraphQL CDA.
    *   Manage content, schema, workflow, tokens, environments, branches, publishing actions: CMA.
2.  What data state do you need?
    *   Published runtime state: delivery plane.
    *   Draft/control-plane/administrative manipulation: management plane.
3.  Where does this call run?
    *   Browser or edge-rendered user route: never requires privileged management credentials.
    *   Trusted backend worker or orchestration service: may use CMA with least privilege.
4.  What token can safely exist in this runtime?
    *   Browser-safe read token for delivery use cases.
    *   Privileged management token only in trusted server context.
5.  What happens if this endpoint is abused?
    *   Delivery misuse typically impacts performance/cost.
    *   Management misuse can impact data integrity and security.

If your answers mix two intent classes in one path, split the design before writing code.

## Architecture pattern that scales

A durable pattern is to create two explicit integration layers in your codebase:

*   content-read layer: wraps CDA/GraphQL CDA and exposes domain read contracts for app teams.
*   content-admin layer: wraps CMA for operational workflows, migrations, and automation jobs.

Each layer gets its own:

*   token lifecycle
*   observability
*   rate/latency SLO expectations
*   failure-handling policy

This prevents accidental privilege creep. It also lets teams evolve frontend frameworks without repeatedly touching CMS administration logic.

## Worked scenario

Imagine product asks for a new “Related Articles” strip on article pages.

A common but flawed path is to let frontend developers use whichever endpoint gives quick access to references, including CMA reads, because “it works locally.”

A better path:

1.  Model and maintain references via CMA-backed tooling (if needed).
2.  Deliver runtime related entries through CDA/GraphQL CDA only.
3.  Expose a stable read contract to frontend teams (articleDetail + related\[\]).
4.  Track payload size and include depth as production metrics.

The result is cleaner ownership:

*   editorial and model governance concerns stay in management workflows
*   runtime rendering concerns stay in delivery contracts

## Light implementation example

The point of the code below is not syntax. It is architectural separation by construction.

// api-clients.ts
export const deliveryClient = {
  host: "https://cdn.contentstack.io",
  headers: {
    api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
    access\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  },
};

export const managementClient = {
  host: "https://api.contentstack.io",
  headers: {
    api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
    authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
  },
};

// Rule: frontend request handlers can import deliveryClient only.
// CMA calls stay in trusted back-office services.

Even simple module boundaries like this reduce accidental misuse dramatically.

## Common failure modes (and prevention)

> **Common pitfall:** Using CMA GET endpoints for frontend delivery seems to work locally but introduces token exposure risk, poor caching behavior, and audit confusion in production.

### Failure mode 1: using CMA reads for frontend delivery

Why it happens: teams optimize for immediate convenience.

What breaks later: token exposure risk, weak caching behavior, mixed responsibilities, harder audits.

Prevention: lint/design rule that frontend runtime packages cannot import management clients.

### Failure mode 2: treating all GET endpoints as equivalent

Why it happens: “GET means read, so it’s safe anywhere.”

What breaks later: governance and runtime concerns bleed into each other.

Prevention: classify endpoints by intent (delivery-plane vs management-plane), not HTTP verb.

### Failure mode 3: token strategy designed after integration ships

Why it happens: security is deferred to “hardening phase.”

What breaks later: painful refactors, emergency rotations, uncertain blast radius.

Prevention: design token ownership and runtime placement before API contract implementation.

### Failure mode 4: no contract boundary for frontend consumers

Why it happens: each team writes direct CMS queries inside feature code.

What breaks later: duplicated logic, inconsistent locale/branch handling, brittle migrations.

Prevention: centralized read contracts with versioning and observability.

## Summary

If you remember one principle, keep this one: choose API by responsibility boundary, not by short-term implementation speed.

*   Delivery APIs are your runtime content plane.
*   CMA is your control plane.

When that separation is explicit in code, token policy, and team ownership, your system becomes easier to secure, easier to scale, and easier to change.

#### Key takeaways

- Connect **Delivery API vs management API responsibilities** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 03 — Regions, clouds, and API endpoints

<!-- ai_metadata: {"lesson_id":"03","type":"text","duration_minutes":1,"topics":["Regions","clouds","and","API","endpoints"]} -->

#### Lesson text

# Regions, clouds, and API endpoints

> **TL;DR**
> 
> *   Your stack's region is locked at creation and determines every API base URL your code targets.
> *   Use the SDK's built-in region constants instead of hardcoding base URLs -- the SDK constructs the correct endpoints automatically.
> *   When credentials are correct but requests return 401 or empty results, verify the region first -- error messages never mention a region mismatch.

Contentstack operates across multiple cloud providers and geographic regions. When you create a stack, you choose a region. That choice determines which data center hosts your content and which base URLs every API call targets. Getting the region wrong is one of the most common causes of silent failures - requests return empty results or authentication errors with no indication that the URL itself is the problem.

This lesson maps out Contentstack's infrastructure so you can configure projects correctly, debug region-related issues quickly, and make informed decisions about where your content lives.

## Why regions matter

Three concerns drive the multi-region architecture:

1.  Data residency - Regulated industries and regional privacy laws (GDPR, data sovereignty requirements) may mandate that content data stays within a specific geography. Choosing the correct region ensures compliance at the infrastructure level.
2.  Latency - API responses are faster when the data center is geographically closer to your server or edge function. While Contentstack's CDN layer mitigates this for cached delivery requests, management API calls and cache misses benefit from regional proximity.
3.  Cloud provider preference - Some organizations have existing commitments to AWS, Azure, or GCP. Contentstack supports all three, so you can align your CMS infrastructure with your broader cloud strategy.

## Available regions

Contentstack currently offers seven regions across three cloud providers:

Region

Cloud provider

Geography

AWS North America

Amazon Web Services

United States

AWS Europe

Amazon Web Services

Europe

AWS Australia

Amazon Web Services

Australia

Azure North America

Microsoft Azure

United States

Azure Europe

Microsoft Azure

Europe

GCP North America

Google Cloud Platform

United States

GCP Europe

Google Cloud Platform

Europe

Your region is locked at stack creation. You cannot migrate a stack between regions after creation. If you need to move content to a different region, you must create a new stack in the target region and migrate content using the CLI or Management API.

## How to find your stack's region

Open the Contentstack dashboard and navigate to Settings > Stack. The region is displayed in the stack information panel. You can also identify it from the URL in your browser - the dashboard URL includes a region indicator (for example, eu-app.contentstack.com for the AWS Europe region, or azure-na-app.contentstack.com for Azure North America).

## API endpoints by region

Every Contentstack API surface has a different base URL per region. The tables below cover the endpoints you will use most frequently as a developer.

### Core APIs

Service

AWS NA

AWS EU

AWS AU

Content Delivery API

cdn.contentstack.io

eu-cdn.contentstack.com

au-cdn.contentstack.com

Content Management API

api.contentstack.io

eu-api.contentstack.com

au-api.contentstack.com

Auth

auth-api.contentstack.com

eu-auth-api.contentstack.com

au-auth-api.contentstack.com

REST Preview

rest-preview.contentstack.com

eu-rest-preview.contentstack.com

au-rest-preview.contentstack.com

GraphQL Content Delivery

graphql.contentstack.com

eu-graphql.contentstack.com

au-graphql.contentstack.com

GraphQL Preview

graphql-preview.contentstack.com

eu-graphql-preview.contentstack.com

au-graphql-preview.contentstack.com

Application (Web App)

app.contentstack.com

eu-app.contentstack.com

au-app.contentstack.com

Service

Azure NA

Azure EU

Content Delivery API

azure-na-cdn.contentstack.com

azure-eu-cdn.contentstack.com

Content Management API

azure-na-api.contentstack.com

azure-eu-api.contentstack.com

Auth

azure-na-auth-api.contentstack.com

azure-eu-auth-api.contentstack.com

REST Preview

azure-na-rest-preview.contentstack.com

azure-eu-rest-preview.contentstack.com

GraphQL Content Delivery

azure-na-graphql.contentstack.com

azure-eu-graphql.contentstack.com

GraphQL Preview

azure-na-graphql-preview.contentstack.com

azure-eu-graphql-preview.contentstack.com

Application (Web App)

azure-na-app.contentstack.com

azure-eu-app.contentstack.com

Service

GCP NA

GCP EU

Content Delivery API

gcp-na-cdn.contentstack.com

gcp-eu-cdn.contentstack.com

Content Management API

gcp-na-api.contentstack.com

gcp-eu-api.contentstack.com

Auth

gcp-na-auth-api.contentstack.com

gcp-eu-auth-api.contentstack.com

REST Preview

gcp-na-rest-preview.contentstack.com

gcp-eu-rest-preview.contentstack.com

GraphQL Content Delivery

gcp-na-graphql.contentstack.com

gcp-eu-graphql.contentstack.com

GraphQL Preview

gcp-na-graphql-preview.contentstack.com

gcp-eu-graphql-preview.contentstack.com

Application (Web App)

gcp-na-app.contentstack.com

gcp-eu-app.contentstack.com

### Assets and images

Service

AWS NA

AWS EU

AWS AU

Image Delivery

images.contentstack.io

eu-images.contentstack.com

au-images.contentstack.com

Asset Delivery

assets.contentstack.io

eu-assets.contentstack.com

au-assets.contentstack.com

Service

Azure NA

Azure EU

Image Delivery

azure-na-images.contentstack.com

azure-eu-images.contentstack.com

Asset Delivery

azure-na-assets.contentstack.com

azure-eu-assets.contentstack.com

Service

GCP NA

GCP EU

Image Delivery

gcp-na-images.contentstack.com

gcp-eu-images.contentstack.com

Asset Delivery

gcp-na-assets.contentstack.com

gcp-eu-assets.contentstack.com

### Platform services

Service

AWS NA

AWS EU

AWS AU

Launch

launch-api.contentstack.com

eu-launch-api.contentstack.com

au-launch-api.contentstack.com

Automate

automations-api.contentstack.com

eu-prod-automations-api.contentstack.com

au-prod-automations-api.contentstack.com

Developer Hub

developerhub-api.contentstack.com

eu-developerhub-api.contentstack.com

au-developerhub-api.contentstack.com

Personalize (Management)

personalize-api.contentstack.com

eu-personalize-api.contentstack.com

au-personalize-api.contentstack.com

Personalize (Edge)

personalize-edge.contentstack.com

eu-personalize-edge.contentstack.com

au-personalize-edge.contentstack.com

Brand Kit

brand-kits-api.contentstack.com

eu-brand-kits-api.contentstack.com

au-brand-kits-api.contentstack.com

GenAI (Knowledge Vault)

ai.contentstack.com/brand-kits

eu-ai.contentstack.com/brand-kits

au-ai.contentstack.com/brand-kits

Composable Studio

composable-studio-api.contentstack.com

eu-composable-studio-api.contentstack.com

au-composable-studio-api.contentstack.com

Service

Azure NA

Azure EU

Launch

azure-na-launch-api.contentstack.com

azure-eu-launch-api.contentstack.com

Automate

azure-na-automations-api.contentstack.com

azure-eu-automations-api.contentstack.com

Developer Hub

azure-na-developerhub-api.contentstack.com

azure-eu-developerhub-api.contentstack.com

Personalize (Management)

azure-na-personalize-api.contentstack.com

azure-eu-personalize-api.contentstack.com

Personalize (Edge)

azure-na-personalize-edge.contentstack.com

azure-eu-personalize-edge.contentstack.com

Brand Kit

azure-na-brand-kits-api.contentstack.com

azure-eu-brand-kits-api.contentstack.com

GenAI (Knowledge Vault)

azure-na-ai.contentstack.com/brand-kits

azure-eu-ai.contentstack.com/brand-kits

Composable Studio

azure-na-composable-studio-api.contentstack.com

azure-eu-composable-studio-api.contentstack.com

Service

GCP NA

GCP EU

Launch

gcp-na-launch-api.contentstack.com

gcp-eu-launch-api.contentstack.com

Automate

gcp-na-automations-api.contentstack.com

gcp-eu-automations-api.contentstack.com

Developer Hub

gcp-na-developerhub-api.contentstack.com

gcp-eu-developerhub-api.contentstack.com

Personalize (Management)

gcp-na-personalize-api.contentstack.com

gcp-eu-personalize-api.contentstack.com

Personalize (Edge)

gcp-na-personalize-edge.contentstack.com

gcp-eu-personalize-edge.contentstack.com

Brand Kit

gcp-na-brand-kits-api.contentstack.com

gcp-eu-brand-kits-api.contentstack.com

GenAI (Knowledge Vault)

gcp-na-ai.contentstack.com/brand-kits

gcp-eu-ai.contentstack.com/brand-kits

Composable Studio

gcp-na-composable-studio-api.contentstack.com

gcp-eu-composable-studio-api.contentstack.com

> **Tip:**
> 
> These endpoints are sourced from the [official Contentstack regions data](https://artifacts.contentstack.com/regions.json). For the most up-to-date list, see the [Contentstack API endpoints documentation](/docs/developers/contentstack-regions/api-endpoints). The [@timbenniks/contentstack-endpoints](https://www.npmjs.com/package/@timbenniks/contentstack-endpoints) package auto-syncs with this same data source weekly.

## The URL naming pattern

Notice the naming convention across all endpoints. AWS North America uses the base domain without a prefix (cdn.contentstack.io, api.contentstack.io). Every other region prepends a region identifier:

*   AWS EU: eu- prefix (e.g., eu-cdn.contentstack.com)
*   AWS AU: au- prefix (e.g., au-cdn.contentstack.com)
*   Azure NA: azure-na- prefix (e.g., azure-na-cdn.contentstack.com)
*   Azure EU: azure-eu- prefix (e.g., azure-eu-cdn.contentstack.com)
*   GCP NA: gcp-na- prefix (e.g., gcp-na-cdn.contentstack.com)
*   GCP EU: gcp-eu- prefix (e.g., gcp-eu-cdn.contentstack.com)

Also note that AWS NA uses the .io TLD (contentstack.io) while all other regions use .com (contentstack.com). This is a common source of copy-paste errors.

## SDK region configuration

When you use @contentstack/delivery-sdk, the SDK handles URL construction for you. You set the region during initialization and the SDK routes all requests to the correct endpoints.

import Contentstack from "@contentstack/delivery-sdk";

const stack = Contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN,
  environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT,
  region: Contentstack.Region.EU,
});

The SDK exposes region constants that map to the correct base URLs:

SDK constant

Region

Contentstack.Region.US

AWS North America

Contentstack.Region.EU

AWS Europe

Contentstack.Region.AU

AWS Australia

Contentstack.Region.AZURE\_NA

Azure North America

Contentstack.Region.AZURE\_EU

Azure Europe

Contentstack.Region.GCP\_NA

GCP North America

Contentstack.Region.GCP\_EU

GCP Europe

When you set the region, the SDK constructs the correct CDN, API, and asset URLs automatically. You never need to hardcode base URLs in your application.

### Resolving endpoints beyond the Delivery SDK

The Delivery SDK handles region routing for content fetching, but Live Preview and Visual Builder require additional region-specific URLs that the SDK does not expose directly -- specifically the REST Preview host (for the live\_preview config) and the Application host (for clientUrlParams.host in the Live Preview SDK). These are covered in Course 4: Live Preview and Visual Builder.

The community package [@timbenniks/contentstack-endpoints](https://www.npmjs.com/package/@timbenniks/contentstack-endpoints) provides a lightweight helper that maps a region string to every Contentstack endpoint URL. It has zero dependencies and stays in sync with the [official regions data](https://artifacts.contentstack.com/regions.json).

import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

const endpoints = getContentstackEndpoints(process.env.CONTENTSTACK\_REGION || "na");

endpoints.contentDelivery;  // e.g. https://eu-cdn.contentstack.com
endpoints.preview;          // e.g. https://eu-rest-preview.contentstack.com
endpoints.application;      // e.g. https://eu-app.contentstack.com
endpoints.graphqlPreview;   // e.g. https://eu-graphql-preview.contentstack.com

Pass true as the second argument to strip the https:// prefix, which is what the Live Preview SDK expects for host values:

const endpoints = getContentstackEndpoints("eu", true);
endpoints.preview;      // eu-rest-preview.contentstack.com
endpoints.application;  // eu-app.contentstack.com

This is a convenience wrapper -- you can always look up the correct host from the tables above and set it manually. The package eliminates the risk of hardcoding the wrong host for your region.

## Debugging region mismatches

> **Common pitfall:** A wrong region produces 401 or empty-result errors with no indication that the URL itself is the problem -- making it one of the hardest misconfigurations to diagnose.

A wrong region is one of the hardest issues to debug because the error messages do not tell you the region is wrong. Here is what you typically see:

*   401 Unauthorized - The API key and token are valid, but for a different region's endpoint. The target region does not recognize them.
*   Empty results - The request succeeds (200 status) but returns zero entries because the stack does not exist in the region you are querying.
*   412 Precondition Failed - Some region/credential combinations produce this instead of a 401.

If you encounter any of these and your credentials are correct, verify the region first. Check Settings > Stack in the dashboard and compare the region against your SDK configuration or API base URL.

A quick diagnostic: make the same request with curl using the base URL for the region shown in your dashboard. If that works but your application does not, your application is targeting the wrong endpoint.

\# Test against AWS NA
curl -s -o /dev/null -w "%{http\_code}" \\
  -H "api\_key: YOUR\_API\_KEY" \\
  -H "access\_token: YOUR\_DELIVERY\_TOKEN" \\
  "https://cdn.contentstack.io/v3/content\_types/YOUR\_CONTENT\_TYPE/entries?environment=YOUR\_ENV"

# Test against AWS EU
curl -s -o /dev/null -w "%{http\_code}" \\
  -H "api\_key: YOUR\_API\_KEY" \\
  -H "access\_token: YOUR\_DELIVERY\_TOKEN" \\
  "https://eu-cdn.contentstack.com/v3/content\_types/YOUR\_CONTENT\_TYPE/entries?environment=YOUR\_ENV"

One of these returns 200, the other returns 401 or 412. The one that returns 200 is your actual region.

#### Key takeaways

- Connect **Regions, clouds, and API endpoints** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 04 — REST vs GraphQL: choosing the right query surface

<!-- ai_metadata: {"lesson_id":"04","type":"text","duration_minutes":1,"topics":["REST","GraphQL","choosing","the","right","query"]} -->

#### Lesson text

# REST vs GraphQL: choosing the right query surface

> **TL;DR**
> 
> *   Both REST CDA and GraphQL CDA are read-only, use the same delivery tokens, and return published content only.
> *   Choose REST for simple lookups with strong CDN caching; choose GraphQL when you need precise field selection or multi-type aggregation in a single request.
> *   GraphQL CDA does not support mutations -- all writes go through the REST-based CMA.

Contentstack exposes two Content Delivery API surfaces: a REST API and a GraphQL API. Both return published content, both use the same delivery tokens, and both sit behind CDN-backed infrastructure. The difference is in how you query, what you get back, and which tradeoffs matter for your use case.

Choosing between them is not a philosophical debate. It is a practical decision based on your query complexity, payload requirements, and team familiarity.

## Two APIs, one delivery plane

Both the REST CDA and GraphQL CDA share these characteristics:

*   Read-only: Neither supports mutations. All content creation, updates, and deletions go through the Content Management API (CMA).
*   Published content only: Both return only published entries for the specified environment. For draft content, use the Preview API.
*   Delivery token authentication: Both use the same access\_token (delivery token) scoped to an environment.
*   CDN-backed: Both benefit from Contentstack's CDN layer for caching and global distribution.

The key differences are in query flexibility, response shape, and caching behavior.

## REST CDA: predictable queries, CDN-friendly caching

The REST Content Delivery API follows standard REST conventions. You query specific content types, and the API returns all fields for matching entries.

Base URL: https://cdn.contentstack.io (North America region)

### Strengths

*   Simple and predictable: Each content type has a straightforward endpoint. Fetching all blog posts is a single GET request to /v3/content\_types/blog\_post/entries.
*   Strong CDN caching: REST URLs are inherently cacheable. The same query string produces the same URL, which CDN edge nodes cache effectively.
*   Mature SDK support: The JavaScript Delivery SDK, Java SDK, and other language SDKs are built around REST.
*   Reference inclusion: The include\[\] parameter lets you resolve referenced entries in a single request, with configurable depth.
*   Rich query operators: Filter with operators like $in, $nin, $gt, $lt, $regex, and more. Sort, skip, and limit results.

### Limitations

*   Over-fetching: REST returns all fields for every entry. If you only need title and url from a content type with 30 fields, you still receive all 30.
*   Fixed response shape: You cannot reshape the response. The structure mirrors the content type schema.
*   Multiple round trips for complex data: If you need data from several content types that are not connected by references, you need multiple API calls.

### Example: fetching entries with REST

// Fetch published blog posts with author references included
const response = await fetch(
  'https://cdn.contentstack.io/v3/content\_types/blog\_post/entries?' +
  'environment=production&include\[\]=author&locale=en-us',
  {
    headers: {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
      access\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN,
    },
  }
);

const { entries } = await response.json();

## GraphQL CDA: flexible queries, precise payloads

The GraphQL Content Delivery API provides a schema that mirrors your stack's content types. You write queries that request exactly the fields you need.

Endpoint: https://graphql.contentstack.com/stacks/{api\_key}?environment={env}

### Strengths

*   No over-fetching: Request only the fields your component needs. A card component that needs title, url, and featured\_image gets exactly those three fields.
*   Single request for complex data: Fetch entries from multiple content types in one query. A homepage might need hero content, featured articles, and navigation items - all in one request.
*   Schema introspection: Use GraphQL tooling to explore your content model. The schema is auto-generated from your content types.
*   Type safety: GraphQL's type system provides compile-time guarantees when combined with code generation tools.

### Limitations

*   No mutations or subscriptions: GraphQL CDA is read-only. You cannot create, update, or delete content through GraphQL. There are no real-time subscriptions.
*   Query complexity: Deeply nested queries that traverse many reference levels can hit complexity limits. Contentstack applies query complexity scoring to prevent expensive queries.
*   CDN caching nuances: POST-based GraphQL requests are harder to cache at the CDN edge compared to REST GET requests. Contentstack mitigates this, but cache hit rates may differ for highly dynamic query patterns.
*   SDK differences: Not all Contentstack SDKs have first-class GraphQL support. The JavaScript SDK primarily targets REST.

### Example: fetching entries with GraphQL

query HomepageData {
  all\_blog\_post(locale: "en-us", limit: 5) {
    items {
      title
      url
      featured\_imageConnection {
        edges {
          node {
            url
            title
          }
        }
      }
      authorConnection {
        edges {
          node {
            ... on Author {
              title
              bio
            }
          }
        }
      }
    }
  }
}

Note the Connection pattern for references and assets. GraphQL CDA uses Relay-style connections for relationships between content types.

## Decision framework: when to use which

Factor

Choose REST

Choose GraphQL

Query complexity

Simple lookups, list pages, single content type

Multi-type aggregation, complex nested data

Payload efficiency

Not a concern (or you need all fields)

Critical - mobile apps, bandwidth-sensitive

Caching requirements

Maximum CDN cache hit rates

Acceptable CDN behavior, query flexibility matters more

Team familiarity

Team knows REST, wants quick integration

Team uses GraphQL elsewhere, wants consistency

SDK usage

Using Contentstack JavaScript/Java SDK

Building custom fetch layer or using GraphQL client

Reference depth

1-2 levels of includes

Deeply nested content graphs

### Common patterns in production

**Pattern 1: REST for most pages, GraphQL for aggregation**

Many teams use the REST SDK for standard page rendering (article detail, product page) and GraphQL for pages that pull from many content types (homepage, search results, dashboards).

**Pattern 2: GraphQL for frontend, REST for backend jobs**

Frontend applications benefit from GraphQL's precise payloads. Backend services like webhook handlers, migration scripts, and content sync jobs use REST for its simplicity and predictable behavior.

**Pattern 3: All-REST with SDK**

Teams that prioritize SDK convenience and don't have complex aggregation needs stay entirely on REST. The JavaScript SDK handles query building, reference inclusion, and pagination.

## Mixing both in one project

There is no rule against using both APIs in the same project. They share the same authentication tokens and return the same published content. The choice is per-query, not per-project.

If you do mix them, establish a convention:

*   Document which queries use which API surface
*   Centralize API client configuration so token management is consistent
*   Monitor cache behavior for both to understand performance characteristics

## Common mistakes

### 1\. Using GraphQL to avoid learning the REST query syntax

GraphQL is not "better REST." If your queries are simple and the SDK handles them well, GraphQL adds complexity without benefit. Choose based on actual needs, not preference.

### 2\. Ignoring GraphQL query complexity limits

Deeply nested queries that traverse many reference levels can be rejected or throttled. Design your queries with awareness of complexity scoring. Flatten where possible.

> **Common pitfall:**
> 
> Assuming GraphQL can handle content mutations leads teams to architect write operations against an API that only supports reads -- all writes, workflow changes, and administrative actions require the REST-based CMA.

### 3\. Assuming GraphQL replaces CMA

GraphQL CDA is read-only. Teams sometimes expect to use GraphQL for content operations. All writes, workflow changes, and administrative actions require the REST-based Content Management API.

#### Key takeaways

- Connect **REST vs GraphQL: choosing the right query surface** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 05 — Authentication and access control concepts

<!-- ai_metadata: {"lesson_id":"05","type":"text","duration_minutes":1,"topics":["Authentication","and","access","control","concepts"]} -->

#### Lesson text

# Authentication and access control concepts

> **TL;DR**
> 
> *   Design your credential model around four questions: who is the actor, what plane is accessed, what is the minimum scope, and how will the credential be rotated?
> *   Place delivery/preview credentials in client-facing runtimes and management credentials only in trusted backends.
> *   Treat rotation and revocation as ongoing operational systems, not one-time setup tasks.

In a CMS integration, authentication failures are rarely just “security bugs.” They are architecture bugs. If your credential model is wrong, every other quality goal suffers: reliability, maintainability, observability, and recovery speed.

This lesson is about building a security model that matches how Contentstack actually operates across Delivery APIs, Preview flows, and Management APIs. The objective is not memorizing token names. The objective is understanding which identity is acting, in which runtime, against which plane of responsibility.

## Security model before token model

Start with four questions before issuing any token:

1.  Who is the actor? (public app, trusted backend, automation job, authenticated user)
2.  What plane is accessed? (delivery plane vs management plane)
3.  What is the minimum required scope?
4.  How will this credential be rotated and audited?

If you cannot answer all four, the integration is under-designed.

Token selection should be the output of this model, not the starting point.

## Authentication surfaces in Contentstack

Contentstack exposes distinct authentication mechanisms because responsibility differs by API surface.

### Delivery and preview use cases

For published content retrieval, use delivery-oriented credentials (for example, delivery token flows). For draft preview behavior, use preview-specific retrieval context and preview-oriented credentials.

Key principle: delivery and preview credentials are read-path credentials and should never grant management capabilities.

### Management use cases

For control-plane operations (entry mutation, content type changes, environment/branch admin, workflow actions), use management credentials such as management tokens, user authtokens, or OAuth tokens according to the integration context.

Key principle: management credentials are privileged and belong only in trusted runtimes.

## Access control is layered, not singular

Many teams oversimplify access control to “token present or not.” In production, robust control is layered:

*   Credential layer: what API classes can this token call?
*   Permission layer: what stack role or scope does actor have?
*   Context layer: what environment/branch/locale is this request allowed to touch?
*   Process layer: what workflow/publish rules are required before release actions?

This layered model matters because real incidents usually involve combinations:

*   an overly broad token
*   plus ambiguous branch targeting
*   plus missing workflow gate

## Credential placement matrix

Use runtime placement as a design constraint:

### Browser or untrusted client runtime

Allowed:

*   read-oriented delivery calls with non-privileged credentials

Not allowed:

*   management credentials
*   privileged branch/environment administration actions

### Trusted application backend / BFF

Allowed:

*   delivery calls on behalf of clients
*   preview calls with request-scoped preview context
*   management operations when necessary and explicitly bounded

### Automation worker / integration service

Allowed:

*   scoped management actions required by workflow automation

Requirements:

*   strong audit trails
*   narrow scope
*   explicit rotation ownership

If your architecture mixes these placements, credential sprawl is almost guaranteed.

## Principle of least privilege in CMS terms

Least privilege is often stated abstractly. In Contentstack integrations, make it concrete:

*   separate delivery and management credentials by service
*   separate credentials by environment
*   where possible, separate branch scope for management operations
*   avoid shared “god tokens” across pipelines and services

A useful smell test: if revoking one token would break many unrelated systems, your security boundary is too broad.

## Rotation and revocation strategy

Credential hygiene is an operational system, not a one-time setup:

1.  Inventory: keep a credential registry with owner, scope, runtime, and rotation date.
2.  Rotation cadence: rotate by policy, not only after incidents.
3.  Safe cutover: support overlap windows where new and old credentials can be validated.
4.  Revocation playbook: define incident actions for immediate disablement and downstream recovery.

Without this, even “secure” token usage decays over time.

## Light implementation example

The main design idea is request-context-aware credential selection in a trusted server boundary.

// contentstack-auth-context.ts
export function getReadHeaders(mode: "published" | "preview") {
  if (mode === "preview") {
    return {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
      preview\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN!,
    };
  }

  return {
    api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
    access\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  };
}

export const managementHeaders = {
  api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
  authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
};

// Rule: managementHeaders never leaves trusted server code.

The value is not the helper itself. The value is making privilege boundaries explicit and hard to violate accidentally.

## Common failure patterns

> **Common pitfall:**
> 
> Shipping management credentials in a client-accessible path (browser bundle, public API route) exposes your entire stack to unauthorized writes and deletions.

### 1\. Privileged credentials in client-accessible paths

Impact: high-severity breach potential.

Mitigation: hard boundary that all management operations run in trusted services only.

### 2\. Shared management token across many automations

Impact: large blast radius, weak attribution.

Mitigation: one credential per service responsibility plus ownership metadata.

### 3\. Hidden request context

Impact: wrong environment/branch/locale touched by default behavior.

Mitigation: require explicit context parameters at API wrapper boundaries.

### 4\. Access policy and workflow policy designed separately

Impact: technically authorized actions bypass intended approval intent.

Mitigation: align role permissions, token scope, and workflow/publish rule design in one review.

## Operational signals you should monitor

Security maturity is visible in telemetry. At minimum track:

*   failed authentication by endpoint class
*   unusual bursts in privileged endpoint usage
*   credential usage from unexpected runtime/service
*   management actions outside expected deployment windows
*   revoke/rotate events and post-rotation error spikes

These signals help you detect both malicious behavior and accidental misconfiguration.

## Summary

Authentication and access control in Contentstack integrations should be treated as system design, not setup trivia.

What good looks like:

*   clear separation between delivery, preview, and management credentials
*   explicit runtime placement rules
*   least-privilege scoping by service and environment
*   rotation and revocation as standard operations
*   observability that can answer “who did what, where, and when” quickly

#### Key takeaways

- Connect **Authentication and access control concepts** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 06 — Rate limiting, error codes, and retry patterns

<!-- ai_metadata: {"lesson_id":"06","type":"text","duration_minutes":1,"topics":["Rate","limiting","error","codes","and","retry"]} -->

#### Lesson text

# Rate limiting, error codes, and retry patterns

> **TL;DR**
> 
> *   Classify errors before retrying: 429 and 500 are retryable with backoff; 401, 403, and 422 are permanent and need a code or config fix.
> *   Always use exponential backoff with jitter to avoid synchronized retry storms across concurrent clients.
> *   Read X-RateLimit-Remaining on every response to throttle proactively, not just reactively after a 429.

Every API has limits. The difference between a fragile integration and a production-grade one is whether your code anticipates those limits and responds correctly. Contentstack enforces rate limits on both the Content Delivery API and the Content Management API, and the error responses it returns follow specific patterns that your client code should handle deliberately rather than optimistically.

This lesson covers how Contentstack rate limiting works, what each error code means for your integration logic, and how to build retry behavior that recovers gracefully without making the problem worse.

## How Contentstack rate limiting works

Contentstack applies rate limits per organization, and the thresholds differ between the Content Delivery API (CDA) and the Content Management API (CMA). The limits also vary by pricing plan. The critical point for developers is that rate limits are not just a theoretical concern for high-traffic sites. Migration scripts, bulk publishing operations, and content synchronization jobs can hit CMA limits quickly during routine operations.

### CDA rate limits

The Content Delivery API at cdn.contentstack.io is optimized for high read throughput. CDA rate limits are generous because delivery traffic is cacheable and read-only. As documented, free plans are measured per minute (for example, 1000 requests/minute), while paid plans are typically measured per second (for example, 200 requests/second). However, aggressive client-side polling, uncached server-side rendering on every request, or misconfigured CDN bypass can still exhaust these limits during traffic spikes.

### CMA rate limits

The Content Management API at api.contentstack.io has tighter rate limits because it handles write operations, workflow mutations, and administrative actions. The documented default limit is 10 requests/second for CMA (with higher limits available on specific plans). This is the API surface where rate limiting becomes a daily engineering concern, especially during:

*   bulk content imports
*   automated publishing across many entries
*   migration scripts that create or update content types
*   webhook-triggered chains that fan out into many CMA calls

### Rate limit headers

Contentstack returns rate limit metadata in HTTP response headers. Your client code should read these headers on every response, not just on error responses.

Header

Meaning

X-RateLimit-Limit

Maximum requests allowed in the current window

X-RateLimit-Remaining

Requests remaining before throttling begins

When X-RateLimit-Remaining reaches 0, the next request will return a 429 Too Many Requests response. Proactive clients check this header and slow down before hitting the wall rather than slamming into it and retrying.

// Reading rate limit headers from a Contentstack API response
function logRateLimitStatus(response: Response): void {
  const limit = response.headers.get("X-RateLimit-Limit");
  const remaining = response.headers.get("X-RateLimit-Remaining");

  if (remaining !== null) {
    const remainingCount = parseInt(remaining, 10);
    const limitCount = parseInt(limit ?? "0", 10);
    const utilizationPct = ((limitCount - remainingCount) / limitCount) \* 100;

    console.log(
      \`Rate limit: ${remaining}/${limit} remaining (${utilizationPct.toFixed(1)}% used)\`
    );

    if (remainingCount < limitCount \* 0.1) {
      console.warn("Rate limit approaching: less than 10% of quota remaining");
    }
  }
}

## Contentstack HTTP error codes

> **Common pitfall:**
> 
> Retrying every error uniformly -- including 422 (invalid payload) and 403 (wrong permissions) -- burns through rate limit quota with zero chance of success and masks the real problem.

Not every error deserves a retry. The most damaging pattern in API integration code is treating all errors as transient. Contentstack error responses include an HTTP status code and a JSON body with an error\_message and error\_code field. Your retry logic must distinguish between errors worth retrying and errors that require a code or configuration fix.

### 429 Too Many Requests

Meaning: You have exceeded the rate limit for the current window.

Retry: Yes. This is the primary retryable error. Back off, wait, and retry with exponential backoff + jitter.

Response body example:

{
  "error\_message": "You've made too many requests too quickly. Please slow down.",
  "error\_code": 429,
  "errors": {}
}

### 401 Unauthorized

Meaning: The request lacks valid authentication credentials. The api\_key, access\_token, authorization header, or authtoken is missing or invalid.

Retry: No. Retrying with the same credentials will produce the same result. Fix the credential, then retry.

### 403 Forbidden

Meaning: The credentials are valid but lack permission for the requested operation. A delivery token trying to access an unpublished entry, or a management token scoped to one stack trying to reach another, will produce a 403.

Retry: No. This is a permission or scope problem, not a transient failure.

### 404 Not Found

Meaning: The requested resource does not exist. The content type UID is wrong, the entry UID does not exist in the target environment, or the API path is malformed.

Retry: Usually no. However, one edge case exists: if you just published an entry and immediately query CDA for it, propagation delay can cause a brief 404. In this narrow scenario, a short retry with a delay is reasonable. In all other cases, 404 is a permanent error.

### 412 Precondition Failed

Meaning: The request conflicts with a server-side precondition. In Contentstack's CMA, this commonly occurs during entry updates when the version number in your request does not match the current version on the server. Another entry update was committed between your read and your write.

Retry: Yes, but not blindly. You'll want to re-fetch the current entry, resolve any conflicts between your intended changes and the new server state, and then resubmit with the correct version number. A naive retry without re-reading the entry will fail again.

Response body example:

{
  "error\_message": "The version of the entry you are trying to update has changed. Please fetch the latest version.",
  "error\_code": 412,
  "errors": {}
}

### 422 Unprocessable Entity

Meaning: The request body is syntactically valid JSON but semantically invalid. Field values violate content type validation rules, required fields are missing, or a reference UID points to a nonexistent entry.

Retry: No. The payload itself is wrong. Fix the data and resubmit.

### 500 Internal Server Error

Meaning: Something went wrong on Contentstack's side.

Retry: Yes, with backoff. Server errors are typically transient. If a 500 persists across multiple retries with increasing delay, escalate to Contentstack support with the request ID from the response headers.

### Quick reference: retry decision table

Status Code

Retryable

Action

429

Yes

Exponential backoff with jitter

401

No

Fix credentials

403

No

Fix permissions or token scope

404

Rarely

Check resource existence; retry only for publish propagation

412

Yes (with re-fetch)

Re-read current version, merge changes, resubmit

422

No

Fix request payload

500

Yes

Exponential backoff with jitter

## Implementing exponential backoff with jitter

When a 429 or 500 occurs, the worst response is to retry immediately at full speed. That creates a retry storm: many clients all retrying at the same instant, re-triggering the same overload condition.

Exponential backoff increases the delay between retries multiplicatively. Jitter adds randomness to that delay so that concurrent clients do not synchronize their retries.

The formula for delay backoff optimization is defined as follows: $$delay = \\min(baseDelay \\times 2^{attempt} + random(0, jitterMax), maxDelay)$$

function calculateBackoff(
  attempt: number,
  baseDelayMs: number = 1000,
  maxDelayMs: number = 30000,
  jitterMs: number = 500
): number {
  const exponentialDelay = baseDelayMs \* Math.pow(2, attempt);
  const jitter = Math.random() \* jitterMs;
  return Math.min(exponentialDelay + jitter, maxDelayMs);
}

## Idempotency considerations for CMA write operations

Rate limit retries on the CMA introduce a subtle danger: if a request times out but the server actually processed it, retrying creates a duplicate operation. Entry creation is not idempotent. Calling POST /v3/content\_types/{content\_type\_uid}/entries twice with the same payload creates two entries with different UIDs.

Guard against this:

1.  Use UID-based updates when possible. PUT /v3/content\_types/{content\_type\_uid}/entries/{entry\_uid} is idempotent. Sending the same update twice produces the same result.
2.  Track operation state externally. Before retrying a create operation, check whether the resource was actually created by querying for it. If it exists, skip the retry and proceed with an update.
3.  Use the entry version field. CMA entry updates require a version field. If you retry an update and the version has already advanced, the 412 response tells you the first attempt succeeded.

async function idempotentCreateOrUpdate(
  apiKey: string,
  managementToken: string,
  contentTypeUid: string,
  uniqueField: string,
  uniqueValue: string,
  entryData: Record
): Promise<{ uid: string; created: boolean }> {
  const baseUrl = "https://api.contentstack.io/v3";
  const headers = {
    api\_key: apiKey,
    authorization: managementToken,
    "Content-Type": "application/json",
  };

  // Check if entry already exists by querying on a unique field
  const searchResponse = await fetch(
    \`${baseUrl}/content\_types/${contentTypeUid}/entries?query={"${uniqueField}":"${uniqueValue}"}\`,
    { headers }
  );
  const searchResult = await searchResponse.json();

  if (searchResult.entries && searchResult.entries.length > 0) {
    const existing = searchResult.entries\[0\];
    // Update existing entry with current version
    const updateResponse = await fetch(
      \`${baseUrl}/content\_types/${contentTypeUid}/entries/${existing.uid}\`,
      {
        method: "PUT",
        headers,
        body: JSON.stringify({
          entry: { ...entryData, version: existing.\_version },
        }),
      }
    );
    const updated = await updateResponse.json();
    return { uid: updated.entry.uid, created: false };
  }

  // Create new entry
  const createResponse = await fetch(
    \`${baseUrl}/content\_types/${contentTypeUid}/entries\`,
    {
      method: "POST",
      headers,
      body: JSON.stringify({ entry: entryData }),
    }
  );
  const created = await createResponse.json();
  return { uid: created.entry.uid, created: true };
}

## Building a resilient API client with retry logic

The following TypeScript implementation ties together everything discussed: rate limit header inspection, error classification, exponential backoff with jitter, and maximum retry caps. This is a production-oriented pattern, not a toy example.

// resilient-contentstack-client.ts

interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  maxDelayMs: number;
  jitterMs: number;
}

const DEFAULT\_RETRY\_CONFIG: RetryConfig = {
  maxRetries: 5,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  jitterMs: 500,
};

type ErrorCategory = "retryable" | "permanent" | "retryable\_with\_refetch";

function classifyError(status: number): ErrorCategory {
  switch (status) {
    case 429:
    case 500:
    case 502:
    case 503:
    case 504:
      return "retryable";
    case 412:
      return "retryable\_with\_refetch";
    case 401:
    case 403:
    case 404:
    case 422:
      return "permanent";
    default:
      return status >= 500 ? "retryable" : "permanent";
  }
}

function getRetryDelay(
  attempt: number,
  \_response: Response,
  config: RetryConfig
): number {
  // Exponential backoff with jitter
  const exponentialDelay = config.baseDelayMs \* Math.pow(2, attempt);
  const jitter = Math.random() \* config.jitterMs;
  return Math.min(exponentialDelay + jitter, config.maxDelayMs);
}

async function sleep(ms: number): Promise {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export async function resilientFetch(
  url: string,
  options: RequestInit,
  config: RetryConfig = DEFAULT\_RETRY\_CONFIG
): Promise {
  let lastResponse: Response | null = null;
  let lastError: Error | null = null;

  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Log rate limit status on every response
      const remaining = response.headers.get("X-RateLimit-Remaining");
      if (remaining !== null) {
        console.log(
          \`\[Attempt ${attempt}\] ${url} - Rate limit remaining: ${remaining}\`
        );
      }

      // Success: return immediately
      if (response.ok) {
        return response;
      }

      const category = classifyError(response.status);

      if (category === "permanent") {
        // No amount of retrying will fix this. Return the error response.
        const body = await response.text();
        console.error(
          \`Permanent error ${response.status} on ${url}: ${body}\`
        );
        return response;
      }

      if (category === "retryable\_with\_refetch") {
        // 412: caller must re-fetch and reconcile before retrying
        console.warn(
          \`Version conflict (412) on ${url}. Caller must re-fetch before retry.\`
        );
        return response;
      }

      // Retryable error: back off and try again
      lastResponse = response;
      if (attempt < config.maxRetries) {
        const delayMs = getRetryDelay(attempt, response, config);
        console.warn(
          \`Retryable error ${response.status} on ${url}. \` +
          \`Retrying in ${delayMs.toFixed(0)}ms (attempt ${attempt + 1}/${config.maxRetries})\`
        );
        await sleep(delayMs);
      }
    } catch (err) {
      // Network-level failures (DNS, connection reset) are retryable
      lastError = err instanceof Error ? err : new Error(String(err));
      if (attempt < config.maxRetries) {
        const delayMs =
          config.baseDelayMs \* Math.pow(2, attempt) +
          Math.random() \* config.jitterMs;
        console.warn(
          \`Network error on ${url}: ${lastError.message}. \` +
          \`Retrying in ${delayMs.toFixed(0)}ms (attempt ${attempt + 1}/${config.maxRetries})\`
        );
        await sleep(delayMs);
      }
    }
  }

  // All retries exhausted
  if (lastResponse) {
    return lastResponse;
  }
  throw lastError ?? new Error(\`Request to ${url} failed after all retries\`);
}

// Usage: fetching entries from CDA with automatic retry on rate limits
async function fetchBlogEntries(): Promise {
  const response = await resilientFetch(
    "https://cdn.contentstack.io/v3/content\_types/product/entries?environment=production&locale=en-us",
    {
      headers: {
        api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
        access\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
      },
    }
  );

  if (!response.ok) {
    throw new Error(\`Failed to fetch blog entries: ${response.status}\`);
  }

  return response.json();
}

// Usage: bulk CMA operations with rate-aware throttling
async function bulkPublishEntries(
  entryUids: string\[\],
  contentTypeUid: string,
  environment: string
): Promise {
  const baseUrl = "https://api.contentstack.io/v3";
  const headers: Record = {
    api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
    authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
    "Content-Type": "application/json",
  };

  for (const uid of entryUids) {
    const response = await resilientFetch(
      \`${baseUrl}/content\_types/${contentTypeUid}/entries/${uid}/publish\`,
      {
        method: "POST",
        headers,
        body: JSON.stringify({
          entry: { environments: \[environment\], locales: \["en-us"\] },
        }),
      }
    );

    if (!response.ok) {
      const body = await response.json();
      console.error(\`Failed to publish ${uid}:\`, body.error\_message);
    }
  }
}

This client handles the entire lifecycle: it reads rate limit headers for observability, classifies errors into retryable and permanent categories, applies exponential backoff with jitter for retryable responses, and surfaces 412 version conflicts to the caller for explicit handling.

## Common mistakes

### Mistake 1: Retrying everything uniformly

Teams wrap all API calls in a generic retry loop that treats 422 and 429 identically. The result: invalid payloads hammer the API on repeat, burning through rate limit quota without any chance of success. Classify errors before deciding whether to retry.

### Mistake 2: Synchronized retry storms

When many serverless functions or workers hit a 429 simultaneously and all retry after exactly the same fixed delay, they create a thundering herd. The retries arrive in unison, trigger another 429 wave, and the cycle continues. Jitter breaks the synchronization. Every retry implementation must include randomness.

### Mistake 3: Ignoring rate limit headers until failure

Some implementations only inspect the response when the status code is not 200. This means the client has zero visibility into how close it is to the rate limit ceiling until it crashes through it. Read X-RateLimit-Remaining on every response. Use it to proactively throttle batch operations before the 429 arrives.

#### Key takeaways

- Connect **Rate limiting, error codes, and retry patterns** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 07 — Fetching and Rendering Content : Overview

<!-- ai_metadata: {"lesson_id":"07","type":"text","duration_minutes":1,"topics":["Fetching","and","Rendering","Content","Overview"]} -->

#### Lesson text

# Fetching and Rendering Content

This module is the hands-on center of the certification: initialize the SDK, query content safely, resolve relationships, and render responses in real applications.

## Why This Module Matters

This is where Contentstack stops being abstract. You fetch content, inspect the JSON, and make it useful in code.

## You Will Be Able To

*   initialize delivery clients with the right region, environment, and token strategy
*   compose queries that handle filtering, pagination, and reference resolution cleanly
*   render entries and assets in ways that scale beyond a single demo page

## Recommended Preparation

Complete Module 3.1 first and come in ready to write TypeScript or JavaScript.

## Estimated Effort

90-120 minutes

## Practice Focus

Build the Veda storefront data layer by fetching product, category, and related content patterns that resemble production work.

## Suggested Next Step

Start with lesson 1 in this module and run the exercises with a real stack rather than reading passively.

#### Key takeaways

- Connect **Fetching and Rendering Content : Overview** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 08 — SDK initialization and query patterns

<!-- ai_metadata: {"lesson_id":"08","type":"text","duration_minutes":1,"topics":["SDK","initialization","and","query","patterns"]} -->

#### Lesson text

# SDK initialization and query patterns

> **TL;DR**
> 
> *   Initialize the SDK with your API key, delivery token, environment, and region -- all four must be correct or queries silently fail.
> *   Use the fluent query builder (.equalTo(), .where(), .includeReference(), .limit(), .skip()) to compose filters, sorting, pagination, and reference resolution before executing with .find().
> *   Always verify your stack region in Settings > Stack Information; a wrong region returns empty results or 401 with no hint about the actual cause.

**Prerequisites**

This module assumes familiarity with TypeScript, Node.js, and basic async/await patterns. If you're new to these, review the [TypeScript handbook](https://www.typescriptlang.org/docs/handbook/) and [Node.js getting started guide](https://nodejs.org/en/learn/getting-started/introduction-to-nodejs) first.

Before you can render content from Contentstack, you need a reliable way to fetch it. While raw REST calls work, the @contentstack/delivery-sdk provides a structured query interface that handles authentication headers, region routing, pagination, and response parsing for you. This is the recommended SDK for all JavaScript and TypeScript projects. This lesson walks through SDK setup, configuration, and the query patterns you will use daily.

The domain example throughout this lesson is a Product content type from Veda: The Revival Collection - a jewelry e-commerce catalog with products, product lines, and categories.

## Installing the SDK

The recommended SDK for JavaScript and TypeScript delivery is @contentstack/delivery-sdk. It is Contentstack's modern, modular SDK with a TypeScript-first design, tree-shaking support, and active development. This course uses @contentstack/delivery-sdk exclusively.

npm install @contentstack/delivery-sdk

> **Note:**
> 
> You may encounter references to the older contentstack npm package in legacy codebases. While it connects to the same Content Delivery API, new projects should always use @contentstack/delivery-sdk.

## Initializing the stack client

Every SDK interaction begins with a Stack instance. You need three credentials and one configuration choice:

1.  Stack API Key - identifies your stack. Found in Settings > Stack in the Contentstack dashboard.
2.  Delivery Token - a read-only token scoped to a specific environment. Found in Settings > Tokens > Delivery Tokens.
3.  Environment - the publishing environment to query (development, staging, production).
4.  Region - the data center region where your stack is hosted.

import Contentstack from "@contentstack/delivery-sdk";
const stack = Contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN,
  environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT,
  region: Contentstack.Region.US,
});

### Region configuration

Contentstack operates across multiple cloud providers and geographic regions. The region you specify must match where your stack was created. Using the wrong region silently returns empty results or authentication failures. For a complete breakdown of all regions, cloud providers, and their API endpoint URLs, see [Lesson 3.1.2: Regions, clouds, and API endpoints](/docs/developers/contentstack-regions/api-endpoints).

The SDK handles URL construction automatically when you set the region correctly using the built-in region constants (e.g., Contentstack.Region.US, Contentstack.Region.EU).

### Branch configuration

If your stack uses branches (a feature for parallel content development), you can target a specific branch during initialization.

const stack = Contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN,
  environment: "production",
  region: Contentstack.Region.US,
  branch: "feature-digital-dawn-v2",
});

When no branch is specified, the SDK queries the main branch. The delivery token must have access to the target branch.

## Fetching all entries of a content type

The most common query fetches entries from a content type. Assume you have a content type with UID product that has fields for title, url, price, short\_description, and description.

const query = stack.contentType("product").entry().query();
const result = await query.find();
console.log(result.entries); // Array of product entry objects

The find() method executes the query and returns a response object. The entries property contains the array of matching entries. Each entry includes all fields defined in the content type schema, plus system fields like uid, created\_at, updated\_at, locale, and \_version.

Under the hood, the SDK sends a GET request to:

GET /v3/content\_types/product/entries?environment=production

with api\_key and access\_token headers set automatically.

## Fetching a single entry by UID

When you know the specific entry UID (for example, from a route parameter or a reference field), fetch it directly:

const entry = await stack
  .contentType("product")
  .entry("blt\_matrix\_link\_001")
  .fetch();

console.log(entry.title);
console.log(entry.price);

The fetch() method returns the entry object directly, not wrapped in an array. This maps to the REST endpoint:

GET /v3/content\_types/product/entries/blt\_matrix\_link\_001?environment=production

This approach is faster and more cache-friendly than querying with a filter when you already have the UID.

## Fetching by URL

Contentstack entries can have a url field (set in the content type schema). This is especially useful for page-driven content types. Rather than looking up a UID, you query by the URL path:

const query = stack.contentType("product").entry().query();
const result = await query.equalTo("url", "/products/digital-dawn/matrix-link-bracelet").find();
console.log(result.entries\[0\]); // The product entry matching that URL

This pattern drives most page rendering in frontend frameworks: the route provides the URL path, and you query Contentstack to resolve it to an entry.

## Query chaining

The SDK provides a fluent query builder. You chain methods to compose filters, sorting, pagination, and reference inclusion before executing with find().

### Filtering with .equalTo() and .where()

For simple equality filters, use .equalTo().

// Products in the Earrings category
const query = stack.contentType("product").entry().query();
const result = await query.equalTo("category", "blt\_earrings\_category\_001").find();

For comparison operators, use .where() with a QueryOperation.

import Contentstack, { QueryOperation } from "@contentstack/delivery-sdk";

// Products above a certain price
const query = stack.contentType("product").entry().query();
const result = await query
  .where("price", QueryOperation.IS\_GREATER\_THAN, 100)
  .find();

The .where() method always takes three arguments: the field UID, a QueryOperation operator, and the value. Available operators include IS\_LESS\_THAN, IS\_GREATER\_THAN, EQUALS, INCLUDES, and others that map to Contentstack's underlying query language ($in, $nin, $gt, $lt, $gte, $lte, $ne, $exists, $regex).

### Pagination with .limit() and .skip()

Contentstack returns a maximum of 100 entries per request by default. Control pagination explicitly:

const query = stack.contentType("product").entry().query();

// First page: 10 products
const page1 = await query.limit(10).skip(0).find();

// Second page: next 10 products
const page2 = await query.limit(10).skip(10).find();

The response includes a count property indicating how many entries were returned in this response. Use this to build pagination controls.

### Sorting with .orderByAscending() and .orderByDescending()

const query = stack.contentType("product").entry().query();

// Products sorted by price, lowest first
const result = await query.orderByAscending("price").find();

// Most recently created products first
const query = stack.contentType("product").entry().query();
const result = await query.orderByDescending("created\_at").find();

### Including references with .includeReference()

When a product has a reference field (for example, product\_line referencing a product\_line content type), the default response only includes the UIDs. To resolve the full referenced entries inline:

const query = stack.contentType("product").entry().query();
const result = await query.includeReference("product\_line").find();

// Each product now has product\_line\[\] with full entry data, not just UIDs
result.entries.forEach((product) => {
  product.product\_line.forEach((line) => {
    console.log(line.title, line.description);
  });
});

You can include multiple reference fields by chaining .includeReference() calls.

const result = await query
  .includeReference("product\_line")
  .includeReference("category")
  .find();

This translates to the REST parameters: include\[\]=product\_line&include\[\]=category.

## Understanding the response object

The SDK response from find() contains structured data:

const result = await query.find();

// result.entries  - Array of entry objects
// result.count    - Number of entries in this response (respects limit)

Each entry object mirrors the JSON structure of the content type. System fields are included at the top level:

{
  "uid": "blt\_matrix\_link\_001",
  "title": "Matrix Link Bracelet",
  "price": 295,
  "url": "/products/digital-dawn/matrix-link-bracelet",
  "short\_description": "A sleek link bracelet composed of interlocking square links...",
  "product\_line": \[
    {
      "uid": "blt\_digital\_dawn\_001",
      "\_content\_type\_uid": "product\_line"
    }
  \],
  "category": \[
    {
      "uid": "blt\_bracelets\_001",
      "\_content\_type\_uid": "category"
    }
  \],
  "locale": "en-us",
  "created\_at": "2025-01-10T08:30:00.000Z",
  "updated\_at": "2025-03-22T14:15:00.000Z",
  "\_version": 3,
  "\_in\_progress": false
}

Without reference inclusion, the safest mental model is "reference stubs" rather than fully resolved entries. In typed frontends such as kickstart-veda, keep product\_line and category typed as referenced entry arrays and treat unresolved responses as partial objects until you call includeReference() or include\_all.

When references are included via includeReference(), those stubs are replaced with the full entry objects.

### Typed queries with generics

The SDK supports TypeScript generics on find() and fetch() to provide type safety on the response:

import { Product } from "./types";

const query = stack.contentType("product").entry().query();
const result = await query
  .equalTo("url", "/products/digital-dawn/matrix-link-bracelet")
  .find();

// result.entries is now typed as Product\[\]
result.entries\[0\].title; // string
result.entries\[0\].price; // number

Define your types to match the content type schema (field UIDs as keys, with the correct TypeScript types). The [kickstart-veda reference application](https://github.com/contentstack/kickstart-veda/blob/main/lib/types.ts) provides a complete example of typing Contentstack entries.

## Putting it together: a real query

Here is a complete example that initializes the SDK and builds a query for products in a product line, sorted by price, with category and product line references resolved:

import Contentstack, { QueryOperation } from "@contentstack/delivery-sdk";

// Initialize
const stack = Contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  environment: "production",
  region: Contentstack.Region.EU,
});

// Build and execute query
async function getProductsByLine(lineUid: string, page = 1) {
  const perPage = 12;
  const query = stack.contentType("product").entry().query();

  const result = await query
    .equalTo("product\_line", lineUid)
    .includeReference("product\_line")
    .includeReference("category")
    .orderByAscending("price")
    .limit(perPage)
    .skip((page - 1) \* perPage)
    .find();

  return {
    products: result.entries,
    hasMore: result.entries.length === perPage,
  };
}

// Usage
const { products, hasMore } = await getProductsByLine("blt\_digital\_dawn\_001");
products.forEach((product) => {
  console.log(\`${product.title} - $${product.price}\`);
  console.log(\`Line: ${product.product\_line.map((l) => l.title).join(", ")}\`);
});

This single function handles filtering, sorting, reference resolution, and pagination in a clean, composable way.

## Common initialization mistakes

> **Common pitfall:**
> 
> Initializing with the wrong region (e.g., Region.US when your stack is in EU) produces empty results or 401 errors with no mention of a region mismatch -- verify your stack's region in Settings > Stack Information.

**Wrong region:** If your stack is in EU and you initialize with Region.US, every request either returns empty results or fails with a 401. The error message does not explicitly say "wrong region." Always verify your stack's region in Settings > Stack Information.

**Environment mismatch:** The delivery token is scoped to an environment. If you initialize with environment: "production" but the token was created for staging, you get authentication errors.

**Missing environment variable:** The SDK does not throw during initialization if credentials are empty strings. The error surfaces on the first query, often as a cryptic 401 or 412. Validate credentials at startup.

**Branch not published:** If you specify a branch but content is not published to the target environment on that branch, queries return empty results without errors.

## Exercise: initialize the SDK and fetch products

Set up a small Node.js script that:

1.  Installs @contentstack/delivery-sdk.
2.  Creates a stack client pointed at your stack (use environment variables for credentials).
3.  Fetches all entries from a content type of your choice.
4.  Filters entries by a field value using .equalTo() or .where() with a QueryOperation.
5.  Limits results to 5 entries and sorts them by created\_at descending.
6.  Logs the title and UID of each result.

If you do not have a stack with content, use the [Veda kickstart seed](https://github.com/contentstack/kickstart-veda-seed) to create a stack with products, product lines, and categories, then publish to a development environment.

#### Key takeaways

- Connect **SDK initialization and query patterns** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 09 — References, includes, and localized content retrieval

<!-- ai_metadata: {"lesson_id":"09","type":"text","duration_minutes":1,"topics":["References","includes","and","localized","content","retrieval"]} -->

#### Lesson text

# References, includes, and localized content retrieval

> **TL;DR**
> 
> *   Use includeReference() (or include\[\] in REST) with the reference _field UID_ to resolve related entries in a single API call instead of making N+1 separate requests.
> *   Always enable include\_fallback on partially localized stacks so visitors see parent-locale content instead of blank fields for untranslated entries.
> *   Limit include depth to what the current view actually renders -- each additional level multiplies response payload and latency.

Content in Contentstack rarely lives in isolation. A product entry references its category and product line. A page references its components. Understanding how references resolve at the API level - and how localization interacts with that resolution - is essential to building correct, performant delivery code.

This lesson uses a **Product** content type from Veda: The Revival Collection with referenced **Category** and **Product Line** entries to illustrate every concept.

## How references work at the data level

A reference field in Contentstack stores an array of objects, each containing the UID and content type of the referenced entry. When you fetch a product entry without any include parameters, the reference field looks like this:

{
  "title": "Matrix Link Bracelet",
  "url": "/products/digital-dawn/matrix-link-bracelet",
  "price": 295,
  "short\_description": "A sleek link bracelet composed of interlocking square links...",
  "category": \[
    {
      "uid": "blt8a3f2e1d0c9b7a65",
      "\_content\_type\_uid": "category"
    }
  \],
  "product\_line": \[
    {
      "uid": "blt2a4b6c8d0e1f3759",
      "\_content\_type\_uid": "product\_line"
    }
  \]
}

Notice: you get the UIDs and content type identifiers, but not the actual data of those categories or product lines. The referenced entries' titles, descriptions, images, and other fields are not included. To render "Digital Dawn - Bracelets" on the page, you need to resolve these references.

There are two ways to resolve them: multiple separate API calls (inefficient) or the include mechanism (correct).

## The include\[\] parameter in REST

The REST Content Delivery API supports an include\[\] query parameter that tells the API to resolve specified reference fields and embed the full referenced entries in the response.

GET /v3/content\_types/product/entries?environment=production&include\[\]=category&include\[\]=product\_line

With this parameter, the response transforms. Instead of UID stubs, each referenced entry is expanded inline.

{
  "title": "Matrix Link Bracelet",
  "category": \[
    {
      "uid": "blt8a3f2e1d0c9b7a65",
      "title": "Bracelets",
      "url": "/category/bracelets",
      "description": "Link bracelets, cuffs, and bangles",
      "\_content\_type\_uid": "category"
    }
  \],
  "product\_line": \[
    {
      "uid": "blt2a4b6c8d0e1f3759",
      "title": "Digital Dawn",
      "url": "/products/digital-dawn",
      "description": "Y2K-inspired unisex jewelry in silver and gold",
      "image": { "url": "https://images.contentstack.io/..." },
      "\_content\_type\_uid": "product\_line"
    }
  \]
}

Each include\[\] value is the field UID of the reference field on the parent content type, not the UID of the referenced content type. This distinction matters: if your product has two reference fields both pointing to the category content type (for example, primary\_category and secondary\_category), you include them separately:

include\[\]=primary\_category&include\[\]=secondary\_category

## The includeReference() method in the SDK

The JavaScript SDK wraps the include\[\] parameter with the includeReference() method.

import Contentstack from "@contentstack/delivery-sdk";

const stack = Contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN,
  environment: "production",
  region: Contentstack.Region.EU,
});

async function getProductWithDetails(slug: string) {
  const query = stack.contentType("product").entry().query();

  const result = await query
    .equalTo("url", \`/products/digital-dawn/${slug}\`)
    .includeReference("category")
    .includeReference("product\_line")
    .find();

  return result.entries\[0\];
}

const product = await getProductWithDetails("matrix-link-bracelet");

// Category and product line are now full objects
product.category.forEach((cat) => {
  console.log(\`${cat.title} - ${cat.url}\`);
});

// Product line is resolved
console.log(\`From ${product.product\_line\[0\].title} collection\`);

Chain .includeReference() calls for each reference field UID you need resolved. The SDK appends the appropriate parameters to the request automatically.

## Include depth: nested references

References can be nested. A product references its product line, and each product line has a products field referencing other products. To resolve both levels, you need nested include paths.

In the REST API, you express this with dot notation:

include\[\]=product\_line.products

In the SDK, you use the same dot notation:

const result = await query
  .includeReference("product\_line.products")
  .includeReference("category")
  .find();

// Access nested reference - other products in the same line
product.product\_line\[0\].products\[0\].title; // "Pixel Stud Earrings"

### Performance implications of include depth

Every level of include depth increases the work the API must do. Contentstack resolves includes server-side by performing additional internal lookups.

The practical lookup limits behave as follows:

*   **One level (e.g.,** **category****):** Standard and performant. This is the common case.
*   **Two levels (e.g.,** **product\_line.products****):** Acceptable for most use cases. Response payloads grow proportionally.
*   **Three or more levels:** Possible but risky. Response sizes balloon, latency increases, and you approach the API's response size limits. If you need deeply nested data, consider restructuring your content model or making separate targeted queries.

flowchart TD
    P\[Product\] -->|depth 1| PL\[Product Line\]
    P -->|depth 1| C\[Category\]
    PL -->|depth 2| P2\[Other Products\]
    P2 -->|depth 3| C2\[Their Categories\]
    style P2 fill:#fff3cd
    style C2 fill:#f8d7da

Depth 1 (green) is standard. Depth 2 (yellow) is acceptable. Depth 3+ (red) risks payload bloat.

Contentstack's REST API supports include depth through dot notation. There is no explicit "depth" integer parameter - you specify each path you need. This gives you fine-grained control over which branches of the reference tree get resolved.

A common anti-pattern is including everything "just in case." Each unnecessary include adds latency and payload bytes. Only include references that the current view actually renders.

### The include\_all shorthand

For page rendering where you need all references resolved, the SDK supports a convenient shorthand via addParams():

const pageQuery = stack.contentType("page").entry();

pageQuery.addParams({ include\_all: true });
pageQuery.addParams({ include\_all\_depth: 2 });

const result = await pageQuery
  .query()
  .where("url", QueryOperation.EQUALS, "/products/digital-dawn")
  .find();

The include\_all parameter resolves all reference fields on the entry, and include\_all\_depth controls how many levels deep to resolve (default is 1). This is the pattern used in the [kickstart-veda reference application](https://github.com/contentstack/kickstart-veda) for page-level queries where you need the full content tree.

The trade-off is payload size: include\_all resolves every reference field, even ones your page does not render. For listing pages where you only need one or two reference fields, explicit .includeReference() calls are more efficient. For detail pages that render most of the entry's data, include\_all with a depth of 2 is a practical default.

## Localized content retrieval

Contentstack supports multi-language content through its localization system. Each stack has a master locale (typically en-us), and you can create additional locales organized in a hierarchy. Content editors can localize entries per locale, and unlocalized fields fall back to the parent locale.

### The locale parameter

To fetch content in a specific locale, pass the locale parameter.

**REST API:**

GET /v3/content\_types/product/entries?environment=production&locale=fr-fr

**SDK:**

async function getProductsInFrench() {
  const query = stack.contentType("product").entry().query();
  const result = await query.locale("fr-fr").find();
  return result.entries;
}

The SDK uses the .locale() method, which maps to the locale query parameter in REST. When you specify a locale, the API returns entries in that locale. Fields that the editor has translated appear in the target language. Fields that have not been translated may appear empty or may fall back, depending on fallback configuration.

### Fallback language behavior

Contentstack supports one level of fallback per locale. When creating a locale in the stack settings, you specify a fallback locale. For example:

*   fr-fr falls back to fr
*   fr falls back to en-us (the master locale)
*   de-at falls back to de-de

When an entry field has not been localized for the requested locale, the fallback determines what happens. However, fallback behavior is not automatic in API responses by default. You'll want to explicitly request it.

### The include\_fallback parameter

To activate fallback resolution in your API responses, include the include\_fallback parameter:

**REST API:**

GET /v3/content\_types/product/entries?environment=production&locale=fr-ca&include\_fallback=true

**SDK:**

async function getProductsWithFallback(locale: string) {
  const query = stack.contentType("product").entry().query();
  const result = await query
    .locale(locale)
    .includeFallback()
    .find();

  return result.entries;
}

// Request French Canadian; unlocalised fields fall back to fr, then en-us
const products = await getProductsWithFallback("fr-ca");

Without include\_fallback, if a product's description field has not been localized into fr-ca, that field may come back as empty or null. With include\_fallback, the API walks the fallback chain: it checks fr-ca, then falls back to the parent locale (e.g., fr), and finally to the master locale (en-us).

**Tip:** This matters most for sites that are partially localized. A product might have its title translated but its short\_description still in English. With fallback enabled, visitors see the French title and the English description rather than a blank description section.

### The publish\_fallback entry field

When fallback resolves, the API includes metadata about which locale the content actually came from. Look for the publish\_details object on the entry, which indicates the locale in which the entry was published. This lets your frontend display locale indicators or "content not yet translated" notices.

## Combining references and localization

The most common production scenario combines both: you need referenced entries resolved, and you need localized content. Both parameters work together smoothly in a single request.

**REST API:**

GET /v3/content\_types/product/entries?environment=production&locale=fr-fr&include\_fallback=true&include\[\]=category&include\[\]=product\_line

**SDK:**

async function getLocalizedProductWithDetails(slug: string, locale: string) {
  const query = stack.contentType("product").entry().query();

  const result = await query
    .equalTo("url", \`/products/digital-dawn/${slug}\`)
    .locale(locale)
    .includeFallback()
    .includeReference("category")
    .includeReference("product\_line")
    .find();

  const product = result.entries\[0\];
  if (!product) return null;

  return product;
}

// Fetch French product with resolved category and product line
const product = await getLocalizedProductWithDetails(
  "matrix-link-bracelet",
  "fr-fr"
);

console.log(product.title); // "Bracelet Maillon Matrice" (if localized)
product.category.forEach((cat) => {
  console.log(\`${cat.title} - ${cat.url}\`);
  // French or English fallback
});

When references and locale are combined, the API resolves referenced entries in the same locale. If the category "Bracelets" has a French localization, the included entry returns the French version. If it does not, and include\_fallback is set, the fallback chain applies to the referenced entries as well.

### Locale consistency across references

One subtlety to be aware of: referenced entries follow the same locale resolution as the parent entry. If you request locale=fr-fr on a product, the included categories and product lines are also resolved in fr-fr. You do not need to specify the locale separately for each reference.

However, if a referenced entry does not exist in the requested locale and has no fallback, it may be excluded from the response entirely. Test your locale coverage across content types, especially for reference-heavy pages. A product page that shows category and product line in English but missing in French (because they were never localized) creates a confusing experience.

## Fetching a single localized entry by UID

When fetching a specific entry by UID, you can combine locale and reference includes on the single-entry fetch as well:

async function getLocalizedEntry(uid: string, locale: string) {
  const entry = await stack
    .contentType("product")
    .entry(uid)
    .includeReference("category")
    .includeReference("product\_line")
    .locale(locale)
    .includeFallback()
    .fetch();

  return entry;
}

This maps to the REST call:

GET /v3/content\_types/product/entries/blt\_matrix\_link\_001?environment=production&locale=fr-fr&include\_fallback=true&include\[\]=category&include\[\]=product\_line

## Common mistakes with references and localization

**Including the content type UID instead of the field UID:** The include\[\] parameter takes the reference _field_ UID from the parent content type, not the _content type_ UID of the target. include\[\]=category is correct for a field named category; include\[\]=categories would be wrong if the field UID is category.

> **Common pitfall:**
> 
> Omitting include\_fallback on a partially localized stack causes blank fields wherever translation is incomplete -- visitors see missing content instead of the parent-locale fallback.

**Assuming all reference entries exist in all locales:** If category entries are only created in English, requesting locale=ja-jp without fallback returns categories with empty fields or missing entries. Audit locale coverage for referenced content types.

**Over-including nested references:** Including product\_line.products.category resolves three levels deep. Every level multiplies the response payload. Include only what the current page renders.

**Not testing fallback chains:** A locale with a misconfigured fallback parent silently returns empty fields. Verify your locale hierarchy in Settings > Languages.

## Exercise: fetch a product with two levels of references in a specific locale

Build a function that:

1.  Queries the product content type by URL slug.
2.  Resolves the product\_line reference and the nested product\_line.products reference (two levels).
3.  Also resolves the category reference (one level).
4.  Requests locale fr-fr with fallback enabled.
5.  Logs the product title, the product line name, and each related product in the line.
6.  Handles the case where the product does not exist in the requested locale.

Test with a Veda product that has partial French localization - some fields translated, others falling back to English.

#### Key takeaways

- Connect **References, includes, and localized content retrieval** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 10 — Image delivery and transformation APIs

<!-- ai_metadata: {"lesson_id":"10","type":"text","duration_minutes":1,"topics":["Image","delivery","and","transformation","APIs"]} -->

#### Lesson text

# Image delivery and transformation APIs

> **TL;DR**
> 
> *   Append query parameters (?width=, ?format=webp, ?quality=) to any Contentstack asset URL for on-the-fly image transformation -- no build step or processing pipeline needed.
> *   Use ?auto=webp as a baseline optimization; it serves WebP to supporting browsers and falls back automatically, reducing payload by 25-35%.
> *   Always set explicit width and height on <img> elements and use loading="lazy" for below-fold images to protect LCP and CLS scores.

Images account for the majority of page weight on most websites. Contentstack provides a dedicated image delivery service with URL-based transformations that let you resize, crop, reformat, and optimize images without a separate image processing pipeline. Mastering these transformations is the difference between a site that scores well on Core Web Vitals and one that does not.

This lesson uses product images from Veda: The Revival Collection (jewelry e-commerce) as the running example.

## Image delivery URL structure

When you upload an asset to Contentstack, it is served through Contentstack's image delivery CDN. The base URL follows this pattern:

https://images.contentstack.io/v3/assets/{stack\_api\_key}/{asset\_uid}/{upload\_uid}/{filename}

For example, a product photo might have this URL:

https://images.contentstack.io/v3/assets/{stack\_api\_key}/{asset\_uid}/matrix-link-bracelet.jpg

This URL is what appears in the url property of an asset object when you fetch entries through the Delivery API. Every image uploaded to Contentstack gets a unique, globally accessible URL on this CDN.

For EU and Azure regions, the host differs:

*   NA: https://images.contentstack.io
*   EU: https://eu-images.contentstack.com
*   Azure NA: https://azure-na-images.contentstack.com
*   Azure EU: https://azure-eu-images.contentstack.com

> **Common pitfall:**
> 
> Serving the original full-resolution image (e.g., 4000px wide) when the rendered size is 800px wastes bandwidth and tanks your Largest Contentful Paint score -- always match ?width= to the rendered size.

The key insight is that transformations are applied by appending query parameters to this URL. No server-side processing step is needed in your application. The CDN handles transformation, caching, and delivery.

## URL-based transformation parameters

Contentstack's Image Delivery API accepts query parameters that transform the image on the fly. The transformed result is cached at the CDN edge, so subsequent requests for the same transformation are served from cache.

### Resizing with width and height

Scale an image to specific dimensions:

https://images.contentstack.io/v3/assets/.../matrix-link-bracelet.jpg?width=400&height=300

You can specify one dimension and let the other scale proportionally:

// Scale to 600px wide, maintain aspect ratio
?width=600

// Scale to 400px tall, maintain aspect ratio
?height=400

Specifying both dimensions without a fit mode may distort the image. Use the fit parameter to control how the image adapts.

### Format conversion with format

Convert images to modern compressed formats for smaller network file sizes:

// Convert to WebP
?format=webp

// Convert to AVIF (where supported)
?format=avif

// Let Contentstack choose the best format based on the Accept header
?auto=webp

The auto=webp parameter is particularly useful. It inspects the browser's Accept header and serves WebP to browsers that support it, falling back to the original format for others. This single parameter can reduce image payload by 25-35% for most browsers without any client-side logic.

### Quality control with quality

Reduce file size by adjusting compression quality (1-100):

// Good balance of quality and file size for product thumbnails
?quality=75

// Higher quality for hero images where detail matters
?quality=90

For JPEG and WebP formats, quality values between 70 and 85 provide a good balance. Below 60, compression artifacts become visible on product photography. For PNG, the quality parameter controls the compression level safely without introducing artifacts.

### Cropping with crop

Extract a specific region of the image:

// Crop to a 400x400 region starting at position (100, 50)
?crop=400,400,x100,y50

The crop parameter accepts width,height,x{offset},y{offset} format profiles. This is useful for creating square thumbnails from rectangular product photos.

### Fit modes with fit

When you specify both width and height, the fit parameter controls how the image fills the target dimensions:

// Scale down to fit within the bounds, preserving aspect ratio
?width=400&height=400&fit=bounds

// Crop to fill the exact dimensions
?width=400&height=400&fit=crop

Available fit modes:

Mode

Behavior

bounds

Scales down to fit within the specified width and height. The image may be smaller than the target on one axis.

crop

Scales and crops to fill the exact dimensions. Parts of the image may be trimmed.

### Trim with trim

Remove uniform borders or whitespace from product images:

// Trim 20px from all sides
?trim=20,20,20,20

The format is configured as trim=top,right,bottom,left. This is useful for product catalog images that have inconsistent whitespace around the product.

## Combining parameters

Parameters can be combined in a single URL string. The CDN processes them sequentially in order and caches the final result:

https://images.contentstack.io/v3/assets/.../matrix-link-bracelet.jpg?width=800&height=600&fit=crop&format=webp&quality=80

This single URL delivers an 800x600 cropped WebP image at 80% quality. No build step, no image processing library, no lambda function. The CDN handles it.

## Building responsive image srcsets

Modern responsive design requires serving different image sizes for different viewport widths. Contentstack's URL-based transformations make this straightforward by parameterizing the width.

### The srcset attribute approach

<img src="https://images.contentstack.io/v3/assets/.../product.jpg?width=800&amp;auto=webp&amp;quality=80" srcset="
    https://images.contentstack.io/v3/assets/.../product.jpg?width=400&amp;auto=webp&amp;quality=80 400w,
    https://images.contentstack.io/v3/assets/.../product.jpg?width=800&amp;auto=webp&amp;quality=80 800w,
    https://images.contentstack.io/v3/assets/.../product.jpg?width=1200&amp;auto=webp&amp;quality=80 1200w,
    https://images.contentstack.io/v3/assets/.../product.jpg?width=1600&amp;auto=webp&amp;quality=80 1600w
  " sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" alt="Matrix Link Bracelet" width="800" height="600">

The browser selects the most appropriate image size based on the viewport width and device pixel ratio. You generate the URLs in code by varying the width parameter.

### A helper function for srcset generation

Rather than constructing URLs manually, building a utility function:

interface ImageTransformOptions {
  quality?: number;
  format?: "webp" | "avif" | "jpg" | "png";
  fit?: "bounds" | "crop";
  height?: number;
}

function buildImageUrl(
  baseUrl: string,
  width: number,
  options: ImageTransformOptions = {}
): string {
  const params = new URLSearchParams();
  params.set("width", String(width));

  if (options.quality) params.set("quality", String(options.quality));
  if (options.format) params.set("format", options.format);
  if (options.fit) params.set("fit", options.fit);
  if (options.height) params.set("height", String(options.height));
  if (!options.format) params.set("auto", "webp");

  return \`${baseUrl}?${params.toString()}\`;
}

function buildSrcSet(
  baseUrl: string,
  widths: number\[\],
  options: ImageTransformOptions = {}
): string {
  return widths
    .map((w) => \`${buildImageUrl(baseUrl, w, options)} ${w}w\`)
    .join(", ");
}

// Usage with a product entry from Contentstack
const product = await fetchProduct("matrix-link-bracelet");
const imageUrl = product.media?.\[0\]?.url;

const srcset = buildSrcSet(imageUrl, \[400, 800, 1200, 1600\], {
  quality: 80,
  fit: "crop",
  height: 600,
});

This utility works with any asset URL from Contentstack. It avoids string concatenation bugs and makes it easy to enforce consistent quality and format settings across the application.

## Lazy loading patterns with Contentstack image URLs

Lazy loading defers off-screen image loading until the user scrolls near them. Combined with Contentstack transformations, you can serve a tiny placeholder followed by the full image asset.

### Native lazy loading

The simplest approach uses the browser's native loading attribute:

<img src="https://images.contentstack.io/v3/assets/.../product.jpg?width=800&amp;auto=webp&amp;quality=80" loading="lazy" alt="Pixel Stud Earrings" width="800" height="600">

For above-the-fold images (hero images, first visible product), omit loading="lazy" or set loading="eager" to ensure they load immediately.

### Low-quality image placeholder (LQIP) pattern

Serve an extremely small version as a placeholder that loads instantly, then swap in the full image:

// Tiny blurred placeholder - loads in ~1-2 KB
const placeholderUrl = buildImageUrl(imageUrl, 40, {
  quality: 30,
  format: "webp",
});

// Full resolution product image
const fullUrl = buildImageUrl(imageUrl, 800, {
  quality: 80,
});

An intersection observer or a utility swaps data-src into src when the image enters the viewport. The CSS blur transition provides a smooth reveal.

## Optimizing Core Web Vitals with proper image sizing

Google's Core Web Vitals - particularly Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS) - are directly impacted by image handling.

### Reducing LCP with right-sized images

LCP measures when the largest visible element finishes rendering. For product pages, this is usually the hero product image. To minimize LCP:

1.  Serve the correct size: A 4000px image displayed at 800px wastes bandwidth. Use ?width=800 to match the rendered size.
2.  Use modern formats: ?auto=webp reduces file size by 25-35% compared to legacy JPEG arrays.
3.  Set appropriate quality: ?quality=80 is visually indistinguishable from 100 for most product photos, at half the raw payload file size.
4.  Preload the LCP image: Add a <link rel="preload"> for the hero image so the browser starts fetching it before it discovers the  tag in the HTML.

### Preventing CLS with explicit dimensions

CLS penalizes layout shifts caused by images loading without reserved space. Always set width and height attributes on  elements:

<img src="https://images.contentstack.io/.../product.jpg?width=800&amp;auto=webp&amp;quality=80" width="800" height="600" alt="Data Drop Earrings">

The browser uses the width/height ratio to reserve layout space before the image loads. If you use CSS to make images responsive (width: 100%; height: auto;), the aspect ratio is preserved from the HTML attributes, preventing layout shifts.

### Combining techniques for a product card

Here is a complete example building a product card component with optimized images:

interface Product {
  title: string;
  price: number;
  media: Array<{ url: string; title: string }> | null;
  url: string;
}

function renderProductCard(product: Product): string {
  const baseUrl = product.media?.\[0\]?.url ?? "";

  const srcset = buildSrcSet(baseUrl, \[300, 600, 900\], {
    quality: 80,
    fit: "crop",
    height: 400,
  });

  const defaultSrc = buildImageUrl(baseUrl, 600, {
    quality: 80,
    fit: "crop",
    height: 400,
  });

  return \`
    
  \`;
}

This card serves right-sized, WebP-converted, quality-optimized images with proper responsive breakpoints, lazy loading for off-screen cards, and explicit dimensions to prevent layout shift.

## When not to use URL transformations

Contentstack's image transformations cover most use cases, but know their structural operational limits:

*   Complex compositing: Overlaying dynamic text, complex watermarks, or combining multiple image binaries requires a dedicated image microservice infrastructure.
*   SVG manipulation: SVG files are delivered as-is as clean vectors. Transformation parameters apply exclusively to raster formats (JPEG, PNG, WebP, GIF).
*   Video thumbnails: Contentstack's image service handles images only. Video poster frames need separate automated asset processing.
*   Extremely high-resolution print assets: The service is optimized for web delivery. For print-resolution assets, download the original and process locally.

## Exercise: construct image URLs for a responsive product card

Using a product entry with an image asset from Contentstack, follow these implementation steps:

1.  Write a buildImageUrl() function that takes a base Contentstack asset URL and returns a transformed URL with width, quality, and format parameters.
2.  Write a buildSrcSet() function that generates a srcset string for widths \[320, 640, 960, 1280\].
3.  Create an HTML <img> element that uses the srcset, includes sizes for a two-column grid layout, sets explicit width and height, and uses loading="lazy" for below-fold images.
4.  Add a <link rel="preload"> tag for the first product card's image (above the fold).
5.  Verify that adding ?auto=webp&quality=75 to a sample image URL reduces its file size compared to the original (use browser DevTools Network tab to compare).

#### Key takeaways

- Connect **Image delivery and transformation APIs** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 11 — Performance, caching, and frontend integration

<!-- ai_metadata: {"lesson_id":"11","type":"text","duration_minutes":1,"topics":["Performance","caching","and","frontend","integration"]} -->

#### Lesson text

# Performance, caching, and frontend integration

> **TL;DR**
> 
> *   Contentstack's CDN invalidates on publish -- no manual cache purge needed for the delivery layer, but your own application caches (Redis, edge, in-memory) need webhook-driven invalidation.
> *   Eliminate N+1 queries by using includeReference() for related entries and Promise.all for independent content type fetches.
> *   Match rendering mode to content volatility: SSG for stable pages, ISR for periodically updated content, SSR only when freshness on every request is non-negotiable.

Fetching content from Contentstack is straightforward. Fetching it efficiently - with the right caching strategy, the right rendering mode, and the right data-fetching patterns - is where production quality lives. This lesson covers the caching behavior of Contentstack's CDN, client-side caching strategies, framework integration patterns, and the common performance mistakes that slow down headless sites.

The running example throughout this lesson is Veda: The Revival Collection built on Contentstack: product pages, category pages, a homepage with product lines and featured content, and campaign updates that need fast propagation.

## CDN behavior for the Contentstack Delivery API

Contentstack's Content Delivery API sits behind a CDN. When you make a GET request to https://cdn.contentstack.io/v3/content\_types/product/entries, the response is served from an edge node close to the user. Subsequent identical requests are served from cache until the cache is invalidated.

### Cache invalidation on publish

Cache invalidation is tied to the publish action. When an editor publishes or unpublishes an entry, Contentstack purges the relevant CDN cache entries. This means:

*   After publish: New content is available within seconds globally. The CDN evicts stale entries and subsequent requests hit the origin, which returns the updated content.
*   Between publishes: Content is served from cache. Identical API calls resolve at the CDN edge without hitting Contentstack's origin servers.
*   Draft changes: Saving a draft does not affect the delivery cache. Only the publish action triggers cache invalidation.

This publish-driven invalidation model is why Contentstack recommends the Delivery API for production traffic. The CDN handles read scale, and cache freshness is managed through the editorial workflow.

### Cache-Control headers

Contentstack's Delivery API responses include Cache-Control headers. The exact values depend on the request, but typical behavior is Cache-Control: public, max-age=0, must-revalidate.

The max-age=0 combined with must-revalidate means downstream caches (browser, reverse proxy) should revalidate on every request. Contentstack's own CDN handles the primary caching layer; it does not intend for browsers to cache API responses long-term on their own.

In practice, this means:

*   Browser requests always check with the CDN edge.
*   The CDN edge serves from its cache if the content has not been republished.
*   There is no stale browser cache problem - the CDN is the source of truth for freshness.

If you need more aggressive client-side caching, implement it in your application layer. The Contentstack CDN does the heavy lifting, but your application can add another caching tier on top.

## Client-side caching strategies

Depending on your rendering architecture, you have several options for caching Contentstack responses closer to the user.

### Stale-while-revalidate

The stale-while-revalidate pattern serves cached content immediately while asynchronously fetching fresh content in the background. This is ideal for content that updates periodically but where a few seconds of staleness is acceptable.

// A minimal stale-while-revalidate cache for Contentstack responses
const cache = new Map();
const STALE\_THRESHOLD\_MS = 60\_000; // 1 minute

async function fetchWithSWR(cacheKey: string, fetcher: () => Promise): Promise {
  const cached = cache.get(cacheKey);
  const now = Date.now();

  if (cached) {
    if (now - cached.timestamp < STALE\_THRESHOLD\_MS) {
      return cached.data as T;
    }

    // Serve stale, revalidate in background
    fetcher().then((fresh) => {
      cache.set(cacheKey, { data: fresh, timestamp: Date.now() });
    });
    return cached.data as T;
  }

  const data = await fetcher();
  cache.set(cacheKey, { data, timestamp: now });
  return data;
}

// Usage for a Veda product
const product = await fetchWithSWR(
  \`product:${slug}\`,
  () => fetchProductBySlug(slug)
);

For e-commerce sites, this pattern means a visitor sees the cached product immediately, and if the product was updated since their last visit, the next page load shows the fresh version.

### Static site generation (SSG)

Static generation pre-renders pages at build time. Each page makes its Contentstack API calls during the build, and the resulting HTML files are deployed to a CDN. No API calls happen at runtime.

Advantages for an e-commerce site:

*   Pages load instantly from the static CDN.
*   No runtime dependency on the Contentstack API.
*   Excellent for SEO - pages are fully rendered HTML.

Disadvantage: content updates require a rebuild. For a campaign site with time-sensitive launches, a rebuild pipeline that takes 5 minutes means the new content is 5 minutes stale.

### Incremental static regeneration (ISR)

ISR is a hybrid: pages are statically generated but can be regenerated on demand or after a time interval. This combines the performance of SSG with the freshness of server-side rendering.

In a Next.js context:

// app/products/\[line\]/\[slug\]/page.tsx
import Contentstack from "@contentstack/delivery-sdk";

const stack = Contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  environment: "production",
  region: Contentstack.Region.US,
});

async function getProduct(line: string, slug: string) {
  const query = stack.contentType("product").entry().query();
  const result = await query
    .equalTo("url", \`/products/${line}/${slug}\`)
    .includeReference("product\_line")
    .includeReference("category")
    .find();
  return result.entries\[0\] ?? null;
}

export const revalidate = 60; // Regenerate page every 60 seconds

export default async function ProductPage({
  params,
}: {
  params: { line: string; slug: string };
}) {
  const product = await getProduct(params.line, params.slug);
  if (!product) return <div>Product not found</div>;

  return (
    
  );
}

The revalidate = 60 tells the engine to serve the cached page and regenerate it in the background at most every 60 seconds. For campaign launches, you might reduce this to 10 seconds. For evergreen product pages, 3600 seconds (one hour) might be appropriate.

### Server-side rendering (SSR)

SSR generates the page on every request. Each visitor triggers a Contentstack API call, and the response is rendered into HTML on the server before being sent to the browser.

SSR guarantees the freshest content but introduces latency (API call + render) on every page load. For a Veda homepage that updates with new campaigns, SSR ensures no visitor sees stale hero content. For product pages that rarely change, SSR wastes resources.

## Framework integration patterns

### Next.js (App Router)

Next.js App Router uses React Server Components. Data fetching happens in server components by default, and Next.js handles caching through its fetch cache and route segment configuration.

// lib/contentstack.ts - centralized Contentstack client
import Contentstack from "@contentstack/delivery-sdk";

export const stack = Contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
  region: Contentstack.Region.US,
});

export async function getHomepageContent() {
  const \[pageResult, digitalDawnResult, earringsResult\] = await Promise.all(\[
    stack.contentType("page").entry().query().equalTo("url", "/").find(),
    stack
      .contentType("product")
      .entry()
      .query()
      .equalTo("product\_line", "blt\_digital\_dawn\_001")
      .orderByDescending("created\_at")
      .limit(4)
      .includeReference("product\_line")
      .find(),
    stack
      .contentType("product")
      .entry()
      .query()
      .equalTo("category", "blt\_earrings\_001")
      .orderByDescending("created\_at")
      .limit(4)
      .includeReference("category")
      .find(),
  \]);

  return {
    hero: pageResult.entries\[0\]?.components?.\[0\],
    digitalDawn: digitalDawnResult.entries,
    earrings: earringsResult.entries,
  };
}

// app/page.tsx - homepage server component
import { getHomepageContent } from "@/lib/contentstack";

export const revalidate = 30;

export default async function HomePage() {
  const { hero, digitalDawn, earrings } = await getHomepageContent();

  return (
    
        {digitalDawn.map((product) => (
          
        ))}
      
        {earrings.map((product) => (
          
        ))}
      
  );
}

The Promise.all call completes queries simultaneously, reducing total data-fetch time compared to sequential blocking calls. This is a key pattern for pages that assemble content from multiple queries.

### Nuxt

Nuxt 3 uses useAsyncData for server-side data fetching with built-in caching:

// composables/useContentstack.ts
import Contentstack from "@contentstack/delivery-sdk";

const stack = Contentstack.stack({
  apiKey: useRuntimeConfig().public.contentstackApiKey,
  deliveryToken: useRuntimeConfig().public.contentstackDeliveryToken,
  environment: useRuntimeConfig().public.contentstackEnvironment,
  region: Contentstack.Region.EU,
});

export function useProductsByCategory(categoryUid: string) {
  return useAsyncData(\`products-${categoryUid}\`, async () => {
    const query = stack.contentType("product").entry().query();
    const result = await query
      .equalTo("category", categoryUid)
      .orderByDescending("created\_at")
      .limit(10)
      .includeReference("product\_line")
      .find();
    return result.entries;
  });
}

const route = useRoute();
const { data: products } = useProductsByCategory(route.params.slug as string);

Nuxt's useAsyncData deduplicates requests: if multiple components request the same data during SSR, only one API call is made. The data is serialized into the page payload so the client does not re-fetch.

### Astro

Astro defaults to static generation with opt-in SSR. Content from Contentstack is fetched at build time:

\---
// src/pages/products/\[line\]/\[slug\].astro
import Contentstack from "@contentstack/delivery-sdk";

const stack = Contentstack.stack({
  apiKey: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
  deliveryToken: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN,
  environment: "production",
  region: Contentstack.Region.US,
});

export async function getStaticPaths() {
  const result = await stack.contentType("product").entry().query().find();
  return result.entries.map((product) => ({
    params: { line: product.url.split("/")\[2\], slug: product.url.split("/")\[3\] },
    props: { product },
  }));
}

const { product } = Astro.props;
---

Astro's getStaticPaths fetches all products at build time and generates a static page for each. For a Veda catalog with hundreds of products, combine this with Astro's on-demand rendering for featured products and static generation for the full catalog.

## Webhook-triggered rebuilds for static sites

Static sites need a mechanism to rebuild when content changes. Contentstack webhooks provide this trigger.

Configure a webhook in Contentstack (Settings > Webhooks) that fires on entry publish and unpublish events. Point it at your hosting platform's build hook:

*   Vercel integrations
*   Netlify build hooks
*   Cloudflare Pages configurations
*   Vercel: https://api.vercel.com/v1/integrations/deploy/{deploy-hook-id}
*   Netlify: https://api.netlify.com/build\_hooks/{hook-id}
*   Cloudflare Pages: Use a Worker to trigger a build via the Pages API.

For a Veda site, configure the webhook to fire only for the product, page, and product\_line content types to avoid unnecessary rebuilds when editors update internal reference data.

With webhook-triggered rebuilds, the flow is: editor publishes product → Contentstack fires webhook → hosting platform triggers rebuild → new static site deploys in 30-120 seconds → visitors see updated content.

For ISR-based sites, webhooks can call an explicit on-demand revalidation endpoint instead of triggering a full rebuild:

// app/api/revalidate/route.ts (Next.js)
import { revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const body = await request.json();
  const secret = request.headers.get("x-webhook-secret");

  if (secret !== process.env.NEXT\_PUBLIC\_CONTENTSTACK\_WEBHOOK\_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const entryUrl = body?.data?.entry?.url;
  if (entryUrl) {
    revalidatePath(entryUrl);
    return NextResponse.json({ revalidated: true, path: entryUrl });
  }

  revalidatePath("/");
  return NextResponse.json({ revalidated: true, path: "/" });
}

This approach revalidates only the affected page rather than rebuilding the entire site.

## Avoiding N+1 query patterns

The N+1 problem occurs when your code makes one query to get a list of entries, then N additional queries to resolve data for each entry. This is the most common performance killer in headless CMS integrations.

### The problem

// BAD: N+1 pattern
const productsResult = await stack
  .contentType("product")
  .entry()
  .query()
  .limit(20)
  .find();

// This makes 20 additional API calls!
const productsWithLines = await Promise.all(
  productsResult.entries.map(async (product) => {
    const productLine = await stack
      .contentType("product\_line")
      .entry(product.product\_line\[0\].uid)
      .fetch();
    return { ...product, productLineData: productLine };
  })
);

Twenty products means twenty product line fetches. For a homepage with multiple content strips, each strip with its own N+1 pattern, you might make 50+ API calls to render a single page.

### The solution: batch reference resolution

Use includeReference() to resolve references in the original query.

// GOOD: Single query with included references
const productsResult = await stack
  .contentType("product")
  .entry()
  .query()
  .limit(20)
  .includeReference("product\_line")
  .includeReference("category")
  .find();

// Product lines and categories are already resolved in each entry
productsResult.entries.forEach((product) => {
  console.log(product.title, "from", product.product\_line\[0\].title);
  console.log("Category:", product.category\[0\].title);
});

One API call instead of 21. The CDN caches this single response, making subsequent page loads even faster.

### When you cannot use includes

If the data you need spans multiple unrelated content types (not connected by references), use Promise.all to parallelize the queries rather than making them sequentially:

// Parallel independent queries for the Veda homepage
const \[products, productLines, categories\] = await Promise.all(\[
  stack.contentType("product").entry().query().limit(12).find(),
  stack.contentType("product\_line").entry().query().find(),
  stack.contentType("category").entry().query().find(),
\]);

Three API calls in parallel complete in the time of the slowest call, not the sum of all three.

## Edge caching considerations

Edge computing platforms (Cloudflare Workers, Vercel Edge Functions, Deno Deploy) execute code close to the user. When paired with Contentstack, you get specialized behaviors:

*   Edge-cached API responses: Cache Contentstack API responses at the edge with a short TTL. This reduces latency compared to fetching from the nearest Contentstack CDN node.
*   Edge rendering: Render HTML at the edge using cached Contentstack data. The user gets fully rendered HTML from the nearest edge node.
*   Purge coordination: If you cache at the edge, you need a mechanism to purge when content is published. Contentstack webhooks can trigger edge cache purges.

The trade-off is complexity. Edge caching adds another layer to manage, another cache to invalidate, and another source of staleness. For most e-commerce sites like Veda, Contentstack's CDN plus framework-level caching (ISR or SWR) provides sufficient performance without custom edge caching.

## When to use SSR vs SSG vs ISR

Rendering mode

Best for

Content freshness

Performance

SSG

Archive pages, reference content, documentation

Only updated on rebuild

Fastest - served from static CDN

ISR

Articles, product pages, category pages

Updated within revalidation window (10s-3600s)

Fast - cached HTML, periodic refresh

SSR

Breaking news homepage, personalized feeds, search results

Always fresh

Depends on API latency + render time

For a Veda e-commerce site, a practical split follows this distribution matrix:

*   Homepage: ISR with 30-second revalidation, or SSR if campaign freshness is critical.
*   Product pages: ISR with 60-second revalidation. Webhook-triggered revalidation for immediate updates.
*   Category pages: ISR with 300-second revalidation or SSG with webhook-triggered rebuilds.
*   Search results: SSR (search parameters vary per request; caching is impractical).

## Common mistakes

> **Common pitfall:**
> 
> Adding your own caching layer (Redis, in-memory, edge cache) without a webhook-driven purge mechanism means editors publish content that never appears on the site -- Contentstack only invalidates its own CDN, not yours.

### Not invalidating application-level caches

Contentstack invalidates its own CDN on publish. But if your application adds its own caching layer (Redis, in-memory, edge cache) without a purge mechanism, editors publish content and it does not appear on the site. Always pair application caching with webhook-driven invalidation.

### Over-fetching in SSR

Fetching all fields of all entries on every SSR request wastes bandwidth and increases Time to First Byte (TTFB). Use the only\[\] REST parameter or GraphQL to request only the fields your page needs. For a product listing, you need title, url, price, short\_description, and media - not the full description HTML.

GET /v3/content\_types/product/entries?environment=production&only\[BASE\]\[\]=title&only\[BASE\]\[\]=url&only\[BASE\]\[\]=price&only\[BASE\]\[\]=short\_description&limit=20

### N+1 queries from template loops

The N+1 pattern often hides inside template rendering. A loop over products that fetches product line data per iteration is invisible in code review but devastating to page load time. Audit data-fetching patterns: if any fetch call appears inside a loop or map, it is likely an N+1.

### Ignoring error handling in data fetching

Contentstack API calls can fail (network issues, rate limits, token expiry). SSR pages that do not handle fetch errors crash the entire page. Always wrap Contentstack calls in standard block catch logic and provide fallback route behavior:

async function getProductsSafe(categoryUid: string) {
  try {
    const query = stack.contentType("product").entry().query();
    const result = await query
      .equalTo("category", categoryUid)
      .limit(10)
      .includeReference("product\_line")
      .find();
    return result.entries;
  } catch (error) {
    console.error("Failed to fetch products:", error);
    return \[\]; // Render empty state rather than crashing
  }
}

### Not measuring actual performance

Set up monitoring for Contentstack API call latency, page TTFB, and Core Web Vitals. Without metrics, you cannot distinguish between a caching misconfiguration and a content model issue. Tools like Vercel Analytics, web-vitals library, or custom logging provide visibility into real user performance.

#### Key takeaways

- Connect **Performance, caching, and frontend integration** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 12 — Environments and Deployment : Overview

<!-- ai_metadata: {"lesson_id":"12","type":"text","duration_minutes":1,"topics":["Environments","and","Deployment","Overview"]} -->

#### Lesson text

# Environments and Deployment

This module connects content delivery to deployment reality: environments, promotion strategy, CI/CD alignment, and migration tooling.

## Why This Module Matters

Many teams know how to fetch content but still ship fragile systems because environments, publish flows, and migrations are not aligned with code deployment.

## You Will Be Able To

*   explain how environments, tokens, and delivery targets fit together
*   coordinate content promotion with CI/CD and rollback planning
*   use the Contentstack CLI for repeatable schema and content operations

## Recommended Preparation

Complete Modules 3.1 and 3.2 first so API and rendering concepts are already grounded.

## Estimated Effort

75-90 minutes

## Practice Focus

Promote the Veda storefront through development, staging, and production patterns without mixing tokens, environments, or migration order.

## Suggested Next Step

Start with lesson 1 in this module and map every environment decision to a real deployment or governance consequence.

#### Key takeaways

- Connect **Environments and Deployment : Overview** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 13 — Environments, publishing, and promotion strategies

<!-- ai_metadata: {"lesson_id":"13","type":"text","duration_minutes":1,"topics":["Environments","publishing","and","promotion","strategies"]} -->

#### Lesson text

# Environments, publishing, and promotion strategies

> **TL;DR**
> 
> *   Environments are deployment targets (development, staging, production), not code branches -- Contentstack has a separate branching model for parallel content work.
> *   Each environment gets its own delivery token; use publish rules to restrict which roles can publish to sensitive environments like production.
> *   Publish assets before the entries that reference them to avoid broken references on the target environment.

> **Prerequisites**
> 
> This module assumes comfort with CLI/terminal usage and basic CI/CD concepts (build pipelines, environment variables, deploy hooks). If you're new to these, review your CI/CD platform's getting-started guide first.

In Contentstack, an environment is a deployment target, not a code branch. This distinction trips up developers who come from git-centric workflows where "environment" and "branch" are loosely interchangeable. They are not interchangeable here. Contentstack has a first-class branching model for content (covered elsewhere in this course), and environments serve a completely different purpose: they represent the destinations where published content becomes available for delivery.

Getting environments right is foundational. Every delivery token is scoped to an environment. Every publish action targets an environment. Every frontend deployment maps to an environment. If your environment strategy is muddled, your entire content delivery pipeline inherits that confusion.

## What environments represent

An environment in Contentstack defines a named deployment target with associated configuration:

*   Name: a human-readable identifier like development, staging, or production.
*   Base URL: the URL of the frontend application that consumes published content from this environment. For an e-commerce site, this might be https://shop.example.com for production and https://staging.shop.example.com for staging.
*   Preview URL: an optional URL used by Live Preview to render draft content in the context of a specific environment.
*   Server or region settings: additional configuration depending on your Contentstack plan and region.

Environments are stack-level resources. When you create a stack for your e-commerce platform, you define environments that match your deployment topology. A typical three-environment setup might look like this:

Environment

Base URL

Purpose

development

[https://dev.shop.example.com](https://dev.shop.example.com/)

Developer integration testing

staging

[https://staging.shop.example.com](https://staging.shop.example.com/)

QA, stakeholder review, UAT

production

[https://shop.example.com](https://shop.example.com/)

Live customer-facing storefront

Each environment operates independently in terms of published content state. An entry published to staging is not automatically available in production. This is by design: it gives teams explicit control over what content reaches which audience.

## Each environment gets its own delivery token

This is where environment strategy meets security architecture. Contentstack issues delivery tokens per environment. Your production frontend uses a delivery token scoped to the production environment. Your staging frontend uses a different token scoped to staging.

// environment-tokens.ts  -  e-commerce storefront configuration
const environmentConfig = {
  development: {
    deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN\_DEV,
    apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
    environment: "development",
  },
  staging: {
    deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN\_STAGING,
    apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
    environment: "staging",
  },
  production: {
    deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN\_PROD,
    apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
    environment: "production",
  },
};

export function getStackConfig(env: keyof typeof environmentConfig) {
  return environmentConfig\[env\];
}

The security implication is direct: if a staging delivery token leaks, production content is unaffected. Token isolation per environment limits blast radius. This also means that rotating tokens can happen per environment without disrupting unrelated deployment targets.

## Publishing to environments

Publishing is the act of making content available through the Content Delivery API for a specific environment. Until an entry or asset is published to an environment, it exists only in draft state and is accessible only through the Content Management API or Preview endpoints.

### Single-environment publish

The most common publish action targets one environment at a time. An editor working on a new product listing for the e-commerce catalog publishes to development first for developer verification, then promotes to staging for QA review.

### Multi-environment publish

Contentstack also supports publishing an entry to multiple environments simultaneously. This is useful when content has been fully vetted and needs to go live across several targets at once. For example, publishing a critical pricing update to both staging and production at the same time. However, multi-environment publish should be used deliberately. In most mature workflows, you want content to move through environments sequentially, not skip stages.

### Publishing entries vs publishing assets

Entries and assets follow the same environment-scoped publishing model, but they have different operational characteristics:

*   Entries carry references to other entries and assets. Publishing an entry does not automatically publish its referenced content. If an entry references an asset that has not been published to the target environment, the delivery response will contain a broken reference.
*   Assets are independently publishable. A product image can be published to production before or after the product entry that references it, but the ordering matters for frontend rendering correctness.

A disciplined approach is to publish assets before the entries that reference them. For the e-commerce storefront, this means publishing product images to staging before publishing the product entries that display those images.

## The publish queue

Every publish action in Contentstack enters a publish queue. The queue processes publish requests asynchronously, which means that clicking "Publish" in the UI or triggering a publish via CMA does not guarantee instant availability on the CDN.

The publish queue provides several operational benefits:

*   Rate smoothing: large bulk-publish operations do not overwhelm the delivery infrastructure.
*   Visibility: the queue shows pending, in-progress, and completed publish actions.
*   Error surfacing: failed publishes appear in the queue with actionable error details.

For the e-commerce site running a seasonal catalog refresh that involves publishing hundreds of product entries and their associated assets, the publish queue becomes operationally critical. Teams can monitor progress, identify failures, and retry individual items without re-triggering the entire batch.

You can inspect the publish queue programmatically through the CMA:

// Check publish queue status for a bulk operation
const response = await fetch(
  "https://api.contentstack.io/v3/publish-queue",
  {
    headers: {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
      authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
    },
  }
);

const queue = await response.json();
queue.queue.forEach((item: any) => {
  console.log(
    \`${item.entry?.title || item.asset?.title} → ${item.environment} \[${item.status}\]\`
  );
});

## Promotion strategy: development to staging to production

A promotion strategy defines how content moves through environments. The standard pattern mirrors application deployment pipelines:

1.  Development: content is authored and initially published here. Developers integrate against this environment. Broken content, experimental entries, and work-in-progress all live here.
2.  Staging: content that passes initial review is promoted to staging. This environment is used for stakeholder sign-off, QA testing, and user acceptance. The staging frontend should closely mirror production behavior.
3.  Production: only fully approved content is published here. This is what end customers see.

Promotion can be manual (editors republish to the next environment) or governed by workflow and publish rules that enforce stage gates. In a well-run e-commerce operation, a new product listing might follow this path:

*   Editor creates the product entry and publishes to development.
*   Developer verifies the entry renders correctly in the storefront.
*   Editor publishes to staging for merchandising team review.
*   After approval, a senior editor publishes to production.

Each publish action is explicit and auditable. Content does not drift between environments without intentional action.

## Publish rules: governing who can publish where

Contentstack supports publish rules that restrict which roles can publish to which environments. This is a governance mechanism, not just a convenience.

For an e-commerce platform with multiple teams, publish rules might look like:

*   Content Authors can publish to development only.
*   Content Managers can publish to development and staging.
*   Senior Editors (or a Release Manager role) can publish to production.

Publish rules enforce the promotion strategy structurally. Without them, any editor with publish permissions could accidentally push an unreviewed product entry directly to the live storefront.

Publish rules work alongside workflow stages. A common pattern combines them: workflow gates ensure content goes through review steps, and publish rules ensure only authorized roles can execute the final publish action to sensitive environments.

## How environments map to frontend deployment targets

The mapping between Contentstack environments and frontend deployments should be explicit and documented. For the e-commerce storefront built on Next.js and deployed via Vercel:

Contentstack environment

Frontend deployment

Vercel environment

Delivery token source

development

dev.shop.example.com

Preview

NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN\_DEV

staging

staging.shop.example.com

Preview (staging)

NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN\_STAGING

production

shop.example.com

Production

NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN\_PROD

Each Vercel environment injects the correct delivery token. The frontend code itself does not change between environments; only the token and the Contentstack environment name differ. This is the same principle as twelve-factor app configuration: environment-specific values live in environment variables, not in code.

// next.config.ts  -  environment-aware Contentstack configuration
import contentstack from "@contentstack/delivery-sdk";

const stack = contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
});

export default stack;

The NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT variable resolves to development, staging, or production depending on where the frontend is deployed. No conditional logic. No hardcoded environment names.

## Common mistakes

### Mistake 1: using environments as branches

Contentstack has a dedicated branching system for parallel content development. Environments are not branches. If you create an environment called feature-new-checkout to isolate content changes, you are misusing the system. Use a Contentstack branch instead, then publish from that branch to the appropriate environment when ready.

> **Common pitfall:**
> 
> Publishing directly to production without staging verification saves a few minutes but can cost hours of incident response when broken content, missing references, or layout issues reach customers.

### Mistake 2: publishing to production without staging verification

Skipping the staging step saves a few minutes and costs hours when broken content reaches customers. Even for "simple text changes," the staging environment exists to catch rendering issues, broken references, and layout problems that are invisible in the CMS entry editor.

### Mistake 3: ignoring asset publish order

Publishing a product entry that references an unpublished hero image produces a broken storefront page. Always verify that referenced assets are published to the target environment before or alongside the entries that depend on them.

### Mistake 4: sharing delivery tokens across environments

Using the production delivery token in your staging frontend defeats the purpose of environment isolation. Each frontend deployment must use the token scoped to its corresponding Contentstack environment.

## Summary

Environments in Contentstack are deployment targets with dedicated delivery tokens, independent publish states, and governance controls. A disciplined promotion strategy moves content from development through staging to production, with publish rules restricting who can act at each stage. The publish queue provides visibility into asynchronous publish operations, and the mapping between CMS environments and frontend deployments should be explicit, documented, and enforced through environment variables.

The key insight is that environments are about where content is delivered, not about content versioning or parallel development. Contentstack branches handle those concerns. Conflating the two leads to architectural confusion that is painful to unwind later.

#### Key takeaways

- Connect **Environments, publishing, and promotion strategies** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 14 — Aligning CMS workflows with CI/CD

<!-- ai_metadata: {"lesson_id":"14","type":"text","duration_minutes":1,"topics":["Aligning","CMS","workflows","with"]} -->

#### Lesson text

# Aligning CMS workflows with CI/CD

> **TL;DR**
> 
> *   Connect Contentstack publish events to your CI/CD pipeline via webhooks -- filter by environment and content type to avoid unnecessary rebuilds.
> *   Export content type schemas to version control and add a CI step that detects schema drift between Contentstack and your committed definitions.
> *   During rollbacks, revert code first (restore the frontend that expects the old schema), then republish previous content versions.

Content changes and code deployments operate on independent timelines. A marketing team updates the corporate homepage banner at 2 PM on a Tuesday. The engineering team deploys a redesigned navigation component the following Thursday. Neither change requires the other, yet both affect what visitors experience on the corporate marketing site.

This independence is a feature of headless architecture, not a flaw. But it creates a coordination problem that many teams discover only after something breaks in production: a new page layout deploys before the content structure it expects exists, or content references a component that was removed in the last code release. Aligning CMS workflows with CI/CD pipelines is the practice of making these two change streams aware of each other without coupling them tightly.

## The coordination problem

In a traditional monolithic CMS, content and presentation ship together. In a headless setup, they ship independently:

*   Content publishes happen through Contentstack environments and are controlled by editorial workflows.
*   Code deploys happen through CI/CD pipelines (GitHub Actions, GitLab CI, Vercel, Netlify) and are controlled by engineering workflows.

The risk surfaces at the intersection. A content type schema change might require a frontend code update. A frontend refactor might expect content fields that editors have not populated yet. Without explicit coordination points, these mismatches produce broken pages that neither team anticipated.

## Webhook-triggered builds

The most direct integration point between Contentstack and CI/CD is the webhook. Contentstack can fire HTTP webhooks on content lifecycle events: entry publish, entry unpublish, asset publish, content type update, and others.

For a corporate marketing site using static site generation, the primary integration pattern is: when content publishes to production, trigger a site rebuild.

### Configuring the webhook in Contentstack

In the Contentstack dashboard, navigate to Settings > Webhooks and create a webhook with these properties:

*   URL: your CI/CD pipeline trigger endpoint (e.g., a Vercel deploy hook or a GitHub Actions repository dispatch URL).
*   Events: select the publish events relevant to your build. Typically entry.publish and asset.publish for the production environment.
*   Environments: filter to the specific environment that should trigger builds. You almost never want development publishes to trigger production builds.

### Example: triggering a Vercel rebuild on publish

// Contentstack webhook configuration (conceptual)
// URL: https://api.vercel.com/v1/integrations/deploy/prj\_xxxx/yyyy
// Method: POST
// Trigger: entry.publish on environment "production"

// The webhook fires automatically. No custom code needed on the CMS side.
// Vercel receives the POST and initiates a new deployment.

For more control, route the webhook through a lightweight serverless function that validates the payload and decides whether to trigger a rebuild:

// /api/cms-webhook-handler.ts  -  corporate marketing site
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const event = req.body;

  // Only rebuild for production environment publishes
  if (event.environment?.name !== "production") {
    return res.status(200).json({ skipped: true, reason: "non-production" });
  }

  // Only rebuild for content types that affect the marketing site
  const rebuildTypes = \["page", "product", "product\_line", "header"\];
  if (!rebuildTypes.includes(event.content\_type?.uid)) {
    return res.status(200).json({ skipped: true, reason: "unrelated content type" });
  }

  // Trigger the actual rebuild
  await fetch(process.env.VERCEL\_DEPLOY\_HOOK\_URL!, { method: "POST" });

  return res.status(200).json({ triggered: true });
}

This filtering layer prevents unnecessary builds. Publishing a metadata-only content type should not trigger a full site rebuild if that content type is not rendered on any page.

## Static site generation with Contentstack

Static site generation (SSG) fetches all content at build time and produces pre-rendered HTML. For a corporate marketing site with relatively stable content, SSG delivers excellent performance and security characteristics.

The build process works like this:

1.  CI/CD pipeline starts (triggered by webhook or code push).
2.  Build script calls Contentstack CDA to fetch all entries needed for the site.
3.  Static pages are generated from the fetched content.
4.  Built artifacts are deployed to the CDN.

// lib/get-all-pages.ts  -  build-time content fetching
import contentstack from "@contentstack/delivery-sdk";

const stack = contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
});

export async function getAllPages() {
  const result = await stack
    .contentType("page")
    .entry()
    .includeReference("components")
    .find();

  return result.entries;
}

// Called at build time in getStaticProps or equivalent

The tradeoff is clear: every content change requires a full rebuild. For a corporate marketing site with 50 pages, rebuilds take seconds. For a site with 10,000 pages, rebuilds can take minutes, and the delay between publish and live visibility becomes a workflow concern.

## Incremental Static Regeneration

Incremental Static Regeneration (ISR) addresses the rebuild-time problem by allowing individual pages to regenerate on demand while the rest of the site serves cached static content.

With ISR, the build produces a baseline set of static pages. When a visitor requests a page after its revalidation window expires, the framework serves the stale version and regenerates the page in the background. The next visitor sees the fresh content.

For a corporate marketing site, ISR eliminates the need for full-site rebuilds on every content publish:

// pages/\[slug\].tsx  -  ISR with Contentstack
import contentstack from "@contentstack/delivery-sdk";

const stack = contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
});

export async function getStaticProps({ params }: { params: { slug: string } }) {
  const result = await stack
    .contentType("page")
    .entry()
    .query()
    .equalTo("url", \`/${params.slug}\`)
    .find();

  return {
    props: { page: result.entries\[0\] || null },
    revalidate: 60, // Regenerate at most every 60 seconds
  };
}

You can also combine ISR with on-demand revalidation triggered by Contentstack webhooks. Instead of waiting for the revalidation timer, the webhook handler explicitly invalidates specific pages when their content is published.

// /api/revalidate.ts  -  on-demand ISR triggered by CMS webhook
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { entry } = req.body;

  if (entry?.url) {
    await res.revalidate(entry.url);
    return res.json({ revalidated: true, path: entry.url });
  }

  return res.status(400).json({ error: "No URL in webhook payload" });
}

This gives you the performance benefits of static generation with the freshness of server-side rendering, and Contentstack publish events drive the cache invalidation.

## Content-as-code: exporting schemas to version control

Content type schemas define the contract between CMS and frontend. When a schema changes, frontend code often needs to adapt. Treating schemas as versionable artifacts keeps these changes traceable.

The pattern is straightforward: export content type definitions from Contentstack and commit them to your repository. This gives you diff visibility, pull request reviews, and the ability to detect schema drift.

\# Add a management-token alias for the source stack
csdx auth:tokens:add \\
  --alias "schema-source" \\
  --stack-api-key "$NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY" \\
  --management \\
  --token "$NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN"

# Export content type schemas from the corporate marketing stack
csdx cm:stacks:export \\
  --alias "schema-source" \\
  --module content-types \\
  --data-dir ./contentstack-schemas

# Commit the exported schemas alongside frontend code
git add contentstack-schemas/
git commit -m "sync: export content type schemas from Contentstack"

In CI, you can add a validation step that compares the current Contentstack schemas against the committed versions and flags drift:

\# .github/workflows/schema-check.yml
name: Schema Drift Check
on: \[pull\_request\]
jobs:
  check-schemas:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Contentstack CLI
        run: npm install -g @contentstack/cli
      - name: Add stack token alias
        run: |
          csdx auth:tokens:add \\
            --alias schema-source \\
            --stack-api-key ${{ secrets.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY }} \\
            --management \\
            --token ${{ secrets.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN }} \\
            --yes
      - name: Export current schemas
        run: |
          csdx cm:stacks:export \\
            --alias schema-source \\
            --module content-types \\
            --data-dir ./current-schemas
      - name: Compare schemas
        run: diff -r ./contentstack-schemas ./current-schemas/content-types

If the diff is non-empty, the schema has changed since the last commit, and the frontend team knows to review.

## Managing environment variables across CI/CD

Contentstack tokens must be managed as CI/CD secrets, not hardcoded values. A corporate marketing site with three Contentstack environments needs distinct token sets in each CI/CD environment:

CI/CD context

NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY

NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN

NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT

Preview deployments

shared

development token

development

Staging branch builds

shared

staging token

staging

Production deploys

shared

production token

production

Store these in your CI/CD platform's secret management (GitHub Secrets, Vercel Environment Variables, GitLab CI Variables). Never commit tokens to the repository.

A subtle failure mode: using the wrong token for the wrong environment because of a copy-paste error in CI configuration. The build succeeds, but the production site serves staging content or vice versa. Guard against this by logging the active environment name (not the token) during builds.

## Blue-green deployments and content synchronization

Blue-green deployment is a release strategy where two identical production environments alternate as the live target. When applied to a headless CMS architecture, content synchronization adds a layer of complexity.

The problem: if "blue" is live and "green" is the next deploy target, content must be available in the Contentstack environment that "green" reads from before the traffic switch. If you publish content after switching traffic, a window opens where the new deployment serves stale or missing content.

Coordination approach:

1.  Publish content to the Contentstack environment mapped to the inactive deployment.
2.  Deploy new code to the inactive deployment.
3.  Verify both code and content are correct on the inactive deployment.
4.  Switch traffic from active to inactive.

This requires that your Contentstack environment strategy supports the blue-green model, potentially with separate environments or with a shared production environment where content is published before the code cutover.

## Rollback considerations

Content rollback and code rollback are different operations with different tools:

*   Code rollback: revert to a previous deployment via your CI/CD platform. Fast, well-understood, usually a single command.
*   Content rollback: revert entries to previous versions in Contentstack. This is entry-level, not environment-level. You unpublish or publish an earlier version of specific entries.

The asymmetry matters. You cannot "roll back production" as a single atomic operation across both code and content. If a broken release involves both a code change and a content type change, the rollback sequence must reverse both, and the order matters:

1.  Roll back the code deployment first (restores the frontend that expects the old schema).
2.  Republish the previous content versions to the production environment.

If you roll back content first while the new code is still live, the new frontend may break on the restored old-format content.

## Common mistakes

### Not triggering rebuilds on content publish

SSG sites that do not have webhook-triggered rebuilds require manual deployments after content changes. Editors publish content and wait indefinitely for it to appear. The fix is simple: configure webhooks to trigger builds for the relevant environment.

> **Common pitfall:**
> 
> A staging CI pipeline using a production delivery token produces builds that silently reflect production content instead of staged content -- defeating the entire purpose of a staging environment.

### Mismatched tokens across CI/CD environments

A staging CI pipeline using a production delivery token produces builds that reflect production content, not staged content. This defeats the purpose of staging. Audit your CI/CD environment variables after initial setup and after any token rotation.

### Ignoring content staging in code release planning

Teams that plan code releases without considering content readiness discover at deploy time that the content their new feature expects does not exist in the target environment. Include "content published to staging" as a checklist item in your release process.

### Webhook handlers without filtering

A webhook that triggers a full rebuild on every publish event, regardless of content type or environment, wastes build minutes and can cause cascading rebuilds. Always filter webhook payloads to the specific events that require action.

## Summary

CMS content changes and code deployments are independent workflows that need explicit coordination points. Webhooks connect Contentstack publish events to CI/CD pipelines. SSG and ISR provide different tradeoffs between build time and content freshness. Schema exports bring content-as-code practices into version control. Environment variable management ensures the right tokens reach the right deployments. And rollback planning must account for the asymmetry between code reverts and content reverts.

The coordination does not require tight coupling. It requires deliberate integration points: a webhook here, an environment variable strategy there, a schema validation step in CI. Each point is small, but together they prevent the class of failures where content and code drift apart silently.

#### Key takeaways

- Connect **Aligning CMS workflows with CI/CD** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

### Lesson 15 — Contentstack CLI and content migration

<!-- ai_metadata: {"lesson_id":"15","type":"text","duration_minutes":1,"topics":["Contentstack","CLI","and","content","migration"]} -->

#### Lesson text

# Contentstack CLI and content migration

> **TL;DR**
> 
> *   Use csdx cm:stacks:export and csdx cm:stacks:import to replicate content model changes across stacks in a scriptable, auditable way.
> *   Always export assets alongside entries to avoid broken references in the target stack, and back up production before importing with \--replace-existing.
> *   Wrap CLI export/import workflows in CI/CD pipelines for consistent, automated content model synchronization across a multi-brand portfolio.

Managing a multi-brand publishing platform through the Contentstack web interface works until it does not. When you operate three brands with separate stacks, each with their own content types, entries, and assets, clicking through the UI to replicate a content model change across all three stacks is slow, error-prone, and impossible to audit. The Contentstack CLI (csdx) turns these operations into repeatable, scriptable commands that belong in your development workflow alongside your code.

This lesson is hands-on. By the end, you should be able to install the CLI, authenticate, export content from one stack, import it into another, and understand how these operations fit into automated pipelines for a multi-brand content operation.

## Installing the Contentstack CLI

The CLI is distributed as an npm package. Install it globally:

npm install -g @contentstack/cli

After installation, verify it works:

csdx --version

The CLI uses a plugin architecture. Core commands cover stack management, content export/import, and authentication. Additional plugins extend functionality for specific use cases. For a multi-brand publishing platform, the core commands handle the majority of migration work.

## Authenticating with the CLI

Before running any stack operations, you need to authenticate. The CLI supports multiple authentication methods:

### Interactive login

csdx auth:login

This opens a browser-based OAuth flow. After authenticating, the CLI stores your session credentials locally. This method works well for local development but is not suitable for CI/CD pipelines.

### Token-based authentication

For automated workflows and CI/CD, add the management token as an alias, then reference that alias in commands:

csdx auth:tokens:add \\
  --alias "source-stack" \\
  --stack-api-key "blt\_matrix\_link\_001" \\
  --management \\
  --token "cs1234567890abcdef" \\
  --yes

csdx cm:stacks:export \\
  --alias "source-stack" \\
  --data-dir ./export-data

Management tokens are created in the Contentstack dashboard under Settings > Tokens. Each token can be scoped to specific permissions, which matters when running migrations across a multi-brand portfolio where different teams own different stacks.

### Listing and managing tokens

\# List stored tokens
csdx auth:tokens

# Add a named token for a specific stack
csdx auth:tokens:add \\
  --alias "brand-alpha-dev" \\
  --stack-api-key "blt\_alpha\_key" \\
  --management --token "cs\_alpha\_mgmt\_token" \\
  --yes

# Remove a stored token
csdx auth:tokens:remove --alias "brand-alpha-dev"

Token aliases simplify repeated operations. For a publishing platform managing Brand Alpha, Brand Beta, and Brand Gamma, you might configure:

csdx auth:tokens:add --alias "alpha-prod" --stack-api-key "$NEXT\_PUBLIC\_CONTENTSTACK\_ALPHA\_API\_KEY" --management --token "$NEXT\_PUBLIC\_CONTENTSTACK\_ALPHA\_MANAGEMENT\_TOKEN" --yes
csdx auth:tokens:add --alias "beta-prod" --stack-api-key "$NEXT\_PUBLIC\_CONTENTSTACK\_BETA\_API\_KEY" --management --token "$NEXT\_PUBLIC\_CONTENTSTACK\_BETA\_MANAGEMENT\_TOKEN" --yes
csdx auth:tokens:add --alias "gamma-prod" --stack-api-key "$NEXT\_PUBLIC\_CONTENTSTACK\_GAMMA\_API\_KEY" --management --token "$NEXT\_PUBLIC\_CONTENTSTACK\_GAMMA\_MANAGEMENT\_TOKEN" --yes

Now every subsequent command can reference \--alias alpha-prod instead of passing raw keys.

## Key CLI commands for content operations

### Seeding a new stack

When launching a new brand in the publishing platform, you often want to start from a known content model rather than building from scratch. The seed command creates a new stack from an existing template:

csdx cm:stacks:seed \\
  --repo "contentstack/stack-starter-app" \\
  --stack-name "Brand Delta - Development"

This clones a predefined content model into a fresh stack. For a multi-brand platform, you might maintain a custom seed repository that contains your standardized product, product line, category, page, and header content types - the shared foundation that every brand starts from. The [Veda kickstart seed](https://github.com/contentstack/kickstart-veda-seed) is an example.

### Exporting content

The export command extracts content from a stack into a local directory structure:

\# Full stack export
csdx cm:stacks:export \\
  --alias "alpha-prod" \\
  --data-dir ./exports/alpha

# Export specific modules only
csdx cm:stacks:export \\
  --alias "alpha-prod" \\
  --module content-types \\
  --data-dir ./exports/alpha-content-types

# Export multiple specific modules
csdx cm:stacks:export \\
  --alias "alpha-prod" \\
  --module content-types \\
  --module global-fields \\
  --module assets \\
  --data-dir ./exports/alpha-schema

The exported directory contains JSON files organized by module:

exports/alpha/
  content-types/
    product.json
    product\_line.json
    category.json
    header.json
  entries/
    product/
      en-us/
        blt\_entry\_1.json
        blt\_entry\_2.json
    product\_line/
      en-us/
        blt\_entry\_3.json
  assets/
    blt\_asset\_1.json
    blt\_asset\_2.json
  global-fields/
    seo\_metadata.json
  environments/
    development.json
    production.json

This file structure is both human-readable and machine-processable. You can inspect it, commit it to version control, modify individual files, and import the result into another stack.

### Importing content

The import command pushes exported content into a target stack:

\# Full import into a different stack
csdx cm:stacks:import \\
  --alias "beta-prod" \\
  --data-dir ./exports/alpha

# Import specific modules only
csdx cm:stacks:import \\
  --alias "beta-prod" \\
  --module content-types \\
  --data-dir ./exports/alpha-content-types

Import respects dependencies: global fields are created before content types that reference them, and content types are created before entries that use them. However, the CLI cannot resolve every dependency automatically, particularly when entries reference other entries across content types that may not exist in the target stack.

## Migrating content between stacks

The primary migration use case for a multi-brand publishing platform is replicating content model changes across brand stacks. When the platform team adds a new seo\_metadata global field to the shared product model, that change needs to propagate to every brand stack.

### Step-by-step migration workflow

1.  Make the change in the source stack (e.g., Add the social\_links global field to Brand Alpha's development stack).
2.  Export the changed modules.
    
    csdx cm:stacks:export \\
      --alias "alpha-dev" \\
      --module content-types \\
      --module global-fields \\
      --data-dir ./migration/social-links-update
    
3.  Review the export: inspect the JSON files to confirm only the intended changes are present. Remove any content type files that were not modified to avoid overwriting unchanged types in the target.
4.  Import into target stacks.
    
    \# Apply to Brand Beta
    csdx cm:stacks:import \\
      --alias "beta-dev" \\
      --module content-types \\
      --module global-fields \\
      --data-dir ./migration/social-links-update
    
    # Apply to Brand Gamma
    csdx cm:stacks:import \\
      --alias "gamma-dev" \\
      --module content-types \\
      --module global-fields \\
      --data-dir ./migration/social-links-update
    
5.  Verify in each target stack: confirm the global field and content type changes appear correctly in the Contentstack dashboard for each brand.
6.  Promote through environments: once verified in development stacks, repeat the import against staging and production stacks.

### Handling conflicts

Import operations can encounter conflicts when the target stack already has a content type with the same UID but a different structure. The CLI provides flags to control behavior:

\# Overwrite existing content types during import
csdx cm:stacks:import \\
  --alias "beta-dev" \\
  --module content-types \\
  --data-dir ./migration/social-links-update \\
  --replace-existing

Without \--replace-existing, the CLI skips content types that already exist. For schema migrations, you typically want to overwrite. For entry imports, the decision depends on whether you want to merge or replace.

## Programmatic migrations using the CMA

For migrations that go beyond simple export/import - renaming fields, transforming data, backfilling values - you need programmatic scripts that use the Content Management API directly.

Consider a scenario where the Veda platform needs to migrate all product entries to include a new description\_word\_count field, calculated from the description text:

// scripts/backfill-description-word-count.ts
// Veda: backfill description\_word\_count across all products

import contentstack from "@contentstack/management";

const client = contentstack.client({
  host: "https://api.contentstack.io",
});

async function backfillDescriptionWordCount(stackApiKey: string, managementToken: string) {
  const stack = client.stack({
    api\_key: stackApiKey,
    management\_token: managementToken,
  });

  // Fetch all product entries
  const products = await stack
    .contentType("product")
    .entry()
    .query({ include\_count: true })
    .find();

  console.log(\`Processing ${products.items.length} products...\`);

  for (const product of products.items) {
    const descText = product.description || product.short\_description || "";
    const wordCount = descText.split(/\\s+/).filter(Boolean).length;

    product.description\_word\_count = wordCount;

    try {
      await product.update();
      console.log(\`Updated "${product.title}" → ${wordCount} words\`);
    } catch (err: unknown) {
      console.error(\`Failed to update "${product.title}": ${(err as Error).message}\`);
    }
  }
}

// Run across all brand stacks
async function main() {
  const brands = \[
    { name: "Alpha", apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ALPHA\_API\_KEY!, token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ALPHA\_MANAGEMENT\_TOKEN! },
    { name: "Beta", apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_BETA\_API\_KEY!, token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_BETA\_MANAGEMENT\_TOKEN! },
    { name: "Gamma", apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_GAMMA\_API\_KEY!, token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_GAMMA\_MANAGEMENT\_TOKEN! },
  \];

  for (const brand of brands) {
    console.log(\`\\n--- Backfilling ${brand.name} ---\`);
    await backfillDescriptionWordCount(brand.apiKey, brand.token);
  }
}

main().catch(console.error);

Run the script with:

npx tsx scripts/backfill-reading-time.ts

Programmatic migrations give you full control over transformation logic, error handling, and execution order. They complement CLI export/import for cases where raw JSON file manipulation is insufficient.

## Using the CLI in CI/CD pipelines

For a multi-brand publishing platform, content model consistency across stacks should not depend on manual CLI runs. Automate it.

\# .github/workflows/content-model-sync.yml
name: Sync Content Model Across Brands
on:
  workflow\_dispatch:
    inputs:
      source\_alias:
        description: "Source stack alias (e.g., alpha-dev)"
        required: true
      modules:
        description: "Modules to sync (e.g., content-types,global-fields)"
        required: true
        default: "content-types,global-fields"

jobs:
  export:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Contentstack CLI
        run: npm install -g @contentstack/cli
      - name: Configure source token
        run: |
          csdx auth:tokens:add \\
            --alias source \\
            --stack-api-key ${{ secrets.SOURCE\_API\_KEY }} \\
            --management --token ${{ secrets.SOURCE\_MGMT\_TOKEN }} \\
            --yes
      - name: Export from source
        run: |
          IFS=',' read -ra MODULES <<< "${{ inputs.modules }}"
          MODULE\_FLAGS=""
          for mod in "${MODULES\[@\]}"; do
            MODULE\_FLAGS="$MODULE\_FLAGS --module $mod"
          done
          csdx cm:stacks:export --alias source $MODULE\_FLAGS --data-dir ./export-data
      - name: Upload export artifact
        uses: actions/upload-artifact@v4
        with:
          name: content-model-export
          path: ./export-data

  import:
    needs: export
    runs-on: ubuntu-latest
    strategy:
      matrix:
        brand: \[beta, gamma\]
    steps:
      - name: Install Contentstack CLI
        run: npm install -g @contentstack/cli
      - name: Download export artifact
        uses: actions/download-artifact@v4
        with:
          name: content-model-export
          path: ./export-data
      - name: Configure target token
        run: |
          csdx auth:tokens:add \\
            --alias target \\
            --stack-api-key ${{ secrets\[format('{0}\_API\_KEY', matrix.brand)\] }} \\
            --management --token ${{ secrets\[format('{0}\_MGMT\_TOKEN', matrix.brand)\] }} \\
            --yes
      - name: Import to target
        run: |
          IFS=',' read -ra MODULES <<< "${{ inputs.modules }}"
          MODULE\_FLAGS=""
          for mod in "${MODULES\[@\]}"; do
            MODULE\_FLAGS="$MODULE\_FLAGS --module $mod"
          done
          csdx cm:stacks:import --alias target $MODULE\_FLAGS --data-dir ./export-data --replace-existing

This workflows exports the content model from one stack and fans out imports across brand stacks in parallel. The workflow\_dispatch trigger lets the platform team run it on demand with specific parameters, and the matrix strategy scales to any number of brands.

## Exercise: export content types from one stack and import into another

Put the concepts together in a practical exercise.

**Prerequisites:** Two Contentstack stacks (or two environments within the same organization) with management tokens configured.

**Steps:**

1.  Install the CLI and authenticate:
    
    npm install -g @contentstack/cli
    csdx auth:tokens:add --alias "source" --stack-api-key "$SOURCE\_KEY" --management --token "$SOURCE\_TOKEN" --yes
    csdx auth:tokens:add --alias "target" --stack-api-key "$TARGET\_KEY" --management --token "$TARGET\_TOKEN" --yes
    
2.  Export content types and global fields from the source stack:
    
    csdx cm:stacks:export --alias "source" --module content-types --module global-fields --data-dir ./exercise-export
    
3.  Inspect the exported files:
    
    ls -la ./exercise-export/content-types/
    # You should see JSON files for each content type
    
4.  Import into the target stack:
    
    csdx cm:stacks:import --alias "target" --module content-types --module global-fields --data-dir ./exercise-export
    
5.  Open the target stack in Contentstack and verify the content types match the source.
6.  Modify a content type in the source stack (add a field), re-export, and re-import with \--replace-existing to practice incremental migration.

**Expected outcome:** The target stack contains identical content types to the source, including any modifications applied in step 6.

## Common mistakes

### Forgetting to export assets

Entries often reference assets (images, documents, videos). Exporting entries without their assets produces an import that contains broken asset references. When migrating entries across stacks, always include the assets module in your export, or confirm that referenced assets already exist in the target stack.

\# Correct: export entries WITH assets
csdx cm:stacks:export \\
  --alias "source" \\
  --module entries \\
  --module assets \\
  --data-dir ./full-migration

### Ignoring reference integrity

Entries reference other entries by UID. When you import entries into a stack that does not contain the referenced entries, those references break silently: the entry imports successfully, but the reference field points to a non-existent UID. Always import referenced content types and their entries before importing the entries that reference them. For Veda where products reference categories and product lines, import categories and product lines first, then products.

> **Common pitfall:**
> 
> Running \--replace-existing imports against a production stack without first exporting a backup leaves you with no rollback path if the migration produces unexpected results.

### Running migrations without a backup

Before running any import with \--replace-existing against a production stack, export the current state first. This gives you a reliable rollback path if the migration produces unexpected results:

\# Backup before migrating
csdx cm:stacks:export --alias "prod" --data-dir ./backup/$(date +%Y%m%d)

# Then run the migration
csdx cm:stacks:import --alias "prod" --data-dir ./migration-data --replace-existing

### Hardcoding stack credentials in scripts

Migration scripts that contain raw API keys and management tokens are security liabilities. Use environment variables or the CLI's token alias system. Never commit credentials to version control, even in "internal" repositories.

### Assuming export/import is atomic

The CLI processes modules sequentially and entries individually. A network interruption mid-import can leave the target stack in a partial state. For large migrations, consider breaking the operation into smaller batches and tracking progress with explicit logging to redirect success/failure statuses for post-migration verification:

csdx cm:stacks:import --alias "target" --data-dir ./migration-data 2>&1 | tee migration-log.txt

## Summary

The Contentstack CLI transforms content operations from manual, click-heavy processes into scriptable, auditable commands. For a multi-brand publishing platform, the CLI is essential: exporting content models from a reference stack, importing them across brand stacks, seeding new brand stacks from templates, and running programmatic migrations for data transformations that go beyond structural changes.

The export/import workflow follows a clear pattern: export modules from the source, inspect and optionally modify the JSON files, import into targets. Wrapping this pattern in CI/CD automation ensures consistency across a growing portfolio of brands without relying on manual coordination. The key operational discipline is treating migrations like database migrations in application development: plan them, test them against non-production stacks, back up before applying to production, and log everything.

#### Key takeaways

- Connect **Contentstack CLI and content migration** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

## Resources & references

| Page | Companion Markdown |
| --- | --- |
| /courses/apis-and-developer-tooling/api-architecture-and-authentication-overview | /academy/md/courses/apis-and-developer-tooling/api-architecture-and-authentication-overview.md |
| /courses/apis-and-developer-tooling/delivery-api-vs-management-api-responsibilities | /academy/md/courses/apis-and-developer-tooling/delivery-api-vs-management-api-responsibilities.md |
| /courses/apis-and-developer-tooling/regions-clouds-and-api-endpoints | /academy/md/courses/apis-and-developer-tooling/regions-clouds-and-api-endpoints.md |
| /courses/apis-and-developer-tooling/rest-vs-graphql-choosing-the-right-query-surface | /academy/md/courses/apis-and-developer-tooling/rest-vs-graphql-choosing-the-right-query-surface.md |
| /courses/apis-and-developer-tooling/authentication-and-access-control-concepts | /academy/md/courses/apis-and-developer-tooling/authentication-and-access-control-concepts.md |
| /courses/apis-and-developer-tooling/rate-limiting-error-codes-and-retry-patterns | /academy/md/courses/apis-and-developer-tooling/rate-limiting-error-codes-and-retry-patterns.md |
| /courses/apis-and-developer-tooling/fetching-and-rendering-content-overview | /academy/md/courses/apis-and-developer-tooling/fetching-and-rendering-content-overview.md |
| /courses/apis-and-developer-tooling/sdk-initialization-and-query-patterns | /academy/md/courses/apis-and-developer-tooling/sdk-initialization-and-query-patterns.md |
| /courses/apis-and-developer-tooling/references-includes-and-localized-content-retrieval | /academy/md/courses/apis-and-developer-tooling/references-includes-and-localized-content-retrieval.md |
| /courses/apis-and-developer-tooling/image-delivery-and-transformation-apis | /academy/md/courses/apis-and-developer-tooling/image-delivery-and-transformation-apis.md |
| /courses/apis-and-developer-tooling/performance-caching-and-frontend-integration | /academy/md/courses/apis-and-developer-tooling/performance-caching-and-frontend-integration.md |
| /courses/apis-and-developer-tooling/environments-and-deployment-overview | /academy/md/courses/apis-and-developer-tooling/environments-and-deployment-overview.md |
| /courses/apis-and-developer-tooling/environments-publishing-and-promotion-strategies | /academy/md/courses/apis-and-developer-tooling/environments-publishing-and-promotion-strategies.md |
| /courses/apis-and-developer-tooling/aligning-cms-workflows-with-ci-cd | /academy/md/courses/apis-and-developer-tooling/aligning-cms-workflows-with-ci-cd.md |
| /courses/apis-and-developer-tooling/contentstack-cli-and-content-migration | /academy/md/courses/apis-and-developer-tooling/contentstack-cli-and-content-migration.md |

## Supplement for indexing

### Content summary

APIs and Developer Tooling Turn Contentstack concepts into implementation muscle memory through API design, SDK usage, rendering patterns, environment strategy, and CLI-driven workflows. Who This Course Is For This cours… APIs and Developer Tooling Turn Contentstack concepts into implementation muscle memory through API design, SDK usage, rendering patterns, environment strategy, and CLI-driven workflows. Who This Course Is For This course is for developers who are ready to write code, call APIs, and connect Contentstack to a real application or deployment pipeline. You Will Be Able To choose the right API surface and credential model for each responsibility initialize SDKs, compose queries, and render entries safely in code align environments, CI/CD, and migrations with your content delivery strategy Recommend

### Retrieval tags

- Contentstack Academy
- apis-and-developer-tooling
- API
- Architecture
- and
- Authentication
- Overview
- Delivery
- management
- responsibilities
- Regions
- clouds
- endpoints
- REST

### Indexing notes

Chunk at each "### Lesson NN — Title" heading; copy lesson_id and topics from the preceding HTML comment into chunk metadata for RAG filters.
Course slug: apis-and-developer-tooling. Union of lesson topic tokens: API, Architecture, and, Authentication, Overview, Delivery, management, responsibilities, Regions, clouds, endpoints, REST, GraphQL, choosing, the, right, query, access, control, concepts, Rate, limiting, error, codes, retry, Fetching, Rendering, Content, SDK, initialization, patterns, References, includes, localized, content, retrieval, Image, delivery, transformation, APIs, Performance, caching, frontend, integration, Environments, Deployment, publishing, promotion, strategies, Aligning, CMS, workflows, with, Contentstack, CLI, migration.
Do not embed or retrieve LMS-only quiz items or mastery exam answer keys from this export.

### Asset references

_No image or video thumbnail URLs were extracted._

### External links

| Label | URL |
| --- | --- |
| Contentstack Academy home | `https://www.contentstack.com/academy/` |
| Training instance setup | `https://www.contentstack.com/academy/training-instance` |
| Academy playground (GitHub) | `https://github.com/contentstack/contentstack-academy-playground` |
| Contentstack documentation | `https://www.contentstack.com/docs/` |
| Delivery API vs Management API responsibilities | `https://contentstack-developer-certification.eu-contentstackapps.com/course-3-apis-and-developer-tooling/module-3-1-api-architecture-and-authentication/01-delivery-api-vs-management-api-responsibilities` |
| official Contentstack regions data | `https://artifacts.contentstack.com/regions.json` |
| @timbenniks/contentstack-endpoints | `https://www.npmjs.com/package/@timbenniks/contentstack-endpoints` |
| TypeScript handbook | `https://www.typescriptlang.org/docs/handbook/` |
| Node.js getting started guide | `https://nodejs.org/en/learn/getting-started/introduction-to-nodejs` |
| kickstart-veda reference application | `https://github.com/contentstack/kickstart-veda/blob/main/lib/types.ts` |
| Veda kickstart seed | `https://github.com/contentstack/kickstart-veda-seed` |
| kickstart-veda reference application | `https://github.com/contentstack/kickstart-veda` |
| https://dev.shop.example.com | `https://dev.shop.example.com/` |
| https://staging.shop.example.com | `https://staging.shop.example.com/` |
| https://shop.example.com | `https://shop.example.com/` |
