Servidor MCP
Conecte Claude, ChatGPT, Gemini, Cursor, VS Code e outros assistentes de IA à sua conta da Postal: 34 ferramentas, OAuth ou chave de API, permissões por escopo e as mesmas regras da API.
#O que é
O servidor MCP (Model Context Protocol) da Postal deixa assistentes de IA, como Claude, ChatGPT, Gemini, Cursor, VS Code e o Claude Code, usarem a sua conta: enviar e-mails, conferir uma entrega, ajustar contatos, escrever e enviar campanhas, ler a caixa de entrada. O assistente chama ferramentas (34 hoje), e cada uma roda o mesmo endpoint da API REST.
Endereço: https://api.postal.so/mcp. É o transporte Streamable HTTP do MCP, sem sessão: cada chamada é um POST com uma mensagem JSON-RPC e traz a própria credencial. Cada chamada age em um time: o que você escolheu ao conectar o app ou o time da chave de API.
- As mesmas regras da API. Validação, limites do plano, cotas de envio, pausa de envio, lista de supressão e limite de requisições do time valem como em qualquer chamada. Os erros chegam como
nome: mensagem(por exemplovalidation_error: …ousending_paused: …), os mesmos nomes de erro da API. - Dois jeitos de entrar. OAuth, em que a pessoa entra na Postal, escolhe o time e pode revogar depois; ou uma chave de API no cabeçalho
Authorization. - Permissões por escopo. Um app conectado pode só ler, só enviar ou fazer tudo, e o assistente só enxerga as ferramentas que pode usar.
- Ações irreversíveis pedem confirmação. Enviar uma campanha exige
confirm: true, e as instruções do servidor mandam o assistente confirmar destinatários e conteúdo com você antes de enviar e-mails.
Depende do plano
O servidor MCP é um recurso do plano (mcp_access). Sem ele, o endpoint responde 403 plan_limit_reached. Veja Limites.
#Conectar um assistente
No painel, abra Configurações › Conexões de IA: a tela mostra a URL do servidor e o passo a passo de cada assistente. Os apps de chat usam OAuth; as ferramentas de desenvolvimento aceitam OAuth ou uma chave de API.
| Assistente | Entrada | Como conectar |
|---|---|---|
| Claude | OAuth | Nas configurações de conectores (web ou desktop), adicione um conector personalizado, cole a URL e conecte: você entra na Postal, escolhe o time e autoriza. |
| ChatGPT | OAuth | Nas configurações de apps e conectores (alguns planos pedem o modo desenvolvedor), crie um conector com a URL e a autenticação OAuth. |
| Gemini | OAuth ou chave | No Gemini CLI, adicione o servidor em ~/.gemini/settings.json (ou use a URL nos apps do Gemini que aceitam servidores MCP). Sem o cabeçalho, o Gemini abre a entrada na Postal no primeiro uso. |
| Cursor | OAuth ou chave | Nas configurações de MCP (ou em ~/.cursor/mcp.json), adicione um servidor. Só com a URL, o Cursor abre a entrada na Postal para você escolher o time. |
| VS Code | OAuth ou chave | Rode MCP: Add Server, escolha HTTP e cole a URL; na primeira chamada o VS Code pede para entrar. Com chave, use o .vscode/mcp.json abaixo. |
| Claude Code | OAuth ou chave | Adicione o servidor com o comando abaixo. Sem o cabeçalho, rode /mcp dentro do Claude Code para entrar na Postal. |
Para entrar com uma chave de API, use os exemplos abaixo e troque ps_... pela sua chave. Os nomes das telas de cada assistente mudam com frequência; na dúvida, siga a documentação dele.
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_..." }
}
}
}
Outros clientes MCP que leem um arquivo com mcpServers e aceitam cabeçalhos usam o mesmo bloco do Cursor.
#Autenticação
O servidor aceita duas credenciais, sempre como Authorization: Bearer. Nenhuma outra é aceita, e a Postal não repassa o token a terceiros.
| OAuth | Chave de API | |
|---|---|---|
| Para quem | Os conectores do Claude, ChatGPT e Gemini e qualquer app que consiga abrir uma janela de entrada. | Ferramentas de desenvolvimento que aceitam cabeçalhos (Claude Code, Cursor, VS Code) e scripts. |
| Como entra | A pessoa entra na Postal, escolhe um time e autoriza. O app recebe um token de acesso (1 hora) e um de renovação (30 dias). | Uma chave que começa com ps_, criada em Chaves de API. |
| Permissões | Os escopos que o app pediu e a pessoa aceitou. | Acesso total (full_access) vira mcp:full; só envio (sending_access) vira mcp:send. |
| Revogar | Em Conexões de IA, a qualquer momento. | Excluindo a chave, em Chaves de API. |
Uma chave de API restrita a um domínio só envia desse domínio, como na API. Um token OAuth deixa de valer na hora quando a conexão é revogada, quando a pessoa sai do time ou quando a conta é suspensa.
Prefira o OAuth quando o app oferecer: você escolhe o time, vê o que o app fez e revoga sem trocar nenhuma chave. Se usar chave, prefira uma só de envio quando o assistente só precisa enviar e-mails e eventos.
Sem credencial, o /mcp responde 401 com o cabeçalho WWW-Authenticate apontando para os metadados do recurso (/.well-known/oauth-protected-resource/mcp): é por ele que um cliente MCP descobre como iniciar o OAuth.
#Escopos e permissões
Cada ferramenta pede um escopo. mcp:full vale por todos; mcp:send não inclui a leitura (um app que só envia não vê e-mails, contatos nem o uso). O assistente só enxerga, em tools/list, as ferramentas que o token ou a chave pode chamar.
| Escopo | Nome na tela de autorização | O que permite | Ferramentas |
|---|---|---|---|
mcp:read | Ler seus dados | Ver e-mails, domínios, contatos, segmentos, tópicos, templates, campanhas, automações, formulários, inboxes, métricas e uso. | 22 ferramentas de leitura |
mcp:send | Enviar e-mails e eventos | Enviar e agendar e-mails dos seus domínios verificados e registrar eventos para as suas automações. | send_email, track_event |
mcp:full | Gerenciar tudo | Criar, alterar e excluir contatos, templates e campanhas, enviar campanhas, cancelar e-mails agendados e trabalhar nas conversas das inboxes (organizar, responder, salvar rascunhos). Inclui os outros dois escopos. | As 34 ferramentas |
Se o app não pedir escopos, a tela oferece os três. A pessoa autoriza ou recusa o pedido como um todo.
Quando falta permissão
- Conexão OAuth: chamar uma ferramenta além dos escopos dá
403comWWW-Authenticate: Bearer error="insufficient_scope", scope="…"e o corpo JSON-RPCinsufficient_scope: …. O cliente pode pedir ao usuário para conectar de novo com o escopo que falta. - Chave de API: uma chave só de envio que chama uma ferramenta de leitura ou de gestão recebe um erro de ferramenta
restricted_api_key. Atenção: no MCP,get_emaileget_usagepedemmcp:read, mesmo que a API REST deixe uma chave só de envio ler esses dois endpoints.
#Ferramentas
Os argumentos de cada ferramenta seguem os campos do endpoint correspondente, e o cliente os lê do inputSchema de tools/list. As listas aceitam limit (1 a 100, padrão 20) e os cursores after e before: passe o último id em after enquanto has_more for true. As ferramentas também declaram se só leem, se são destrutivas e se são idempotentes (annotations), para o assistente decidir quando pedir confirmação.
E-mails
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
send_email | mcp:send | Envia ou agenda um e-mail transacional (até 50 destinatários; HTML, texto ou template publicado). | POST /emails |
list_emails | mcp:read | Lista os e-mails, com filtros de texto, status, domínio e datas. | GET /emails |
get_email | mcp:read | Um e-mail com o conteúdo, o status e a linha do tempo de eventos. | GET /emails/{id} |
cancel_scheduled_email | mcp:full | Cancela um e-mail ainda agendado. | POST /emails/{id}/cancel |
Domínios
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
list_domains | mcp:read | Domínios de envio, com o status da verificação e a região. | GET /domains |
get_domain | mcp:read | Um domínio com os registros DNS e o que está errado em cada um. | GET /domains/{id} |
Contatos e audiência
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
list_contacts | mcp:read | Contatos, com filtros por texto, segmento, tópico e descadastro. | GET /contacts |
get_contact | mcp:read | Um contato por ID ou e-mail. | GET /contacts/{id} |
create_contact | mcp:full | Cria um contato, com segmentos e tópicos. Conta no limite de contatos do plano. | POST /contacts |
update_contact | mcp:full | Altera só os campos enviados de um contato. | PATCH /contacts/{id} |
delete_contact | mcp:full | Exclui um contato e as suas inscrições, sem volta. | DELETE /contacts/{id} |
list_segments | mcp:read | Segmentos estáticos e dinâmicos, com o total de contatos. | GET /segments |
list_topics | mcp:read | Tópicos de inscrição. | GET /topics |
list_contact_properties | mcp:read | Propriedades personalizadas dos contatos (chave, tipo e valor padrão). | GET /contact-properties |
Templates
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
list_templates | mcp:read | Templates, publicados ou rascunhos. | GET /templates |
get_template | mcp:read | Um template por ID ou alias. | GET /templates/{id} |
create_template | mcp:full | Cria um template; com publish: true já o publica. | POST /templates |
Campanhas
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
list_broadcasts | mcp:read | Campanhas, com status e estatísticas. | GET /broadcasts |
get_broadcast | mcp:read | Uma campanha com a audiência, o conteúdo e as estatísticas de entrega. | GET /broadcasts/{id} |
create_broadcast | mcp:full | Cria uma campanha como rascunho. Nada é enviado. | POST /broadcasts |
send_broadcast | mcp:full | Envia ou agenda uma campanha. Exige confirm: true. | POST /broadcasts/{id}/send |
Automações e eventos
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
list_automations | mcp:read | Automações, com status e total de execuções. | GET /automations |
get_automation | mcp:read | Uma automação com o gatilho e os passos. | GET /automations/{id} |
track_event | mcp:send | Registra um evento de um contato, que pode iniciar automações (e enviar e-mails). | POST /events |
Métricas e uso
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
get_metrics | mcp:read | Envios, entregas, aberturas, cliques, bounces e reclamações de um período. | GET /emails/metrics |
get_usage | mcp:read | Plano, uso e limites, limite de requisições e se o envio está pausado. | GET /usage |
Formulários
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
list_forms | mcp:read | Formulários de inscrição, com o total de envios. | GET /forms |
list_form_submissions | mcp:read | Envios de um formulário: respostas, contato e double opt-in. | GET /forms/{id}/submissions |
Inboxes
| Ferramenta | Escopo | O que faz | Endpoint da API |
|---|---|---|---|
list_inboxes | mcp:read | Inboxes do time e as configurações do agente de IA. | GET /inboxes |
list_inbox_threads | mcp:read | Conversas de uma inbox, com filtros de pasta, etiqueta, não lidas, responsável e texto. | GET /inboxes/{id}/threads |
get_inbox_thread | mcp:read | Uma conversa com todas as mensagens e rascunhos (inclusive o otp_token das mensagens recebidas). | GET /inboxes/{id}/threads/{thread_id} |
update_inbox_thread | mcp:full | Muda a pasta, marca como lida, atribui e etiqueta uma conversa. | PATCH /inboxes/{id}/threads/{thread_id} |
reply_to_inbox_thread | mcp:full | Responde a uma conversa pelo endereço da inbox. Envia um e-mail de verdade. | POST /inboxes/{id}/threads/{thread_id}/reply |
create_inbox_draft | mcp:full | Salva um rascunho para uma pessoa revisar e enviar no painel. Não envia. | POST /inboxes/{id}/drafts |
O que o assistente não deve fazer sozinho
send_email, send_broadcast e reply_to_inbox_thread chegam a pessoas de verdade e não podem ser desfeitos. As instruções do servidor mandam o assistente confirmar destinatários, remetente, assunto e conteúdo com você antes. send_broadcast recusa a chamada com confirmation_required se confirm não for true. Para uma resposta que uma pessoa deve revisar, use create_inbox_draft, que só salva o rascunho. Para testar sem enviar de verdade, mande para os endereços de teste ([email protected]…): nada é entregue e a cota não é usada, mas os eventos e webhooks são reais.
#Recursos e prompts
Além das ferramentas, o servidor oferece contexto pronto e receitas:
| Tipo | Nome | O que é |
|---|---|---|
| Recurso | postal://domains | Os domínios de envio do time e a verificação de cada um, em JSON (os 100 primeiros). Pede mcp:read. |
| Recurso | postal://docs/quickstart | Um guia curto, em Markdown, de como domínios, envio, templates, contatos, campanhas, automações, inboxes e erros se encaixam. |
| Prompt | write_and_send_campaign | Guia o assistente do objetivo até uma campanha enviada ou agendada, com a sua aprovação a cada passo. Argumentos: goal (obrigatório), segment e send_at. |
| Prompt | diagnose_delivery | Investiga por que um e-mail não chegou ou foi para o spam: eventos, registros DNS, métricas e limites, sem enviar nada. Argumentos: email_id, recipient e domain. |
Os prompts aparecem com os títulos “Escrever e enviar campanha” e “Diagnosticar entrega”.
#Exemplos
Um assistente faz essas chamadas por você, mas elas são só JSON-RPC sobre HTTP, o que ajuda a testar a chave e entender as respostas. Os cabeçalhos Accept e MCP-Protocol-Version acompanham toda chamada; a resposta é JSON. Não é preciso abrir sessão (initialize) antes: cada POST se basta.
Listar as ferramentas
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 }
},
…
]
}
}
Com uma chave só de envio, a lista traz só send_email e track_event.
Enviar um e-mail
A ferramenta send_email aceita os campos de POST /emails, e idempotency_key faz o papel do cabeçalho Idempotency-Key. O exemplo manda para um endereço de teste, então nada é entregue.
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
}
}
O resultado vem em structuredContent (e também como texto em content). Siga o e-mail com get_email.
Quando a API recusa
Um erro da API vira um resultado com isError: true e o texto nome: mensagem, com o status HTTP 200 do JSON-RPC. Aqui, o remetente não está num domínio 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
}
}
Falhas de credencial e de permissão, ao contrário, são respostas HTTP (veja Erros).
#OAuth para quem desenvolve um cliente MCP
Quem usa um assistente pronto não precisa desta parte. Ela é para quem constrói um cliente MCP e quer conectar a Postal. O servidor de autorização é a própria API (https://api.postal.so) e segue o OAuth 2.1: fluxo de código de autorização com PKCE, sem outros fluxos.
1. Descoberta
GET /.well-known/oauth-protected-resource/mcp(RFC 9728) diz qual é o servidor de autorização e os escopos do recurso. Também existe na raiz.GET /.well-known/oauth-authorization-server(RFC 8414) traz os endpoints (authorization_endpoint,token_endpoint,registration_endpoint,revocation_endpoint), os escopos (mcp:read,mcp:send,mcp:full) ecode_challenge_methods_supported: ["S256"]. As duas respostas ficam em cache por 1 hora.
2. Registro do cliente
Há dois jeitos, e ambos dispensam cadastro manual:
- Registro dinâmico (RFC 7591):
POST /oauth/register. Os clientes são limitados por IP (30 por minuto e 300 por hora), e os que nunca concluem uma autorização são apagados depois de 24 horas. - Client ID Metadata Document: use como
client_iduma URL HTTPS que devolve o JSON do cliente. A Postal busca o documento sem seguir redirecionamentos, com prazo de 5 segundos e até 5 KB, nunca em rede privada, e o guarda por 1 hora.
As redirect_uris (até 10) precisam ser https, de loopback (http://localhost, 127.0.0.1, [::1]) ou de um esquema próprio de app de desktop conhecido (cursor, vscode, vscode-insiders, windsurf, zed), sem credenciais nem fragmento. O pedido de autorização precisa usar uma delas, exatamente igual. O método de autenticação padrão é client_secret_basic (a resposta traz um client_secret, uma única vez); com none o cliente é público e se apoia só no 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. Autorização
Abra o navegador da pessoa em GET /oauth/authorize. São obrigatórios response_type=code, client_id, redirect_uri e o PKCE com code_challenge_method=S256. Mande também resource (o endereço do MCP, https://api.postal.so/mcp, ou a origem https://api.postal.so), scope e state. Quem não está logado vai para o login do painel e volta; a tela de consentimento mostra o app, o endereço de retorno e as permissões, e a pessoa escolhe o time. O app não escolhe o time: um escopo team:… no pedido é recusado com invalid_scope. Com prompt=none o servidor responde login_required ou consent_required em vez de mostrar telas.
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
Depois que a pessoa autoriza, ela volta para a redirect_uri com code, state e iss (o emissor, RFC 9207: confira-o). Se recusa, volta com error=access_denied.
4. Tokens
Troque o code em POST /oauth/token (corpo application/x-www-form-urlencoded), com o code_verifier do PKCE e o mesmo resource. Só os grants authorization_code e refresh_token existem.
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>"
}
- O token de acesso é um JWT de 1 hora, com a audiência do MCP (
https://api.postal.so/mcp) e o escopoteam:<id>do time escolhido. Um token que não nomeia este servidor como audiência é recusado (401 invalid_token). - O token de renovação vale 30 dias e é trocado a cada uso: guarde sempre o novo, porque o antigo deixa de servir.
- Erros do OAuth vêm no formato da RFC 6749 (
{"error": "invalid_grant", "error_description": "…"}), não no formato de erro da 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, o app chama POST /oauth/revoke (RFC 7009) com o token (de acesso ou de renovação) e as credenciais do cliente. Isso encerra a conexão inteira do app com aquele time, como se a pessoa a revogasse no painel, e responde 200 mesmo para um token desconhecido.
Os endpoints de registro, token, revogação, metadados e o /mcp aceitam chamadas de navegadores (CORS aberto, sem cookies). Limites: o token, 60 por minuto por cliente e IP; a autorização, 30 por minuto.
#Gerenciar as conexões
Em Configurações › Conexões de IA ficam os apps conectados ao time, com quem conectou, quando e o último uso, e o histórico recente de conexões, recusas e revogações do time. Revogar corta o acesso na hora; para usar o app de novo, ele precisa ser conectado outra vez.
- Cada pessoa revoga as próprias conexões. Donos e administradores do time revogam as de qualquer pessoa.
- Cada conexão é de uma pessoa em um time: se a pessoa sai do time ou a conta é suspensa, os tokens deixam de valer.
- As chaves de API não aparecem ali; elas se gerenciam em Chaves de API.
#Limites
| O quê | Limite |
|---|---|
| Plano | Recurso mcp_access; sem ele, 403 plan_limit_reached |
| Requisições | O mesmo limite por segundo do time (Limites), contado à parte das chamadas REST; cada ferramenta ainda conta como uma requisição da API. Acima dele, 429 rate_limit_exceeded |
| Transporte | Só POST em /mcp; GET e DELETE respondem 405 (Allow: POST) |
| Listas | limit de 1 a 100 (padrão 20), paginação por cursor |
| Destinatários e agendamento | Os da API: até 50 destinatários por e-mail e agendamento até 30 dias à frente |
| Token de acesso OAuth | 1 hora |
| Token de renovação OAuth | 30 dias, trocado a cada uso |
| Registro de clientes OAuth | 30 por minuto e 300 por hora, por IP |
#Erros
Problemas de credencial, de plano e de escopo são respostas HTTP; problemas de uma chamada de ferramenta (validação, cotas, pausa de envio…) vêm como resultado com isError: true.
| Onde | Nome | Quando acontece |
|---|---|---|
401 | unauthorized | Faltou o token. A resposta traz WWW-Authenticate com os metadados do recurso. |
401 | invalid_token | O token OAuth é inválido, expirou, foi revogado ou não é para este servidor (audiência), a pessoa saiu do time ou a conta foi suspensa. Conecte o app de novo. |
401 | invalid_api_key | A chave ps_… não existe ou foi excluída. |
403 | plan_limit_reached | O plano do time não inclui o servidor MCP. |
403 | insufficient_scope | A conexão OAuth não tem o escopo da ferramenta chamada (JSON-RPC error.code -32001, com WWW-Authenticate e o escopo que falta). |
429 | rate_limit_exceeded | O time passou do limite de requisições por segundo. Espere o Retry-After. |
tool | restricted_api_key | Uma chave só de envio chamou uma ferramenta que não é de envio. |
tool | validation_error | Dados inválidos, como um remetente fora de um domínio verificado. |
tool | sending_paused | O envio do time está pausado (bounces ou reclamações demais). |
tool | daily_quota_exceeded | A cota diária de envio acabou. |
tool | confirmation_required | send_broadcast sem confirm: true. |
tool | not_found | O ID não existe neste time. |
A linha “tool” é um erro dentro de um resultado de tools/call (HTTP 200). Os demais nomes são os da API; leia a mensagem e corrija a causa em vez de repetir a chamada às cegas.
#Veja também
- Autenticação e os tipos de chave de API.
- SDKs e CLI, para fazer pelo código ou pelo terminal o que o assistente faz por conversa.
- Limites e Erros da API, que valem também para o MCP.
- Integre seu app e Conexões de CRM, para os dados do seu produto e do seu CRM chegarem à Postal.