Menu

SDKs

The official TypeScript and Python libraries: one typed method per operation, with retries, idempotency, pagination and dry runs built in. For other languages, the OpenAPI contract.

LanguagePackageStatus
TypeScript / JavaScript@mb-suite/sdkPreview, on the 2026-11-01 contract. Not yet published to npm.
Pythonmb-suitePreview, on the 2026-11-01 contract. Not yet published to PyPI. See Python SDK.
The package is not on npm yet: this guide describes how it works. The OpenAPI contract can already be downloaded (at the end of this page).

Installation

npm install @mb-suite/sdk
import { MBSuite } from "@mb-suite/sdk";

// Without apiKey, it reads the MB_API_KEY environment variable
const mb = new MBSuite({ apiKey: process.env.MB_API_KEY });
  • Node.js 20 or later, or any runtime with a global fetch: Deno, Bun, Cloudflare Workers, Vercel functions. No dependencies.
  • Server-side only: an API key is a secret and the API does not accept requests from a browser, so the client refuses to start in one.
  • An API key (mbs_live_…) or an OAuth token (mbo_…): see Authentication.

Your first request

const me = await mb.platform.me(); // GET /v1/me

console.log(me.actor.uid, me.org.id);
console.log(me.lastResponse.requestId); // req_…: include it if you write to us

Every operation has a stable id and the method follows it: crm.deals.create is mb.crm.deals.create(). Each segment is camelCased: crm.custom_fields.list is mb.crm.customFields.list() and task_management.tasks.complete is mb.taskManagement.tasks.complete(). The Reference shows each one's id.

  1. 1First, the path parameters, as strings and in the order of the path: mb.crm.deals.get("deal_123").
  2. 2Then, the parameters: one object in snake_case, just like the API (the query of a GET, the body of the rest).
  3. 3Last, the request options: organization, idempotencyKey, confirmToken, timeout, maxRetries, signal and headers.

List, create and edit

const { data, has_more } = await mb.crm.deals.list({ status: "open", limit: 50 });

const { deal_id } = await mb.crm.deals.create({
  title: "Plan anual — Acme",
  value: 4800,
  currency: "USD",
  pipeline_id: "PIPELINE_ID",
  stage_id: "STAGE_ID",
});

await mb.crm.deals.move(deal_id, { stage_id: "NEXT_STAGE_ID" });
await mb.crm.deals.update(deal_id, { updates: { value: 5200 } });
const deal = await mb.crm.deals.get(deal_id);

Parameters and results are typed from the contract, and every method carries its operation's description: your editor shows it on hover.

Pagination

Every paginated list has an …All() twin that walks every page and yields the items one by one. A page can come back empty with has_more set to true: the walk keeps going. Break out of the loop when you have what you need. See Pagination.

// Follows next_cursor until has_more is false; limit is the size of each page
for await (const deal of mb.crm.deals.listAll({ status: "open", limit: 200 })) {
  console.log(deal.id, deal.title);
}

Dry run and confirmation

The operations that need confirmation —bulk, irreversible, the ones that send or change something outside MB Suite or have a separate cost, and the ones that always ask— have .dryRun(): it writes nothing and returns the preview and confirm(), which runs the same operation with the same parameters and the preview's confirm_token (valid for 10 minutes). Pass your own idempotencyKey to .dryRun() or to confirm(): it goes with the confirmation, never with the dry run, which writes nothing. See Dry run and confirm_token.

const { preview, confirm } = await mb.crm.deals.bulk.delete.dryRun({
  deal_ids: ["DEAL_A", "DEAL_B"],
});

console.log(preview.summary, preview.counts, preview.warnings); // nothing was deleted yet

const { deleted } = await confirm(); // within 10 minutes
  • Calling it without a token throws ConfirmationRequiredError.
  • If the confirm_token arrives some other way (an approval, an agent), pass it as an option: mb.crm.deals.bulk.delete(params, { confirmToken }).
  • With an API key, the plain DELETE of a record that goes to the trash runs directly: await mb.crm.deals.delete("deal_123"). With an OAuth token it also goes through .dryRun().

Retries and idempotency

  • Every mutation goes out with an Idempotency-Key (a UUID the SDK generates, or yours) and keeps it across retries: if a response is lost, the retry returns the stored one instead of running twice.
  • Retried on their own, up to 3 times with exponential backoff (0.5 s, 1 s, 2 s): network errors, timeouts, a 429 (rate_limited, external_rate_limited, a short cooldown_active), a 409 idempotency_in_progress and 5xx (the 503 external_unavailable and pdf_unavailable included). When the response carries Retry-After, it waits exactly that.
  • Not retried: the rest of the 4xx (a 409 external_rejected, for example: the platform already said no; or a 409 external_outcome_unknown: a send that may have gone out, check before repeating it), a 429 quota_exceeded (the daily quota resets at 00:00 UTC), a 502 ai_generation_failed (retrying calls the AI again and spends credits if it answers: your call) or a Retry-After longer than 60 seconds (a daily_limit_reached). The error comes back with retryAfter so you can reschedule.

To make a logical operation safe across your own retries, give it your own key (valid for 24 hours). The same key with a different body throws IdempotencyError. See Idempotency.

// The same key on a retry of yours (another process, another deploy) returns the stored response
await mb.crm.deals.create(newDeal, { idempotencyKey: `import-${row.id}` });

Errors

Every error extends MBSuiteError and carries the API's single error format: type, code (stable: branch on it), message, status, param, requestId, docUrl and details. The class depends on the type:

ClasstypeHTTP
InvalidRequestErrorinvalid_request400
AuthenticationErrorauthentication401
PaymentRequiredErrorpayment_required402
PermissionErrorpermission403
NotFoundErrornot_found404
ConflictErrorconflict409
ConfirmationRequiredErrorconfirmation_required409
IdempotencyErroridempotency422
RateLimitErrorrate_limit429
QuotaExceededErrorquota429
UpstreamErrorupstream502
ServiceUnavailableErrorunavailable503
MaintenanceErrormaintenance503
InternalServerErrorinternal5xx
ConnectionError, TimeoutErrorconnection—
import { MBSuiteError, NotFoundError, RateLimitError } from "@mb-suite/sdk";

try {
  await mb.crm.deals.get(dealId);
} catch (err) {
  if (err instanceof NotFoundError) return null;
  if (err instanceof RateLimitError) console.log(`Retry in ${err.retryAfter} s`);
  if (err instanceof MBSuiteError) console.error(err.code, err.requestId, err.docUrl);
  throw err;
}

InvalidRequestError adds issues (which field and why), ConfirmationRequiredError adds reason (expired, mismatch), RateLimitError and ServiceUnavailableError add retryAfter, QuotaExceededError is a RateLimitError with quota and MaintenanceError is a ServiceUnavailableError. Every code is in Errors.

Accounts

A workspace API key works on the Accounts in its scope with MB-Organization. Choose the Account for the whole client, for one request, or with a client bound to it. mb.platform.orgs.list() returns the ones the API key reaches. See Workspaces and Accounts.

// For every request of the client
const mb = new MBSuite({ apiKey: process.env.MB_API_KEY, organization: "ACCOUNT_ID" });

// For one request (null = the key's own organization)
await mb.crm.deals.list({}, { organization: "OTHER_ACCOUNT_ID" });

// A client for that Account; the original one does not change
const account = mb.withOptions({ organization: "ACCOUNT_ID" });

Response metadata

Every result carries lastResponse, which does not show up in JSON.stringify or when you copy the object: the request_id, the version, the burst window and today's quota (to slow down before a 429, see Limits) and whether it was the stored response of an earlier request.

const page = await mb.crm.deals.list();

page.lastResponse.requestId;          // "req_…"
page.lastResponse.rateLimit;          // { limit, remaining, reset }: the key's burst window (10 s)
page.lastResponse.quota;              // { limit, remaining, reset }: today's calls
page.lastResponse.idempotentReplayed; // true when it is the stored response of an earlier request

Versions

The SDK sends MB-Version: 2026-11-01 on every request: the version its types describe, whatever version your organization is pinned to. When a new API version ships, a new SDK version follows; the previous one stays live for 12 months, and if a response comes from a version being retired, the SDK warns once with its Sunset date. See Versions and the Changelog.

Types and raw requests

import type { Item, Params } from "@mb-suite/sdk";

type NewDeal = Params<"crm.deals.create">; // the body of POST /v1/crm/deals
type Deal = Item<"crm.deals.list">;        // one element of data[]

// Any endpoint by method and path, with the same retries and errors
await mb.request({ method: "POST", path: "/crm/deals/DEAL_ID/archive", body: { archived: true } });

By default the client talks to https://api.mb-suite.com/v1; baseUrl points it somewhere else (the same API answers at https://mb-suite.com/api/platform/v1). The contract's raw types are exported as paths, operations and components.

Other languages: the OpenAPI contract

The full contract, in OpenAPI 3.1, is at GET /v1/openapi.json (with your API key or your token, like any request). The TypeScript and Python SDKs are generated from it, and you can use it to generate a client in another language.

curl https://api.mb-suite.com/v1/openapi.json \
  -H "Authorization: Bearer $MB_API_KEY" \
  -o mb-suite-openapi.json

Every operation in the contract carries x-mb-* extensions: the kind (x-mb-kind), whether it is idempotent, the batch size, the events it emits, the MCP tool and the permission. Within a version, the contract only adds: see Versions.