Guest API
Everything a view calls funnels through one client — @conexus-x/sdk’s default export. No fetch, no token and no CRM base URL anywhere in this package on purpose: a view that cannot address the API directly is a view that cannot be tricked into addressing it with someone else’s credentials.
import conexus from "@conexus-x/sdk";
const cx = conexus();
const { context, settings, grantedScopes } = await cx.connect();Connection
| Call | What it does |
|---|---|
cx.connect() | Handshake. Resolves { context, settings, grantedScopes, hostVersion }. Safe to call from ten components — one handshake happens. |
cx.context | The last context the host sent, kept current by pushes. null before connect. |
cx.hasScope(scope) | Feature-detect. Never assume a scope was granted just because it was requested. |
cx.getContext() | Fetch the context fresh, rather than reading the cached value. |
cx.getSettings() | Fetch the view's settings fresh. |
cx.destroy() | Drop every listener and reject anything still in flight. |
Subscribing
cx.listen(topic, handler) subscribes to a host push and returns the unsubscribe function — handing it straight back as a React effect’s cleanup is the whole reason it is shaped this way.
| Topic | Payload |
|---|---|
| context | The live ViewContext — pushed again whenever the person moves: switches collection, the module changes. |
| settings | The settings object configured for this view instance. |
| selection | { recordIds: string[] } — rows selected on the module. |
| change | The CRM realtime envelope, already filtered to this module. |
| theme | { theme: "light" | "dark" } when the host's theme flips. |
Reading and writing data
cx.api.* are typed helpers over the same allowlist described in Scopes & routes — thin on purpose, they build a path and hand back the unwrapped payload.
const records = await cx.api.records.list(context.collectionId);
if (cx.hasScope("records:write")) {
await cx.api.records.update(records[0]._id, { isCompleted: true });
}cx.api.modules
list(workspaceId) · create(workspaceId, body) · update(moduleId, body) · remove(moduleId)
cx.api.collections
list(moduleId) · create(moduleId, body) · update(collectionId, body) · remove(collectionId)
cx.api.columns
list(moduleId) · create(moduleId, body) · update(columnId, body) · remove(columnId)
cx.api.records
list(collectionId) · create(collectionId, body) · update(recordId, body) · remove(recordId) · subRecords(recordId) · createSubRecord(recordId, body)
cx.api.values
forRecord(recordId) · set({ record, column, value }) · update(recordValueId, { value }) · clear(recordValueId)
cx.api.amendments
list(recordId) · post(recordId, { message, parentComment? })
cx.api.members / cx.api.activity
members.list(workspaceId) and activity.list(workspaceId, query?) — both read-only; there is no members/activity write surface for a view at all.
The raw call
cx.request({ method, path, query, body }) reaches an endpoint the typed helpers do not cover yet — still checked against the exact same allowlist and scopes.
Commands — asking the host to do something
Things only the host can do, because they touch chrome the iframe cannot reach. An unimplemented command rejects with command_unsupported rather than resolving quietly, so a view can feature-detect instead of assuming it worked.
| Command | Params | Resolves to |
|---|---|---|
| notice | { message, type?, timeoutMs? } | void — a toast in the host chrome |
| openRecord | { recordId } | void — opens the record panel the module already has |
| confirm | { message, confirmLabel?, cancelLabel? } | boolean — the answer |
| resize | { height? } | void — omit height to fit content |
| navigate | { path } | void — paths only, never a foreign origin |
| copyToClipboard | { text } | void — through the host, which has the user gesture |
Storage
cx.storage.get/set/delete/keys — a small key-value store scoped to THIS mount of the view. For view state (a chosen grouping, a collapsed panel), not customer data.
Sizing itself
cx.autoResize(element?) keeps the iframe as tall as its content, via a ResizeObserver. Opt-in — a view that draws its own scroll area does not want it.
Errors
Every failure rejects with a ConexusError carrying a code. Branch on the code — the message is for your console and may be reworded.
| Code | Meaning |
|---|---|
| protocol_mismatch | The host and SDK speak different major protocol versions. Upgrade @conexus-x/sdk. |
| not_connected | Not embedded in an iframe, or connect() hasn't resolved yet. |
| timeout | The host did not answer within the request/handshake timeout. |
| scope_denied | The call needs a scope this view was not granted. |
| route_denied | The endpoint is not on the allowlist at all. |
| bad_request | The CRM API rejected the request — see .status and .details. |
| command_unsupported | This host build does not implement that command. |
| storage_unavailable | The host's storage backend is down. |
| host_error | The host returned an error with no further detail. |
| api_error | The proxied CRM API call failed — .status carries its HTTP status. |