Skip to Content
SDKHeadless client

Headless client

@ceros/markup-sdk-core is the headless transport layer that the browser SDK is built on: a typed ApiClient with retries, timeouts, auth refresh, and resource accessors — and no UI.

Most integrations should use @ceros/markup-sdk. Reach for markup-sdk-core only when you need raw, headless API access — server-side scripts, Node tooling, or a custom UI. Installing the browser SDK already pulls in the core client and re-exports its types.

Stability. Like the browser SDK, this package is published to the Ceros private registry and is unstable during its internal 1.0.0-beta.x phase — breaking changes are allowed between releases. The @ceros/markup-sdk-core/internal subpath is reserved for sibling packages and is not part of the public contract; don’t import from it.


Quickstart

ApiClient is the single entry point. Construct it once and use its resource accessors — never instantiate the resource classes or the internal HTTP client directly.

Create an ApiClient
import {ApiClient, StaticTokenAdapter} from "@ceros/markup-sdk-core"; const client = new ApiClient({ baseUrl: "https://api.markup.io", publicKey: process.env.MARKUP_PUBLIC_KEY, authAdapter: new StaticTokenAdapter(process.env.MARKUP_TOKEN, {workspaceId: "ws_123"}) }); const open = await client.threads.listOpen("project_abc"); for (const thread of open.data) { console.log(thread.id, thread.priority, thread.createdAt); }

Construction options

OptionTypeDescription
baseUrlstringRequired. Base URL of the MarkUp API (e.g. https://api.markup.io).
authAdapterIAuthAdapter | nullSupplies and refreshes sessions. Required for authenticated calls.
publicKeystringYour SDK installation public key, sent for identification.
apiVersionstringAPI version header. Default "2023-02-22".
clientNamestringClient name for telemetry (markup-client: sdk/<name>/<version>). Default "core".
timeoutnumberRequest timeout in ms. Default 30000.
retryPartial<RetryConfig>Retry policy (max retries, backoff, retryable status codes).

Authentication

Auth is supplied by an adapter passed at construction. The transport pulls sessions lazily, caches them until expiresAt, and transparently calls getSession(true) on a 401 to refresh before retrying the request once. You never set tokens imperatively.

Static token

For scripts or tests that already hold a token:

StaticTokenAdapter
import {StaticTokenAdapter} from "@ceros/markup-sdk-core"; const adapter = new StaticTokenAdapter("eyJhbGciOi...", {workspaceId: "ws_123"});

Custom adapter

Implement IAuthAdapter to plug in your own refresh strategy:

IAuthAdapter
import type {IAuthAdapter, AuthSession} from "@ceros/markup-sdk-core"; class MyAuthAdapter implements IAuthAdapter { private cached: AuthSession | null = null; async getSession(forceRefresh?: boolean): Promise<AuthSession> { if (!forceRefresh && this.cached && this.cached.expiresAt! > Date.now()) { return this.cached; } const res = await fetch("/api/markup-token"); const {accessToken, workspaceId, expiresAt} = await res.json(); this.cached = {accessToken, workspaceId, expiresAt}; return this.cached; } clear() { this.cached = null; } }
MemberSignatureDescription
getSessiongetSession(forceRefresh?: boolean): Promise<AuthSession>Resolve the current session. When forceRefresh is true, bypass the cache and fetch fresh credentials.
clearclear(): voidDrop any stored credentials and invalidate the cached session.
onSessionChangeonSessionChange?(listener): () => voidOptional. Register a listener invoked after each resolved session.

An AuthSession is {accessToken: string; workspaceId: string | null; expiresAt?: number} (expiresAt is millisecond epoch; omit for no expiry).


Resource APIs

Five resource groups are exposed on the client:

AccessorOperations
client.projectsget, getTagData, setReadOnly
client.threadslist, listOpen, listResolved, listByProject, get, create, resolve, unresolve, delete, movePin
client.commentslist, listByThread, create, update, delete
client.viewModeslist
client.uploadsgetUploadPolicy, completeUpload, directUpload
Threads and comments
const open = await client.threads.listOpen("project_abc", {limit: 50}); const page2 = await client.threads.listOpen("project_abc", {cursor: open.meta.cursor}); await client.comments.create({ threadId: "thread_123", content: "Looks good — shipping.", mentionIds: ["user_456"] });

threads.list({status: "all"}) merges two parallel requests and does not support cursor pagination. Pass "open" or "resolved" (or use listOpen / listResolved) for paginated results.


Error handling

Every HTTP error is a MarkUpError subclass. Narrow with instanceof and read code, status, requestId, and retryable for structured recovery.

Handle errors
import { MarkUpError, AuthenticationError, NotFoundError, RateLimitError, ValidationError } from "@ceros/markup-sdk-core"; try { await client.threads.create(request); } catch (err) { if (err instanceof ValidationError) { for (const fieldErr of err.errors) { console.warn(`${fieldErr.field}: ${fieldErr.message}`); } } else if (err instanceof RateLimitError) { await new Promise((r) => setTimeout(r, err.retryAfter * 1000)); } else if (err instanceof NotFoundError) { console.warn(`Missing ${err.resourceType}: ${err.resourceId}`); } else if (err instanceof MarkUpError) { console.error({code: err.code, status: err.status, requestId: err.requestId, retryable: err.retryable}); } else { throw err; } }

MarkUpError subclasses include ApiError, AuthenticationError, NotFoundError, ValidationError, RateLimitError, TimeoutError, and NetworkError. All carry code, status, requestId, retryable, and sdkVersion, plus a toJSON() serializer for telemetry.

Two helpers handle errors that can’t rely on instanceof (e.g. across realms):

  • isMarkUpError(err) — type guard for any MarkUpError.
  • canRetryError(err)true for retryable MarkUpErrors (and for a raw TypeError, treated as a network failure).

The SDKErrorCode enum also defines the browser sign-in codes — POPUP_BLOCKED, POPUP_CLOSED, WORKSPACE_FORBIDDEN, and SDK_SIGNIN_FAILED — surfaced by the browser SDK.


Subpath exports

Granular entry points for consumers that only need a subset of the package. Every symbol is also re-exported from the root.

SubpathContents
@ceros/markup-sdk-coreEverything below, re-exported.
@ceros/markup-sdk-core/clientThe runtime ApiClient and error classes.
@ceros/markup-sdk-core/typesType-only domain models (Thread, Comment, Project, …).
@ceros/markup-sdk-core/apiThe /api/v2 path and header constants.
@ceros/markup-sdk-core/utilsValidation and formatting helpers.
@ceros/markup-sdk-core/internalInternal. For sibling Ceros packages only — not covered by semver.
Subpath imports
import {ApiClient, MarkUpError} from "@ceros/markup-sdk-core/client"; import type {Thread, Comment, Project} from "@ceros/markup-sdk-core/types"; import {isValidUuid, formatRelativeTime} from "@ceros/markup-sdk-core/utils";

Next steps