Empezar
Autenticación
Las integraciones que leen o cambian datos de un negocio actúan siempre en nombre de su titular y solo sobre ese negocio. Hay dos formas de conseguir acceso: OAuth 2.1, que es la recomendada, o una clave de API que el titular crea en su panel.
| Método | Para qué | Quién lo concede |
|---|---|---|
| OAuth 2.1 + PKCE | Asistentes de IA y aplicaciones que conectan la cuenta de un negocio | El titular, iniciando sesión en Citeri y aceptando |
Clave de API ctk_… | Clientes MCP sin soporte de OAuth o integraciones de servidor a servidor | El titular, en su panel → Developer → Claves de API |
Las rutas públicas de disponibilidad y el widget de reservas no necesitan autenticación.
Descubrimiento#
Citeri publica sus metadatos para que el cliente no tenga que configurar nada a mano:
| Documento | URL |
|---|---|
| Servidor de autorización (RFC 8414) | https://citeri.es/.well-known/oauth-authorization-server |
| OpenID Connect | https://citeri.es/.well-known/openid-configuration |
| Recurso protegido del MCP (RFC 9728) | https://citeri.es/.well-known/oauth-protected-resource/api/mcp |
curl https://citeri.es/.well-known/oauth-authorization-serverconst meta = await fetch("https://citeri.es/.well-known/oauth-authorization-server").then((r) => r.json());
console.log(meta.authorization_endpoint, meta.token_endpoint);import requests
meta = requests.get("https://citeri.es/.well-known/oauth-authorization-server", timeout=10).json()
print(meta["authorization_endpoint"], meta["token_endpoint"])| Campo | Valor |
|---|---|
authorization_endpoint | https://citeri.es/oauth/authorize |
token_endpoint | https://citeri.es/oauth/token |
registration_endpoint | https://citeri.es/oauth/register |
grant_types_supported | authorization_code |
code_challenge_methods_supported | S256 |
token_endpoint_auth_methods_supported | none (clientes públicos con PKCE) |
scopes_supported | openid, citeri |
1. Registrar el cliente#
Registra tu aplicación una vez con registro dinámico (RFC 7591). Solo se admiten clientes públicos: sin client_secret y siempre con PKCE.
curl -X POST https://citeri.es/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Mi integración",
"redirect_uris": ["https://mi-app.example/callback"],
"token_endpoint_auth_method": "none"
}'const res = await fetch("https://citeri.es/oauth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_name: "Mi integración",
redirect_uris: ["https://mi-app.example/callback"],
token_endpoint_auth_method: "none",
}),
});
const { client_id } = await res.json();import requests
r = requests.post("https://citeri.es/oauth/register", json={
"client_name": "Mi integración",
"redirect_uris": ["https://mi-app.example/callback"],
"token_endpoint_auth_method": "none",
}, timeout=10)
client_id = r.json()["client_id"]Respuesta 201:
{
"client_id": "8f3c…",
"client_name": "Mi integración",
"redirect_uris": ["https://mi-app.example/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "none"
}2. Pedir autorización#
Genera un code_verifier aleatorio (43–128 caracteres), calcula code_challenge = BASE64URL(SHA256(code_verifier)) y abre en el navegador:
https://citeri.es/oauth/authorize
?response_type=code
&client_id=CLIENT_ID
&redirect_uri=https%3A%2F%2Fmi-app.example%2Fcallback
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&scope=citeri
&state=VALOR_ALEATORIOEl titular inicia sesión con su cuenta de Citeri, ve qué podrá hacer la integración y acepta. Citeri vuelve a tu redirect_uri con ?code=…&state=…. Comprueba que state coincide con el que enviaste.
El redirect_uri debe ser exactamente uno de los registrados. El código caduca a los 10 minutos y solo se puede canjear una vez.
3. Canjear el código#
curl -X POST https://citeri.es/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=CODIGO \
-d client_id=CLIENT_ID \
-d redirect_uri=https://mi-app.example/callback \
-d code_verifier=CODE_VERIFIERconst res = await fetch("https://citeri.es/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: CLIENT_ID,
redirect_uri: "https://mi-app.example/callback",
code_verifier,
}),
});
const { access_token } = await res.json();import requests
r = requests.post("https://citeri.es/oauth/token", data={
"grant_type": "authorization_code",
"code": code,
"client_id": CLIENT_ID,
"redirect_uri": "https://mi-app.example/callback",
"code_verifier": code_verifier,
}, timeout=10)
access_token = r.json()["access_token"]{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 31536000,
"scope": "citeri"
}Si pides el scope openid, la respuesta incluye además un id_token con el email de la cuenta. Si mandas el parámetro resource (RFC 8707), debe ser la URL del MCP: https://citeri.es/api/mcp.
No hay refresh tokens. El token dura un año. Cuando caduque o el titular lo revoque, repite la autorización.
Usar el token#
Envía el token en la cabecera Authorization de cada petición al servidor MCP. El token queda ligado a un solo negocio: el que el titular eligió al autorizar.
curl https://citeri.es/api/mcp \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Claves de API#
Para clientes MCP que no soportan OAuth, el titular puede crear una clave en Panel → Developer → Claves de API. Empieza por ctk_, se muestra una sola vez y Citeri solo guarda su huella (SHA-256). Desde el mismo sitio se revoca.
- Úsala exactamente igual que un token:
Authorization: Bearer ctk_…. - Da acceso al negocio que la creó, con los mismos permisos que el titular. Trátala como una contraseña: nunca en el navegador ni en un repositorio.
- Si el negocio deja de tener una suscripción activa, las peticiones devuelven
402.
Errores de OAuth#
| Código | Cuándo |
|---|---|
invalid_request | Falta un parámetro obligatorio, como code. |
invalid_grant | Código caducado o ya usado, client_id o redirect_uri distintos, o code_verifier incorrecto. |
unsupported_grant_type | Solo se admite authorization_code. |
invalid_target | El resource no es el del MCP. |
invalid_client_metadata | Registro con un método de autenticación distinto de none. |