Menu

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}). Num GET, o resto vai na query: limit=50, include_archived=true, listas como label_ids=a,b ou label_ids[]=a, objetos como JSON (filter={…}). Em POST, PATCH e DELETE, num corpo JSON de até 1 MB com Content-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_field em vez de ser ignorado (um filtro mal escrito nunca devolve a lista sem filtrar). param o 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_items no contrato e bulk.max_items em mb_describe_operation (x-mb-bulk.maxItems continua 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.

Alguns filtros se aplicam sobre a página lida: uma página pode trazer menos que 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"
  }
}
TipoHTTPO que fazer
invalid_request400O 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.
authentication401A 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_required402A assinatura da organização está bloqueada por falta de pagamento: a API e o MCP ficam pausados até regularizar.
permission403A 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_found404Nã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.
conflict409O 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_required409A 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.
idempotency422A Idempotency-Key (no MCP, idempotency_key) já foi usada com outro pedido. Use uma chave nova para um pedido novo.
rate_limit429Um 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.
quota429A organização chegou à sua cota diária. Ela reinicia às 00:00 UTC; details.quota diz qual e quando.
maintenance503A plataforma ou o módulo estão em manutenção. Tente de novo depois de Retry-After.
unavailable503Um 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.
upstream502A 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).
internal500Algo 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, com Retry-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_rejected ou limit_reached volta 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" }'
Resposta do dry_run
{
  "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
}
O 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 Deprecation e Sunset (RFC 8594).
  • Uma versão que não existe ou não está mais ativa: 400 api_version_invalid ou api_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».

PlanoRajada por API key (10 s)Por minuto (organização)Chamadas por diaLeituras por diaEscritas por diaOperações externas por diaAPI keys ativasWebhooks ativos
Standard5030020.000100.00020.00030055
Growth10060050.000250.00050.0001.0001010
Scale1501.200150.000750.000150.0002.5002520
Enterprise2002.400500.0002.500.000500.0005.0005020
Prática (API keys de teste)251505.00025.0005.0005055
O teste grátis não inclui a API nem o MCP: eles funcionam com uma assinatura paga ativa. A linha Prática é a cota da organização de prática, onde trabalham as API keys de teste, e nunca gasta a do seu workspace.

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.