Paginação, erros, idempotência e versões
As regras comuns a todos os endpoints: formato e entradas estritas, listas com cursor, o erro único, novas tentativas seguras, a prévia e a confirmação, versões e limites.
Formato
- JSON em snake_case nos dois sentidos (
pipeline_id,has_more). As chaves que são dados seus —os campos personalizados, os cabeçalhos— são mantidas como estão. - Os parâmetros de rota vão na URL (
/crm/deals/{deal_id}). NumGET, o resto vai na query:limit=50,include_archived=true, listas comolabel_ids=a,boulabel_ids[]=a, objetos como JSON (filter={…}). EmPOST,PATCHeDELETE, num corpo JSON de até 1 MB comContent-Type: application/json. - Cada resposta traz
MB-Request-Id(req_…): inclua-o se escrever para nós sobre um problema. - Nas respostas, uma operação nova ou um campo novo podem aparecer sem aviso: ignore o que você não conhece.
- No que você envia é o contrário: um campo que a operação não tem é 400
unknown_fieldem vez de ser ignorado (um filtro mal escrito nunca devolve a lista sem filtrar).paramo nomeia e, se parece que ele vai em outro nível do corpo, a mensagem diz onde. Os mapas de dados seus (campos personalizados, cabeçalhos) aceitam qualquer chave. - Uma operação em massa diz quantos itens aceita no máximo: «Em massa · até N» na referência,
x-mb-bulk.max_itemsno contrato ebulk.max_itemsemmb_describe_operation(x-mb-bulk.maxItemscontinua saindo, obsoleto).
Paginação
As listas devolvem { data, has_more, next_cursor }. Peça a próxima página com cursor=<next_cursor> até has_more ser false. Nas listas do CRM, limit vai de 1 a 200 (50 por padrão); a referência diz o intervalo de cada lista. Um cursor que não pode ser lido é 400 invalid_cursor: envie next_cursor exatamente como veio, para a mesma lista.
limit —até nenhum— com has_more em true. Siga o cursor; não conte resultados.let cursor;
do {
const url = new URL("https://api.mb-suite.com/v1/crm/deals");
url.searchParams.set("limit", "200");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.MB_API_KEY}` } });
const page = await res.json();
for (const deal of page.data) console.log(deal.id, deal.title);
// Uma página pode trazer menos que limit (até nenhum) com has_more true: siga o cursor.
cursor = page.has_more ? page.next_cursor : undefined;
} while (cursor);Erros
Todos os erros têm a mesma forma e o HTTP do seu tipo. code é estável: decida com ele e não com message, cuja redação pode mudar. param nomeia o campo quando se aplica e details traz dados extras estáveis de alguns códigos. Quando o input não cumpre o schema, details.issues traz cada problema com o seu path e a sua message (até 10).
{
"error": {
"type": "not_found",
"code": "deal_not_found",
"message": "No deal with that id in this organization.",
"doc_url": "https://developers.mb-suite.com/errors#deal_not_found",
"request_id": "req_6f1d2c0b9a8e4f7d3c2b1a09"
}
}| Tipo | HTTP | O que fazer |
|---|---|---|
invalid_request | 400 | O pedido está mal formado: JSON inválido, um campo que não cumpre o schema ou um cabeçalho com um valor errado. param nomeia o campo quando se aplica. Não tente de novo sem corrigir. |
authentication | 401 | A credencial está faltando, é inválida ou não serve mais (revogada, vencida, suspensa). A resposta traz WWW-Authenticate. Revise a API key ou renove o token. |
payment_required | 402 | A assinatura da organização está bloqueada por falta de pagamento: a API e o MCP ficam pausados até regularizar. |
permission | 403 | A credencial é válida mas não alcança: falta a permissão ou o papel à pessoa por trás (role_required), falta o escopo à credencial, o módulo não está ativo, a API e o MCP não estão habilitados (platform_not_enabled) ou a assinatura não os inclui (platform_requires_paid_plan), ou a organização é de demonstração (demo_read_only). Pedir de novo não muda nada. |
not_found | 404 | Não existe nesta organização, ou sua credencial não pode vê-lo; uma conta externa que não está vinculada responde external_account_not_linked. Nunca dizemos se um registro de outra organização existe: para você, não existe. |
conflict | 409 | O estado não permite a operação: o do registro (já restaurado, já em andamento), um limite (limit_reached), a integração (integration_not_connected, integration_reconnect_required) ou a plataforma externa, que a rejeitou (external_rejected) ou não confirmou um envio (external_outcome_unknown). O code diz qual; alguns trazem details. É guardado na idempotência: com a mesma chave volta a mesma resposta. |
confirmation_required | 409 | A operação pede confirmação (em massa, irreversível, que envia ou muda algo fora do MB Suite, que tem um custo à parte ou das que sempre pedem): primeiro dry_run: true, depois o mesmo pedido com o confirm_token. |
idempotency | 422 | A Idempotency-Key (no MCP, idempotency_key) já foi usada com outro pedido. Use uma chave nova para um pedido novo. |
rate_limit | 429 | Um limite: a rajada da sua credencial (rate_limited), o limite diário ou a espera de uma ação manual (daily_limit_reached, cooldown_active) ou a plataforma externa, que está limitando (external_rate_limited). Espere o que diz Retry-After (segundos) e tente de novo. |
quota | 429 | A organização chegou à sua cota diária. Ela reinicia às 00:00 UTC; details.quota diz qual e quando. |
maintenance | 503 | A plataforma ou o módulo estão em manutenção. Tente de novo depois de Retry-After. |
unavailable | 503 | Um serviço de que a operação precisa não está disponível agora: a plataforma externa não responde (external_unavailable), o serviço de PDF está fora do ar (pdf_unavailable) ou o banco do MB Suite não respondeu ao verificar a credencial (platform_unavailable). Nada mudou e não é guardado na idempotência: tente de novo depois de Retry-After com a mesma chave. |
upstream | 502 | A IA não devolveu um resultado que sirva (ai_generation_failed): vazio, fora do formato ou cortado. Nada foi salvo e não é guardado na idempotência. Os SDKs não o repetem sozinhos: repetir chama a IA de novo (e gasta créditos se ela responder). |
internal | 500 | Algo falhou do nosso lado (a falha de um terceiro nunca é um 500: tem seu próprio código); já ficou registrado. Tente de novo com backoff e, se repetir, escreva para nós com o request_id. |
A lista completa de códigos, com o que fazer em cada caso, está em Erros. Um 401 traz também WWW-Authenticate; um 429, um 503 e um 409 idempotency_in_progress, Retry-After.
O que se repete entre módulos tem sempre o mesmo código, tipo e details: a integração sem conectar ou para reconectar (integration_not_connected, integration_reconnect_required), uma conta externa sem vincular (external_account_not_linked), a plataforma externa que rejeita, não responde ou limita (external_rejected, external_unavailable, external_rate_limited) ou que não confirma um envio que pode ter saído (external_outcome_unknown), uma organização de demonstração (demo_read_only), um papel (role_required), um limite (limit_reached, daily_limit_reached, cooldown_active), a IA (ai_generation_failed) e o PDF (pdf_unavailable). Um 500 é sempre algo do MB Suite.
Idempotência
Os POST, PATCH e DELETE aceitam Idempotency-Key (até 255 caracteres; um UUID por operação lógica). Durante 24 horas:
- A mesma chave com o mesmo pedido devolve a resposta guardada, com
Idempotent-Replayed: true, sem executar de novo. - A mesma chave com outro pedido: 422
idempotency_key_reused. - Se o primeiro ainda está rodando: 409
idempotency_in_progress, comRetry-After. - Se o primeiro rodou mas a resposta não ficou guardada (era grande demais ou não pôde ser salva): 409
idempotency_response_unavailable. Não executa de novo: leia o recurso em vez de repetir o pedido. - Um 5xx (o 502 da IA e o 503 de uma plataforma externa incluídos) ou um 429 não são guardados: tente de novo com a mesma chave e desta vez executa.
- O resto dos 4xx sim: um 409
external_rejectedoulimit_reachedvolta igual com a mesma chave. Depois de corrigir a causa, envie o pedido com uma chave nova.
A chave é de cada credencial: duas API keys diferentes não compartilham chaves. As leituras não precisam dela. Pelo MCP é o mesmo com o argumento idempotency_key (veja MCP).
Prévia e confirmação
Fora da tela, algumas operações não executam de uma vez: as em massa, as irreversíveis, as que enviam ou mudam algo fora do MB Suite (um e-mail, um WhatsApp, um SMS, uma publicação, uma mudança numa plataforma de anúncios), as que têm um custo à parte (a IA, o DataForSEO, um rastreamento) e as que sempre pedem (como editar um agente de IA que atende ao vivo). Primeiro envie o mesmo corpo com "dry_run": true: não escreve nada e devolve o que vai acontecer e um confirm_token válido por 10 minutos e por uma única execução, preso a essa operação, esse input, essa pessoa e essa credencial. Depois repita o pedido com confirm_token no lugar de dry_run; usar o mesmo token de novo é 409 confirm_token_used (para tentar de novo sem risco, a mesma chave de idempotência). Sem token, a resposta é 409 confirmation_required.
No contrato isso aparece em x-mb-kind (irreversible), x-mb-bulk e x-mb-confirm: always. Algumas poucas pedem só com certos dados: x-mb-confirm: conditional, com a condição em x-mb-confirm-when (por exemplo, convidar um membro só quando sai o e-mail); sem esses dados elas executam direto. Quando é porque algo sai para fora ou tem custo, o 409 diz isso: details.reason é token_required, details.external nomeia a plataforma e details.effect diz o que ela faz (sends, sends_later ou pays). Um token que não serve traz em details.reason se venceu (expired), se é de outra operação, input ou pessoa (mismatch) ou se veio malformado (malformed). Qualquer outra escrita também aceita "dry_run": true para ver o que faria sem escrever nada (o needs_confirmation dela diz false: executa sem o token); uma leitura não tem prévia (400 dry_run_not_supported).
A prévia traz summary, counts, items e warnings, em inglês como as outras mensagens da API; needs_confirmation, que diz se executá-la pede o token, e, conforme a operação, validated (se verificou que existe o que ela toca) e skipped_ids (os ids de um lote que ela não vai tocar). O que sai para fora ou tem custo é avisado em warnings.
# 1. Prévia: não exclui nada e devolve um confirm_token (10 minutos)
curl -X POST https://api.mb-suite.com/v1/crm/deals/bulk/delete \
-H "Authorization: Bearer $MB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "deal_ids": ["DEAL_A", "DEAL_B"], "dry_run": true }'
# 2. Executar: o mesmo corpo, com o confirm_token no lugar de dry_run
curl -X POST https://api.mb-suite.com/v1/crm/deals/bulk/delete \
-H "Authorization: Bearer $MB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "deal_ids": ["DEAL_A", "DEAL_B"], "confirm_token": "CONFIRM_TOKEN" }'{
"dry_run": true,
"summary": "About to run \"Delete many deals\" on 2 items: \"Plan anual — Acme\", \"Renovación — Beta\". Deleted data stays 30 days in the trash.",
"counts": { "items": 2 },
"items": [
{ "type": "deal", "id": "DEAL_A", "name": "Plan anual — Acme" },
{ "type": "deal", "id": "DEAL_B", "name": "Renovación — Beta" }
],
"warnings": [
"This preview checked that what the operation touches exists; your permission was checked for the operation as a whole, and its own rules are checked only when it runs, so it can still fail."
],
"generic": true,
"validated": true,
"needs_confirmation": true,
"confirm_token": "ZTNiMGM0…",
"expires_at": 1762000600000
}DELETE simples de um registro que vai para a lixeira (um negócio, uma pessoa) executa direto com uma API key: fica 30 dias na lixeira. Com um token OAuth, pelo MCP, e para tudo o que é em massa ou não volta, a prévia é obrigatória. Uma exclusão que deixa cópia traz na resposta trash_item_id (num lote, trash_item_ids) e trash_expires_at: restaure com POST /v1/trash/{item_id}/restore (owner ou admin).Versões
A API é versionada por data; a vigente é 2026-11-01. Cada organização fica fixada na versão com que criou a primeira API key; um pedido pode pedir outra versão ativa com MB-Version: AAAA-MM-DD, e a resposta diz em MB-Version qual usou. Um token OAuth usa a vigente.
- Uma mudança que quebra (tirar um campo, mudar um tipo, tornar um parâmetro obrigatório) sai sempre numa versão nova; a anterior continua ativa por 12 meses.
- Uma versão em retirada responde com
DeprecationeSunset(RFC 8594). - Uma versão que não existe ou não está mais ativa: 400
api_version_invalidouapi_version_unsupported.
As mudanças de cada versão estão no Registro de mudanças.
Limites
Cada organização tem limites de rajada e cotas diárias conforme o plano. Ao passar: 429 rate_limited com Retry-After, ou 429 quota_exceeded até as 00:00 UTC. Cada operação declara quantas leituras e escritas pode fazer e é isso que desconta da cota; as que usam uma plataforma externa (enviar um WhatsApp, um e-mail) contam também em «operações externas».
| Plano | Rajada por API key (10 s) | Por minuto (organização) | Chamadas por dia | Leituras por dia | Escritas por dia | Operações externas por dia | API keys ativas | Webhooks ativos |
|---|---|---|---|---|---|---|---|---|
| Standard | 50 | 300 | 20.000 | 100.000 | 20.000 | 300 | 5 | 5 |
| Growth | 100 | 600 | 50.000 | 250.000 | 50.000 | 1.000 | 10 | 10 |
| Scale | 150 | 1.200 | 150.000 | 750.000 | 150.000 | 2.500 | 25 | 20 |
| Enterprise | 200 | 2.400 | 500.000 | 2.500.000 | 500.000 | 5.000 | 50 | 20 |
| Prática (API keys de teste) | 25 | 150 | 5.000 | 25.000 | 5.000 | 50 | 5 | 5 |
São os limites de cada plano: GET /v1/me devolve os que valem hoje para a sua organização, que podem estar ajustados. Há também um limite por IP: na API, 600 pedidos por minuto, antes de ler a credencial; no MCP, 300 por minuto que contam só os pedidos sem uma credencial válida (faltando, que não é do MB Suite, revogada, suspensa ou vencida), assim os pedidos válidos não somam. Os dois respondem 429 rate_limited com Retry-After.