Iniciar sesión Crear cuenta
Guías

Servidor MCP

Conecta Claude, ChatGPT, Gemini, Cursor, VS Code y otros asistentes de IA a tu cuenta de Postal: 34 herramientas, OAuth o clave de API, permisos por alcance y las mismas reglas de la API.

#Qué es

El servidor MCP (Model Context Protocol) de Postal permite que asistentes de IA, como Claude, ChatGPT, Gemini, Cursor, VS Code y Claude Code, usen tu cuenta: enviar correos electrónicos, comprobar una entrega, ajustar contactos, escribir y enviar campañas, leer el buzón de entrada. El asistente llama a herramientas (34 hoy), y cada una ejecuta el mismo endpoint de la API REST.

Dirección: https://api.postal.so/mcp. Es el transporte Streamable HTTP de MCP, sin sesión: cada llamada es un POST con un mensaje JSON-RPC y lleva su propia credencial. Cada llamada actúa sobre un equipo: el que elegiste al conectar la aplicación o el equipo de la clave de API.

  • Las mismas reglas de la API. La validación, los límites del plan, las cuotas de envío, la pausa de envío, la lista de supresión y el límite de solicitudes del equipo valen igual que en cualquier llamada. Los errores llegan como nome: mensagem (por ejemplo validation_error: … o sending_paused: …), los mismos nombres de error de la API.
  • Dos formas de acceder. OAuth, en el que la persona inicia sesión en Postal, elige el equipo y puede revocar el acceso después; o una clave de API en el encabezado Authorization.
  • Permisos por alcance. Una aplicación conectada puede solo leer, solo enviar o hacerlo todo, y el asistente solo ve las herramientas que puede usar.
  • Las acciones irreversibles piden confirmación. Enviar una campaña exige confirm: true, y las instrucciones del servidor indican al asistente que confirme contigo los destinatarios y el contenido antes de enviar correos electrónicos.

Depende del plan

El servidor MCP es un recurso del plan (mcp_access). Sin él, el endpoint responde 403 plan_limit_reached. Consulta Límites.

#Conectar un asistente

En el panel, abre Configuración › Conexiones de IA: la pantalla muestra la URL del servidor y el paso a paso de cada asistente. Las aplicaciones de chat usan OAuth; las herramientas de desarrollo aceptan OAuth o una clave de API.

AsistenteAccesoCómo conectar
ClaudeOAuthEn la configuración de conectores (web o escritorio), añade un conector personalizado, pega la URL y conecta: inicias sesión en Postal, eliges el equipo y autorizas.
ChatGPTOAuthEn la configuración de aplicaciones y conectores (algunos planes piden el modo desarrollador), crea un conector con la URL y la autenticación OAuth.
GeminiOAuth o claveEn Gemini CLI, añade el servidor en ~/.gemini/settings.json (o usa la URL en las aplicaciones de Gemini que aceptan servidores MCP). Sin el encabezado, Gemini abre el inicio de sesión de Postal en el primer uso.
CursorOAuth o claveEn la configuración de MCP (o en ~/.cursor/mcp.json), añade un servidor. Solo con la URL, Cursor abre el inicio de sesión de Postal para que elijas el equipo.
VS CodeOAuth o claveEjecuta MCP: Add Server, elige HTTP y pega la URL; en la primera llamada VS Code te pide iniciar sesión. Con clave, usa el .vscode/mcp.json de abajo.
Claude CodeOAuth o claveAñade el servidor con el comando de abajo. Sin el encabezado, ejecuta /mcp dentro de Claude Code para iniciar sesión en Postal.

Para acceder con una clave de API, usa los ejemplos de abajo y cambia ps_... por tu clave. Los nombres de las pantallas de cada asistente cambian con frecuencia; si tienes dudas, sigue su documentación.

Configuración con clave de API

Claude Code

claude mcp add --transport http postal https://api.postal.so/mcp \
  --header "Authorization: Bearer ps_..."

Cursor

// ~/.cursor/mcp.json
{
  "mcpServers": {
    "postal": {
      "url": "https://api.postal.so/mcp",
      "headers": { "Authorization": "Bearer ps_..." }
    }
  }
}

VS Code

// .vscode/mcp.json
{
  "servers": {
    "postal": {
      "type": "http",
      "url": "https://api.postal.so/mcp",
      "headers": { "Authorization": "Bearer ${input:postal-api-key}" }
    }
  },
  "inputs": [
    { "id": "postal-api-key", "type": "promptString", "description": "Postal API key", "password": true }
  ]
}

Gemini CLI

// ~/.gemini/settings.json
{
  "mcpServers": {
    "postal": {
      "httpUrl": "https://api.postal.so/mcp",
      "headers": { "Authorization": "Bearer ps_..." }
    }
  }
}

Otros clientes MCP que leen un archivo con mcpServers y aceptan encabezados usan el mismo bloque de Cursor.

#Autenticación

El servidor acepta dos credenciales, siempre como Authorization: Bearer. No se acepta ninguna otra, y Postal no reenvía el token a terceros.

OAuthClave de API
Para quiénLos conectores de Claude, ChatGPT y Gemini y cualquier aplicación que pueda abrir una ventana de inicio de sesión.Herramientas de desarrollo que aceptan encabezados (Claude Code, Cursor, VS Code) y scripts.
Cómo se accedeLa persona inicia sesión en Postal, elige un equipo y autoriza. La aplicación recibe un token de acceso (1 hora) y uno de renovación (30 días).Una clave que empieza con ps_, creada en Claves de API.
PermisosLos alcances que la aplicación pidió y la persona aceptó.Acceso total (full_access) se convierte en mcp:full; solo envío (sending_access) se convierte en mcp:send.
RevocarEn Conexiones de IA, en cualquier momento.Eliminando la clave, en Claves de API.

Una clave de API restringida a un dominio solo envía desde ese dominio, como en la API. Un token OAuth deja de valer al instante cuando se revoca la conexión, cuando la persona sale del equipo o cuando la cuenta se suspende.

Prefiere OAuth cuando la aplicación lo ofrezca: eliges el equipo, ves lo que hizo la aplicación y revocas sin cambiar ninguna clave. Si usas una clave, prefiere una solo de envío cuando el asistente únicamente necesite enviar correos electrónicos y eventos.

Sin credencial, /mcp responde 401 con el encabezado WWW-Authenticate que apunta a los metadatos del recurso (/.well-known/oauth-protected-resource/mcp): así es como un cliente MCP descubre cómo iniciar OAuth.

#Alcances y permisos

Cada herramienta pide un alcance. mcp:full vale por todos; mcp:send no incluye la lectura (una aplicación que solo envía no ve correos, contactos ni el uso). El asistente solo ve, en tools/list, las herramientas que el token o la clave puede llamar.

AlcanceNombre en la pantalla de autorizaciónQué permiteHerramientas
mcp:readLeer tus datosVer correos, dominios, contactos, segmentos, temas, plantillas, campañas, automatizaciones, formularios, buzones de entrada, métricas y uso.22 herramientas de lectura
mcp:sendEnviar correos y eventosEnviar y programar correos de tus dominios verificados y registrar eventos para tus automatizaciones.send_email, track_event
mcp:fullGestionar todoCrear, modificar y eliminar contactos, plantillas y campañas, enviar campañas, cancelar correos programados y trabajar en las conversaciones de los buzones de entrada (organizar, responder, guardar borradores). Incluye los otros dos alcances.Las 34 herramientas

Si la aplicación no pide alcances, la pantalla ofrece los tres. La persona autoriza o rechaza la solicitud en conjunto.

Cuando falta un permiso

  • Conexión OAuth: llamar a una herramienta fuera de los alcances da 403 con WWW-Authenticate: Bearer error="insufficient_scope", scope="…" y el cuerpo JSON-RPC insufficient_scope: …. El cliente puede pedir al usuario que vuelva a conectar con el alcance que falta.
  • Clave de API: una clave solo de envío que llama a una herramienta de lectura o de gestión recibe un error de herramienta restricted_api_key. Atención: en MCP, get_email y get_usage piden mcp:read, aunque la API REST permita que una clave solo de envío lea esos dos endpoints.

#Herramientas

Los argumentos de cada herramienta siguen los campos del endpoint correspondiente, y el cliente los lee del inputSchema de tools/list. Las listas aceptan limit (de 1 a 100, por defecto 20) y los cursores after y before: pasa el último id en after mientras has_more sea true. Las herramientas también declaran si solo leen, si son destructivas y si son idempotentes (annotations), para que el asistente decida cuándo pedir confirmación.

E-mails

HerramientaAlcanceQué haceEndpoint de la API
send_emailmcp:sendEnvía o programa un correo transaccional (hasta 50 destinatarios; HTML, texto o plantilla publicada).POST /emails
list_emailsmcp:readLista los correos, con filtros de texto, estado, dominio y fechas.GET /emails
get_emailmcp:readUn correo con el contenido, el estado y la línea de tiempo de eventos.GET /emails/{id}
cancel_scheduled_emailmcp:fullCancela un correo todavía programado.POST /emails/{id}/cancel

Dominios

HerramientaAlcanceQué haceEndpoint de la API
list_domainsmcp:readDominios de envío, con el estado de la verificación y la región.GET /domains
get_domainmcp:readUn dominio con los registros DNS y lo que está mal en cada uno.GET /domains/{id}

Contactos y audiencia

HerramientaAlcanceQué haceEndpoint de la API
list_contactsmcp:readContactos, con filtros por texto, segmento, tema y baja.GET /contacts
get_contactmcp:readUn contacto por ID o correo.GET /contacts/{id}
create_contactmcp:fullCrea un contacto, con segmentos y temas. Cuenta en el límite de contactos del plan.POST /contacts
update_contactmcp:fullModifica solo los campos enviados de un contacto.PATCH /contacts/{id}
delete_contactmcp:fullElimina un contacto y sus suscripciones, sin vuelta atrás.DELETE /contacts/{id}
list_segmentsmcp:readSegmentos estáticos y dinámicos, con el total de contactos.GET /segments
list_topicsmcp:readTemas de suscripción.GET /topics
list_contact_propertiesmcp:readPropiedades personalizadas de los contactos (clave, tipo y valor por defecto).GET /contact-properties

Plantillas

HerramientaAlcanceQué haceEndpoint de la API
list_templatesmcp:readPlantillas, publicadas o borradores.GET /templates
get_templatemcp:readUna plantilla por ID o alias.GET /templates/{id}
create_templatemcp:fullCrea una plantilla; con publish: true ya la publica.POST /templates

Campañas

HerramientaAlcanceQué haceEndpoint de la API
list_broadcastsmcp:readCampañas, con estado y estadísticas.GET /broadcasts
get_broadcastmcp:readUna campaña con la audiencia, el contenido y las estadísticas de entrega.GET /broadcasts/{id}
create_broadcastmcp:fullCrea una campaña como borrador. No se envía nada.POST /broadcasts
send_broadcastmcp:fullEnvía o programa una campaña. Exige confirm: true.POST /broadcasts/{id}/send

Automatizaciones y eventos

HerramientaAlcanceQué haceEndpoint de la API
list_automationsmcp:readAutomatizaciones, con estado y total de ejecuciones.GET /automations
get_automationmcp:readUna automatización con el disparador y los pasos.GET /automations/{id}
track_eventmcp:sendRegistra un evento de un contacto, que puede iniciar automatizaciones (y enviar correos).POST /events

Métricas y uso

HerramientaAlcanceQué haceEndpoint de la API
get_metricsmcp:readEnvíos, entregas, aperturas, clics, rebotes y quejas de un período.GET /emails/metrics
get_usagemcp:readPlan, uso y límites, límite de solicitudes y si el envío está en pausa.GET /usage

Formularios

HerramientaAlcanceQué haceEndpoint de la API
list_formsmcp:readFormularios de suscripción, con el total de envíos.GET /forms
list_form_submissionsmcp:readEnvíos de un formulario: respuestas, contacto y double opt-in.GET /forms/{id}/submissions

Buzones de entrada

HerramientaAlcanceQué haceEndpoint de la API
list_inboxesmcp:readBuzones de entrada del equipo y la configuración del agente de IA.GET /inboxes
list_inbox_threadsmcp:readConversaciones de un buzón de entrada, con filtros de carpeta, etiqueta, no leídas, responsable y texto.GET /inboxes/{id}/threads
get_inbox_threadmcp:readUna conversación con todos los mensajes y borradores (incluido el otp_token de los mensajes recibidos).GET /inboxes/{id}/threads/{thread_id}
update_inbox_threadmcp:fullCambia la carpeta, marca como leída, asigna y etiqueta una conversación.PATCH /inboxes/{id}/threads/{thread_id}
reply_to_inbox_threadmcp:fullResponde a una conversación desde la dirección del buzón de entrada. Envía un correo de verdad.POST /inboxes/{id}/threads/{thread_id}/reply
create_inbox_draftmcp:fullGuarda un borrador para que una persona lo revise y lo envíe en el panel. No envía.POST /inboxes/{id}/drafts

Lo que el asistente no debe hacer por sí solo

send_email, send_broadcast y reply_to_inbox_thread llegan a personas reales y no se pueden deshacer. Las instrucciones del servidor indican al asistente que confirme contigo antes los destinatarios, el remitente, el asunto y el contenido. send_broadcast rechaza la llamada con confirmation_required si confirm no es true. Para una respuesta que una persona deba revisar, usa create_inbox_draft, que solo guarda el borrador. Para probar sin enviar de verdad, envía a las direcciones de prueba ([email protected]…): no se entrega nada y no se usa la cuota, pero los eventos y los webhooks son reales.

#Recursos y prompts

Además de las herramientas, el servidor ofrece contexto listo y recetas:

TipoNombreQué es
Recursopostal://domainsLos dominios de envío del equipo y la verificación de cada uno, en JSON (los primeros 100). Pide mcp:read.
Recursopostal://docs/quickstartUna guía breve, en Markdown, de cómo encajan los dominios, el envío, las plantillas, los contactos, las campañas, las automatizaciones, los buzones de entrada y los errores.
Promptwrite_and_send_campaignGuía al asistente desde el objetivo hasta una campaña enviada o programada, con tu aprobación en cada paso. Argumentos: goal (obligatorio), segment y send_at.
Promptdiagnose_deliveryInvestiga por qué un correo no llegó o fue a spam: eventos, registros DNS, métricas y límites, sin enviar nada. Argumentos: email_id, recipient y domain.

Los prompts aparecen con los títulos “Escribir y enviar campaña” y “Diagnosticar entrega”.

#Ejemplos

Un asistente hace estas llamadas por ti, pero son solo JSON-RPC sobre HTTP, lo que ayuda a probar la clave y entender las respuestas. Los encabezados Accept y MCP-Protocol-Version acompañan a toda llamada; la respuesta es JSON. No hace falta abrir sesión (initialize) antes: cada POST se basta por sí solo.

Listar las herramientas

POST /mcp (tools/list)

cURL

curl -X POST https://api.postal.so/mcp \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  }'

Node.js

const response = await fetch('https://api.postal.so/mcp', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
    Accept: 'application/json, text/event-stream',
    'MCP-Protocol-Version': '2025-11-25',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'tools/list',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/mcp",
    headers={
        "Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}",
        "Accept": "application/json, text/event-stream",
        "MCP-Protocol-Version": "2025-11-25",
    },
    json={
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('mcp', [
    'headers' => [
        'Authorization' => 'Bearer '.getenv('POSTAL_API_KEY'),
        'Accept' => 'application/json, text/event-stream',
        'MCP-Protocol-Version' => '2025-11-25',
    ],
    'json' => [
        'jsonrpc' => '2.0',
        'id' => 1,
        'method' => 'tools/list',
    ],
]);

$data = json_decode($response->getBody(), true);
200Respuesta (resumida)
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "send_email",
        "title": "Send email",
        "description": "Sends (or schedules) a transactional email …",
        "inputSchema": { "type": "object", "properties": { … }, "required": ["from", "to"] },
        "annotations": { "title": "Send email", "readOnlyHint": false, "destructiveHint": false, "idempotentHint": false, "openWorldHint": true }
      },
      …
    ]
  }
}

Con una clave solo de envío, la lista trae únicamente send_email y track_event.

Enviar un correo

La herramienta send_email acepta los campos de POST /emails, y idempotency_key cumple la función del encabezado Idempotency-Key. El ejemplo envía a una dirección de prueba, así que no se entrega nada.

POST /mcp (tools/call)

cURL

curl -X POST https://api.postal.so/mcp \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "send_email",
      "arguments": {
        "from": "Loja Exemplo <[email protected]>",
        "to": ["[email protected]"],
        "subject": "Pedido enviado",
        "html": "<p>Seu pedido saiu para entrega.</p>",
        "idempotency_key": "pedido-1042-enviado"
      }
    }
  }'

Node.js

const response = await fetch('https://api.postal.so/mcp', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
    Accept: 'application/json, text/event-stream',
    'MCP-Protocol-Version': '2025-11-25',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 2,
    method: 'tools/call',
    params: {
      name: 'send_email',
      arguments: {
        from: 'Loja Exemplo <[email protected]>',
        to: ['[email protected]'],
        subject: 'Pedido enviado',
        html: '<p>Seu pedido saiu para entrega.</p>',
        idempotency_key: 'pedido-1042-enviado',
      },
    },
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/mcp",
    headers={
        "Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}",
        "Accept": "application/json, text/event-stream",
        "MCP-Protocol-Version": "2025-11-25",
    },
    json={
        "jsonrpc": "2.0",
        "id": 2,
        "method": "tools/call",
        "params": {
            "name": "send_email",
            "arguments": {
                "from": "Loja Exemplo <[email protected]>",
                "to": ["[email protected]"],
                "subject": "Pedido enviado",
                "html": "<p>Seu pedido saiu para entrega.</p>",
                "idempotency_key": "pedido-1042-enviado",
            },
        },
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('mcp', [
    'headers' => [
        'Authorization' => 'Bearer '.getenv('POSTAL_API_KEY'),
        'Accept' => 'application/json, text/event-stream',
        'MCP-Protocol-Version' => '2025-11-25',
    ],
    'json' => [
        'jsonrpc' => '2.0',
        'id' => 2,
        'method' => 'tools/call',
        'params' => [
            'name' => 'send_email',
            'arguments' => [
                'from' => 'Loja Exemplo <[email protected]>',
                'to' => ['[email protected]'],
                'subject' => 'Pedido enviado',
                'html' => '<p>Seu pedido saiu para entrega.</p>',
                'idempotency_key' => 'pedido-1042-enviado',
            ],
        ],
    ],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"id\":\"019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b\"}"
      }
    ],
    "structuredContent": {
      "id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
    },
    "isError": false
  }
}

El resultado llega en structuredContent (y también como texto en content). Sigue el correo con get_email.

Cuando la API rechaza

Un error de la API se convierte en un resultado con isError: true y el texto nome: mensagem, con el estado HTTP 200 de JSON-RPC. Aquí, el remitente no está en un dominio verificado:

200Respuesta
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "validation_error: The outro.com domain is not verified. Please, add and verify your domain."
      }
    ],
    "isError": true
  }
}

Los fallos de credencial y de permiso, en cambio, son respuestas HTTP (consulta Errores).

#OAuth para quien desarrolla un cliente MCP

Quien usa un asistente ya hecho no necesita esta parte. Es para quien construye un cliente MCP y quiere conectar Postal. El servidor de autorización es la propia API (https://api.postal.so) y sigue OAuth 2.1: flujo de código de autorización con PKCE, sin otros flujos.

1. Descubrimiento

  • GET /.well-known/oauth-protected-resource/mcp (RFC 9728) indica cuál es el servidor de autorización y los alcances del recurso. También existe en la raíz.
  • GET /.well-known/oauth-authorization-server (RFC 8414) trae los endpoints (authorization_endpoint, token_endpoint, registration_endpoint, revocation_endpoint), los alcances (mcp:read, mcp:send, mcp:full) y code_challenge_methods_supported: ["S256"]. Las dos respuestas se guardan en caché durante 1 hora.

2. Registro del cliente

Hay dos formas, y ambas evitan el registro manual:

  • Registro dinámico (RFC 7591): POST /oauth/register. Los clientes están limitados por IP (30 por minuto y 300 por hora), y los que nunca completan una autorización se borran pasadas 24 horas.
  • Client ID Metadata Document: usa como client_id una URL HTTPS que devuelva el JSON del cliente. Postal obtiene el documento sin seguir redirecciones, con un plazo de 5 segundos y hasta 5 KB, nunca en una red privada, y lo guarda durante 1 hora.

Las redirect_uris (hasta 10) deben ser https, de loopback (http://localhost, 127.0.0.1, [::1]) o de un esquema propio de una aplicación de escritorio conocida (cursor, vscode, vscode-insiders, windsurf, zed), sin credenciales ni fragmento. La solicitud de autorización debe usar una de ellas, exactamente igual. El método de autenticación por defecto es client_secret_basic (la respuesta trae un client_secret, una única vez); con none el cliente es público y se apoya solo en PKCE.

POST /oauth/register

cURL

curl -X POST https://api.postal.so/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Meu assistente",
    "redirect_uris": ["https://assistente.exemplo.com/oauth/callback"],
    "token_endpoint_auth_method": "none",
    "scope": "mcp:read mcp:send"
  }'

Node.js

const response = await fetch('https://api.postal.so/oauth/register', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    client_name: 'Meu assistente',
    redirect_uris: ['https://assistente.exemplo.com/oauth/callback'],
    token_endpoint_auth_method: 'none',
    scope: 'mcp:read mcp:send',
  }),
});

const data = await response.json();

Python

import requests

response = requests.post(
    "https://api.postal.so/oauth/register",
    json={
        "client_name": "Meu assistente",
        "redirect_uris": ["https://assistente.exemplo.com/oauth/callback"],
        "token_endpoint_auth_method": "none",
        "scope": "mcp:read mcp:send",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('oauth/register', [
    'json' => [
        'client_name' => 'Meu assistente',
        'redirect_uris' => ['https://assistente.exemplo.com/oauth/callback'],
        'token_endpoint_auth_method' => 'none',
        'scope' => 'mcp:read mcp:send',
    ],
]);

$data = json_decode($response->getBody(), true);
201Respuesta
{
  "client_id": "0198f3c2-7a41-7d0e-9b3a-5e6f7a8b9c0d",
  "client_id_issued_at": 1793000000,
  "client_name": "Meu assistente",
  "redirect_uris": ["https://assistente.exemplo.com/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

3. Autorización

Abre el navegador de la persona en GET /oauth/authorize. Son obligatorios response_type=code, client_id, redirect_uri y PKCE con code_challenge_method=S256. Envía también resource (la dirección de MCP, https://api.postal.so/mcp, o el origen https://api.postal.so), scope y state. Quien no ha iniciado sesión va al inicio de sesión del panel y vuelve; la pantalla de consentimiento muestra la aplicación, la dirección de retorno y los permisos, y la persona elige el equipo. La aplicación no elige el equipo: un alcance team:… en la solicitud se rechaza con invalid_scope. Con prompt=none el servidor responde login_required o consent_required en lugar de mostrar pantallas.

URL de autorización (ejemplo)
https://api.postal.so/oauth/authorize?response_type=code&client_id=0198f3c2-7a41-7d0e-9b3a-5e6f7a8b9c0d&redirect_uri=https%3A%2F%2Fassistente.exemplo.com%2Foauth%2Fcallback&scope=mcp%3Aread%20mcp%3Asend&state=<state>&code_challenge=<code_challenge>&code_challenge_method=S256&resource=https%3A%2F%2Fapi.postal.so%2Fmcp

Después de que la persona autoriza, vuelve a la redirect_uri con code, state e iss (el emisor, RFC 9207: compruébalo). Si rechaza, vuelve con error=access_denied.

4. Tokens

Intercambia el code en POST /oauth/token (cuerpo application/x-www-form-urlencoded), con el code_verifier de PKCE y el mismo resource. Solo existen los grants authorization_code y refresh_token.

Intercambiar el código
curl -X POST https://api.postal.so/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=0198f3c2-7a41-7d0e-9b3a-5e6f7a8b9c0d \
  -d "redirect_uri=https://assistente.exemplo.com/oauth/callback" \
  -d code=<code> \
  -d code_verifier=<code_verifier> \
  -d "resource=https://api.postal.so/mcp"
200Respuesta
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "<JWT>",
  "refresh_token": "<refresh_token>"
}
  • El token de acceso es un JWT de 1 hora, con la audiencia de MCP (https://api.postal.so/mcp) y el alcance team:<id> del equipo elegido. Un token que no nombra a este servidor como audiencia se rechaza (401 invalid_token).
  • El token de renovación vale 30 días y se intercambia en cada uso: guarda siempre el nuevo, porque el anterior deja de servir.
  • Los errores de OAuth vienen en el formato de la RFC 6749 ({"error": "invalid_grant", "error_description": "…"}), no en el formato de error de la API.
Renovar
curl -X POST https://api.postal.so/oauth/token \
  -d grant_type=refresh_token \
  -d client_id=0198f3c2-7a41-7d0e-9b3a-5e6f7a8b9c0d \
  -d refresh_token=<refresh_token>

Para desconectar, la aplicación llama a POST /oauth/revoke (RFC 7009) con el token (de acceso o de renovación) y las credenciales del cliente. Esto cierra toda la conexión de la aplicación con ese equipo, como si la persona la revocara en el panel, y responde 200 incluso para un token desconocido.

Los endpoints de registro, token, revocación, metadatos y /mcp aceptan llamadas desde navegadores (CORS abierto, sin cookies). Límites: el token, 60 por minuto por cliente e IP; la autorización, 30 por minuto.

#Gestionar las conexiones

En Configuración › Conexiones de IA están las aplicaciones conectadas al equipo, con quién las conectó, cuándo y el último uso, y el historial reciente de conexiones, rechazos y revocaciones del equipo. Revocar corta el acceso al instante; para volver a usar la aplicación, hay que conectarla de nuevo.

  • Cada persona revoca sus propias conexiones. Los propietarios y administradores del equipo revocan las de cualquier persona.
  • Cada conexión es de una persona en un equipo: si la persona sale del equipo o la cuenta se suspende, los tokens dejan de valer.
  • Las claves de API no aparecen ahí; se gestionan en Claves de API.

#Límites

QuéLímite
PlanRecurso mcp_access; sin él, 403 plan_limit_reached
SolicitudesEl mismo límite por segundo del equipo (Límites), contado aparte de las llamadas REST; cada herramienta cuenta además como una solicitud de la API. Por encima, 429 rate_limit_exceeded
TransporteSolo POST en /mcp; GET y DELETE responden 405 (Allow: POST)
Listaslimit de 1 a 100 (por defecto 20), paginación por cursor
Destinatarios y programaciónLos de la API: hasta 50 destinatarios por correo y programación hasta 30 días por delante
Token de acceso OAuth1 hora
Token de renovación OAuth30 días, intercambiado en cada uso
Registro de clientes OAuth30 por minuto y 300 por hora, por IP

#Errores

Los problemas de credencial, de plan y de alcance son respuestas HTTP; los problemas de una llamada de herramienta (validación, cuotas, pausa de envío…) llegan como resultado con isError: true.

DóndeNombreCuándo ocurre
401unauthorizedFaltó el token. La respuesta trae WWW-Authenticate con los metadatos del recurso.
401invalid_tokenEl token OAuth no es válido, expiró, fue revocado o no es para este servidor (audiencia), la persona salió del equipo o la cuenta fue suspendida. Conecta la aplicación de nuevo.
401invalid_api_keyLa clave ps_… no existe o fue eliminada.
403plan_limit_reachedEl plan del equipo no incluye el servidor MCP.
403insufficient_scopeLa conexión OAuth no tiene el alcance de la herramienta llamada (JSON-RPC error.code -32001, con WWW-Authenticate y el alcance que falta).
429rate_limit_exceededEl equipo superó el límite de solicitudes por segundo. Espera el Retry-After.
toolrestricted_api_keyUna clave solo de envío llamó a una herramienta que no es de envío.
toolvalidation_errorDatos no válidos, como un remitente fuera de un dominio verificado.
toolsending_pausedEl envío del equipo está en pausa (demasiados rebotes o quejas).
tooldaily_quota_exceededSe agotó la cuota diaria de envío.
toolconfirmation_requiredsend_broadcast sin confirm: true.
toolnot_foundEl ID no existe en este equipo.

La fila “tool” es un error dentro de un resultado de tools/call (HTTP 200). Los demás nombres son los de la API; lee el mensaje y corrige la causa en lugar de repetir la llamada a ciegas.

#Consulta también