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 formarkup-sdk-coreonly 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.xphase — breaking changes are allowed between releases. The@ceros/markup-sdk-core/internalsubpath 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.
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
| Option | Type | Description |
|---|---|---|
baseUrl | string | Required. Base URL of the MarkUp API (e.g. https://api.markup.io). |
authAdapter | IAuthAdapter | null | Supplies and refreshes sessions. Required for authenticated calls. |
publicKey | string | Your SDK installation public key, sent for identification. |
apiVersion | string | API version header. Default "2023-02-22". |
clientName | string | Client name for telemetry (markup-client: sdk/<name>/<version>). Default "core". |
timeout | number | Request timeout in ms. Default 30000. |
retry | Partial<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:
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:
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;
}
}| Member | Signature | Description |
|---|---|---|
getSession | getSession(forceRefresh?: boolean): Promise<AuthSession> | Resolve the current session. When forceRefresh is true, bypass the cache and fetch fresh credentials. |
clear | clear(): void | Drop any stored credentials and invalidate the cached session. |
onSessionChange | onSessionChange?(listener): () => void | Optional. 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:
| Accessor | Operations |
|---|---|
client.projects | get, getTagData, setReadOnly |
client.threads | list, listOpen, listResolved, listByProject, get, create, resolve, unresolve, delete, movePin |
client.comments | list, listByThread, create, update, delete |
client.viewModes | list |
client.uploads | getUploadPolicy, completeUpload, directUpload |
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 uselistOpen/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.
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 anyMarkUpError.canRetryError(err)—truefor retryableMarkUpErrors (and for a rawTypeError, 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.
| Subpath | Contents |
|---|---|
@ceros/markup-sdk-core | Everything below, re-exported. |
@ceros/markup-sdk-core/client | The runtime ApiClient and error classes. |
@ceros/markup-sdk-core/types | Type-only domain models (Thread, Comment, Project, …). |
@ceros/markup-sdk-core/api | The /api/v2 path and header constants. |
@ceros/markup-sdk-core/utils | Validation and formatting helpers. |
@ceros/markup-sdk-core/internal | Internal. For sibling Ceros packages only — not covered by semver. |
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
- API reference — the higher-level browser SDK built on this client.
- Authentication — how sessions and tokens are issued.