Two consumer-side clients ship in the box, both intentionally read-first. Writes go through rpc() β€” the same dispatch as the HTTP POST /rpc β€” because most consumers are renderers, not editors. Editing happens through the admin GUI, the inline editor, or an MCP-connected agent.


@ledric/sdk (TypeScript)

Install:

npm install @ledric/sdk
# pnpm add @ledric/sdk
# yarn add @ledric/sdk

Construct:

import { LedricClient } from '@ledric/sdk';

const client = new LedricClient({
  baseUrl: 'https://cms.example.com',
  apiKey: process.env.LEDRIC_READER_KEY  // optional under default reads-open mode
});

Options:

Option Type Notes
baseUrl string Origin of your ledric server. Required.
apiKey string API key. Optional under reads-open mode.
headers object Extra headers merged into every request.
fetch function Custom fetch implementation (e.g. node-fetch for old Node, or a tracing-instrumented fetch).

Reading content

// Single entry
const post = await client.read({ type: 'blog_post', slug: 'why-kysely' });
// or as a string:
const post = await client.read('blog_post/why-kysely');

// With options
const post = await client.read('blog_post/why-kysely', {
  expandAssets: true,           // inline asset metadata + URLs
  expandAssets: ['hero'],       // or just specific fields
  resolveRefs: true,            // walk markdown for :::ref{}
  version: 3,                   // historical version
  locale: 'fr'                  // localized projection
});

// Returns null on 404. Follows slug-rename redirects transparently.
if (!post) { /* ... */ }

console.log(post.fields.title);

Listing

const { results, total, offset } = await client.find('blog_post', {
  tags: ['featured'],           // AND semantics
  limit: 10,
  offset: 0,
  locale: 'fr',
  expandAssets: true
});

Generate types from the live schema

ledric types reads your schema and writes a ledric.types.ts file next to your ledric.config.json. Re-run after any create_type / alter_type to keep consumer code in sync β€” every content type becomes an interface, every references field a typed EntryRef<T>[], every asset a branded AssetId, every date a DateString.

ledric types                        # writes ./ledric.types.ts (reads local DB)
ledric types --from http://127.0.0.1:3000   # … via HTTP against a remote ledric
ledric types --augment-sdk          # … plus a declare-module block so
                                    # client.read<'blog_post'>('hello')
                                    # picks up your shapes automatically
ledric types --stdout               # … print to stdout instead of writing

Use the generated types either by passing the interface as a generic:

import type { BlogPost, Entries } from './ledric.types';

const post = await client.read<BlogPost>('blog_post/why-kysely');
post?.fields.title;                 // string β€” typed

…or, with --augment-sdk enabled, by letting the type-name string drive inference:

const post = await client.read<'blog_post'>('blog_post/why-kysely');
// post.fields.title β€” typed automatically

Types

const model = await client.types();           // every type's full def
const blogPost = await client.type('blog_post'); // one type, or null

Assets

const meta = await client.asset(refKeyOrId);  // metadata
const url = client.assetUrl(refKeyOrId, {     // build a URL with imgix params
  w: 800,
  fm: 'webp',
  auto: 'format'
});
const bytes = await client.assetBytes(refKeyOrId); // ArrayBuffer

const list = await client.assets({ kind: 'image', tags: ['hero'] });

Tags

await client.tags();                           // all tags + counts
await client.addEntryTags({ type: 'blog_post', slug: 'why-kysely' }, ['featured']);
await client.removeEntryTags({ type: 'blog_post', slug: 'why-kysely' }, ['draft']);
await client.addAssetTags(assetId, ['hero']);
await client.updateTag('featured', 'Featured Posts');

Writes (via rpc)

The TS SDK exposes a generic rpc() for everything else. Same shape as the HTTP POST /rpc and the MCP tool catalogue.

const draft = await client.rpc('draft', {
  type: 'blog_post',
  fields: { title: 'Hello', slug: 'hello', body: '# Hi' }
});

await client.rpc('publish', {
  ref: { type: 'blog_post', slug: 'hello' }
});

await client.rpc('alter_type', {
  name: 'blog_post',
  parent_version: 3,
  merge_patch: { fields: { reading_time: { type: 'number', integer: true } } }
});

See MCP tools for every tool name and arg shape.

Errors

LedricApiError (extends Error) is thrown on non-2xx responses. It carries:

try {
  await client.rpc('draft', { /* ... */ });
} catch (err) {
  if (err instanceof LedricApiError) {
    err.status;     // HTTP status
    err.code;       // ledric error code (VALIDATION_FAILED, VERSION_CONFLICT, ...)
    err.errors;     // [{ path, message }] on validation failures
  }
}

Ledric\LedricClient (PHP)

Install:

composer require ledric/sdk

Construct:

use Ledric\LedricClient;

$client = new LedricClient('https://cms.example.com', [
    'apiKey' => getenv('LEDRIC_READER_KEY')
]);

Method surface mirrors the TS SDK:

$post = $client->read('blog_post/why-kysely');
$post = $client->read('blog_post/why-kysely', [
    'expandAssets' => true,
    'resolveRefs' => true,
    'locale' => 'fr'
]);

$list = $client->find('blog_post', [
    'tags' => ['featured'],
    'limit' => 10
]);

$model = $client->types();
$type  = $client->type('blog_post');

$asset = $client->asset($refKeyOrId);
$url   = $client->assetUrl($refKeyOrId, ['w' => 800, 'fm' => 'webp']);
$bytes = $client->assetBytes($refKeyOrId);

$client->addEntryTags(['type' => 'blog_post', 'slug' => 'why-kysely'], ['featured']);
$client->updateTag('featured', 'Featured Posts');

// Writes via generic rpc()
$client->rpc('draft', [
    'type' => 'blog_post',
    'fields' => ['title' => 'Hello', 'slug' => 'hello', 'body' => '# Hi']
]);

PHP-specific notes:

  • Uses ext-curl and ext-json. No Guzzle / no extra HTTP layer.
  • Returns associative arrays (not objects) for entries / types / assets β€” so $post['fields']['title'] rather than $post->fields->title.
  • null on 404 (same shape as the TS SDK).
  • Throws Ledric\LedricApiError on non-2xx β€” same fields (status, code, errors).

Inline editor helpers

Both SDKs export a refAttrs() helper for building the data-ledric-ref / data-ledric-field attributes the inline editor uses. See Inline editor for the full walkthrough.

// TypeScript / JSX
import { refAttrs } from '@ledric/sdk';

<article {...refAttrs(post)}>
  <h1 {...refAttrs(post, 'title')}>{post.fields.title}</h1>
</article>
// PHP
<article <?= $client->refAttrs($post) ?>>
  <h1 <?= $client->refAttrs($post, 'title') ?>><?= htmlspecialchars($post['fields']['title']) ?></h1>
</article>

Returns empty (object / string) when the entry is null β€” safe to spread/inject without conditional logic.