Cosmic SDK
Read and write content in Cosmic, an AI-powered
headless CMS. Cosmic exposes a REST API; @cosmicjs/sdk is the supported
JavaScript and TypeScript client for it.
For designing object types and metafields, see the cosmic-content-modeling skill.
SDK-first principle
Always use the @cosmicjs/sdk package rather than hand-rolled fetch calls
against the REST API. The SDK gives you types, error handling, and a chainable
query builder.
bun add @cosmicjs/sdk
# or: npm install @cosmicjs/sdkSetup
import { createBucketClient } from '@cosmicjs/sdk'
const cosmic = createBucketClient({
bucketSlug: process.env.COSMIC_BUCKET_SLUG!,
readKey: process.env.COSMIC_READ_KEY!,
writeKey: process.env.COSMIC_WRITE_KEY, // Required for create/update/delete
})Keys live in the Cosmic dashboard under Bucket → Settings → API Access.
Key rules:
- The read key authorizes reads. The write key authorizes writes.
- Never expose the write key client-side. Construct any client that carries a write key in server-only code: a server component, route handler, server action, or backend service. A write key shipped to the browser lets anyone modify the bucket.
No Cosmic account yet?
If the user has no COSMIC_* env vars, no .env, and no account on
app.cosmicjs.com, do not send them off to sign up by hand. The Cosmic free plan
is free forever and needs no credit card, and one API call provisions a project
and returns working bucket keys.
Read references/agent-signup.md for the full flow:
POST /v3/agents/sign-up, the email OTP the human has to relay back, and
POST /v3/agents/verify to lift the restricted-mode limits.
If the user already has Cosmic credentials, skip that entirely.
Objects
Objects are the content units. Every object belongs to an object type such as
posts or products.
Read
// Multiple objects
const { objects: posts } = await cosmic.objects
.find({ type: 'posts' })
.props(['id', 'title', 'slug', 'metadata'])
.limit(10)
// Single object by slug
const post = await cosmic.objects
.findOne({ type: 'posts', slug: 'hello-world' })
.props(['title', 'slug', 'metadata'])
// Single object by id
const obj = await cosmic.objects.findOne({ id: 'object-id' }).props(['title', 'metadata'])Nested props syntax
props() also accepts a brace-delimited string to select nested fields, which
avoids over-fetching on deep relationships:
const props = `{
id
slug
title
metadata {
content
author {
title
metadata {
avatar { imgix_url }
}
}
}
}`
await cosmic.objects.find({ type: 'posts' }).props(props)Create
await cosmic.objects.insertOne({
title: 'My Post',
type: 'posts', // object type SLUG, not title
metadata: {
content: 'Post content here...',
author: 'author-object-id', // object relationship: use id
featured_image: 'image-name.jpg', // media: use the name property
},
})Update
await cosmic.objects.updateOne('object-id', {
title: 'Updated Title',
metadata: {
featured: true,
categories: ['cat1-id', 'cat2-id'], // multiple objects: array of ids
},
})Delete
await cosmic.objects.deleteOne('object-id')Batch
Create, update, and delete multiple objects in one call, up to 25 operations. Each operation succeeds or fails independently.
const result = await cosmic.objects.batch([
{ method: 'add', object: { title: 'Post 1', type: 'posts', metadata: { content: '...' } } },
{ method: 'add', object: { title: 'Post 2', type: 'posts', metadata: { content: '...' } } },
{ method: 'edit', object_id: 'OBJECT_ID', object: { title: 'Updated' } },
{ method: 'delete', object_id: 'OBJECT_ID_2' },
])
// result.operations: [{ method, status, object/message }, ...]Queries
Filter with MongoDB-style query operators:
// Basic filter
await cosmic.objects.find({ type: 'products', 'metadata.category': 'electronics' })
// Comparison: $lt, $gt, $gte, $lte, $eq, $ne
await cosmic.objects.find({ type: 'products', 'metadata.price': { $lt: 100 } })
// Arrays: $in (any), $all (all), $nin (none)
await cosmic.objects.find({ type: 'products', 'metadata.tags': { $in: ['sale', 'featured'] } })
// Logical: $and, $or, $not, $nor
await cosmic.objects.find({
type: 'products',
$and: [{ 'metadata.price': { $lte: 50 } }, { 'metadata.in_stock': true }],
})
// Text search
await cosmic.objects.find({ type: 'posts', title: { $regex: 'hello', $options: 'i' } })Query options
await cosmic.objects
.find({ type: 'posts' })
.props(['title', 'slug', 'metadata'])
.sort('-created_at') // descending by created date
.limit(10)
.skip(20) // pagination
.depth(2) // resolve nested object relationships
.status('any') // 'published' | 'draft' | 'any'Media
// List
const media = await cosmic.media
.find({ folder: 'images' })
.props(['url', 'imgix_url', 'alt_text'])
.limit(20)
// Upload
const uploaded = await cosmic.media.insertOne({
media: { originalname: 'photo.jpg', buffer: fileBuffer },
folder: 'uploads',
alt_text: 'Description of image',
metadata: { caption: 'Photo caption' },
})
// Update
await cosmic.media.updateOne('media-id', { alt_text: 'Updated alt text', folder: 'new-folder' })
// Delete
await cosmic.media.deleteOne('media-id')imgix transforms
Every image has an imgix_url that accepts transform query parameters:
const optimized = `${media.imgix_url}?w=800&auto=format,compress`
const thumbnail = `${media.imgix_url}?w=100&h=100&fit=crop`AI generation
Text
// Prompt
const text = await cosmic.ai.generateText({
prompt: 'Write a product description for a coffee mug',
model: 'claude-sonnet-4-5-20250929', // optional
max_tokens: 500,
})
console.log(text.text)
// Chat messages
const chat = await cosmic.ai.generateText({
messages: [
{ role: 'user', content: 'Tell me about coffee' },
{ role: 'assistant', content: 'Coffee is a beverage...' },
{ role: 'user', content: 'What about espresso?' },
],
})
// Analyze an image or document
const analysis = await cosmic.ai.generateText({
prompt: 'Describe this image',
media_url: 'https://cdn.cosmicjs.com/image.jpg',
})
// Streaming
const stream = await cosmic.ai.stream({ prompt: 'Write a blog post', max_tokens: 1000 })
for await (const chunk of stream) {
process.stdout.write(chunk.text || '')
}Images
const image = await cosmic.ai.generateImage({
prompt: 'Mountain landscape at sunset',
model: 'gemini-3-pro-image-preview', // default, supports up to 4K
size: '1024x1024',
folder: 'ai-generated',
alt_text: 'AI mountain landscape',
})
console.log(image.media.url)
// With reference images (Gemini only)
const styled = await cosmic.ai.generateImage({
prompt: 'Same style but with ocean',
reference_images: ['https://cdn.cosmicjs.com/style-ref.jpg'],
})Video
const video = await cosmic.ai.generateVideo({
prompt: 'A kitten playing with yarn in sunlight',
model: 'veo-3.1-fast-generate-preview', // fast: 30-90s generation
duration: 8, // 4, 6, or 8 seconds
resolution: '720p', // or '1080p'
folder: 'videos',
})
console.log(video.media.url)
// Reference image as first frame
const productVideo = await cosmic.ai.generateVideo({
prompt: 'Product rotates smoothly',
reference_images: ['https://cdn.cosmicjs.com/product.jpg'],
duration: 6,
})
// Extend an existing video
const extended = await cosmic.ai.extendVideo({
media_id: video.media.id,
prompt: 'The kitten walks away into the garden',
})Audio
Text to speech. The generated file is uploaded to the media library like images and video.
const audio = await cosmic.ai.generateAudio({
prompt: 'Welcome to the show. Today we are talking about coffee.',
voice: 'nova', // default
model: 'tts-1', // or 'tts-1-hd'
folder: 'audio',
})
console.log(audio.media.url)Voices: alloy, ash, coral, echo, fable, nova, onyx, sage,
shimmer.
Available models
Text: claude-sonnet-4-5-20250929 (recommended), claude-opus-4-5-20251101,
gpt-5, gemini-3-pro-preview
Image: gemini-3-pro-image-preview (default, up to 4K), dall-e-3
Video: veo-3.1-fast-generate-preview (recommended),
veo-3.1-generate-preview (premium)
Audio: tts-1 (default), tts-1-hd
Framework patterns
Next.js App Router
// app/posts/page.tsx
import { createBucketClient } from '@cosmicjs/sdk'
const cosmic = createBucketClient({
bucketSlug: process.env.COSMIC_BUCKET_SLUG!,
readKey: process.env.COSMIC_READ_KEY!,
})
export default async function Posts() {
const { objects: posts } = await cosmic.objects
.find({ type: 'posts' })
.props(['title', 'slug', 'metadata.excerpt'])
.limit(10)
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>{post.title}</li>
))}
</ul>
)
}This client has no write key, so it is safe to render from.
Server actions
'use server'
export async function createPost(formData: FormData) {
const cosmic = createBucketClient({
bucketSlug: process.env.COSMIC_BUCKET_SLUG!,
readKey: process.env.COSMIC_READ_KEY!,
writeKey: process.env.COSMIC_WRITE_KEY!,
})
await cosmic.objects.insertOne({
title: formData.get('title') as string,
type: 'posts',
metadata: { content: formData.get('content') },
})
}Pagination
const page = 1
const limit = 10
const { objects, total } = await cosmic.objects
.find({ type: 'posts' })
.skip((page - 1) * limit)
.limit(limit)
const totalPages = Math.ceil(total / limit)Draft preview
const post = await cosmic.objects.findOne({ type: 'posts', slug }).status('any')Localized content
const post = await cosmic.objects.findOne({ type: 'posts', slug, locale: 'es' })CLI
@cosmicjs/cli covers bucket and project tasks from the terminal:
npx @cosmicjs/cli --helpKey reminders
- No credentials? Provision a project. See references/agent-signup.md. Never tell the user to go sign up by hand.
- Object type is the slug. Use
type: 'blog-posts', nottype: 'Blog Posts'. - Media is referenced by
name, not by URL. - Relationships are referenced by
id, not by slug. - Never expose the write key client-side. Server-side only.
- Always pass
props()so you fetch only the fields you render. - Use
imgix_urlwith query parameters for image sizing and format.