@craftware/crafty-sdk
v0.2.1
Published
Official JavaScript SDK for Crafty CMS public API
Readme
@craftware/crafty-sdk
Official JavaScript SDK for the Crafty public API (current local implementation).
AI agent skill
This package ships an agent skill that teaches Claude, Cursor, and other agents how to configure a Crafty site through its MCP server and build against it with this SDK. It is included in the published package under skills/crafty-cms/.
To activate it, copy it into your project's skills directory after installing the SDK:
cp -r node_modules/@craftware/crafty-sdk/skills/crafty-cms .claude/skills/(Use the equivalent skills directory for your agent, e.g. .cursor/skills/.) The skill is self-contained — it includes SKILL.md plus references/mcp-configuration.md and references/sdk-development.md.
Requirements
- Node
>=20(package engine) - Or any runtime with
fetchavailable (browser, Bun, Deno, etc.)
Create a client
import { createCraftyClient } from "@craftware/crafty-sdk";
const crafty = createCraftyClient({
baseUrl: "https://tenant.crafty.do",
apiKey: "ENVIRONMENT_API_TOKEN",
timeoutMs: 10_000
});Client config
baseUrl(required): tenant base URL. The SDK appends/api/v1automatically if missing.apiKey(optional): environment API token sent asX-API-Key.- When provided, the API resolves the active site from this token.
pages.*requests use this site context automatically (you do not passsiteSlug).
siteSlug(optional): site slug used for public form submissions (forms.submit()).fetch(optional): custom fetch implementation.headers(optional): default headers applied to all requests.timeoutMs(optional): default timeout (ms) for all requests. A timeout is only applied when the value is a number greater than0.
Available resources
collections
crafty.collections.list(options?)crafty.collections.get(slug, options?)
entries
crafty.entries.list(collectionSlug, query?, options?)crafty.entries.get(collectionSlug, id, options?)
Supported common query params:
sort_by(any sortable field, including collection fields liketitle)sort_order(asc|desc)per_pagepage- Additional filters as key/value pairs (passed through to the API)
pages
crafty.pages.list(query?, options?)crafty.pages.get(pageSlug, options?)
pages.* endpoints are scoped by the environment resolved from apiKey.
pages.get() returns a page response enriched with block helpers:
page.data=> the rawCraftyPagepage.blocks=> all blocks (CraftyBlock[]; a non-arraydata.blocksis coerced to[])page.block(type)=> first block of that type ornullpage.blocksOf(type)=> all blocks of that type ([]if none)page.hasBlock(type)=> booleanpage.requireBlock(type)=> first block, or throws a plainError(not aCraftyError)page.byType.<type>=> array of blocks for that typepage.firstByType.<type>=> first block for that type ornull
forms
crafty.forms.list(options?)— list all forms (requires API key)crafty.forms.get(slug, options?)— get a single form by slug (requires API key)crafty.forms.submit(formSlug, data, options?)— submit a public form (no API key needed)
The submit() method uses a public endpoint that does not require authentication. You must provide siteSlug in the client config for submissions to work — calling submit() without it throws a synchronous CraftyError with code BAD_REQUEST:
const crafty = createCraftyClient({
baseUrl: "https://tenant.crafty.do",
apiKey: "ENVIRONMENT_API_TOKEN",
siteSlug: "my-site",
});
// List forms (authenticated)
const forms = await crafty.forms.list();
// Get form details (authenticated)
const contactForm = await crafty.forms.get("contact");
// Submit a form (public, no API key sent)
const result = await crafty.forms.submit("contact", {
name: "Jane Doe",
email: "[email protected]",
message: "Hello!",
});
// result => { id, form_id, submitted_at }mediaCollections
crafty.mediaCollections.list(query?, options?)
Supported query.include values:
"stats""directories"- Array form is supported, de-duplicated, and serialized as comma-separated values; a raw string is passed through unchanged
mediaFiles
crafty.mediaFiles.list(query?, options?)
Supported common query params:
collectiondirectory_idttl_minutesper_pagepage
Request options (per call)
Every method accepts options? as the last argument:
signal:AbortSignalheaders: request-specific headers (merged with client headers)timeoutMs: overrides client timeout for that call
const posts = await crafty.entries.list(
"posts",
{ per_page: 15, page: 1, sort_by: "updated_at", sort_order: "desc" },
{ timeoutMs: 5_000 }
);Responses (current shapes)
collections.list()=>{ data: CraftyCollection[] }collections.get()=>CraftyCollectionentries.list()=> paginated{ data, links, meta }entries.get()=>{ data: CraftyEntry }pages.list()=> paginated{ data, links, meta }pages.get()=>{ data: CraftyPage }plus the block helpers listed abovemediaCollections.list()=>{ data, meta: { includes } }mediaFiles.list()=>{ data, links, meta }(metaincludesttl_minutes)forms.list()=>{ data: CraftyForm[] }forms.get()=>{ data: CraftyForm }forms.submit()=>{ id, form_id, submitted_at }
Errors
The SDK throws CraftyError for HTTP, timeout, abort, network, and configuration failures (e.g. no fetch implementation available at construction).
import { CraftyError } from "@craftware/crafty-sdk";
try {
await crafty.pages.get("home");
} catch (error) {
if (error instanceof CraftyError) {
console.error(error.code, error.status, error.message);
console.error(error.requestId); // if API sent x-request-id
}
}Current error codes (with their HTTP-status mapping):
NETWORK_ERROR— network failure, or nofetchavailable at constructionTIMEOUT— request exceededtimeoutMsBAD_REQUEST— HTTP 400 or 422UNAUTHORIZED— HTTP 401FORBIDDEN— HTTP 403NOT_FOUND— HTTP 404RATE_LIMITED— HTTP 429SERVER_ERROR— HTTP 5xxUNKNOWN_ERROR— any other / unmapped status
End-to-end example
import { createCraftyClient } from "@craftware/crafty-sdk";
const crafty = createCraftyClient({
baseUrl: "https://tenant.crafty.do",
apiKey: process.env.CRAFTY_API_KEY,
siteSlug: "my-site",
timeoutMs: 10_000
});
const collections = await crafty.collections.list();
const posts = await crafty.entries.list("posts", { per_page: 15 });
const mediaCollections = await crafty.mediaCollections.list({ include: ["stats", "directories"] });
const mediaFiles = await crafty.mediaFiles.list({ collection: "library", per_page: 24 });
const homePage = await crafty.pages.get("home");
const forms = await crafty.forms.list();
const submission = await crafty.forms.submit("contact", { name: "Jane", email: "[email protected]" });Pages scoping (site+environment via API key)
pages requests no longer require a siteSlug parameter. The site is inferred server-side from the
X-API-Key header (set from apiKey in createCraftyClient()).
const crafty = createCraftyClient({
baseUrl: "https://tenant.crafty.do",
apiKey: "ENVIRONMENT_API_TOKEN",
});
await crafty.pages.list({ per_page: 12 });
const landing = await crafty.pages.get("landing");
const processBlock = landing.block("process");
const allProcessBlocks = landing.blocksOf("process");
const processFirst = landing.firstByType.process;
const processList = landing.byType.process ?? [];Notes
- The SDK normalizes
baseUrland avoids duplicating/api/v1. Accept: application/jsonis set automatically unless overridden.src/types/generated.tsis still a placeholder until OpenAPI codegen is wired.- Public
blockTypesendpoints are not exposed yet; block-related types are currently hand-modeled.
