Contenido

Documentación

Guías

Recetas completas: cobrar con Mercado Pago o con tu Stripe, tu cartelera, tu scanner, reconciliación.

Cada una de estas recetas está también como proyecto corriendo, listo para clonar: Ejemplos. Acá está el porqué; allá, el código entero.

Las dos primeras recetas son del plan Autogestionado: cobrás vos, con tu cuenta y tu pasarela, y POV no toca la plata. Sirve cualquier pasarela —acá están las dos más pedidas—. Si preferís que cobre POV, eso es el plan Pasarela POV, no se integra: se configura en el panel.

Cobrar con Mercado Pago

1. Embebés POV en la página de la función.
2. Escuchás `pov:hold` y te quedás con { holdToken, amountCents, currency }.
3. Tu backend crea una Preference de Mercado Pago por `amountCents`,
   con external_reference = holdToken.
4. El comprador paga en Mercado Pago.
5. Tu backend recibe la notificación de MP y verifica que el pago está aprobado.
6. Tu backend llama a POST /holds/{holdToken}/confirm con Idempotency-Key
   igual al id del pago de MP.
7. Le mostrás al comprador sus entradas, o el enlace https://pov.uy/r/{publicToken}.

Detalles que evitan los problemas típicos:

external_reference = holdToken es lo que une los dos mundos. Sin eso, la notificación de MP no sabe qué reserva cerrar.

Idempotency-Key = id del pago de MP. MP puede notificarte el mismo pago varias veces; con la clave atada al pago, confirmar dos veces devuelve la misma reserva.

Mirá los 10 minutos. Si el comprador se demora en el checkout de MP, el bloqueo puede vencer. El confirm rescata hasta 10 minutos después si las butacas siguen libres; si devuelve 410, usá details.unavailableSeats para reembolsar diciendo qué butaca se perdió.

Si el comprador vuelve sin pagar, llamá a nodo.pov.release(holdToken) para devolver las butacas enseguida.

Cobrar con tu cuenta de Stripe

Igual que arriba, cambiando las piezas:

3. Tu backend crea un PaymentIntent por `amountCents` en `currency`,
   con metadata.holdToken = holdToken.
5. Tu webhook recibe `payment_intent.succeeded` y lee metadata.holdToken.
6. POST /holds/{holdToken}/confirm con Idempotency-Key = payment_intent.id.

Confirmá desde el webhook, no desde el retorno del navegador. El navegador puede cerrarse justo después de pagar; el webhook no.

Esto es tu Stripe, no el nuestro: es el plan Autogestionado y la plata te llega directo. Si preferís que POV cobre, eso es la Pasarela POV y es otro plan.

Tu cartelera, tus ids

Ya tenés la programación en tu CMS y no querés cargarla dos veces.

1. Creás la sala en POV una sola vez, con sus sectores y precios.
   Copiás su id: es el único id nuestro que vas a guardar.
2. En cada guardado de una función en tu CMS, tu backend llama a
   PUT /api/v1/showtimes/by-ref/{tu-id} con { venueId, startsAt, title }.
3. Tu plantilla escribe el <div> con data-venue (fijo) y data-ref (tu id).

Los precios se administran en POV, por sector y por función. Es lo que hace que esta puerta sea segura de abrir: tu sistema nombra las funciones, POV pone los importes.

Si además activás «Programación desde tu web» en la sala, te podés saltear el paso 2: la función nace la primera vez que alguien abre esa página. Leé los frenos en Programación externa antes de encenderlo.

Tu propio scanner en la puerta

El panel ya trae una consola de ingreso (Ingreso, funciona con cualquier lector USB). Si preferís tu propia app:

1. Tu app escanea el QR y obtiene el token (qr_… o rsv_…).
2. Tu backend llama a POST /api/v1/tickets/{token}/validate con sk_,
   pasando `gate` para saber después por qué puerta entró.
3. 200 → adelante.  409 ALREADY_USED → ya se usó, y te dice cuándo.

Con un rsv_ de una compra de varias, decidí: sin seats entran todos de una; con seats entra el que llegó. Si tu puerta necesita velocidad, validá derecho; si el grupo llega separado, preguntá. El QR de cada entrada habilita su butaca, que es el camino normal.

Reconciliación de pagos

Cuadrar tu día contra POV. Hay dos formas, y conviene saber cuál usar cuándo.

Cuadrar un día entero

Barrés lo que cambió desde tu último corte y lo comparás contra tus pagos:

GET /api/v1/reservations?updated_since=2026-08-25T00:00:00Z&limit=100
Authorization: Bearer sk_live_xxx

# → { "data": [ … ], "nextCursor": "…" }
# Seguí pidiendo con ?cursor=<nextCursor> hasta que venga null.

Cada reserva trae externalPaymentRef —el id que le pusiste al confirmar—, así que el cruce contra tus pagos es directo. Y como ordena por fecha de actualización y no por fecha de venta, una reserva reembolsada hoy aparece en el barrido de hoy aunque se haya vendido la semana pasada: si no fuera así, cuadrar un día no serviría de nada.

Un pago suelto que no cuadra

GET /api/v1/reservations?externalPaymentRef=mp_12345
Authorization: Bearer sk_live_xxx

Vacío significa que ese pago no cerró ninguna venta: cobraste y no hay entradas, así que hay que reembolsar. Si tenés el id de la reserva, también podés ir directo con GET /api/v1/reservations/{id}.

Si sólo guardaste el bloqueo

Sirve igual, y es lo que había antes de que existiera la consulta por reserva:

GET /api/v1/holds/{holdToken}
  · status CONFIRMED → hay venta, y `reservationId` es su id.
  · status EXPIRED o RELEASED → cobraste algo sin venta: hay que reembolsar.
  · status ACTIVE → todavía está en curso.

Reconfirmar también sirve: POST /holds/{token}/confirm es idempotente, así que si dudás si confirmaste, confirmá de nuevo — te devuelve la misma reserva.

Lo que hace que todo esto funcione es mandar el id de tu pago en el confirm, como externalPaymentRef. Sin eso, el único hilo entre los dos sistemas es el holdToken, y tenés que guardarlo vos. Con eso, la búsqueda va en la dirección que importa: del pago a la venta.

Personalizar el widget

Tres niveles, de menos a más.

1. Colores, desde el panel

Marca → Diseño: acento, pantalla, butaca libre, ocupada, seleccionada, ícono y fondo. Se aplican solos a todos tus widgets. Es la forma recomendada: no requiere CSS y se ve al instante.

Ahí mismo subís tu logo (PNG/JPG/WebP hasta 8 MB), que es lo que se ve en la pantalla de la sala 3D cuando el contenido no trae imagen propia.

2. Colores en vivo, desde tu página

document.querySelector('[data-pov]').pov.setTheme({
  accent: '#2c69d6',
  available: '#2A2F3A',
  occupied: '#555',
});

Para previsualizar, o para seguir el modo oscuro de tu sitio. Desde el HTML también podés fijar data-accent.

3. CSS propio

Marca → CSS para lo permanente (hasta 40 KB), o nodo.pov.setCss(css) en vivo. Se inyecta después de los estilos base, así que puede sobreescribir casi todo: tipografía, espaciados, bordes, formas.

Scopealo con [data-embed], que envuelve todo el widget. Para encontrar los selectores exactos, abrí el widget y usá el inspector del navegador.

[data-embed] { font-family: 'Poppins', system-ui, sans-serif; }
[data-embed] button { border-radius: 9999px; letter-spacing: 0.01em; }

El CSS se sanea antes de aplicarse: no se aceptan hojas que intenten salirse del bloque de estilo, y si el panel te rechaza una, te dice por qué.

El CSS afecta el selector 2D. La escena 3D no se configura por CSS sino desde el editor de la sala: modelo de butaca, tapizado, luces, pantalla.