Menú

Paginación, errores, idempotencia y versiones

Las reglas comunes a todos los endpoints: formato y entradas estrictas, listas con cursor, el error único, reintentos seguros, la vista previa y su confirmación, versiones y límites.

Formato

  • JSON en snake_case en los dos sentidos (pipeline_id, has_more). Las claves que son datos tuyos —los campos personalizados, los encabezados— se respetan tal cual.
  • Los parámetros de ruta van en la URL (/crm/deals/{deal_id}). En un GET, el resto va en la query: limit=50, include_archived=true, listas como label_ids=a,b o label_ids[]=a, objetos como JSON (filter={…}). En POST, PATCH y DELETE, en un cuerpo JSON de hasta 1 MB con Content-Type: application/json.
  • Cada respuesta trae MB-Request-Id (req_…): inclúyelo si nos escribes por un problema.
  • En las respuestas, una operación nueva o un campo nuevo pueden aparecer sin aviso: ignora lo que no conozcas.
  • En lo que mandas es al revés: un campo que la operación no tiene es 400 unknown_field, en vez de ignorarse (un filtro mal escrito nunca devuelve la lista sin filtrar). param lo nombra y, si parece que va en otro nivel del cuerpo, el mensaje dice dónde. Los mapas de datos tuyos (campos personalizados, encabezados) aceptan cualquier clave.
  • Una operación masiva dice cuántos elementos acepta como máximo: «Masiva · hasta N» en la referencia, x-mb-bulk.max_items en el contrato y bulk.max_items en mb_describe_operation (x-mb-bulk.maxItems sigue saliendo, obsoleto).

Paginación

Las listas devuelven { data, has_more, next_cursor }. Pide la página siguiente con cursor=<next_cursor> hasta que has_more sea false. En las listas del CRM, limit va de 1 a 200 (50 por defecto); la referencia dice el rango de cada lista. Un cursor que no se puede leer es 400 invalid_cursor: manda next_cursor tal como vino, a la misma lista.

Algunos filtros se aplican sobre la página leída: una página puede traer menos de limit —incluso ninguno— con has_more en true. Sigue el cursor; no cuentes 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);
  // Una página puede traer menos de limit (incluso ninguno) con has_more en true: sigue el cursor.
  cursor = page.has_more ? page.next_cursor : undefined;
} while (cursor);

Errores

Todos los errores tienen la misma forma y el HTTP de su tipo. code es estable: decide con él y no con message, que puede cambiar de redacción. param nombra el campo cuando aplica y details trae datos extra estables de algunos códigos. Cuando el input no cumple el schema, details.issues trae cada problema con su path y su message (hasta 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"
  }
}
TipoHTTPQué hacer
invalid_request400El pedido está mal armado: JSON inválido, un campo que no cumple el schema o un encabezado con un valor que no corresponde. param nombra el campo cuando aplica. No reintentes sin corregirlo.
authentication401Falta la credencial, no es válida o ya no sirve (revocada, vencida, suspendida). La respuesta trae WWW-Authenticate. Revisa la API key o renueva el token.
payment_required402La suscripción de la organización está bloqueada por falta de pago: la API y el MCP quedan en pausa hasta regularizarla.
permission403La credencial es válida pero no alcanza: a la persona detrás le falta el permiso o el rol (role_required), a la credencial le falta el alcance, el módulo no está activo, la API y el MCP no están habilitados (platform_not_enabled) o la suscripción no los incluye (platform_requires_paid_plan), o la organización es de demostración (demo_read_only). Pedir de nuevo no cambia nada.
not_found404No existe en esta organización, o tu credencial no lo puede ver; una cuenta externa que no está vinculada responde external_account_not_linked. Nunca decimos si un registro de otra organización existe: para ti, no existe.
conflict409El estado no permite la operación: el del registro (ya se restauró, ya está en curso), un tope (limit_reached), la integración (integration_not_connected, integration_reconnect_required) o la plataforma externa, que lo rechazó (external_rejected) o no confirmó un envío (external_outcome_unknown). El code dice cuál; algunos traen details. Se guarda en la idempotencia: con la misma clave vuelve la misma respuesta.
confirmation_required409La operación pide confirmación (es masiva, irreversible, manda o cambia algo fuera de MB Suite, tiene un costo aparte o es de las que la piden siempre): primero dry_run: true, después el mismo pedido con el confirm_token.
idempotency422La Idempotency-Key (en el MCP, idempotency_key) ya se usó con otro pedido. Usa una clave nueva para un pedido nuevo.
rate_limit429Un freno: la ráfaga de tu credencial (rate_limited), el tope diario o la espera de una acción manual (daily_limit_reached, cooldown_active) o la plataforma externa, que está limitando (external_rate_limited). Espera lo que dice Retry-After (segundos) y reintenta.
quota429La organización llegó a su cupo diario. Se reinicia a las 00:00 UTC; details.quota dice cuál y cuándo.
maintenance503La plataforma o el módulo están en mantenimiento. Reintenta después de Retry-After.
unavailable503Un servicio que la operación necesita no está disponible ahora: la plataforma externa no contesta (external_unavailable), el servicio de PDF está caído (pdf_unavailable) o la base de MB Suite no contestó al comprobar la credencial (platform_unavailable). No cambió nada y no se guarda en la idempotencia: reintenta después de Retry-After con la misma clave.
upstream502La IA no devolvió un resultado que sirva (ai_generation_failed): vacío, fuera de formato o cortado. No se guardó nada y no se guarda en la idempotencia. Los SDK no lo reintentan solos: reintentarlo vuelve a llamar a la IA (y gasta créditos si responde).
internal500Algo falló de nuestro lado (lo de un tercero nunca es un 500: tiene su propio código); ya quedó registrado. Reintenta con backoff y, si se repite, escríbenos con el request_id.

La lista completa de códigos, con qué hacer en cada caso, está en Errores. Un 401 trae además WWW-Authenticate; un 429, un 503 y un 409 idempotency_in_progress, Retry-After.

Lo que se repite entre módulos tiene siempre el mismo código, tipo y details: la integración sin conectar o para reconectar (integration_not_connected, integration_reconnect_required), una cuenta externa sin vincular (external_account_not_linked), la plataforma externa que rechaza, no contesta o limita (external_rejected, external_unavailable, external_rate_limited) o que no confirma un envío que puede haber salido (external_outcome_unknown), una organización de demostración (demo_read_only), un rol (role_required), un tope (limit_reached, daily_limit_reached, cooldown_active), la IA (ai_generation_failed) y el PDF (pdf_unavailable). Un 500 es siempre algo de MB Suite.

Idempotencia

Los POST, PATCH y DELETE aceptan Idempotency-Key (hasta 255 caracteres; un UUID por operación lógica). Durante 24 horas:

  • La misma clave con el mismo pedido devuelve la respuesta guardada, con Idempotent-Replayed: true, sin volver a ejecutar.
  • La misma clave con otro pedido: 422 idempotency_key_reused.
  • Si el primero todavía corre: 409 idempotency_in_progress, con Retry-After.
  • Si el primero corrió pero su respuesta no quedó guardada (era demasiado grande o no se pudo guardar): 409 idempotency_response_unavailable. No se vuelve a ejecutar: lee el recurso en vez de repetir el pedido.
  • Un 5xx (el 502 de la IA y el 503 de una plataforma externa incluidos) o un 429 no se guardan: reintenta con la misma clave y esta vez se ejecuta.
  • El resto de los 4xx sí: un 409 external_rejected o limit_reached vuelve igual con la misma clave. Después de corregir la causa, manda el pedido con una clave nueva.

La clave es de cada credencial: dos API keys distintas no comparten claves. Las lecturas no la necesitan. Por el MCP es lo mismo con el argumento idempotency_key (ver MCP).

Vista previa y confirmación

Fuera de la pantalla, algunas operaciones no se ejecutan de un tirón: las masivas, las irreversibles, las que mandan o cambian algo fuera de MB Suite (un correo, un WhatsApp, un SMS, una publicación, un cambio en una plataforma de anuncios), las que tienen un costo aparte (la IA, DataForSEO, un rastreo) y las que la piden siempre (como editar un agente de IA que atiende en vivo). Primero manda el mismo cuerpo con "dry_run": true: no escribe nada y devuelve qué va a pasar y un confirm_token que vale 10 minutos y una sola vez, atado a esa operación, ese input, esa persona y esa credencial. Después repite el pedido con confirm_token en lugar de dry_run; un segundo uso del mismo token es 409 confirm_token_used (para reintentar sin riesgo, la misma clave de idempotencia). Sin token, la respuesta es 409 confirmation_required.

En el contrato lo dicen x-mb-kind (irreversible), x-mb-bulk y x-mb-confirm: always. Unas pocas la piden solo con ciertos datos: x-mb-confirm: conditional, con la condición en x-mb-confirm-when (por ejemplo, invitar a un miembro solo si sale el correo); sin esos datos se ejecutan directo. Cuando es porque algo sale afuera o paga, el 409 lo dice: details.reason es token_required, details.external nombra la plataforma y details.effect dice qué hace (sends, sends_later o pays). Un token que no sirve trae en details.reason si venció (expired), si es de otra operación, input o persona (mismatch) o si vino mal (malformed). Cualquier otra escritura acepta también "dry_run": true para ver qué haría sin escribir nada (su needs_confirmation dice false: se ejecuta sin el token); una lectura no tiene seco (400 dry_run_not_supported).

La vista previa trae summary, counts, items y warnings, en inglés como los demás mensajes de la API; needs_confirmation, que dice si ejecutarla pide el token, y, según la operación, validated (si comprobó que existe lo que toca) y skipped_ids (los ids de un lote que no va a tocar). Lo que sale afuera o paga lo avisa en warnings.

# 1. Vista previa: no borra nada y devuelve un 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. Ejecutar: el mismo cuerpo, con el confirm_token en vez 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" }'
Respuesta del 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
}
El DELETE simple de un registro que va a la papelera (un trato, una persona) se ejecuta directo con una API key: queda 30 días en la papelera. Con un token OAuth, por el MCP, y para todo lo masivo o lo que no vuelve, la vista previa es obligatoria. Una baja que deja copia trae en la respuesta trash_item_id (en un lote, trash_item_ids) y trash_expires_at: se restaura con POST /v1/trash/{item_id}/restore (owner o admin).

Versiones

La API se versiona por fecha; la vigente es 2026-11-01. Cada organización queda fijada en la versión con la que creó su primera API key; un pedido puede pedir otra versión viva con MB-Version: AAAA-MM-DD, y la respuesta dice en MB-Version cuál usó. Un token OAuth usa la vigente.

  • Un cambio que rompe (quitar un campo, cambiar un tipo, volver requerido un parámetro) sale siempre en una versión nueva; la anterior sigue viva 12 meses.
  • Una versión en retiro responde con Deprecation y Sunset (RFC 8594).
  • Una versión que no existe o ya no está viva: 400 api_version_invalid o api_version_unsupported.

Los cambios de cada versión están en el Registro de cambios.

Límites

Cada organización tiene frenos por ráfaga y cupos diarios según su plan. Al pasarse: 429 rate_limited con Retry-After, o 429 quota_exceeded hasta las 00:00 UTC. Cada operación declara cuántas lecturas y escrituras puede hacer y eso es lo que descuenta del cupo; las que tocan una plataforma externa (enviar un WhatsApp, un correo) cuentan además en «operaciones externas».

PlanRáfaga por API key (10 s)Por minuto (organización)Llamadas por díaLecturas por díaEscrituras por díaOperaciones externas por díaAPI keys activasWebhooks activos
Standard5030020.000100.00020.00030055
Growth10060050.000250.00050.0001.0001010
Scale1501.200150.000750.000150.0002.5002520
Enterprise2002.400500.0002.500.000500.0005.0005020
Práctica (API keys de prueba)251505.00025.0005.0005055
La prueba gratis no incluye la API ni el MCP: funcionan con una suscripción paga activa. La fila Práctica es el cupo de la organización de práctica, donde trabajan las API keys de prueba, y nunca gasta el de tu workspace.

Son los límites de cada plan: GET /v1/me devuelve los que rigen hoy para tu organización, que pueden estar ajustados. Hay además un freno por IP: en la API, 600 pedidos por minuto, antes de leer la credencial; en el MCP, 300 por minuto que cuentan solo los pedidos sin una credencial válida (falta, no es de MB Suite, está revocada, suspendida o vencida), así los pedidos válidos no suman. Los dos responden 429 rate_limited con Retry-After.