Menu

API reference · CRM

People

crm · persons · 21 operations

GET/v1/crm/persons/{person_id}/messages

List the messages of all of a person's conversations

crm.persons.messages.list

ReadIdempotentSince 2026-11-01

Lists the messages of every inbox conversation linked to a person (WhatsApp on any line and Instagram, Messenger or TikTok direct messages), newest first, in one call instead of one crm.messages.list per conversation, paginated by cursor (limit up to 200). Each message comes as in crm.messages.list (conversation_id, direction, content (the text), media, template, delivery status, who sent it, external_id), with attachments as links signed for one hour (media_urls_expire_at) and without the raw platform payload. Filters: since and until (epoch ms, both exclusive), channel (only the person's conversations of that channel) and direction (inbound: what the contacts wrote; outbound: what the house sent), the last one applied to the page that was read, so a page can bring fewer than limit while has_more is true (follow next_cursor). It reads the person's 30 conversations with the most recent activity: with more, conversations_truncated is true and the older conversations are left out (read them with crm.conversations.list with person_id and crm.messages.list). The same conversations crm.conversations.list shows are included: conversations on a line that belongs to another part of the product never are, and a seller with restricted scope only gets the conversations they own or those on lines they work. Not found (person_not_found) if the person does not exist or the seller cannot see them. Requires view_inbox. With TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their records are left out of the response (the permissions guide lists what each platform allows).

Scope
crm:read
Permission
View the module · CRM action: view_inbox
MCP tool
crm_persons_messages_listOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.messages.list".

Declared cost: up to 260 reads and 0 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired
limitintegerqueryoptional

How many items this page brings at most (see the maximum and the default in the schema).

Default: 50

cursorstringqueryoptional

The next_cursor of the previous page, exactly as it came (only for the same list and filters); without it, the first page.

sinceintegerqueryoptional

Only messages created after this time (epoch ms, exclusive): to poll, the createdAt of the last message you handled.

untilintegerqueryoptional

Only messages created before this time (epoch ms, exclusive).

channelenumqueryoptional

Only the person's conversations of this channel.

Values: whatsapp, instagram, facebook, tiktok, email

directionenumqueryoptional

Only what the contacts wrote (inbound) or what the house sent (outbound); applied to the page that was read, so a page can bring fewer than limit while has_more is true.

Values: inbound, outbound

Response

dataarray<object>
has_moreboolean
next_cursorstring
conversations_truncatedboolean

Present (true) when the person has more than 30 conversations: only the messages of the 30 with the most recent activity were read.

Errors

Example

curl "https://api.mb-suite.com/v1/crm/persons/{person_id}/messages" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/crm/persons/{person_id}/messages" \
  -H "Authorization: Bearer $MB_TEST_KEY"
GET/v1/crm/persons

List persons

crm.persons.list

ReadIdempotentSince 2026-11-01

Lists persons (contacts) most recently updated first, paginated by cursor; company_id keeps only the persons linked to that company. Each person carries their contactability: do_not_contact, unsubscribed_at, bounced_at, whatsapp_unreachable, and the name of their primary company as stored on their record (company_name). Archived persons are left out unless include_archived is true; merged-away duplicates never appear. A seller with restricted scope only gets their own, the free ones and those of their deals. Both filters apply to the page read, so a page can bring fewer than limit while has_more is true: follow next_cursor until has_more is false. Use crm.persons.lookup to find one by email or phone. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their records or fields are left out of the response (the permissions guide lists what each platform allows).

Scope
crm:read
Permission
View the module
MCP tool
crm_persons_listOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.list".

Declared cost: up to 201 reads and 0 writes per call (they count against the daily quota).

Parameters

limitintegerqueryoptional

How many items this page brings at most (default 20, up to 200).

Default: 20

cursorstringqueryoptional

The next_cursor of the previous page, exactly as it came (only for the same list and filters); without it, the first page.

company_idstringqueryoptional

Only the persons linked to this company (as their primary company or another one).

include_archivedbooleanqueryoptional

Include archived records (left out by default; merged-away duplicates never come).

Default: false

Response

dataarray<object>
has_moreboolean
next_cursorstring

Errors

Example

curl "https://api.mb-suite.com/v1/crm/persons" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/crm/persons" \
  -H "Authorization: Bearer $MB_TEST_KEY"
GET/v1/crm/persons/{person_id}

Get one person

crm.persons.get

ReadIdempotentSince 2026-11-01

Returns a person with their emails, phones, companies (company_name: their primary company's name as stored on their record), owner, labels and custom fields, and whether they can be contacted: do_not_contact, unsubscribed_at (emails), bounced_at and whatsapp_unreachable (their number has no WhatsApp). Not found if the id does not exist or the seller's scope hides them. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their records or fields are left out of the response (the permissions guide lists what each platform allows).

Scope
crm:read
Permission
View the module
MCP tool
crm_persons_getOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.get".

Declared cost: up to 2 reads and 0 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

Response

idstring
org_idstring
first_namestring
full_namestring
created_atnumber
updated_atnumber
last_namestring | null
emailstring | null

The primary email (one of emails).

emailsarray<string>
phonestring | null

The primary phone, E.164 when it could be read (one of phones).

phonesarray<string>
job_titlestring | null
countrystring | null

ISO-2 country; local phone numbers are read with it.

websitestring | null
linked_in_urlstring | null
avatar_urlstring | null
company_idsarray<string>

Every company of the person (primary_company_id is one of them).

primary_company_idstring | null
company_namestring | null

Name of the person's primary company as stored on their record: copied when a company becomes their primary one (or written directly); renaming the company later does not update it (read the company for its current name).

owner_idstring | null

The owner: a member's user id (members.members.list), or an AI position's ai-… id.

owner_namestring | null

The name of the owner, resolved when the record is read.

label_idsarray<string>
custom_fieldsobject
do_not_contactboolean
unsubscribed_atnumber | null

When the person unsubscribed from emails (epoch ms); sequences and campaigns leave them out.

bounced_atnumber | null

When an email to the person bounced (epoch ms).

whatsapp_unreachableboolean | null

true when WhatsApp reported that their number has no WhatsApp or cannot receive messages (learned from a failed send).

whatsapp_unreachable_atnumber | null

When whatsapp_unreachable was set, epoch ms.

source_channelstring | null

Where the person came from: meta, google, whatsapp, web, manual, csv…

origin_channelsarray<string> | null

The lead channels (as in source_channel) of platforms with their own terms whose data the person carries: added when a form or a message from one of them creates or completes the person, never removed.

attributionobject | null
first_seen_atnumber | null

When the person first entered the CRM, epoch ms.

last_seen_atnumber | null

When the person last came in through an intake (a form, a message, a booking), epoch ms.

last_activity_atnumber | null

Last activity linked to the person, epoch ms.

chat_data_suggestionsarray<object> | null

Phones, emails and sites found in the person's chats and not decided yet: accept or dismiss each with crm.persons.chat_data.update.

is_archivedboolean
archived_atnumber | null
merged_into_idstring | null

Set on a duplicate absorbed by a merge: the person that kept it (lists never return these).

created_by_idstring | null

Who created the person: a member's user id (members.members.list), or an AI position's ai-… id.

created_by_namestring | null

The name of who created the person, resolved when the record is read.

Errors

Example

curl "https://api.mb-suite.com/v1/crm/persons/{person_id}" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/crm/persons/{person_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY"
GET/v1/crm/persons/{person_id}/context

Get a person with their company and deals

crm.persons.context

ReadIdempotentSince 2026-11-01

Returns the person, their primary company and their deals in one call: what you need to talk about them. deals are the deals where the person is linked or is the main contact (up to 50 of each), open, won and lost, not archived, open first and then the most recently moved. Not found if the person does not exist or is out of scope. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their records or fields are left out of the response (the permissions guide lists what each platform allows).

Scope
crm:read
Permission
View the module
MCP tool
crm_persons_contextOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.context".

Declared cost: up to 23 reads and 0 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

Response

personobject | null
companyobject | null
dealsarray<object>

Errors

Example

curl "https://api.mb-suite.com/v1/crm/persons/{person_id}/context" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/crm/persons/{person_id}/context" \
  -H "Authorization: Bearer $MB_TEST_KEY"
GET/v1/crm/persons/lookup

Find a person by email or phone (or many phones)

crm.persons.lookup

ReadIdempotentSince 2026-11-01

Finds the person that has the given email (any of their emails) or phone (normalized to E.164): person is the first one that is not a merged-away duplicate, by email first and then by phone, or null when there is no match or the seller's scope hides them. matches lists every record that has that email or that phone, not just one: the merged-away duplicates too (merged: true and merged_into_id, the record that remains), archived ones, and for the phone also the records that store it in another of its forms (the Argentine 9, the Mexican 1), each with matched_by (email, phone); up to 25 per value, only those the caller can see. phones (up to 30; by REST ?phones=a,b) looks up many numbers in one call: by_phone has one entry per number, in the order asked, with the number as asked, its E.164 form (normalized: null when it cannot be read as a phone with a country code, and then it is not looked up), person_id (the same rule as person) and its matches; person and matches stay those of email and phone. Use it before creating a person to avoid duplicates, and to find sibling records of a number before writing to it. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their records or fields are left out of the response (the permissions guide lists what each platform allows).

Scope
crm:read
Permission
View the module
MCP tool
crm_persons_lookupOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.lookup".

Declared cost: up to 802 reads and 0 writes per call (they count against the daily quota).

Parameters

emailstring (email)queryoptional

An email to look for (any of a person's emails, case does not matter).

phonestringqueryoptional

A phone to look for, with its country code or local with country.

phonesarray<string>queryoptional

Up to 30 phones looked up in one call (by REST: ?phones=a,b or the key repeated). Each one gets its own entry in byPhone, in the order asked; person and matches stay those of email and phone.

countrystringqueryoptional

ISO-2 country (AR, MX…) to read phones written without their country code (011 4000-0002 with AR is +541140000002). Without it, only phones with their country code (+54 11 4000-0002, 0054…) are looked up.

Response

personobject | null
matchesarray<object>
by_phonearray<object>

Errors

Example

curl "https://api.mb-suite.com/v1/crm/persons/lookup" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/crm/persons/lookup" \
  -H "Authorization: Bearer $MB_TEST_KEY"
GET/v1/crm/persons/similar

Find prior deals and prospects that look like this contact

crm.persons.similar

ReadIdempotentSince 2026-11-01

Given a person and/or company (or a prospect, looked up by its person and company), returns the prior deals and prospects that look related: same person, a duplicate person with the same email or phone, the same company or another company on the same web domain. strong matches first. Use it before creating a deal or prospect to avoid working a contact twice.

Scope
crm:read
Permission
View the module
MCP tool
crm_persons_similarOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.similar".

Declared cost: up to 40 reads and 0 writes per call (they count against the daily quota).

Parameters

person_idstringqueryoptional

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

company_idstringqueryoptional

The company, by id (crm.companies.list or crm.search).

prospect_idstringqueryoptional

A prospect: looked up by its person and company.

Response

dealsarray<object>
prospectsarray<object>

Errors

Example

curl "https://api.mb-suite.com/v1/crm/persons/similar" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/crm/persons/similar" \
  -H "Authorization: Bearer $MB_TEST_KEY"
POST/v1/crm/persons

Create a person

crm.persons.create

WriteSince 2026-11-01

Creates a person (contact) with name, emails, phones, job title, links, country, an optional company (becomes their primary company), owner, labels and custom fields; created: true. If a person already has the primary email (any of their emails), nothing is written: their id comes back with created: false and matched_by: email (a merged-away duplicate never comes back, the record that remains does; archived persons count). A seller with restricted scope who cannot see that person gets 409 person_exists_not_visible instead: their id never comes back and nothing is written. It does not deduplicate by phone: use crm.persons.lookup first. company_name is only the text shown on the record: it does not link a company (use company_id, from crm.companies.list or crm.companies.create, for that). Labels must be person labels (crm.labels.list with entity person: label_not_found otherwise), custom fields must exist for persons with a value of their type (custom_field_unknown, custom_field_invalid), the owner must be a member or an AI position (owner_not_member), and outside the screen every phone must be readable (with its country code or with country: invalid_phone otherwise). A new person fires the person_created sequence trigger, unless skip_automations is true (the audit log says what was skipped); the crm.person.created webhook fires either way. Requires create_person.

Scope
crm:write
Permission
Edit in the module · CRM action: create_person
MCP tool
crm_persons_createOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.create".
Events it can emit
crm.person.created

Declared cost: up to 7 reads and 1 writes per call (they count against the daily quota).

Parameters

first_namestringbodyrequired

First name, as written.

last_namestringbodyoptional

Last name.

emailstring (email)bodyoptional

The primary email (it is also added to emails).

emailsarray<string>bodyoptional

Every email of the person (the primary one included).

phonestringbodyoptional

The primary phone, with its country code (+54 11 4000-0002) or local with country; it is also added to phones.

phonesarray<string>bodyoptional

Every phone of the person, each with its country code or local with country (outside the screen, one that cannot be read is invalid_phone).

job_titlestringbodyoptional

Their job title.

linked_in_urlstring (uri)bodyoptional

Their LinkedIn profile URL.

websitestring (uri)bodyoptional

Their personal website URL.

countrystringbodyoptional

ISO-2 country (AR, MX…): local phone numbers are read with it.

company_idstringbodyoptional

The company (crm.companies.list or crm.companies.create): it becomes their primary company.

company_namestringbodyoptional

Only the text shown as their company: it does not link a company (use company_id).

owner_idstringbodyoptional

The owner: a member's user id or an AI position's ai-… id.

label_idsarray<string>bodyoptional

Person labels (crm.labels.list with entity person).

custom_fieldsobjectbodyoptional

Custom fields: slug (crm.custom_fields.list with entity person) → value of the field's type.

skip_automationsbooleanbodyoptional

true: the new person does not enter the sequences that start when a contact is created (the person_created trigger). Recorded in the audit log (skip_automations, automationsSkipped). The crm.person.created webhook still fires, and deduplication by email works as always.

Response

person_idstring
createdboolean

true when the person is new; false when one with that email already existed (person_id is hers and nothing was written).

matched_byenum

Why an existing person came back (only with created: false).

Values: email

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "first_name": "FIRST_NAME"
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "first_name": "FIRST_NAME"
}'
POST/v1/crm/persons/{person_id}/do-not-contact

Mark one person as do-not-contact (or clear it)

crm.persons.set_do_not_contact

WriteIdempotentSince 2026-11-01

Sets do_not_contact on a person: send value or do_not_contact (the same flag, by the name it has on the person) with true to mark them, or with false to clear it and let them be contacted again. MB Suite stops starting the contact with a person marked «Do not contact»: sequences leave them out, the AI Setter does not write first (it still answers what they write), the voice agent does not call them, and a manual email, a WhatsApp template or an SMS to them is refused (409 do_not_contact) unless a person of the team confirms it on a closed basis (about their account, a charge or support, or because they asked us to write); a WhatsApp marketing template never goes to them. A reply to what they wrote after being marked still goes out, and so does WhatsApp free text (Meta only delivers it within 24 hours of their message). reason (optional, up to 500 characters) says why and is kept in the audit log of the change (the record has no field for it). Idempotent: if nothing changes, nothing is logged (changed: false). Requires edit_person.

Scope
crm:write
Permission
Edit in the module
MCP tool
crm_persons_set_do_not_contactOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.set_do_not_contact".

Declared cost: up to 1 reads and 1 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

valuebooleanbodyoptional

true marks the person as do-not-contact; false clears it. Same as do_not_contact.

do_not_contactbooleanbodyoptional

The same flag with the name it has on the person (do_not_contact): send this or value.

reasonstringbodyoptional

Why (the lead asked not to be called, a wrong number…). It is kept in the audit log of the change; the person's record has no field for it.

Response

changedboolean

false when the person already was that way: nothing was written.

do_not_contactboolean

The value the person has now.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/do-not-contact" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/do-not-contact" \
  -H "Authorization: Bearer $MB_TEST_KEY"
PATCH/v1/crm/persons/{person_id}

Update a person

crm.persons.update

WriteSince 2026-11-01

Updates one or more fields of a person: names, emails (the full list replaces; a single email is added), phones, job title, links, country, companies and primary company, owner, labels, custom fields, do-not-contact. A renamed person propagates their name to their deals and activities. Labels, custom fields, owner and phones are checked as in crm.persons.create. Returns the person as saved. Requires edit_person (a seller edits their own, the free ones and those of their deals). With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their records or fields are left out of the response (the permissions guide lists what each platform allows).

Scope
crm:write
Permission
Edit in the module
MCP tool
crm_persons_updateOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.update".
Events it can emit
crm.person.updated

Declared cost: up to 2 reads and 502 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

updatesobjectbodyrequired

The fields to change; the rest stay as they are.

Response

personobject

Errors

Example

curl -X PATCH "https://api.mb-suite.com/v1/crm/persons/{person_id}" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "updates": {}
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X PATCH "https://api.mb-suite.com/v1/crm/persons/{person_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "updates": {}
}'
DELETE/v1/crm/persons/{person_id}

Delete a person

crm.persons.delete

IrreversibleSince 2026-11-01

Deletes a person. They go to the 30-day trash (restoring re-links them to the deals and lists they were removed from); their deals lose the link (the next person becomes primary) and their activities and conversations are kept. Prefer crm.persons.archive to just hide them. Requires delete_person.

With an API key, this DELETE runs directly: the record stays 30 days in the trash and the response carries its trash_item_id. With an OAuth token, and through the MCP, it needs dry_run → confirm_token.

Scope
crm:write and “Allow deletions” (crm:delete in OAuth)
Permission
Edit in the module · CRM action: delete_person
MCP tool
crm_persons_deleteOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.delete".
Events it can emit
crm.person.deleted

Declared cost: up to 10 reads and 10 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

Response

oktrue
trash_item_idstring

The trash item this deletion left: the item_id of platform.trash.restore (POST /trash/{item_id}/restore). Missing if nothing went to the trash.

trash_item_idsarray<string>

Only when the deletion left more than one copy: all of them, the first being trash_item_id.

trash_expires_atnumber

When the copy leaves the trash for good (epoch ms): 30 days after the deletion.

Errors

Example

curl -X DELETE "https://api.mb-suite.com/v1/crm/persons/{person_id}" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X DELETE "https://api.mb-suite.com/v1/crm/persons/{person_id}" \
  -H "Authorization: Bearer $MB_TEST_KEY"
POST/v1/crm/persons/{person_id}/archive

Archive or unarchive a person

crm.persons.archive

WriteIdempotentSince 2026-11-01

Archives (archived: true) or restores (false) a person; archived persons leave the lists but keep everything. Idempotent: changed is false when they already were that way. Returns the person as they are now. Requires edit_person. With Google Ads, LinkedIn or TikTok, their terms limit it through the API and the MCP: what they do not allow answers 403 platform_terms_restricted, and their records or fields are left out of the response (the permissions guide lists what each platform allows).

Scope
crm:write
Permission
Edit in the module
MCP tool
crm_persons_archiveOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.archive".

Declared cost: up to 1 reads and 1 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

archivedbooleanbodyrequired

true archives them; false brings them back.

Response

changedboolean
personobject

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/archive" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "archived": true
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/archive" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "archived": true
}'
POST/v1/crm/persons/{person_id}/companies

Link a person to a company

crm.persons.companies.add

WriteIdempotentSince 2026-11-01

Adds a company to the person's companies; if they had none it becomes their primary company. Already linked: nothing is written (changed: false). Requires edit_person.

Scope
crm:write
Permission
Edit in the module
MCP tool
crm_persons_companies_addOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.companies.add".

Declared cost: up to 2 reads and 1 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

company_idstringbodyrequired

The company, by id (crm.companies.list or crm.search).

Response

oktrue
changedboolean

false when the link was already that way: nothing was written.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/companies" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "company_id": "COMPANY_ID"
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/companies" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "company_id": "COMPANY_ID"
}'
POST/v1/crm/persons/{person_id}/companies/remove

Unlink a person from a company

crm.persons.companies.remove

WriteIdempotentSince 2026-11-01

Removes a company from the person's companies; if it was their primary company, the next one takes over. Not linked: nothing is written (changed: false). Requires edit_person.

Scope
crm:write
Permission
Edit in the module
MCP tool
crm_persons_companies_removeOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.companies.remove".

Declared cost: up to 2 reads and 1 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

company_idstringbodyrequired

The company, by id (crm.companies.list or crm.search).

Response

oktrue
changedboolean

false when the link was already that way: nothing was written.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/companies/remove" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "company_id": "COMPANY_ID"
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/companies/remove" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "company_id": "COMPANY_ID"
}'
POST/v1/crm/persons/{person_id}/companies/primary

Set the primary company of a person

crm.persons.set_primary_company

WriteIdempotentSince 2026-11-01

Makes a company the person's primary one (adding it to their companies if needed); their denormalized company name follows. Already the primary one: nothing is written (changed: false). Requires edit_person.

Scope
crm:write
Permission
Edit in the module
MCP tool
crm_persons_set_primary_companyOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.set_primary_company".

Declared cost: up to 2 reads and 1 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list, crm.persons.lookup or crm.search).

company_idstringbodyrequired

The company, by id (crm.companies.list or crm.search).

Response

oktrue
changedboolean

false when the link was already that way: nothing was written.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/companies/primary" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "company_id": "COMPANY_ID"
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/{person_id}/companies/primary" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "company_id": "COMPANY_ID"
}'
PATCH/v1/crm/persons/{person_id}/chat-data

Accept or dismiss a chat-data suggestion

crm.persons.chat_data.update

WriteSince 2026-11-01

Resolves one suggestion that the person wrote in a chat (a phone, an email, a website): accept writes it to their record (a phone may need a country), dismiss hides it. The key and message_id come from the person's chat_data_suggestions. Requires edit_person.

Scope
crm:write
Permission
Edit in the module
MCP tool
crm_persons_chat_data_updateOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.chat_data.update".

Declared cost: up to 2 reads and 1 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person, by id (crm.persons.list).

keystringbodyrequired

The suggestion's key, from the person's chatDataSuggestions.

message_idstringbodyrequired

The message the suggestion was found in, from chatDataSuggestions.

decisionenumbodyrequired

accept writes it to the person; dismiss hides it.

Values: accept, dismiss

countrystringbodyoptional

For a phone without its country code: the ISO-2 country to read it with.

Response

oktrue
kindstring
channelstring

Errors

Example

curl -X PATCH "https://api.mb-suite.com/v1/crm/persons/{person_id}/chat-data" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "key": "KEY",
  "message_id": "MESSAGE_ID",
  "decision": "accept"
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X PATCH "https://api.mb-suite.com/v1/crm/persons/{person_id}/chat-data" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "key": "KEY",
  "message_id": "MESSAGE_ID",
  "decision": "accept"
}'
POST/v1/crm/persons/bulk/delete

Delete many persons

crm.persons.bulk.delete

IrreversibleBulk · up to 500Since 2026-11-01

Deletes up to 500 persons; each goes to the 30-day trash (restorable one by one) and leaves their deals and lists. deleted counts the persons that existed; the other ids come back in not_found. Requires bulk_delete_persons (Manager-tier). Outside the screen it requires a dry run and a confirm_token.

Needs confirmation: first dry_run: true (preview and confirm_token, valid 10 minutes and for one run), then the same body with confirm_token. In a few operations only with certain data: the description says when. Why and how: Dry run and confirm_token.

Scope
crm:write and “Allow deletions” (crm:delete in OAuth)
Permission
Edit in the module · CRM action: bulk_delete_persons
MCP tool
crm_persons_bulk_deleteOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.bulk.delete".
Events it can emit
crm.person.deleted

Declared cost: up to 2000 reads and 1000 writes per call (they count against the daily quota).

Parameters

person_idsarray<string>bodyrequired

The persons (up to 500), by id (crm.persons.list).

Response

deletednumber

How many persons were deleted (each one goes to the 30-day trash).

not_foundarray<string>

The ids that do not exist in the organization: nothing to delete.

countnumber

Same as deleted (kept for older clients).

trash_item_idsarray<string>

The trash items this deletion left, one per record: the item_id of platform.trash.restore (POST /trash/{item_id}/restore).

trash_expires_atnumber

When the copy leaves the trash for good (epoch ms): 30 days after the deletion.

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/delete" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "dry_run": true
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/delete" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "dry_run": true
}'
POST/v1/crm/persons/bulk/labels

Add or remove labels on many persons

crm.persons.bulk.update_labels

WriteBulk · up to 500Since 2026-11-01

Adds and/or removes labels (ids from crm.labels.list with entity person) on up to 500 persons; fires the label_added sequence trigger for the labels that are really new. A label that is not a person label of the organization is label_not_found (with details.unknown_ids). Persons that already have exactly those labels are left as they are (unchanged) and ids that do not exist are skipped (not_found). The dry run reads the same persons and names those that would change. Requires bulk_update_persons. Outside the screen it requires a dry run and a confirm_token.

Needs confirmation: first dry_run: true (preview and confirm_token, valid 10 minutes and for one run), then the same body with confirm_token. In a few operations only with certain data: the description says when. Why and how: Dry run and confirm_token.

Scope
crm:write
Permission
Edit in the module · CRM action: bulk_update_persons
MCP tool
crm_persons_bulk_update_labelsOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.bulk.update_labels".

Declared cost: up to 540 reads and 500 writes per call (they count against the daily quota).

Parameters

person_idsarray<string>bodyrequired

The persons (up to 500), by id (crm.persons.list).

to_addarray<string>bodyoptional

Label ids to add (crm.labels.list with entity person), up to 20; send to_add, to_remove or both.

to_removearray<string>bodyoptional

Label ids to remove (crm.labels.list with entity person), up to 20; send to_add, to_remove or both.

Response

updatednumber

How many persons changed.

unchangednumber

How many persons already were that way: not written.

not_foundarray<string>

The ids that do not exist in the organization (or are merged-away duplicates): skipped.

countnumber

Same as updated (kept for older clients).

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/labels" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "dry_run": true
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/labels" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "dry_run": true
}'
POST/v1/crm/persons/bulk/owner

Set the owner of many persons

crm.persons.bulk.update_owner

WriteBulk · up to 500Since 2026-11-01

Sets (or clears with null) the owner of up to 500 persons: a member's user id (members.members.list) or an AI position's ai-… id (owner_not_member otherwise). Persons that already have that owner are left as they are (unchanged) and ids that do not exist are skipped (not_found). The dry run reads the same persons and names those that would change. Requires bulk_update_persons. Outside the screen it requires a dry run and a confirm_token.

Needs confirmation: first dry_run: true (preview and confirm_token, valid 10 minutes and for one run), then the same body with confirm_token. In a few operations only with certain data: the description says when. Why and how: Dry run and confirm_token.

Scope
crm:write
Permission
Edit in the module · CRM action: bulk_update_persons
MCP tool
crm_persons_bulk_update_ownerOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.bulk.update_owner".

Declared cost: up to 501 reads and 500 writes per call (they count against the daily quota).

Parameters

person_idsarray<string>bodyrequired

The persons (up to 500), by id (crm.persons.list).

owner_idstring | nullbodyrequired

The new owner (a member's user id or an AI position's ai-… id); null leaves them without one.

Response

updatednumber

How many persons changed.

unchangednumber

How many persons already were that way: not written.

not_foundarray<string>

The ids that do not exist in the organization (or are merged-away duplicates): skipped.

countnumber

Same as updated (kept for older clients).

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/owner" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "owner_id": "OWNER_ID",
  "dry_run": true
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/owner" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "owner_id": "OWNER_ID",
  "dry_run": true
}'
POST/v1/crm/persons/bulk/company

Set the primary company of many persons

crm.persons.bulk.update_company

WriteBulk · up to 500Since 2026-11-01

Makes a company the primary one of up to 500 persons (adding it to their companies), or clears the primary company with null without removing any. Persons that already have that primary company are left as they are (unchanged) and ids that do not exist are skipped (not_found). The dry run reads the same persons and names those that would change. Requires bulk_update_persons. Outside the screen it requires a dry run and a confirm_token.

Needs confirmation: first dry_run: true (preview and confirm_token, valid 10 minutes and for one run), then the same body with confirm_token. In a few operations only with certain data: the description says when. Why and how: Dry run and confirm_token.

Scope
crm:write
Permission
Edit in the module · CRM action: bulk_update_persons
MCP tool
crm_persons_bulk_update_companyOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.bulk.update_company".

Declared cost: up to 501 reads and 500 writes per call (they count against the daily quota).

Parameters

person_idsarray<string>bodyrequired

The persons (up to 500), by id (crm.persons.list).

company_idstring | nullbodyrequired

The company that becomes their primary one (crm.companies.list); null clears the primary company without removing any.

company_namestring | nullbodyoptional

The company's name as shown on their records (by default, the company's).

Response

updatednumber

How many persons changed.

unchangednumber

How many persons already were that way: not written.

not_foundarray<string>

The ids that do not exist in the organization (or are merged-away duplicates): skipped.

countnumber

Same as updated (kept for older clients).

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/company" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "company_id": "COMPANY_ID",
  "dry_run": true
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/company" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "company_id": "COMPANY_ID",
  "dry_run": true
}'
POST/v1/crm/persons/bulk/do-not-contact

Mark many persons as do-not-contact (or clear it)

crm.persons.bulk.set_do_not_contact

WriteBulk · up to 500Since 2026-11-01

Sets do_not_contact on up to 500 persons (send value or do_not_contact, true or false), as crm.persons.set_do_not_contact does on one. MB Suite stops starting the contact with a person marked «Do not contact»: sequences leave them out, the AI Setter does not write first (it still answers what they write), the voice agent does not call them, and a manual email, a WhatsApp template or an SMS to them is refused (409 do_not_contact) unless a person of the team confirms it on a closed basis (about their account, a charge or support, or because they asked us to write); a WhatsApp marketing template never goes to them. A reply to what they wrote after being marked still goes out, and so does WhatsApp free text (Meta only delivers it within 24 hours of their message). Persons that already are that way are left as they are (unchanged) and ids that do not exist are skipped (not_found). The dry run reads the same persons and names those that would change. Requires bulk_update_persons. Outside the screen it requires a dry run and a confirm_token.

Needs confirmation: first dry_run: true (preview and confirm_token, valid 10 minutes and for one run), then the same body with confirm_token. In a few operations only with certain data: the description says when. Why and how: Dry run and confirm_token.

Scope
crm:write
Permission
Edit in the module · CRM action: bulk_update_persons
MCP tool
crm_persons_bulk_set_do_not_contactOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.bulk.set_do_not_contact".

Declared cost: up to 500 reads and 500 writes per call (they count against the daily quota).

Parameters

person_idsarray<string>bodyrequired

The persons (up to 500), by id (crm.persons.list).

valuebooleanbodyoptional

true marks the persons as do-not-contact; false clears it. Same as do_not_contact.

do_not_contactbooleanbodyoptional

The same flag with the name it has on the person (do_not_contact): send this or value.

Response

updatednumber

How many persons changed.

unchangednumber

How many persons already were that way: not written.

not_foundarray<string>

The ids that do not exist in the organization (or are merged-away duplicates): skipped.

countnumber

Same as updated (kept for older clients).

Errors

Example

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/do-not-contact" \
  -H "Authorization: Bearer $MB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "dry_run": true
}'

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl -X POST "https://api.mb-suite.com/v1/crm/persons/bulk/do-not-contact" \
  -H "Authorization: Bearer $MB_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "person_ids": [
    "PERSON_ID"
  ],
  "dry_run": true
}'
GET/v1/crm/persons/{person_id}/enrollments

List the sequence enrollments of a person

crm.persons.enrollments.list

ReadIdempotentSince 2026-11-01

Lists the sequence enrollments of one person across every sequence, newest first, paginated by cursor (limit up to 200), with the same fields as crm.sequences.enrollments.list (sequence_id, status, current step, pause and exit reasons, counters, last_history). Use it to know which sequences are writing to a contact before enrolling them in another one. Not found (person_not_found) if the person does not exist or a seller with restricted scope cannot see them. Requires view access to the CRM.

Scope
crm:read
Permission
View the module
MCP tool
crm_persons_enrollments_listOn the full surface; on the compact one, mb_execute_operation with id: "crm.persons.enrollments.list".

Declared cost: up to 203 reads and 0 writes per call (they count against the daily quota).

Parameters

person_idstringpathrequired

The person: an id from crm.persons.list.

limitintegerqueryoptional

How many items this page brings at most (see the maximum and the default in the schema).

Default: 50

cursorstringqueryoptional

The next_cursor of the previous page, exactly as it came (only for the same list and filters); without it, the first page.

Response

dataarray<object>
has_moreboolean
next_cursorstring

Errors

Example

curl "https://api.mb-suite.com/v1/crm/persons/{person_id}/enrollments" \
  -H "Authorization: Bearer $MB_API_KEY"

Try it with your test API key

With a test API key the request always goes to your practice organization: example data you can reset.

curl "https://api.mb-suite.com/v1/crm/persons/{person_id}/enrollments" \
  -H "Authorization: Bearer $MB_TEST_KEY"