Skip to content

CMS API

Manage structured content with collections and entries. Arky’s CMS uses a flexible blocks-based system for dynamic content modeling.

Before using the raw API, see the CMS Content Integration Guide for complete patterns:

  • SSR Content Fetching: Server-side initialization with API keys
  • Multilingual Content: Locale-based fetching with fallbacks
  • Block Value Extraction: Use SDK utilities for clean value parsing
  • Media CDN URLs: Build optimized image URLs from storage paths
import { createArkySDK } from 'arky-sdk';
// Server-side init (Astro, Next.js, etc.)
const sdk = createArkySDK({
baseUrl: 'https://api.arky.io',
businessId: process.env.ARKY_BUSINESS_ID,
autoGuest: false,
getToken: () => ({
accessToken: process.env.ARKY_API_KEY,
provider: 'API',
expiresAt: 0,
}),
setToken: () => {},
logout: () => {},
});
// Fetch collection
const collection = await sdk.cms.getCollection({ id: 'blog' });
// Fetch entries
const { items: posts } = await sdk.cms.getCollectionEntries({
collectionId: 'blog',
limit: 20,
});
// Extract block values
posts.forEach(post => {
const title = sdk.utils.getBlockTextValue(
post.blocks.find(b => b.key === 'title'),
'en' // locale
);
const imageBlock = post.blocks.find(b => b.key === 'featured_image');
const imageUrl = sdk.utils.getImageUrl(imageBlock);
console.log({ title, imageUrl });
});

See CMS Integration Guide for full patterns.


All CMS endpoints are prefixed with /v1/businesses/{businessId}.

Requires Authorization: Bearer <access_token> header with appropriate permissions.


Collections are content types (like “Blog Posts”, “Products”, “Pages”). They define the structure and blocks schema.

Endpoint: POST /v1/businesses/{businessId}/collections

const collection = await sdk.cms.createCollection({
name: 'Blog Posts',
slug: 'blog-posts',
description: 'Articles and blog content',
blocks: [
{
id: 'title',
type: 'TEXT',
label: 'Title',
required: true,
settings: { maxLength: 200 },
},
{
id: 'content',
type: 'TEXT',
label: 'Content',
required: true,
settings: { variant: 'HTML' },
},
{
id: 'author',
type: 'RELATIONSHIP',
label: 'Author',
settings: { targetType: 'USER' },
},
{
id: 'published',
type: 'BOOLEAN',
label: 'Published',
},
],
statuses: [
{ id: 'draft', status: 'DRAFT' },
{ id: 'published', status: 'PUBLISHED' },
],
});

Response:

{
"id": "coll_abc123",
"businessId": "biz_123",
"name": "Blog Posts",
"slug": "blog-posts",
"description": "Articles and blog content",
"blocks": [...],
"statuses": [
{
"id": "draft",
"status": "DRAFT",
"changedBy": "SYSTEM",
"timestamp": 1698765432
}
],
"createdAt": 1698765432,
"updatedAt": 1698765432
}

List all collections.

Endpoint: GET /v1/businesses/{businessId}/collections

const { items, cursor } = await sdk.cms.getCollections({
limit: 20,
cursor: null,
});

Fetch a specific collection by ID.

Endpoint: GET /v1/businesses/{businessId}/collections/{id}

const collection = await sdk.cms.getCollection({
id: 'coll_abc123',
});

Endpoint: PUT /v1/businesses/{businessId}/collections/{id}

const updated = await sdk.cms.updateCollection({
id: 'coll_abc123',
name: 'Updated Blog Posts',
blocks: [...], // Updated schema
});

Endpoint: DELETE /v1/businesses/{businessId}/collections/{id}

await sdk.cms.deleteCollection({
id: 'coll_abc123',
});

Entries are individual content items (like a specific blog post) that belong to a collection.

Entries use an owner field to specify their parent:

  • collection:{collectionId} - Entry belongs to a collection
  • order:{orderId} - Entry belongs to an order
  • reservation:{reservationId} - Entry belongs to a reservation

The SDK automatically converts collectionId to owner: "collection:{collectionId}".


Endpoint: POST /v1/businesses/{businessId}/entries

const entry = await sdk.cms.createCollectionEntry({
collectionId: 'coll_abc123', // Converted to owner: "collection:coll_abc123"
locale: 'en',
blocks: [
{
id: 'title',
type: 'TEXT',
value: 'My First Blog Post',
},
{
id: 'content',
type: 'TEXT',
value: { en: '<p>This is the content...</p>' },
},
{
id: 'author',
type: 'RELATIONSHIP',
value: 'user_xyz',
},
{
id: 'published',
type: 'BOOLEAN',
value: true,
},
],
statuses: [
{
id: 'status_1',
status: 'PUBLISHED',
changedBy: 'USER',
userId: 'user_xyz',
timestamp: Date.now() / 1000,
},
],
});

List entries (optionally filtered by collection).

Endpoint: GET /v1/businesses/{businessId}/entries

const { items, cursor } = await sdk.cms.getCollectionEntries({
collectionId: 'coll_abc123', // Optional: filter by collection
limit: 20,
cursor: null,
});
// Or get all entries across collections:
const allEntries = await sdk.cms.getCollectionEntries({
limit: 50,
});

Fetch a specific entry by ID.

Endpoint: GET /v1/businesses/{businessId}/entries/{id}

const entry = await sdk.cms.getCollectionEntry({
id: 'entry_abc123',
});
console.log('Blocks:', entry.blocks);
console.log('Owner:', entry.owner); // e.g., "collection:coll_abc123"

Endpoint: PUT /v1/businesses/{businessId}/entries/{id}

const updated = await sdk.cms.updateCollectionEntry({
id: 'entry_abc123',
collectionId: 'coll_abc123',
locale: 'en',
blocks: [
{
id: 'title',
type: 'TEXT',
value: 'Updated Title',
},
// ... other blocks
],
statuses: [...],
});

Endpoint: DELETE /v1/businesses/{businessId}/entries/{id}

await sdk.cms.deleteCollectionEntry({
id: 'entry_abc123',
});

Generate block content using AI.

Endpoint: POST /v1/businesses/{businessId}/collections/blocks/generate

const generated = await sdk.cms.generateBlocks({
prompt: 'Write a blog post about TypeScript best practices',
blocks: [
{ id: 'title', type: 'TEXT' },
{ id: 'content', type: 'TEXT', settings: { variant: 'HTML' } },
{ id: 'tags', type: 'TEXT' },
],
});
console.log('Generated blocks:', generated.blocks);
// Use in createCollectionEntry

Response:

{
"blocks": [
{
"id": "title",
"type": "TEXT",
"value": "10 TypeScript Best Practices for 2024"
},
{
"id": "content",
"type": "TEXT",
"value": { "en": "<p>TypeScript has become...</p>" }
}
]
}

Newsletters are Collections with type: 'NEWSLETTER'. They support subscriber management, paid plans, and email sending.

Create a collection with newsletter-specific settings:

const newsletter = await sdk.cms.createCollection({
name: 'Weekly Tech Digest',
slug: 'weekly-tech',
type: 'NEWSLETTER',
description: 'Weekly technology news and insights',
plans: [
{
id: 'plan_free',
name: 'Free Edition',
description: 'Basic weekly digest',
price: {
amount: 0,
currency: 'usd',
interval: 'MONTHLY',
},
features: ['Weekly newsletter', 'Archive access'],
},
{
id: 'plan_premium',
name: 'Premium',
description: 'Full access with exclusive content',
price: {
amount: 999, // $9.99
currency: 'usd',
interval: 'MONTHLY',
},
features: ['Daily newsletter', 'Exclusive content', 'Archive access', 'Ad-free'],
},
],
blocks: [
{
id: 'subject',
type: 'TEXT',
label: 'Email Subject',
required: true,
},
{
id: 'content',
type: 'TEXT',
label: 'Content',
required: true,
settings: { variant: 'HTML' },
},
],
});

Subscribe a user to a newsletter with a specific plan.

Endpoint: POST /v1/businesses/{businessId}/collections/{collectionId}/subscribe

// Free newsletter subscription
const subscription = await sdk.cms.subscribeToCollection({
collectionId: 'coll_newsletter_123',
planId: 'plan_free', // Required - must exist in collection.plans
});
// Paid newsletter subscription (returns Stripe checkout URL)
const { checkoutUrl } = await sdk.cms.subscribeToCollection({
collectionId: 'coll_newsletter_123',
planId: 'plan_premium',
});
// Redirect user to checkoutUrl to complete payment
window.location.href = checkoutUrl;

Request Body:

  • id (string, required): Collection ID
  • email (string, required): Subscriber email
  • market (string, required): Market code (e.g., “us”, “uk”)
  • plan_id (string, required): Plan ID from collection.plans

Response (Free Plan):

{
"id": "sub_abc123",
"entity": "collection:coll_newsletter_123",
"email": "[email protected]",
"planId": "plan_free",
"status": "ACTIVE",
"createdAt": 1698765432
}

Response (Paid Plan):

{
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_..."
}

List all subscribers to a newsletter.

Endpoint: GET /v1/businesses/{businessId}/collections/{id}/subscribers

const { items, cursor } = await sdk.cms.getCollectionSubscribers({
id: 'coll_newsletter_123',
limit: 50,
cursor: null,
});
items.forEach(sub => {
console.log(`${sub.email} - Plan: ${sub.planId} - Status: ${sub.status}`);
});

Response:

{
"items": [
{
"id": "sub_abc123",
"entity": "collection:coll_newsletter_123",
"email": "[email protected]",
"planId": "plan_free",
"status": "ACTIVE",
"createdAt": 1698765432,
"updatedAt": 1698765432
}
],
"cursor": "next_page_token"
}

Unsubscribe using a token (typically sent in newsletter emails).

Endpoint: POST /v1/businesses/{businessId}/collections/unsubscribe

await sdk.cms.unsubscribeFromCollection({
token: 'unsubscribe_token_from_email',
});

Send an entry as a newsletter email to all subscribers.

Endpoint: POST /v1/businesses/{businessId}/entries/{entryId}/send

// Send immediately
await sdk.cms.sendEntry({
entryId: 'entry_abc123',
});
// Schedule for later
await sdk.cms.sendEntry({
entryId: 'entry_abc123',
scheduledAt: Math.floor(Date.now() / 1000) + 3600, // Send in 1 hour
});

Note: Entry must belong to a newsletter collection with owner: "collection:{newsletterId}".


Get available variables for entry templates.

Endpoint: GET /v1/collections/entry-types/{entryType}/variables

const metadata = await sdk.cms.getVariableMetadata({
entryType: 'NEWSLETTER',
});
console.log('Available variables:', metadata.variables);
// e.g., {{ subscriber.name }}, {{ unsubscribe_url }}

Common block types available:

  • TEXT: Text content (use variant: "HTML" for rich content)
  • NUMBER: Numeric values
  • BOOLEAN: True/false
  • DATE: Timestamp
  • RELATIONSHIP: Reference to another entity (user, product, etc.)
  • GEO_LOCATION: Latitude/longitude
  • MEDIA: Image/video references
  • SELECT: Single selection from options
  • MULTI_SELECT: Multiple selections

See Blocks Guide for detailed documentation.