Autenticación

La API de Mosend acepta dos métodos de autenticación. Toda ruta no marcada como pública requiere uno de los dos.

Bearer JWT (sesión interactiva)

Es el método que usa el dashboard. Obtienes un par accessToken + refreshToken con el endpoint POST /auth/login. El access token vive 15 minutos; renuévalo con POST /auth/refresh.

# Login
curl -X POST https://api.mosend.dev/auth/login \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "info@empresa.com",
    "password": "...",
    "twoFactorCode": "123456"  // si tiene 2FA activado
  }'

# Respuesta:
# { "data": { "user": {...}, "tokens": { "accessToken": "...", "refreshToken": "...", "expiresIn": 900 } } }

Para todas las rutas posteriores, envía el header:

Authorization: Bearer <accessToken>

API Key (server-to-server)

Para integraciones desde tu backend usa una API Key. Se crea desde el dashboard (Configuración → Integraciones → API Keys) o vía API POST /organizations/{orgId}/api-keys. La clave se muestra una sola vez en la respuesta.

Formato del token completo: mk_live_<prefix>.<secret> — la primera mitad (visible siempre en el dashboard) identifica la key, la segunda mitad solo se ve al crearla y se hashea con bcrypt en BD. Si la perdiste tenés que regenerarla.

Aceptamos la API key en cualquiera de los dos headers (vos elegís el que te quede más cómodo según tu cliente HTTP):

# Opción 1 (recomendada) — header dedicado
X-Api-Key: mk_live_a1b2c3d4.tu-secret-de-32-bytes-en-hex

# Opción 2 — estilo Stripe/OpenAI, usando Authorization: Bearer
Authorization: Bearer mk_live_a1b2c3d4.tu-secret-de-32-bytes-en-hex

# Ejemplo completo
curl 'https://api.mosend.dev/organizations/<orgId>/contacts' \
  -H 'X-Api-Key: mk_live_a1b2c3d4.tu-secret-de-32-bytes-en-hex'

El backend distingue por el prefijo del token: cualquier valor que empiece con mk_live_ o mk_test_, sea cual sea el header, se trata como API key. Cualquier otro Bearer se valida como JWT.

Scopes (opcional)

Si creás la key con un array de scopes específicos, solo podrá llamar endpoints que requieran esos scopes. Si dejás scopes: [] (default desde el dashboard), la key tiene acceso total a los permisos de tu organización — útil para empezar, pero recomendamos restringir cuando puedas.

  • conversations:read · conversations:write
  • messages:send · messages:read
  • contacts:read · contacts:write
  • templates:read · templates:write
  • phone-numbers:read · phone-numbers:write
  • waba:read · waba:write
  • webhooks:read · webhooks:write
  • billing:read · billing:write
  • integrations:read · integrations:write

Restricción por número (phoneNumberIds)

Además de scopes, podés acotar una key a uno o más números de WhatsAppespecíficos pasando phoneNumberIds (UUID de Mosend) al crearla o editarla. Pensado para cuando le entregás una key a un bot externo o a un tercero y querés que solo pueda operar su número — no el resto de la org.

# Key acotada a un número, solo para enviar mensajes
curl -X POST 'https://api.mosend.dev/organizations/<orgId>/api-keys' \
  -H 'X-Api-Key: mk_live_<prefix>.<secret>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Bot ventas - número 333",
    "scopes": ["messages:send", "messages:read", "conversations:read"],
    "phoneNumberIds": ["<UUID-del-numero>"]
  }'
  • Vacío o ausente ([]) = la key opera sobre todos los números de la org.
  • Con scope, los envíos (POST /messages, broadcasts) hacia un número fuera de la lista devuelven 403.
  • Los listados (conversaciones, etc.) se filtran a los números permitidos automáticamente.

Las API keys no expiran a menos que les pongas expiresAt al crearlas. Si vino un X-Api-Key pero falló (revocada, expirada, prefix inválido), el backend devuelve 401 — no cae a JWT como fallback.

2FA (TOTP)

Si el usuario tiene 2FA activada, el endpoint /auth/login exige el campo twoFactorCode (6 dígitos) o un código de recuperación. Códigos de recuperación se invalidan al usarse.

Multi-tenancy

Todas las rutas con prefijo /organizations/{orgId}/... validan que el sujeto autenticado (usuario o API key) pertenezca a esa organización y tenga el permiso requerido. Si no, devuelven 403 Forbidden.