Capítulo pagos de la guía Neblla — productos, checkout Stripe, validación y consumo. El contrato base está en core.txt; el criterio de cuándo usar Neblla, en empezar.txt.

Compras

Integración con Stripe. El desarrollador no procesa pagos, no monta endpoints de Stripe, no gestiona los webhooks de Stripe. Si quiere enterarse de cada compra en su propio sistema (un n8n, un CRM), el webhook saliente purchase de Neblla se lo manda (ver core.txt). Tres tipos de producto: one_time, consumable y subscription (mensual o anual, con prueba gratis si quieres). Los tres son entitlements dentro de la app — un permiso o un contador atado a un usuario — y un producto único o un consumible puede además ser un fichero a la venta (una foto en alta resolución, un PDF, un pack de texturas: "Ficheros a la venta" más abajo), tener unidades a la venta (una edición de 10, "Unidades a la venta") o ser físico, enviado con sus gastos de envío y seguido como pedido ("Productos físicos y pedidos").

En el pre de la app las compras van, por defecto, por el modo de prueba de Stripe: nada se cobra y pro no las cuenta (ver publicar.txt, «Las compras en pre»).

API

neblla.shop.purchases es un objeto indexado por id de producto, no un array — {} si el usuario no tiene nada. Comprueba la propiedad con neblla.shop.purchases['pack_invierno'], nunca con .some(...).


neblla.shop.products();                  // listar lo que tienes en venta
neblla.shop.buy('skin_dorado');          // abre checkout de Stripe
neblla.shop.buy('monedas_pack', 100);    // consumible con cantidad
neblla.shop.consume('monedas_pack', 10); // consumir unidades
neblla.shop.buy('premium');              // suscripción: se renueva sola
neblla.shop.manage('premium');           // cancelar, cambiar la tarjeta, ver facturas
neblla.shop.validate(userId, 'vip_pass');  // verificar (también desde el servidor)

Firmas:

Evento purchase

Cuando la compra se confirma, el evento neblla.shop.on('purchase', cb) ({ userId, productId, type, purchases }) se dispara en los dispositivos del comprador, y solo en los suyos: lo que tiene cada usuario es suyo y no viaja a la sala. Útil para reaccionar en tiempo real (animación, desbloqueo del skin). Si otros tienen que ver algo que alguien compró (su skin, su insignia), que lo anuncie tu código con un mensaje y que room.js lo compruebe con neblla.shop.validate(userId, productId): solo el servidor de la sala puede mirar lo de otro usuario. Desde un cliente, validate mira lo del propio usuario, y por otro responde { valid: false, error: 'not_allowed' }.

Trampas. El SDK compara de vez en cuando las compras del dispositivo con las del servidor: si tiene menos (una pestaña vieja), se corrige y ya está; si dice tener más de lo que compró, se corrige, el usuario recibe una marca (la columna de marcas del backoffice) y lo ves en el panel, Personas › Moderación, con lo que decía tener y lo que tiene de verdad. Es un aviso: no se le hace nada más.

Si la venta se reembolsa entera desde Stripe, o el comprador abre una disputa y la pierdes (contracargo), la compra deja de contar en todas partes y, si el usuario está conectado, llega otro shop.on('purchase') con type: 'revoked' y los purchases ya actualizados: quita lo que desbloqueaba. Un reembolso parcial solo queda anotado; qué unidades cubre lo decides tú. Mientras una disputa está abierta, o si la ganas, el usuario conserva lo comprado.

Tus ventas, en el panel. Dinero › Ventas lista cada compra con lo pagado, su estado y quién la hizo. Desde una venta puedes reembolsarla entera (vuelve el dinero en Stripe, con la comisión de Neblla, y la compra se retira al momento como arriba; una suscripción no se reembolsa ahí: se reembolsa o se cancela en tu panel de Stripe, y su venta trae el enlace, «Abrir en Stripe», stripeUrl) y abrir el recibo o las facturas de Stripe de esa compra (los enlaces que da Stripe; Neblla no guarda PDFs). Las ventas (con el id, el email y el nombre del comprador) y tus usuarios se descargan en CSV. Por el MCP, neblla_sales; por HTTP, POST /api/backend/apps/<appId>/sales (y /sales/csv, /sales/<id>/refund, /sales/<id>/documents, /users/csv) con tu authToken.

Propiedad: neblla.shop.purchases (CLIENT) — las compras del usuario.

Suscripciones

Para un plan premium que se paga cada mes o cada año. El producto lleva type: 'subscription', el precio por periodo, interval: 'month' | 'year' y, si quieres, trialDays de prueba gratis. Se vende igual: neblla.shop.buy('premium').

Un plan por equipo (por ejemplo, 9 € al mes por equipo) no es otro tipo de producto: es la suscripción de quien paga más un grupo para su gente (ver identidad.txt, "Grupos y permisos"). Lo montas como quieras; un ejemplo, con tu n8n (o tu servidor) entre los webhooks y la API de administración:


// 1. Papeles y avisos. Los grupos los crea tu n8n, no el cliente.
neblla_update_app({
  roles:    { titular: { tareas: 'read write' }, equipo: { tareas: 'read write' } },
  groups:   { invitable: ['equipo'], maxMembers: 10 },
  webhooks: [
    { event: 'purchase',         secret: 'N8N', path: '/equipo-alta' },
    { event: 'subscription_end', secret: 'N8N', path: '/equipo-baja' },
  ],
})

`` POST https://neblla.com/api/apps/<appId>/groups/create { "group": "eq-<userId>" } POST https://neblla.com/api/apps/<appId>/groups/add { "group": "eq-<userId>", "user": "<userId>", "role": "titular" } ``

``javascript const mio = (await neblla.groups.mine()).find(g => g.role === 'titular'); const { url } = await neblla.groups.invite(mio.group, { role: 'equipo' }); ` Y el equipo trabaja con filas del grupo: neblla.db.save(datos, { table: 'tareas', group: mio.group, id })`.

Otras formas valen igual: quitar solo al equipo y dejar al titular, o pasarlos a un papel de solo lectura (groups/add con otro role).

Validación autoritaria

neblla.shop.validate también se puede llamar desde room.js — útil para gatear acciones autoritarias detrás de la posesión de un producto. Ejemplo: para entrar a la sala VIP, room.js valida que el usuario tenga vip_pass antes de admitirlo, sin que pueda fingirlo desde su cliente.

Schema de producto

Al crear un producto desde el dashboard (o desde el MCP):


{
  name:        { en: 'Gold skin', es: 'Skin dorado' },  // i18n map o string
  description: { en: '...', es: '...' },                // opcional
  image:       'https://.../skin.png',                  // opcional
  type:        'one_time' | 'consumable' | 'subscription',
  price:       499,            // unidad más pequeña (céntimos en eur): one_time, o por periodo en subscription
  interval:    'month',        // 'month' | 'year', sólo subscription
  trialDays:   7,              // opcional, sólo subscription
  pricePerUnit: 10,            // unidad más pequeña, sólo consumable
  step:        1,              // tamaño del bundle (consumable)
  min:         5, max: 1000,   // límites del slider (consumable); la compra mínima, 5 × 10, llega al mínimo de Stripe
  published:   true,           // visible en shop.products()
  currency:    'eur',          // ISO-4217 (por defecto eur)
  defaultLang: 'en',           // fallback i18n
  stock:       10,             // opcional, one_time y consumable: unidades a la venta en total (null: sin límite)
  stockRefundReturns: true,    // opcional: una unidad reembolsada vuelve a la venta (false en una edición numerada)
  shipping:    { zones: [{ name: 'España', countries: ['ES'], amount: 1500 }] },  // opcional: un producto físico
}

Ficheros a la venta

Un original privado es un fichero de los medios de la app (neblla.media, referencia.txt) que solo descarga quien tiene derecho: «Agua nº 3» en alta resolución, las partituras de una academia en PDF, el pack de texturas HD de un juego, un ebook, un wav. De cualquier tipo: imágenes, PDF, EPUB, ZIP, sonido, vídeo. La lectura pública da solo sus variantes hasta el ancho que elige el dev (publicWidth, 1280 px por defecto, una vista previa que no se puede imprimir), nunca el original, que vive en un bucket aparte.

Unidades a la venta

Un producto único o un consumible puede tener N unidades a la venta (stock): una edición de 10 copias, 100 espadas legendarias, 50 tazas, entradas sin butaca. neblla.shop.products() dice, de cada uno con stock, stock: { units, left, soldOut } («quedan 7 de 10», «agotado»), nunca quién está pagando ni quién compró. Abrir el pago aparta las unidades compradas mientras vive el checkout (se cierra a los 30 minutos más o menos, en vez de las 24 horas de Stripe): pagadas, se venden; abandonado, vuelven solas; con un reembolso completo, vuelven a la venta salvo stockRefundReturns: false (una edición numerada y firmada). Sin unidades, shop.buy rechaza con sold_out (y left); dos compradores a la vez nunca se llevan la última los dos. El dev pone y cambia las unidades con neblla_create_product / neblla_update_product (stock, stockRefundReturns) o en Dinero › Productos, donde cada producto enseña lo que queda, lo que se está pagando y lo vendido; bajarlas por debajo de lo vendido deja cero. Una compra de prueba en el pre se rechaza si no queda ninguna, pero no aparta ni vende: las unidades son de pro.

Productos físicos y pedidos

Un producto único o un consumible con shipping es físico: copias impresas de una foto, tazas, camisetas, un fanzine impreso. shipping: { zones: [{ name, countries, amount }] }: cada zona con sus países (códigos ISO, un país en una sola zona) y sus gastos de envío en la moneda del producto (0: gratis), p. ej. «España 15 €, resto de Europa 35 €».

Impuestos

El vendedor eres tú (el cobro se hace en tu cuenta de Stripe), así que los impuestos son tuyos. Puedes configurar Stripe Tax en tu propio panel de Stripe, o dejar que Neblla te lo monte con neblla_tax:

Tu cuenta de cobros

Para vender necesitas una cuenta de Stripe conectada a Neblla. Se abre la primera vez que pides el alta (neblla_payments_account con onboarding_link, o "Configurar Stripe" en tu cuenta del panel), no al registrarte. El alta es la de Stripe, dentro de neblla.com: el formulario de Stripe aparece en tu cuenta del panel, con tu email, tu nombre y la web de tu studio (play.neblla.com/studio/<nombre corto>, ver publicar.txt) ya rellenados. La cuenta es tuya y vale para todas tus apps: el alta no va atada a ninguna. Un proyecto puede vender con un Stripe propio (neblla_projects con use_own_stripe; neblla_payments_account y neblla_tax con su projectId actúan sobre esa cuenta) y los demás usan el de la cuenta. Una app que vende (productos de pago o suscripciones vivas) no puede moverse a un proyecto con otro Stripe: lo vendido sigue atado a la cuenta en la que se vendió. Identidad, cuenta bancaria y tu usuario de Stripe van directamente a Stripe; Neblla nunca los ve ni los guarda, y el MCP nunca te los pide. Si ya usas Stripe, entra con tu usuario durante el alta y la cuenta nueva queda junto a las que ya tienes.

Si publicas un producto de pago antes de tenerla, no hace falta ir a buscarla: el panel te lleva al alta en ese momento, y por el MCP la respuesta trae connectRequired: true y connectUrl, una página de neblla.com que abre el alta desde tu sesión (el enlace de Stripe en sí no sale de Neblla). El producto queda guardado, oculto a los usuarios y esperando (publishWhenReady), y se publica solo en cuanto Stripe da la cuenta por activa.

Qué falta, en palabras. neblla_payments_account con status (o Ajustes › Cuenta › Stripe) le pregunta a Stripe cada vez y dice qué falta agrupado por cosa, no por campo: «Faltan 3 cosas: tu cuenta bancaria (IBAN), tu fecha de nacimiento y aceptar las condiciones de Stripe; tu email, tu nombre y tu web ya están». missing trae { key, words, overdue }; errors, lo que Stripe no pudo verificar; verifying, lo que está comprobando; filledByNeblla, lo que rellenó Neblla; y disabledReason si Stripe paró la cuenta. Eso solo lo puedes poner tú, en el formulario de Stripe: al volver de él, vuelve a pedir status y verás lo que queda. Si Stripe no responde, status dice stripeReachable: false con el porqué (error.kind: down), y no te dice «pendiente». Si la cuenta guardada no existe en la plataforma de Neblla (por ejemplo, se abrió con la clave de prueba de Stripe), lo dice (lostAccount.reason: test_mode_account, live_mode_account o not_on_platform) y el siguiente alta abre una nueva; si vuelve a estar a mano, se recupera sola.

Lo que pone el extracto del banco. En un cobro con tarjeta el extracto de tu cliente dice tu studio y la app, 22 letras como mucho: «GRACIA* AJEDREZ». El prefijo (2 a 10 letras latinas o números, con al menos una letra) es de tu cuenta: Neblla te propone el nombre corto de tu studio al hacer el alta y lo cambias con neblla_payments_account({ action: 'set_descriptor', prefix: 'GRACIA' }) o en Ajustes › Cuenta › Stripe. El sufijo es la app, cortado para que quepa; vale para compras sueltas y pagos al reservar. En una suscripción Stripe pone la línea entera sin asterisco («GRACIA AJEDREZ»). El recibo de Stripe nombra la app: «Ajedrez · Pase del club».

La cuenta es tuya, con el panel completo de Stripe (pagos, transferencias, reembolsos, disputas, impuestos); dashboard_link te lleva a él. Como el cobro se hace en tu cuenta, la comisión de Stripe, los reembolsos y las disputas van por tu cuenta, y si tu saldo queda en negativo es Stripe quien te lo reclama, no Neblla. Puedes desconectar Neblla cuando quieras, desde Neblla (la cuenta queda guardada para volver a ella) o desde tu panel de Stripe (Neblla deja de vender por ti en ese momento).

Varios vendedores, cada uno cobra lo suyo

Una app con muchos vendedores — los clubes de una app de pádel, las artistas de una galería, los autores de una tienda de plantillas, los profes de una plataforma de clases — cobra cada venta en la cuenta de Stripe del vendedor, no en la tuya. Tú te quedas tu comisión y Neblla la suya de siempre. Vale igual para juegos, apps y webs.

Comisión

Neblla se lleva el 1 % de cada venta, antes de impuestos. La comisión de Stripe va aparte y la paga tu cuenta de Stripe, porque el cobro se hace en ella: tú eres el vendedor. No hay cuota fija por venta ni por producto.

Garantías

Los pagos están mediados: room.js solo ve el resultado (la compra confirmada), nunca datos de tarjeta, ni tiene capacidad de regalar ítems gratis o falsificar compras de otro usuario.

Un gasto se hace entero o no se hace: pedir gastar más unidades de las que tiene el usuario no le quita nada, en lugar de vaciar lo que había y fallar después.

Una tienda de un vendedor, de punta a punta

Este es el recorrido completo de un pedido, porque las piezas están repartidas por tres capítulos y por separado no se ve que el circuito cierra. El caso es tú vendes a tus usuarios — un MVP tipo Etsy con un solo vendedor, que eres tú. Si los que venden son varios (clubes, artistas, autores), mira "Varios vendedores, cada uno cobra lo suyo", arriba; lo que no existe es la reventa entre usuarios.

0. Cobrar de verdad. neblla_payments_account conecta tu Stripe Connect. Hasta que esté activa, todo producto queda con published: false por la fuerza — o sea que puedes montar el catálogo entero antes de tener papeles, pero nadie podrá comprar.

1. El catálogo. neblla_create_product con price en la unidad más pequeña de la moneda y currency ISO 4217 (por defecto eur; cualquiera que cobre Stripe). El precio va en la unidad más pequeña de la moneda: céntimos en casi todas, unidades enteras en las que no tienen (jpy, krw…), milésimas acabadas en 0 en bhd, jod, kwd, omr y tnd. Lo que Stripe no cobraría se rechaza al crear o editar el producto con invalid_price, no al pagar: una moneda que no acepta, o un precio bajo su mínimo (0,50 € o 0,50 $; en un consumible, la compra mínima, min × pricePerUnit). Si el usuario dio su email a la app, Stripe le manda el recibo. name y description aceptan un objeto por idioma. Dos tipos y no son intercambiables: one_time para la pieza única que se vende una vez, y consumable con min, max y step, que es literalmente un selector de cantidad. El cliente pinta la tienda con neblla.shop.products().

2. Las fotos van por URL, y una de esas URLs puede ser de Neblla. image es una URL, no un fichero subido al producto. Pero no te vayas a buscar un CDN por un JPEG de 40 kB: sube la foto como fichero de la app —neblla_upload_file acepta binarios hasta 25 MB— y pasa su URL pública. Una app publicada la sirve desde Cloudflare sin gastar cuota; en pre o en una app en caliente cuenta sus bytes, salvo cuando el navegador ya la tiene. Lo que gastas es el cupo de ficheros (300 por cuenta gratis, compartidos por sus apps, o 600 por proyecto Pro, compartido con el HTML, el CSS y todo lo demás); para un catálogo grande, guarda las fotos en tu propio almacenamiento y enlázalas. No las metas en base64 dentro de otro sitio para esquivarlo: ahí las pagas en cuota cada vez que alguien abre la tienda.

3. El pago. neblla.shop.buy(productId, amount) abre una ventana de checkout de Stripe de verdad; la promesa resuelve cuando la ventana está abierta, y la compra confirmada llega después por neblla.shop.on('purchase'). Después, neblla.shop.purchases lo lleva, salta neblla.shop.on('purchase'), y el servidor revalida contra Stripe por su cuenta — lo que un cliente afirme sobre lo que compró no es la autoridad.

4. El pedido. Para algo que se envía, dale al producto shipping ("Productos físicos y pedidos", arriba): el checkout pide la dirección y cobra el envío de la zona del comprador, cada pago es un pedido con su estado en Dinero › Ventas y neblla_orders, y el comprador se entera de cada paso en su buzón. Lo que la dirección no cubre (la talla, una nota de regalo) el comprador lo manda con neblla.inbox.send({ message, data, thread }), donde data admite JSON arbitrario; lo lees con neblla_list_communications y respondes en el mismo thread con neblla_send_communication: es tu atención al cliente, sin montar ningún endpoint. El buzón guarda 100 mensajes en free y 2.000 en Pro.

5. Quién compró y qué le debes. neblla_list_users y neblla_search_users_by_email te dan al comprador, y purchases va por usuario. Para marcar un consumable como servido, neblla_consume_purchase. Cuidado con neblla_update_user_data: es un reemplazo completo del documento del usuario, no un merge — lee, modifica y vuelve a escribir entero, o pierdes lo demás.

6. El sitio. Los pedidos viajan por el buzón, que tiene su propio tope por usuario: 100 mensajes en gratis y 2.000 en Pro. Lo que la app guarde con db.save() —carrito, preferencias, historial de compras— va a la base de datos de la app (50 MB por cuenta listos para usar, compartidos por sus apps gratis; 100 MB en un proyecto Pro, o el tuyo con neblla_byodb); si la tienda guarda más de 10 KB por usuario, sube el límite antiabuso a lo que necesite.