Menu

Python SDK

The official Python library: one typed method per operation, with retries, idempotency, pagination and dry runs built in. Not yet published to PyPI.

PackagePythonStatus
mb-suite3.9+Preview, on the 2026-11-01 contract. Not yet published to PyPI.
The package is not on PyPI yet: this guide describes how it works. For TypeScript, see SDKs.

Installation

pip install mb-suite
  • Python 3.9 or later. No dependencies: it uses the standard library (urllib), or the HTTP client you plug in (see “Transports”).
  • Server-side only: an API key is a secret. Never put it in a browser or a mobile app.
  • An API key (mbs_live_…) or an OAuth token (mbo_…): see Authentication.

Your first request

from mb_suite import MBSuite, response_meta

mb = MBSuite()  # without api_key, it reads the MB_API_KEY environment variable

me = mb.platform.me()  # GET /v1/me
print(me["actor"]["uid"], me["org"]["id"])
print(response_meta(me).request_id)  # req_…: include it if you write to us

Every operation has a stable id and the method follows it as is: crm.deals.create is mb.crm.deals.create() and crm.custom_fields.list is mb.crm.custom_fields.list(). A segment that is a Python keyword gets a trailing underscore: contacts.contacts.bulk.import is mb.contacts.contacts.bulk.import_(). 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 dict in snake_case, just like the API (the query of a GET, the body of the rest).
  3. 3Last, the request options as keyword arguments: organization, idempotency_key, confirm_token, timeout, max_retries and extra_headers. An option that does not exist is a TypeError.

List, create and edit

page = mb.crm.deals.list({"status": "open", "limit": 50})

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

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

Results are plain dicts, the JSON of the API. Parameters and results are typed from the contract with TypedDict (in mb_suite.types) and every method carries its operation's description: your editor shows it, and mypy or Pyright check them.

Pagination

Every paginated list has a …_all() twin that walks every page and yields the items one by one, fetching each page as you go. 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 deal in mb.crm.deals.list_all({"status": "open", "limit": 200}):
    print(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 .dry_run(): 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 idempotency_key to .dry_run() or to confirm(): it goes with the confirmation, never with the dry run, which writes nothing. See Dry run and confirm_token.

run = mb.crm.deals.bulk.delete.dry_run({"deal_ids": ["DEAL_A", "DEAL_B"]})

print(run.preview["summary"], run.preview["counts"])  # nothing was deleted yet

result = run.confirm()  # within 10 minutes
print(result["deleted"])
  • Calling it without a token raises ConfirmationRequiredError.
  • If the confirm_token arrives some other way (an approval, an agent), pass it as an option: mb.crm.deals.bulk.delete(params, confirm_token=token).

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 (except the 502 ai_generation_failed). When the response carries Retry-After, it waits exactly that.
  • Not retried: the rest of the 4xx, 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. The error comes back with retry_after 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 raises IdempotencyError. See Idempotency.

# The same key on a retry of yours (another process, another deploy) returns the stored response
mb.crm.deals.create(new_deal, idempotency_key=f"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, request_id, doc_url, details and retry_after. The class depends on the type; the names do not shadow Python's built-ins (PermissionDeniedError, APIConnectionError, APITimeoutError):

ClasstypeHTTP
InvalidRequestErrorinvalid_request400
AuthenticationErrorauthentication401
PaymentRequiredErrorpayment_required402
PermissionDeniedErrorpermission403
NotFoundErrornot_found404
ConflictErrorconflict409
ConfirmationRequiredErrorconfirmation_required409
IdempotencyErroridempotency422
RateLimitErrorrate_limit429
QuotaExceededErrorquota429
UpstreamErrorupstream502
ServiceUnavailableErrorunavailable503
MaintenanceErrormaintenance503
InternalServerErrorinternal5xx
APIConnectionError, APITimeoutErrorconnection—
from mb_suite import MBSuiteError, NotFoundError, RateLimitError

try:
    deal = mb.crm.deals.get(deal_id)
except NotFoundError:
    deal = None
except RateLimitError as err:
    print(f"Retry in {err.retry_after} s")
except MBSuiteError as err:
    print(err.code, err.request_id, err.doc_url)
    raise

InvalidRequestError adds issues (which field and why), ConfirmationRequiredError adds reason (expired, mismatch), 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
mb = MBSuite(organization="ACCOUNT_ID")

# For one request (None = the key's own organization)
mb.crm.deals.list({}, organization="OTHER_ACCOUNT_ID")

# A client for that Account; the original one does not change
account = mb.with_options(organization="ACCOUNT_ID")

Response metadata

response_meta(result) returns 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. It is also the last_response attribute of the result, which is not a key: it does not show up in json.dumps or dict(result).

meta = response_meta(page)

meta.request_id           # "req_…"
meta.rate_limit           # LimitState(limit, remaining, reset): the key's burst window (10 s)
meta.quota                # LimitState(limit, remaining, reset): today's calls
meta.idempotent_replayed  # True when it is the stored response of an earlier request

The SDK sends MB-Version: 2026-11-01 on every request: the version its types describe. If a response comes from a version being retired, it warns once with an APIVersionDeprecationWarning and its Sunset date. See Versions.

Types and raw requests

from mb_suite.types import CrmDealsCreateParams, CrmDealsListItem

new_deal: CrmDealsCreateParams = {"title": "Acme", "value": 4800, "currency": "USD", "pipeline_id": "P", "stage_id": "S"}

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

For each operation (crm.deals.list → CrmDealsList): …Params, …Result and, for paginated lists, …Item. By default the client talks to https://api.mb-suite.com/v1; base_url points it somewhere else (the same API answers at https://mb-suite.com/api/platform/v1).

Transports

By default the SDK uses the standard library (system certificates, the HTTP(S)_PROXY variables, no redirects followed). To use another HTTP client, or to test without a network, pass any object with a send(request) -> HTTPResponse method: it sends one request; retries, keys and errors stay with the client.

import requests
from mb_suite import HTTPRequest, HTTPResponse, MBSuite

class RequestsTransport:
    def __init__(self) -> None:
        self.session = requests.Session()

    def send(self, request: HTTPRequest) -> HTTPResponse:
        r = self.session.request(request.method, request.url, headers=request.headers, data=request.body,
                                 timeout=request.timeout, allow_redirects=False)
        return HTTPResponse(status=r.status_code, headers={k.lower(): v for k, v in r.headers.items()}, body=r.content)

mb = MBSuite(transport=RequestsTransport())

To receive events, verify the MB-Signature header of each delivery as shown in the Webhooks guide (it has the Python snippet).