Referencia técnica completa de la implementación OAuth 2.0 de LUMA. Estos endpoints siguen las especificaciones OAuth 2.0 RFC 6749 y PKCE RFC 7636.
#URLs base
| Entorno | URL |
|---|---|
| Autorización | https://app.midday.ai/oauth |
| Token y API | https://api.midday.ai/v1 |
#Endpoint de autorización
Inicia el flujo OAuth redirigiendo a los usuarios para que inicien sesión y autoricen tu aplicación.
GET https://app.midday.ai/oauth/authorize
#Parámetros de la solicitud
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
response_type | string | Sí | Debe ser code |
client_id | string | Sí | El ID de cliente de tu aplicación |
redirect_uri | string | Sí | URI a la que redirigir tras la autorización (debe estar registrada) |
scope | string | Sí | Lista de scopes separados por espacios |
state | string | Recomendado | Valor opaco para protección CSRF |
code_challenge | string | PKCE | Hash SHA-256 del verificador de código, codificado en Base64-URL |
code_challenge_method | string | PKCE | Debe ser S256 |
#Ejemplo de solicitud
https://app.midday.ai/oauth/authorize?
response_type=code&
client_id=mid_client_abc123&
redirect_uri=https://yourapp.com/callback&
scope=transactions.read%20invoices.read&
state=xyz789&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
#Respuesta correcta
Redirige a tu redirect_uri con:
| Parámetro | Descripción |
|---|---|
code | Código de autorización (válido durante 10 minutos) |
state | El mismo valor que enviaste (¡verifícalo!) |
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=xyz789
#Respuesta de error
Redirige a tu redirect_uri con:
| Parámetro | Descripción |
|---|---|
error | Código de error |
error_description | Descripción legible del error |
state | El mismo valor que enviaste |
https://yourapp.com/callback?error=access_denied&error_description=User%20denied%20access&state=xyz789
#Códigos de error
| Código | Descripción |
|---|---|
invalid_request | Parámetro ausente o inválido |
unauthorized_client | El cliente no está autorizado para este tipo de concesión |
access_denied | El usuario denegó la autorización |
invalid_scope | Scope inválido o desconocido |
server_error | Error interno del servidor |
#Endpoint de token
Intercambia códigos de autorización por tokens de acceso, o renueva tokens existentes.
POST https://api.midday.ai/v1/oauth/token
#Tipos de contenido
Acepta ambos:
application/jsonapplication/x-www-form-urlencoded
#Concesión de código de autorización
Intercambia un código de autorización por tokens.
#Cuerpo de la solicitud
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
grant_type | string | Sí | Debe ser authorization_code |
code | string | Sí | Código de autorización recibido en el callback |
redirect_uri | string | Sí | Misma URI usada en la autorización |
client_id | string | Sí | El ID de cliente de tu aplicación |
client_secret | string | Clientes confidenciales | El secreto de tu cliente |
code_verifier | string | PKCE | Verificador de código original |
#Ejemplo de solicitud (cliente confidencial)
curl -X POST https://api.midday.ai/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"redirect_uri": "https://yourapp.com/callback",
"client_id": "mid_client_abc123",
"client_secret": "mid_secret_xyz789"
}'
#Ejemplo de solicitud (cliente público con PKCE)
curl -X POST https://api.midday.ai/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"redirect_uri": "https://yourapp.com/callback",
"client_id": "mid_client_abc123",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}'
#Respuesta correcta
{
"access_token": "mid_at_xxxxxxxxxxxxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mid_rt_xxxxxxxxxxxxx",
"scope": "transactions.read invoices.read"
}
| Campo | Descripción |
|---|---|
access_token | Token para las solicitudes a la API |
token_type | Siempre Bearer |
expires_in | Segundos hasta la expiración (3600 = 1 hora) |
refresh_token | Token para obtener nuevos tokens de acceso |
scope | Scopes concedidos (separados por espacios) |
#Concesión de refresh token
Obtén un nuevo token de acceso usando un refresh token.
#Cuerpo de la solicitud
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
grant_type | string | Sí | Debe ser refresh_token |
refresh_token | string | Sí | Refresh token actual |
client_id | string | Sí | El ID de cliente de tu aplicación |
client_secret | string | Clientes confidenciales | El secreto de tu cliente |
scope | string | No | Solicita un subconjunto de los scopes originales |
#Ejemplo de solicitud
curl -X POST https://api.midday.ai/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "mid_rt_xxxxxxxxxxxxx",
"client_id": "mid_client_abc123",
"client_secret": "mid_secret_xyz789"
}'
#Respuesta
Mismo formato que la concesión de código de autorización. El refresh token puede rotar (se devuelve un token nuevo).
#Errores del endpoint de token
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}
| Error | Descripción |
|---|---|
invalid_request | Falta un parámetro obligatorio |
invalid_client | Credenciales de cliente inválidas |
invalid_grant | Código o token inválido, expirado o ya usado |
unauthorized_client | El cliente no está autorizado para este tipo de concesión |
unsupported_grant_type | Tipo de concesión no compatible |
#Endpoint de revocación
Revoca un token de acceso o un refresh token.
POST https://api.midday.ai/v1/oauth/revoke
#Cuerpo de la solicitud
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
token | string | Sí | Token a revocar |
client_id | string | Sí | El ID de cliente de tu aplicación |
client_secret | string | Clientes confidenciales | El secreto de tu cliente |
#Ejemplo de solicitud
curl -X POST https://api.midday.ai/v1/oauth/revoke \
-H "Content-Type: application/json" \
-d '{
"token": "mid_at_xxxxxxxxxxxxx",
"client_id": "mid_client_abc123",
"client_secret": "mid_secret_xyz789"
}'
#Respuesta
Siempre devuelve éxito, incluso si el token ya era inválido:
{
"success": true
}
#Límites de peticiones
Los endpoints OAuth tienen límites de peticiones específicos para evitar abusos:
| Endpoint | Límite |
|---|---|
/oauth/authorize | 20 peticiones cada 15 minutos por IP |
/oauth/token | 20 peticiones cada 15 minutos por IP |
/oauth/revoke | 20 peticiones cada 15 minutos por IP |
Superar los límites devuelve 429 Too Many Requests.
#Vida útil de los tokens
| Tipo de token | Vida útil | Notas |
|---|---|---|
| Código de autorización | 10 minutos | Uso único |
| Token de acceso | 1 hora | Usa el refresh token para renovarlo |
| Refresh token | 30 días | Rota con cada uso |
#Implementación de PKCE
PKCE añade seguridad para clientes públicos (apps móviles, SPA).
#1. Genera el verificador de código
Crea una cadena aleatoria (43-128 caracteres, segura para URL):
function base64UrlEncode(buffer: Uint8Array): string {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
#2. Crea el code challenge
Hash SHA-256 del verificador, codificado en base64-URL:
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(hash));
}
#3. Úsalo en el flujo
- Guarda el
code_verifierde forma segura (almacenamiento de sesión) - Envía el
code_challengeen la solicitud de autorización - Envía el
code_verifieren el intercambio del token
#Consideraciones de seguridad
#Parámetro state
Usa y valida siempre el parámetro state:
// Generar
const state = crypto.randomUUID();
sessionStorage.setItem("oauth_state", state);
// Validar en el callback
const storedState = sessionStorage.getItem("oauth_state");
if (callbackState !== storedState) {
throw new Error("State mismatch - possible CSRF attack");
}
#Validación de la redirect URI
- Registra todas las redirect URIs en los ajustes de tu aplicación
- Usa validación de coincidencia exacta (sin comodines)
- Usa siempre HTTPS en producción
#Almacenamiento de tokens
- Guarda los tokens de forma segura (cifrados, preferiblemente en el servidor)
- No expongas nunca los tokens en URLs o logs
- Elimina los tokens al cerrar sesión
#Protección del client secret
- No incluyas nunca los client secrets en código de cliente
- Usa variables de entorno en los servidores
- Rota los secretos si se ven comprometidos
#Ejemplos de manejo de errores
#Gestionar errores de autorización
app.get("/callback", (req, res) => {
const { error, error_description, code, state } = req.query;
if (error) {
console.error(`OAuth error: ${error} - ${error_description}`);
return res.redirect("/connect?error=" + encodeURIComponent(error as string));
}
// Verificar el state
if (state !== req.session.oauthState) {
return res.status(400).send("Invalid state");
}
// Intercambiar el código por tokens
// ...
});
#Gestionar errores de token
async function exchangeCode(code: string) {
const response = await fetch("https://api.midday.ai/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Token error: ${error.error} - ${error.error_description}`);
}
return response.json();
}
#Gestionar fallos de refresco
async function refreshTokens(refreshToken: string) {
try {
const response = await fetch("https://api.midday.ai/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
const tokens = await response.json();
if (tokens.error) {
// El refresh token expiró o fue revocado
// Redirigir al usuario para que vuelva a autorizar
return null;
}
return tokens;
} catch (error) {
console.error("Refresh failed:", error);
return null;
}
}
#Usar el SDK con tokens OAuth
Una vez que tengas un token de acceso, usa el SDK de Midday para las solicitudes a la API:
import { Midday } from "@midday-ai/sdk";
const midday = new Midday({
token: accessToken, // Token de acceso OAuth
});
// Listar transacciones
const transactions = await midday.transactions.list({
pageSize: 50,
});
// Obtener facturas
const invoices = await midday.invoices.list({
statuses: ["unpaid", "overdue"],
});
// Obtener métricas financieras
const profit = await midday.metrics.profit({
from: "2024-01-01",
to: "2024-12-31",
});
Consulta la documentación del SDK para ver todos los métodos disponibles.
#Relacionado
- Crea una app OAuth — Guía de introducción
- Referencia de scopes OAuth — Permisos disponibles
- Proceso de revisión de apps — Verifica tu app
- Referencia de la API — Documentación completa de la API