OAuth 2.1
Como uma aplicação obtém um token em nome de uma pessoa, o que esse token pode fazer e todos os erros que ele pode receber.
O Pingo Notify roda o próprio servidor de autorização OAuth 2.1. É dele que sai a credencial de software que age em nome de uma pessoa — um assistente de IA, uma integração de terceiro, qualquer coisa a que você prefira não entregar um token de API.
Qual credencial usar
| Token de API | Token OAuth 2.1 | |
|---|---|---|
| Enviado como | apikey: sk_live_… | Authorization: Bearer … |
| Identidade | o membro que o criou | a pessoa que aprovou a tela de consentimento |
| Workspace | o do próprio token; X-Account-Id troca | fixado no consentimento; X-Account-Id é recusado |
| Permissões | tudo que o criador pode fazer | só os escopos aprovados, cruzados com o papel daquela pessoa |
| Validade | não expira — revogado à mão | 1 hora, renovado por refresh token |
| Serve para | o seu próprio backend | software que não é seu, ou que muita gente instala |
Nunca envie os dois headers. Num endpoint que espera um, o outro é ignorado, não somado.
O fluxo
Cinco chamadas. 1 e 2 acontecem uma vez por aplicação, 3 uma vez por pessoa que autoriza, 4 em toda requisição, 5 mais ou menos a cada hora.
1 — Registre o cliente
POST /v3/oauth/register (RFC 7591). Aberto, sem credencial, responde 201.
curl -X POST https://api.pingonotify.com/v3/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Assistente da Acme",
"redirect_uris": ["https://acme.example/oauth/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"scope": "profile connections:read messages:send"
}'
| Campo | Obrigatório | Valor |
|---|---|---|
redirect_uris | sim | de 1 a 10 URIs, casadas exatamente no /authorize. Absolutas, sem fragmento, sem credenciais. https sempre; http só em loopback (localhost, 127.0.0.1, [::1]); esquema de app nativo vale se tiver ponto (com.acme.app:/oauth) |
client_name | não | Aparece na tela de consentimento, até 255 caracteres. Omita e o seu app aparece como Aplicativo sem nome — mande sempre |
client_uri | não | Aparece na tela de consentimento como o site do app |
logo_uri | não | URL de imagem. É o rosto do app na tela de consentimento |
scope | não | Teto de escopos deste cliente, separados por espaço. Omitido concede todos os pedíveis |
grant_types | não | authorization_code, refresh_token. Omitido significa só authorization_code |
response_types | não | code é o único valor aceito |
token_endpoint_auth_method | não | none, client_secret_basic ou client_secret_post. Omitido cria cliente confidencial |
software_id, software_version | não | Aceitos e validados, depois ignorados: não são gravados nem devolvidos |
{
"client_id": "xf3K9…",
"client_id_issued_at": 1780000000,
"client_secret_expires_at": 0,
"client_name": "Assistente da Acme",
"redirect_uris": ["https://acme.example/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "profile connections:read messages:send"
}
client_secret só aparece para cliente confidencial, e só nesta resposta.
Duas omissões custam uma tarde cada.
- Deixe
grant_typesde fora e o cliente fica só comauthorization_code— nenhum refresh token é emitido, e a sua integração morre uma hora depois precisando de gente. - Deixe
token_endpoint_auth_methodde fora e você recebe um cliente confidencial, com umclient_secretmostrado uma única vez e que passa a ser exigido em toda chamada de token. Mande"none"para ser um cliente público, que se prova por PKCE.
scope omitido concede ao cliente todos os escopos pedíveis. Registrar não é aprovar: um cliente registrado assim não tem dono nem workspace até que alguém o autorize.
2 — Mande a pessoa para o /authorize
GET /v3/oauth/authorize, no navegador. Não é chamada de API — responde sempre 302.
| Parâmetro | Obrigatório | Valor |
|---|---|---|
response_type | sim | code — o único valor aceito |
client_id | sim | vindo do passo 1 |
redirect_uri | se o cliente registrou mais de uma | tem de casar exatamente com uma registrada |
code_challenge | sim | base64url de SHA-256(code_verifier) |
code_challenge_method | sim | S256 — explícito; plain não existe aqui |
scope | não | separados por espaço; o padrão é profile |
state | recomendado | devolvido intacto |
resource | não | a URL exata para a qual o token vale — veja Audiência |
prompt | não | consent força a tela; none proíbe qualquer interação |
PKCE é obrigatório para todo cliente, inclusive os confidenciais.
A pessoa entra na conta, escolhe um workspace e aprova. Aí o navegador cai na sua redirect_uri com code, state e iss.
Só um Owner ou um Admin pode autorizar uma aplicação para um workspace. A tela oferece apenas os workspaces em que o papel da pessoa permite. Aprovar sem escolher um amarra o token à conta pessoal dela.
A pessoa pode aprovar menos escopos do que você pediu. Leia o scope que volta no passo 3 — ele é a verdade; o seu pedido era um desejo.
3 — Troque o código
POST /v3/oauth/token, em application/x-www-form-urlencoded ou JSON.
curl -X POST https://api.pingonotify.com/v3/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=aG9sYS4uLg \
-d redirect_uri=https://acme.example/oauth/callback \
-d client_id=xf3K9... \
-d code_verifier=dBjftJeZ4CVP...
{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8xLOxBt…",
"scope": "profile connections:read messages:send"
}
refresh_token só aparece se o cliente registrou esse grant. Cliente público não manda client_secret; confidencial manda, por HTTP Basic ou como campo do formulário.
O código vale 60 segundos e uma vez só. Repetir o seu próprio código revoga a concessão inteira — isso é detecção de reuso, não defeito.
4 — Chame a API
curl https://api.pingonotify.com/v3/connections \
-H "Authorization: Bearer eyJ…"
Três regras que não têm equivalente do lado do token de API:
- O workspace é fixo. Mandar
X-Account-Idde outro workspace responde 403 —This access token is bound to a different workspace; re-authorize to switch. - Escopo é teto, nunca concessão. A permissão efetiva é a interseção entre os escopos aprovados e o que o papel daquela pessoa permite. Um token com
helpdesk:writena mão de um Agent continua fazendo só o que um Agent faz. resourceprende o token a um caminho. Peçahttps://api.pingonotify.com/v3/mcpe o token é aceito sob/v3/mcpe responde 401 em todo o resto. Omitaresourcee o token vale na API inteira. Peça o recurso que você de fato pretende chamar.
Qualquer coisa que nenhum escopo cubra está fechada para token OAuth, e ela diz isso: this operation is not available to OAuth clients. A gestão de consentimento é o caso notável — nenhum escopo a alcança, então uma aplicação nunca consegue listar nem revogar o acesso de outra.
5 — Renove
curl -X POST https://api.pingonotify.com/v3/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=refresh_token \
-d refresh_token=8xLOxBt... \
-d client_id=xf3K9...
A resposta tem a mesma forma do passo 3, com um refresh token novo. A rotação é obrigatória: o que você apresentou morre no instante em que o par novo é emitido. Guarde o novo antes de usar o access token novo.
Apresentar um refresh token duas vezes revoga a concessão inteira — todos os tokens que aquele cliente tem daquela pessoa naquele workspace, de uma vez, com invalid_grant. É a defesa contra token roubado, e ela não consegue distinguir o seu laço de retry de um ladrão. Nunca renove de dois processos ao mesmo tempo, e nunca repita uma renovação cuja resposta você não leu.
Tanto scope quanto resource só podem estreitar na renovação. Pedir mais devolve invalid_scope ou invalid_target.
Escopos
Doze escopos podem ser pedidos. O legacy existe, mas nenhum cliente pode pedi-lo — ele pertence à ponte do token de API da v1.
| Escopo | Concede |
|---|---|
profile | Quem autorizou e em qual workspace. É o padrão quando scope é omitido |
connections:read | Ler conexões e o status delas |
connections:connect | Parear e desconectar um número (QR code e logout), sem poder criar nem apagar conexões |
connections:write | Criar, editar, conectar e desconectar conexões. Também satisfaz connections:read |
messages:read | Ler o histórico de mensagens e baixar os anexos recebidos |
messages:send | Enviar mensagens: texto, mídia, áudio, figurinha, template, lista e botões |
contacts:read | Ler contatos |
contacts:write | Criar e editar contatos |
helpdesk:read | Ler conversas, contatos e configurações do helpdesk |
helpdesk:write | Responder e gerenciar o helpdesk. Exige o plano PRO além do escopo |
webhooks:read | Ver webhooks e o histórico de entregas deles |
webhooks:write | Criar, editar e excluir webhooks, e ler o segredo de assinatura |
stats:read | Ver estatísticas do workspace |
Peça o mínimo. Todo escopo a mais é uma linha na tela de consentimento que a pessoa pode recusar — e recusar estreita a aprovação inteira.
Se o seu aplicativo não funciona sem uma permissão específica, ela pode ser marcada como obrigatória: a caixa aparece travada na tela, com o motivo, e a autorização é recusada se alguém tentar removê-la por baixo. Isso é configurado no cadastro do cliente, não no /authorize — fale com o suporte para o seu caso.
Como o seu app aparece na tela
O usuário vê o nome, o logo e o site que você registrou — e um aviso quando o cliente se registrou sozinho pelo endpoint dinâmico, dizendo que o nome exibido foi escolhido pelo próprio app.
Aplicativos que o Pingo verificou exibem um selo Verificado pelo Pingo no lugar desse aviso. A verificação é concedida pela plataforma e confirma quem publica o aplicativo — ela não muda o que ele pode fazer nem pula o consentimento: as permissões continuam sendo escolhidas pelo usuário, uma a uma. Não há como um aplicativo se autodeclarar verificado.
Revogando
| Objetivo | Chamada | Credencial |
|---|---|---|
| A aplicação descarta o próprio token | POST /v3/oauth/revoke | o próprio cliente |
| O workspace lista quem tem acesso | GET /v3/oauth/consents | token de API, ou o painel |
| O workspace corta uma aplicação | DELETE /v3/oauth/consents/{id} | token de API, ou o painel |
O revoke responde 200 para qualquer entrada, inclusive um token que nunca existiu. Isso é de propósito: responder diferente transformaria o endpoint num oráculo para adivinhar token válido.
Reaprovar uma aplicação com menos escopos também revoga os tokens já emitidos sob a aprovação antiga, para que a tela de consentimento e a realidade nunca discordem.
Erros
Onde o erro chega importa mais que o código dele, porque é isso que decide o que o seu cliente tem de parsear. São três lugares.
Como corpo JSON
O corpo é exatamente { "error": "…", "error_description": "…" } — sem outro envelope, sem campo statusCode.
error | HTTP | Endpoint | Causa | Conserto |
|---|---|---|---|---|
invalid_client_metadata | 400 | register | Um campo do registro está errado, grant_types tem valor não suportado, ou o registro dinâmico está desligado neste servidor | O error_description nomeia o campo |
invalid_redirect_uri | 400 | register, authorize | Uma redirect URI é relativa, tem fragmento ou credenciais, ou é http fora de loopback | Use URI absoluta https, ou http://localhost |
invalid_scope | 400 | register, token | Escopo desconhecido, legacy, fora do que o cliente registrou, ou uma renovação tentou ampliar | Peça um subconjunto dos escopos do cliente |
invalid_request | 400 | authorize, token, revoke, introspect | Falta um parâmetro, ou ele está malformado ou é contraditório | Leia o error_description |
invalid_client | 400 no authorize · 401 no token, revoke, introspect | client_id desconhecido, ou o segredo de um cliente confidencial falta ou está errado, ou um cliente público mandou um | Cliente público não manda segredo | |
unauthorized_client | 400 | token | O cliente não registrou o grant que está usando | Registre refresh_token antes de usá-lo |
unsupported_grant_type | 400 | token | grant_type não é authorization_code nem refresh_token | — |
invalid_grant | 400 | token | O código expirou (60 s), já foi usado, é de outro cliente, ou um refresh token foi repetido | No reuso a concessão inteira se foi: recomece no /authorize |
invalid_target | 400 | token | resource fora da origem do issuer, com fragmento, ou uma renovação tentou ampliá-lo | Mande a URL exata do recurso, ou nada |
Na sua redirect_uri
Assim que client_id e redirect_uri são validados, todo erro restante do authorize vira um 302 para o seu callback, nunca um corpo JSON — responder no corpo antes desse ponto transformaria o endpoint num open redirect. Parseie a query, não o status:
https://acme.example/oauth/callback?error=invalid_scope&error_description=…&state=…&iss=https://api.pingonotify.com
O iss existe para você saber qual servidor de autorização respondeu (RFC 9207). Os códigos que chegam por aqui: unsupported_response_type, unauthorized_client, invalid_request, invalid_scope, invalid_target, login_required, consent_required, access_denied, server_error.
access_denied significa que uma pessoa recusou, ou que o papel dela não permite autorizar aquele workspace. Não há o que repetir.
Numa chamada de API
error | HTTP | Causa | Conserto |
|---|---|---|---|
invalid_token | 401 | Falta, expirou, foi revogado, ou foi emitido para outro resource | Renove; se falhar, autorize de novo |
insufficient_scope | 403 | Falta um escopo ao token, ou nada concede aquela operação a cliente OAuth | Reautorize com o escopo que a resposta nomeia |
Os dois levam um header WWW-Authenticate, e no insufficient_scope ele nomeia o scope que falta:
WWW-Authenticate: Bearer realm="pingo", error="invalid_token",
error_description="the access token is invalid, expired or revoked",
resource_metadata="https://api.pingonotify.com/.well-known/oauth-protected-resource/v3/mcp"
Siga o resource_metadata em vez de fixar endpoints no código — ele é o fio de volta ao servidor de autorização.
Rate limit responde 429, e o corpo dele não está no formato de erro do OAuth. Trate 429 como transporte, não como erro de protocolo.
Dois códigos existem no vocabulário do OAuth mas este servidor nunca os devolve: interaction_required e temporarily_unavailable. Não escreva tratamento para eles.
Validades e limites
| Código de autorização | 60 segundos, uso único |
| Access token | 1 hora — a menos que o cliente desligue a expiração |
| Refresh token | 30 dias de inatividade, rotacionado a cada uso. Cada renovação reinicia o relógio |
| Requisição de autorização pendente | 10 minutos |
POST /v3/oauth/token, /revoke, /introspect | 120 requisições por minuto, por IP |
GET /v3/oauth/authorize, POST /v3/oauth/register | 15 requisições por minuto, por IP |
| Documentos de descoberta | sem limite, cacheados por 5 minutos |
Expirar é uma opção por cliente. Um cliente registrado pelo painel pode desmarcar Expirar tokens de acesso — aí o access token não tem prazo, o expires_in não vem na resposta, e nenhum refresh token é emitido, porque refresh existe para renovar o que expira. O registro dinâmico (RFC 7591) não consegue definir isso: cliente que se registra sozinho sempre recebe token com prazo.
Com a expiração desligada, revogar é o único jeito de cortar o acesso — pela tela de Aplicativos conectados ou por POST /v3/oauth/revoke. Mudar a opção vale para tokens novos; os já emitidos seguem com a validade com que nasceram.
Descoberta
Não fixe os endpoints no código. Os dois documentos são públicos, não pedem credencial e são a forma suportada de achar tudo que está acima:
| Documento | Responde |
|---|---|
/.well-known/oauth-authorization-server | issuer, todos os endpoints, escopos suportados, grant types, code_challenge_methods_supported, prompt_values_supported |
/.well-known/oauth-protected-resource | a API como um todo, e qual servidor de autorização a guarda |
/.well-known/oauth-protected-resource/v3/mcp | especificamente o endpoint MCP, e só os escopos que ele usa |
A primeira coisa construída sobre tudo isto é o Servidor MCP — um assistente de IA conecta exatamente por este fluxo, e a página dele mostra ponta a ponta. Para a credencial de token de API, veja API Keys e Autenticação.