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 ejemplovalidation_error: …osending_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.
| Asistente | Acceso | Cómo conectar |
|---|---|---|
| Claude | OAuth | En 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. |
| ChatGPT | OAuth | En la configuración de aplicaciones y conectores (algunos planes piden el modo desarrollador), crea un conector con la URL y la autenticación OAuth. |
| Gemini | OAuth o clave | En 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. |
| Cursor | OAuth o clave | En 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 Code | OAuth o clave | Ejecuta 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 Code | OAuth o clave | Añ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.
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.
| OAuth | Clave de API | |
|---|---|---|
| Para quién | Los 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 accede | La 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. |
| Permisos | Los 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. |
| Revocar | En 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.
| Alcance | Nombre en la pantalla de autorización | Qué permite | Herramientas |
|---|---|---|---|
mcp:read | Leer tus datos | Ver correos, dominios, contactos, segmentos, temas, plantillas, campañas, automatizaciones, formularios, buzones de entrada, métricas y uso. | 22 herramientas de lectura |
mcp:send | Enviar correos y eventos | Enviar y programar correos de tus dominios verificados y registrar eventos para tus automatizaciones. | send_email, track_event |
mcp:full | Gestionar todo | Crear, 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
403conWWW-Authenticate: Bearer error="insufficient_scope", scope="…"y el cuerpo JSON-RPCinsufficient_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_emailyget_usagepidenmcp: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
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
send_email | mcp:send | Envía o programa un correo transaccional (hasta 50 destinatarios; HTML, texto o plantilla publicada). | POST /emails |
list_emails | mcp:read | Lista los correos, con filtros de texto, estado, dominio y fechas. | GET /emails |
get_email | mcp:read | Un correo con el contenido, el estado y la línea de tiempo de eventos. | GET /emails/{id} |
cancel_scheduled_email | mcp:full | Cancela un correo todavía programado. | POST /emails/{id}/cancel |
Dominios
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
list_domains | mcp:read | Dominios de envío, con el estado de la verificación y la región. | GET /domains |
get_domain | mcp:read | Un dominio con los registros DNS y lo que está mal en cada uno. | GET /domains/{id} |
Contactos y audiencia
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
list_contacts | mcp:read | Contactos, con filtros por texto, segmento, tema y baja. | GET /contacts |
get_contact | mcp:read | Un contacto por ID o correo. | GET /contacts/{id} |
create_contact | mcp:full | Crea un contacto, con segmentos y temas. Cuenta en el límite de contactos del plan. | POST /contacts |
update_contact | mcp:full | Modifica solo los campos enviados de un contacto. | PATCH /contacts/{id} |
delete_contact | mcp:full | Elimina un contacto y sus suscripciones, sin vuelta atrás. | DELETE /contacts/{id} |
list_segments | mcp:read | Segmentos estáticos y dinámicos, con el total de contactos. | GET /segments |
list_topics | mcp:read | Temas de suscripción. | GET /topics |
list_contact_properties | mcp:read | Propiedades personalizadas de los contactos (clave, tipo y valor por defecto). | GET /contact-properties |
Plantillas
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
list_templates | mcp:read | Plantillas, publicadas o borradores. | GET /templates |
get_template | mcp:read | Una plantilla por ID o alias. | GET /templates/{id} |
create_template | mcp:full | Crea una plantilla; con publish: true ya la publica. | POST /templates |
Campañas
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
list_broadcasts | mcp:read | Campañas, con estado y estadísticas. | GET /broadcasts |
get_broadcast | mcp:read | Una campaña con la audiencia, el contenido y las estadísticas de entrega. | GET /broadcasts/{id} |
create_broadcast | mcp:full | Crea una campaña como borrador. No se envía nada. | POST /broadcasts |
send_broadcast | mcp:full | Envía o programa una campaña. Exige confirm: true. | POST /broadcasts/{id}/send |
Automatizaciones y eventos
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
list_automations | mcp:read | Automatizaciones, con estado y total de ejecuciones. | GET /automations |
get_automation | mcp:read | Una automatización con el disparador y los pasos. | GET /automations/{id} |
track_event | mcp:send | Registra un evento de un contacto, que puede iniciar automatizaciones (y enviar correos). | POST /events |
Métricas y uso
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
get_metrics | mcp:read | Envíos, entregas, aperturas, clics, rebotes y quejas de un período. | GET /emails/metrics |
get_usage | mcp:read | Plan, uso y límites, límite de solicitudes y si el envío está en pausa. | GET /usage |
Formularios
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
list_forms | mcp:read | Formularios de suscripción, con el total de envíos. | GET /forms |
list_form_submissions | mcp:read | Envíos de un formulario: respuestas, contacto y double opt-in. | GET /forms/{id}/submissions |
Buzones de entrada
| Herramienta | Alcance | Qué hace | Endpoint de la API |
|---|---|---|---|
list_inboxes | mcp:read | Buzones de entrada del equipo y la configuración del agente de IA. | GET /inboxes |
list_inbox_threads | mcp:read | Conversaciones de un buzón de entrada, con filtros de carpeta, etiqueta, no leídas, responsable y texto. | GET /inboxes/{id}/threads |
get_inbox_thread | mcp:read | Una conversación con todos los mensajes y borradores (incluido el otp_token de los mensajes recibidos). | GET /inboxes/{id}/threads/{thread_id} |
update_inbox_thread | mcp:full | Cambia la carpeta, marca como leída, asigna y etiqueta una conversación. | PATCH /inboxes/{id}/threads/{thread_id} |
reply_to_inbox_thread | mcp:full | Responde 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_draft | mcp:full | Guarda 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:
| Tipo | Nombre | Qué es |
|---|---|---|
| Recurso | postal://domains | Los dominios de envío del equipo y la verificación de cada uno, en JSON (los primeros 100). Pide mcp:read. |
| Recurso | postal://docs/quickstart | Una 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. |
| Prompt | write_and_send_campaign | Guí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. |
| Prompt | diagnose_delivery | Investiga 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
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);
{
"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.
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);
{
"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:
{
"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) ycode_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_iduna 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.
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);
{
"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.
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.
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"
{
"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 alcanceteam:<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.
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 |
|---|---|
| Plan | Recurso mcp_access; sin él, 403 plan_limit_reached |
| Solicitudes | El 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 |
| Transporte | Solo POST en /mcp; GET y DELETE responden 405 (Allow: POST) |
| Listas | limit de 1 a 100 (por defecto 20), paginación por cursor |
| Destinatarios y programación | Los de la API: hasta 50 destinatarios por correo y programación hasta 30 días por delante |
| Token de acceso OAuth | 1 hora |
| Token de renovación OAuth | 30 días, intercambiado en cada uso |
| Registro de clientes OAuth | 30 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ónde | Nombre | Cuándo ocurre |
|---|---|---|
401 | unauthorized | Faltó el token. La respuesta trae WWW-Authenticate con los metadatos del recurso. |
401 | invalid_token | El 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. |
401 | invalid_api_key | La clave ps_… no existe o fue eliminada. |
403 | plan_limit_reached | El plan del equipo no incluye el servidor MCP. |
403 | insufficient_scope | La conexión OAuth no tiene el alcance de la herramienta llamada (JSON-RPC error.code -32001, con WWW-Authenticate y el alcance que falta). |
429 | rate_limit_exceeded | El equipo superó el límite de solicitudes por segundo. Espera el Retry-After. |
tool | restricted_api_key | Una clave solo de envío llamó a una herramienta que no es de envío. |
tool | validation_error | Datos no válidos, como un remitente fuera de un dominio verificado. |
tool | sending_paused | El envío del equipo está en pausa (demasiados rebotes o quejas). |
tool | daily_quota_exceeded | Se agotó la cuota diaria de envío. |
tool | confirmation_required | send_broadcast sin confirm: true. |
tool | not_found | El 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
- Autenticación y los tipos de clave de API.
- SDKs y CLI, para hacer por código o desde la terminal lo que el asistente hace por conversación.
- Límites y Errores de la API, que valen también para MCP.
- Integra tu aplicación y Conexiones de CRM, para que los datos de tu producto y de tu CRM lleguen a Postal.