API HTTP de cada frontend (rutas /api/*). Para el backend compartido (registro de dominios, extensiones, firma) ve a API del backend y firma. Cuerpos y respuestas son JSON salvo indicación; los errores son {"error": "..."}.
Autenticación: cookie de sesión (__Host-next-auth.session-token en producción). Usa un *cookie jar* (curl -c/-b) sobre https. Una ruta protegida sin cookie válida responde un `307` a `/login?callbackUrl=…`, no 401. Los 401 JSON aparecen cuando el JWT es válido pero la sesión fue revocada o el usuario ya no existe.
`Authorization: Bearer <jwt>` solo omite el control CSRF y lo aceptan las rutas que leen la sesión, pero el middleware exige la cookie en las rutas protegidas. Para clientes programáticos usa la API molt (Bearer propio) (abajo).
CSRF: en métodos que cambian estado, si el navegador envía Origin, su host debe coincidir con Host (403 Forbidden). Exentas: webhooks, cron, molt, /api/config, /api/register, reservas públicas, /api/appointments/schedules/* y parte de /api/auth.
Rate limit: 429 con Retry-After en la mayoría de rutas (ver Seguridad). POST /api/emails usa un tope por hora en BD, sin Retry-After.
Propiedad: los recursos ajenos responden 404 (no se revela su existencia).
Públicas (sin sesión): /login, /register, /api/register, /api/auth/*, /api/cron*, /api/config*, /api/webhooks*, /api/molt*, /api/assets*, /api/internal/*, /api/secure-message*, /secure/*, /api/admin* (auth en cada ruta), /docs, /book y las rutas públicas de citas.
# Resend firma con Svix (cabeceras svix-id, svix-timestamp, svix-signature). Solo se exige si WEBHOOK_SECRET está definido.
POST /api/webhooks/resend # email.received y estados de entrega
POST /api/webhooks/resend-events # rebotes y quejas (RESEND_WEBHOOK_SECRET)
GET /api/webhooks/unsubscribe?t=<token> # página de confirmación (no da de baja)
POST /api/webhooks/unsubscribe?t=<token> # baja de un clic (RFC 8058)
API para clientes externos (molt)
# API para clientes externos (Bearer): sin cookie, sin CSRF
curl -sS -X POST "$APP/api/molt/auth/connect" -H 'content-type: application/json' \
-d '{"email":"ana@mi-empresa.com","password":"<contraseña>"}'
# => {"success":true,"user":{...},"accessToken":"...","refreshToken":"...","expiresAt":...} (el access token dura 1 h)
curl -sS "$APP/api/molt/messages?limit=10&folder=inbox" -H "Authorization: Bearer $ACCESS"
curl -sS -X POST "$APP/api/molt/messages" -H "Authorization: Bearer $ACCESS" -H 'content-type: application/json' \
-d '{"to":"luis@ejemplo.com","subject":"Hola","text":"..."}' # 60/h por usuario
{email, password}. 20/15 min por IP y 10/15 min por cuenta (429 + Retry-After). 401 Invalid credentials; 503 si MFA es obligatorio y falta la tabla. Devuelve {success, token, user} y la cookie; con MFA, mfaRequired/mfaEnrollRequired + mfaToken (5 min) sin cookie
POST /api/register
Pública
{email, password, name?, key}. 5/h por IP. 403 si key no coincide con REGISTRATION_KEY (o en producción falta/dev-secret); 400 política de contraseña o User already exists
POST /api/auth/mfa/{setup,confirm,verify,disable,recovery-codes} · GET …/status
Sesión o mfaToken
Alta de TOTP (devuelve otpauthUri), confirmación (10 códigos de recuperación), verificación, baja (403 MFA is required si es obligatorio) y regeneración. 6/5 min por usuario y 40/15 min por IP
POST /api/auth/logout · POST /api/auth/refresh · POST /api/auth/set-cookie · GET /api/auth/me
Sesión
logout revoca la sesión ({all:true} cierra todas). refresh renueva (120/min; 503 si falla la infraestructura). me siempre 200 ({user:null} sin sesión)
GET /api/auth/google · /api/auth/callback/google · /api/auth/[provider]
Pública / sesión
OAuth con state. Redirigen a /login?error=… o /settings?error=… en fallos (InvalidState, NoEmail, SignupDisabled, MfaRequiredUsePassword…). Proveedores: slack, hubspot, notion, zoom, trello
GET ?thread=true; PATCH {starred?, read?, folder?, labelIds?, toggleLabelId?} (400 Invalid folder). 404 si no es tuyo (no revela existencia). delete es borrado completo ({success, storage})
snooze {snoozeUntil} y cancel (programados) sin interfaz; rsvp {response: accepted|tentative|declined}
GET/POST /api/drafts · DELETE /api/drafts/[id] · POST /api/drafts/batch
Sesión
Máx. 50 borradores en la lista; lote ≤500 ids. Propiedad por remitente autorizado
POST /api/upload
Sesión
Multipart file. ≤200 MB, 120/10 min. 400 File type not allowed/File content not allowed, 422 antivirus, 503 si no se pudo escanear con AV_FAIL_MODE=closed
GET /api/assets/<clave>?exp&sig&filename
Firma HMAC o propietario
Descarga con Range. 404 para todo rechazo (nunca 403). sig = HMAC-SHA256(secreto, "asset:v1\n"+clave+"\n"+exp). Ver Seguridad
GET /api/counts · GET /api/sse
Sesión
Contadores por carpeta y etiqueta. SSE: máx. 3 conexiones por usuario (429 Retry-After: 30), vida 50 s, Last-Event-ID para reanudar
start/end/calendarId. 404 Calendar not found; 400 This calendar is read-only. Con asistentes envía invitación .ics; DELETE envía cancelación
GET /api/calendar/domain-availability · POST /api/google/sync
Sesión
Disponibilidad solo del mismo dominio; sync unidireccional (409 SYNC_IN_PROGRESS)
GET /api/calendar/conferencing/providers · POST|DELETE /api/calendar/conferencing/[provider] · POST …/[provider]/test (admin) · POST …/meet (alias)
Sesión
Reuniones Zoom / Google Meet / enlace propio: estado por proveedor, crear (cabecera Idempotency-Key, 20/min), borrar (solo las propias) y probar conexión. Errores tipados { error: { code, message, retryAfter? } }. Ver Reuniones
GET/POST /api/appointments/schedules · GET/PUT/DELETE …/[id] · GET /api/appointments/bookings
Sesión
CRUD de agendas del anfitrión. Estas rutas están exentas del control CSRF por Origin
GET …/schedules/[id]/public · GET …/[id]/slots?date= · POST /api/appointments/book/[id]
Pública
slots: 120/min por IP, 7 días, horizonte 365 (400 fecha inválida). book {guestName, guestEmail, startsAt, guestNotes?}: 10/h por IP y 3/h por correo; 409This slot is no longer available
Proxy al backend (/api/config?domain=): {config, extensions} con marca, tema (con landing) y extensiones del dominio. Si el backend falla devuelve su mismo estado ({error:"Backend error"}) o 500
GET/POST /api/expansions
Sesión
Proxy firmado al backend (/api/extensions, /api/extension/execute). El navegador no puede inyectar context.auth
POST /api/internal/mail
Firma Ed25519 del backend
Puente services.mail: op = listRecent|getEmail|applyBatch|undoRun. Cuerpo ≤256 KiB; 503 Backend key unavailable; 401 firma inválida; 300/min. No es para uso manual
run: global con Bearer; POST con sesión para el usuario; 401 si el Bearer es inválido. elixir: Bearer o clave interna; ?campaign=<id> (400 campaign inválido). Ver Despliegue
POST /api/webhooks/resend · …/resend-events · GET|POST …/unsubscribe
Firma Svix (opcional) / token
Ver Correo: Resend y DNS. Firma inválida: 400 Invalid signature; resend-events >512 000 caracteres: 413; 500 para que Resend reintente
Ruta protegida sin cookie de sesión válida (redirige a /login)
400
Cuerpo o parámetros inválidos; firma de webhook inválida
401
JWT revocado o usuario inexistente; credenciales inválidas; MFA code required
403
Origen cruzado (CSRF), sin permiso (no admin / MFA requerido), clave de registro inválida, módulo desactivado
404
No existe o no es tuyo; rechazo de asset
409
Conflicto: contacto o etiqueta repetidos, transición inválida, sincronización en curso, hueco de cita ocupado
413
Cuerpo o adjuntos demasiado grandes; límite de filas
422
Envío bloqueado por una extensión (EXTENSION_BLOCKED), plantilla Liquid inválida, adjunto con virus
425
IDEMPOTENCY_IN_PROGRESS: hay un envío idéntico en curso (reintenta tras Retry-After)
429
Rate limit o tope de envíos
503
Tabla o servicio no disponible (MFA sin UserMfa, supresión no disponible, AV cerrado, reglas sin migrar)
Algunas rutas exponen `error.message`
Varias rutas devuelven en 500 el mensaje interno (emails/[id]/rsvp, calendar/events/[id]/attach-invite, molt/messages, molt/auth/refresh). No dependas de ese texto.