Facturación por uso con Stripe: un modelo de datos práctico
La facturación por uso con Stripe requiere almacenamiento de eventos limpio y reconciliación. Aprende un esquema simple, flujo de webhooks, backfills y cómo arreglar doble conteo.

Qué estás construyendo realmente (y por qué se rompe)
La facturación por uso suena simple: mide lo que un cliente usó, multiplícalo por un precio y cárgalo al final del periodo. En la práctica, estás construyendo un pequeño sistema contable. Tiene que permanecer correcto incluso cuando los datos llegan tarde, llegan dos veces o nunca llegan.
La mayoría de fallos no ocurren en el checkout o en el panel. Ocurren en el modelo de datos de medición. Si no puedes responder, con confianza, “¿Qué eventos de uso se contaron para esta factura, y por qué?”, eventualmente cobrarás de más, de menos o perderás confianza.
La facturación por uso suele fallar de formas previsibles: eventos faltan tras una caída, reintentos crean duplicados, llegadas tardías aparecen después de que se calcularon totales, o sistemas diferentes discrepan y no puedes reconciliar la diferencia.
Stripe es excelente en precios, facturas, impuestos y cobros. Pero Stripe no conoce el uso bruto de tu producto a menos que tú se lo envíes. Eso obliga a decidir una fuente de verdad: ¿es Stripe el libro contable, o es tu base de datos el libro que Stripe refleja?
Para la mayoría de equipos, la división más segura es:
- Tu base de datos es la fuente de verdad para eventos de uso en bruto y su ciclo de vida.
- Stripe es la fuente de verdad para lo que realmente se facturó y pagó.
Ejemplo: rastreas “llamadas a la API”. Cada llamada genera un evento de uso con una clave única estable. A la hora de facturar, totalizas solo los eventos elegibles que no han sido facturados aún, y luego creas o actualizas el item de factura en Stripe. Si hay reintentos de ingestión o llega un webhook duplicado, las reglas de idempotencia hacen que el duplicado sea inofensivo.
Decisiones a tomar antes de diseñar tablas
Antes de crear tablas, fija las definiciones que decidirán si la facturación sigue siendo explicable después. La mayoría de “errores misteriosos en facturas” vienen de reglas poco claras, no de SQL malo.
Empieza por la unidad que cobras. Elige algo fácil de medir y difícil de discutir. “Llamadas a la API” se complica con reintentos, solicitudes en lote y fallos. “Minutos” se complica con solapamientos. “GB” necesita una base clara (GB vs GiB) y un método de medición claro (promedio vs pico).
Luego, define límites. Tu sistema necesita saber exactamente a qué ventana pertenece un evento. ¿El uso se cuenta por hora, por día, por periodo de facturación o por acción del cliente? Si un cliente se actualiza a mitad de mes, ¿divides la ventana o aplicas un precio a todo el mes? Estas decisiones determinan cómo agrupas eventos y cómo explicas totales.
También decide qué sistema posee qué hechos. Un patrón común con Stripe es: tu app posee eventos en bruto y totales derivados, mientras Stripe posee facturas y estado de pagos. Ese enfoque funciona mejor cuando no editas la historia silenciosamente. Registra correcciones como nuevas entradas y conserva el registro original.
Un breve conjunto de no negociables ayuda a mantener el esquema honesto:
- Trazabilidad: cada unidad facturada puede vincularse a eventos almacenados.
- Auditabilidad: puedes responder “¿por qué se cobró esto?” meses después.
- Reversibilidad: los errores se corrigen con ajustes explícitos.
- Idempotencia: la misma entrada no puede contarse dos veces.
- Propiedad clara: un sistema posee cada hecho (uso, precios, facturación).
Ejemplo: si cobras por “mensajes enviados”, decide si los reintentos cuentan, si las entregas fallidas cuentan y qué timestamp tiene prioridad (hora del cliente vs hora del servidor). Escríbelo y luego codifícalo en campos de evento y validación, no en la memoria de alguien.
Un modelo de datos simple para eventos de uso
La facturación por uso es más sencilla cuando tratas el uso como contabilidad: los hechos en bruto son append-only y los totales se derivan. Esa elección única previene la mayoría de disputas porque siempre puedes explicar de dónde vino un número.
Un punto de partida práctico usa cinco tablas principales (los nombres pueden variar):
- customer: id interno del cliente, id de cliente en Stripe, estado, metadatos básicos.
- subscription: id interno de suscripción, id de suscripción en Stripe, plan/precios esperados, timestamps de inicio/fin.
- meter: lo que mides (llamadas a la API, asientos, GB-horas de almacenamiento). Incluye una clave de medidor estable, unidad y cómo se agrega (suma, máximo, único).
- usage_event: una fila por acción medida. Almacena customer_id, subscription_id (si se conoce), meter_id, quantity, occurred_at (cuando ocurrió), received_at (cuando lo ingeriste), source (app, import por lotes, partner) y una clave externa estable para deduplicar.
- usage_aggregate: totales derivados, normalmente por customer + meter + bucket de tiempo (día u hora) y periodo de facturación. Almacena la cantidad sumada más una versión o last_event_received_at para soportar recálculos.
Mantén usage_event inmutable. Si luego descubres un error, escribe un evento compensatorio (por ejemplo, -3 asientos por una cancelación) en lugar de editar la historia.
Almacena eventos en bruto para auditorías y disputas. Si no puedes guardarlos para siempre, consérvalos al menos mientras dure tu ventana de lookback de facturación más la ventana de reembolsos/disputas.
Mantén los totales derivados separados. Los agregados son rápidos para facturas y paneles, pero son desechables. Debes poder reconstruir usage_aggregate desde usage_event en cualquier momento, incluso después de un backfill.
Idempotencia y estados del ciclo de vida de eventos
Los datos de uso son ruidosos. Los clientes reintentan peticiones, las colas entregan duplicados y los webhooks de Stripe pueden llegar fuera de orden. Si tu base de datos no puede probar “este evento de uso ya fue contado”, eventualmente facturarás dos veces.
Dale a cada evento de uso un event_id estable y determinista y aplica unicidad sobre él. No confíes solo en un id autoincremental como identificador. Un buen event_id se deriva de la acción de negocio, como customer_id + meter + source_record_id (o customer_id + meter + timestamp_bucket + sequence). Si la misma acción se envía otra vez, produce el mismo event_id y el insert se convierte en un no-op seguro.
La idempotencia debe cubrir cada ruta de ingestión, no solo tu API pública. Llamadas desde SDK, imports por lotes, jobs de workers y procesadores de webhooks se reintentan. Usa una regla: si la entrada puede reintentarse, necesita una clave de idempotencia guardada en tu base de datos y comprobada antes de cambiar totales.
Un modelo simple de estados de ciclo de vida hace que los reintentos sean seguros y facilita el soporte. Mantenlo explícito y guarda una razón cuando algo falla:
received: almacenado, aún no verificadovalidated: pasa esquema, cliente, medidor y reglas de ventana de tiempoposted: contado en los totales del periodo de facturaciónrejected: ignorado permanentemente (con un código de motivo)
Ejemplo: tu worker se cae después de validar pero antes de postear. En el reintento encuentra el mismo event_id en estado validated, y luego continúa a posted sin crear un segundo evento.
Para webhooks de Stripe, usa el mismo patrón: guarda el event.id de Stripe y márcalo procesado solo una vez, así las entregas duplicadas son inofensivas.
Paso a paso: ingestión de eventos de medición de extremo a extremo
Trata cada evento de medición como dinero: valídalo, almacena el original y luego deriva totales desde la fuente de verdad. Eso mantiene la facturación predecible cuando los sistemas reintentan o envían datos tarde.
Un flujo de ingestión fiable
Valida cada evento entrante antes de tocar cualquier total. Como mínimo, exige: un identificador estable de cliente, un nombre de medidor, una cantidad numérica, un timestamp y una clave única de evento para idempotencia.
Escribe primero el evento en bruto, incluso si planeas agregar más tarde. Ese registro bruto es lo que reprocesarás, auditarás y usarás para arreglar errores sin adivinar.
Un flujo confiable se ve así:
- Acepta el evento, valida campos requeridos, normaliza unidades (por ejemplo, segundos vs minutos).
- Inserta una fila de evento de uso en bruto usando la clave del evento como restricción única.
- Agrega en un bucket (diario o por periodo de facturación) aplicando la cantidad del evento.
- Si reportas uso a Stripe, registra lo que enviaste (medidor, cantidad, periodo e identificadores de respuesta de Stripe).
- Registra anomalías (eventos rechazados, conversiones de unidades, llegadas tardías) para auditorías.
Mantén la agregación repetible. Un enfoque común es: insertar el evento en bruto en una transacción y luego encolar un job para actualizar buckets. Si el job corre dos veces, debe detectar que el evento en bruto ya fue aplicado.
Cuando un cliente pregunte por qué se le cobró 12.430 llamadas a la API, deberías poder mostrar el conjunto exacto de eventos en bruto incluidos en esa ventana de facturación.
Reconciliar webhooks de Stripe con tu base de datos
Los webhooks son el recibo de lo que Stripe hizo realmente. Tu app puede crear borradores y enviar uso, pero el estado de la factura solo se vuelve real cuando Stripe lo confirma.
La mayoría de equipos se enfocan en un pequeño conjunto de tipos de webhook que afectan resultados de facturación:
invoice.created,invoice.finalized,invoice.paid,invoice.payment_failedcustomer.subscription.created,customer.subscription.updated,customer.subscription.deletedcheckout.session.completed(si inicias suscripciones mediante Checkout)
Almacena cada webhook que recibas. Conserva el payload bruto además de lo que observaste al llegar: Stripe event.id, event.created, el resultado de la verificación de la firma y tu timestamp de recepción. Ese historial importa cuando depuras una discrepancia o respondes “¿por qué me cobraron?”.
Un patrón sólido e idempotente de reconciliación se parece a esto:
- Inserta el webhook en una tabla
stripe_webhook_eventscon una restricción única enevent_id. - Si el insert falla, es un reintento. Para.
- Verifica la firma y registra pase/fallo.
- Procesa el evento buscando tus registros internos por IDs de Stripe (cliente, suscripción, factura).
- Aplica el cambio de estado solo si avanza el estado.
La entrega fuera de orden es normal. Usa una regla de “el estado máximo gana” más timestamps: nunca retrocedas un registro.
Ejemplo: recibes invoice.paid para la factura in_123, pero tu fila de factura interna aún no existe. Crea una fila marcada como “vista desde Stripe” y luego adjúntala a la cuenta correcta usando el id de cliente de Stripe. Eso mantiene tu libro consistente sin reprocesar dos veces.
De totales de uso a líneas de factura
Convertir uso bruto en líneas de factura es principalmente cuestión de tiempos y límites. Decide si necesitas totales en tiempo real (paneles, alertas de gasto) o solo al momento de facturar. Muchos equipos hacen ambas cosas: escriben eventos continuamente y calculan totales listos para factura en un job programado.
Alinea tu ventana de uso con el periodo de facturación de Stripe. No adivines meses calendario. Usa el periodo de inicio y fin del item de suscripción y suma solo eventos cuyos timestamps caigan dentro de esa ventana. Guarda timestamps en UTC y haz la ventana de facturación en UTC también.
Mantén el historial inmutable. Si encuentras un error después, no edites eventos antiguos ni reescribas totales previos. Crea un registro de ajuste que apunte a la ventana original y sume o reste cantidad. Es más fácil de auditar y de explicar.
Los cambios de plan y las prorratas son donde la trazabilidad suele perderse. Si un cliente cambia de plan a mitad de ciclo, divide el uso en subventanas que coincidan con el rango activo de cada precio. Tu factura puede incluir dos líneas de uso (o una línea más un ajuste), cada una vinculada a un precio y rango de tiempo específico.
Un flujo práctico:
- Obtén la ventana de la factura desde period start y end de Stripe.
- Agrega eventos de uso elegibles en un total para esa ventana y precio.
- Genera líneas de factura desde el total de uso más cualquier ajuste.
- Guarda un id de ejecución del cálculo para poder reproducir los números más tarde.
Backfills y datos tardíos sin romper la confianza
Los datos de uso tardíos son normales. Dispositivos se desconectan, jobs por lotes se retrasan, partners reenvían archivos y logs se reproducen tras una caída. La clave es tratar los backfills como trabajo de corrección, no como una forma de “forzar los números”.
Sé explícito sobre de dónde pueden venir los backfills (logs de aplicación, exports del warehouse, sistemas de partners). Registra la fuente en cada evento para poder explicar por qué llegó tarde.
Cuando haces un backfill, conserva dos timestamps: cuándo ocurrió el uso (el tiempo por el que quieres facturar) y cuándo lo ingeriste. Marca el evento como backfilled, pero no sobrescribas la historia.
Prefiere reconstruir totales desde eventos en bruto en lugar de aplicar deltas a la tabla de agregados del día. Las re-ejecuciones son la forma de recuperarse de bugs sin adivinar. Si tu pipeline es idempotente, puedes volver a ejecutar un día, una semana o un periodo de facturación completo y obtener los mismos totales.
Una vez que existe una factura, las correcciones deben seguir una política clara:
- Si la factura no está finalizada, recalcula y actualiza antes de la finalización.
- Si está finalizada y se quedó corta, emite una factura adicional (o añade un item) con una descripción clara.
- Si está finalizada y se cobró de más, emite una nota de crédito y referencia la factura original.
- No muevas uso a otro periodo para evitar una corrección.
- Guarda una razón breve para la corrección (reenvío de partner, entrega tardía de logs, corrección de bug).
Ejemplo: un partner envía eventos faltantes del 28-29 de enero el 3 de febrero. Los insertas con occurred_at en enero, ingested_at en febrero y una fuente de backfill “partner”. La factura de enero ya se pagó, así que creas una pequeña factura adicional por las unidades faltantes, con la razón guardada junto al registro de reconciliación.
Errores comunes que causan doble conteo
El doble conteo ocurre cuando un sistema trata “llegó un mensaje” como “la acción ocurrió”. Con reintentos, webhooks retrasados y backfills, necesitas separar la acción del cliente de tu procesamiento.
Los culpables habituales:
- Reintentos tratados como uso nuevo. Si cada evento no lleva un id de acción estable (request_id, message_id) y tu base de datos no aplica unicidad, contarás dos veces.
- Tiempo del evento mezclado con tiempo de procesamiento. Reportar por tiempo de ingestión en lugar de por occurred_at hace que eventos tardíos caigan en el periodo equivocado y luego se cuenten otra vez durante replays.
- Eventos en bruto borrados u sobrescritos. Si solo mantienes un total acumulado, no puedes probar qué pasó y el reprocesado puede inflar totales.
- Suponer orden en webhooks. Los webhooks pueden duplicarse, llegar fuera de orden o representar estados parciales. Reconcílialos por IDs de objetos de Stripe y mantén una protección de “ya procesado”.
- Cancelaciones, reembolsos y créditos no modelados explícitamente. Si solo sumas uso y nunca registras ajustes negativos, acabarás “arreglando” totales con imports y volviendo a contar.
Ejemplo: registras “10 llamadas a la API” y luego emites un crédito por 2 llamadas por una caída. Si haces un backfill reenviando todo el uso del día y también aplicas el crédito, el cliente puede ver 18 llamadas (10 + 10 - 2) en lugar de 8.
Lista rápida antes de salir a producción
Antes de activar facturación por uso para clientes reales, repasa lo básico que previene errores de facturación costosos. La mayoría de fallos no son “problemas de Stripe”. Son problemas de datos: duplicados, días faltantes y reintentos silenciosos.
Mantén la lista corta y exigible:
- Aplica unicidad en eventos de uso (por ejemplo, una restricción única en
event_id) y comprométete con una estrategia de id. - Almacena cada webhook, verifica su firma y procésalo idempotentemente.
- Trata el uso en bruto como inmutable. Corrige con ajustes (positivos o negativos), no con ediciones.
- Ejecuta un job diario de reconciliación que compare totales internos (por cliente, por medidor, por día) contra el estado de facturación en Stripe.
- Añade alertas para brechas y anomalías: días faltantes, totales negativos, picos repentinos o una gran diferencia entre “eventos ingeridos” y “eventos facturados”.
Una prueba simple: elige un cliente, vuelve a ejecutar la ingestión de los últimos 7 días y confirma que los totales no cambien. Si cambian, todavía tienes un problema de idempotencia o ciclo de vida.
Escenario de ejemplo: un mes realista de uso y facturas
Un pequeño equipo de soporte usa un portal que cobra $0.10 por conversación gestionada. Lo venden como facturación por uso con Stripe, pero la confianza viene de lo que pasa cuando los datos son desordenados.
El 1 de marzo, el cliente inicia un nuevo periodo de facturación. Cada vez que un agente cierra una conversación, tu app emite un evento de uso:
event_id: un UUID estable de tu appcustomer_idysubscription_item_idquantity: 1 conversaciónoccurred_at: la hora de cierreingested_at: cuando lo viste por primera vez
El 3 de marzo, un worker reintenta tras un timeout y envía la misma conversación otra vez. Porque event_id es único, el segundo insert es un no-op y los totales no cambian.
A mitad de mes, Stripe envía webhooks para vista previa de factura y luego la factura finalizada. Tu handler de webhooks almacena stripe_event_id, type y received_at, y la marca como procesada solo después de que la transacción en tu base de datos confirme. Si el webhook se entrega dos veces, la segunda entrega se ignora porque stripe_event_id ya existe.
El 18 de marzo importas un lote tardío desde un cliente móvil que estuvo offline. Contiene 35 conversaciones del 17 de marzo. Esos eventos tienen occurred_at más antiguos, pero siguen siendo válidos. Tu sistema los inserta, recalcula totales diarios para el 17 de marzo y el uso extra se incluye en la siguiente factura porque aún está dentro del periodo de facturación abierto.
El 22 de marzo descubres que una conversación se registró dos veces por un bug que generó dos event_id diferentes. En lugar de borrar historia, escribes un evento de ajuste con quantity = -1 y una razón como “duplicado detectado”. Eso mantiene la pista de auditoría intacta y hace que el cambio en la factura sea explicable.
Próximos pasos: implementar, monitorear e iterar con seguridad
Empieza pequeño: un medidor, un plan, un segmento de clientes que entiendas bien. La meta es consistencia simple: tus números coinciden con Stripe mes a mes, sin sorpresas.
Construye pequeño y luego endurece
Un despliegue inicial práctico:
- Define una forma de evento (qué se cuenta, en qué unidad, en qué momento).
- Almacena cada evento con una clave de idempotencia única y un estado claro.
- Agrega en totales diarios (u horarios) para que las facturas puedan explicarse.
- Reconcílialo contra webhooks de Stripe en un horario, no solo en tiempo real.
- Después de facturar, trata el periodo como cerrado y enruta eventos tardíos por un camino de ajustes.
Incluso con herramientas no-code, puedes mantener integridad de datos si haces estados inválidos imposibles: aplica restricciones únicas para claves de idempotencia, requiere claves foráneas a cliente y suscripción, y evita actualizar eventos en bruto aceptados.
Monitoreo que te salva después
Añade pantallas de auditoría simples desde el principio. Te devuelven el valor la primera vez que alguien pregunta “¿por qué mi factura es más alta este mes?”. Vistas útiles incluyen: buscar eventos por cliente y periodo, ver totales por periodo día a día, rastrear estado de procesamiento de webhooks y revisar backfills y ajustes con quién/cuándo/por qué.
Si implementas esto con AppMaster (appmaster.io), el modelo encaja de forma natural: define eventos en bruto, agregados y ajustes en el Data Designer, luego usa Business Processes para ingestión idempotente, agregación programada y reconciliación de webhooks. Aún tendrás un libro real y una pista de auditoría, sin escribir todo el plumbing a mano.
Cuando tu primer medidor sea estable, añade el siguiente. Mantén las mismas reglas de ciclo de vida, las mismas herramientas de auditoría y el mismo hábito: cambia una cosa a la vez y verifica de extremo a extremo.
FAQ
Trátalo como un pequeño libro contable. Lo difícil no es cobrar la tarjeta; es mantener un registro preciso y explicable de lo que se contó, incluso cuando los eventos llegan tarde, llegan duplicados o necesitan correcciones.
Un valor seguro es: tu base de datos es la fuente de verdad para los eventos de uso en bruto y su estado, y Stripe es la fuente de verdad para facturas y resultados de pago. Esta separación mantiene la trazabilidad mientras Stripe maneja precios, impuestos y cobros.
Hazla estable y determinista para que los reintentos produzcan el mismo identificador. Suele derivarse de la acción de negocio real, por ejemplo: id de cliente + clave del medidor + id de registro de origen, de modo que un envío duplicado sea un no-op en lugar de uso extra.
No edites ni borres eventos de uso aceptados. Registra un evento de ajuste compensatorio (incluyendo cantidad negativa cuando sea necesario) y conserva el original intacto, así podrás explicar el historial más tarde sin adivinar qué cambió.
Mantén los eventos de uso en bruto como append-only y guarda los agregados por separado como datos derivados que puedas reconstruir. Los agregados son para velocidad y reportes; los eventos en bruto son para auditorías, disputas y reconstruir totales tras bugs o backfills.
Guarda al menos dos timestamps: cuándo ocurrió y cuándo lo ingeriste, y conserva la fuente. Si la factura no está finalizada, recalcula antes de la finalización; si ya está finalizada, trátalo como una corrección clara (cargo adicional o nota de crédito) en lugar de cambiar silenciosamente el periodo.
Almacena cada payload de webhook que recibas y aplica procesamiento idempotente usando el event id de Stripe como clave única. Los webhooks se duplican o llegan fuera de orden, así que tu handler solo debe aplicar cambios de estado que avancen los registros.
Usa el period start y end del periodo de facturación de Stripe como ventana, y divide el uso cuando cambie el precio activo. El objetivo es que cada línea de factura pueda vincularse a un rango de tiempo y precio específico para que los totales sean explicables.
Guarda un identificador de ejecución del cálculo o metadatos equivalentes para poder reproducir los totales. Si volver a ejecutar la ingestión para la misma ventana cambia los totales, probablemente tienes un problema de idempotencia o de estados de ciclo de vida.
Modela eventos de uso en bruto, agregados, ajustes y una tabla de bandeja de entrada de webhooks en el Data Designer, luego implementa ingestión y reconciliación en Business Processes con restricciones de unicidad para idempotencia. Puedes construir un libro contable auditable y una reconciliación programada sin escribir todo el plumbing a mano.


