# Create author Source: https://docs.marblecms.com/api-reference/authors/create-author https://api.marblecms.com/openapi.json post /v1/authors Create a new author. Requires a private API key. Plan limits apply. # Delete author Source: https://docs.marblecms.com/api-reference/authors/delete-author https://api.marblecms.com/openapi.json delete /v1/authors/{identifier} Delete an author by ID or slug. Requires a private API key. # Get author Source: https://docs.marblecms.com/api-reference/authors/get-author https://api.marblecms.com/openapi.json get /v1/authors/{identifier} Get a single author by ID or slug # List authors Source: https://docs.marblecms.com/api-reference/authors/list-authors https://api.marblecms.com/openapi.json get /v1/authors Get a paginated list of authors who have published posts # Update author Source: https://docs.marblecms.com/api-reference/authors/update-author https://api.marblecms.com/openapi.json patch /v1/authors/{identifier} Update an existing author by ID or slug. Requires a private API key. # Create category Source: https://docs.marblecms.com/api-reference/categories/create-category https://api.marblecms.com/openapi.json post /v1/categories Create a new category. Requires a private API key. # Delete category Source: https://docs.marblecms.com/api-reference/categories/delete-category https://api.marblecms.com/openapi.json delete /v1/categories/{identifier} Delete a category by ID or slug. Requires a private API key. Cannot delete a category that has posts assigned to it. # Get category Source: https://docs.marblecms.com/api-reference/categories/get-category https://api.marblecms.com/openapi.json get /v1/categories/{identifier} Get a single category by ID or slug # List categories Source: https://docs.marblecms.com/api-reference/categories/list-categories https://api.marblecms.com/openapi.json get /v1/categories Get a paginated list of categories # Update category Source: https://docs.marblecms.com/api-reference/categories/update-category https://api.marblecms.com/openapi.json patch /v1/categories/{identifier} Update an existing category by ID or slug. Requires a private API key. # Delete media asset Source: https://docs.marblecms.com/api-reference/media/delete-media-asset https://api.marblecms.com/openapi.json delete /v1/media/{id} Delete a media asset and its R2 object. Requires a private API key. # Get media asset Source: https://docs.marblecms.com/api-reference/media/get-media-asset https://api.marblecms.com/openapi.json get /v1/media/{id} Retrieve a single media asset by ID. # List media assets Source: https://docs.marblecms.com/api-reference/media/list-media-assets https://api.marblecms.com/openapi.json get /v1/media Retrieve media assets for the authenticated workspace. # Update media asset Source: https://docs.marblecms.com/api-reference/media/update-media-asset https://api.marblecms.com/openapi.json patch /v1/media/{id} Update media asset metadata. Requires a private API key. # Upload media asset Source: https://docs.marblecms.com/api-reference/media/upload-media-asset https://api.marblecms.com/openapi.json post /v1/media/upload Upload a media file and create a media asset. Requires a private API key. Maximum file size is 5 MiB. # Create post Source: https://docs.marblecms.com/api-reference/posts/create-post https://api.marblecms.com/openapi.json post /v1/posts Create a new post. Requires a private API key. Category is required. If authors are not provided, the first workspace author is used. # Delete post Source: https://docs.marblecms.com/api-reference/posts/delete-post https://api.marblecms.com/openapi.json delete /v1/posts/{identifier} Delete a post by ID or slug. Requires a private API key. # Get post Source: https://docs.marblecms.com/api-reference/posts/get-post https://api.marblecms.com/openapi.json get /v1/posts/{identifier} Get a single post by ID or slug, with optional status filtering # List posts Source: https://docs.marblecms.com/api-reference/posts/list-posts https://api.marblecms.com/openapi.json get /v1/posts Get a paginated list of published posts with optional filtering # Update post Source: https://docs.marblecms.com/api-reference/posts/update-post https://api.marblecms.com/openapi.json patch /v1/posts/{identifier} Update an existing post by ID or slug. All fields are optional — only provided fields are updated. Requires a private API key. # Create tag Source: https://docs.marblecms.com/api-reference/tags/create-tag https://api.marblecms.com/openapi.json post /v1/tags Create a new tag. Requires a private API key. # Delete tag Source: https://docs.marblecms.com/api-reference/tags/delete-tag https://api.marblecms.com/openapi.json delete /v1/tags/{identifier} Delete a tag by ID or slug. Requires a private API key. # Get tag Source: https://docs.marblecms.com/api-reference/tags/get-tag https://api.marblecms.com/openapi.json get /v1/tags/{identifier} Get a single tag by ID or slug # List tags Source: https://docs.marblecms.com/api-reference/tags/list-tags https://api.marblecms.com/openapi.json get /v1/tags Get a paginated list of tags # Update tag Source: https://docs.marblecms.com/api-reference/tags/update-tag https://api.marblecms.com/openapi.json patch /v1/tags/{identifier} Update an existing tag by ID or slug. Requires a private API key. # Filtering Source: https://docs.marblecms.com/api/filtering Filter Marble posts by categories, tags, featured status, and search queries with include and exclude semantics, SDK examples, and precedence rules. Marble API provides powerful filtering options to retrieve exactly the content you need. Filters are applied using query parameters and can be combined for precise content selection. ## How Filtering Works When you request a list of posts, you can include or exclude content based on: * **Categories** - Filter by the post's category (a post belongs to one category) * **Tags** - Filter by the post's tags (a post can have multiple tags) * **Featured status** - Filter by whether a post is featured * **Search queries** - Search for matches in title and content All filters are combined using **AND logic**. When multiple filters are applied, posts must match **all** conditions to be included in the results. ## Filter Parameters Comma-separated list of category slugs. Posts must belong to **one of** the specified categories. **Example:** `tech,news` Comma-separated list of category slugs to exclude. Posts in these categories will be omitted from results. **Example:** `changelog,legal` Comma-separated list of tag slugs. Posts must have **at least one** of the specified tags. **Example:** `javascript,react` Comma-separated list of tag slugs to exclude. Posts with **any** of these tags will be omitted. **Example:** `outdated,draft` Search term to filter by title and content. **Example:** `nextjs` Sort order by `publishedAt`. Options: `asc` or `desc` Filter by featured status. Options: `true` or `false` ## SDK Examples Get all posts in the "tutorials" category: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); const result = await marble.posts.list({ categories: ["tutorials"], }); for await (const page of result) { console.log(page.posts); } ``` Get all posts except those in "changelog" or "legal": ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); const result = await marble.posts.list({ excludeCategories: ["changelog", "legal"], }); for await (const page of result) { console.log(page.posts); } ``` Get posts tagged with "javascript" or "typescript": ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); const result = await marble.posts.list({ tags: ["javascript", "typescript"], }); for await (const page of result) { console.log(page.posts); } ``` Get posts in "tutorials" but exclude outdated content: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); const result = await marble.posts.list({ categories: ["tutorials"], excludeTags: ["outdated", "deprecated"], }); for await (const page of result) { console.log(page.posts); } ``` Get only featured posts: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); const result = await marble.posts.list({ featured: "true", }); for await (const page of result) { console.log(page.posts); } ``` ## Filter Behavior **AND Logic**: All specified filters are applied together. A post must satisfy every condition to appear in results. ### Understanding Filter Precedence When using both include and exclude filters, they work together—not against each other: | Filter Combination | Result | | -------------------------------------- | ------------------------------------------ | | `categories=tech` | Posts in "tech" category | | `excludeTags=outdated` | Posts without "outdated" tag | | `categories=tech&excludeTags=outdated` | Posts in "tech" AND without "outdated" tag | **Important**: If a post is in the "tech" category but has the "outdated" tag, it will be **excluded** when using `categories=tech&excludeTags=outdated`. Exclude filters take effect regardless of category matches. ### Include vs Exclude Logic Posts must belong to **one of** the specified categories: ``` ?categories=tech,news ``` Returns posts in "tech" **OR** "news" categories. Posts must **not** belong to any excluded category: ``` ?excludeCategories=legal,changelog ``` Returns all posts **except** those in "legal" or "changelog". Posts must have **at least one** of the specified tags: ``` ?tags=react,nextjs ``` Returns posts tagged with "react" **OR** "nextjs" (or both). Posts must have **none** of the excluded tags: ``` ?excludeTags=outdated ``` Returns posts that do **not** have the "outdated" tag. ## Search Queries Use the `query` parameter to search for matches in post titles and content: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); const result = await marble.posts.list({ query: "getting started", }); for await (const page of result) { console.log(page.posts); } ``` Combine search with category filters for targeted results: `?categories=tutorials&query=authentication` ## Common Use Cases Exclude system pages like legal and changelog: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} await marble.posts.list({ excludeCategories: ["legal", "changelog"], }); ``` Get only featured posts: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} await marble.posts.list({ featured: "true", }); ``` Exclude outdated posts: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} await marble.posts.list({ excludeTags: ["outdated", "archived"], }); ``` Category + search combo: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} await marble.posts.list({ categories: ["tutorials"], query: "react hooks", }); ``` ## Best Practices **Use excludes for "everything except"**: When you want all content except a few categories, use `excludeCategories` instead of listing every category you want. **Combine with pagination**: Filtering works seamlessly with pagination. Apply filters first, then paginate through the filtered results. **Filter order doesn't matter**: The API applies all filters together regardless of the order you specify them in the query string. **Watch your rate limits**: Each search request counts against your [rate limit](/api/rate-limits). For real-time "search as you type" experiences, fetch your posts once and filter them client-side instead of making a new API request on every keystroke. # Marble API Reference Source: https://docs.marblecms.com/api/introduction Overview of the Marble REST API: base URL, API key authentication, scope-based permissions, JSON response format, and HTTP error codes. The Marble API is organized around REST. Our API has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes and verbs. ## Base URL All API access is over HTTPS and is accessed from the `api.marblecms.com` domain. All data is sent and received as JSON. ``` https://api.marblecms.com/v1/:resource ``` Where `:resource` is the specific resource you want to interact with (e.g., `posts`, `categories`, `tags`, `authors`). ## Authentication Marble authenticates API requests using API keys. You can create and manage API keys from your workspace dashboard under **Settings > API Keys**. Include your API key in the `Authorization` header of every request: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -H "Authorization: YOUR_API_KEY" https://api.marblecms.com/v1/posts ``` Alternatively, you can pass the API key as a query parameter: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl "https://api.marblecms.com/v1/posts?key=YOUR_API_KEY" ``` ### API key types and scopes Every API key carries a set of **scopes** that determine which resources and operations it can access. The API checks the key's scopes on every request and returns `403 Forbidden` if a required scope is missing. There are two key types: * **Public keys** (`mpk_…`) can only hold read scopes. They are safe to use from environments where the key may be observed, such as build pipelines for static sites. * **Private keys** (`msk_…`) can hold any scope, including write scopes and `posts_read_drafts`. Use them only from trusted server-side environments. The full list of scopes: | Scope | Key types | Grants | | ------------------- | --------------- | ------------------------------------------------ | | `posts_read` | public, private | Read published posts | | `posts_read_drafts` | private | Read drafts via `?status=draft` or `?status=all` | | `posts_write` | private | Create, update, and delete posts | | `authors_read` | public, private | Read authors | | `authors_write` | private | Create, update, and delete authors | | `categories_read` | public, private | Read categories | | `categories_write` | private | Create, update, and delete categories | | `tags_read` | public, private | Read tags | | `tags_write` | private | Create, update, and delete tags | | `media_read` | public, private | Read media assets | | `media_write` | private | Upload, update, and delete media assets | | `fields_read` | public, private | Read custom fields | | `fields_write` | private | Create, update, and delete custom fields | Public API keys are scoped to read operations only, but they should still be handled with care. Exposing your key in client-side code allows anyone to make requests on your behalf, which can lead to your [rate limits](/api/rate-limits) being exhausted. We recommend using keys on the server-side whenever possible. **Best Practice:** Use the `Authorization` header for all requests. Query parameters may be logged in server access logs. ### Reading drafts By default, `GET /v1/posts` returns only published posts. To include drafts you must: 1. Use a **private** API key, and 2. Grant it the `posts_read_drafts` scope, and 3. Pass `?status=draft` (drafts only) or `?status=all` (drafts + published). ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -H "Authorization: msk_…" \ "https://api.marblecms.com/v1/posts?status=draft" ``` Requests for drafts using a public key, or a private key without `posts_read_drafts`, return `403 Forbidden`. ## Response Format All responses are returned as JSON. Successful responses include the requested data, while errors include an `error` field with details. All timestamps are in UTC ISO 8601 format: `YYYY-MM-DDTHH:MM:SSZ` ## Error Handling The API uses conventional HTTP response codes to indicate the success or failure of a request: | Code | Description | | ----- | ----------------------------------------------- | | `200` | Success | | `400` | Bad Request - Invalid parameters | | `401` | Unauthorized - Invalid or missing API key | | `403` | Forbidden - API key is missing a required scope | | `404` | Not Found - Resource doesn't exist | | `429` | Too Many Requests - Rate limit exceeded | | `500` | Internal Server Error | Example error response: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "error": "Unauthorized", "message": "API key required. Provide via Authorization header or ?key= query parameter" } ``` When the key is valid but lacks the scope required for the endpoint, the API returns `403` with a message naming the missing scope: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "error": "Forbidden", "message": "API key missing required scope: posts_write" } ``` To resolve this, edit the key in **Settings > API Keys** and grant the missing scope, or use a different key that already has it. Remember that public keys cannot be granted write scopes or `posts_read_drafts`. # Pagination Source: https://docs.marblecms.com/api/pagination Paginate list endpoints in the Marble API with the limit and page query parameters, response metadata, async iteration, and error handling examples. Marble API uses offset-based pagination for endpoints that return multiple items. Pagination metadata is included in every response to help you navigate through large datasets efficiently. ## How Pagination Works When you request a list of resources (like posts), the API automatically includes pagination information in the response. This allows you to: * Control how many items are returned per request * Navigate to specific pages * Understand the total size of the dataset * Build pagination UI components ## Query Parameters The maximum number of items to return per page. **Range:** 1-200 items per page The page number to retrieve. Pages start at 1. **Minimum:** 1 ## Pagination Response Every paginated response includes a `pagination` object with the following fields: Metadata about the current page and navigation options. The number of items requested per page (matches your query parameter). The current page number being returned. The next page number, or `null` if you're on the last page. The previous page number, or `null` if you're on the first page. The total number of pages available. The total number of items across all pages. ## Request Examples Get the first page with default limit (10 items): ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -H "Authorization: YOUR_API_KEY" \ "https://api.marblecms.com/v1/posts" ``` Get 5 items per page: ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -H "Authorization: YOUR_API_KEY" \ "https://api.marblecms.com/v1/posts?limit=5" ``` Get page 2 with 5 items per page: ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -H "Authorization: YOUR_API_KEY" \ "https://api.marblecms.com/v1/posts?page=2&limit=5" ``` ## SDK Examples Fetch posts with pagination options: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); const result = await marble.posts.list({ limit: 10, page: 1, }); ``` The SDK returns an async iterable for paginated endpoints: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const result = await marble.posts.list({ limit: 10 }); for await (const page of result) { console.log(page.posts); console.log(page.pagination.currentPage); } ``` Use the pagination response to build navigation UI: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const result = await marble.posts.list({ limit: 10, page: 1 }); for await (const page of result) { const { pagination } = page; // Use these for your pagination UI const canGoBack = pagination.previousPage !== null; const canGoForward = pagination.nextPage !== null; const pageInfo = `Page ${pagination.currentPage} of ${pagination.totalPages}`; const itemCount = `${pagination.totalItems} total items`; } ``` The SDK's async iterable automatically handles fetching subsequent pages. If you only need a single page (e.g., for pagination controls), iterate once and break. ```json Success Response theme={"theme":{"light":"github-light","dark":"github-dark"}} { "posts": [ { "id": "post_abc123def456", "slug": "getting-started-with-cms-integration", "title": "Getting Started with CMS Integration", "content": "

Learn how to integrate your content management system...

", "coverImage": "https://cdn.marblecms.com/images/cms-guide.webp", "description": "A comprehensive guide to integrating your CMS...", "publishedAt": "2024-01-15T10:30:00.000Z", "updatedAt": "2024-01-20T14:22:33.456Z", "authors": [ { "id": "author_xyz789", "name": "John Smith", "image": "https://cdn.marblecms.com/avatars/john.jpg" } ], "category": { "id": "cat_tutorials123", "name": "Tutorials", "slug": "tutorials" }, "tags": [ { "id": "tag_integration456", "name": "Integration", "slug": "integration" } ] } ], "pagination": { "limit": 5, "currentPage": 1, "nextPage": null, "previousPage": null, "totalPages": 1, "totalItems": 2 } } ```
```json Empty Result Set theme={"theme":{"light":"github-light","dark":"github-dark"}} { "posts": [], "pagination": { "limit": 10, "currentPage": 1, "nextPage": null, "previousPage": null, "totalPages": 0, "totalItems": 0 } } ``` ```json Error - Invalid Page theme={"theme":{"light":"github-light","dark":"github-dark"}} { "error": "Invalid page number", "details": { "message": "Page 2 does not exist.", "totalPages": 1, "requestedPage": 2 } } ``` ## Paginated Endpoints The following endpoints support pagination: Get all published posts with pagination support. Browse all content categories with pagination. List all authors with their associated content. Retrieve all content tags with pagination. Browse workspace media assets with pagination. ## Best Practices **Optimize Performance**: Use smaller page sizes (5-20 items) for better performance, especially on mobile devices. **Handle Empty Results**: Always check if the data array is empty and handle the empty state in your UI. **Navigation Logic**: Use `nextPage` and `previousPage` values to build navigation controls. These fields will be `null` when navigation in that direction isn't possible. **Building Pagination UI**: The `pagination` object in every response gives you everything needed for pagination controls: current page, total pages, and next/previous page numbers. ## Error Handling When you request a page that doesn't exist, the API returns a structured error response: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "error": "Invalid page number", "details": { "message": "Page 2 does not exist.", "totalPages": 1, "requestedPage": 2 } } ``` Always check for the `error` field in your response before processing pagination data. * Values below 1 will default to 1 * Values above 200 will be capped at 200 * Non-numeric values will default to 10 When there are no items, `totalPages` will be 0 and `totalItems` will be 0. The data array will be empty. # API Authentication Source: https://docs.marblecms.com/api/quickstart Create a public or private API key, choose the right scopes, and make your first authenticated request to the Marble REST API in minutes. ## Quick Start Make your first API request in seconds! Copy and paste this command into your terminal: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -H "Authorization: YOUR_API_KEY" https://api.marblecms.com/v1/posts ``` Replace `YOUR_API_KEY` with an API key from your Marble dashboard. ## Creating an API Key Navigate to your workspace dashboard and click **Settings** in the sidebar. Click on **API Keys** in the settings menu. Click **Create API Key**, give it a descriptive name (e.g., "Production Website"), and choose a key type and permissions (see below). Your API key will only be shown once. Copy it and store it securely. ### Choosing a key type * **Public key** (`mpk_…`) — read-only. Pick this for client-side code, static site generators, or anywhere the key may be observed. Public keys can only hold the read scopes: `posts_read`, `authors_read`, `categories_read`, `tags_read`, `media_read`, and `fields_read`. * **Private key** (`msk_…`) — full access. Use it from trusted server-side environments. Private keys can additionally hold write scopes (`posts_write`, `authors_write`, `categories_write`, `tags_write`, `media_write`, `fields_write`) and `posts_read_drafts` for reading unpublished posts. ### Choosing permissions When creating a key, the **Permissions** section lists every available scope. Grant only the scopes the integration needs — for example, a website that lists published posts and authors needs `posts_read` and `authors_read` and nothing else. To read drafts via `GET /v1/posts?status=draft` or `?status=all`, create a private key and include the `posts_read_drafts` scope; otherwise the request returns `403 Forbidden`. See [Authentication](/api/introduction#authentication) for the full scope reference and the `403 "API key missing required scope: "` response. API keys should be kept secure. Public keys are scoped to read operations only, but exposing them in client-side code can lead to your [rate limits](/api/rate-limits) being consumed by others. Never ship a private key to the client — it can write to your workspace and read drafts. We recommend performing API requests on the server-side whenever possible. ## Authentication Include your API key in the `Authorization` header: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -H "Authorization: YOUR_API_KEY" https://api.marblecms.com/v1/posts ``` ```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}} const response = await fetch("https://api.marblecms.com/v1/posts", { headers: { Authorization: "YOUR_API_KEY", }, }); const data = await response.json(); console.log(data); ``` Alternatively, use the `key` query parameter: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl "https://api.marblecms.com/v1/posts?key=YOUR_API_KEY" ``` The `Authorization` header method is recommended for security. Query parameters may appear in server logs. ## Available Resources * **[Posts](/api-reference/posts/list-posts)**: List, get, create, update, or delete posts * **[Authors](/api-reference/authors/list-authors)**: List, get, create, update, or delete authors * **[Categories](/api-reference/categories/list-categories)**: List, get, create, update, or delete categories * **[Tags](/api-reference/tags/list-tags)**: List, get, create, update, or delete tags * **[Media](/api-reference/media/list-media-assets)**: List, get, upload, update, or delete media assets ## Next Steps Learn how to paginate through large result sets. Understand API rate limiting and best practices. Use our TypeScript definitions for type-safe development. # Rate Limits Source: https://docs.marblecms.com/api/rate-limits Marble API rate limits, X-RateLimit response headers, 429 error handling, and an exponential backoff pattern to keep clients within per-API-key quotas. Marble applies rate limits to protect the stability of the API for all users. This page explains the limits, how they are calculated, and how to design your integration to handle them gracefully. ## Overview Marble API uses rate limiting to ensure fair usage and API stability. Rate limits are enforced per API key using a sliding window approach. Most applications will not hit these limits during normal use, but you should still build in defensive handling for rate limit responses. ## Rate Limit Tiers ### API Key Requests All authenticated requests are rate limited per API key: * **Limit**: **200 requests per 10 seconds** * **Applies to**: All resource routes * `GET /posts` * `GET /posts/:identifier` * `GET /categories` * `GET /tags` * `GET /authors` This means each API key can make up to 200 requests every 10 seconds before receiving `429 Too Many Requests` responses. ### Unauthenticated Requests Requests without a valid API key have a more conservative limit: * **Limit**: **10 requests per 10 seconds** ## Rate Limit Response Headers When rate limiting is active, responses include standard headers that let you understand how many requests you have remaining in the current window. The maximum number of requests allowed in the current window. The number of requests remaining in the current window before you hit the limit. Unix timestamp (in seconds) when the current rate limit window resets. You should use these headers to dynamically adjust your request rate, especially for background jobs and batch operations. ## When You Exceed the Limit If you exceed the allowed number of requests in the current window, the API returns a `429 Too Many Requests` error. ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -i -H "Authorization: YOUR_API_KEY" "https://api.marblecms.com/v1/posts" ``` ```json 429 Too Many Requests theme={"theme":{"light":"github-light","dark":"github-dark"}} { "error": "Too many requests", "details": { "message": "You have exceeded the allowed number of requests. Please try again after the reset time.", "statusCode": 429 } } ``` If you receive `429` responses, you should back off immediately, respect the `X-RateLimit-Reset` header, and implement exponential backoff with jitter in automated clients. ## Implementing Backoff Here's a simple example of exponential backoff in JavaScript: ```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}} async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status === 429) { const resetTime = response.headers.get("X-RateLimit-Reset"); const waitMs = resetTime ? parseInt(resetTime) * 1000 - Date.now() : Math.pow(2, attempt) * 1000; await new Promise((resolve) => setTimeout(resolve, waitMs)); continue; } return response; } throw new Error("Max retries exceeded"); } ``` ## Best Practices * **Cache responses**: Store API responses where possible to reduce API calls. * **Batch operations**: Process items sequentially with delays rather than parallel requests. * **Monitor headers**: Track `X-RateLimit-Remaining` and slow down before hitting limits. * **Use webhooks**: For real-time updates, consider webhooks instead of polling the API. If you consistently need higher rate limits for your use case, please [contact us](mailto:support@marblecms.com) to discuss your requirements. # TypeScript Types Source: https://docs.marblecms.com/api/types TypeScript types and Zod schemas for Marble API responses — Post, Author, Category, Tag, Pagination — via the SDK or copy-paste definitions. **We recommend using the [TypeScript SDK](/tools/sdk)** for the best developer experience. The SDK is fully type-safe and exports all up-to-date types automatically. If you're using `fetch` directly and need type definitions, you can import them from the SDK or copy the definitions below. ## Importing from the SDK The `@usemarble/sdk` package exports all TypeScript types: ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @usemarble/sdk ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add @usemarble/sdk ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add @usemarble/sdk ``` ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add @usemarble/sdk ``` ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import type { Post, Category, Tag, Author, Pagination } from "@usemarble/sdk"; // Use with fetch const response = await fetch("https://api.marblecms.com/v1/posts", { headers: { Authorization: process.env.MARBLE_API_KEY }, }); const data: { posts: Post[]; pagination: Pagination } = await response.json(); ``` *** ## Type Definitions If you prefer to copy the types directly into your project, here are the complete type definitions: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} export type Post = { id: string; slug: string; title: string; status: "published" | "draft"; content: string; featured: boolean; description: string; coverImage: string | null; publishedAt: Date; updatedAt: Date; authors: Author[]; category: Omit; tags: Omit[]; // Custom field values, keyed by field key fields: Record; }; export type Pagination = { limit: number; currentPage: number; nextPage: number | null; previousPage: number | null; totalItems: number; totalPages: number; }; export type MarblePostListResponse = { posts: Post[]; pagination: Pagination; }; export type MarblePostResponse = { post: Post; }; export type Tag = { id: string; name: string; slug: string; description: string | null; count: { posts: number; }; }; export type MarbleTagResponse = { tag: Tag; }; export type MarbleTagListResponse = { tags: Tag[]; pagination: Pagination; }; export type Category = { id: string; name: string; slug: string; description: string | null; count: { posts: number; }; }; export type MarbleCategoryResponse = { category: Category; }; export type MarbleCategoryListResponse = { categories: Category[]; pagination: Pagination; }; export type Author = { id: string; name: string; slug: string; image: string | null; bio: string | null; role: string | null; socials: Social[]; // Present on list/detail endpoints, omitted when embedded in a post count?: { posts: number; }; }; export type Social = { url: string; platform: SocialPlatform; }; export type SocialPlatform = | "x" | "twitter" | "github" | "facebook" | "instagram" | "youtube" | "tiktok" | "linkedin" | "website" | "onlyfans" | "discord" | "bluesky"; export type MarbleAuthorResponse = { author: Author; }; export type MarbleAuthorListResponse = { authors: Author[]; pagination: Pagination; }; ``` Copy these type definitions into a `types/marble.ts` file in your project. If you prefer a published package, use the official SDK that exports these types. ### NPM package The official `@usemarble/sdk` package exports all TypeScript types shown on this page. Installing the package avoids copying the type definitions into multiple projects. To install the package run: ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @usemarble/sdk ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add @usemarble/sdk ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add @usemarble/sdk ``` ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add @usemarble/sdk ``` You can import the types like this: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import type { Post, Category, Tag, Author } from "@usemarble/sdk"; ``` The package will be kept up-to-date with the API types. ### Zod Schemas For runtime validation, you can also create Zod schemas for your data: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { z } from "zod"; const PostSchema = z.object({ id: z.string(), slug: z.string(), title: z.string(), status: z.enum(["published", "draft"]), content: z.string(), featured: z.boolean(), description: z.string(), coverImage: z.string().url().nullable(), publishedAt: z.coerce.date(), updatedAt: z.coerce.date(), authors: z.array( z.object({ id: z.string(), name: z.string(), slug: z.string(), image: z.string().nullable(), bio: z.string().nullable(), role: z.string().nullable(), socials: z.array(SocialSchema), }) ), category: z.object({ id: z.string(), slug: z.string(), name: z.string(), description: z.string().nullable(), }), tags: z.array( z.object({ id: z.string(), slug: z.string(), name: z.string(), description: z.string().nullable(), }) ), fields: z.record( z.string(), z.union([ z.string(), z.number(), z.boolean(), z.array(z.string()), z.null(), ]) ), }); const PaginationSchema = z.object({ limit: z.number(), currentPage: z.number(), nextPage: z.number().nullable(), previousPage: z.number().nullable(), totalItems: z.number(), totalPages: z.number(), }); export const SocialSchema = z.object({ url: z.string(), platform: SocialPlatformSchema, }); export const SocialPlatformSchema = z.enum([ "x", "twitter", "github", "facebook", "instagram", "youtube", "tiktok", "linkedin", "website", "onlyfans", "discord", "bluesky", ]); ``` # Content Model Source: https://docs.marblecms.com/concepts/content-model Marble's content model — how posts relate to authors, a single category, multiple tags, and custom fields, with the fields and JSON shape returned by the API. Marble's content model is intentionally small. Everything centers on the **post**, which is connected to **authors**, a **category**, **tags**, and optional **custom fields**. Understanding how these relate makes it straightforward to query and render content from your frontend. ## The entities at a glance | Entity | What it is | Relationship to a post | | ---------------- | ------------------------------------------------------------------ | --------------------------------------- | | **Post** | A single piece of content (an article, a changelog entry, a page). | The core entity. | | **Author** | A byline — name, bio, avatar, and social links. | A post has one or more authors. | | **Category** | A single primary grouping for a post. | A post belongs to exactly one category. | | **Tag** | A lightweight, flexible label. | A post can have many tags. | | **Custom field** | Workspace-defined metadata. | Optional extra values on a post. | Every entity is scoped to a [workspace](/concepts/workspaces) and has a URL-friendly `slug` that's unique within that workspace. You can fetch most resources by either their `id` or their `slug`. ## Posts A post is the unit of content you write in the [editor](/features/editor). Beyond its title and body, a post carries the metadata your frontend needs to render and route it. Key fields returned by the API: | Field | Description | | ------------- | ------------------------------------------------------------------------------- | | `id` | Unique identifier. | | `slug` | URL-friendly identifier, unique per workspace. | | `title` | The post title. | | `description` | Short summary, useful for previews and SEO meta tags. | | `content` | The body, returned as HTML or Markdown (see [content format](#content-format)). | | `status` | `published` or `draft`. | | `featured` | Whether the post is flagged as featured. | | `coverImage` | URL of the cover image, or `null`. | | `publishedAt` | Publish timestamp (ISO 8601, UTC). | | `updatedAt` | Last-updated timestamp. | | `authors` | Array of author objects. | | `category` | The post's single category. | | `tags` | Array of tag objects. | | `fields` | Custom field values, keyed by field key. | ### Content format Post content can be delivered as **HTML** (default) or **Markdown** using the `format` query parameter — useful if you'd rather render Markdown yourself on the frontend: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -H "Authorization: YOUR_API_KEY" \ "https://api.marblecms.com/v1/posts?format=markdown" ``` ### Status and drafts A post is either `published` or `draft`. By default the API returns only published posts. Pass `status=draft` or `status=all` to include unpublished content — handy for building a preview environment. ## Authors An **author** is a byline you attach to posts. Authors are managed at the workspace level and reused across posts, so updating an author's bio updates it everywhere. An author includes `name`, `slug`, `bio`, `image`, `role`, and an array of `socials` (each with a `platform` and `url`). A post can have **multiple authors**, with one designated as the primary author. Authors are distinct from workspace **members**. A member has dashboard access; an author is a byline that may or may not correspond to a member. See [Workspaces & Teams](/concepts/workspaces#team-members-and-roles). ## Categories A **category** is the single, primary grouping for a post — think "Engineering", "Product", or "Changelog". Each post belongs to **exactly one** category, which makes categories ideal for top-level sections and routing (e.g. `/blog/[category]/[slug]`). A category has a `name`, `slug`, and optional `description`. You can filter posts by category, or exclude categories, when listing posts: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} # Only posts in the "engineering" or "product" categories curl -H "Authorization: YOUR_API_KEY" \ "https://api.marblecms.com/v1/posts?categories=engineering,product" # Everything except the "changelog" category curl -H "Authorization: YOUR_API_KEY" \ "https://api.marblecms.com/v1/posts?excludeCategories=changelog" ``` ## Tags A **tag** is a lightweight, flexible label. Unlike categories, a post can have **many tags**, making them well suited to cross-cutting topics like "javascript", "tutorial", or "release". Tags have the same shape as categories (`name`, `slug`, `description`) and support the same include/exclude filtering via the `tags` and `excludeTags` parameters. **Category vs. tag:** use a single **category** for the primary section a post belongs to, and **tags** for the many topics it touches. ## Custom fields Custom fields let you extend the post schema with workspace-specific metadata — a release date, a priority score, a list of related links — without changing your code on the CMS side. Field values come back on each post under `fields`, keyed by the field's key. See [Custom Fields](/features/custom-fields) for setup and the full list of field types. ## How it maps to the API When you fetch a post, the related entities are embedded directly in the response, so a single request gives you everything you need to render it: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "post": { "id": "cryitfjp5678mn09qrstuvwx", "slug": "getting-started-with-nextjs", "title": "Getting Started with Next.js", "status": "published", "description": "A beginner's guide to Next.js", "coverImage": "https://media.marblecms.com/cover.jpg", "publishedAt": "2024-01-15T10:00:00Z", "authors": [ { "name": "John Doe", "slug": "john-doe", "image": "...", "socials": [] } ], "category": { "name": "Engineering", "slug": "engineering" }, "tags": [{ "name": "Next.js", "slug": "nextjs" }], "fields": { "release_date": "2024-01-15" }, "content": "

Hello world

" } } ``` ## Next steps Extend posts with your own metadata. Query posts by category, tag, and search. Full endpoint reference for every resource. Typed access to posts, authors, categories, and tags. # What Is a Headless CMS? Source: https://docs.marblecms.com/concepts/headless-cms What a headless CMS is, how it differs from monolithic platforms like WordPress, the benefits for developers, and where Marble fits in the content flow. A **headless CMS** is a content management system that stores and organizes your content, then delivers it over an API — without dictating how or where that content is displayed. The "head" (the frontend that visitors see) is decoupled from the "body" (the place you write and manage content). Marble is a headless CMS: you write posts in a clean dashboard, and your content is served as JSON through a [REST API](/api/introduction) that any frontend can consume. ## Traditional vs. headless In a **traditional (or monolithic) CMS** like WordPress, the editing experience, the database, and the public website are all bundled into one system. The CMS renders your pages and ships the HTML to the browser. This is convenient, but it ties your content to a specific stack, theming system, and hosting model. A **headless CMS** splits these concerns apart: Content + presentation are coupled. The CMS renders and serves the final HTML. You build with its themes and plugins. Content is delivered as data over an API. You build the frontend with any framework and fetch content into it. ## Why go headless? Pull the same content into Next.js, Astro, TanStack Start, a mobile app, or anything that can call an API. Statically generate or cache pages on your own frontend instead of rendering on every request. No theme system to fight. Your design, your components, your stack. One source of content can power a website, a newsletter, and an app at once. ### Trade-offs to know A headless CMS hands you flexibility, but you bring your own frontend. There's no built-in theme that renders pages for you — you fetch content and decide how to display it. For developers building custom sites, that's the point. If you only need a templated blog with zero code, a traditional CMS may be simpler. ## When to choose Marble Marble is a good fit when you want: * A **clean writing experience** for yourself or a team, without a bloated admin. * To build your site with a **modern framework** and just need a content source. * A **simple, predictable API** instead of GraphQL schemas and complex configuration. Marble keeps content and presentation cleanly separated — you manage posts in the dashboard, and your frontend stays entirely yours. ## How content flows in Marble Create posts in the [editor](/features/editor) and organize them with authors, categories, and tags. Set a post's status to published. It becomes available through the API. Call the [REST API](/api/introduction) or use the [TypeScript SDK](/tools/sdk) to pull content into your app. Display the returned JSON with your own components and design. ## Next steps How Marble organizes content and collaborators. Posts, authors, categories, and tags explained. Set up your workspace and publish your first post. Explore the endpoints that deliver your content. # Workspaces & Teams Source: https://docs.marblecms.com/concepts/workspaces How Marble workspaces isolate posts, media, API keys, and members, plus Owner, Admin, and Member team roles and the difference between members and authors. A **workspace** is the top-level container for everything in Marble. Each workspace holds its own posts, media, authors, categories, tags, custom fields, API keys, and team members — fully isolated from every other workspace. Think of a workspace as a single project: a personal blog, a company website, or a documentation site. If you manage multiple sites, create a separate workspace for each. ## What lives in a workspace Posts, authors, categories, and tags — see the [content model](/concepts/content-model). Images and video uploaded to the workspace's [media library](/features/media). Workspace-specific [metadata fields](/features/custom-fields) added to posts. Keys that scope API access to this workspace's content. ## Workspaces and the API A workspace is also the **boundary for API access**. Every API key belongs to a single workspace, so a key only ever returns content from that workspace. This keeps your projects cleanly separated — there's no way for one site's key to read another site's posts. Create and manage keys from **Settings → API Keys** in your dashboard. See [API Authentication](/api/quickstart) to make your first request. ## Team members and roles Invite collaborators to a workspace from **Settings → Members**. Each member has a role that determines what they can do: | Role | Description | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **Owner** | Full control of the workspace, including billing, deleting the workspace, and managing the logo and members. There is one owner per workspace. | | **Admin** | Manage content and members, but cannot perform owner-only actions like deleting the workspace. | | **Member** | Write, edit, and publish content within the workspace. | Go to your workspace dashboard and open **Settings → Members**. Enter the person's email and choose a role. They'll receive an invitation to join. Once accepted, they appear in the members list and can collaborate on content. Workspace members and **authors** are related but distinct. A member is someone with dashboard access; an author is a byline attached to posts. See the [content model](/concepts/content-model#authors) for how they connect. ## Next steps How posts, authors, categories, and tags fit together. Create a key and make your first request. # Custom Fields Source: https://docs.marblecms.com/features/custom-fields Extend Marble posts with structured workspace-specific metadata like release dates, priorities, or product names and read them from the API. Custom fields let you add your own metadata to every post in a workspace. Instead of forcing everything into tags or description, you can model structured values like release dates, reading levels, priorities, product names, or any internal metadata your team needs. ## Why use custom fields * Extend Marble's default post schema without changing your frontend code structure each time. * Keep metadata consistent across all posts with typed inputs. * Return custom field values directly in API responses under a single `fields` object. ## Create and manage fields Open your workspace settings, then select the **Custom Fields** section under the developer settings area. Add a field name, key, type, and optional description. Mark it as required if editors must provide a value. Open any post and set values in the **Metadata** tab. Every post can store a value for each custom field. You can also manage field definitions through the API, SDK, or MCP server. Agents should call `get_fields` before writing post field values, create any missing field with `create_field`, then pass values under `fields` when creating or updating a post. ## Field types | Type | Input in Marble | API value | | ------------- | ---------------- | ---------------------------------------------- | | `text` | Multi-line text | `string \| null` | | `number` | Numeric input | `number \| null` | | `boolean` | Toggle/switch | `boolean \| null` | | `date` | Date picker | `string \| null` (ISO date, e.g. `2026-04-02`) | | `richtext` | Rich text editor | `string \| null` (HTML string) | | `select` | Single option | `string \| null` | | `multiselect` | Multiple options | `string[] \| null` | `select` and `multiselect` fields require predefined options. Option values are what you receive in the API and what you send when writing post values. ## Writing fields through the API Create field definitions first: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "key": "audience", "name": "Audience", "type": "multiselect", "required": false, "options": [ { "value": "developers", "label": "Developers" }, { "value": "founders", "label": "Founders" } ] } ``` Then pass values by field key when creating or updating posts: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "title": "Launch Notes", "content": "

Hello world

", "description": "What changed this week", "slug": "launch-notes", "categoryId": "cat_123", "status": "draft", "fields": { "audience": ["developers", "founders"] } } ``` Unknown field keys, wrong value types, and option values that do not match a configured `select` or `multiselect` option are rejected with `400` responses. ## API response shape Custom fields are exposed on posts as `post.fields` (single post) and `posts[].fields` (list posts). In the current public API, custom fields are returned in `GET` post responses. ### Example response ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "post": { "id": "cm9x...", "slug": "shipping-custom-fields", "title": "Shipping Custom Fields", "fields": { "release_date": "2026-04-02", "priority_score": 7, "evergreen": true, "target_channels": ["blog", "newsletter"], "editor_note": "

Republish in Q3

" } } } ``` ### Unset fields If a field exists in your workspace but a post has no value for it yet, Marble returns that key with `null`. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "fields": { "release_date": null, "priority_score": null } } ``` ## Important behavior * Field keys are workspace-scoped and must be unique. * Post writes never create fields implicitly. Create or update field definitions first, then write values to posts. * Field type and options are effectively schema choices. Plan them early to avoid migration work later. * Deleting a field removes that field and its stored values from all posts. # Marble's Editor Source: https://docs.marblecms.com/features/editor Marble's distraction-free editor with inline formatting, image and video embeds, metadata sidebar, and Content AI readability scoring and writing suggestions. Marble's editor is designed to get out of your way. A clean, Notion-like writing experience with no clutter — just you and your content. All metadata and settings live in a collapsible sidebar so you can focus when you need to. ## Writing Experience The editor opens empty and distraction-free. Start typing and your content takes center stage. Formatting options appear when you need them, and disappear when you don't. ### Formatting Standard formatting options are available inline: * **Bold** and *italic* text * Text highlights and colors * Headings (H1 through H6) * Bullet and numbered lists * Blockquotes * Code blocks and inline code * Links ### Nodes Beyond text, the editor supports rich content nodes for embedding media: Upload images directly or pick from your media library. Embed YouTube videos by pasting a video URL. Embed tweets by pasting a tweet URL. Upload and embed video files from your media library. ## Sidebar The sidebar keeps metadata and analytics tucked away until you need them. Toggle it open to access two tabs: **Metadata** and **Analysis**. ### Metadata Tab Control everything about your post without leaving the editor: | Field | Description | | ---------------- | ------------------------------------------------------------------------------------ | | **Published** | Toggle to publish or unpublish your post. Unpublished posts are hidden from the API. | | **Featured** | Mark a post as featured for special treatment on your site. | | **Cover Image** | Set a featured image for your post. | | **Description** | A short summary for SEO and previews. | | **Slug** | The URL-friendly identifier for your post. | | **Authors** | Select one or more authors. | | **Tags** | Add multiple tags to organize content. | | **Category** | Assign a single category. | | **Published On** | Set or schedule the publish date. | ### Analysis Tab The Analysis tab gives you real-time feedback on your writing with text statistics and AI-powered suggestions. **Text Statistics:** * Word count * Sentence count * Words per sentence * Estimated reading time ## Content AI Marble's Content AI acts like a writing instructor, giving you a readability score and actionable suggestions to improve your content. ### Readability Score Every post gets a score based on the **Flesch Reading Ease** formula — a widely used standard for measuring how easy a document is to read. The higher the score, the more accessible your writing. ### Writing Suggestions The AI analyzes your content and surfaces suggestions in the sidebar: Break long sentences into two to keep your writing crisp and improve pacing. Simplify complex words for quicker reading. Cut softening phrases to tighten flow and maintain focus. Get feedback on your opening to hook readers from the first sentence. Click on any suggestion to highlight the affected paragraph, sentence, or word in the editor. Make changes directly in your content, or ignore suggestions that don't fit your style. Content AI works best as a second pair of eyes. Write naturally first, then review the suggestions to polish your content. # Exporting Content Source: https://docs.marblecms.com/features/export Export your workspace as a downloadable JSON archive containing posts, categories, tags, authors, media metadata, custom fields, and a manifest. You can export your entire workspace as a downloadable JSON archive. Exports are useful for taking backups, migrating to another Marble workspace, or piping your content into another tool. ## What's in an export Each export is a single `.zip` archive containing one JSON file per resource plus a manifest: | File | Contents | | ----------------- | ----------------------------------------------------------------------------------- | | `manifest.json` | Provider, schema version, format, export timestamp, workspace, and resource counts. | | `posts.json` | Every post with its content, status, category, tags, and authors. | | `categories.json` | All categories. | | `tags.json` | All tags. | | `authors.json` | Authors with bios and social links. | | `media.json` | Metadata for every media item (URL, mime type, dimensions, size). | | `fields.json` | Custom fields and their options. | Exports include media **metadata** — not the underlying media files. Media URLs in `media.json` continue to point to the originally hosted files. ## Starting an export 1. Open **Settings → Data** in your workspace. 2. Find the **Export Workspace Data** section. 3. Click **Start Export**. Marble queues the export and starts building the archive in the background. You can leave the page while it runs — the status updates automatically when you return. ## Job lifecycle Each export moves through a few states: | Status | Meaning | | ------------ | ------------------------------------------------------------------------ | | `queued` | The export has been requested and is waiting to be picked up. | | `processing` | A worker is collecting your content and packaging the archive. | | `ready` | The archive is built and available to download. | | `failed` | Something went wrong. The error reason is shown next to the job. | | `expired` | The 24-hour download window has passed and the archive has been removed. | ## Downloading the archive Once a job is `ready`, a **Download** button appears next to it in the export list. The most recent three exports are shown, and any finished export remains downloadable for **24 hours**. After that window the file is deleted and the job is marked `expired` — start a new export to get a fresh copy. ## Email notification When an export finishes, Marble sends an **"Your export is ready"** email containing the download link and its expiry time. The email goes to: * The workspace member who started the export. * All workspace **owners**. This means owners always have a record of completed exports, even if someone else triggered them. Use exports to back up your workspace, hand off content to another team, or seed a new Marble workspace with your existing data. The JSON layout is stable across exports (`schemaVersion: 1`), so you can safely script against it. # Importing Content Source: https://docs.marblecms.com/features/import Bring existing Markdown and MDX files into Marble as draft posts using the queued import workflow, with a syntax compatibility reference. You can bring your existing `.md` and `.mdx` files into Marble as draft posts. Marble imports happen asynchronously: you upload a file, Marble queues a job, a worker parses the content in the background, and the created drafts show up in your posts list when the job completes. ## Starting an import 1. Open **Settings → Data** in your workspace. 2. Find the **Import Content** section. 3. Click **Import**. 4. Upload a single Markdown file or a ZIP archive (see [Supported uploads](#supported-uploads)). Marble queues the import and starts processing it in the background. You can leave the page while it runs — the status updates automatically when you return. Imports are capped at **4 MB per upload**. Each ZIP archive may contain up to **100** Markdown files and **20 MB** of extracted content. Entries that are not `.md` or `.mdx` are skipped. ## Supported uploads | Upload type | Behavior | | -------------- | ------------------------------------------------------------------------------------------ | | `.md` / `.mdx` | Creates a single draft post from the file. | | `.zip` archive | Creates one draft post per `.md` or `.mdx` file in the archive. Other entries are skipped. | For each file Marble pulls the title from frontmatter when available, otherwise from the first heading, otherwise from the filename. Imported posts are always created as **drafts** so you can review them before publishing. URL-based imports are **not supported**. You must upload a file or ZIP archive. ## Job lifecycle Each import moves through these states: | Status | Meaning | | ------------- | ------------------------------------------------------------------------------ | | `queued` | The import has been requested and is waiting to be picked up. | | `discovering` | A worker is reading the upload and listing the files inside. | | `processing` | The worker is parsing each file and preparing draft records. | | `importing` | Drafts are being written to your workspace. | | `completed` | All drafts are created. The number of imported posts is shown next to the job. | | `failed` | Something went wrong. The error reason is shown next to the job. | The most recent imports are listed under **Import Content**, with a **View posts** link once a job completes. ## Supported Markdown syntax The following core Markdown features are **preserved on import**: | Syntax | Import | Editor | | ------------------------ | ------ | ------ | | Headings | ✅ | ✅ | | Paragraphs & line breaks | ✅ | ✅ | | Emphasis | ✅ | ✅ | | Unordered lists | ✅ | ✅ | | Ordered lists | ✅ | ✅ | | Links | ✅ | ✅ | | Images | ✅ | ✅ | | Blockquotes | ✅ | ✅ | | Code blocks | ✅ | ✅ | | Inline code | ✅ | ✅ | | Tables | ❌ | ❌ | Importing unsupported syntax might not display properly. ## MDX and components If your content uses MDX, Marble will keep JSX-style components and custom MDX syntax **in the source**: * MDX elements like `` * Shortcodes or layout wrappers used in `.mdx` files However: * ❌ **Custom components will not render inside the Marble editor preview.** * ❌ **Marble does not execute or parse your components for you.** You need to parse the imported MDX inside your own codebase (for example, using your framework's MDX pipeline) for those components to work at runtime. You can safely import your existing Markdown (`.md`) and MDX (`.mdx`) files into Marble. Each file becomes a draft post you can review before publishing, and any MDX components remain in the source so they continue to work when parsed in your own application. # Media & Image Hosting Source: https://docs.marblecms.com/features/media Upload images, video, audio, and documents to Marble's built-in media library and CDN, then embed them in posts or access them via the REST API and MCP. Marble handles your media so you can focus on writing. Upload files to your workspace, keep useful metadata with them, and serve them through Marble's CDN. ## Supported media types Upload common image formats including JPEG, PNG, GIF, AVIF, and WebP. Images include dimensions when Marble can read them. Host video files such as MP4 and WebM for posts, pages, and custom frontends. Store audio files such as MP3, WAV, OGG, and AAC for use in custom experiences. Upload documents such as PDFs and plain text files when you need CDN-backed downloads. ## How it works When you upload media to Marble, the file is stored in Marble's object storage and served from a CDN URL. * **Fast delivery** — Media is served from `cdn.marblecms.com` * **Stable URLs** — Use the returned URL in posts, websites, apps, or feeds * **Metadata included** — Marble stores fields like name, alt text, MIME type, size, media type, dimensions, duration, blur hash, and upload date when available ## Using media in your posts In the Marble dashboard, click on **Media** in the sidebar to access your media library. Click **Upload** to add images, videos, audio, or documents. Files are available as soon as the upload finishes. When editing a post, use the image or video controls to insert media from your library. You can also paste CDN URLs directly. ## Accessing media via API You can read and manage media through the public API. | Endpoint | Description | | ----------------------- | --------------------------------------------------------------------- | | `GET /v1/media` | List media assets in your workspace. | | `GET /v1/media/:id` | Get one media asset and its metadata. | | `POST /v1/media/upload` | Upload a small file directly. Requires a private API key. | | `PATCH /v1/media/:id` | Update media metadata. Requires a private API key. | | `DELETE /v1/media/:id` | Delete a media asset and its stored file. Requires a private API key. | Uploads through the API are intended for small files. For larger uploads, use the dashboard uploader. As a rule of thumb, avoid using very large images in your posts. Users with slower internet connections will experience longer load times, which can hurt engagement. ## Managing your media From the Media Library, you can: * **Search and filter** — Find files quickly by name or media type * **Edit metadata** — Keep names and alt text useful for humans and frontends * **Delete unused files** — Remove media you no longer need * **View file details** — See URL, file size, MIME type, dimensions, and upload date ## Using media with MCP Marble's MCP server includes media tools for AI agents. Agents can list media, inspect a single asset, update metadata, delete assets, and upload media from a public URL. Remote MCP servers cannot read files from your local filesystem directly. To upload through MCP, provide a URL that the server can fetch. Deleting media removes the stored file. Any post, website, app, or feed that uses that URL will stop loading it, so update those references before deleting an asset. # Webhooks Source: https://docs.marblecms.com/features/webhooks Configure Marble webhooks to revalidate static sites, automate workflows, verify signatures, and inspect deliveries when content changes. Webhooks provide a powerful way to keep your site's content fresh and automate workflows. When an event occurs in your Marble workspace, such as publishing a post, Marble sends an HTTP POST payload to a URL you configure. Webhooks are processed asynchronously by Marble's background jobs worker. Marble records events, retries failed deliveries, and stores delivery attempts so webhook activity can be inspected later. ## Content revalidation For statically generated sites, content is fetched at build time. If you update a post in Marble, the change won't be live until you trigger a new deployment. Frameworks like Next.js provide built-in solutions for this, such as [Incremental Static Regeneration (ISR)](https://nextjs.org/docs/app/guides/incremental-static-regeneration), which can be triggered on-demand by a webhook for instant updates. ## Automating workflows Create a serverless function that listens for a `post.published` event from a Marble webhook. When triggered, your function can: * Send a newsletter to your subscribers with the new post * Share the post on social media * Sync the content to another service or backup location ## Setting up webhooks in Marble In the Marble dashboard, open your workspace, go to **Settings**, then choose **Webhooks** under the Developers section. Click **Create Webhook** and fill in the required details: * **Name**: A descriptive name for your webhook. * **URL**: The endpoint where the webhook payload will be sent. * **Format**: Choose JSON, Discord, or Slack. * **Events**: Select the events you want to listen for, such as `post.published` or `post.updated`. Save your webhook, then open it from the list by clicking the 3 dots and selecting **Details**. On the webhook details page, click **Copy secret** next to the masked secret value. You'll need this to verify the authenticity of incoming requests. ## Inspecting webhook deliveries Every time an event matches a webhook's subscription, Marble queues a delivery and stores the result. You can inspect this history from the webhook details page in the dashboard. To open it, go to **Settings → Webhooks**, click the 3 dots on the webhook's row, and choose **Details**. The details page is split into two parts: * **Webhook settings** (side panel): edit the webhook's name, URL, payload format, enabled state, and subscribed events. Changes apply to future deliveries only. * **Deliveries**: a paginated table of every delivery attempted for this webhook, with filters and expandable rows. ### Filtering deliveries Above the deliveries table you can filter by: * **Status**: `pending`, `sending`, `success`, `retrying`, or `failed`. * **HTTP response**: `2xx`, `3xx`, `4xx`, `5xx`, or `No response` (the endpoint never responded, for example on a network error or timeout). * **Event type**: any of the events the webhook is subscribed to, such as `post.published` or `tag.created`. Filters can be combined and the results are paginated. ### Reading a delivery Click a row to expand it and see the full delivery record: * **Event ID** and **event timestamp**: identify the workspace event that triggered the delivery. * **Delivery ID**: matches the `x-marble-delivery-id` header your endpoint received. * **Sent at**: when the most recent attempt was sent. * **HTTP response**: the status code returned by your endpoint, or `No response` if the request never completed. * **Duration**: how long the latest attempt took, in milliseconds. * **Error message**: shown when the latest attempt failed, for example with a connection error or non-2xx response body. * **Payload**: the exact JSON body that was sent. Use **Copy payload** to copy it for replay or debugging. Use this view to confirm whether your endpoint received an event, what it returned, and what payload it was given. ### Sending a test webhook To verify that your endpoint is reachable and that your signature verification works without waiting for a real content event, click **Send test webhook** on the details page. Marble queues a test delivery against the webhook's configured URL, and a new row appears in the deliveries table once it is sent. This is useful when: * You've just created a webhook and want to confirm the URL and secret are wired up correctly. * You've changed your endpoint's deployment or signature verification code. * A previous delivery failed and you want to retry against the current endpoint without editing real content. ### Verifying webhook requests To ensure that incoming webhook requests are from Marble and haven't been tampered with, you can verify the request using the webhook secret. Here's an example of how to verify a webhook request in Next.js using the `crypto` module: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} // lib/marble/webhook.ts import { createHmac, timingSafeEqual } from "node:crypto"; import { revalidatePath, revalidateTag } from "next/cache"; import type { PostEventData } from "@/types/blog"; export async function handleWebhookEvent(payload: PostEventData) { const event = payload.type; const data = payload.data; // Handle any post.* events (published, updated, deleted, etc.) if (event.startsWith("post")) { // Revalidate the blog index and the single post page revalidatePath("/blog"); revalidatePath(`/blog/${data.slug}`); // If your data fetches use tags, revalidate that tag as well: // e.g. fetch(..., { next: { tags: ["posts"] } }) revalidateTag("posts"); return { revalidated: true, now: Date.now(), message: "Post event handled", }; } return { revalidated: false, now: Date.now(), message: "Event ignored", }; } export function verifySignature( secret: string, signatureHeader: string, bodyText: string ) { // Strip possible "sha256=" prefix const expectedHex = signatureHeader.replace(/^sha256=/, ""); const computedHex = createHmac("sha256", secret) .update(bodyText) .digest("hex"); // Convert to buffers for constant-time compare const expected = Buffer.from(expectedHex, "hex"); const computed = Buffer.from(computedHex, "hex"); // lengths must match for timingSafeEqual if (expected.length !== computed.length) return false; return timingSafeEqual(expected, computed); } ``` ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} // app/api/revalidate/route.ts import { NextResponse } from "next/server"; import type { PostEventData } from "@/types/blog"; import { verifySignature, handleWebhookEvent } from "@/lib/marble/webhook"; export async function POST(request: Request) { const signature = request.headers.get("x-marble-signature"); const secret = process.env.MARBLE_WEBHOOK_SECRET; if (!secret || !signature) { return NextResponse.json( { error: "Secret or signature missing" }, { status: 400 } ); } const bodyText = await request.text(); if (!verifySignature(secret, signature, bodyText)) { return NextResponse.json({ error: "Invalid signature" }, { status: 400 }); } let payload: PostEventData; try { payload = JSON.parse(bodyText) as PostEventData; } catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } if (!payload.type || !payload.data) { return NextResponse.json( { error: "Invalid payload structure" }, { status: 400 } ); } try { const result = await handleWebhookEvent(payload); return NextResponse.json(result); } catch (err) { return NextResponse.json( { error: "Failed to process webhook" }, { status: 500 } ); } } ``` If you want a better guide on how to invalidate cache in your framework of choice, you can check out our blog post on [Using Marble's Webhooks with the Next.js App Router](https://marblecms.com/blog/nextjs-webhooks-with-marble). Marble will send the signature in the `x-marble-signature` header of the request. You can use this signature to verify the authenticity of the request. Make sure to add the `process.env.MARBLE_WEBHOOK_SECRET` environment variable. ### Request Payload When a webhook is triggered, Marble sends a POST request to the specified URL with a JSON payload. JSON webhooks receive a stable envelope that includes the event metadata, the affected resource, the actor when available, and event-specific data. Webhook requests include these headers: * `x-marble-event`: The event type, such as `post.updated`. * `x-marble-event-id`: The ID of the workspace event. * `x-marble-delivery-id`: The ID of the delivery attempt. * `x-marble-timestamp`: The Unix timestamp for the request. * `x-marble-signature`: The HMAC SHA-256 signature for the request body. Here are a few post event examples: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "evt_cmf3d1gsv11469tlkp53bcutv", "type": "post.published", "createdAt": "2026-05-22T14:42:31.120Z", "workspaceId": "org_cms96emp70001l60415sft0i5", "resource": { "type": "post", "id": "post_cmf3d1gsv11469tlkp53bcutv" }, "actor": { "type": "api_key", "id": "key_cmf3cp0nz0001l604a8c7h2sc" }, "data": { "id": "post_cmf3d1gsv11469tlkp53bcutv", "title": "Getting Started with Marble CMS", "slug": "getting-started-with-marble", "description": "Learn how to publish your first post with Marble.", "coverImage": "https://cdn.marblecms.com/images/getting-started.png", "status": "published", "featured": false, "categoryId": "cat_cmf3cujdp0003l604w7h3whg9", "primaryAuthorId": "author_cmf3cvj1e0004l604x7m6g6fa", "publishedAt": "2026-05-22T14:00:00.000Z", "createdAt": "2026-05-21T18:12:09.431Z", "updatedAt": "2026-05-22T14:42:30.951Z" } } ``` ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "evt_cmf3d1gsv11469tlkp53bcutv", "type": "post.updated", "createdAt": "2026-05-22T14:42:31.120Z", "workspaceId": "org_cms96emp70001l60415sft0i5", "resource": { "type": "post", "id": "post_cmf3d1gsv11469tlkp53bcutv" }, "actor": { "type": "api_key", "id": "key_cmf3cp0nz0001l604a8c7h2sc" }, "data": { "id": "post_cmf3d1gsv11469tlkp53bcutv", "title": "Getting Started with Marble CMS", "slug": "getting-started-with-marble", "description": "Learn how to publish your first post with Marble.", "coverImage": "https://cdn.marblecms.com/images/getting-started.png", "status": "published", "featured": false, "categoryId": "cat_cmf3cujdp0003l604w7h3whg9", "primaryAuthorId": "author_cmf3cvj1e0004l604x7m6g6fa", "publishedAt": "2026-05-22T14:00:00.000Z", "createdAt": "2026-05-21T18:12:09.431Z", "updatedAt": "2026-05-22T14:42:30.951Z", "changes": ["title", "description"] } } ``` ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "evt_cmf3d1gsv11469tlkp53bcutv", "type": "post.deleted", "createdAt": "2026-05-22T14:42:31.120Z", "workspaceId": "org_cms96emp70001l60415sft0i5", "resource": { "type": "post", "id": "post_cmf3d1gsv11469tlkp53bcutv" }, "actor": { "type": "api_key", "id": "key_cmf3cp0nz0001l604a8c7h2sc" }, "data": { "id": "post_cmf3d1gsv11469tlkp53bcutv", "title": "Getting Started with Marble CMS", "slug": "getting-started-with-marble", "description": "Learn how to publish your first post with Marble.", "coverImage": "https://cdn.marblecms.com/images/getting-started.png", "status": "published", "featured": false, "categoryId": "cat_cmf3cujdp0003l604w7h3whg9", "primaryAuthorId": "author_cmf3cvj1e0004l604x7m6g6fa", "publishedAt": "2026-05-22T14:00:00.000Z", "createdAt": "2026-05-21T18:12:09.431Z", "updatedAt": "2026-05-22T14:42:30.951Z" } } ``` The top-level envelope is the same for `post.published`, `post.updated`, `post.deleted`, `tag.created`, `category.updated`, `author.deleted`, and other JSON events. The `type`, `resource`, and `data` fields change to match the event. Post payloads include `id`, `title`, `slug`, `description`, `coverImage`, `status`, `featured`, `categoryId`, `primaryAuthorId`, `publishedAt`, `createdAt`, and `updatedAt`. Update events also include `changes`, an array of field names that changed. Category and tag payloads include `id`, `name`, `slug`, `description`, `createdAt`, and `updatedAt`. Author payloads include profile fields such as `name`, `slug`, `bio`, `role`, `image`, `email`, and `socials`. Media payloads use `name` and media metadata such as `url`, `alt`, `type`, `size`, `mimeType`, dimensions, duration, and `blurHash`. ## Event Types Marble supports a variety of event types that you can listen for with webhooks. Here are the currently available events: * `post.published`: Triggered when a post is published. * `post.unpublished`: Triggered when a post is unpublished. * `post.updated`: Triggered when a post is updated. * `post.deleted`: Triggered when a post is deleted. * `tag.created`: Triggered when a new tag is created. * `tag.updated`: Triggered when a tag is updated. * `tag.deleted`: Triggered when a tag is deleted. * `category.created`: Triggered when a new category is created. * `category.updated`: Triggered when a category is updated. * `category.deleted`: Triggered when a category is deleted. * `media.uploaded`: Triggered when a media file is uploaded. * `media.updated`: Triggered when a media file is updated. * `media.deleted`: Triggered when a media file is deleted. * `author.created`: Triggered when a new author is created. * `author.updated`: Triggered when an author is updated. * `author.deleted`: Triggered when an author is deleted. With more events planned for the future, you can stay tuned for updates in the Marble documentation. # Astro Integration Source: https://docs.marblecms.com/integrations/astro Integrate Marble with Astro using Content Collections and a custom SDK-powered Content Loader, with typed list pages and dynamic slug routes. To integrate Marble in your Astro application, you first need an Astro site. If you don't have one, you can check out the [Astro documentation](https://docs.astro.build/en/tutorial/0-introduction/) to get started, or use our [Astro blog template](https://github.com/usemarble/astro-example) for a ready-to-go solution. Astro's **Content Collections** are the best way to manage content in your project. We'll use a **Content Loader** to fetch your posts from Marble and make them available in your Astro site with full type-safety. You can learn more about this in the [Astro Content Collections documentation](https://docs.astro.build/en/guides/content-collections/). Install the Marble SDK in your Astro project: ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @usemarble/sdk ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add @usemarble/sdk ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add @usemarble/sdk ``` ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add @usemarble/sdk ``` Add your Marble API key to your environment variables. Create a `.env` file in the root of your Astro project: ```bash .env theme={"theme":{"light":"github-light","dark":"github-dark"}} MARBLE_API_KEY="your_api_key_here" ``` While public API keys are currently read-only, they should be used on the server-side whenever possible to prevent your [rate limits](/api/rate-limits) from being exhausted by others. You can find your API Key in your Marble dashboard under **Settings > API Keys**. Define your content collection with a loader that uses the Marble SDK. Create a file named `src/content.config.ts`: ```typescript src/content.config.ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import { defineCollection } from "astro:content"; import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: import.meta.env.MARBLE_API_KEY, }); const postsCollection = defineCollection({ loader: async () => { const result = await marble.posts.list({ limit: 100 }); const data = await result.next(); const posts = data.value?.posts ?? []; return posts.map((post) => ({ id: post.id, ...post, })); }, }); export const collections = { posts: postsCollection, }; ``` The SDK provides full TypeScript types out of the box, so you get type-safety without needing to define your own schemas. Use `getCollection()` to fetch all your posts and display them on a page. Create a file at `src/pages/blog/index.astro`: ```astro src/pages/blog/index.astro theme={"theme":{"light":"github-light","dark":"github-dark"}} --- import Layout from '../../layouts/Layout.astro'; import { getCollection } from 'astro:content'; const posts = await getCollection('posts'); ---

Blog

```
Create a dynamic route at `src/pages/blog/[...slug].astro` to display individual posts: ```astro src/pages/blog/[...slug].astro theme={"theme":{"light":"github-light","dark":"github-dark"}} --- import Layout from '../../layouts/Layout.astro'; import { getCollection } from 'astro:content'; export async function getStaticPaths() { const posts = await getCollection('posts'); return posts.map((entry) => ({ params: { slug: entry.data.slug }, props: { entry }, })); } const { entry } = Astro.props; ---

{entry.data.title}

Published on: {new Date(entry.data.publishedAt).toLocaleDateString()}

{entry.data.title}
``` **Rendering HTML Content** We use the `set:html` directive to render the post content. Marble sanitizes all HTML content on the server, ensuring it's safe to render directly in your application.
That's it! You now have a fully functional blog in your Astro project powered by Marble. For a complete, ready-to-use example, you can clone our [Astro blog template](https://github.com/usemarble/astro-example). # Framer Integration Source: https://docs.marblecms.com/integrations/framer Sync Marble posts into Framer CMS managed collections using the no-code Marble plugin — install, map fields, and re-run to keep content in sync. The Marble plugin syncs content from your Marble workspace into Framer CMS collections. No code required—you install the plugin from the Framer Marketplace, connect your workspace, and import with one click. Install the Marble plugin from the Framer Marketplace. ## Before you start * A Marble workspace with at least one published post * A Framer project and account * The Marble plugin installed from the Framer Marketplace Sign in at [marblecms.com](https://marblecms.com), go to **Settings → API Keys**, and create an API key. A **public (read-only)** key is all Framer needs, since the plugin only reads your content. Keep the key private—you'll enter it in the Framer plugin UI when configuring the connection. Add a CMS collection in your Framer project and attach the Marble plugin from the [Framer Marketplace](https://www.framer.com/marketplace/plugins/marble/). The plugin creates a **managed collection**—it controls the field structure and items, so there's no manual CMS setup. Enter your API key, choose a source (Posts, Categories, Tags, Authors, or Media), then map Marble fields to your CMS fields. You can keep the defaults, exclude fields you don't need, or rename them in Framer. Click **Import** when you're done. For posts, map the Marble **slug** to your Framer slug field. This keeps your Framer URLs aligned with the slugs you already manage in Marble. ## Features * One-click sync via the CMS toolbar * Map posts, categories, tags, authors, and media to CMS fields * Support for rich text, images, dates, and more * Simple workspace connection and field mapping ## Keeping content in sync Re-run the plugin whenever you add or update content in Marble. The plugin matches items by ID, so each sync **updates** existing entries, **adds** new ones, and **removes** items that no longer exist in your workspace. Prefer a detailed, step-by-step writeup? Read the complete Framer setup guide. # Next.js Integration Source: https://docs.marblecms.com/integrations/nextjs Connect Marble to a Next.js App Router project — install the SDK, configure env vars, render dynamic slug routes, and revalidate posts with ISR. Getting your content to your Next.js site is as simple as using the Marble SDK. This guide will walk you through setting up a blog with Marble and Next.js. If you want to get started quickly, you can clone our [Next.js blog template](https://github.com/usemarble/nextjs-example). Install the Marble SDK in your Next.js project: ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @usemarble/sdk ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add @usemarble/sdk ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add @usemarble/sdk ``` ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add @usemarble/sdk ``` Add your Marble API key to your environment variables. Create a `.env.local` file in the root of your Next.js project: ```bash .env.local theme={"theme":{"light":"github-light","dark":"github-dark"}} MARBLE_API_KEY=your_api_key_here ``` While public API keys are currently read-only, they should be used on the server-side whenever possible to prevent your [rate limits](/api/rate-limits) from being exhausted by others. You can find your API Key in your Marble dashboard under **Settings > API Keys**. Create a shared Marble client instance. Create a file at `lib/marble.ts`: ```typescript lib/marble.ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; export const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); ``` Use the Marble client in a Next.js page to fetch and display your posts: ```tsx app/blog/page.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}} import { marble } from "@/lib/marble"; // Revalidate this page every hour export const revalidate = 3600; export default async function BlogPage() { const result = await marble.posts.list({ limit: 10 }); // Get the first page of results const data = await result.next(); const posts = data.value?.posts ?? []; return (
); } ```
Create a dynamic route at `app/blog/[slug]/page.tsx` to display individual posts: ```tsx app/blog/[slug]/page.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}} import { marble } from "@/lib/marble"; export default async function PostPage({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const { post } = await marble.posts.get({ identifier: slug }); return (

{post.title}

); } ``` **A note on `dangerouslySetInnerHTML`** We use `dangerouslySetInnerHTML` to render the HTML content of your post. Marble sanitizes all HTML content before it's sent to you, so you can be confident that it's safe to render in your application.
To improve the appearance of your content, you can use Tailwind CSS Typography to add beautiful typographic defaults. We provide a simple `Prose` component that makes it easy to style your Marble content. First, install the Tailwind CSS Typography plugin: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install -D @tailwindcss/typography ``` Add it to your Tailwind CSS configuration. If you're using Tailwind CSS v4: ```css app/globals.css theme={"theme":{"light":"github-light","dark":"github-dark"}} @import "tailwindcss"; @plugin "@tailwindcss/typography"; ``` For Tailwind CSS v3, add it to your `tailwind.config.js`: ```javascript tailwind.config.js theme={"theme":{"light":"github-light","dark":"github-dark"}} module.exports = { theme: { // ... }, plugins: [ require("@tailwindcss/typography"), // ... ], }; ``` Now create a `Prose` component that you can use to style your content. Create `components/prose.tsx`: ```tsx components/prose.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}} import { cn } from "@/lib/utils"; import type { HTMLAttributes } from "react"; type ProseProps = HTMLAttributes & { as?: "article"; html: string; }; export function Prose({ children, html, className }: ProseProps) { return (
{html ?
: children}
); } ``` **Note:** This component assumes you have a `cn` utility function for combining class names. You can install and set up `clsx` or use a similar utility, or simply replace `cn()` with string concatenation. Now you can update your post page to use the `Prose` component: ```tsx app/blog/[slug]/page.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}} import { marble } from "@/lib/marble"; import { Prose } from "@/components/prose"; export default async function PostPage({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const { post } = await marble.posts.get({ identifier: slug }); return (

{post.title}

); } ``` This will give your content beautiful, readable typography with proper spacing, font sizes, and styling for headings, links, images, and other elements. You can customize the styles by modifying the classes in the `Prose` component or by extending the typography configuration in your Tailwind config. For more information about customizing the typography styles, check out [Tailwind CSS Typography](https://github.com/tailwindlabs/tailwindcss-typography)!
# TanStack Start Integration Source: https://docs.marblecms.com/integrations/tanstack Add Marble as a headless CMS to a TanStack Start app — install the SDK, fetch posts in route loaders, and render dynamic slug pages with full type safety. Getting your content to your TanStack Start site is as simple as using the Marble SDK. This guide will walk you through setting up a blog with Marble and TanStack Start. If you want to get started quickly, you can clone our [TanStack Start blog template](https://github.com/usemarble/tanstack-start-example). To create a new TanStack Start project, run: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npx create-start-app@latest ``` Install the Marble SDK in your TanStack Start project: ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @usemarble/sdk ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add @usemarble/sdk ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add @usemarble/sdk ``` ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add @usemarble/sdk ``` Add your Marble API key to your environment variables. Create a `.env` file in the root of your TanStack Start project: ```bash .env theme={"theme":{"light":"github-light","dark":"github-dark"}} MARBLE_API_KEY=your_api_key_here ``` While public API keys are currently read-only, they should be used on the server-side whenever possible to prevent your [rate limits](/api/rate-limits) from being exhausted by others. You can find your API Key in your Marble dashboard under **Settings > API Keys**. Create a shared Marble client instance. Create a file at `lib/marble.ts`: ```typescript lib/marble.ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; export const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); ``` Use the Marble client in a TanStack Start route to fetch and display your posts. Create a file at `src/routes/posts/index.tsx`: ```tsx src/routes/posts/index.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}} import { createFileRoute, Link } from "@tanstack/react-router"; import { marble } from "@/lib/marble"; export const Route = createFileRoute("/posts/")({ component: PostsPage, loader: async () => { const result = await marble.posts.list({ limit: 10 }); const data = await result.next(); return { posts: data.value?.posts ?? [] }; }, }); function PostsPage() { const { posts } = Route.useLoaderData(); if (!posts.length) return
No posts found
; return (

Posts

    {posts.map((post) => (
  • {post.title}
  • ))}
); } ```
Create a dynamic route at `src/routes/posts/$slug.tsx` to display individual posts: ```tsx src/routes/posts/$slug.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}} import { createFileRoute } from "@tanstack/react-router"; import { marble } from "@/lib/marble"; export const Route = createFileRoute("/posts/$slug")({ component: PostPage, loader: async ({ params }) => { return await marble.posts.get({ identifier: params.slug }); }, }); function PostPage() { const { post } = Route.useLoaderData(); if (!post) return
Post not found
; return (

{post.title}

); } ``` **A note on `dangerouslySetInnerHTML`** We use `dangerouslySetInnerHTML` to render the HTML content of your post. Marble sanitizes all HTML content before it's sent to you, so you can be confident that it's safe to render in your application.
To improve the appearance of your content, you can use Tailwind CSS Typography to add beautiful typographic defaults. We provide a simple `Prose` component that makes it easy to style your Marble content. First, install the Tailwind CSS Typography plugin: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install -D @tailwindcss/typography ``` Add it to your Tailwind CSS configuration. If you're using Tailwind CSS v4: ```css src/styles.css theme={"theme":{"light":"github-light","dark":"github-dark"}} @import "tailwindcss"; @plugin "@tailwindcss/typography"; ``` For Tailwind CSS v3, add it to your `tailwind.config.js`: ```javascript tailwind.config.js theme={"theme":{"light":"github-light","dark":"github-dark"}} module.exports = { theme: { // ... }, plugins: [ require("@tailwindcss/typography"), // ... ], }; ``` Now create a `Prose` component that you can use to style your content. Create `components/Prose.tsx`: ```tsx components/Prose.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}} import { cn } from "@/lib/utils"; import type { HTMLAttributes } from "react"; type ProseProps = HTMLAttributes & { as?: "article"; html: string; }; export function Prose({ children, html, className }: ProseProps) { return (
{html ?
: children}
); } ``` **Note:** This component assumes you have a `cn` utility function for combining class names. You can install and set up `clsx` or use a similar utility, or simply replace `cn()` with string concatenation. Now you can update your post page to use the `Prose` component: ```tsx src/routes/posts/$slug.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}} import { createFileRoute } from "@tanstack/react-router"; import { marble } from "@/lib/marble"; import { Prose } from "@/components/Prose"; export const Route = createFileRoute("/posts/$slug")({ component: PostPage, loader: async ({ params }) => { return await marble.posts.get({ identifier: params.slug }); }, }); function PostPage() { const { post } = Route.useLoaderData(); if (!post) return
Post not found
; return (

{post.title}

); } ``` This will give your content beautiful, readable typography with proper spacing, font sizes, and styling for headings, links, images, and other elements. You can customize the styles by modifying the classes in the `Prose` component or by extending the typography configuration in your Tailwind config. For more information about customizing the typography styles, check out [Tailwind CSS Typography](https://github.com/tailwindlabs/tailwindcss-typography)!
# What is Marble Source: https://docs.marblecms.com/introduction Marble is a simple, open-source headless CMS for writers and developers, with a clean editor, REST API, team collaboration, and built-in image hosting. **Marble is a simple, headless CMS built for writers and developers.** It helps you manage and publish content without the complexity of traditional CMS platforms. ## Why use Marble? A distraction-free writing experience with rich formatting and AI assistance. Pull your content into any frontend (Next.js, Astro, etc.) with a flexible REST API. Invite others to write, edit, and review posts in a shared workspace. Upload images directly to Marble or link from your own storage solution. ## Who is it for? * **Developers** who want to build custom sites without a bloated, monolithic CMS. * **Writers** who just want a clean and simple interface to write and publish. * **Small teams** that need a fast, collaborative way to manage content. **We're actively improving.** We ship fast and iterate based on feedback from users like you. ## Ready to get started? Follow our quickstart guide to set up your account and create your first post in minutes. # Set Up Your Workspace Source: https://docs.marblecms.com/quickstart Get started with Marble in minutes — sign up, create a workspace, explore the dashboard, and publish your first post before wiring it to a frontend. To get started, head over to [marblecms.com](https://marblecms.com) and click the **Sign Up** button. You can sign up with your email, Google, or a GitHub account. Marble sign-up page After signing up, you'll be prompted to create your first workspace. **What is a workspace?** Think of a workspace as a container for all the content related to a single project, like your personal blog or a company website. Each workspace holds its own posts, media, tags, categories, and team members. Give your workspace a name (you can always change it later) and click **Create Workspace**. Create a new workspace modal Once your workspace is ready, you'll be taken to the dashboard. This is your central hub for managing content. From here, you can create posts, upload media, and invite teammates. The Marble dashboard Click **New Post** to open the editor. The editor provides a clean, minimal interface where you can write with rich formatting, add images from your media library, and organize your content with tags and categories. When you're ready, you can save your post as a draft or publish it immediately. Marble does not currently have auto-save functionality. Make sure to save your work regularly when writing large articles to avoid losing content if the page refreshes. The Marble post editor That's it! You've successfully set up your Marble workspace and created your first post. 🚀 Now you're ready to connect your content to your frontend. Check out our integration guides to get started. For faster integration, add the [Context7 MCP server](https://context7.com/websites/marblecms) to your setup. # Syntax Highlighting Source: https://docs.marblecms.com/recipes/code-highlighting Add Shiki-powered syntax highlighting to code blocks in Marble posts, with a reusable highlighter, dual light and dark themes, and SDK integration. Your posts might include code blocks that you want to style with syntax highlighting. This recipe shows how to use [Shiki](https://shiki.style) to transform your Marble content with beautiful code highlighting. There are many syntax highlighting libraries available, such as [Prism.js](https://prismjs.com/), [highlight.js](https://highlightjs.org/), and [Shiki](https://shiki.style). This recipe uses Shiki. ## Install Dependencies ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install shiki ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add shiki ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add shiki ``` ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add shiki ``` ## Create a Highlighter Create a utility function that transforms HTML content with syntax highlighting: ```typescript lib/highlight.ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import { createHighlighter } from "shiki"; // Highlighter singleton let highlighter: Awaited> | null = null; async function getHighlighter() { if (!highlighter) { highlighter = await createHighlighter({ themes: ["github-dark", "github-light"], langs: ["javascript", "typescript", "html", "css", "json", "bash"], }); } return highlighter; } /** * Transform Marble HTML content to add syntax highlighting to code blocks */ export async function highlightContent(htmlContent: string): Promise { const hl = await getHighlighter(); // Marble returns code blocks as:
...
const codeBlockRegex = /
]*>([\s\S]*?)<\/code><\/pre>/g;

  return htmlContent.replace(codeBlockRegex, (match, language, code) => {
    try {
      // Decode HTML entities
      const decodedCode = code
        .replace(/</g, "<")
        .replace(/>/g, ">")
        .replace(/&/g, "&")
        .replace(/"/g, '"')
        .replace(/'/g, "'");

      const lang = language || "text";
      const supported = hl.getLoadedLanguages();
      const finalLang = supported.includes(lang) ? lang : "text";

      return hl.codeToHtml(decodedCode, {
        lang: finalLang,
        theme: "github-dark",
      });
    } catch (error) {
      console.warn("Failed to highlight code block:", error);
      return match;
    }
  });
}
```

## Use with the SDK

Apply the highlighter when fetching posts:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Marble } from "@usemarble/sdk";
import { highlightContent } from "./lib/highlight";

const marble = new Marble({
  apiKey: process.env["MARBLE_API_KEY"] ?? "",
});

async function getPost(slug: string) {
  const { post } = await marble.posts.get({ identifier: slug });

  return {
    ...post,
    content: await highlightContent(post.content),
  };
}
```

## Configuration

Shiki supports many themes and languages. Update the `createHighlighter` options to match your needs:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
highlighter = await createHighlighter({
  // Add more themes
  themes: ["github-dark", "github-light", "dracula", "nord"],
  // Add more languages
  langs: ["javascript", "typescript", "python", "rust", "go"],
});
```

For dual themes (light/dark mode), use:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
return hl.codeToHtml(decodedCode, {
  lang: finalLang,
  themes: {
    dark: "github-dark",
    light: "github-light",
  },
});
```


  For more configuration options, themes, and languages, see the [Shiki
  documentation](https://shiki.style).



# Reading Time
Source: https://docs.marblecms.com/recipes/reading-time

Calculate and display estimated reading time for Marble posts with a small TypeScript helper that strips HTML and counts words per minute.

Some blogs like to show an estimated reading time for each post. Marble provides these estimates inside the editor interface, but you can also display them on the frontend of your site.

## How It Works

The math behind reading time is fairly simple: it's based on an average reading speed of 238 words per minute (often rounded to 200 for simplicity).

## Implementation

This is a simple function you can use to calculate reading time for any given content:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
export function calculateReadTime(content: string): number {
  const wordsPerMinute = 238;
  const plainText = content.replace(/<[^>]*>/g, "").trim();
  const wordCount = plainText.split(/\s+/).length;

  const readingTime = Math.ceil(wordCount / wordsPerMinute);
  return readingTime;
}
```

To then use this function, simply pass in the HTML content of your post, and it will return the estimated reading time in minutes.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
const readTime = calculateReadTime("

Your post content goes here...

"); ``` You can then use that number to display the reading time on your post pages. Feel free to tweak the words-per-minute value to better suit your audience! # Marble MCP Server Source: https://docs.marblecms.com/tools/mcp Connect Claude, Cursor, and other Model Context Protocol clients to your Marble workspace to read, create, and manage content with AI agents. Use Marble's Model Context Protocol server to let AI agents read and manage content in your workspace through the Marble API. You need a Marble API key. Write tools such as create, update, and delete require a private API key. ## Installation The quickest way to add Marble to supported MCP clients is with the `add-mcp` CLI. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npx add-mcp https://mcp.marblecms.com/mcp \ --header "Mcp-Marble-Api-Key: $MCP_MARBLE_API_KEY" \ -g ``` This installs Marble globally for the agents you select. Without `-g`, `add-mcp` writes project-level MCP config. Open Cursor with the Marble MCP server pre-filled. Click **Install in Cursor** and allow your browser to open Cursor. Cursor installs the server with this header: ```txt theme={"theme":{"light":"github-light","dark":"github-dark"}} Mcp-Marble-Api-Key: ${MCP_MARBLE_API_KEY} ``` Set `MCP_MARBLE_API_KEY` in your environment before starting Cursor. Open Cursor's MCP settings and confirm that the `marble` server is enabled. The server URL should end with `/mcp`. You can also add the server manually in Cursor: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "mcpServers": { "marble": { "type": "streamableHttp", "url": "https://mcp.marblecms.com/mcp", "headers": { "Mcp-Marble-Api-Key": "${MCP_MARBLE_API_KEY}" } } } } ``` Open VS Code with the Marble MCP server pre-filled. Click **Install in VS Code** and allow your browser to open VS Code. Set `MCP_MARBLE_API_KEY` in your environment before starting VS Code. Use the MCP controls in VS Code to start or restart the `marble` server. You can also create `.vscode/mcp.json` manually: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "servers": { "marble": { "type": "http", "url": "https://mcp.marblecms.com/mcp", "headers": { "Mcp-Marble-Api-Key": "${MCP_MARBLE_API_KEY}" } } } } ``` Add the remote MCP server with: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} claude mcp add --transport http marble https://mcp.marblecms.com/mcp \ --header "Mcp-Marble-Api-Key: ${MCP_MARBLE_API_KEY}" ``` Set your API key in your shell before starting Claude Code: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export MCP_MARBLE_API_KEY="" ``` Add Marble to your Codex MCP configuration: ```toml theme={"theme":{"light":"github-light","dark":"github-dark"}} [mcp_servers.marble] command = "npx" args = [ "mcp-remote", "https://mcp.marblecms.com/mcp", "--header", "Mcp-Marble-Api-Key:${MCP_MARBLE_API_KEY}" ] [mcp_servers.marble.env] MCP_MARBLE_API_KEY = "" ``` Some MCP clients support remote Streamable HTTP servers directly: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "mcpServers": { "marble": { "type": "streamableHttp", "url": "https://mcp.marblecms.com/mcp", "headers": { "Mcp-Marble-Api-Key": "${MCP_MARBLE_API_KEY}" } } } } ``` Clients that only support local stdio servers can connect through `mcp-remote`: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "mcpServers": { "marble": { "command": "npx", "args": [ "mcp-remote", "https://mcp.marblecms.com/mcp", "--header", "Mcp-Marble-Api-Key:${MCP_MARBLE_API_KEY}" ], "env": { "MCP_MARBLE_API_KEY": "" } } } } ``` ## Available Tools Tool badges describe how clients may present or approve tool calls: * `READ-ONLY`: reads data without changing your workspace * `DESTRUCTIVE`: may update or delete existing content | Tool | Description | Badges | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | `get_posts` | Get a paginated list of published posts with optional filtering. | `READ-ONLY` | | `search_posts` | Search posts by title and content. Use this when an agent needs to find content before editing or linking to it. | `READ-ONLY` | | `get_post` | Get a single post by ID or slug, with optional status and content format filtering. | `READ-ONLY` | | `create_post` | Create a new post. Category is required. If authors are not provided, the first workspace author is used. Requires a private API key. | | | `update_post` | Update an existing post by ID or slug. All fields are optional, and only provided fields are updated. Requires a private API key. | `DESTRUCTIVE` | | `delete_post` | Delete a post by ID or slug. Requires a private API key. | `DESTRUCTIVE` | | Tool | Description | Badges | | ----------------- | -------------------------------------------------------------------------------------------------------------------- | ------------- | | `get_categories` | Get a paginated list of categories. | `READ-ONLY` | | `get_category` | Get a single category by ID or slug. | `READ-ONLY` | | `create_category` | Create a new category. Requires a private API key. | | | `update_category` | Update an existing category by ID or slug. Requires a private API key. | `DESTRUCTIVE` | | `delete_category` | Delete a category by ID or slug. Cannot delete a category that has posts assigned to it. Requires a private API key. | `DESTRUCTIVE` | | Tool | Description | Badges | | ------------ | ----------------------------------------------------------------- | ------------- | | `get_tags` | Get a paginated list of tags. | `READ-ONLY` | | `get_tag` | Get a single tag by ID or slug. | `READ-ONLY` | | `create_tag` | Create a new tag. Requires a private API key. | | | `update_tag` | Update an existing tag by ID or slug. Requires a private API key. | `DESTRUCTIVE` | | `delete_tag` | Delete a tag by ID or slug. Requires a private API key. | `DESTRUCTIVE` | | Tool | Description | Badges | | --------------- | ----------------------------------------------------------------------------------------------- | ------------- | | `get_authors` | Get a paginated list of authors who have published posts. | `READ-ONLY` | | `get_author` | Get a single author by ID or slug. | `READ-ONLY` | | `create_author` | Create a new author. Hobby plan workspaces are limited to 1 author. Requires a private API key. | | | `update_author` | Update an existing author by ID or slug. Requires a private API key. | `DESTRUCTIVE` | | `delete_author` | Delete an author by ID or slug. Requires a private API key. | `DESTRUCTIVE` | | Tool | Description | Badges | | ----------------------- | -------------------------------------------------------------------------------------- | ------------- | | `get_media` | Get a paginated list of media assets with optional type, search, and sort filters. | `READ-ONLY` | | `get_media_asset` | Get a single media asset by ID, including its CDN URL and metadata. | `READ-ONLY` | | `upload_media_from_url` | Upload media from a public URL into your Marble workspace. Requires a private API key. | | | `update_media` | Update media metadata such as name and alt text. Requires a private API key. | `DESTRUCTIVE` | | `delete_media` | Delete a media asset and its stored file. Requires a private API key. | `DESTRUCTIVE` | | Tool | Description | Badges | | -------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------- | | `get_fields` | Get all custom field definitions, including select and multiselect options. | `READ-ONLY` | | `get_field` | Get a single custom field by ID or key. | `READ-ONLY` | | `create_field` | Create a custom field definition. Requires a private API key. | | | `update_field` | Update a custom field by ID or key. Type and options cannot be changed after values have been saved. Requires a private API key. | `DESTRUCTIVE` | | `delete_field` | Delete a custom field and its saved values. Requires a private API key. | `DESTRUCTIVE` | ## Authentication The MCP server accepts your Marble API key through one of these headers: ```txt theme={"theme":{"light":"github-light","dark":"github-dark"}} Mcp-Marble-Api-Key: X-Marble-Api-Key: Authorization: Bearer ``` Keep private API keys out of client-side code and public repositories. ## Troubleshooting If your client cannot connect, verify that: * The URL ends with `/mcp` * The API key header is present * Your key has the permissions needed for the tool you are calling * Write operations use a private Marble API key # Marble TypeScript SDK Source: https://docs.marblecms.com/tools/sdk Official Marble TypeScript SDK with full type safety, automatic retries, pagination helpers, and framework-agnostic access to your workspace content. Developer-friendly & type-safe TypeScript SDK for the Marble API. Built with automatic retries, pagination helpers, and full TypeScript support. ## Installation ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install @usemarble/sdk ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add @usemarble/sdk ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add @usemarble/sdk ``` ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add @usemarble/sdk ``` This package is published with CommonJS and ES Modules (ESM) support. ## Quick Start ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); async function run() { const result = await marble.posts.list({ limit: 10, page: 1, }); for await (const page of result) { console.log(page); } } run(); ``` ## Authentication The SDK uses API key authentication. Pass your API key when initializing the client: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); ``` Set the `MARBLE_API_KEY` environment variable and the SDK will automatically use it. ## Available Resources * List all posts * Get a single post * List all categories * Get a single category * List all tags * Get a single tag * List all authors * Get a single author * List custom fields * Create and update field definitions * Write field values on posts ## Filtering Posts Filter posts by categories, tags, or featured status: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const result = await marble.posts.list({ featured: "true", categories: "tech,news", excludeCategories: "drafts", tags: "javascript,react", excludeTags: "outdated", }); ``` For a complete guide on filtering options and behavior, see the [Filtering documentation](/api/filtering). ## Pagination Paginated endpoints return an async iterable. Use `for await...of` to iterate through pages: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const result = await marble.posts.list({ limit: 10, page: 1, }); for await (const page of result) { console.log(page.posts); console.log(page.pagination); } ``` For full pagination details and response fields, see the [Pagination documentation](/api/pagination). ## Error Handling The SDK provides typed error classes for different error scenarios: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { Marble } from "@usemarble/sdk"; import * as errors from "@usemarble/sdk/models/errors"; const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", }); try { const result = await marble.posts.get("non-existent-slug"); } catch (error) { if (error instanceof errors.NotFoundError) { console.log("Post not found"); } else if (error instanceof errors.MarbleError) { console.log(error.message); console.log(error.statusCode); } } ``` ## Retries The SDK automatically retries failed requests. You can customize retry behavior: ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const marble = new Marble({ apiKey: process.env["MARBLE_API_KEY"] ?? "", retryConfig: { strategy: "backoff", backoff: { initialInterval: 1, maxInterval: 50, exponent: 1.1, maxElapsedTime: 100, }, retryConnectionErrors: false, }, }); ``` ## Framework Guides Use Content Collections with Astro. Sync content to Framer CMS with the Marble plugin. Build static and server-rendered pages. Server functions with TanStack Start. ## Additional Resources View the package on npm. View the source code.