Management API

Conventions

Authentication, envelopes, pagination, idempotency, optimistic concurrency, and a tour of every route group in the Management API.

The Management API is the write side of Myna: schema pushes, drafts, change sets, publishing, assets, webhooks, and organization administration. Everything lives under https://api.myna.sh/v1 and follows a small set of uniform conventions. The complete machine-readable surface is the OpenAPI 3.1 document at /openapi.json.

Authentication

Send an API key as a bearer token:

$ curl -H "Authorization: Bearer myna_sk_..." \
    https://api.myna.sh/v1/projects/my-site/entries

Keys carry explicit scopes; each route requires a specific capability (for example content:write to create an entry, project:admin to manage keys). A request with a valid key but a missing scope fails with PERMISSION_DENIED. Browser sessions from the dashboard work on the same routes, with capabilities derived from the member's role — see Authentication.

If you already have the CLI authenticated, myna api calls any route on this page with the credential it already resolved, and renders the errors below the way the rest of the CLI does:

$ myna api /projects/my-site/entries?collection=posts

The SDK has the same escape hatch as myna.raw(method, path). Both are documented under using the CLI.

Envelopes

Every successful response wraps its payload:

{ "data": { ... } }

List endpoints add a pagination block:

{ "data": [ ... ], "pagination": { "nextCursor": "eyJrIjoi..." } }

Pagination

Lists use cursor pagination. limit is clamped to 1–100 and defaults to 25; cursor is the opaque nextCursor from the previous page. nextCursor is null on the last page. Cursors encode the sort position internally — treat them as opaque strings, and expect VALIDATION_FAILED for malformed ones. Pages do not overlap: following nextCursor walks every row exactly once.

Filters such as ?status= on entry lists are applied before the page is cut, so a page is full whenever more matching rows exist and nextCursor reflects the filtered result rather than the unfiltered one.

$ curl -H "Authorization: Bearer myna_sk_..." \
    "https://api.myna.sh/v1/projects/my-site/entries?limit=50&cursor=eyJrIjoi..."

Idempotency

All mutating methods (POST, PATCH, PUT, DELETE) accept an Idempotency-Key header. The first response with a given key is persisted for 24 hours, keyed by the authenticated actor, the key, and the route; retries with the same key on the same route replay the stored status and body verbatim instead of re-executing. Responses with 5xx status are not stored, so a retry after a server error executes normally. Anonymous requests are never idempotency-tracked.

The key is claimed before the handler runs, so concurrency is handled rather than raced: if a second request arrives with a key whose first request has not finished, it receives 409 with code CONFLICT and a message saying the request is already in flight. Retry once the original completes and you will get the stored response. This is what makes a client-side retry of a request whose response was lost safe — without it, two concurrent retries of POST /projects/:project/entries would each create an entry.

$ curl -X POST -H "Authorization: Bearer myna_sk_..." \
    -H "Idempotency-Key: 018f3b1e-create-post" \
    -H "Content-Type: application/json" \
    -d '{"collection":"posts","fields":{"title":"Hello"}}' \
    https://api.myna.sh/v1/projects/my-site/entries

Optimistic concurrency

Entry updates accept an expectedRevisionId. If it no longer matches the entry's current draft revision, the API responds 409 with code STALE_REVISION and a details object containing expectedRevisionId and currentRevisionId — re-read, merge, and retry. This is the primary guard against two agents clobbering each other's edits.

{
  "type": "https://myna.sh/errors/stale-revision",
  "title": "Stale revision",
  "status": 409,
  "code": "STALE_REVISION",
  "details": { "expectedRevisionId": "rev_a", "currentRevisionId": "rev_b" }
}

Errors

All errors are RFC 9457 problem details (application/problem+json) with a stable machine-readable code, a requestId, and — for validation failures — a per-field errors array. See error codes for the full catalog.

Route groups

A tour of the surface. All paths are relative to https://api.myna.sh/v1.

Group Routes Purpose
Meta GET /meta Public and unauthenticated: the API version, the oldest client release it supports, and the capabilities a client can branch on. See versioning and compatibility.
Organizations GET/POST /organizations, GET/PATCH/DELETE /organizations/:organization, POST .../export, GET .../members, PATCH/DELETE .../members/:user, GET/POST .../invitations, DELETE .../invitations/:invitation, POST /invitations/:token/accept Tenancy: orgs, membership roles, invitations, data export.
Projects GET/POST /organizations/:organization/projects, GET/PATCH /projects/:project, POST /projects/:project/archive, GET /projects/:project/export, GET /projects/:project/cors-check, GET /projects/:project/access, GET/POST /projects/:project/views, DELETE .../views/:view Project lifecycle and settings (slug, origins, public API, locales, publish gates), full JSON export, browser-access diagnosis, denial diagnosis, and saved entry views.
Schema GET /projects/:project/collections, GET .../collections/:collection, GET .../collections/:collection/versions, GET .../collections/:collection/translations, POST /projects/:project/schema/diff, POST /projects/:project/schema/push, GET /projects/:project/schema/drift, POST /projects/:project/schema/promote Code-owned schemas: inspect collections, diff a proposed schema, push immutable versions, report translation coverage, and compare or promote between environment projects.
Entries GET/POST /projects/:project/entries, GET/PATCH/DELETE .../entries/:entry, POST .../unpublish, POST .../restore, POST .../duplicate, POST /projects/:project/entries/bulk, POST /projects/:project/entries/import, POST /projects/:project/collections/:collection/reorder, GET .../references, GET .../revisions, GET .../revisions/:revision, POST .../revisions/:revision/restore Draft content: every write creates a revision on a change set. Bulk delete/unpublish, duplication, import with dry-run, editorial ordering, reference backlinks, and full-text search with ?q=.
Change sets GET/POST /projects/:project/change-sets, GET/PATCH .../:changeSet, GET .../diff, POST .../validate, POST .../publish, POST .../rebase, POST .../close, POST/DELETE .../schedule, GET/POST .../reviews, DELETE .../reviews/:reviewer, POST .../approve, POST .../request-changes, GET/POST .../comments, PATCH .../comments/:comment, GET .../checks, POST .../checks/run, POST .../checks/:name Group, diff, validate, review, check, schedule, rebase, and atomically publish batches of edits.
Releases GET /projects/:project/releases, GET .../releases/:release, POST .../releases/:release/revert Read what shipped, and generate a reviewable change set that restores the state before a release.
Checks GET/POST /projects/:project/checks, DELETE .../checks/:name Declare the external checks a project expects, and which of them block publishing.
Previews POST/GET /projects/:project/previews, DELETE .../:preview, GET /previews/resolve Mint and resolve read-only preview tokens for unpublished content.
Assets POST /projects/:project/assets/uploads, POST .../uploads/:upload/complete, GET /projects/:project/assets, GET/PATCH/DELETE .../assets/:asset, POST .../assets/:asset/replace Two-step uploads plus asset metadata (alt, caption, tags), identity-preserving replace, and orphan/checksum list filters.
API keys GET/POST /organizations/:organization/api-keys, DELETE .../api-keys/:key, GET/POST /projects/:project/api-keys, DELETE .../api-keys/:key Mint and revoke org- or project-scoped keys with explicit scopes.
Webhooks GET/POST /projects/:project/webhooks, PATCH/DELETE .../:webhook, GET .../:webhook/deliveries, POST .../deliveries/:delivery/retry Signed event delivery with per-delivery inspection and retry.
Activity GET /projects/:project/activity The project audit feed: who changed what, when, with which actor type.
Billing GET /organizations/:organization/billing, GET .../usage, POST .../billing/checkout, POST .../billing/portal Plan status, metered usage, and checkout/portal sessions. See plans.

Invitations

POST /organizations/:organization/invitations records the invitation and returns it — but never its token. The token only ever exists inside the email Myna sends to the invited address, so the emailed link is the one way to accept.

Delivery is a durable job, not part of the request. A provider outage retries with backoff for roughly a day rather than dropping the invitation, and each attempt mints a fresh token: if two emails arrive, only the most recent link resolves. Revoking an invitation before the job runs cancels the send.

Accepting is a separate, authenticated call. The recipient signs in, the dashboard POSTs to /invitations/:token/accept, and the API checks that the signed-in account owns the invited address — a mismatch fails with PERMISSION_DENIED rather than joining the wrong user. Expired, revoked, and already-accepted tokens are all NOT_FOUND, so a used link reveals nothing about whether it ever existed.

Invitations expire after seven days, and creation is rate-limited per organization since it sends mail to addresses that never opted in.