Empezar
Entrar con Citeri
«Entrar con Citeri» permite que tu aplicación actúe sobre un negocio de Citeri con el permiso de quien lo usa: la dueña, una empleada o el acceso compartido del salón. Es OAuth 2.1 con OpenID Connect: código de autorización, PKCE S256 y tokens de refresco con rotación.
Una aplicación nunca puede hacer más que la persona que la autorizó. Si una empleada no puede ver los teléfonos en el panel, tu app tampoco, aunque haya pedido clientas:telefono. Y si el negocio pausa a esa empleada o le quita un permiso, el cambio se aplica a la app al momento.
1. Crea la aplicación#
En el panel de tu negocio, ve a Ajustes → Desarrolladores → Nueva aplicación. Indica el nombre, las URLs de vuelta (redirect_uri, siempre https://; http://localhost solo para pruebas) y los permisos que podrá pedir.
| Tipo | Cuándo | Autenticación en /oauth/token |
|---|---|---|
| Confidencial | Tienes un servidor que guarda el secreto | client_secret_basic o client_secret_post (PKCE recomendado) |
| Pública | Web sin servidor, app móvil o de escritorio | none + PKCE S256 obligatorio |
El client_secret se enseña una sola vez al crearla. Si lo pierdes, genera uno nuevo desde el mismo apartado; el anterior deja de valer al momento.
2. Verificación#
Recién creada, la aplicación funciona solo en tu propio negocio. Para que otros negocios puedan conectarla, pulsa Pedir verificación (hace falta la web de la aplicación y su política de privacidad). Citeri revisa la app y, si la aprueba, aparece con el sello «Verificada por Citeri» en la pantalla de permisos.
Si después cambias el nombre, el logo, las URLs de vuelta o los permisos, la app pierde la verificación y hay que volver a pedirla.
Descubrimiento#
| Documento / endpoint | URL |
|---|---|
| Servidor de autorización (RFC 8414) | https://citeri.es/.well-known/oauth-authorization-server |
| OpenID Connect | https://citeri.es/.well-known/openid-configuration |
| Autorización | https://citeri.es/oauth/authorize |
| Token | https://citeri.es/oauth/token |
| Datos de la persona (userinfo) | https://citeri.es/oauth/userinfo |
| Revocación (RFC 7009) | https://citeri.es/oauth/revoke |
| Claves públicas (JWKS) | https://citeri.es/oauth/jwks.json |
curl https://citeri.es/.well-known/openid-configurationPermisos (scopes)#
Pide solo lo que necesites: la persona ve en castellano cada permiso antes de aceptar. Tu app solo puede pedir los que marcaste al crearla.
| Scope | Qué permite |
|---|---|
openid | Saber quién eres (identificador de tu cuenta) |
profile | Ver tu nombre y el del negocio |
email | Ver tu correo electrónico |
offline_access | Seguir conectada aunque no estés usando la aplicación |
citas:leer | Ver la agenda y las citas |
citas:escribir | Crear, cambiar y cancelar citas |
clientas:leer | Ver las fichas de clientas |
clientas:escribir | Crear y modificar fichas de clientas |
clientas:telefono | Ver los teléfonos de las clientas sensible |
catalogo:leer | Ver servicios, productos y precios |
catalogo:escribir | Modificar servicios, productos y precios |
cobros | Cobrar y operar la caja |
finanzas | Ver y gestionar facturas, ventas e importes sensible |
informes | Ver informes y estadísticas |
marketing | Gestionar marketing y mensajes |
equipo | Gestionar el equipo y sus horarios |
ajustes | Cambiar los ajustes del negocio sensible |
mcp | Usar Citeri desde asistentes de IA (MCP) |
citeri | Acceso completo al negocio legado |
Pedir escritura no da lectura: si tu app crea citas y también las consulta, pide citas:leer citas:escribir. citeri existe solo por compatibilidad con integraciones antiguas; no lo uses en apps nuevas.
3. PKCE#
Genera un code_verifier aleatorio (43–128 caracteres) por cada intento de inicio de sesión y envía su huella SHA-256 en base64url como code_challenge. Solo se admite S256.
CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '=+/\n' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 16)4. Envía a la persona a autorizar#
https://citeri.es/oauth/authorize?response_type=code
&client_id=citeri_XXXXXXXXXXXXXXXXXXXXXXXX
&redirect_uri=https%3A%2F%2Fmiapp.es%2Foauth%2Fcallback
&scope=openid%20profile%20email%20offline_access%20citas%3Aleer%20citas%3Aescribir
&state=$STATE
&code_challenge=$CODE_CHALLENGE
&code_challenge_method=S256
&nonce=$(openssl rand -hex 16)Si la persona ya tiene la sesión del panel abierta, no vuelve a iniciar sesión. Si gestiona varios negocios, elige uno: el token queda ligado a ese negocio. Si ya autorizó antes los mismos permisos, no se le vuelven a preguntar.
Al volver, comprueba que state coincide y recoge el code (vale 10 minutos y un solo uso). Si la persona rechaza, recibes error=access_denied.
5. Cambia el código por tokens#
curl -X POST https://citeri.es/oauth/token \
-u "citeri_XXXXXXXXXXXXXXXXXXXXXXXX:cs_TU_SECRETO" \
-d grant_type=authorization_code \
-d code=EL_CODIGO_RECIBIDO \
-d redirect_uri=https://miapp.es/oauth/callback \
-d code_verifier=$CODE_VERIFIEREn una app pública, quita -u y envía -d client_id=…. Respuesta:
{{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "…",
"scope": "openid profile email offline_access citas:leer citas:escribir",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…"
}}El access_token dura 1 hora. El refresh_token (solo si pediste offline_access) dura 90 días. El id_token va firmado con RS256; verifícalo con las claves de /oauth/jwks.json (campo kid) y comprueba iss, aud y nonce.
6. Llama a la API#
curl https://citeri.es/oauth/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl "https://citeri.es/api/mcp" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{{"jsonrpc":"2.0","id":1,"method":"tools/list"}}'Si el token no cubre lo que pides, la respuesta es 403 con WWW-Authenticate: Bearer error="insufficient_scope". Puede pasar aunque el scope esté concedido, si la persona que autorizó ya no tiene ese permiso en el panel.
7. Renueva el token#
curl -X POST https://citeri.es/oauth/token \
-u "citeri_XXXXXXXXXXXXXXXXXXXXXXXX:cs_TU_SECRETO" \
-d grant_type=refresh_token \
-d refresh_token=$REFRESH_TOKENCada renovación devuelve un refresh_token nuevo y el anterior deja de valer. Guarda siempre el último: si se vuelve a usar uno ya cambiado, Citeri lo trata como un robo y revoca toda la conexión, y la persona tendrá que autorizar otra vez.
8. Desconectar#
curl -X POST https://citeri.es/oauth/revoke \
-u "citeri_XXXXXXXXXXXXXXXXXXXXXXXX:cs_TU_SECRETO" \
-d token=$REFRESH_TOKENEl negocio también puede desconectar tu app cuando quiera desde Ajustes → Aplicaciones conectadas; a partir de ese momento tus tokens devuelven 401.
Errores frecuentes#
| Error | Causa |
|---|---|
invalid_request | Falta un parámetro, o redirect_uri no es exactamente una de las registradas |
invalid_client | client_id o secreto incorrectos, o app suspendida |
invalid_grant | Código caducado o ya usado, code_verifier que no cuadra, o refresh revocado |
invalid_scope | Pides un permiso que tu app no tiene marcado |
access_denied | La persona rechazó, o la app no está verificada y el negocio no es el suyo |
insufficient_scope (403) | El token no cubre esa operación |