Desarrolladores
Apps para Wislab
Construye una app que viva fuera de Wislab y entre a una academia por la API, los webhooks y su panel. Pruébala gratis en tu propia academia y publícala cuando esté lista.
Qué es una app externa
Una app externa corre fuera de Wislab: tiene su propio repositorio, su propia base de datos y su propio deploy. Wislab nunca ejecuta su código.
Entra a una academia por tres caminos: la API v1, con el token de instalación que la academia le da; los webhooks firmados que Wislab le envía; y un iframe dentro del panel de la academia, en /admin/apps/<handle>.
Si la academia quiere, también puede mostrarse en su sitio público, con bloques saneados o páginas completas aisladas.
| Tipo | Qué es |
|---|---|
De Wislab (first_party) | Las apps que construye y registra Wislab. |
De una academia (custom) | La app propia de una academia. La registra su dueño o un admin en Admin > Apps > Desarrollar apps. |
| Del catálogo | Una app custom que Wislab revisó y publicó: aparece en Admin > Apps de todas las academias. |
Desarrolla gratis en tu academia
Cualquier dueño o admin de una academia puede registrar apps y probar la suya en su propia academia, con cualquier paquete, también el gratuito.
En tu academia puedes
- Instalar tu app.
- Abrirla dentro del panel.
- Entrar a ella desde fuera, con tu academia.
- Llamar a la API v1 con su token de instalación. Mientras desarrollas, el límite es más bajo: 30 peticiones por minuto por instalación, en vez de 120.
- Recibir sus webhooks y enviar un evento de prueba.
Necesitas un paquete con apps externas para
- Instalar apps de otros desarrolladores: las de Wislab, las del catálogo o las de un enlace de instalación.
- Pedirle a Wislab la revisión, para que otras academias puedan instalar tu app.
- Que tu app muestre algo en el sitio público de la academia: bloques o páginas completas.
Las apps no cambian lo que vende una academia
HTTP/1.1 429 Too Many Requests
Retry-After: 18
{ "error": "RATE_LIMITED", "message": "Rate limit exceeded." }Registrar la app
Registra tu app en Admin > Apps > Desarrollar apps (/admin/apps/develop). Pueden hacerlo el dueño de la academia o un admin.
El formulario pide estos campos:
| Campo | Qué es |
|---|---|
handle | El identificador de la app en las URLs. Minúsculas, números y guiones, de 2 a 41 caracteres, y empieza con letra o número. Es el aud de cada token de sesión y nunca cambia. No puede ser install, develop, new ni launch, ni el id de una app de Wislab. |
name | El nombre que ve la academia, hasta 80 caracteres. Ni el nombre ni el handle pueden hacerse pasar por otra marca. |
description | Para qué sirve la app, hasta 500 caracteres. Si pides datos personales, explica para qué los usas. |
appUrl | La página que el panel carga en el iframe. Siempre https y pública. http://localhost solo funciona en un Wislab local, nunca en producción. No puede ser un dominio de Wislab ni el de una academia. |
allowedOrigins | Orígenes extra que pueden enviarle mensajes al panel, solo esquema y host: https://app.example.com. En producción, siempre https. |
scopes | Los permisos de la API que usará la app. La academia los ve antes de instalar; los que exponen datos personales van marcados. |
webhookUrl, webhookEvents | Dónde recibir eventos y cuáles. Opcional. La URL debe ser https pública, nunca localhost: en desarrollo usa un túnel. Cada evento exige su scope. |
storefrontBlocks, proxyPath, storefrontOrigins | Lo que la app puede mostrar en el sitio público. Lo explica la sección Vitrina. |
Al registrarla ves una sola vez el secreto de la app y el enlace de instalación https://wislab.app/apps/install/<handle>?key=…. Guarda el secreto solo en tu servidor.
Puedes regenerar el secreto y el enlace desde la página de tu app. Un secreto nuevo deja sin sesión a la app hasta que lo uses; un enlace nuevo invalida el anterior, pero las instalaciones siguen.
# Shown once when you register the app (Admin > Apps > Develop apps)
WISLAB_APP_HANDLE=my-app
WISLAB_APP_SECRET=
# Per academy, shown once at install. Store one pair per academy.
WISLAB_INSTALLATION_TOKEN=wsk_app_...
WISLAB_WEBHOOK_SECRET=whsec_...
WISLAB_API_URL=https://wislab.app/api/v1Sesión dentro del panel
El panel carga tu appUrl en un iframe. Cuando tu página envía wislab:ready, el panel responde con wislab:session y un token de sesión.
El panel solo acepta mensajes del iframe de tu app y de los orígenes que la academia aceptó al instalarla.
| Mensaje | Dirección | Qué hace |
|---|---|---|
wislab:ready | App a panel | Avisa que tu página está lista. El panel responde con la sesión. |
wislab:getSessionToken | App a panel | Pide un token nuevo cuando el tuyo venció. |
wislab:resize { height } | App a panel | El alto de tu página en píxeles, para que el iframe se ajuste (entre 320 y 6000). |
wislab:toast { level, message } | App a panel | Muestra un aviso en el panel, con el nombre de tu app delante. level es success o error; el mensaje se corta a 200 caracteres. |
wislab:session { token, tenant, handle } | Panel a app | El token de sesión, el slug de la academia y tu handle. |
El token de sesión
Es un JWT HS256 firmado con el secreto de tu app, válido por 60 segundos. Lleva estos claims:
| Claim | Valor |
|---|---|
iss | El slug de la academia. |
aud | Tu handle. |
sub | El id de la persona en esa academia. |
role | Su rol en la academia: owner, admin… |
tenantId, tenantSlug | La academia. |
name, email | La persona, cuando la academia tiene esos datos. Si no, no vienen. |
Verifica siempre en tu servidor
Entrar con la academia, fuera del iframe
- Si tu app también se abre fuera del panel, su pantalla de entrada enlaza a
https://<academia>/admin/apps/launch/<handle>. - Wislab pide una sesión de dueño o admin (si no la hay, primero se inicia sesión y se vuelve) y revisa lo mismo que al abrir la app en el panel.
- Luego manda el navegador a tu
appUrlcon el token en el fragmento:#wislab_session=<jwt>. El fragmento no llega a ningún servidor, log ni Referer. - Canjéalo por tu propia sesión con el mismo intercambio que usas dentro del iframe.
// app/api/wislab/session/route.js
// Trades the 60-second session token for your own session.
import jwt from "jsonwebtoken";
export async function POST(req) {
const { token } = await req.json();
let claims;
try {
claims = jwt.verify(token, process.env.WISLAB_APP_SECRET, {
algorithms: ["HS256"],
audience: process.env.WISLAB_APP_HANDLE,
});
} catch {
return Response.json({ error: "UNAUTHORIZED" }, { status: 401 });
}
if (claims.iss !== claims.tenantSlug) {
return Response.json({ error: "UNAUTHORIZED" }, { status: 401 });
}
// claims.sub is the person; claims.role, claims.tenantId, claims.tenantSlug.
// Start your own session here, bound to claims.tenantSlug.
return Response.json({ data: { userId: claims.sub, role: claims.role } });
}// The embedded page. The admin frames it with referrerpolicy=origin.
const admin = new URL(document.referrer).origin;
window.addEventListener("message", async (event) => {
if (event.source !== window.parent || event.origin !== admin) return;
if (event.data?.type !== "wislab:session") return;
await fetch("/api/wislab/session", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token: event.data.token }),
});
});
window.parent.postMessage({ type: "wislab:ready" }, admin);
window.parent.postMessage({ type: "wislab:resize", height: document.body.scrollHeight }, admin);
window.parent.postMessage({ type: "wislab:toast", level: "success", message: "Saved" }, admin);// Your appUrl, opened from /admin/apps/launch/<handle>
const token = new URLSearchParams(location.hash.slice(1)).get("wislab_session");
if (token) {
// Drop the token from the address bar, then trade it on your server.
history.replaceState(null, "", location.pathname + location.search);
fetch("/api/wislab/session", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ token }),
});
}Deja que te enmarque solo la academia
Para el navegador, todas las academias en *.wislab.app son el mismo sitio. Una cookie que tu app pone dentro del panel de una academia también viaja cuando otra academia enmarca tu app. Con un comodín, esa otra academia podría mostrar tu app con la sesión de la primera y engañar a su admin para que haga clic (clickjacking).
- El panel carga tu
appUrlcon?wislab_academy=<origen de la academia>, por ejemplohttps://tuacademia.wislab.appo el dominio propio de la academia. - Si ese origen es una academia donde tu app está instalada, responde
Content-Security-Policy: frame-ancestors <ese origen exacto>. Si no,frame-ancestors 'none'. El origen de cada academia es elurlque devuelveGET /api/v1/academycon su token. - Nunca uses
X-Frame-Optionsni el comodínhttps://*.wislab.app. - Guarda la sesión embebida por carga de página: en memoria, renovada con
wislab:getSessionToken. Si necesitas una cookie, átala a la academia: dale un nombre por academia o guarda elissdel token, y recházala si no coincide con elwislab_academyde la página.
La revisión lo comprueba: carga tu página con el origen de una academia y falla si hay X-Frame-Options, si falta frame-ancestors o si usa un comodín.
// The page the admin embeds (your appUrl)
export async function GET(req) {
const academy = new URL(req.url).searchParams.get("wislab_academy");
// Exact match against the academy origins you stored at install
// (the `url` of GET /api/v1/academy for each installation token).
const allowed = academy !== null && (await installedAcademyOrigins()).has(academy);
return new Response(await renderEmbeddedPage(), {
headers: {
"content-type": "text/html; charset=utf-8",
"content-security-policy": `frame-ancestors ${allowed ? academy : "'none'"}`,
},
});
}El token de instalación
- Cuando una academia instala tu app recibe, una sola vez, un token
wsk_app_…. Hay uno por academia, y la API sabe a qué academia pertenece. Guárdalo por academia, por ejemplo con su slug. - Envíalo desde tu servidor en el header
Authorization: Bearer. - Solo sirve para los scopes que la academia concedió al instalar. Un scope que no tiene responde 403.
- El límite de peticiones es por instalación: 120 por minuto, o 30 mientras desarrollas en tu academia.
- Deja de funcionar tras 90 días sin uso. Instalar o rotar el token reinicia el plazo. La academia lo rota en Admin > Apps y el anterior deja de servir en el acto.
- Si Wislab suspende la app, todos sus tokens responden 403.
GET /api/v1/academyno necesita scope: devuelve la academia del token (id, nombre, slug, dominio, URL, idiomas, moneda, paquete, logo y color). Nunca personas.
Todos los endpoints, scopes, errores y límites están en la referencia de la API v1.
curl https://wislab.app/api/v1/academy \
-H "Authorization: Bearer $WISLAB_INSTALLATION_TOKEN"{
"error": "FORBIDDEN",
"message": "Installation lacks required scope: payments:read."
}Webhooks
Wislab envía un POST con un JSON { id, type, apiVersion, createdAt, data }. Hoy apiVersion es v1.
Lleva tres headers: webhook-id, webhook-timestamp (segundos Unix) y webhook-signature. La firma es v1,<base64 HMAC-SHA256> sobre {id}.{timestamp}.{body}, con la clave que sale de decodificar en base64 el secreto sin su prefijo whsec_. Es el formato de Svix, así que sus librerías también lo verifican.
Cada academia tiene su propio secreto whsec_…. Lo recibe una sola vez al instalar, junto con el token: guárdalo por academia.
Cómo recibirlos
- Verifica la firma sobre el cuerpo crudo, antes de parsearlo: volver a serializar el JSON cambia los bytes.
- Rechaza los timestamps a más de 5 minutos de tu reloj.
- Deduplica por
id: el mismo evento puede llegar dos veces. - Responde 2xx rápido (tienes 10 segundos) y haz el trabajo después. Una redirección cuenta como fallo.
- Si no respondes 2xx, Wislab reintenta a 1 min, 5 min, 30 min, 2 h, 8 h y 24 h, con un margen de 10 %: 7 intentos en total. Después descarta la entrega.
- Si Wislab suspende la app, las entregas paran y lo que estaba en cola se descarta: al reactivarla no llega lo acumulado.
Eventos y scopes
Cada evento exige el scope que te daría el mismo dato por la API. El registro rechaza un evento sin su scope, y el endpoint de cada academia solo recibe lo que cubren los scopes que concedió.
| Eventos | Scope |
|---|---|
user.created, user.deactivated | users:read |
lead.created | crm:read |
payment.succeeded | payments:read |
enrollment.created | enrollments:read |
course.completed, exam.passed | completions:read |
certification.issued, certification.verified, certification.revoked | certificates:read |
import { createHmac, timingSafeEqual } from "node:crypto";
// Throws unless the delivery is signed with this secret and is recent.
export function verifyWislabWebhook(secret, body, headers) {
const id = headers.get("webhook-id");
const timestamp = Number(headers.get("webhook-timestamp"));
const signature = headers.get("webhook-signature") ?? "";
if (!id || !Number.isInteger(timestamp)) throw new Error("missing headers");
if (Math.abs(Date.now() / 1000 - timestamp) > 300) throw new Error("stale");
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest();
// Several signatures may travel space-separated; any valid one is enough.
const valid = signature.split(" ").some((entry) => {
const [version, value] = entry.split(",");
if (version !== "v1" || !value) return false;
const given = Buffer.from(value, "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
});
if (!valid) throw new Error("bad signature");
const event = JSON.parse(body);
if (event.id !== id) throw new Error("id mismatch");
return event;
}
// app/api/wislab/webhooks/route.js
export async function POST(req) {
const body = await req.text(); // the raw body, before any JSON.parse
let event;
try {
// The webhook secret of the academy (one per installation).
event = verifyWislabWebhook(process.env.WISLAB_WEBHOOK_SECRET, body, req.headers);
} catch {
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
}
// Dedupe by event.id, queue the work and answer right away.
return Response.json({ received: true });
}Eventos de ciclo de vida
Toda app con URL de webhooks recibe estos eventos sin pedir scope ni suscribirse: Wislab crea un endpoint por instalación siempre que la app declare una URL de webhooks. No llevan datos personales.
Van firmados con el secreto de webhooks de esa instalación, como cualquier otro evento.
| Evento | Cuándo llega |
|---|---|
app.installed | Justo después de instalar. Va firmado con el secreto nuevo que la academia te entrega junto con el token: hasta que lo guardes, responde algo distinto de 2xx y los reintentos te lo entregan. |
app.uninstalled | La academia desinstaló la app. Es la última entrega a ese endpoint: se reintenta como las demás y el endpoint se borra cuando se entrega o se agotan los intentos. Deja de llamar a la API y borra lo que guardas de esa academia. |
app.scopes_changed | La academia reinstaló la app desde la pantalla de consentimiento y concedió scopes distintos a los que tenía. Trae también previousScopes. |
app.test | Lo envías tú desde Desarrollar apps con «Enviar evento de prueba». Trae { app, academy, message, occurredAt }, sin scopes. |
Qué trae data
app:{ handle }.academy:{ id, slug, name }.scopes: los scopes concedidos a la instalación después del cambio.occurredAt: cuándo pasó, en ISO 8601.
{
"id": "evt_8f2c41d09a7b3e6512c4d8f0",
"type": "app.uninstalled",
"apiVersion": "v1",
"createdAt": "2026-11-03T18:20:00.000Z",
"data": {
"app": {
"handle": "my-app"
},
"academy": {
"id": "cmh2x9k0a0001lc04ab12cd34",
"slug": "my-academy",
"name": "My Academy"
},
"scopes": [
"courses:read",
"enrollments:read"
],
"occurredAt": "2026-11-03T18:20:00.000Z"
}
}Vitrina: bloques y páginas
Una app instalada puede mostrarse en el sitio público de la academia, con dos reglas que no cambian: tu código nunca corre dentro de la página de la academia, y la academia decide dónde aparece lo tuyo, desde Diseño, como cualquier sección de su tema.
Solo los temas que aceptan bloques de apps los ofrecen. La academia necesita un paquete con apps externas, y sus visitantes solo ven lo tuyo cuando Wislab aprobó la app.
Bloques html
Wislab hace un GET de servidor a servidor a <tu origen><path>?tenant=&instance=&settings=, con los headers firmados. Tienes 2 segundos y 256 KB. El HTML se sanea con una lista cerrada: etiquetas de estructura y de texto, class como único estilo y URLs http(s); nada de script, style, iframe, form, estilos en línea ni manejadores de eventos. El resultado se guarda 5 minutos. Si no respondes a tiempo, el bloque no muestra nada esa vez.
Bloques iframe
El bloque es un <iframe> de <tu origen><path> con la misma query más wislab_tenant, wislab_instance, wislab_ts y wislab_sig, dentro de un sandbox. La firma está atada al path del bloque: un src copiado de una página pública abre ese bloque y nada más. Tu página debe dejar que la enmarque la academia que la colocó, con frame-ancestors y su origen exacto.
Páginas completas
- Con
proxyPath(por ejemplo/proxy), la academia sirve tus páginas enhttps://<academia>/apps/<handle>/<path>. Wislab reenvía la petición a<tu origen><proxyPath>/<path>con la misma query y los headers firmados. - Tu respuesta se devuelve como una página propia, con CSP
sandboxy origen opaco,frame-ancestors 'none'ynoindex. Sin scripts ni formularios, salvo que la revisión los haya permitido. 10 segundos y 2 MB en cada sentido; las redirecciones no se siguen. x-wislab-userlleva un seudónimo de quien tiene sesión: siempre el mismo para tu app, distinto en cada app, nunca el id real. Solo viaja si la petición salió de la propia academia; lo que dispara otro sitio llega anónimo. Mantén tus GET sin efectos secundarios.
Firma v2
- Cada petición va firmada con el secreto de tu app:
v2,<base64 HMAC-SHA256>sobre estos campos unidos por salto de línea:v2, tenant, instancia, usuario, timestamp, método, path, query y el SHA-256 del cuerpo (base64url). Los ids que no aplican van vacíos. - De servidor a servidor la firma viaja en los headers
x-wislab-timestamp,x-wislab-signature,x-wislab-tenantyx-wislab-instance(yx-wislab-useren las páginas completas). En un iframe viaja en la query. Rechaza las peticiones a más de 5 minutos.
Lo que la academia aceptó
Al instalar se congela lo que la academia aceptó: el origen de tu app, el path de las páginas completas y cada bloque con su forma y su path. Un bloque nuevo, uno que pasa de html a iframe o cambia de path, un proxy nuevo u otro origen no se sirven hasta que la academia los acepta en Admin > Apps.
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
// Verifies an html block request, an iframe block src or a full-page request.
export function verifyWislabStorefront(secret, url, headers, method = "GET", body = "") {
const u = new URL(url);
const fromHeaders = headers.get("x-wislab-signature");
let tenant, instance, user, ts, query, signature;
if (fromHeaders) {
tenant = headers.get("x-wislab-tenant");
instance = headers.get("x-wislab-instance") ?? "";
user = headers.get("x-wislab-user") ?? "";
ts = headers.get("x-wislab-timestamp");
query = u.search.slice(1);
signature = fromHeaders;
} else {
// An iframe src: the signature covers the query without wislab_sig.
signature = u.searchParams.get("wislab_sig");
tenant = u.searchParams.get("wislab_tenant");
instance = u.searchParams.get("wislab_instance") ?? "";
user = "";
ts = u.searchParams.get("wislab_ts");
u.searchParams.delete("wislab_sig");
query = u.searchParams.toString();
method = "GET";
body = "";
}
if (!signature || !tenant || !Number.isInteger(Number(ts))) throw new Error("missing signature");
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error("stale");
const bodySha256 = createHash("sha256").update(body).digest("base64url");
const message = ["v2", tenant, instance, user, ts, method.toUpperCase(), u.pathname, query, bodySha256].join("\n");
const expected = createHmac("sha256", secret).update(message).digest();
const valid = signature.split(" ").some((entry) => {
const [version, value] = entry.split(",");
if (version !== "v2" || !value) return false;
const given = Buffer.from(value, "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
});
if (!valid) throw new Error("bad signature");
return { tenantId: tenant, instanceId: instance || null, userId: user || null };
}Revisión y publicación
Tu app pasa por estos estados:
| Estado | Qué significa |
|---|---|
| Borrador (solo tu academia) | Funciona solo en tu academia y los visitantes no ven nada. Cuando esté lista, pides la revisión; para eso necesitas un paquete con apps externas. |
| En revisión | Wislab la está revisando. |
| Aprobada (enlace de instalación) | Otras academias pueden instalarla con tu enlace de instalación. |
| En el catálogo | La pediste de nuevo y Wislab la publicó: aparece en Admin > Apps de todas las academias. |
| Rechazada | Wislab no la aprobó. Ves la nota de quien la revisó con el motivo. |
| Suspendida | Wislab la detuvo: sus tokens responden 403, el panel no la abre y no salen webhooks. |
Si cambias en una app aprobada algo que las academias consienten (scopes, URLs, webhook, orígenes, bloques o páginas completas), vuelve a revisión. Las instalaciones que ya existen conservan lo que aceptaron.
Chequeos automáticos
- La página que se embebe responde 2xx en menos de 5 segundos, sin redirecciones.
- Deja que la enmarque la academia: sin
X-Frame-Optionsy con unframe-ancestorsque nombra su origen exacto, sin comodín. - Cada origen permitido es solo esquema y host.
- La URL de webhooks responde, y rechaza con un 4xx una entrega sin firma.
- Todos los scopes que pide existen.
- Si pide datos personales, la descripción dice para qué.
Lo que revisa una persona
- Quién la registró: la academia, su paquete y la persona.
- Que los permisos que pide tengan sentido para lo que hace, sobre todo los de datos personales.
- Que sus bloques y páginas no pidan contraseñas, tarjetas ni datos de inicio de sesión.
- Que sus enlaces y direcciones vayan a su propio dominio y no se hagan pasar por otra marca.
- Si sirve páginas completas, si de verdad necesitan formularios.
Nombres que no se aceptan
Plantilla inicial
Wislab tiene una plantilla, templates/wislab-app/, con archivos para copiar a un proyecto Next.js. Solo dependen de next, react y jsonwebtoken.
| Archivo | Qué hace |
|---|---|
lib/wislab-session.ts | Verifica el token de sesión de 60 segundos que te da el panel. |
lib/wislab-api.ts | Cliente tipado de la API v1 con tu token de instalación y paginación. |
lib/wislab-webhooks.ts | Verifica la firma de los webhooks, sin dependencias. |
lib/wislab-storefront.ts | Verifica la firma v2 de bloques y páginas completas, y lee los ajustes del bloque. |
components/WislabBridge.tsx | El puente postMessage: listo, sesión, alto y avisos. |
app/embedded/page.tsx | La página que embebe el panel (ejemplo). |
app/api/wislab/session/route.ts | Canjea el token de sesión por tu propia sesión. |
app/api/wislab/webhooks/route.ts | Recibe los eventos. |
app/blocks/[id]/route.ts | Sirve un bloque html (ejemplo). |
app/proxy/[[...path]]/route.ts | Sirve páginas completas (ejemplo). |
.env.example | Los secretos y de dónde sale cada uno. |
Antes de salir a producción
- La página que se embebe no manda
X-Frame-Optionsy respondeframe-ancestors <wislab_academy>solo para una academia donde está instalada ('none'si no). - La sesión embebida vive por carga de página, o su cookie está atada a la academia que la emitió.
- Los secretos viven solo en el servidor: el navegador nunca ve el secreto de la app, el token de instalación ni el secreto de webhooks.
- Verificas el token de sesión en cada intercambio y no lo guardas más allá de su
exp. - Tu receptor de webhooks es idempotente por
id. - Tus rutas de bloques y páginas verifican la firma en cada petición, y los bloques html responden en menos de 2 segundos.
- Tus GET de páginas completas no tienen efectos secundarios.
templates/wislab-app/
lib/wislab-session.ts
lib/wislab-api.ts
lib/wislab-webhooks.ts
lib/wislab-storefront.ts
components/WislabBridge.tsx
app/embedded/page.tsx
app/api/wislab/session/route.ts
app/api/wislab/webhooks/route.ts
app/blocks/[id]/route.ts
app/proxy/[[...path]]/route.ts
.env.example¿Construyes una app y algo no cuadra?
Cuéntanos el handle de tu app y qué ves, y lo revisamos contigo.