OAuth para aplicaciones
Si tu producto lo usan varios clientes de Mosend, no les pidas una API key: deja que cada uno autorice tu aplicación desde una pantalla de Mosend. Obtienes tokens ligados a su organización, con los alcances que pediste y que ellos pueden revocar cuando quieran desde Integraciones.
Cuándo usar cada cosa.
API key: tu propio backend hablando con tu propia cuenta. Ver autenticación.
OAuth: una aplicación de terceros (un CRM, un asistente de IA, una automatización) que necesita entrar en cuentas de otros.
1.Registra tu aplicación
El registro lo hace el equipo de Mosend. Escríbenos con el nombre de la app, un logo, la URL de tu sitio, las direcciones de retorno exactas (sin comodines; http solo en localhost) y los alcances que vas a pedir. Recibirás un client_id y, si tu app corre en un servidor, un client_secret que se muestra una sola vez.
Aplicaciones de escritorio, CLI o SPA se registran como públicas: sin secreto, solo PKCE.
GET https://api.mosend.dev/.well-known/oauth-authorization-server
{
"issuer": "https://api.mosend.dev",
"authorization_endpoint": "https://api.mosend.dev/oauth/authorize",
"token_endpoint": "https://api.mosend.dev/oauth/token",
"revocation_endpoint": "https://api.mosend.dev/oauth/revoke",
"introspection_endpoint": "https://api.mosend.dev/oauth/introspect",
"userinfo_endpoint": "https://api.mosend.dev/oauth/userinfo",
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"scopes_supported": ["contacts:read", "messages:send", "..."]
}2.Manda a la persona a «Autorizar acceso»
Genera un par PKCE y un state, y redirige el navegador. La persona inicia sesión si hace falta, elige la organización y ve en lenguaje llano qué podrá hacer tu app. Los alcances son las claves del catálogo de permisos de Mosend (contacts:read, messages:send…), separadas por espacio. Pide siempre organizations:read: sin él no hay /oauth/userinfo ni acceso a las rutas que solo comprueban la pertenencia a la organización.
https://api.mosend.dev/oauth/authorize ?response_type=code &client_id=mc_… &redirect_uri=https%3A%2F%2Ftuapp.com%2Fcallback &scope=organizations%3Aread%20contacts%3Aread%20messages%3Asend &state=<aleatorio> &code_challenge=<BASE64URL(SHA256(code_verifier))> &code_challenge_method=S256
Si acepta, vuelve a tu redirect_uri con code y state. Si no, con error=access_denied. Un client_id o una redirección desconocidos se muestran en Mosend y nunca se redirigen. Con prompt=none se salta la pantalla cuando exactamente una de sus organizaciones ya te tenía autorizado eso mismo, tenga las que tenga; si son dos o ninguna, la pantalla aparece igual. Mosend no devuelve interaction_required: si la pantalla hace falta, se muestra.
3.Canjea el código y usa el token
curl -X POST https://api.mosend.dev/oauth/token \
-u 'mc_…:mcs_…' \
-d grant_type=authorization_code \
-d code=moc_… \
-d redirect_uri=https://tuapp.com/callback \
-d code_verifier=<el code_verifier del par PKCE>
# Aplicaciones públicas: sin -u, con -d client_id=mc_…
{
"access_token": "mot_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mor_…",
"scope": "organizations:read contacts:read messages:send"
}Si tu aplicación corre en varias instancias, renueva bajo un cerrojo o relee el par guardado antes de hacerlo: dos instancias que renuevan con el mismo token fuera de la ventana de 60 segundos pierden la autorización de golpe.
El token de acceso va como Authorization: Bearer mot_… en cualquier ruta /organizations/{orgId}/…. La organización sale de GET /oauth/userinfo (org_id). Fuera de alcance responde 403; caducado o revocado, 401: renueva.
curl -X POST https://api.mosend.dev/oauth/token \ -u 'mc_…:mcs_…' \ -d grant_type=refresh_token \ -d refresh_token=mor_…
El token de renovación rota en cada uso, salvo dentro de una ventana de 60 segundos: ahí dos peticiones simultáneas del mismo cliente reciben cada una su par, ambos válidos, sin que la segunda se tome por robo. Pasada esa ventana, presentar un token ya rotado revoca la autorización completa. Reutilizar un code también, pero solo si viene con su code_verifier correcto: con uno equivocado es un intento inválido y no revoca nada.
4.Revocar e inspeccionar
# Revocar (siempre 200). Revocar el refresh token corta toda la cadena.
curl -X POST https://api.mosend.dev/oauth/revoke -u 'mc_…:mcs_…' \
-d token=mor_… -d token_type_hint=refresh_token
# Introspección: solo la app dueña del token (clientes confidenciales)
curl -X POST https://api.mosend.dev/oauth/introspect -u 'mc_…:mcs_…' -d token=mot_…
{ "active": true, "scope": "contacts:read", "client_id": "mc_…", "sub": "<userId>", "org_id": "<orgId>", "exp": 1789000000 }La persona también puede quitar el acceso desde Integraciones → Aplicaciones conectadas. Tu siguiente petición recibirá 401 y la renovación invalid_grant: vuelve a pedir autorización.
5.Con el SDK o el servidor MCP
import {
MosendClient, createPkcePair, createState, buildAuthorizeUrl, exchangeCode,
fetchOAuthUserinfo,
} from '@moshipp/mosend-sdk';
// 1. Redirigir
const { codeVerifier, codeChallenge } = await createPkcePair();
const state = createState();
const url = buildAuthorizeUrl({
clientId: 'mc_…', redirectUri: 'https://tuapp.com/callback',
scopes: ['organizations:read', 'contacts:read', 'messages:send'], state, codeChallenge,
});
// 2. En el callback
const tokens = await exchangeCode(
{ clientId: 'mc_…', clientSecret: 'mcs_…' },
{ code, redirectUri: 'https://tuapp.com/callback', codeVerifier },
);
// 3. La organización de la concesión (exige organizations:read)
const { org_id } = await fetchOAuthUserinfo({}, tokens.accessToken);
// 4. Cliente que renueva solo y te avisa para persistir el par nuevo.
// Guarda también el scope: el servidor puede recortarlo en cualquier
// momento y hace falta para reconstruir la sesión al arrancar.
const mosend = new MosendClient({
oauth: { clientId: 'mc_…', clientSecret: 'mcs_…', tokens },
orgId: org_id,
onTokenRefresh: (t) => guardar(t),
});
await mosend.contacts.list();El servidor MCP oficial acepta MOSEND_OAUTH_CLIENT_ID: la primera vez abre el navegador para autorizar y después renueva solo, sin API key ni id de organización.
Errores
Los endpoints de token responden con la forma del estándar: { "error", "error_description" }.
| error | Qué significa |
|---|---|
| invalid_request | Falta un parámetro o PKCE. |
| invalid_client | client_id desconocido, suspendido o secreto incorrecto (401). |
| invalid_grant | Código o refresh token inválido, usado, caducado o de otra app; PKCE o redirect_uri no coinciden. |
| invalid_scope | Alcance desconocido o que tu app no tiene permitido pedir. |
| access_denied | La persona no autorizó, o no tiene esos permisos en la organización. |
| unsupported_grant_type | Solo se admiten authorization_code y refresh_token. |
| unsupported_response_type | Solo se admite response_type=code. |
| unauthorized_client | La aplicación quedó suspendida entre la autorización y el canje. |
Seguridad
- PKCE S256 es obligatorio para todos los clientes; la redirección debe coincidir carácter a carácter.
- La app nunca obtiene más de lo que tiene la persona: los alcances se recortan a su rol y se revalidan en cada petición.
- Los tokens son opacos y viven una hora; el de renovación, treinta días.
- Los endpoints de personas (sesiones, 2FA, passkeys, perfil) rechazan tokens OAuth.