SDK
Management client
Full typed access to the management API from @myna-sh/sdk/management, for servers and agents only.
@myna-sh/sdk/management exports createManagementClient, a typed wrapper over the entire management API following the management API conventions. It is server-only: it imports node:crypto and node:fs, and its token can mutate schema, entries, members, and billing. Never bundle it for a browser or expose its token to one — browser reads belong to the public client. See authentication for how to mint tokens and what scopes they carry.
Create a client
import { createManagementClient } from "@myna-sh/sdk/management";
const client = createManagementClient({ token: process.env.MYNA_TOKEN! });
| Option | Type | Default | Description |
|---|---|---|---|
token |
string |
— (required) | Management/CLI credential (myna_sk_...). |
apiUrl |
string |
https://api.myna.sh |
API origin; the client appends /v1. |
fetch |
FetchLike |
global fetch |
Custom fetch — also used for the presigned asset PUT. |
retry |
RetryOptions |
{ maxRetries: 3, baseDelayMs: 200, maxDelayMs: 5000 } |
Retry policy (see idempotency below). |
idempotencyKey |
() => string |
randomUUID per call |
Factory for the Idempotency-Key attached to every mutation. |
Namespace tour
| Namespace | Representative methods | Covers |
|---|---|---|
organizations |
list, create, get, update, delete, export, usage |
Organization lifecycle, data export, usage metering. |
members |
list, update, remove |
Organization membership and roles. |
invitations |
list, create, revoke, accept |
Inviting users; accept(token) redeems an invitation token. |
projects |
list, create, get, update, archive, export, corsCheck, access, views |
Projects within an organization; corsCheck and access diagnose a blank page and a denial respectively. |
schema |
collections, collection, versions, translations, diff, push |
Reading collection schemas, versions, and per-locale translation coverage; diff/push mirror myna schema push (with allowDestructive, changeSummary). |
entries |
list, create, get, update, delete, unpublish, restore, revisions, revision, restoreRevision |
Draft entries, revision history, restore. Writes stage into change sets. list accepts q for full-text search. |
changeSets |
list, create, get, update, diff, validate, publish, close, checks, runChecks, reportCheck |
The staging/publish unit; publish sends { confirm: true }, diff returns per-field before/after. |
checks |
list, declare, remove |
The external checks a project expects, and which block publishing. |
previews |
create, list, revoke |
Preview tokens for a change set or a single entry (max 7-day expiry). |
assets |
upload, createUpload, completeUpload, list, get, usage, similar, update, replace, delete |
Binary assets; upload runs the whole presigned flow, similar finds near-duplicate images. |
apiKeys |
listForOrganization, createForOrganization, revokeForOrganization, listForProject, createForProject, revokeForProject |
Org- and project-scoped API keys. |
webhooks |
list, create, update, delete, deliveries, retry |
Webhook endpoints, delivery history, redelivery. |
billing |
status, checkout, portal |
Billing state, checkout sessions, customer portal. |
activity() |
activity(project, { actorType?, action?, targetType?, from?, to?, limit?, cursor? }) |
Paginated audit log (a method, not a namespace). |
Most methods take the parent resource id/slug first (project or organization), then the resource id, then a typed body. Reads accept an optional AbortSignal.
Worked example: the agent-safe loop
Every entry write stages into a change set — nothing reaches the published surface until an explicit publish. This is the loop an agent should run: stage, validate, hand back a preview, and publish only when authorized.
const project = "my-site";
// 1. Open a change set to stage work in.
const changeSet = await client.changeSets.create(project, {
title: "March launch posts",
description: "Two posts plus hero image.",
});
// 2. Create an entry inside it.
const entry = await client.entries.create(project, {
collection: "posts",
slug: "march-launch",
data: { title: "March launch", body: "..." },
changeSetId: changeSet.id,
});
// 3. Upload an asset (create → presigned PUT → complete, in one call)
// and reference it from the entry.
const hero = await client.assets.upload(project, "./hero.png");
await client.entries.update(project, entry.id, {
data: { ...entry.data, heroImage: hero.id },
expectedRevisionId: entry.draftRevisionId ?? undefined,
changeSetId: changeSet.id,
});
// 4. Validate the whole change set against the current schema.
const result = await client.changeSets.validate(project, changeSet.id);
if (!result.valid) throw new Error(JSON.stringify(result.errors, null, 2));
// 5. Mint a preview for human review.
const preview = await client.previews.create(project, {
changeSetId: changeSet.id,
expiresInSeconds: 86_400,
});
console.log("Review at:", preview.url);
// 6. Publish — only with the content:publish scope and explicit approval.
const { publishedEntryIds } = await client.changeSets.publish(project, changeSet.id);
validate returns { valid, errors } where each error names resourceType, resourceId, path, message, and an optional code. previews.create takes exactly one of changeSetId or entryId. Publishing requires the content:publish scope; tokens without it can run the entire loop through step 5, which is the recommended posture for autonomous agents — see change sets.
assets.upload(project, input, meta?) accepts a filesystem path (string), Uint8Array, or ArrayBuffer. It computes an MD5 checksum, guesses the content type from the filename (override with meta.contentType / meta.filename / meta.byteSize), then performs create → presigned PUT → completeUpload and returns the finished AssetContract. The presigned PUT goes to the storage host directly and throws a plain Error (not MynaApiError) on failure.
Optimistic concurrency
entries.update accepts expectedRevisionId. Pass the revision you last read; if someone else has written in the meantime the API rejects the update with a 409 conflict instead of silently overwriting:
import { isMynaApiError } from "@myna-sh/sdk/management";
const current = await client.entries.get(project, entryId);
try {
await client.entries.update(project, entryId, {
data: { ...current.data, title: "Updated title" },
expectedRevisionId: current.draftRevisionId ?? undefined,
});
} catch (error) {
if (isMynaApiError(error) && error.status === 409) {
// Re-read, re-apply, retry — or surface the conflict.
} else {
throw error;
}
}
Always send expectedRevisionId from agents: it turns lost-update races into explicit, recoverable conflicts.
Pagination
Paginated surfaces — entries.list, changeSets.list, assets.list, and activity() — return Page<T>: { data, nextCursor }, with nextCursor: null on the last page. The standard drain loop:
const all = [];
let cursor: string | undefined;
do {
const page = await client.entries.list(project, {
collection: "posts",
status: "draft",
limit: 100,
cursor,
});
all.push(...page.data);
cursor = page.nextCursor ?? undefined;
} while (cursor);
List-shaped methods without cursors (organizations.list, schema.collections, previews.list, webhooks.list, entries.revisions, and similar) return plain arrays.
Idempotency and retries
Every mutation automatically carries an Idempotency-Key header, generated by the idempotencyKey factory (default: a fresh randomUUID() per call). Because the key is present, mutations are eligible for the same automatic retry policy as reads: statuses 408, 425, 429, 500, 502, 503, 504 and network failures are retried up to maxRetries times with exponential backoff and jitter, honoring Retry-After. The server deduplicates replays by key, so a retried create cannot double-create.
To make a mutation replay-safe across process restarts (not just across in-process retries), supply a stable key derived from your own operation identity:
const client = createManagementClient({
token: process.env.MYNA_TOKEN!,
idempotencyKey: () => `import-posts-${jobId}`,
});
Note the factory is client-wide and called once per mutation — use a per-operation client, or vary the value it returns, when different mutations need different stable keys.
Typed errors
Failed API calls throw the same MynaApiError as the public client, re-exported from @myna-sh/sdk/management alongside isMynaApiError. Branch on the stable error.code (see error codes), inspect error.fields for field-level validation failures, and log error.requestId for support:
try {
await client.entries.create(project, { collection: "posts", data: {} });
} catch (error) {
if (isMynaApiError(error) && error.code === "VALIDATION_FAILED") {
for (const field of error.fields ?? []) {
console.error(`${field.path}: ${field.message}`);
}
} else {
throw error;
}
}
