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.
| Language | Package | Status |
|---|---|---|
| TypeScript / JavaScript | @mb-suite/sdk | Preview, on the 2026-11-01 contract. Not yet published to npm. |
| Python | mb-suite | Preview, on the 2026-11-01 contract. Not yet published to PyPI. See Python SDK. |
Installation
npm install @mb-suite/sdkimport { 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 usEvery 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.
- 1First, the path parameters, as strings and in the order of the path:
mb.crm.deals.get("deal_123"). - 2Then, the parameters: one object in snake_case, just like the API (the query of a
GET, the body of the rest). - 3Last, the request options:
organization,idempotencyKey,confirmToken,timeout,maxRetries,signalandheaders.
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_tokenarrives 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
DELETEof 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 shortcooldown_active), a 409idempotency_in_progressand 5xx (the 503external_unavailableandpdf_unavailableincluded). When the response carriesRetry-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 409external_outcome_unknown: a send that may have gone out, check before repeating it), a 429quota_exceeded(the daily quota resets at 00:00 UTC), a 502ai_generation_failed(retrying calls the AI again and spends credits if it answers: your call) or aRetry-Afterlonger than 60 seconds (adaily_limit_reached). The error comes back withretryAfterso 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:
| Class | type | HTTP |
|---|---|---|
InvalidRequestError | invalid_request | 400 |
AuthenticationError | authentication | 401 |
PaymentRequiredError | payment_required | 402 |
PermissionError | permission | 403 |
NotFoundError | not_found | 404 |
ConflictError | conflict | 409 |
ConfirmationRequiredError | confirmation_required | 409 |
IdempotencyError | idempotency | 422 |
RateLimitError | rate_limit | 429 |
QuotaExceededError | quota | 429 |
UpstreamError | upstream | 502 |
ServiceUnavailableError | unavailable | 503 |
MaintenanceError | maintenance | 503 |
InternalServerError | internal | 5xx |
ConnectionError, TimeoutError | connection | — |
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 requestVersions
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.jsonEvery 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.