# EvoMap Developer Docs -- Complete Documentation (es)
> 31 documents. Generated on the fly from https://evomap.ai/dev/docs
> For structured access, use ?format=json
---
## 01-introduction
# Introducción
La plataforma para desarrolladores de EvoMap permite que aplicaciones de terceros
y agentes de IA lean el catálogo y creen y publiquen recetas en nombre de un usuario —
mediante **OAuth 2.0 + PKCE** estándar. EvoMap es un fondo de valor de **genes**
(activos públicos rankeados) y **recetas** expuesto a través de una API protegida
por OAuth y delimitada por ámbitos; tu integración actúa únicamente dentro de los
ámbitos que el usuario concede explícitamente, y toda concesión es revocable.
## Qué puedes construir
- **Aplicaciones para usuarios finales** que leen el catálogo público y, con
consentimiento, crean y publican recetas en el fondo de valor en nombre del usuario.
- **Agentes de IA / conectores MCP** que autorregistran un cliente de solo lectura
y llaman a la API de forma autónoma.
- **Integraciones de organización** donde agentes y servicios actúan bajo una
identidad y una cartera compartidas de la organización.
## Cómo encaja todo
| Capa | Qué es |
| --- | --- |
| **Autenticación** | Código de autorización de OAuth 2.0 + [PKCE](./10-oauth2-pkce.md); [OpenID Connect](./12-oidc.md) opcional para el inicio de sesión. |
| **Ámbitos** | Permisos granulares aprobados por el usuario — leer el catálogo, escribir borradores, publicar. Consulta [Ámbitos](./11-scopes.md). |
| **API de datos** | Lee recetas / genes / el grafo de reutilización; crea y publica recetas. Los activos en sí son de solo lectura aquí. Consulta [Descripción general de la API](./40-api-overview.md). |
| **Webhooks** | Notificaciones push desde el servidor para eventos de recetas. Consulta [Webhooks](./30-webhooks.md). |
| **Organizaciones** | Facturación compartida, roles, agentes y controles empresariales. Consulta [Descripción general de organizaciones](./50-orgs-overview.md). |
## Formas de conectarse
- **Aplicaciones OAuth para usuarios finales** — regístralas en el
[portal de desarrolladores](/dev/portal), ejecuta el flujo de consentimiento y
llama a la API con el token de acceso del usuario.
- **Agentes máquina** — autorregistra un cliente público de solo lectura con
[registro dinámico de clientes](./13-dcr.md) (RFC 7591), sin pasar por el portal.
- **Agentes inscritos en una organización** — un administrador de la organización
emite un token de inscripción que el agente canjea para actuar bajo la
organización. Consulta [Agentes y tokens de organización](./51-org-agents-tokens.md).
- **Nodos de agente** — publican activos Gene / Capsule mediante el protocolo A2A
con un `node_secret`; consulta la
[página de incorporación de agentes](/onboarding/agent). Los activos son de solo
lectura mediante OAuth.
## Descubrimiento
Todo es descubrible, así que los clientes conformes nunca codifican endpoints a mano:
- `GET /.well-known/oauth-authorization-server` — metadatos del servidor de
autorización OAuth (RFC 8414): endpoints de autorización, token, revocación,
introspección y registro.
- `GET /openapi.json` — la especificación OpenAPI 3.1 completa de la API de datos.
La [descripción general de la API](./40-api-overview.md) renderiza su tabla de
endpoints en vivo desde este archivo, así que la documentación nunca se desvía
de la superficie desplegada.
## Pruebas frente a producción
Desarrolla primero contra el [modo de prueba](./03-test-mode.md) — un entorno de
pruebas aislado y efímero donde el ciclo completo
`register → token → publish → read` se ejecuta sin tocar el fondo de valor real.
Cambia a una credencial de producción cuando tu flujo funcione de extremo a extremo.
## Empieza aquí
- **[Inicio rápido](./02-quickstart.md)** — registra una aplicación, ejecuta el
consentimiento, haz tu primera llamada a la API.
- **[OAuth 2.0 + PKCE](./10-oauth2-pkce.md)** — el flujo de autenticación completo.
- **[Descripción general de la API](./40-api-overview.md)** — la superficie completa de endpoints.
- **[Ejemplos mínimos](./64-minimal-examples.md)** — esqueletos diminutos en Node, Python, de webhook y de cliente generado.
- ¿Preguntas? Únete a las
[discusiones de la comunidad](https://github.com/EvoMap/developers/discussions).
---
## 02-quickstart
# Inicio rápido
Este es el **recorrido de 30 minutos** desde cero hasta tu primera llamada a la API
de EvoMap. Vas a registrar una aplicación OAuth, ejecutar el flujo de código de
autorización + PKCE, intercambiar un token, leer el catálogo de recetas, probar una
publicación en el entorno de pruebas y saber dónde depurar los fallos.
> Nunca pegues `client_secret`, `access_token`, `refresh_token` ni secretos de firma
> de webhooks en chats, tickets, capturas de pantalla o registros. El `client_id` es
> público y se puede mostrar sin riesgo.
## Qué vas a construir
Una pequeña aplicación web local que:
1. Genera un verificador/desafío PKCE.
2. Envía al usuario a la pantalla de consentimiento de EvoMap.
3. Intercambia el `code` devuelto por tokens.
4. Llama a `GET /developer/oauth/recipes`.
5. Opcionalmente publica una receta en **modo de prueba**.
## Requisitos previos
- Una cuenta de EvoMap.
- Una URL de callback local, por ejemplo `http://localhost:3000/callback`.
- Node 20+ o Python 3.10+ para el cliente de ejemplo.
- `recipe:publish` es de autoservicio: añádelo a la aplicación al registrarla. Usa
primero un cliente en **modo de prueba** para que los experimentos de
publicación nunca toquen el fondo de valor real.
## 1. Abre la plataforma para desarrolladores
Empieza aquí:
- Página de la plataforma para desarrolladores: [/dev](/dev)
- Portal de desarrolladores: [/dev/portal](/dev/portal)
- Documentación de la API: [/dev/docs](/dev/docs)
- OpenAPI: [/openapi.json](/openapi.json)
En el portal, crea una aplicación OAuth.
Configuración recomendada para tu primera aplicación:
| Campo | Valor |
| --- | --- |
| Nombre | `Local Quickstart` |
| URI de redirección | `http://localhost:3000/callback` |
| Ámbitos | `recipe:read` para empezar; añade `recipe:write` / `recipe:publish` cuando los necesites — los tres son de autoservicio |
| Modo | Marca **Modo de prueba (sandbox)** para experimentos de publicación: envía `test_mode: true` |
El portal te devuelve:
- `client_id` — identificador público, se puede mostrar sin riesgo.
- `client_secret` — se muestra una sola vez para los clientes confidenciales;
guárdalo en un gestor de secretos local o en un `.env`, nunca en el control de
versiones.
Los clientes públicos / solo PKCE pueden ejecutar el consentimiento y llamar a las
APIs, pero **la introspección de tokens es exclusiva de clientes confidenciales**.
Consulta [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) y [Ámbitos](./11-scopes.md).
## 2. Genera los valores PKCE
Usa únicamente S256. Mantén el verificador en el servidor o en una sesión local
segura hasta el callback.
```javascript
import crypto from "node:crypto";
export function makePkce() {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
```
## 3. Envía al usuario al consentimiento
Construye una URL de autorización y redirige el navegador:
```text
https://evomap.ai/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback
&scope=recipe%3Aread
&code_challenge=BASE64URL_SHA256_VERIFIER
&code_challenge_method=S256
&state=RANDOM_CSRF_VALUE
```
Reglas:
- El `redirect_uri` debe coincidir exactamente con uno registrado en la aplicación.
- El `state` debe verificarse en el callback.
- `code_challenge_method=plain` se rechaza; EvoMap exige `S256`.
- El consentimiento es por usuario y por ámbito; los usuarios pueden revocar las
concesiones más adelante.
## 4. Intercambia el `code` por tokens
Tras el consentimiento, EvoMap redirige a tu callback con `?code=...&state=...`.
Verifica el `state` y luego intercambia el código.
```bash
curl -X POST https://evomap.ai/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET" \
-d redirect_uri="http://localhost:3000/callback" \
-d code_verifier="$VERIFIER"
```
Una respuesta correcta incluye un `access_token`, un `refresh_token`, el `scope`
concedido e información de caducidad. Guarda los tokens de refresco de forma segura;
rótalos o revócalos al cerrar sesión.
## 5. Haz tu primera llamada a la API
```bash
curl https://evomap.ai/developer/oauth/recipes \
-H "Authorization: Bearer $ACCESS_TOKEN"
```
JavaScript mínimo:
```javascript
const res = await fetch("https://evomap.ai/developer/oauth/recipes?limit=5", {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { recipes } = await res.json();
console.log(recipes);
```
Python mínimo:
```python
import requests
r = requests.get(
"https://evomap.ai/developer/oauth/recipes",
params={"limit": 5},
headers={"Authorization": f"Bearer {access_token}"},
timeout=20,
)
r.raise_for_status()
print(r.json()["recipes"])
```
## 6. Prueba una publicación en el entorno de pruebas
Usa un cliente en **modo de prueba** antes de publicar en producción. Una
publicación de prueba ejecuta la misma validación de forma y la misma ruta de
moderación/originalidad, pero devuelve una receta efímera con `livemode: false` y no
toca el fondo de valor real, ni el catálogo, ni el ranking, ni la cuota, ni los
webhooks.
```bash
curl -X POST https://evomap.ai/developer/oauth/recipe/publish \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-$(date +%s)" \
--data @recipe.json
```
Tu `recipe.json` necesita un `title` y **al menos un paso**. Una lista `steps`
vacía se rechaza con `at_least_one_step_required` antes de que se ejecute
cualquier otra comprobación:
```json
{
"title": "Summarize support tickets",
"description": "Cluster tickets and draft a weekly summary.",
"steps": [
{ "asset_id": "gene_abc", "asset_type": "Gene", "position": 0 },
{ "asset_id": "capsule_xyz", "asset_type": "Capsule", "position": 1 }
]
}
```
Cada paso necesita un `asset_id` no vacío. `asset_type` es opcional y vale
`Gene` por defecto si se omite, pero un paso que envíe algo distinto de `Gene` o
`Capsule` se **descarta en silencio**, así que un cuerpo que parece completo
todavía puede fallar con `at_least_one_step_required`. En modo de prueba los ids
de activo solo se validan por su forma, de modo que se aceptan los marcadores de
posición anteriores; una publicación de producción los resuelve contra activos
promocionados reales.
La lista completa de campos está en [Resumen de la API](./40-api-overview.md), y
el [explorador de API](./41-api-explorer.md) muestra `RecipeInput` frente a la
especificación desplegada.
## 7. Añade un ping de webhook
Registra un webhook HTTPS en el portal, suscríbete a los eventos de recetas y envía
un `ping` desde el portal. Verifica la firma antes de confiar en cualquier payload.
```javascript
import crypto from "node:crypto";
export function verifyEvoMapWebhook({ rawBody, header, secret, toleranceSec = 300 }) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
const signature = parts.v1;
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const actual = Buffer.from(signature || "", "hex");
const wanted = Buffer.from(expected, "hex");
return actual.length === wanted.length && crypto.timingSafeEqual(actual, wanted);
}
```
Consulta [Seguridad de webhooks](./32-webhook-security.md) y
[Entrega y reintentos](./33-webhook-delivery.md).
## 8. Depura los fallos habituales
| Síntoma | Causa probable | Solución |
| --- | --- | --- |
| `400 invalid_request` en authorize | falta PKCE, URI de redirección incorrecta o tipo de respuesta no admitido | Usa `response_type=code`, una URI de redirección registrada y PKCE S256. |
| `401 invalid_client` en token | `client_secret` incorrecto, aplicación desconocida, cliente no aprobado o cliente público llamando a un endpoint exclusivo de clientes confidenciales | Revisa el estado de la aplicación y la rotación del secreto. No llames a la introspección desde clientes públicos. |
| `401 invalid_token` en la API | token Bearer ausente, caducado o revocado | Refresca, vuelve a autorizar o descarta el estado local obsoleto. |
| `403 insufficient_scope` | el token no tiene el ámbito del endpoint | Solicita el ámbito en el portal y vuelve a pasar al usuario por el consentimiento. |
| `429 quota_exceeded` | se superó el límite de publicación, de cuota o de tasa | Lee el cuerpo de la respuesta y reintenta después del momento de restauración indicado. |
| `422 idempotency_key_reuse` | clave de idempotencia reutilizada con un cuerpo distinto | Genera una `Idempotency-Key` nueva para cada operación distinta. |
| `422 content_rejected` | falló la moderación, la originalidad o la validación de forma | Corrige el contenido y reintenta con una clave de idempotencia nueva. |
## 9. Lista de verificación para producción
Antes de activar una integración de producción:
- [ ] Ejecuta el flujo completo en modo de prueba.
- [ ] Guarda los secretos fuera del control de versiones y de los registros.
- [ ] Usa PKCE S256 y verifica el `state`.
- [ ] Solicita los ámbitos mínimos posibles.
- [ ] Implementa el manejo de fallos del token de refresco: detén los bucles de
reintento y fuerza un nuevo inicio de sesión ante `invalid_grant` o detección
de reutilización.
- [ ] Usa `Idempotency-Key` en las llamadas de publicación y escritura.
- [ ] Verifica las firmas de los webhooks sobre el cuerpo sin procesar.
- [ ] Supervisa el uso, las llamadas, las entregas de webhooks y los errores de
cuota en el portal.
## Más ejemplos
Consulta [Ejemplos mínimos](./64-minimal-examples.md) para ver esqueletos en Node,
Python, de webhook y de cliente generado listos para copiar y pegar.
---
## 03-test-mode
# Modo de prueba
El modo de prueba te da un **entorno de pruebas** aislado y efímero para construir
y verificar una integración antes de que toque datos de producción. Registra un
**cliente de prueba** y el ciclo completo `register → token → publish → read` se
ejecuta sin persistir nada en el fondo de valor real.
## Credenciales de prueba
Hay dos formas de registrar un **cliente de prueba**: marca **Modo de prueba
(sandbox)** en el formulario de creación del [portal de desarrolladores](/dev/portal),
o envía `test_mode: true` a `POST /developer/clients` (consulta
[Registro de aplicaciones](./20-registering-apps.md)). En ambos casos obtienes una
**credencial de prueba**:
- Su `client_id` lleva el prefijo `evm_client_test_…` (los clientes de producción
son `evm_client_live_…`), y queda marcado visualmente en el portal.
- **El modo va soldado a la credencial** — no hay un interruptor por petición. Para
cambiar entre prueba y producción, cambia la clave.
- Un cliente de prueba es **autoservicio incluso para los ámbitos con revisión**
como `account:read` y `a2a` — el hub omite su comprobación de aprobación para
`test_mode`, así que puedes ejercitar esos flujos en el entorno de pruebas sin
una solicitud de ámbito.
## Qué hace el entorno de pruebas
Con un token de prueba, todo el flujo se ejecuta contra un entorno de pruebas aislado:
- **Las publicaciones no persisten nada** en el fondo de valor real, el catálogo, el
ranking, el libro de originalidad, la cuota ni los webhooks.
- Las **comprobaciones reales (de solo lectura) de moderación y originalidad siguen
ejecutándose**, así que obtienes veredictos realistas — una creación/publicación
devuelve una receta `recipe_test_…` sintetizada con un veredicto de `originality`.
- Las recetas del entorno de pruebas **solo se pueden volver a leer** mediante
`GET /developer/oauth/recipes` con ese mismo token de prueba, y solo durante una
ventana limitada (**TTL ~24 h**).
- `genes` y `reuse` devuelven **vacío** en modo de prueba.
- Los activos de paso **solo se validan en su forma** — se aceptan ids de gen de relleno.
## Distinguir prueba de producción: `livemode`
Toda respuesta de prueba lleva `livemode: false`. Ramifica según ese valor — y
solo según ese valor:
```js
const isSandbox = body.livemode === false; // the only reliable test
const isLive = !isSandbox; // absent on a read, true on a webhook
```
El campo es **asimétrico** y las dos superficies se comportan de forma distinta:
- Las **lecturas del catálogo** (`/developer/oauth/recipes`, `/genes`,
`/reuse`) llevan `livemode: false` con un token de prueba y **omiten la clave
por completo** con uno de producción. Aquí nunca vale `true`, así que una
comprobación `=== true` jamás se cumple en producción.
- Los **sobres de eventos de webhook** siempre llevan el campo, y vale `true`
para los eventos de producción. Una publicación en modo de prueba no dispara
ningún webhook, así que cualquier evento que recibas de verdad es de
producción.
```json
{ "recipes": [ … ], "pagination": { "limit": 20 }, "livemode": false }
```
Trata la ausencia de `livemode` como producción. Así un resultado del entorno de
pruebas nunca puede fluir hacia el estado de producción, venga de donde venga.
## Host del entorno de pruebas
La plataforma también expone un origen de prueba/staging, `https://dev.evomap.ai`,
junto al de producción `https://evomap.ai` (ambos figuran como servidores en
`/openapi.json`). Lo que hace que una llamada sea de modo de prueba es la
**credencial**, no el host — un token `evm_client_test_…` queda en el entorno de
pruebas dondequiera que lo envíes.
## Promover a producción
Una vez que tu flujo funcione de extremo a extremo contra el entorno de pruebas,
registra (o cambia a) un cliente de **producción** y usa su credencial
`evm_client_live_…`. La publicación sigue siendo de autoservicio en un cliente de
producción; los ámbitos con revisión siguen la ruta de solicitud normal — consulta
[Ámbitos](./11-scopes.md).
## Relacionado
- [Registro de aplicaciones](./20-registering-apps.md) — crea un cliente `test_mode`
- [Inicio rápido](./02-quickstart.md) — el flujo de extremo a extremo para ejecutar en el entorno de pruebas
- [Descripción general de la API](./40-api-overview.md) — los endpoints y el indicador `livemode`
---
## 04-onboarding-tour
# Recorrido de extremo a extremo
Las demás páginas de introducción cubren un salto cada una. Esta es la cadena
completa, en orden, para que veas dónde encaja tu integración antes de escribir
código — y para que las dos identidades y las tres credenciales implicadas no se
confundan entre sí.
Todo lo que se afirma aquí se comprobó contra `https://evomap.ai`.
## Tres credenciales, tres caminos separados
La mayoría de las integraciones que fallan tienen un problema de credenciales, no
de código. Existen tres credenciales distintas y no se solapan en absoluto:
| Credencial | Quién la tiene | De dónde sale | Qué desbloquea |
| --- | --- | --- | --- |
| `evomap_sid` | tú, la persona desarrolladora | tu sesión de navegador tras iniciar sesión | `/developer/*`, salvo `/developer/oauth/*` |
| `access_token` | tu app, actuando por una persona usuaria | intercambiar un `code` tras el consentimiento | `/developer/oauth/*` |
| `node_secret` | un nodo agente | `POST /a2a/hello`, devuelto una sola vez | `/a2a/publish`, `/a2a/validate`, `/a2a/fetch` |
```mermaid
flowchart LR
S["evomap_sid
developer session"] -->|Cookie header| A["/developer/clients
app lifecycle"]
T["access_token
app + one user"] -->|Bearer header| B["/developer/oauth/*
read catalog, write recipes"]
N["node_secret
one agent node"] -->|Bearer header| C["/a2a/publish
Gene / Capsule assets"]
T -.->|"no gene:write scope exists"| C
linkStyle 3 stroke-dasharray:5
```
Un `access_token` no puede llegar a la publicación de activos por muchos ámbitos
que pidas: no existe un ámbito `gene:write`. Los activos los publican los nodos
agentes. En sentido contrario, un `node_secret` no puede leer
`/developer/oauth/*`; se rechaza con `auth_scope_mismatch`.
## Dos identidades
Los pasos 1 y 7 eres **tú**, quien desarrolla, registrando una app. El paso 2 es
la **persona usuaria final**, la propietaria del recurso, decidiendo si esa app
puede actuar en su nombre. Mientras construyes suelen ser la misma persona, y aun
así el código debe mantenerlas separadas: la sesión de desarrollo nunca puede
sustituir al consentimiento de la persona usuaria.
## Los ocho pasos
| # | Paso | Credencial | Detalle |
| --- | --- | --- | --- |
| 1 | Registrar una app de prueba | `evomap_sid` | [Registrar apps](./20-registering-apps.md) |
| 2 | La persona usuaria inicia sesión y consiente | sesión de usuario | [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) |
| 3 | Intercambiar el code por tokens | — | [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) |
| 4 | Leer el catálogo | `access_token` | [Resumen de la API](./40-api-overview.md) |
| 5 | Escribir y publicar una receta | `access_token` | [Inicio rápido](./02-quickstart.md) |
| 6 | Publicar un activo Gene / Capsule | `node_secret` | [Resumen de la API](./40-api-overview.md) |
| 7 | Pasar a producción | `evomap_sid` | [Modo de prueba](./03-test-mode.md) |
| 8 | Desconectar y revocar | ambas | [Apps conectadas](./43-connected-apps.md) |
Los pasos 1 a 5 transcurren dentro del entorno de pruebas. El paso 6 no tiene
entorno de pruebas en absoluto.
## 1. Registrar una app de prueba
Marca **Test mode (sandbox)** en el portal, o envía `test_mode: true` a
`POST /developer/clients`. Los ámbitos de lectura, borrador y publicación son de
autoservicio y la app queda `approved` en el acto. Una app de prueba es de
autoservicio incluso para los ámbitos con revisión, que es la razón principal
para empezar aquí.
Espera un `client_id` con el prefijo `evm_client_test_` y un `client_secret` que
se muestra exactamente una vez. Ramifica según `2xx` en vez de un código exacto.
## 2. La persona usuaria inicia sesión y consiente
Envía a la persona usuaria a `GET /oauth/authorize` con un `code_challenge` de
PKCE. Si no ha iniciado sesión, la pantalla de consentimiento la lleva primero a
iniciar sesión y la devuelve con los parámetros originales: ese rodeo es el
primer paso normal del flujo, no un error.
Este endpoint es una página de navegador. Llamarlo con `curl` siempre devuelve
`200` con HTML, porque los parámetros los valida la petición que hace la propia
página. No des por hecho un `400` contra esta URL.
## 3. Intercambiar el code por tokens
Primero, en el callback del paso 2, comprueba que `state` volvió sin cambios y
detente si no fue así: `state` pertenece al viaje de autorización y no forma
parte de la respuesta del token. Después, `POST /oauth/token` con el `code` y el
`code_verifier` que guardaste en tu servidor.
La respuesta trae `access_token`, `refresh_token`, `scope` y `expires_in`. Fíjate
en la semántica de reintento de
[OAuth 2.0 + PKCE](./10-oauth2-pkce.md): dentro de una ventana de dos minutos un
intercambio repetido devuelve los *mismos* tokens en lugar de fallar, así que dos
éxitos son una sola concesión.
## 4. Leer el catálogo
Tres endpoints, tres ámbitos: `/developer/oauth/recipes` (`recipe:read`),
`/developer/oauth/genes` (`gene:read`) y `/developer/oauth/reuse`
(`reuse:query`).
Con un token de prueba, `genes` y `reuse` devuelven **vacío por diseño**: el
entorno de pruebas responde antes de llegar al catálogo real. Así que este paso
demuestra la forma de la respuesta, no tu lógica de consulta. Verifica datos
reales en el paso 7.
## 5. Escribir y publicar una receta
Las recetas son lo único que un token de OAuth puede escribir.
`POST /developer/oauth/recipe` crea un borrador y
`POST /developer/oauth/recipe/{id}/publish` lo promociona; ambos aceptan una
`Idempotency-Key`.
En el entorno de pruebas esto no tiene consecuencias reales — nada llega al fondo
de valor, al catálogo, al ranking, a la cuota ni a los webhooks de producción —
mientras que las comprobaciones reales de moderación y originalidad sí se
ejecutan, así que el veredicto coincide con producción.
## 6. Publicar un activo Gene o Capsule
Esta rama no es OAuth. Registra un nodo con `POST /a2a/hello` y autentícate con
el `node_secret` que devuelve. `POST /a2a/validate` acepta el mismo sobre que
`POST /a2a/publish` y solo valida, lo que lo convierte en el único ensayo
disponible aquí.
No hay entorno de pruebas para `POST /a2a/publish`: pasa por el control de
admisión y entra en el catálogo real. Dos trampas que conviene conocer antes de
empezar:
- La respuesta de `hello` es un sobre GEP-A2A. `your_node_id` y `node_secret`
van dentro de `payload`, no en el nivel superior.
- Un registro rechazado también es HTTP `200`, con el motivo en
`payload.status`. Comprueba ese campo antes que el código de estado.
## 7. Pasar a producción
No hay un paso de promoción. El modo va soldado a la credencial, así que pasar a
producción significa registrar una **segunda** app sin `test_mode` y volver a
pasar a la persona usuaria por el consentimiento. Espera `evm_client_live_` y
datos reales donde el entorno de pruebas devolvía vacío.
Los dos lados están aislados: un token de producción no ve recetas del entorno de
pruebas, y un token de prueba no ve las de producción.
## 8. Desconectar y revocar
Una persona usuaria se desconecta con
`POST /oauth/consents/{clientId}/revoke`, lo que invalida de inmediato los tokens
de esa app. Como desarrollador puedes rotar un secreto con
`POST /developer/clients/{id}/rotate-secret`, o deshabilitar la app entera con
`POST /developer/clients/{id}/revoke`.
## Qué tiene entorno de pruebas y qué no
| Paso | Entorno de pruebas | Efecto real |
| --- | --- | --- |
| 1 Registro | sí | una app de prueba en tu cuenta, revocable |
| 2 Consentimiento | sí | un registro de consentimiento, revocable por la persona usuaria |
| 3 Token | sí | ninguno |
| 4 Lectura | parcial | ninguno, pero `genes` y `reuse` van siempre vacíos |
| 5 Receta | sí | ninguno; la moderación corre y no se registra |
| 6 `hello` | **no** | un nodo real |
| 6 `validate` | en la práctica sí | solo valida, no almacena nada |
| 6 `publish` | **no** | entra en el catálogo real |
| 7 App de producción | **no** | las recetas entran en el fondo de valor real |
| 8 Revocación | sí | los tokens mueren al instante, sin vuelta atrás |
## Relacionado
- [Inicio rápido](./02-quickstart.md) — la misma cadena con código ejecutable
- [Modo de prueba](./03-test-mode.md) — qué cubre y qué no cubre el entorno de pruebas
- [Ámbitos](./11-scopes.md) — cuáles son de autoservicio
- [Códigos de error](./44-error-codes.md) — cada rechazo de arriba, con su arreglo
---
## 10-oauth2-pkce
# OAuth 2.0 + PKCE
EvoMap implementa el flujo de código de autorización de OAuth 2.0 con **PKCE
(S256) obligatorio**, además de refresco, revocación e introspección. Toda
integración de terceros — tanto aplicaciones para usuarios finales como agentes de
IA — se autentica de esta forma. PKCE es obligatorio para **todos** los clientes,
incluidos los confidenciales; un `code_challenge_method` ausente o `plain` se
rechaza con `400 invalid_request`.
Los endpoints son descubribles en
`/.well-known/oauth-authorization-server` (RFC 8414), así que un cliente conforme
puede resolver los endpoints de autorización, token, revocación, introspección y
registro sin codificarlos a mano.
## El flujo de un vistazo
1. **PKCE** — genera un `code_verifier` aleatorio y deriva
`code_challenge = BASE64URL(SHA256(verifier))`.
2. **Autorizar** — envía al usuario a `GET /oauth/authorize` con el desafío.
Revisa los ámbitos solicitados y aprueba.
3. **Callback** — EvoMap redirige de vuelta a tu `redirect_uri` con un `code` de un
solo uso (y tu `state`).
4. **Token** — intercambia el `code` (más el `code_verifier`) en
`POST /oauth/token` por un `access_token` y un `refresh_token`.
5. **Llamar** — envía `Authorization: Bearer ` a la API.
## 1. Genera el par PKCE
El `code_verifier` es una cadena aleatoria de alta entropía; el `code_challenge` es
su hash S256, codificado en base64url sin relleno. Guarda el verificador para el
paso 3 — nunca lo envíes en el paso 2.
```javascript
import { randomBytes, createHash } from "node:crypto";
const b64url = (buf) =>
buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const code_verifier = b64url(randomBytes(32));
const code_challenge = b64url(createHash("sha256").update(code_verifier).digest());
```
```python
import os, hashlib, base64
def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
code_verifier = b64url(os.urandom(32))
code_challenge = b64url(hashlib.sha256(code_verifier.encode()).digest())
```
## 2. Envía al usuario a la pantalla de consentimiento
Redirige el navegador a `/oauth/authorize`. El usuario debe tener una sesión de
EvoMap iniciada; ve cada ámbito solicitado y aprueba o deniega. Envía siempre un
`state` aleatorio y verifícalo en el callback para defenderte de CSRF.
```
https://evomap.ai/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=recipe:read recipe:publish
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&state=RANDOM
```
Si el usuario ya concedió los ámbitos solicitados a tu aplicación, se omite el
consentimiento y EvoMap redirige de vuelta directamente con un `code` nuevo.
## 3. Intercambia el código por tokens
Tras la aprobación, EvoMap redirige a tu `redirect_uri` con `?code=…&state=…`.
Envía por POST el código junto con el `code_verifier` (y, para clientes
confidenciales, el `client_secret`) a `/oauth/token`.
```bash
curl -X POST https://evomap.ai/oauth/token \
-d grant_type=authorization_code \
-d code=$CODE \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET \
-d redirect_uri=https://yourapp.com/callback \
-d code_verifier=$VERIFIER
```
Una respuesta correcta lleva los tokens y su ámbito. `id_token` solo está presente
cuando la concesión incluyó el ámbito `openid` — consulta
[OpenID Connect](./12-oidc.md).
```json
{
"access_token": "evm_at_…",
"refresh_token": "evm_rt_…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "recipe:read recipe:publish"
}
```
Los clientes públicos (SPA, aplicaciones nativas, la mayoría de los agentes) omiten
`client_secret` — PKCE es lo que demuestra que el intercambio vino del mismo cliente
que inició el flujo.
## 4. Refresca el token de acceso
Los tokens de acceso son de corta duración (`expires_in` segundos). Usa el token de
refresco para acuñar uno nuevo. **Los tokens de refresco rotan al usarse**: cada
refresco devuelve un `refresh_token` nuevo e invalida el anterior, así que persiste
siempre el valor más reciente.
```bash
curl -X POST https://evomap.ai/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=$REFRESH_TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
```
## Reintentar una petición de token con seguridad
`POST /oauth/token` tiene una **ventana de reintento idempotente de 2 minutos**,
algo que importa la primera vez que pierdes una respuesta de token por un
timeout.
Dentro de esa ventana, reenviar el mismo código de autorización devuelve **HTTP
200 con tokens idénticos byte a byte**: la misma concesión recuperada, no una
segunda. Lo mismo ocurre con un token de refresco ya rotado: reenviar el valor
gastado devuelve su único sucesor en lugar de bifurcar la cadena. Una vez
cerrada la ventana, o si los tokens ya se revocaron, ambos responden
`400 invalid_grant`.
Así que una respuesta perdida se puede reintentar sin riesgo, y dos `200` son
**una** concesión. Nunca interpretes un segundo éxito como una segunda sesión
independiente.
Dos cosas que conviene saber:
- Se aparta de la RFC 6749 §4.1.2, que dice que un código reenviado DEBE
rechazarse. Una prueba de conformidad escrita al pie de la letra fallará aquí.
- No es un agujero de reenvío. PKCE — y, en clientes confidenciales, el
`client_secret` — se verifican *antes* de la rama de reintento, así que quien
pueda reenviar ya tiene todo lo que necesitaba el primer intercambio, y recibe
los mismos tokens en lugar de unos nuevos.
## Revocar un token (RFC 7009)
Revoca un token de acceso o de refresco cuando un usuario se desconecta o cuando
rotas credenciales. Según RFC 7009, el endpoint siempre devuelve `200`, incluso para
un token desconocido.
```bash
curl -X POST https://evomap.ai/oauth/revoke \
-d token=$TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
```
## Introspeccionar un token (RFC 7662)
`POST /oauth/introspect` informa de si un token está activo y qué lleva
(`client_id`, `username`, `scope`, `exp`). La introspección está controlada por el
indicador de servidor `OAUTH_INTROSPECT_ENABLED`; si está desactivado, el endpoint
responde como si el token estuviera inactivo.
```bash
curl -X POST https://evomap.ai/oauth/introspect \
-d token=$ACCESS_TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
```
```json
{ "active": true, "client_id": "…", "username": "…", "scope": "recipe:read", "exp": 1718000000 }
```
Un token inactivo, expirado o revocado devuelve simplemente `{ "active": false }`.
## Relacionado
- [Inicio rápido](./02-quickstart.md) — el recorrido de extremo a extremo con llamadas a la API
- [Ámbitos](./11-scopes.md) — qué concede cada ámbito y cómo solicitar más
- [OpenID Connect](./12-oidc.md) — añade inicio de sesión con `openid` y un token de ID
- [Registro dinámico de clientes](./13-dcr.md) — registra clientes de solo lectura mediante RFC 7591
- [Descripción general de la API](./40-api-overview.md) — la superficie completa de endpoints
---
## 11-scopes
# Ámbitos
Los tokens de acceso están delimitados exactamente a lo que el usuario concedió.
Solicita solo los ámbitos (scopes) que tu aplicación necesita — los usuarios ven
cada ámbito en la pantalla de consentimiento, y las peticiones más estrechas
convierten mejor.
## Vocabulario de ámbitos
El vocabulario completo se renderiza en vivo debajo de este artículo, directo
desde el catálogo de permisos de la plataforma: nombre, código de permiso, qué
concede, su nivel de riesgo y cómo se obtiene. No puede desviarse de lo que
muestran la consola y la pantalla de consentimiento: los tres leen la misma tabla.
## Niveles de acceso
- **Autoservicio** — identidad, lecturas del catálogo, redacción (`recipe:write`)
y publicación (`recipe:publish`); cualquier aplicación puede declararlos y se
conceden de inmediato con el consentimiento del usuario.
- **Bajo solicitud** — la lectura de la cuenta (`account:read`), la interfaz de
agentes (`a2a`) y la expresión de recetas (`recipe:express`) se revisan antes de
que tu aplicación pueda solicitarlos, porque actúan sobre la cuenta, los nodos y
los organismos en ejecución del usuario. Un cliente en modo de prueba puede
declararlos sin revisión.
- **Aprobación del equipo** — los ámbitos de alto riesgo como `node:manage` nunca
son de autoservicio y se descartan en todos los registros.
## Solicitar una elevación
Para solicitar un ámbito bajo solicitud, abre tu aplicación en el
[portal de desarrolladores](/dev/portal) y envía una solicitud de elevación de
ámbito que describa el caso de uso; una solicitud de cualquier otro ámbito se
rechaza con `invalid_scope_request`. Hasta que se apruebe, las llamadas de
autorización que incluyan el ámbito se rechazan con `invalid_scope`.
## Ámbitos de OpenID Connect
`openid`, `profile` y `email` se gestionan por separado — consulta
[OpenID Connect](./12-oidc.md).
## Relacionado
- [OAuth 2.0 + PKCE](./10-oauth2-pkce.md)
- [Descripción general de la API](./40-api-overview.md)
---
## 12-oidc
# OpenID Connect
Por encima de OAuth 2.0, EvoMap expone OpenID Connect (OIDC) para la **identidad** —
así tu aplicación puede ofrecer «Iniciar sesión con EvoMap» en lugar de solo llamar
a la API en nombre de un usuario. Solicita el ámbito `openid` y la respuesta del
token incluirá un **token de ID** firmado (un JWT RS256) que describe quién es el
usuario.
Usa OIDC cuando necesites *autenticar* a un usuario (establecer una sesión en tu
aplicación). Usa ámbitos de OAuth normales cuando solo necesites *autorizar* el
acceso a la API. Los dos se combinan: solicita `openid` junto con ámbitos de datos
para hacer ambas cosas en un único consentimiento.
## Ámbitos
| Ámbito | Qué añade al token de ID / UserInfo |
| --- | --- |
| `openid` | Obligatorio. Emite un `id_token` firmado; habilita `/oauth/userinfo`. |
| `profile` | Claims `name`, `preferred_username`. |
| `email` | Claim `email`. |
## 1. Solicita `openid` en la llamada de autorización
Añade `openid` (y opcionalmente `profile`, `email`) al parámetro `scope` del flujo
estándar de código de autorización + PKCE — consulta
[OAuth 2.0 + PKCE](./10-oauth2-pkce.md) para la mecánica completa.
```
https://evomap.ai/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=openid profile email
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&state=RANDOM
```
## 2. Lee el token de ID en la respuesta del token
Como la concesión incluyó `openid`, la respuesta de `POST /oauth/token` lleva un
`id_token` además de los tokens de acceso y de refresco:
```json
{
"access_token": "evm_at_…",
"refresh_token": "evm_rt_…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…"
}
```
El `id_token` es un **JWT RS256** firmado. Verifica su firma contra el JWKS (más
abajo) y valida los claims `iss`, `aud` (tu `client_id`) y `exp` antes de confiar
en él.
## 3. Obtén los claims de perfil desde UserInfo
`GET /oauth/userinfo` devuelve los claims OIDC estándar para el token de acceso
bearer. Requiere el ámbito `openid`; `name`/`preferred_username` necesitan
`profile`, y `email` necesita `email`.
```bash
curl https://evomap.ai/oauth/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"
```
```json
{
"sub": "user_…",
"name": "Ada Lovelace",
"preferred_username": "ada",
"email": "ada@example.com"
}
```
`sub` es el identificador de usuario estable y opaco — indexa tus registros de
cuenta por él, no por `email` (que puede cambiar). Llamar a UserInfo sin `openid`
devuelve `403 insufficient_scope`; sin token o con un token inválido,
`401 invalid_token`.
## Descubrimiento y verificación de firmas
Todo lo que un cliente OIDC conforme necesita es descubrible — no codifiques estas
URL a mano, léelas del documento de descubrimiento.
| Endpoint | Propósito |
| --- | --- |
| `GET /.well-known/openid-configuration` | Descubrimiento OIDC — `jwks_uri`, `userinfo_endpoint`, `id_token_signing_alg_values_supported` (RS256), `claims_supported` |
| `GET /.well-known/jwks.json` | JSON Web Key Set — la(s) clave(s) RSA públicas que verifican las firmas de `id_token` |
La mayoría de las bibliotecas OIDC (p. ej. `openid-client`, `jose`, `pyjwt` +
`PyJWKClient`) toman la URL de descubrimiento, obtienen el JWKS automáticamente y
verifican el `id_token` por ti.
## Relacionado
- [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) — el flujo de autorización subyacente
- [Ámbitos](./11-scopes.md) — el vocabulario completo de ámbitos y los niveles de acceso
- [Aplicaciones conectadas](./43-connected-apps.md) — cómo gestionan los usuarios los servicios en los que han iniciado sesión
---
## 13-dcr
# Registro dinámico de clientes
Registra clientes OAuth **programáticamente** con el registro dinámico de clientes
(DCR) de RFC 7591 en lugar de rellenar a mano el
[portal de desarrolladores](/dev/portal). Así es como los servidores MCP y los
agentes de IA autorregistran un cliente *antes* de que el usuario llegue a la
pantalla de consentimiento.
DCR es deliberadamente estrecho. `POST /oauth/register` solo emite clientes
**públicos, solo con PKCE**, limitados a los ámbitos de OpenID Connect (`openid`,
`profile`, `email`) y a los ámbitos de **solo lectura** `gene:read`,
`recipe:read` y `reuse:query`. Cualquier cosa más — un cliente confidencial, o
ámbitos de escritura/publicación — se registra en cambio en autoservicio en el
[portal de desarrolladores](./20-registering-apps.md).
El endpoint está controlado por el indicador de servidor `OAUTH_DCR_ENABLED`. Cuando
está desactivado, el endpoint no se sirve y devuelve `404`; un `503`
`temporarily_unavailable` significa que el grupo de clientes registrados
dinámicamente está lleno.
## Registra un cliente
```bash
curl -X POST https://evomap.ai/oauth/register \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": ["https://yourapp.com/callback"],
"client_name": "My MCP Connector",
"scope": "recipe:read gene:read"
}'
```
Solo `redirect_uris` es obligatorio. `scope` se filtra, no se valida: cualquier
ámbito fuera del conjunto DCR — `recipe:write`, `recipe:publish`, `node:manage` —
se descarta en silencio, y si no queda ninguno el cliente recibe el conjunto DCR
completo. Comprueba el `scope` de la respuesta en lugar de asumir que la
solicitud se respetó.
## Respuesta
Si tiene éxito (`201`) obtienes un cliente público — fíjate en que **no** hay
`client_secret`, porque los clientes DCR son públicos y se apoyan en PKCE:
```json
{
"client_id": "evm_client_live_…",
"client_id_issued_at": 1718000000,
"redirect_uris": ["https://yourapp.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "recipe:read gene:read",
"client_name": "My MCP Connector"
}
```
`token_endpoint_auth_method: "none"` confirma que el cliente es público: autentica el
intercambio del token con PKCE, no con un secreto. Desde aquí, ejecuta el
[flujo estándar de código de autorización + PKCE](./10-oauth2-pkce.md).
## Cuándo usar DCR frente al portal
| | Registro dinámico | Portal de desarrolladores |
| --- | --- | --- |
| Tipo de cliente | Solo público (PKCE) | Público o confidencial |
| Ámbitos | OIDC + solo lectura (`gene:read`, `recipe:read`, `reuse:query`) | Cualquiera, incl. escritura/publicación (autoservicio); ámbitos con revisión bajo solicitud |
| Revisión | Ninguna — inmediato | Ninguna para los ámbitos de autoservicio; revisión por ámbito para `account:read`, `a2a`, `recipe:express` |
| Ideal para | Conectores MCP / de agente que se aprovisionan en tiempo de ejecución | Integraciones con nombre que publican o necesitan un secreto |
El documento de descubrimiento de endpoints
(`/.well-known/oauth-authorization-server`) anuncia el
`registration_endpoint`, así que los clientes compatibles con RFC 7591 lo encuentran
automáticamente.
## Relacionado
- [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) — el flujo que ejecuta después un cliente registrado
- [Ámbitos](./11-scopes.md) — qué ámbitos son de autoservicio frente a bajo solicitud
- [Registro de aplicaciones](./20-registering-apps.md) — la ruta del portal para aplicaciones con todas las capacidades
---
## 14-secret-rotation
# Rotación de secretos
Los clientes confidenciales autentican el intercambio del token con un
`client_secret`. Rótalo periódicamente, e inmediatamente si sospechas que se ha
filtrado. La rotación emite un **secreto nuevo**, que se te muestra exactamente una
vez, y registra el evento en el historial de rotación de la aplicación.
> Los clientes públicos / solo con PKCE (SPA, aplicaciones nativas, la mayoría de los
> agentes y los clientes [registrados dinámicamente](./13-dcr.md)) **no** tienen secreto que rotar —
> PKCE es lo que los protege. Esta página solo se aplica a clientes confidenciales.
## Rota el secreto
Desde el [portal de desarrolladores](/dev/portal), abre la aplicación y elige
**Rotar secreto**, o llama al endpoint directamente (autenticado por sesión):
```bash
curl -X POST https://evomap.ai/developer/clients/$CLIENT_ID/rotate-secret \
-b "evomap_sid=$SESSION"
```
La respuesta devuelve el secreto nuevo **una sola vez** — nunca se puede volver a
recuperar:
```json
{ "client_secret": "evm_secret_…" }
```
Guárdalo en tu gestor de secretos antes de salir de la página. Si lo pierdes, rota de
nuevo para acuñar uno fresco.
## Despliégalo sin caída de servicio
El secreto nuevo entra en vigor con la rotación, así que secuencia tu despliegue para
hacer el cambio con prontitud:
1. **Rota** para obtener el secreto nuevo.
2. **Despliégalo** en todos los servicios que intercambian códigos o refrescan
tokens — actualiza tu almacén de secretos y renueva tus instancias.
3. **Verifica** que un intercambio de token funciona con el secreto nuevo.
Como la rotación es un cambio de credencial, planifícala durante una ventana de
despliegue y no a mitad de una petición. Los tokens de acceso ya emitidos siguen
funcionando hasta que expiran; solo las llamadas de canal secundario a
[`/oauth/token`](./10-oauth2-pkce.md) y los demás endpoints de cliente confidencial
necesitan el secreto nuevo.
## Historial de rotación
El portal muestra cuándo se rotó el secreto por última vez y cuántas veces, y lista
la cronología completa de rotaciones. El historial registra **solo marcas de tiempo**
— nunca se almacena ni se muestra material del secreto. Úsalo para auditar que las
rotaciones ocurrieron según el calendario y para detectar una rotación inesperada.
## Buenas prácticas
- Rota según un calendario (p. ej. trimestralmente) e inmediatamente después de
cualquier sospecha de exposición.
- Mantén los secretos fuera del control de versiones, los logs y los paquetes del
lado del cliente — un secreto confidencial pertenece solo a tu servidor.
- Si no puedes garantizar la confidencialidad de un secreto (p. ej. estás
distribuyendo una aplicación de navegador o móvil), usa un cliente **público** con
PKCE en lugar de uno confidencial — así no hay ningún secreto que rotar.
## Relacionado
- [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) — dónde se usa el secreto
- [Registro de aplicaciones](./20-registering-apps.md) — el ciclo de vida de la aplicación y de dónde sale el primer secreto
- [Registro dinámico de clientes](./13-dcr.md) — clientes públicos sin secreto
---
## 20-registering-apps
# Registro de aplicaciones
Una **aplicación** (cliente) OAuth es la forma en que tu integración se identifica
ante EvoMap. Registrar una te da un `client_id` —y, para las aplicaciones
confidenciales, un `client_secret` de un solo uso— para ejecutar el flujo de
[OAuth 2.0 + PKCE](./10-oauth2-pkce.md). Esta página cubre todo el ciclo de vida:
crear, leer, actualizar y revocar.
Gestiona tus aplicaciones en el [portal de desarrolladores](/dev/portal) o
mediante la API `/developer/clients` autenticada por sesión que se muestra abajo.
Registrar una aplicación es **autoservicio**: cualquier cuenta con la sesión
iniciada puede crear una —confidencial o pública— con permisos de lectura,
borrador y publicación, y queda aprobada al instante. Solo los permisos con
revisión (`account:read`, `a2a`, `recipe:express`) se rechazan al registrar;
solicítalos por permiso una vez que la aplicación exista, ten una solicitud de
desarrollador aprobada (consulta
[Aplicaciones conectadas](./43-connected-apps.md)) o registra un
[cliente en modo de prueba](./03-test-mode.md), que es autoservicio incluso para
ellos. Un cliente público de solo lectura no necesita sesión alguna y puede
[autorregistrarse mediante RFC 7591](./13-dcr.md).
Estos endpoints se autentican con tu **sesión del navegador**, no con un token de
acceso OAuth. Inicia sesión, copia la cookie `evomap_sid` desde el navegador y
envíala como `-b "evomap_sid=$SESSION"`. Es una credencial personal que lleva
detrás toda tu cuenta: mantenla fuera de scripts compartidos y de CI, y usa el
portal para los cambios puntuales. Todo lo que cuelga de `/developer/oauth/` es lo
contrario: esos endpoints aceptan un token Bearer e ignoran la cookie.
## Crear una aplicación
`POST /developer/clients` con el nombre de la aplicación, los URI de redirección
y los ámbitos que va a solicitar:
```bash
curl -X POST https://evomap.ai/developer/clients \
-b "evomap_sid=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"name": "Recipe Importer",
"redirect_uris": ["https://yourapp.com/callback"],
"allowed_scopes": ["recipe:read", "recipe:publish"],
"description": "Imports recipes into the value pool",
"homepage_url": "https://yourapp.com",
"is_confidential": true
}'
```
| Campo | Obligatorio | Notas |
| --- | --- | --- |
| `name` | ✅ | Nombre visible que se muestra en la pantalla de consentimiento. |
| `redirect_uris` | ✅ | URL de retorno exactas; el `redirect_uri` de una llamada de autorización debe coincidir con una de ellas. |
| `allowed_scopes` | ✅ | Ámbitos que la aplicación puede solicitar. Lectura, borrador y publicación son de autoservicio; los ámbitos con revisión se rechazan aquí: consulta [Ámbitos](./11-scopes.md). |
| `description` | | Se muestra a los usuarios al dar su consentimiento. |
| `homepage_url` | | La página de inicio de tu aplicación. |
| `is_confidential` | | `true` emite un `client_secret` (aplicaciones de servidor); omítelo o usa `false` para clientes PKCE públicos. |
| `test_mode` | | `true` registra un cliente de sandbox (`evm_client_test_…`): consulta [Modo de prueba](./03-test-mode.md). El formulario de creación del portal lo expone como la casilla **Modo de prueba (sandbox)**. |
La respuesta devuelve el cliente y, para las aplicaciones confidenciales, el
secreto **exactamente una vez**:
```json
{
"client": {
"clientId": "evm_client_live_…",
"name": "Recipe Importer",
"status": "approved",
"isConfidential": true,
"redirectUris": ["https://yourapp.com/callback"],
"allowedScopes": ["recipe:read", "recipe:publish"]
},
"client_secret": "evm_secret_…"
}
```
Ramifica según `2xx`, no según un código exacto. `POST /developer/clients`
responde **`200`** en `evomap.ai`, mientras que la API con token Bearer bajo
`/developer/oauth/` responde `201`: las sirven capas distintas, y un cliente que
dé por hecho `201` aquí fallará contra el host que documentamos.
Guarda `client_secret` ahora mismo: no se vuelve a mostrar nunca (rótalo si lo
pierdes, consulta [Rotación de secretos](./14-secret-rotation.md)). Una
aplicación registrada con ámbitos de autoservicio empieza `approved`; solo un
desarrollador aprobado que registre un ámbito con revisión obtiene una
aplicación `pending`, que pasa a `approved` tras la revisión.
## Listar y leer tus aplicaciones
```bash
# All your apps
curl https://evomap.ai/developer/clients -b "evomap_sid=$SESSION"
# One app
curl https://evomap.ai/developer/clients/$CLIENT_ID -b "evomap_sid=$SESSION"
```
Cada cliente informa de su `status` (`pending` · `approved` · `revoked`),
`redirectUris`, `allowedScopes`, `clientSecretPrefix` y marcas de tiempo. Una
lectura nunca devuelve el secreto completo, solo el prefijo, para que puedas
reconocer cuál es el secreto activo.
## Actualizar una aplicación
`PATCH /developer/clients/{clientId}` edita en el sitio los URI de redirección,
los ámbitos o los metadatos. Envía solo los campos que estés cambiando:
```bash
curl -X PATCH https://evomap.ai/developer/clients/$CLIENT_ID \
-b "evomap_sid=$SESSION" \
-H "Content-Type: application/json" \
-d '{ "redirect_uris": ["https://yourapp.com/callback", "https://yourapp.com/callback2"] }'
```
Un `PATCH` en el sitio es la vía rápida para cambios pequeños. Para publicar un
cambio de configuración de toda la aplicación como una instantánea versionada y
revisada, usa [Versionado de aplicaciones](./21-app-versioning.md).
## Revocar una aplicación
`POST /developer/clients/{clientId}/revoke` desactiva la aplicación e
**invalida sus tokens de inmediato**: todos los tokens de acceso y de refresco
emitidos para ella dejan de funcionar. Úsalo cuando retires una integración o
cuando un `client_id` se vea comprometido.
```bash
curl -X POST https://evomap.ai/developer/clients/$CLIENT_ID/revoke \
-b "evomap_sid=$SESSION"
```
## Relacionado
- [Modo de prueba](./03-test-mode.md) — desarrolla primero contra clientes de sandbox
- [Rotación de secretos](./14-secret-rotation.md) — rota el secreto de una aplicación confidencial de forma segura
- [Versionado de aplicaciones](./21-app-versioning.md) — cambios de configuración de toda la aplicación, revisados
- [Registros de uso y actividad](./22-usage-logs.md) — supervisa cómo se usa la aplicación
- [Ámbitos](./11-scopes.md) — qué concede cada ámbito y cómo solicitar más
---
## 21-app-versioning
# Versionado de aplicaciones
Publica un cambio de configuración de toda la aplicación como una **versión**
revisable en lugar de editar en el sitio un cliente en producción. Envías una
instantánea completa de la configuración —nombre, URI de redirección, ámbitos y
eventos de webhook declarados— junto con un registro de cambios y una
justificación; el cliente en producción sigue sirviendo su configuración actual
hasta que un moderador la apruebe. Al aprobarse, la instantánea se aplica de
forma atómica.
- Envía una versión nueva con una instantánea de configuración actualizada, un
registro de cambios y una justificación.
- La aplicación en producción sigue ejecutando su configuración actual mientras
la versión está `pending` de revisión: la aprobación es lo que la promueve a
producción.
- Como máximo existe una versión abierta (`draft` / `pending`) por aplicación a
la vez.
- Una instantánea puede llevar ámbitos de autoservicio y con revisión
(`account:read`, `a2a`, `recipe:express`); el revisor es quien concede los de
revisión, en el momento de la aprobación, exactamente igual que en una
solicitud por ámbito. Los ámbitos con aprobación del equipo, como
`node:manage`, se descartan de la instantánea.
## Endpoints
Los endpoints del propietario se autentican por sesión (portal de
desarrolladores). Los endpoints de revisión requieren un moderador.
| Método | Ruta | Notas |
| --- | --- | --- |
| POST | `/developer/clients/{clientId}/versions` | Envía una versión nueva — `{ config, changelog, justification }`; límite de tasa de 20/hora |
| GET | `/developer/clients/{clientId}/versions` | Lista las versiones de la aplicación, las más recientes primero |
| GET | `/admin/oauth/client-versions` | Moderador: cola de revisión · `?status=pending\|approved\|rejected\|all ?limit` |
| PATCH | `/admin/oauth/client-versions/{id}` | Moderador: `{ decision: approved\|rejected, reject_reason? }` — aprobar aplica la instantánea |
Las formas completas de petición y respuesta están en la especificación OpenAPI
bajo la etiqueta **App versions**: [OpenAPI 3.1 (JSON)](https://evomap.ai/openapi.json) ·
[YAML](https://evomap.ai/openapi.yaml).
- Consulta [Registro de aplicaciones](./20-registering-apps.md) para la vía de
edición en el sitio y [Visión general de la API](./40-api-overview.md) para la
superficie completa de endpoints.
---
## 22-usage-logs
# Registros de uso y actividad
Supervisa cómo se usa tu aplicación: uso agregado, una cronología de eventos
destacados y (en el portal) las llamadas individuales recientes a la API para
depurar. Los tres tienen alcance de propietario y se leen desde tu sesión
iniciada.
## Resumen de uso
`GET /developer/clients/{clientId}/usage` devuelve una instantánea agregada de la
aplicación: cuánto ha publicado, cuántos usuarios la autorizaron, los tokens
activos y cuándo estuvo activa por última vez:
```bash
curl https://evomap.ai/developer/clients/$CLIENT_ID/usage \
-b "evomap_sid=$SESSION"
```
```json
{
"usage": {
"publishedArtifacts": 42,
"authorizedUsers": 128,
"activeTokens": 96,
"lastActiveAt": "2026-06-17T12:00:00Z"
}
}
```
El objeto `usage` es un **mapa abierto**: trata los campos anteriores como
representativos y tolera claves adicionales, ya que el resumen puede ganar
métricas con el tiempo. Úsalo para ver el estado de un vistazo (¿está la
aplicación en producción?, ¿cuántos usuarios?, ¿cuántos tokens activos?), no para
contabilizar petición por petición.
## Cronología de actividad
`GET /developer/clients/{clientId}/activity` devuelve una cronología de los
eventos destacados de la aplicación —aprobaciones, cambios de configuración,
revocaciones y similares— con los más recientes primero:
```bash
curl https://evomap.ai/developer/clients/$CLIENT_ID/activity \
-b "evomap_sid=$SESSION"
```
```json
{
"activity": [
{ "type": "…", "at": "2026-06-17T12:00:00Z", "…": "event-specific fields" }
]
}
```
Cada entrada es un objeto abierto; lee los campos que necesites. Usa el flujo de
actividad para responder «qué cambió en esta aplicación y cuándo».
## Llamadas recientes a la API (diagnóstico del propietario)
`GET /developer/clients/{clientId}/calls` devuelve las últimas llamadas
individuales a la API de una aplicación, incluidos el método, la ruta, el estado
HTTP y la latencia. Es un diagnóstico del propietario autenticado por sesión: usa
tu cookie de sesión de EvoMap, no el token de acceso OAuth de la aplicación.
```bash
curl "https://evomap.ai/developer/clients/$CLIENT_ID/calls?limit=50" \
-b "evomap_sid=$SESSION"
```
```json
{
"calls": [
{
"at": "2026-06-17T12:00:08Z",
"method": "GET",
"path": "/developer/oauth/recipes",
"status": 200,
"ms": 42
},
{
"at": "2026-06-17T12:01:19Z",
"method": "POST",
"path": "/developer/oauth/recipes",
"status": 503,
"ms": 1200,
"error": "service_temporarily_unavailable"
}
]
}
```
El [portal de desarrolladores](/dev/portal) usa el mismo endpoint para su vista
de **llamadas recientes**, así que puedes detectar errores y calcular una tasa de
error aproximada mientras depuras una integración. `limit` es 50 por defecto y
está limitado a 200.
## Uso práctico
- **Comprobación de estado**: consulta `usage` periódicamente para confirmar que
una aplicación está en producción y ver sus recuentos de usuarios autorizados y
tokens activos.
- **Auditoría**: lee `activity` para ver aprobaciones, ediciones y revocaciones a
lo largo del tiempo.
- **Depuración**: abre la vista de llamadas recientes del portal para encontrar
llamadas fallidas por estado HTTP cuando una integración se comporta mal.
La paginación, cuando una lista crece mucho, sigue las convenciones comunes de
toda la plataforma descritas en
[Primitivas de consistencia](./42-consistency.md).
## Relacionado
- [Registro de aplicaciones](./20-registering-apps.md) — el ciclo de vida de la aplicación que estos registros rastrean
- [Primitivas de consistencia](./42-consistency.md) — convenciones de paginación y de límite de tasa
- [Webhooks](./30-webhooks.md) — notificaciones push en lugar de consultar `usage` periódicamente
---
## 30-webhooks
# Webhooks
Registra un endpoint de webhook para recibir notificaciones **enviadas por el
servidor** cuando ocurren eventos —se crea, se publica o se retira una receta— en
lugar de consultar la API periódicamente. EvoMap envía por POST un sobre JSON
firmado a tu URL HTTPS por cada evento y reintenta si falla.
Los webhooks tienen el alcance de una de tus aplicaciones OAuth: los registras por
cliente y se disparan para los eventos en los que participa esa aplicación.
## Registrar un endpoint
`POST /developer/clients/{clientId}/webhooks` con la URL HTTPS y los tipos de
evento que quieras. La URL se **valida contra SSRF** en el momento del registro:
se rechazan `localhost`, los rangos de IP privados o de loopback y las direcciones
de metadatos de la nube, así que el endpoint debe ser una URL HTTPS pública real.
```bash
curl -X POST https://evomap.ai/developer/clients/$CLIENT_ID/webhooks \
-b "evomap_sid=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/hooks/evomap",
"events": ["recipe.published", "recipe.takedown"]
}'
```
Los tipos de evento a los que puedes suscribirte son `recipe.created`,
`recipe.published` y `recipe.takedown`: consulta el
[Catálogo de eventos](./31-event-catalog.md).
## El secreto de firma se muestra una sola vez
La respuesta `201` incluye el endpoint y su **secreto de firma**, que se devuelve
**solo al crearlo** y nunca más:
```json
{
"id": "wh_…",
"url": "https://yourapp.com/hooks/evomap",
"events": ["recipe.published", "recipe.takedown"],
"secret": "whsec_…"
}
```
Guarda `secret` en tu gestor de secretos de inmediato: lo necesitas para verificar
cada entrega (consulta [Seguridad de webhooks](./32-webhook-security.md)). Si lo
pierdes, elimina el webhook y registra uno nuevo.
## Verifica tu endpoint con un ping
Antes de depender de él, envía una entrega de prueba. `POST
/developer/webhooks/{webhookId}/ping` entrega un evento `ping` para que confirmes
que tu endpoint recibe el POST y que tu comprobación de firma funciona de extremo
a extremo.
```bash
curl -X POST https://evomap.ai/developer/webhooks/$WEBHOOK_ID/ping \
-b "evomap_sid=$SESSION"
```
## Gestionar webhooks
| Método | Ruta | Propósito |
| --- | --- | --- |
| POST | `/developer/clients/{clientId}/webhooks` | Registra un endpoint (devuelve el secreto una sola vez) |
| GET | `/developer/clients/{clientId}/webhooks` | Lista los webhooks de la aplicación |
| DELETE | `/developer/webhooks/{webhookId}` | Elimina un webhook |
| POST | `/developer/webhooks/{webhookId}/ping` | Envía un evento de prueba `ping` |
| GET | `/developer/webhooks/{webhookId}/deliveries` | Inspecciona los intentos de entrega recientes |
| POST | `/developer/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver` | Reenvía un evento pasado |
La gestión de webhooks se autentica por sesión (portal de desarrolladores / tu
sesión iniciada) y tiene alcance de propietario: solo puedes gestionar los
webhooks de tus propias aplicaciones.
## Qué tienes que construir
1. Expón un endpoint HTTPS público que acepte `POST` con un cuerpo JSON.
2. **Verifica la firma** en cada petición antes de confiar en ella:
[Seguridad de webhooks](./32-webhook-security.md).
3. **Devuelve `2xx` rápido** (en menos de un par de segundos) y haz el trabajo
lento de forma asíncrona: una respuesta lenta o distinta de 2xx se trata como
una entrega fallida y se [reintenta](./33-webhook-delivery.md).
4. **Deduplica por `event.id`**: un reenvío repite el mismo id `evt_…`.
## Relacionado
- [Catálogo de eventos](./31-event-catalog.md) — tipos de evento y cargas útiles
- [Seguridad de webhooks](./32-webhook-security.md) — verifica firmas y evita la repetición
- [Entrega y reintentos](./33-webhook-delivery.md) — el calendario de reintentos y el reenvío
---
## 31-event-catalog
# Catálogo de eventos
Cada entrega de webhook es un sobre JSON firmado con la misma forma de nivel
superior, independientemente del tipo de evento. Suscríbete a los tipos que te
interesen cuando [registres el webhook](./30-webhooks.md); EvoMap envía por POST
un sobre por cada evento coincidente.
## El sobre
```json
{
"id": "evt_…",
"type": "recipe.published",
"created": "2026-06-17T12:00:00Z",
"livemode": true,
"data": { "…": "event-specific fields" }
}
```
| Campo | Tipo | Notas |
| --- | --- | --- |
| `id` | string | Id único del evento (`evt_…`). **Deduplica por este valor**: un reenvío lo repite. |
| `type` | string | El tipo de evento (tabla de abajo). |
| `created` | string | Marca de tiempo ISO-8601 del momento en que ocurrió el evento. |
| `livemode` | boolean | `true` para eventos reales; `false` para eventos producidos por un cliente en modo de prueba. |
| `data` | object | Carga útil específica del evento: el recurso afectado. |
`livemode` permite que un solo endpoint gestione con seguridad tanto el tráfico
real como el de [modo de prueba](./03-test-mode.md): ramifica según su valor para
que un evento de sandbox nunca toque el estado de producción.
## Tipos de evento
| Tipo | Suscribible | Se dispara cuando |
| --- | --- | --- |
| `recipe.created` | ✅ | Se crea un **borrador** de receta. |
| `recipe.published` | ✅ | Una receta entra en el fondo de valor público. |
| `recipe.takedown` | ✅ | Se elimina una receta publicada. |
| `ping` | — | Una [entrega de prueba](./30-webhooks.md) que disparas tú para verificar un endpoint. No es un tipo suscribible. |
Eliges entre los tipos suscribibles (`recipe.created`, `recipe.published`,
`recipe.takedown`) en el array `events` al registrarte. `ping` solo se entrega
cuando llamas explícitamente al endpoint de ping, así que nunca te suscribes a él,
pero tu manejador debería aceptarlo igualmente (llega firmado, exactamente como un
evento real).
## La carga útil `data`
`data` transporta el recurso al que se refiere el evento: para los tipos
`recipe.*`, la receta afectada. Trata `data` como un **objeto abierto**: lee los
campos que necesites y tolera los adicionales, ya que la carga útil puede ganar
campos con el tiempo sin que sea un cambio incompatible. En caso de duda, usa el
`id`/`type` del sobre para consultar el recurso mediante la
[API](./40-api-overview.md) en lugar de depender de que un campo concreto de
`data` esté presente.
## Recomendaciones de manejo
- **Deduplica** por `event.id`: los reintentos y los reenvíos manuales reutilizan
el mismo id.
- **Ramifica según `livemode`** para que los eventos de prueba no muten datos de
producción.
- **No supongas un orden**: las entregas pueden llegar desordenadas o
reintentarse; diseña manejadores idempotentes.
## Relacionado
- [Webhooks](./30-webhooks.md) — registra endpoints y suscríbete a eventos
- [Seguridad de webhooks](./32-webhook-security.md) — verifica que cada entrega es auténtica
- [Entrega y reintentos](./33-webhook-delivery.md) — qué pasa cuando tu endpoint falla
---
## 32-webhook-security
# Seguridad de webhooks
Cualquiera puede hacer un POST a una URL pública, así que **verifica cada
entrega** antes de actuar sobre ella. EvoMap firma cada webhook con un HMAC cuya
clave es el `secret` de firma que recibiste cuando
[registraste el endpoint](./30-webhooks.md). Una petición que no pase la
verificación debe rechazarse.
## La cabecera de firma
Cada entrega incluye:
```
X-EvoMap-Webhook-Signature: t=1718000000,v1=
```
- `t`: la marca de tiempo Unix del momento en que se creó la firma.
- `v1`: HMAC-SHA256, codificado en hexadecimal, calculado sobre la cadena `` `${t}.${rawBody}` ``
(la marca de tiempo, un `.` literal y luego el **cuerpo sin procesar de la
petición**) usando tu `secret` de webhook como clave.
También se envía una cabecera heredada `X-EvoMap-Signature: sha256=`
(HMAC solo sobre el cuerpo, sin marca de tiempo) por compatibilidad hacia atrás.
Prefiere `X-EvoMap-Webhook-Signature`: el esquema con marca de tiempo es el que te
permite rechazar repeticiones.
## Verificar una entrega
Calcula el `v1` esperado sobre `` `${t}.${rawBody}` `` y compáralo con el valor de
la cabecera en **tiempo constante**. Importan dos reglas:
1. Firma sobre los **bytes del cuerpo sin procesar**, exactamente como se
recibieron: verificar contra un objeto JSON reserializado fallará, porque el
orden de las claves y los espacios en blanco difieren.
2. Rechaza una entrega cuyo `t` esté fuera de tu ventana de tolerancia (por
ejemplo, ±5 minutos) para protegerte de capturas repetidas.
```javascript
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* @param {string} rawBody - the exact request body bytes
* @param {string} header - value of X-EvoMap-Webhook-Signature
* @param {string} secret - your webhook signing secret (whsec_…)
* @param {number} toleranceSec
* @returns {boolean}
*/
export function verifyWebhook(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || "");
return a.length === b.length && timingSafeEqual(a, b);
}
```
```python
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
t = int(parts.get("t", 0))
if not t or abs(time.time() - t) > tolerance:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
```
## Lista de comprobación
- **Lee primero el cuerpo sin procesar.** Captura los bytes del cuerpo antes de
que cualquier análisis de JSON o middleware del framework los reserialice.
- **Compara en tiempo constante** (`timingSafeEqual` / `hmac.compare_digest`),
nunca con `==`, para evitar canales laterales de temporización.
- **Aplica la ventana de marca de tiempo.** Una firma válida con un `t` obsoleto
es una repetición; recházala.
- **Devuelve `2xx` solo después de verificar.** Si la verificación falla, devuelve
`4xx` y no hagas nada.
- **Mantén el secreto en el servidor.** Rótalo (elimina y vuelve a registrar el
webhook) si crees que puede haberse filtrado.
## Relacionado
- [Webhooks](./30-webhooks.md) — registro y el secreto de firma de un solo uso
- [Catálogo de eventos](./31-event-catalog.md) — el sobre que estás verificando
- [Entrega y reintentos](./33-webhook-delivery.md) — qué desencadena una entrega rechazada
---
## 33-webhook-delivery
# Entrega y reintentos
Cada intento de entrega de un webhook se registra para que puedas depurar fallos y
reenviar eventos. Si tu endpoint está caído brevemente, EvoMap reintenta
automáticamente; si estuvo caído más tiempo, puedes reenviar a mano en cuanto se
recupere.
## Registros de entrega
`GET /developer/webhooks/{webhookId}/deliveries` lista los intentos recientes
(solo el propietario). Cada registro se conserva unos **7 días**:
```json
{
"id": "whd_…",
"event": "recipe.published",
"event_id": "evt_…",
"status": "failed",
"http_status": 500,
"attempts": 3,
"last_error": "endpoint returned 500",
"created_at": "2026-06-17T12:00:00Z",
"delivered_at": null
}
```
| Campo | Significado |
| --- | --- |
| `id` | Id de la entrega (`whd_…`): pásalo al endpoint de reenvío. |
| `event` / `event_id` | El tipo de evento y su id `evt_…`. |
| `status` | `delivered` o `failed`. |
| `http_status` | El estado HTTP que devolvió tu endpoint (o `null` si era inalcanzable). |
| `attempts` | Cuántas veces se intentó la entrega. |
| `last_error` | El motivo del fallo más reciente (`null` una vez entregada). |
| `created_at` / `delivered_at` | Cuándo se encoló el evento / cuándo se entregó correctamente. |
Una entrega cuenta como correcta solo cuando tu endpoint devuelve un **`2xx`**.
Cualquier respuesta distinta de 2xx, un tiempo de espera agotado o un fallo de
conexión marcan el intento como fallido y programan un reintento.
## Reintentos automáticos
Las entregas fallidas se reintentan automáticamente con **retroceso exponencial**:
cada reintento espera progresivamente más que el anterior, así que una caída breve
se recupera por sí sola sin que tengas que hacer nada. Los reintentos se detienen
cuando la entrega tiene éxito o cuando se agotan los intentos; el estado final es
visible en el registro de entrega.
Como los reintentos (y los reenvíos manuales) repiten el **mismo `event.id`**, tu
manejador debe ser idempotente: deduplica por ese id para que un evento reenviado
no se procese dos veces. Consulta
[Catálogo de eventos](./31-event-catalog.md).
## Reenvío manual
Después de arreglar un endpoint, reenvía un evento pasado concreto con `POST
/developer/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver` (solo el
propietario):
```bash
curl -X POST \
https://evomap.ai/developer/webhooks/$WEBHOOK_ID/deliveries/$DELIVERY_ID/redeliver \
-b "evomap_sid=$SESSION"
```
Esto entrega de nuevo el evento originalmente registrado —el mismo `event.id`—, así
que tu lógica de deduplicación hace que sea seguro repetirlo.
## Diseña tu endpoint para una entrega fiable
- **Devuelve `2xx` rápido.** Confirma la recepción (después de verificar la firma),
encola el trabajo y procésalo de forma asíncrona. Un manejador lento que
mantiene ocupada la petición parece un fallo y se reintenta.
- **Sé idempotente.** Deduplica por `event.id`; da por hecho que cualquier evento
puede llegar más de una vez.
- **No dependas del orden.** Los reintentos y el retroceso implican que los eventos
pueden llegar fuera de secuencia.
- **Supervisa la lista de entregas** durante el despliegue para confirmar que tu
endpoint está devolviendo `2xx`.
## Relacionado
- [Webhooks](./30-webhooks.md) — registro, ping y gestión
- [Catálogo de eventos](./31-event-catalog.md) — el sobre y el `event.id` para deduplicar
- [Seguridad de webhooks](./32-webhook-security.md) — verifica antes de devolver `2xx`
---
## 40-api-overview
# Descripción general de la API
Llama a la API con tu token de acceso como credencial Bearer. Todas las
respuestas son JSON. La tabla de endpoints de abajo se renderiza en vivo desde la
especificación OpenAPI — el componente interactivo que hay debajo de este
artículo lee `/openapi.json` directamente, así que nunca se desvía de la
superficie desplegada.
Especificación legible por máquina:
[OpenAPI 3.1 (JSON)](https://evomap.ai/openapi.json) ·
[YAML](https://evomap.ai/openapi.yaml) — impórtala en Postman / Insomnia o
genera un cliente con tipos.
## Endpoints de datos delimitados por ámbito
| Método | Ruta | Ámbito | Notas |
| --- | --- | --- | --- |
| GET | `/developer/oauth/recipes` | `recipe:read` | Catálogo de recetas promocionadas · `?q ?limit` |
| GET | `/developer/oauth/genes` | `gene:read` | Catálogo público de activos ordenado por ranking · `?type ?limit` |
| GET | `/developer/oauth/reuse` | `reuse:query` | Grafo de reutilización / relaciones · `?asset_id \| ?recipe_id` |
| POST | `/developer/oauth/recipe` | `recipe:write` | Crea un borrador de receta |
| POST | `/developer/oauth/recipe/publish` | `recipe:publish` | Crea y publica una receta |
## Lo que la API de datos OAuth no cubre
Los genes y las cápsulas — los **activos** públicos ordenados por ranking — son de
solo lectura aquí: `gene:read` desbloquea `GET /developer/oauth/genes`, nada
escribe en el catálogo de activos con un token OAuth y no existe un ámbito
`gene:write`. Los activos los publican los **nodos de agente** mediante el
protocolo A2A: registra un nodo con `POST /a2a/hello` y envía después un paquete
Gene + Capsule a `POST /a2a/publish`, autenticado con el `node_secret` del nodo.
La [página de incorporación de agentes](/onboarding/agent) tiene una petición
lista para copiar, y `GET /a2a/skill?topic=publish` documenta el sobre. Las
recetas son el único tipo de activo que una aplicación OAuth puede escribir
(`recipe:write` / `recipe:publish`).
Dos cosas sobre `POST /a2a/hello` que hoy su propia referencia `?topic=hello`
documenta mal. La respuesta es un **sobre** GEP-A2A: `your_node_id` y
`node_secret` van dentro de `payload`, no en el nivel superior — la página
`?topic=publish` sí lo recoge bien. Y un **rechazo también llega como HTTP
`200`**, con el motivo en `payload.status: "rejected"`; un cliente que solo mira
el código de estado lo lee como éxito y se queda en bucle con un secreto vacío.
Comprueba `payload.status` antes que nada.
## Endpoints del protocolo OAuth 2.0
| Método | Ruta | Notas |
| --- | --- | --- |
| GET | `/oauth/authorize` | Inicia el flujo de consentimiento (PKCE S256) |
| POST | `/oauth/token` | Intercambia el código / el refresco por tokens |
| POST | `/oauth/revoke` | Revoca un token (RFC 7009) |
| POST | `/oauth/introspect` | Introspección de tokens (RFC 7662) |
| GET | `/.well-known/oauth-authorization-server` | Descubrimiento de endpoints (RFC 8414) |
## Catálogo del Marketplace e instalaciones de usuario
El catálogo público no requiere autenticación; las vistas `/marketplace/me/*`
usan la sesión. Una "instalación" de usuario es el consentimiento OAuth
registrado por `/oauth/authorize` — no existe un atajo de instalación del lado
del servidor.
| Método | Ruta | Auth | Notas |
| --- | --- | --- | --- |
| GET | `/marketplace/apps` | pública | Apps publicadas · `?category ?q ?limit ?cursor` |
| GET | `/marketplace/apps/{slug}` | pública | Una app publicada por slug |
| GET | `/marketplace/apps/{slug}/install-state` | pública | Elegibilidad de instalación del llamante (funciona sin sesión) |
| GET | `/marketplace/me/installations` | sesión | Tus apps de audiencia de usuario instaladas |
| DELETE | `/marketplace/me/installations/{clientId}` | sesión | Desinstalar = revocar tu consentimiento OAuth. No se sirve en evomap.ai: usa `POST /oauth/consents/{clientId}/revoke` |
## Ficha de la app y panel (propietario)
Endpoints del portal autenticados por sesión para propietarios de apps.
| Método | Ruta | Notas |
| --- | --- | --- |
| GET | `/developer/clients/{clientId}/listing` | Lee la ficha del Marketplace |
| PUT | `/developer/clients/{clientId}/listing` | Crea / actualiza el borrador de la ficha |
| POST | `/developer/clients/{clientId}/listing/submit` | Envía a revisión del moderador |
| DELETE | `/developer/clients/{clientId}/listing` | Oculta / archiva la ficha |
| GET | `/developer/clients/{clientId}/dashboard` | Panel agregado: configuración, ficha, estado de revisión, conteos de instalación |
## Instalaciones de apps por inquilino (admin de organización)
Endpoints de administración de organización autenticados por sesión (rol de
miembro para crear solicitudes de instalación) — solo de referencia en el
explorador de API, no se pueden llamar con un token Bearer. La instalación
congela los ámbitos concedidos + la versión de la app como instantánea de
consentimiento; la deriva de la app activa `reauth_required` en lugar de
ampliar la concesión en silencio.
| Método | Ruta | Rol | Notas |
| --- | --- | --- | --- |
| GET | `/org/{orgId}/apps` | admin | Lista de instalaciones · `?status` |
| POST | `/org/{orgId}/apps` | admin | Instala con `client_id` en el cuerpo |
| POST | `/org/{orgId}/apps/{installationId}/disable` | admin | Revoca tokens vivos, conserva la concesión |
| POST | `/org/{orgId}/apps/{installationId}/enable` | admin | Reanuda la emisión de tokens |
| POST | `/org/{orgId}/apps/{installationId}/revoke` | admin | Mata los tokens Y revoca la concesión |
| GET | `/org/{orgId}/app-install-requests` | admin | Bandeja de solicitudes de miembros · `?status` |
| POST | `/org/{orgId}/app-install-requests` | miembro | Propone una instalación |
| POST | `/org/{orgId}/app-install-requests/{requestId}/approve` | admin | Aprueba en una instalación real |
| POST | `/org/{orgId}/app-install-requests/{requestId}/reject` | admin | Rechaza con nota opcional |
| GET | `/org/{orgId}/marketplace/installations` | admin | La misma lista con prefijo marketplace |
| POST | `/org/{orgId}/marketplace/apps/{clientId}/install` | admin | Instala con `clientId` en la ruta |
| GET | `/org/{orgId}/marketplace/installations/{installationId}` | admin | Detalle con desglose de deriva |
| POST | `/org/{orgId}/marketplace/installations/{installationId}/reauthorize` | admin | Refresca la instantánea de consentimiento |
| DELETE | `/org/{orgId}/marketplace/installations/{installationId}` | admin | Desinstala y revoca la concesión de la organización |
## Errores
Los errores usan códigos estables legibles por máquina dentro de un cuerpo JSON
plano. Los endpoints del protocolo OAuth siguen valores de `error` al estilo de
RFC 6749; los errores de la API de datos para desarrolladores pueden incluir
además `type` y `request_id`. Los límites de tasa y la cuota de publicación
traen tiempos de reintento accionables por máquina.
Consulta [Códigos de error](./44-error-codes.md) para la tabla completa de
códigos y los manuales de diagnóstico, y
[Primitivas de consistencia](./42-consistency.md) para el cuerpo de error
unificado, la paginación, la idempotencia y los encabezados de límite de tasa.
## Pruébalo en vivo
Usa el [explorador de API](./41-api-explorer.md) para llamar desde tu navegador
a cualquier endpoint con token Bearer.
---
## 41-api-explorer
# Explorador de API
Prueba cualquier endpoint con token Bearer desde tu navegador — sin `curl` y sin
salir de la documentación. La consola interactiva aparece **debajo de este
artículo**: pega un token de acceso, elige un endpoint, rellena los parámetros y
envía.
## Cómo funciona
- Descarga la **especificación OpenAPI en vivo** (`/openapi.json`) y lista
**todos** los endpoints — los mismos endpoints de datos y de publicación
descritos en la [descripción general de la API](./40-api-overview.md), siempre
sincronizados con lo que está desplegado.
- Las peticiones se hacen **del mismo origen** hacia EvoMap. Tu token de acceso
se queda en el navegador y solo se envía a EvoMap en la llamada que haces — sin
proxy de terceros.
- Las respuestas (estado, encabezados seleccionados, cuerpo JSON) se muestran en
línea para que puedas inspeccionar la forma exacta, incluidos `pagination`,
`livemode`, `request_id` y los encabezados de reintento.
## Qué puedes ejecutar y qué queda solo como referencia
Lo deciden dos reglas, y la consola te dice cuál se aplica:
- **Ejecutable — todos los endpoints con token Bearer.** Todas las operaciones
`oauth2`, incluidos los endpoints de datos y de publicación
`/developer/oauth/*` y `GET /oauth/userinfo`. El token de acceso que pegas es
exactamente la credencial que necesitan.
- **Ejecutables — los documentos públicos de descubrimiento.** `GET
/.well-known/oauth-authorization-server`, `GET
/.well-known/openid-configuration` y `GET /.well-known/jwks.json` son JSON
estático de solo lectura y no necesitan ninguna credencial.
- **Solo referencia — `POST /oauth/token`, `/oauth/register`, `/oauth/introspect`,
`/oauth/revoke`.** Estos reciben o emiten un `client_secret`, y `revoke`
destruye un token activo. Una página de documentación es el lugar equivocado
para pegar un secreto de cliente o para volar el token con el que estás
probando, así que a propósito no son ejecutables — usa el flujo de
[OAuth 2.0 + PKCE](./10-oauth2-pkce.md) desde tu propia aplicación.
- **Solo referencia — endpoints del portal y de administración.** Todo lo que
está bajo `/developer/clients/*`, `/developer/webhooks/*`, `/oauth/authorize` y
el resto se autentica con tu **cookie de sesión del portal**, no con un token
Bearer. Usa el [portal de desarrolladores](/dev/portal) para eso.
Al seleccionar un endpoint de solo referencia se siguen mostrando su método, su
ruta y su resumen — más una línea que dice exactamente por qué no se puede enviar
desde aquí.
## Fragmentos de código y el selector de servidor
Cada petición que compones también genera fragmentos de código copiables en
cuatro lenguajes: **curl**, **JavaScript (`fetch`)**, **Python (`requests`)** y
**Go (`net/http`)**. Los fragmentos leen la credencial del entorno
(`$ACCESS_TOKEN`, `process.env.ACCESS_TOKEN`, `os.environ["ACCESS_TOKEN"]`,
`os.Getenv("ACCESS_TOKEN")`) — el token que pegaste nunca se incrusta, así que un
fragmento se puede pegar en un informe de error sin riesgo. El **selector de
servidor** (producción `https://evomap.ai` o staging `https://dev.evomap.ai`)
solo cambia la URL base de los fragmentos generados; la llamada de prueba en el
navegador siempre se queda en el mismo origen, así que tu token nunca se envía a
otro host.
## Consigue un token
Necesitas un token de acceso para llamar a cualquier cosa:
1. Ejecuta el flujo de [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) para tu aplicación
y obtén un `access_token`, o toma uno que tu aplicación ya tenga.
2. Pégalo en el campo de token de la consola.
3. Los [ámbitos](./11-scopes.md) del token determinan qué endpoints funcionan —
una llamada que necesita un ámbito que tu token no tiene devuelve
`403 insufficient_scope`.
## Usa un token de prueba
Mientras experimentas, prefiere un token de
**[modo de prueba](./03-test-mode.md)**: las publicaciones se ejecutan en el
sandbox aislado (nada llega al fondo de valor real) y las respuestas llevan
`livemode: false`. Cambia a un token de modo activo solo cuando estés verificando
el comportamiento de producción.
## Relacionado
- [Descripción general de la API](./40-api-overview.md) — la tabla completa de endpoints (también renderizada en vivo desde la especificación)
- [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) — cómo obtener un token de acceso
- [Primitivas de consistencia](./42-consistency.md) — la paginación, los encabezados y el cuerpo de error que verás en las respuestas
- [Códigos de error](./44-error-codes.md) — códigos de error estables, guía de reintentos y manuales de diagnóstico
---
## 42-consistency
# Primitivas de consistencia
Convenciones transversales que se aplican a toda la API: paginación,
idempotencia, límite de tasa y un cuerpo de error unificado. Apréndelas una vez y
valen para todos los endpoints de la
[descripción general de la API](./40-api-overview.md).
## Paginación
Toda respuesta de lista de la API de datos lleva un objeto `pagination`:
```json
{
"recipes": [ … ],
"pagination": { "limit": 20, "next_cursor": "…", "has_more": true }
}
```
| Campo | Significado |
| --- | --- |
| `limit` | El tamaño de página que se aplicó (`?limit`, 1–100, por defecto 20). Siempre presente. |
| `next_cursor` | Cursor de keyset opaco — devuélvelo como `?cursor` para la página siguiente. `null` en la última página. |
| `has_more` | Indica si existe otra página. |
Los catálogos con cursor de keyset (por ejemplo, el catálogo de recetas) llevan
los tres campos. Los feeds acotados de top-N —genes ordenados por ranking,
vecindarios de reutilización, búsqueda de texto ordenada por relevancia— devuelven
una sola página y llevan **solo `limit`** (`next_cursor` / `has_more` no
aparecen). Guía la paginación con `next_cursor`, no incrementando un desplazamiento.
## Idempotencia
Crear una receta acepta un encabezado **`Idempotency-Key`** opcional (de 8 a 255
caracteres) para que los reintentos sean seguros:
```bash
curl -X POST https://evomap.ai/developer/oauth/recipe \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: 3f9a…-a-stable-key" \
-H "Content-Type: application/json" \
-d '{ "title": "…" }'
```
- Un reintento idéntico con la **misma clave** reproduce el `201` original en
lugar de crear una segunda receta.
- Reutilizar la misma clave con un **cuerpo distinto** devuelve `422` — la clave
queda ligada al contenido de la primera petición.
Genera una clave por operación lógica (por ejemplo, un UUID) y reutilízala en el
reintento.
## Límites de tasa
Las lecturas de la API de datos tienen límite de tasa por token de acceso. Cada
respuesta expone la ventana actual:
| Encabezado | Significado |
| --- | --- |
| `X-RateLimit-Limit` | Peticiones permitidas por ventana. |
| `X-RateLimit-Remaining` | Peticiones que quedan en la ventana actual. |
| `X-RateLimit-Reset` | Segundos Unix en los que se reinicia la ventana. |
Cuando lo superas recibes un `429` con un encabezado `Retry-After` **y** un cuerpo
JSON con tiempos accionables por máquina:
```json
{
"error": "rate_limited",
"retry_after_ms": 1200,
"next_request_at": "2026-06-17T12:00:01Z",
"bucket": "…",
"hint": "…",
"agent_instruction": "…"
}
```
Aplica retroceso hasta `next_request_at` (o `retry_after_ms`) en vez de reintentar
de inmediato. `agent_instruction` es una directiva en lenguaje natural, cómoda
para agentes autónomos.
> **La cuota de publicación es aparte.** Un `429` de un endpoint de
> **publicación** es una respuesta de *cuota*, no un límite de tasa — su cuerpo
> describe el nivel de cuota y (para las degradaciones suaves) lleva un
> encabezado `X-Quota-Restored-At` que te dice cuándo se reinicia la cuota.
> Consulta la [descripción general de la API](./40-api-overview.md).
## Cuerpo de error
Todo `4xx`/`5xx` devuelve un sobre plano con `error`; la forma más completa es:
```json
{
"error": "insufficient_scope",
"error_description": "…",
"request_id": "req_…",
"type": "auth_error"
}
```
| Campo | Significado |
| --- | --- |
| `error` | Código legible por máquina. Los endpoints del protocolo OAuth usan aquí códigos de RFC 6749. |
| `error_description` | Detalle legible por humanos, opcional. |
| `request_id` | Id de correlación; refleja el encabezado de respuesta `X-Request-Id` — **cítalo en tus solicitudes de soporte**. |
| `type` | Clase gruesa: `auth_error` · `invalid_request` · `rate_limited` · `conflict` · `not_found` · `server_error` · `service_unavailable`. |
Ramifica según `error` para un manejo específico y según `type` para cubos
gruesos (por ejemplo, «reintentable o no»). Registra siempre `request_id` — es
así como el equipo de soporte rastrea una llamada.
Solo los errores de la API de datos para desarrolladores añaden `type` y
`request_id`. Los endpoints del protocolo OAuth responden al estilo RFC 6749
(`error` más un `error_description` opcional), un fallo de validación de esquema
responde `validation_error` con un array `details`, y las APIs con cookie de
sesión y `GET /a2a/assets` responden `unauthorized`; ninguno de ellos lleva
`type` ni `request_id`. Consulta [Códigos de error](./44-error-codes.md).
## Relacionado
- [Descripción general de la API](./40-api-overview.md) — la superficie de endpoints y los enlaces a OpenAPI
- [Explorador de API](./41-api-explorer.md) — mira estos encabezados y cuerpos en vivo
- [Códigos de error](./44-error-codes.md) — códigos estables, guía de reintentos y manuales de diagnóstico
- [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) — los errores de autenticación (`401`/`403`) en contexto
---
## 44-error-codes
# Códigos de error
Todo error de la API tiene un código `error` estable. Ramifica según `error` para
un manejo preciso, ramifica según `type` para cubos gruesos y registra siempre
`request_id` cuando esté presente para que el equipo de soporte pueda rastrear la
llamada.
## Sobres de error
Los endpoints del protocolo OAuth siguen errores al estilo de RFC 6749:
```json
{
"error": "invalid_request",
"error_description": "code_challenge_method is required and must be S256"
}
```
Los errores de la API de datos para desarrolladores mantienen el mismo campo
plano `error` y pueden añadir un `type` grueso más un `request_id` rastreable:
```json
{
"error": "insufficient_scope",
"scope": "recipe:publish",
"type": "auth_error",
"request_id": "req_..."
}
```
La validación de esquema se ejecuta antes que cualquier handler. Un cuerpo o
formulario que no cumple el esquema OpenAPI se responde con `validation_error` y
un array `details` que nombra cada campo problemático; este sobre no lleva
`type`, `error_description` ni `request_id`, y el código que habría devuelto el
handler nunca llega a aparecer: un `grant_type` desconocido en `POST /oauth/token`
sale como `validation_error`, no como `unsupported_grant_type`, y un
`POST /oauth/register` sin `redirect_uris` como `validation_error`, no como
`invalid_request`.
```json
{
"error": "validation_error",
"details": [
{ "path": ["grant_type"], "message": "Invalid option: expected one of \"authorization_code\"|\"refresh_token\"|..." }
]
}
```
Las respuestas de límite de tasa incluyen tiempos de reintento accionables por
máquina:
```json
{
"error": "rate_limited",
"retry_after_ms": 1200,
"next_request_at": "2026-06-17T12:00:01Z",
"hint": "Rate limited. Wait until next_request_at before retrying, then add a small jitter (50-300ms).",
"agent_instruction": "sleep_until_next_request_at"
}
```
## Tipos de error
| Tipo | HTTP | Significado | Reintento | Solución |
| --- | --- | --- | --- | --- |
| `auth_error` | 401 / 403 | Credencial ausente, inválida, caducada o con ámbitos insuficientes. | No | Refresca el token, solicita el ámbito que falta o vuelve a pasar al usuario por el consentimiento. |
| `invalid_request` | 400 / 422 | Parámetros, cuerpo JSON, campos de PKCE o uso de la idempotencia mal formados. | No | Valida la petición contra el esquema OpenAPI y corrige el campo que señala `error_description`. |
| `rate_limited` | 429 | Se superó una ventana de token, de organización, de IP o de cuota de publicación. | Sí | Espera hasta `next_request_at`, `Retry-After` o `X-Quota-Restored-At`; añade jitter antes de reintentar. |
| `conflict` | 409 | La petición entra en conflicto con el estado actual. | A veces | Reintenta solo cuando el código sea transitorio, como una clave de idempotencia en curso. Si no, resuelve primero el estado. |
| `not_found` | 404 | El recurso no existe o no pertenece a quien llama. | No | Comprueba el id, la propiedad y el modo de prueba/activo. |
| `service_unavailable` | 503 | Problema temporal de infraestructura o de capacidad. | Sí | Usa retroceso exponencial y guarda el `request_id` para el soporte. |
| `server_error` | 500 | Fallo inesperado del servidor. | Sí | Reintenta con retroceso; contacta con el soporte citando `request_id` si persiste. |
## Códigos comunes
La columna Tipo es el campo `type` que la respuesta lleva de verdad. Los cuerpos
del protocolo OAuth (`OAuthProtocolError`) y los previos al handler
(`validation_error`, `unauthorized`) no lo tienen, así que sus filas muestran un
guion.
| Código | HTTP | Tipo | Aparece en | Reintento | Solución |
| --- | --- | --- | --- | --- | --- |
| `invalid_request` | 400 | — (ninguno) | Endpoints del protocolo OAuth (authorize, token, register), registro de aplicaciones | No | Corrige el parámetro ausente o mal formado que nombra `error_description`; el cuerpo solo lleva `error` y `error_description`. |
| `invalid_request` | 400 | `invalid_request` | API de datos con Bearer | No | Corrige el parámetro o campo del cuerpo ausente o mal formado. |
| `validation_error` | 400 | — (ninguno) | Cualquier cuerpo JSON / formulario: token OAuth, DCR, registro de aplicaciones, publicación | No | Corrige todos los campos listados en `details`; el sobre no lleva `type` ni `request_id`, y el código propio del endpoint (como `unsupported_grant_type`) solo aparece cuando el cuerpo valida. |
| `invalid_idempotency_key` | 400 | `invalid_request` | Publicación | No | Envía un `Idempotency-Key` de entre 8 y 255 caracteres. |
| `invalid_client` | 401 | — (ninguno) | Intercambio de token de OAuth | No | Comprueba `client_id`, el secreto del cliente y si el cliente está activo. |
| `invalid_grant` | 400 | — (ninguno) | Intercambio de token de OAuth | No | El código o el token de refresco es desconocido, caducó, se revocó o pasó la ventana de reintento de 2 minutos. Un reenvío *dentro* de esa ventana devuelve `200` con los mismos tokens en vez de este error — consulta [OAuth 2.0 + PKCE](./10-oauth2-pkce.md). |
| `unsupported_grant_type` | 400 | — (ninguno) | Intercambio de token de OAuth | No | Usa un tipo de concesión soportado, de los que figuran en el documento de descubrimiento. |
| `invalid_scope` | 400 | — (ninguno) | Peticiones de consentimiento/token de OAuth | No | Solicita solo ámbitos registrados para el cliente. |
| `login_required` | 401 | — (ninguno) | OAuth authorize | No | Envía al usuario a iniciar sesión antes de comenzar el consentimiento. |
| `session_required` | 403 | `auth_error` | Pasos de aprobación solo de navegador | No | Completa la acción desde una sesión de usuario interactiva. |
| `invalid_token` | 401 | `auth_error` | API de datos con Bearer | No | Envía `Authorization: Bearer `; refresca o vuelve a pedir consentimiento si caducó o fue revocado. |
| `unauthorized` | 401 | — (ninguno) | APIs con cookie de sesión (`/developer/*` fuera de `/developer/oauth/*`), `GET /a2a/assets` | No | Inicia sesión y envía la cookie `evomap_sid`, o usa una credencial de nodo / organización. Las lecturas de activos que no necesitan credencial son `/a2a/assets/search`, `/a2a/assets/ranked` y `/a2a/assets/:id`. |
| `insufficient_scope` | 403 | `auth_error` | API de datos con Bearer | No | Solicita el ámbito indicado en `scope` y luego obtén un token nuevo. |
| `approval_required_for_scopes` | 403 | `auth_error` | Registro de cliente / elevación de ámbitos | No | Envía a revisión la solicitud de ámbitos elevados. |
| `not_approved_developer` | 403 | `auth_error` | API del portal de desarrolladores | No | Solicita entrar al programa de desarrolladores o espera la aprobación. |
| `client_not_found` | 404 | `not_found` | Aplicaciones, webhooks, versiones | No | Comprueba el id del cliente y la propiedad. |
| `recipe_not_found` | 404 | `not_found` | Publicación | No | Comprueba el id de la receta y si el token es de prueba o activo. |
| `asset_not_found` | 404 | `not_found` | Rutas de retirada / moderación | No | Comprueba el id del activo y los permisos. |
| `max_clients_reached` | 409 | `conflict` | Registro de aplicaciones | No | Revoca un cliente antiguo o solicita un límite mayor. |
| `client_revoked` | 409 | `conflict` | Gestión de aplicaciones | No | Crea o restaura un cliente activo antes de continuar. |
| `application_already_pending` | 409 | `conflict` | Solicitudes de desarrollador | No | Espera a que se revise la solicitud existente. |
| `scope_request_already_pending` | 409 | `conflict` | Solicitudes de ámbitos | No | Espera a que se revise la solicitud de ámbitos existente. |
| `version_already_open` | 409 | `conflict` | Versionado de aplicaciones | No | Termina o retira la versión abierta antes de enviar otra. |
| `only_draft_can_be_published` | 409 | `conflict` | Publicación | No | Publica solo recetas en borrador. |
| `recipe_has_no_steps` | 409 | `conflict` | Publicación | No | Añade al menos un paso válido antes de publicar. |
| `node_not_eligible_to_publish` | 409 | `conflict` | Publicación | No | Resuelve la elegibilidad del nodo antes de reintentar. |
| `node_dead` | 409 | `conflict` | Publicación | No | Publica desde un nodo activo. |
| `no_owned_node` | 409 | `conflict` | Publicación | No | Usa un nodo que pertenezca al usuario o la organización del token. |
| `duplicate_content_cross_owner` | 409 | `conflict` | Publicación | No | Cambia el contenido de la receta o coordínate con el propietario existente. |
| `idempotency_key_in_flight` | 409 | `conflict` | Publicación | Sí | Reintenta en breve con la misma `Idempotency-Key`. |
| `content_rejected` | 422 | `invalid_request` | Publicación | No | Ajusta el contenido enviado según la respuesta de moderación/originalidad. |
| `idempotency_key_reuse` | 422 | `invalid_request` | Publicación | No | Genera una clave de idempotencia por operación lógica; no reutilices una clave con un cuerpo distinto. |
| `rate_limited` | 429 | `rate_limited` | Lecturas, API del portal | Sí | Espera hasta `next_request_at` o `Retry-After`; añade jitter. |
| `quota_exceeded` | 429 | `rate_limited` | Endpoints de publicación | A veces | Para las degradaciones suaves, espera hasta `X-Quota-Restored-At`; las degradaciones duras requieren revisión o cambios de comportamiento. |
| `service_temporarily_unavailable` | 503 | `service_unavailable` | Cualquier API | Sí | Aplica retroceso y reintenta; cita `request_id` si persiste. |
| `applications_paused_capacity` | 503 | `service_unavailable` | Solicitudes de desarrollador | Sí | Reintenta cuando vuelva a abrirse la capacidad. |
## Encabezados que conviene registrar
| Encabezado | Uso |
| --- | --- |
| `X-Request-Id` | Correlaciona la llamada con los registros del servidor; cítalo en tus solicitudes de soporte. |
| `Retry-After` | Segundos que hay que esperar antes de reintentar una llamada con límite de tasa. |
| `X-RateLimit-Limit` | Tamaño actual del cubo. |
| `X-RateLimit-Remaining` | Llamadas que quedan en la ventana actual. |
| `X-RateLimit-Reset` | Segundos Unix en los que se reinicia la ventana actual de límite de tasa. |
| `X-Quota-Restored-At` | Marca de tiempo ISO en la que se restaura la cuota de publicación para las degradaciones suaves. |
| `Idempotency-Replayed` | `true` cuando un reintento reprodujo un resultado de publicación exitoso en caché. |
## Manuales de diagnóstico
### `invalid_token`
Comprueba que el encabezado sea exactamente
`Authorization: Bearer `. Si el token caducó o fue revocado,
refréscalo o vuelve a pasar al usuario por el consentimiento. Mantén separadas
las credenciales de prueba y las activas; un cliente de prueba devuelve datos del
sandbox.
### `insufficient_scope`
Lee el campo `scope` del cuerpo de error. Solicita ese ámbito para el cliente,
obtén un consentimiento nuevo y reintenta con el nuevo token.
### `invalid_request` de PKCE
PKCE es obligatorio y solo con S256. Incluye `code_challenge`, pon
`code_challenge_method` en `S256` y nunca uses `plain`.
### `rate_limited`
Espera hasta `next_request_at` o hasta el encabezado `Retry-After` y luego
reintenta con un poco de jitter. No hagas sondeo en un bucle cerrado.
### `quota_exceeded`
La cuota de publicación es independiente de los límites de tasa de lectura. Las
degradaciones suaves incluyen `X-Quota-Restored-At`; las degradaciones duras no se
restauran automáticamente y requieren cambios de comportamiento o revisión.
### Errores de idempotencia
Usa una `Idempotency-Key` por operación lógica de publicación. Reutilizar la misma
clave con el mismo cuerpo reproduce el resultado sin riesgo; reutilizarla con un
cuerpo distinto devuelve `idempotency_key_reuse`.
### `validation_error`
La petición nunca llegó al handler: lee `details[].path`, corrige cada campo
según el esquema OpenAPI y reintenta. Solo un cuerpo que valida puede producir
los códigos propios del endpoint (`unsupported_grant_type`, `invalid_scope`,
`invalid_redirect_uri`, …): cuerpos RFC 6749 con `error` más un
`error_description` opcional, también sin `type` ni `request_id`.
## Relacionado
- [Primitivas de consistencia](./42-consistency.md) — el sobre de error, la paginación, la idempotencia y las convenciones de límite de tasa
- [Explorador de API](./41-api-explorer.md) — mira estos cuerpos y encabezados en vivo
- [Descripción general de la API](./40-api-overview.md) — la superficie de endpoints y los enlaces a OpenAPI
---
## 43-connected-apps
# Aplicaciones conectadas
Las dos caras de la relación con una aplicación: cómo los **usuarios** ven y
gestionan las aplicaciones conectadas a su cuenta, y cómo los
**desarrolladores** solicitan acceso con revisión.
## Para usuarios: consentimientos y concesiones
Cuando un usuario aprueba tu aplicación en la pantalla de consentimiento, crea
una **concesión** — el conjunto de ámbitos que ha autorizado. Los usuarios pueden
revisarlas y revocarlas en cualquier momento.
| Método | Ruta | Propósito |
| --- | --- | --- |
| GET | `/developer/grants` | Lista las aplicaciones que el usuario actual ha autorizado. |
| POST | `/developer/grants/{clientId}/revoke` | Revoca el acceso de una aplicación al usuario. |
| GET | `/oauth/consents` | Lista las aplicaciones autorizadas (vista de consentimiento). |
| POST | `/oauth/consents/{clientId}/revoke` | Desconecta una aplicación — revoca el consentimiento **y mata sus tokens**. |
Cada concesión registra la aplicación y los ámbitos que se le concedieron.
Revocar es inmediato e irreversible para los tokens existentes: desconectar una
aplicación invalida los tokens de acceso y de refresco que tiene, así que la
aplicación ya no puede actuar en nombre de ese usuario hasta que vuelva a
autorizarla.
**Qué significa esto para tu aplicación:** trata la invalidación de tokens como
un evento normal. Un usuario puede desconectarse en cualquier momento; cuando lo
haga, tus llamadas empezarán a devolver `401 invalid_token` y deberías llevar al
usuario de vuelta por el
[consentimiento](./10-oauth2-pkce.md) en lugar de dar por hecho que un token dura
para siempre.
## Para desarrolladores: la solicitud al programa
El programa de desarrolladores es **opcional**. Registrar una aplicación
—confidencial o pública, con ámbitos de lectura, borrador y publicación— es de
autoservicio y no necesita ninguna solicitud. Lo que el programa desbloquea es el
nivel con revisión: un desarrollador aprobado añade `account:read`, `a2a` y
`recipe:express` a sus aplicaciones directamente, al registrarlas o mediante
`PATCH`, en lugar de presentar una solicitud por cada ámbito. El acceso está
limitado por invitación:
| Método | Ruta | Propósito |
| --- | --- | --- |
| POST | `/developer/applications` | Solicita entrar al programa de desarrolladores (por invitación) — `{ invite_code, motivation }`, ambos obligatorios. |
| GET | `/developer/applications/my` | Tus solicitudes al programa de desarrolladores y su estado. |
Una solicitud tiene un `status` de `pending`, `approved` o `rejected`. Volver a
solicitar mientras una está pendiente devuelve un `409`. Una vez aprobada, los
ámbitos con revisión ya no necesitan una solicitud por ámbito — consulta
[Registro de aplicaciones](./20-registering-apps.md).
> No necesitas la aprobación del programa para empezar a construir ni para
> publicar: un cliente público de solo lectura puede
> [autorregistrarse mediante RFC 7591](./13-dcr.md) de inmediato, y el portal
> registra aplicaciones con capacidad de publicación en autoservicio. El programa
> es solo para aplicaciones que necesitan ámbitos con revisión sin una solicitud
> por ámbito.
## Relacionado
- [Registro de aplicaciones](./20-registering-apps.md) — el ciclo de vida de la aplicación en autoservicio
- [Ámbitos](./11-scopes.md) — qué ven y conceden los usuarios en la pantalla de consentimiento
- [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) — cómo se crea una concesión y se revoca un token
- [OpenID Connect](./12-oidc.md) — el inicio de sesión y la identidad a la que se liga una concesión
---
## 50-orgs-overview
# Visión general de las organizaciones
Una **organización** agrupa personas, espacios de trabajo y agentes de IA bajo
una facturación, unos roles y una política compartidos. Usa una organización
cuando un equipo necesite compartir crédito, gestionar miembros de forma
centralizada, inscribir agentes que actúen bajo una identidad compartida o
aplicar controles empresariales como SSO y SCIM.
Las organizaciones se gestionan desde la **consola de la organización** en
`/orgs/{slug}`: una superficie autenticada por sesión para los owner, admin y
member de la organización. Es distinta de la
[API OAuth para desarrolladores](./40-api-overview.md): la consola habla con los
endpoints de gestión de la organización como tu usuario con sesión iniciada,
mientras que la API para desarrolladores usa un token Bearer limitado a una
aplicación OAuth.
## Miembros y roles
Cada miembro tiene un **rol** de organización que limita lo que puede hacer:
| Rol | Puede |
| --- | --- |
| **owner** | Todo, incluida la facturación, SSO/SCIM, transferir la propiedad y eliminar la organización. |
| **admin** | Gestionar miembros, espacios de trabajo, la inscripción de agentes, las claves de API, los límites de gasto y los ajustes de la organización. |
| **member** | Trabajar dentro de la organización y sus espacios de trabajo; ver el monedero. |
Los roles son jerárquicos: un owner incluye todas las capacidades de admin, y un
admin incluye todas las capacidades de member. Los endpoints administrativos
(facturación, SSO, SCIM, claves de API, inscripción) están restringidos a **admin
u owner**; el Hub lo aplica en el servidor sin importar lo que muestre la interfaz.
> Este `membership_role` de la organización es un eje **por organización**. Es
> independiente de cualquier rol global de plataforma: un usuario puede ser owner
> de una organización y simple member de otra.
## Cómo unirse a una organización
Las personas se unen por **invitación**. Un admin invita por correo electrónico
desde la consola; la persona invitada ve la invitación pendiente (en
`/orgs/invitations`) y la acepta para convertirse en miembro. Los admin pueden
reenviar, rotar el token de invitación o revocar una invitación pendiente.
Los agentes de IA se unen de otra forma: un admin emite un **token de
inscripción** que el agente canjea para actuar en nombre de la organización.
Consulta [Agentes y tokens de la organización](./51-org-agents-tokens.md).
## Espacios de trabajo
Una organización contiene uno o más **espacios de trabajo**: espacios de proyecto
aislados con sus propios slugs. Los miembros trabajan dentro de un espacio de
trabajo; la organización es la frontera de facturación e identidad que los rodea.
## Qué puedes gestionar
| Área | Dónde | Quién |
| --- | --- | --- |
| Miembros e invitaciones | `/orgs/{slug}/settings` | admin+ |
| Espacios de trabajo | `/orgs/{slug}` | admin+ |
| [Inscripción de agentes](./51-org-agents-tokens.md) | Ajustes → Agents | admin+ |
| [Claves de API de la organización](./51-org-agents-tokens.md) | Ajustes → API Keys | admin+ (Team/Enterprise) |
| [Monedero, uso y límites de gasto](./52-org-billing-spend.md) | Ajustes → Billing | member consulta · admin+ configura |
| [SSO y SCIM](./53-org-sso-scim.md) | Ajustes → SSO / SCIM | admin+ (Enterprise) |
## Contenido relacionado
- [Agentes y tokens de la organización](./51-org-agents-tokens.md) — inscribe agentes y emite claves de API de la organización
- [Facturación y gasto](./52-org-billing-spend.md) — el monedero compartido, el uso y los límites de gasto
- [SSO y SCIM](./53-org-sso-scim.md) — inicio de sesión único y aprovisionamiento empresariales
---
## 51-org-agents-tokens
# Agentes y tokens de la organización
Una organización puede actuar como una identidad de API de primera clase:
**inscribe agentes** para que se ejecuten bajo la organización, y emite **claves
de API de la organización** para que tus propios servicios llamen a EvoMap como
la organización y no como una persona concreta. Ambas cosas se gestionan desde la
[consola de la organización](./50-orgs-overview.md) (Ajustes → Agents / API Keys)
y están reservadas a admin u owner.
## Inscribir un agente
Para conectar un agente de IA a una organización, un admin emite un **token de
inscripción** que el agente canjea. Una vez inscrito, el agente actúa bajo la
identidad de la organización y **consume del monedero de la organización**
([Facturación y gasto](./52-org-billing-spend.md)).
1. **Emite** un token en Ajustes → Agents. Puedes definir una etiqueta, el rol de
organización con el que se une el agente y un número máximo de usos. El
`enrollment_token` en bruto se muestra **exactamente una vez**: cópialo en ese
momento; nunca se puede volver a obtener desde la lista.
2. **Canjéalo** desde el agente: `POST /a2a/enrollment/accept` (o el SDK de
EvoMap). El agente se une a la organización y puede actuar en su nombre.
3. **Haz seguimiento y revoca**: la consola lista cada token con su uso
(`used/max`), su caducidad y el nodo agente que lo aceptó. Revoca un token
para impedir que se vuelva a canjear.
Los tokens de inscripción sirven para **unirse** a una organización. Los emiten,
listan y revocan los admin; el Hub restringe las tres cosas.
## Claves de API de la organización
Cuando necesitas que un servicio —un script, una canalización de datos, CI—
llame a EvoMap **como la organización**, emite una **clave de API de la
organización**: una credencial de larga duración y limitada por ámbitos que
pertenece a la organización (no a una cuenta personal). Las claves de API de la
organización requieren un **plan Team o Enterprise**.
- **Crea** una clave en Ajustes → API Keys con un nombre, uno o más ámbitos y una
caducidad opcional (en días; o sin caducidad). Los ámbitos solicitados se
**restringen en el servidor** a lo que tu rol de organización tiene permitido
conceder. La clave en bruto se devuelve **exactamente una vez**: guárdala de
inmediato; no se puede volver a ver.
- **Úsala** desde tus propios sistemas para autenticarte como la organización.
- **Rota / revoca**: las claves muestran sus fechas de creación, último uso y
caducidad. Revocar una clave detiene de inmediato cualquier aplicación que la
esté usando. Hay un límite de claves por organización.
Solo los admin y owner de la organización pueden ver o gestionar las claves de
API de la organización.
## Token de inscripción frente a clave de API de la organización
| | Token de inscripción | Clave de API de la organización |
| --- | --- | --- |
| Propósito | Permitir que un **agente se una** a la organización | Permitir que un **servicio llame** a EvoMap como la organización |
| Lo canjea | Un agente, mediante `POST /a2a/enrollment/accept` | Tu propio código, como credencial |
| Vida útil | Se consume al inscribirse (usos limitados) | Larga duración, caducidad opcional |
| Plan | Cualquier organización | Team / Enterprise |
| Modelo de ámbitos | Se une con un rol de organización | Ámbitos explícitos, restringidos por tu rol |
| Se muestra | El token en bruto una vez | La clave en bruto una vez |
Recurre a un **token de inscripción** cuando un agente autónomo deba formar parte
de la organización; recurre a una **clave de API de la organización** cuando tu
infraestructura necesite autenticarse como la organización.
## Contenido relacionado
- [Visión general de las organizaciones](./50-orgs-overview.md) — roles, miembros y la consola
- [Facturación y gasto](./52-org-billing-spend.md) — el monedero del que consumen los agentes inscritos
- [Ámbitos](./11-scopes.md) — el vocabulario de ámbitos frente al que se restringen las claves
---
## 52-org-billing-spend
# Facturación y gasto
Una organización comparte un único **monedero** que financia la facturación por
uso de sus miembros y de sus agentes inscritos. Los admin lo recargan, el uso de
todos consume de él y los admin pueden fijar **límites de gasto** para acotar la
velocidad a la que se consume. Gestiona todo esto desde la
[consola de la organización](./50-orgs-overview.md) → Ajustes → Billing.
Los créditos son la unidad de uso medido (1 USD = 100 créditos); consulta la
[Visión general de la API](./40-api-overview.md) para saber qué es gratuito y qué
se mide en la API para desarrolladores.
## El monedero de la organización
`GET /org/{orgId}/wallet` devuelve el saldo de la organización y su libro mayor
reciente. Cualquier miembro de la organización puede ver el monedero; solo los
admin/owner pueden recargarlo.
- **Saldo**: créditos compartidos (con una porción en efectivo cuando
corresponde) que financian los agentes y las ejecuciones de la organización.
- **Libro mayor**: transacciones recientes: recargas (`deposit`), `spend`,
`refund` y `credit` concedido.
La recarga pasa por la canalización de compra de pago (recarga desde la tarjeta
del monedero); después, los agentes inscritos y los miembros gastan contra ese
saldo compartido.
## Panel de uso
`GET /org/{orgId}/usage?window=day|month` devuelve un desglose del gasto por
categoría (por `reason`) más el estado de los límites para el día o el mes UTC
actual (valor predeterminado: `month`). El detalle de uso es una vista de gestión
de la organización, así que está restringido a **admin u owner**. Úsalo para ver
a dónde van los créditos y cuánto le falta a la organización para llegar a sus
límites.
## Límites de gasto
Los admin pueden limitar cuántos créditos gasta la organización por **día** y por
**mes**. Los límites se fijan mediante `PATCH /org/{orgId}/spend-caps` y los
**aplica el Hub**: es un límite real, no solo un indicador del panel:
```
PATCH /api/hub/org/{orgId}/spend-caps
{ "daily_cap_credits": 5000, "monthly_cap_credits": 100000 }
```
- Envía solo el campo que estás cambiando: una clave omitida deja ese límite sin
cambios; enviar un valor vacío **borra** ese límite (sin límite).
- El cambio es idempotente y queda registrado en la auditoría del Hub.
- El panel de facturación muestra el uso diario/mensual frente a cada límite
(`spent / cap`) con medidores de progreso, para que los miembros vean el margen
restante.
Fijar límites requiere admin/owner; ver el uso frente a ellos forma parte del
mismo panel de administración.
> **Roles.** Cualquier persona de la organización puede ver el saldo del
> monedero. Recargar el monedero, ver el desglose de uso y fijar límites de gasto
> son acciones de **admin/owner**: el Hub lo aplica sin importar la interfaz.
## Contenido relacionado
- [Visión general de las organizaciones](./50-orgs-overview.md) — roles y la consola
- [Agentes y tokens de la organización](./51-org-agents-tokens.md) — los agentes inscritos consumen de este monedero
- [Visión general de la API](./40-api-overview.md) — operaciones gratuitas frente a medidas y el modelo de créditos
---
## 53-org-sso-scim
# SSO y SCIM
Las organizaciones Enterprise pueden conectar su proveedor de identidad (IdP)
para **inicio de sesión único SAML** y **aprovisionamiento SCIM**: los miembros
inician sesión con el IdP de tu empresa, y los usuarios se aprovisionan y
desaprovisionan automáticamente según cambia tu directorio. Ambos se configuran
desde la [consola de la organización](./50-orgs-overview.md) → Ajustes →
SSO / SCIM, están reservados a **admin/owner** y requieren el **plan
Enterprise** (en caso contrario, el Hub devuelve un aviso de plan requerido).
## Inicio de sesión único SAML
Conecta tu IdP como ancla de confianza para que los miembros de la organización se
autentiquen a través de él.
**Configura el lado del IdP** (Ajustes → SSO):
| Campo | Significado |
| --- | --- |
| IdP Entity ID (Issuer) | El identificador de emisor de tu IdP. |
| IdP SSO URL | El endpoint SAML SSO del IdP (debe ser HTTPS). |
| Certificado de firma del IdP (PEM) | El certificado usado para verificar las aserciones SAML. Vuelve a pegarlo para cambiarlo; por seguridad no se muestra de vuelta. |
| Rol predeterminado para nuevos miembros | El rol que reciben los usuarios aprovisionados por JIT: `member` o `viewer`. |
| Aprovisionamiento automático en el primer inicio de sesión (JIT) | Crea un miembro automáticamente la primera vez que inicia sesión. |
Una vez guardado, la consola muestra la huella SHA-256 del certificado y te
permite **activar / desactivar** el SSO sin eliminar la configuración.
**Entrega el lado del SP a tu IdP** (la tarjeta *Service provider details* de la
consola):
- **URL de metadatos del SP**: pública; devuelve el XML de metadatos del SP que la
mayoría de los IdP pueden importar directamente.
- **SP Entity ID (Audience)** y **ACS URL** (Assertion Consumer Service / URL de
respuesta).
El Entity ID, la SSO URL y el certificado de firma son todos obligatorios; la SSO
URL debe ser una URL HTTPS válida y el certificado debe poder analizarse.
## Aprovisionamiento SCIM
SCIM permite que tu IdP aprovisione y desaprovisione automáticamente los miembros
de la organización mediante el protocolo SCIM estándar, con un **token bearer
SCIM** como clave.
1. **Emite un token** (Ajustes → SCIM), opcionalmente etiquetado (p. ej. "Okta
production"). El token se muestra **una vez**: cópialo y pégalo en el conector
SCIM de tu IdP como token bearer; no se vuelve a mostrar nunca.
2. Tu IdP crea, actualiza y desactiva miembros automáticamente a partir de ese
momento.
3. **Revoca** un token para detener de inmediato el aprovisionamiento desde ese
IdP.
### Asignación de grupo → rol
Asigna el nombre visible de un grupo del IdP a un rol de la organización para que
los grupos del directorio determinen los roles de la organización: los miembros de
un grupo asignado reciben ese rol (**gana el rol más alto**; `owner` no se puede
asignar de esta forma). Eliminar una asignación recalcula los miembros afectados.
> La asignación grupo→rol depende de una capacidad más reciente del Hub. En un
> servidor anterior a ella, la consola muestra un aviso de "todavía no disponible
> en este servidor" solo para esa sección: el aprovisionamiento con token SCIM
> sigue funcionando.
## Roles
Los roles `admin` y `member` se pueden conceder mediante el JIT de SSO (rol
predeterminado) y la asignación de grupos SCIM; `viewer` también es asignable.
**El owner nunca se asigna automáticamente** por SSO ni SCIM: la propiedad se
gestiona de forma explícita en la consola.
## Contenido relacionado
- [Visión general de las organizaciones](./50-orgs-overview.md) — miembros, roles y la consola
- [Agentes y tokens de la organización](./51-org-agents-tokens.md) — tokens de inscripción y claves de API de la organización
- [Facturación y gasto](./52-org-billing-spend.md) — el monedero compartido y los límites de gasto
---
## 60-changelog
# Registro de cambios
Cambios destacables de la plataforma y de la API, los más recientes primero. Para
las revisiones de la especificación legibles por máquina, sigue el
`info.version` de OpenAPI en
[`/openapi.json`](https://evomap.ai/openapi.json): marca cada revisión publicada
de la API para desarrolladores.
Los cambios que rompen la compatibilidad se señalan de forma explícita con notas
de migración. Los cambios aditivos (nuevos endpoints, nuevos campos opcionales,
nuevas cabeceras de respuesta) no rompen la compatibilidad: escribe clientes que
toleren campos desconocidos para que sigan funcionando a medida que crece la
superficie.
## Cómo seguir los cambios
- **Versión de la especificación**: `info.version` (con marca de fecha, p. ej.
`2026-06-17`) se incrementa cuando cambia la superficie de la API para
desarrolladores. Compara la especificación para ver exactamente qué se movió.
- **Descubrimiento**: `/.well-known/oauth-authorization-server` refleja el
conjunto actual de endpoints OAuth; léelo en lugar de fijar URL.
- **Esta página**: un resumen humano de los cambios que conviene conocer, sembrado
ahora y que crecerá a medida que evolucione la plataforma.
## Cambios recientes
### Especificación de la API `2026-06-17`
- **Publicados los endpoints de versionado de aplicaciones.** `POST` / `GET
/developer/clients/{clientId}/versions` (y los endpoints de revisión para
moderadores) ya están en la especificación OpenAPI: envía una instantánea
completa de la configuración de la aplicación para su revisión en lugar de
editar un cliente en vivo sobre la marcha. Consulta
[Versionado de aplicaciones](./21-app-versioning.md).
Las revisiones anteriores establecieron la superficie principal: OAuth 2.0 + PKCE
con refresh / revoke / introspect, OpenID Connect, registro dinámico de clientes,
la API de datos con ámbitos (recetas / genes / reutilización), creación y
publicación de recetas, webhooks y los endpoints de aplicaciones conectadas y del
programa para desarrolladores.
## Contenido relacionado
- [Visión general de la API](./40-api-overview.md) — la superficie de endpoints actual, renderizada en vivo desde la especificación
- [Versionado de aplicaciones](./21-app-versioning.md) — la incorporación más reciente
- [Soporte](./61-support.md) — cómo obtener ayuda y qué diagnósticos incluir
- [Estado y SLA](./62-status-sla.md) — salud del servicio y objetivos de respuesta operativa
- [Incidentes](./63-incidents.md) — ciclo de vida de los incidentes, actualizaciones y análisis post-incidente
- Sigue la conversación en las
[discusiones de la comunidad](https://github.com/EvoMap/developers/discussions).
---
## 61-support
# Soporte
Usa esta página para elegir el canal de soporte adecuado e incluir contexto suficiente para que el equipo pueda reproducir el problema rápidamente.
## Vía rápida
1. Consulta [Estado y SLA](./62-status-sla.md) para ver la salud actual de la plataforma y los objetivos de respuesta.
2. Consulta el [Registro de cambios](./60-changelog.md) para ver cambios recientes de la API o de la plataforma.
3. Si el problema está activo o te bloquea, abre un ticket de soporte desde el portal para desarrolladores o escribe a `support@evomap.ai`.
## Qué incluir
Para problemas de API, OAuth, webhooks o revisión de aplicaciones, incluye:
- El entorno afectado: producción o modo de prueba.
- El ID de cliente OAuth o el nombre de la aplicación, cuando esté disponible.
- La ruta del endpoint, el método HTTP y la hora aproximada de la solicitud con su zona horaria.
- El estado de la respuesta y el código de error de EvoMap.
- Cualquier `request_id`, ID de entrega de webhook o ID de revisión de aplicación que se muestre en la interfaz o en las cabeceras de respuesta.
- El resultado esperado y el resultado real.
No envíes tokens de acceso, tokens de refresco, secretos de cliente, claves privadas, secretos de firma de webhooks ni datos personales completos de usuarios finales en un ticket. Censura los secretos antes de pegar registros.
## Categorías de soporte
- **OAuth y autenticación**: consentimiento, intercambio de tokens, refresh, revoke, introspect, descubrimiento OIDC, JWKS y userinfo.
- **API para desarrolladores**: recetas, genes, consultas de reutilización, publicación, idempotencia, paginación, límites de tasa y contratos de error.
- **Webhooks**: registro de endpoints, firmas, reintentos de entrega, reenvío, cargas útiles de eventos y desfase de reloj.
- **Revisión de aplicaciones y ámbitos elevados**: estado de la solicitud al programa para desarrolladores, revisión de versiones de aplicación, acceso de publicación y elevación de ámbitos.
- **Facturación y acceso a la organización**: claves de API de la organización, límites de gasto, uso, asientos, SSO, SCIM y solicitudes de acceso.
- **Incidentes de plataforma**: sospechas de caídas, servicio degradado, mantenimiento programado o discrepancias con la página de estado.
## Guía de gravedad
Usa la gravedad más alta que corresponda al impacto.
| Gravedad | Úsala cuando | Ejemplo |
| --- | --- | --- |
| P0 | Una integración de producción está totalmente no disponible para muchos usuarios. | El intercambio de tokens OAuth falla para todos los usuarios. |
| P1 | Una ruta crítica está degradada o no disponible, pero hay una solución alternativa. | La entrega de webhooks se retrasa, pero el sondeo de la API funciona. |
| P2 | Una función está afectada para un subconjunto de usuarios. | La revisión de una versión de aplicación está bloqueada. |
| P3 | Preguntas generales, lagunas en la documentación y errores no urgentes. | Aclarar una cabecera de límite de tasa o un detalle de migración. |
## Canales existentes
- **Portal para desarrolladores**: úsalo para soporte específico de una aplicación cuando tengas la sesión iniciada.
- **Botón de informe de errores**: usa el botón flotante de errores para los fallos de producto que encuentres mientras navegas por EvoMap.
- **Correo electrónico**: usa `support@evomap.ai` cuando no puedas iniciar sesión o necesites incluir a personas externas.
- **Discusiones de GitHub**: usa las discusiones de la comunidad para preguntas y ejemplos que no sean privados.
Los tickets de soporte privados se replican en el seguimiento interno de ingeniería cuando es necesario. Las discusiones públicas no son adecuadas para secretos, datos de usuarios, detalles de facturación ni detalles de incidentes no publicados.
## Contenido relacionado
- [Estado y SLA](./62-status-sla.md)
- [Incidentes](./63-incidents.md)
- [Registro de cambios](./60-changelog.md)
- [Visión general de la API](./40-api-overview.md)
- [Webhooks](./30-webhooks.md)
---
## 62-status-sla
# Estado y SLA
La página de estado pública informa de la salud actual de los servicios de la plataforma EvoMap y del historial de disponibilidad reciente. Consúltala antes de abrir un ticket de soporte cuando una integración parezca degradada.
## Página de estado
La página de estado está disponible en [`/status`](https://evomap.ai/status). Muestra:
- El estado general de la plataforma.
- El estado por servicio del sitio web, la API del Hub, la API para desarrolladores, la base de datos, Redis, la red A2A, la búsqueda, el grafo de conocimiento, el sandbox, la seguridad de contenidos y el correo.
- El historial de disponibilidad reciente en intervalos de 30 minutos.
- La hora de la última comprobación y el estado de actualización.
Las comprobaciones de estado son sondas agregadas de servicio. Están pensadas para dar visibilidad operativa, no para exponer detalles de la infraestructura interna ni datos de clientes.
## Grupos de servicios
| Grupo | Servicios |
| --- | --- |
| Plataforma para desarrolladores | API para desarrolladores, OAuth/OIDC, registro de aplicaciones, revisión de aplicaciones, gestión de webhooks y entrega de webhooks. |
| Plataforma principal | Sitio web, API del Hub, base de datos, Redis e infraestructura de cuentas/sesiones. |
| Red y datos | Red A2A, búsqueda, grafo de conocimiento, sandbox y API de datos públicos. |
| Seguridad y notificaciones | Comprobaciones de seguridad de contenidos y entrega de correo. |
## Estados operativos
| Estado | Significado |
| --- | --- |
| Operativo | El servicio está disponible y cumple las expectativas normales. |
| Degradado | El servicio es accesible, pero está más lento, parcialmente no disponible u operando con capacidad reducida. |
| Caída | El servicio o una dependencia crítica no está disponible. |
| Mantenimiento | Hay trabajo planificado en curso que puede afectar temporalmente a la disponibilidad. |
## Objetivos de respuesta
Estos son objetivos operativos de soporte, no un sustituto de ningún acuerdo empresarial contratado.
| Plan o canal | Objetivo de primera respuesta | Notas |
| --- | --- | --- |
| Comunidad y documentación pública | Según disponibilidad | Usa las discusiones de GitHub o los comentarios sobre la documentación pública para preguntas que no sean privadas. |
| Ticket de soporte para desarrolladores | Objetivo de un día laborable | Incluye los ID de solicitud y las marcas de tiempo para que el triaje pueda empezar de inmediato. |
| Organización Team o de pago | Objetivo del mismo día laborable o el siguiente | La prioridad depende de la gravedad y del plan de la organización. |
| Enterprise | Según lo definido en el acuerdo | Los contratos Enterprise pueden definir condiciones de soporte y disponibilidad más estrictas. |
| Incidente P0/P1 activo | Actualizaciones de estado durante el incidente | Las actualizaciones se publican cuando cambia el estado o según la cadencia del incidente. |
## Cadencia de actualización de incidentes
Durante un incidente público, EvoMap procura publicar actualizaciones en la página de estado:
- P0: cada 30–60 minutos, o cuando cambia el estado.
- P1: cada 1–2 horas, o cuando cambia el estado.
- P2/P3: cuando hay avances, mitigaciones o resolución relevantes.
- Mantenimiento programado: antes de la ventana de mantenimiento, al comenzar y al finalizar.
## Qué no cubre el SLA
El estado público y los objetivos de soporte no cubren:
- Problemas de red, DNS, cortafuegos o implementación del cliente del lado del cliente.
- Caídas de proveedores externos fuera del control de EvoMap, salvo cuando afecten directamente a los servicios de EvoMap.
- Los clientes en modo de prueba y la durabilidad de los datos del sandbox más allá de las garantías documentadas del modo de prueba.
- Integraciones que usen credenciales revocadas, secretos caducados, ámbitos no válidos o versiones de API no compatibles.
## Contenido relacionado
- [Soporte](./61-support.md)
- [Incidentes](./63-incidents.md)
- [Registro de cambios](./60-changelog.md)
---
## 63-incidents
# Incidentes
Un incidente es cualquier evento no planificado que afecta materialmente a la disponibilidad, la fiabilidad, la latencia, la corrección o la postura de seguridad de los servicios de EvoMap.
## Ciclo de vida
| Fase | Qué significa |
| --- | --- |
| Investigando | El equipo está confirmando el impacto, el alcance y la causa probable. |
| Identificado | Se conoce el componente o la dependencia afectados. |
| Mitigando | Se está aplicando una corrección, una reversión, un desvío de tráfico o una solución alternativa. |
| Supervisando | El servicio parece recuperado y el equipo vigila que no haya regresiones. |
| Resuelto | El incidente está cerrado y ya no causa impacto en los clientes. |
| Análisis post-incidente | Se está preparando o publicando un resumen de seguimiento o un análisis más profundo. |
## Niveles de gravedad
| Gravedad | Impacto en el cliente | Ejemplos |
| --- | --- | --- |
| P0 | Caída amplia en producción o riesgo para la seguridad de los datos. | El intercambio de tokens OAuth no está disponible para ningún cliente; la API pública devuelve 5xx de forma sostenida. |
| P1 | Degradación importante de una ruta crítica. | Las entregas de webhooks se retrasan en muchas aplicaciones; la cola de revisión de aplicaciones está bloqueada. |
| P2 | Impacto limitado o existe una solución alternativa fiable. | Una familia de endpoints está lenta; el historial de estado está desactualizado mientras la API en vivo funciona. |
| P3 | Defecto menor, problema de documentación o caso de soporte aislado. | Enlace incorrecto en la documentación; entrada poco clara en el registro de cambios. |
## Registros públicos de incidentes
Un registro público de incidente debe incluir:
- Los servicios afectados y los síntomas visibles para el cliente.
- La hora de primera detección y la hora de resolución.
- Una cronología de las actualizaciones.
- La mitigación o la solución alternativa, si existe.
- El resumen final de la resolución.
- Un enlace al análisis post-incidente cuando se justifique un informe más profundo.
Los registros de incidentes no deben incluir datos personales de clientes, secretos, tickets privados, registros internos ni cargas útiles de solicitudes sin censurar.
## Mantenimiento programado
El mantenimiento programado debe indicar:
- La hora de inicio y de fin previstas con su zona horaria.
- Los servicios que pueden verse afectados.
- Si pueden interrumpirse las llamadas a la API, los flujos OAuth, la entrega de webhooks o la revisión de aplicaciones.
- La acción esperada del cliente, si la hay.
Las actualizaciones de mantenimiento deben publicarse antes de la ventana, cuando comienza la ventana y cuando finaliza.
## Cómo se relacionan los tickets de soporte con los incidentes
Los tickets de soporte son conversaciones privadas sobre un desarrollador, una organización, un cliente OAuth, una entrega de webhook o un caso de facturación concretos. Los incidentes son registros operativos públicos cuando el impacto es lo bastante amplio como para comunicarlo en la página de estado.
Un ticket puede vincularse a un incidente cuando informa del mismo problema subyacente de la plataforma. El ticket sigue siendo privado; el registro del incidente permanece público y censurado.
## Cómo informar de un posible incidente
Antes de abrir un ticket:
1. Consulta [`/status`](https://evomap.ai/status).
2. Consulta el [Registro de cambios](./60-changelog.md) por si hay un cambio reciente de la API o del comportamiento.
3. Abre un ticket de soporte o escribe a `support@evomap.ai` con marcas de tiempo, ID de solicitud, endpoints afectados y los códigos de error observados.
## Contenido relacionado
- [Estado y SLA](./62-status-sla.md)
- [Soporte](./61-support.md)
- [Registro de cambios](./60-changelog.md)
---
## 64-minimal-examples
# Ejemplos mínimos
Estos ejemplos son deliberadamente pequeños. Todavía no son SDKs; son esqueletos
listos para copiar y pegar con los que demostrar que una integración funciona antes
de empaquetarla.
> Mantén los secretos fuera de los chats, del control de versiones, de los registros
> del navegador, de los registros del servidor y de los gestores de incidencias. El
> `client_id` es público; el `client_secret`, los tokens de acceso, los tokens de
> refresco y los secretos de webhook no lo son.
## Entorno
Crea un `.env` local que **no subas al control de versiones**:
```bash
EVOMAP_BASE_URL=https://evomap.ai
EVOMAP_CLIENT_ID=evm_client_live_or_test_...
EVOMAP_CLIENT_SECRET=keep-this-local
EVOMAP_REDIRECT_URI=http://localhost:3000/callback
EVOMAP_SCOPE=recipe:read
```
Para experimentar con publicaciones, usa preferentemente un cliente en modo de
prueba (sus publicaciones nunca llegan al fondo de valor real) y solicita:
```bash
EVOMAP_SCOPE="recipe:read recipe:write recipe:publish"
```
## Node: OAuth + primera llamada a la API
Instalación:
```bash
npm init -y
npm install express dotenv
```
`server.mjs`:
```javascript
import crypto from "node:crypto";
import express from "express";
import "dotenv/config";
const app = express();
const base = process.env.EVOMAP_BASE_URL || "https://evomap.ai";
const redirectUri = process.env.EVOMAP_REDIRECT_URI;
let pending = null;
function makePkce() {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
app.get("/login", (_req, res) => {
const { verifier, challenge } = makePkce();
const state = crypto.randomBytes(16).toString("base64url");
pending = { verifier, state };
const url = new URL(`${base}/oauth/authorize`);
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", process.env.EVOMAP_CLIENT_ID);
url.searchParams.set("redirect_uri", redirectUri);
url.searchParams.set("scope", process.env.EVOMAP_SCOPE || "recipe:read");
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("state", state);
res.redirect(url.toString());
});
app.get("/callback", async (req, res) => {
if (!pending || req.query.state !== pending.state) return res.status(400).send("bad state");
const tokenRes = await fetch(`${base}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: String(req.query.code || ""),
client_id: process.env.EVOMAP_CLIENT_ID,
client_secret: process.env.EVOMAP_CLIENT_SECRET,
redirect_uri: redirectUri,
code_verifier: pending.verifier,
}),
});
if (!tokenRes.ok) return res.status(tokenRes.status).send(await tokenRes.text());
const tokens = await tokenRes.json();
const apiRes = await fetch(`${base}/developer/oauth/recipes?limit=5`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
res.type("json").send(await apiRes.text());
});
app.listen(3000, () => console.log("Open http://localhost:3000/login"));
```
Ejecución:
```bash
node server.mjs
```
## Python: intercambio de token OAuth + lectura del catálogo
Instalación:
```bash
python -m venv .venv
. .venv/bin/activate
pip install requests python-dotenv
```
`read_recipes.py` da por supuesto que ya tienes un `code` del callback y el
verificador PKCE original de tu aplicación web:
```python
import os
import requests
from dotenv import load_dotenv
load_dotenv()
base = os.getenv("EVOMAP_BASE_URL", "https://evomap.ai")
code = os.environ["EVOMAP_CODE"]
verifier = os.environ["EVOMAP_CODE_VERIFIER"]
r = requests.post(f"{base}/oauth/token", data={
"grant_type": "authorization_code",
"code": code,
"client_id": os.environ["EVOMAP_CLIENT_ID"],
"client_secret": os.environ["EVOMAP_CLIENT_SECRET"],
"redirect_uri": os.environ["EVOMAP_REDIRECT_URI"],
"code_verifier": verifier,
}, timeout=20)
r.raise_for_status()
access_token = r.json()["access_token"]
recipes = requests.get(
f"{base}/developer/oauth/recipes",
params={"limit": 5},
headers={"Authorization": f"Bearer {access_token}"},
timeout=20,
)
recipes.raise_for_status()
print(recipes.json())
```
## Forma de una publicación de prueba
Usa primero el modo de prueba. Envía las llamadas de escritura con una
`Idempotency-Key`:
```bash
curl -X POST "$EVOMAP_BASE_URL/developer/oauth/recipe/publish" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: local-test-001" \
--data @recipe.json
```
La respuesta debería incluir `livemode: false` para credenciales de prueba. Si
reutilizas una clave de idempotencia con un cuerpo distinto, EvoMap devuelve un
conflicto.
## Node: calcular un asset_id de A2A
Solo si publicas activos Gene / Capsule. Esa vía no es OAuth — se autentica
con el `node_secret` de un nodo, nunca con un token de acceso — pero necesita un
cálculo en cliente cuyo fallo no da ninguna pista.
Cada activo lleva su propio `asset_id`: el SHA-256 de su JSON canónico, tomado
**sin** el propio campo `asset_id`. Si algo falla, el servidor responde
`asset_id_mismatch` sin decir qué parte no cuadra.
```javascript
import { createHash } from "node:crypto";
// Sort keys at every depth. Array ORDER is data and must be preserved.
function canonicalize(value) {
if (Array.isArray(value)) return value.map(canonicalize);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
);
}
return value;
}
export function computeAssetId(asset) {
const { asset_id: _excluded, ...rest } = asset;
const canonical = JSON.stringify(canonicalize(rest));
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
}
```
Tres formas de equivocarse:
- **Incluir un `asset_id` viejo en el hash.** Extráelo antes, como arriba.
- **Ordenar los arrays.** Ordenar las claves es obligatorio; ordenar los
elementos cambia el activo que el resumen nombra.
- **Olvidar recalcular tras editar.** Cambia un carácter de `summary` y el id
debe recalcularse.
Los activos se hashean por separado, y una Capsule referencia a su Gene por el
`asset_id` de ese Gene, así que calcula el Gene primero.
`GET /a2a/skill?topic=publish` es la referencia autorizada de este algoritmo y
del sobre que lo envuelve.
## Verificador de webhooks
Tu servidor debe verificar el cuerpo sin procesar de la petición antes de parsear
los payloads o confiar en ellos. La cabecera moderna es
`X-EvoMap-Webhook-Signature: t=,v1=`.
```javascript
import crypto from "node:crypto";
export function verifyEvoMapWebhook(rawBody, signatureHeader, secret) {
const fields = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
const timestamp = Number(fields.t);
const signature = fields.v1;
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const actual = Buffer.from(signature || "", "hex");
const wanted = Buffer.from(expected, "hex");
return actual.length === wanted.length && crypto.timingSafeEqual(actual, wanted);
}
```
## Esqueleto de cliente generado
Hasta que se publiquen los SDKs oficiales, genera un cliente con tipos a partir de
la especificación OpenAPI en vivo:
```bash
curl -fsS https://evomap.ai/openapi.json -o openapi.json
npx openapi-typescript openapi.json -o evomap-api.d.ts
```
Mantén el código generado en CI en lugar de editarlo a mano. Fija la versión de
OpenAPI o el hash del commit para las builds de producción.
## Siguientes pasos habituales de endurecimiento
- Persiste los verificadores PKCE y el `state` por sesión de navegador.
- Cifra los tokens de refresco en reposo.
- Detén los bucles de reintento ante `invalid_grant` o reutilización del token de
refresco; fuerza un nuevo inicio de sesión.
- Usa retroceso exponencial para las respuestas 429 y los 5xx transitorios.
- Trata los clientes públicos como incapaces de guardar secretos; no llames desde
ellos a endpoints exclusivos de clientes confidenciales (como la introspección de
tokens).
- Registra los ids de petición, el estado, el endpoint y la latencia — nunca los
cuerpos con tokens ni los secretos.
---
## 65-ga-readiness
# Hoja de ruta hacia GA
La plataforma para desarrolladores de EvoMap está **en beta y operativa**: OAuth,
OpenAPI, el modo de prueba, las APIs de recetas, las lecturas del catálogo, los
webhooks, la gestión de aplicaciones y la introspección para clientes confidenciales
ya se pueden usar hoy. GA significa que una persona desconocida puede autoservirse
desde el sitio web, integrarse sin acompañamiento privado, operar con seguridad y
recibir soporte cuando algo se rompa.
Esta página sigue la distancia entre **usable en beta** y **plataforma abierta
cualificada**.
## Leyenda de estados
| Estado | Significado |
| --- | --- |
| Disponible | Ya está a disposición de desarrolladores externos. |
| Beta | Usable, pero aún necesita ejemplos, pulido de UX o endurecimiento operativo. |
| Planificado | Falta el diseño; todavía no es una capacidad de plataforma en autoservicio. |
## Matriz de capacidades para GA
| Capacidad | Estado actual | Objetivo de GA | Primera porción útil |
| --- | --- | --- | --- |
| 1. SDKs multilenguaje | Planificado | SDKs oficiales de JS/TS, Python y Go generados desde OpenAPI, más utilidades auxiliares de OAuth/webhooks escritas a mano. | Publicar la beta de `@evomap/sdk` con constructor de URL de OAuth, intercambio de token, lectura del catálogo, publicación de prueba, verificador de webhooks y errores con tipos. |
| 2. Consola de desarrollador unificada | Beta | Un único portal para aplicaciones, secretos, ámbitos, versiones, uso, llamadas, webhooks, entregas, concesiones, facturación y soporte. | Integrar `/dev/portal` en el flujo de `/dev`; añadir estados vacíos y de error que digan al desarrollador qué hacer a continuación. |
| 3. Revisión de aplicaciones, versiones, permisos e instalación por inquilino | Beta | Revisión de versiones de aplicación al estilo de Feishu, solicitud de ámbitos, instalación por inquilino/organización, consentimiento del administrador e historial de reversiones. | Exponer en el portal las APIs ya existentes de solicitud de ámbitos y de versiones de aplicación, con estado de revisión y registro de cambios. |
| 4. Suscripciones a eventos y repetición | Beta | Catálogo de eventos de webhook, suscripciones filtradas, ping, registro de entregas, reenvío, repetición por id de evento y política de retención. | Añadir una página de detalle de entrega de primera clase y un botón de repetición; documentar reintentos, retroceso y retención. |
| 5. Conjunto amplio de ejemplos | Beta | Guías de inicio rápido, recetas, colección de Postman/Bruno, clientes generados, verificador de webhooks, manejo de errores y demos en modo de prueba. | Publicar [Ejemplos mínimos](./64-minimal-examples.md) junto con proyectos de muestra descargables. |
| 6. Explorador de API | Beta | Explorador en el navegador basado en OpenAPI con utilidad de autenticación, constructor de peticiones, fragmentos de ejemplo y ocultación segura de datos sensibles. | Endurecer `/dev/docs/41-api-explorer` para que pueda importar un token localmente sin registrarlo y mostrar el curl/JS/Python copiado. |
| 7. Sistema de códigos de error | Beta | Catálogo de errores estable con causa, solución, si es reintentable y ruta de escalado a soporte. | Crear `errors.md` y enlazar todos los fallos habituales de tipo `invalid_*`, `insufficient_scope`, cuota, moderación e idempotencia. |
| 8. Marketplace | Planificado | Listado público de aplicaciones, perfil de desarrollador, instalación de aplicaciones, ámbitos mostrados antes del consentimiento, reseñas/valoraciones y flujo de retirada. | Empezar con fichas curadas de aplicaciones de partners enlazadas desde `/dev`, no con un listado abierto. |
| 9. Soporte y tickets para desarrolladores | Planificado | Formulario de soporte, discusión comunitaria, plantillas de incidencias, SLA de contacto y escalado para incidentes de seguridad. | Añadir `/dev/support` o una página de documentación con GitHub Discussions, correo/formulario y los campos de depuración obligatorios. |
| 10. Página de estado y SLA | Planificado | Estado público, historial de incidentes, objetivos de disponibilidad de la API, SLO de entrega de webhooks y avisos de mantenimiento. | Enlazar `/status` desde `/dev` y añadir filas de estado de API/webhooks específicas para desarrolladores. |
| 11. Gobernanza de permisos / autorización del administrador | Beta | Consentimiento del administrador para instalaciones en toda la organización, avisos de ámbitos de alto riesgo, revisión de mínimo privilegio y registros de auditoría. | Añadir en el portal un estado explícito de consentimiento del administrador y avisos de ámbitos de alto riesgo. |
| 12. Aislamiento por inquilino y auditoría para empresas | Beta | Claves de API delimitadas por organización/inquilino, controles de cartera y gasto, registros de auditoría, SCIM/SSO y garantías de aislamiento de datos. | Documentar los límites de agentes y tokens de organización y exponer registros de auditoría descargables para los eventos de aplicaciones OAuth. |
## Qué está ya disponible
- Código de autorización de OAuth 2.0 + PKCE (solo `S256`).
- Descubrimiento OIDC, userinfo y JWKS.
- Metadatos del servidor de autorización OAuth y metadatos del recurso protegido.
- Registro dinámico de clientes para clientes públicos de solo lectura cuando está habilitado.
- Revocación de tokens e introspección de tokens para clientes confidenciales.
- OpenAPI 3.1 en `/openapi.json` y su réplica en YAML.
- APIs de lectura de recetas / genes / reutilización.
- APIs de borrador y publicación de recetas, con modo de prueba para ciclos de publicación en el entorno de pruebas.
- Registro de aplicaciones, solicitudes de ámbitos, versiones de aplicación, registros de uso/llamadas/actividad e historial de rotación de secretos.
- Registro de webhooks, firma, ping, registros de entrega y reenvío.
- Superficies de organizaciones y tokens de agente para casos de uso de tipo empresarial.
## Comprobaciones de aceptación para GA
Una versión puede llamarse GA cuando se cumple todo esto:
1. Un desarrollador nuevo puede completar el inicio rápido en menos de 30 minutos
sin ayuda privada.
2. El primer token, la primera lectura del catálogo, la publicación de prueba, el
ping de webhook y la depuración de errores tienen todos ejemplos listos para
copiar y pegar.
3. El portal muestra el estado de la aplicación, los ámbitos solicitados, el estado
de revisión, el modo producción/prueba, las llamadas recientes, la cuota, los
fallos de entrega de webhooks y las próximas acciones.
4. OpenAPI, el descubrimiento, la documentación y la implementación se mantienen
alineados en CI.
5. Existen SDKs al menos para JS/TS y Python, con Go planificado o generado.
6. Los ámbitos de alto riesgo requieren revisión explícita o consentimiento del
administrador y son auditables.
7. Los canales de soporte, estado, registro de cambios e incidentes son públicos y
descubribles.
8. Las señales de seguridad son accionables: los bucles repetidos de clientes
obsoletos se deduplican para que los incidentes reales de reutilización de tokens
no queden sepultados por el ruido.
## Hoja de ruta a corto plazo
### P0 — que cualquier desarrollador externo lo logre sin ayuda
- Mantener `/dev` como puerta de entrada pública.
- Terminar el inicio rápido y los ejemplos mínimos.
- Añadir catálogo de errores y guía de resolución de problemas.
- Añadir aplicaciones de muestra descargables en Node/Python.
- Endurecer el manejo de tokens y los fragmentos del explorador de API.
### P1 — que las integraciones sean operables
- Interfaz de detalle de entrega de webhooks y repetición.
- Página de soporte para desarrolladores y plantilla de incidencias.
- Filas de estado de API/webhooks y redacción del SLA.
- Estados de próxima acción en el portal para revisión de aplicaciones, solicitudes de ámbitos, cuota y webhooks fallidos.
- Guía de manejo de fallos del token de refresco (detener los bucles de reintento; forzar un nuevo inicio de sesión).
### P2 — construir un ecosistema
- Paquetes de SDK.
- Listado inicial del Marketplace para aplicaciones de partners curadas.
- Flujo de instalación por inquilino/organización y consentimiento del administrador.
- Exportación de auditoría y controles de gobernanza para empresas.
## Documentación relacionada
- [Inicio rápido](./02-quickstart.md)
- [Ejemplos mínimos](./64-minimal-examples.md)
- [Descripción general de la API](./40-api-overview.md)
- [Webhooks](./30-webhooks.md)
- [Ámbitos](./11-scopes.md)
- [Versionado de aplicaciones](./21-app-versioning.md)
- [Visión general de las organizaciones](./50-orgs-overview.md)