Faturamento por uso com Stripe: um modelo de dados prático
Faturamento por uso com Stripe exige armazenamento limpo de eventos e reconciliação. Aprenda um esquema simples, fluxo de webhooks, backfills e correções de contagem dupla.

O que você realmente está construindo (e por que quebra)
O faturamento por uso parece simples: medir o que um cliente usou, multiplicar por um preço e cobrar no fim do período. Na prática, você está construindo um pequeno sistema contábil. Ele precisa permanecer correto mesmo quando dados chegam atrasados, chegam duas vezes ou nunca chegam.
A maioria das falhas não acontece no checkout ou no painel. Acontece no modelo de dados de medição. Se você não consegue responder, com confiança, “Quais eventos de uso foram contabilizados para esta fatura, e por quê?”, você eventualmente cobrará a mais, a menos, ou perderá confiança.
O faturamento por uso normalmente quebra de algumas maneiras previsíveis: eventos desaparecem após uma queda, retries criam duplicatas, chegadas tardias aparecem depois que os totais foram computados, ou diferentes sistemas discordam e você não consegue reconciliar a diferença.
O Stripe é excelente em preços, faturas, impostos e cobrança. Mas o Stripe não conhece o uso bruto do seu produto a menos que você envie. Isso força uma decisão sobre a fonte de verdade: o Stripe é o razão, ou seu banco de dados é o razão que o Stripe reflete?
Para a maioria das equipes, a divisão mais segura é:
- Seu banco de dados é a fonte de verdade para eventos brutos de uso e seu ciclo de vida.
- O Stripe é a fonte de verdade para o que foi efetivamente faturado e pago.
Exemplo: você acompanha “chamadas de API”. Cada chamada gera um evento de uso com uma chave única estável. Na hora da fatura, você totaliza apenas eventos elegíveis que ainda não foram faturados e então cria ou atualiza o item de fatura no Stripe. Se houver retries de ingestão ou um webhook chegar duas vezes, regras de idempotência tornam a duplicata inofensiva.
Decisões a tomar antes de projetar tabelas
Antes de criar tabelas, defina as regras que decidirão se o faturamento continuará explicável depois. A maioria dos “bugs misteriosos de fatura” vem de regras pouco claras, não de SQL ruim.
Comece pela unidade que você cobra. Escolha algo fácil de medir e difícil de discutir. “Chamadas de API” vira complicado com retries, requisições em lote e falhas. “Minutos” complica com sobreposições. “GB” precisa de uma base clara (GB vs GiB) e um método de medição claro (média vs pico).
Em seguida, defina limites. Seu sistema precisa saber exatamente a qual janela um evento pertence. O uso é contado por hora, por dia, por período de faturamento, ou por ação do cliente? Se um cliente faz upgrade no meio do mês, você divide a janela ou aplica um preço único ao mês inteiro? Essas escolhas orientam como você agrupa eventos e como explica os totais.
Também decida quem possui quais fatos. Um padrão comum com Stripe é: seu app possui eventos brutos e totais derivados, enquanto o Stripe possui faturas e status de pagamento. Essa abordagem funciona melhor quando você não edita o histórico silenciosamente. Você registra correções como entradas novas e mantém o registro original.
Um conjunto curto de não negociáveis ajuda a manter seu esquema honesto:
- Rastreabilidade: cada unidade faturada pode ser vinculada a eventos armazenados.
- Auditabilidade: você pode responder “por que isso foi cobrado?” meses depois.
- Reversibilidade: erros são corrigidos com ajustes explícitos.
- Idempotência: a mesma entrada não pode ser contabilizada duas vezes.
- Propriedade clara: um sistema possui cada fato (uso, preço, faturamento).
Exemplo: se você cobra por “mensagens enviadas”, decida se retries contam, se entregas falhadas contam e qual timestamp prevalece (horário do cliente vs horário do servidor). Escreva isso e codifique em campos de evento e validação, não na memória de alguém.
Um modelo de dados simples para eventos de uso
O faturamento por uso fica mais fácil quando você trata o uso como contabilidade: fatos brutos são append-only e totais são derivados. Essa escolha única previne a maioria das disputas porque você sempre pode explicar de onde veio um número.
Um ponto de partida prático usa cinco tabelas centrais (nomes podem variar):
- customer: id interno do cliente, id do cliente no Stripe, status, metadata básica.
- subscription: id interno da assinatura, id da assinatura no Stripe, plano/preços esperados, timestamps de início/fim.
- meter: o que você mede (chamadas de API, assentos, GB-horas de armazenamento). Inclua uma chave estável do meter, unidade e como agrega (soma, máximo, único).
- usage_event: uma linha por ação medida. Armazene customer_id, subscription_id (se conhecido), meter_id, quantity, occurred_at (quando aconteceu), received_at (quando foi ingerido), source (app, import em lote, parceiro) e uma chave externa estável para deduplicação.
- usage_aggregate: totais derivados, normalmente por customer + meter + bucket de tempo (dia ou hora) e período de faturamento. Armazene a quantidade somada mais uma versão ou last_event_received_at para suportar recálculo.
Mantenha usage_event imutável. Se depois você descobrir um erro, escreva um evento compensatório (por exemplo, -3 assentos por um cancelamento) em vez de editar o histórico.
Armazene eventos brutos para auditoria e disputas. Se você não puder armazená-los para sempre, mantenha-os pelo menos pelo seu período de lookback de faturamento mais a janela de reembolso/disputa.
Mantenha totais derivados separados. Agregados são rápidos para faturas e painéis, mas descartáveis. Você deve conseguir reconstruir usage_aggregate a partir de usage_event a qualquer momento, inclusive após um backfill.
Idempotência e estados do ciclo de vida do evento
Dados de uso são ruidosos. Clientes fazem retries, filas entregam duplicatas e webhooks do Stripe podem chegar fora de ordem. Se seu banco de dados não consegue provar “este evento de uso já foi contabilizado”, você acabará cobrando duas vezes.
Dê a cada evento de uso um event_id estável e determinístico e imponha unicidade sobre ele. Não confie apenas em um id autoincremental como identificador. Um bom event_id é derivado da ação de negócio, como customer_id + meter + source_record_id (ou customer_id + meter + timestamp_bucket + sequence). Se a mesma ação for enviada de novo, ela produz o mesmo event_id e o insert se torna um no-op seguro.
Idempotência deve cobrir todo caminho de ingestão, não apenas sua API pública. Chamadas do SDK, imports em lote, jobs de worker e processadores de webhook também são reexecutados. Use uma regra: se a entrada pode ser retryada, ela precisa de uma chave de idempotência armazenada no banco e checada antes de mudar totais.
Um modelo simples de estados de lifecycle torna retries seguros e o suporte mais fácil. Mantenha-o explícito e armazene uma razão quando algo falha:
received: armazenado, ainda não validadovalidated: passa no esquema, cliente, meter e regras de janela de tempoposted: contabilizado nos totais do período de faturamentorejected: ignorado permanentemente (com um código de motivo)
Exemplo: seu worker falha depois de validar mas antes de postar. Ao retryar, ele encontra o mesmo event_id em estado validated, então continua para posted sem criar um segundo evento.
Para webhooks do Stripe, use o mesmo padrão: armazene o event.id do Stripe e marque como processado apenas uma vez, assim entregas duplicadas são inofensivas.
Passo a passo: ingestão de eventos de medição de ponta a ponta
Trate cada evento de medição como dinheiro: valide, armazene o original e então derive totais a partir da fonte de verdade. Isso mantém o faturamento previsível quando sistemas retryam ou enviam dados tarde.
Um fluxo de ingestão confiável
Valide cada evento recebido antes de tocar em quaisquer totais. No mínimo, exija: um identificador de cliente estável, um nome de meter, uma quantidade numérica, um timestamp e uma chave única de evento para idempotência.
Grave o evento bruto primeiro, mesmo que planeje agregar depois. Esse registro bruto é o que você vai reprocessar, auditar e usar para corrigir erros sem adivinhar.
Um fluxo confiável se parece com isto:
- Aceite o evento, valide campos obrigatórios, normalize unidades (por exemplo, segundos vs minutos).
- Insira uma linha de evento de uso bruto usando a chave do evento como constraint única.
- Agregue em um bucket (diário ou por período de faturamento) aplicando a quantidade do evento.
- Se você reporta uso ao Stripe, registre o que foi enviado (meter, quantidade, período e identificadores de resposta do Stripe).
- Registre anomalias (eventos rejeitados, conversões de unidade, chegadas tardias) para auditoria.
Mantenha a agregação repetível. Uma abordagem comum é: inserir o evento bruto em uma transação, então enfileirar um job para atualizar buckets. Se o job rodar duas vezes, ele deve detectar que o evento bruto já foi aplicado.
Quando um cliente perguntar por que foi cobrado por 12.430 chamadas de API, você deve poder mostrar o conjunto exato de eventos brutos incluídos naquela janela de faturamento.
Reconciliando webhooks do Stripe com seu banco de dados
Webhooks são o recibo do que o Stripe realmente fez. Seu app pode criar rascunhos e enviar uso, mas o estado da fatura só se torna real quando o Stripe confirma.
A maioria das equipes foca num pequeno conjunto de tipos de webhook que afetam resultados de faturamento:
invoice.created,invoice.finalized,invoice.paid,invoice.payment_failedcustomer.subscription.created,customer.subscription.updated,customer.subscription.deletedcheckout.session.completed(se você inicia assinaturas via Checkout)
Armazene todo webhook que você receber. Guarde o payload bruto mais o que você observou na chegada: Stripe event.id, event.created, o resultado da verificação de assinatura e o timestamp em que seu servidor recebeu. Esse histórico importa quando você está depurando uma discrepância ou respondendo “por que fui cobrado?”.
Um padrão sólido e idempotente de reconciliação se parece com isto:
- Insira o webhook na tabela
stripe_webhook_eventscom uma restrição única emevent_id. - Se o insert falhar, é um retry. Pare.
- Verifique a assinatura e registre sucesso/falha.
- Processe o evento olhando seus registros internos pelos IDs do Stripe (customer, subscription, invoice).
- Aplique a mudança de estado apenas se ela mover adiante.
Entrega fora de ordem é normal. Use uma regra “max state wins” mais timestamps: nunca mova um registro para trás.
Exemplo: você recebe invoice.paid para a invoice in_123, mas sua linha interna de fatura ainda não existe. Crie uma linha marcada como “vista pelo Stripe” e depois anexe-a à conta certa usando o customer id do Stripe. Isso mantém seu razão consistente sem processar em dobro.
De totais de uso a itens de linha na fatura
Transformar uso bruto em linhas de fatura é principalmente sobre tempo e limites. Decida se precisa de totais em tempo real (dashboards, alertas de gasto) ou apenas na hora do faturamento (faturas). Muitas equipes fazem ambos: gravam eventos continuamente e calculam totais prontos para fatura em um job agendado.
Alinhe sua janela de uso com o período de faturamento do Stripe. Não adivinhe meses do calendário. Use o subscription item’s current billing period start e end, então some apenas eventos cujos timestamps caem dentro daquela janela. Armazene timestamps em UTC e faça a janela de faturamento em UTC também.
Mantenha o histórico imutável. Se você encontrar um erro depois, não edite eventos antigos ou reescreva totais prévios. Crie um registro de ajuste que aponte para a janela original e adicione ou subtraia quantidade. É mais fácil auditar e explicar.
Mudanças de plano e proration são onde a rastreabilidade frequentemente se perde. Se um cliente muda de plano no meio do ciclo, divida o uso em sub-janelas que batam com o intervalo ativo de cada preço. Sua fatura pode incluir duas linhas de uso (ou uma linha mais um ajuste), cada uma vinculada a um preço e intervalo de tempo específicos.
Um fluxo prático:
- Puxe a janela da fatura a partir do period start e end do Stripe.
- Agregue eventos de uso elegíveis nessa janela e preço.
- Gere itens de linha da fatura a partir do total de uso mais quaisquer ajustes.
- Armazene um calculation run id para que você possa reproduzir os números depois.
Backfills e dados tardios sem quebrar a confiança
Dados de uso tardios são normais. Dispositivos ficam offline, jobs em lote atrasam, parceiros reenviam arquivos e logs são reproduzidos após uma queda. O importante é tratar backfills como trabalho de correção, não como uma forma de “fazer os números caberem”.
Seja explícito sobre de onde os backfills podem vir (logs da aplicação, exports do warehouse, sistemas parceiros). Registre a fonte em cada evento para que você possa explicar por que ele chegou tarde.
Ao fazer backfill, mantenha dois timestamps: quando o uso aconteceu (o tempo que você quer faturar) e quando você o ingeriu. Marque o evento como backfilled, mas não sobrescreva o histórico.
Prefira reconstruir totais a partir de eventos brutos em vez de aplicar deltas na tabela de agregados atual. Replays são como você se recupera de bugs sem adivinhar. Se seu pipeline for idempotente, você pode reexecutar um dia, uma semana ou um período inteiro e obter os mesmos totais.
Uma vez que uma fatura exista, correções devem seguir uma política clara:
- Se a fatura não estiver finalizada, recalcule e atualize os totais antes da finalização.
- Se estiver finalizada e subfaturada, emita uma fatura adicional (ou adicione um item de fatura) com descrição clara.
- Se estiver finalizada e superfaturada, emita uma nota de crédito referenciando a fatura original.
- Não mova uso para um período diferente para evitar uma correção.
- Armazene uma razão curta para a correção (reenvio de parceiro, entrega de log atrasada, correção de bug).
Exemplo: um parceiro envia eventos faltantes de 28-29 de janeiro em 3 de fevereiro. Você os insere com occurred_at em janeiro, ingested_at em fevereiro e fonte de backfill “partner”. A fatura de janeiro já foi paga, então você cria uma pequena fatura adicional para as unidades faltantes, com o motivo armazenado junto ao registro de reconciliação.
Erros comuns que causam contagem dupla
Contagem dupla acontece quando um sistema trata “uma mensagem chegou” como “a ação ocorreu”. Com retries, webhooks atrasados e backfills, você precisa separar a ação do cliente do seu processamento.
Os culpados comuns:
- Retries tratados como novo uso. Se cada evento não carrega um action id estável (request_id, message_id) e seu banco não impõe unicidade, você contará duas vezes.
- Tempo do evento misturado com tempo de processamento. Reportar por tempo de ingestão em vez de occurred_at faz eventos tardios caírem no período errado e depois serem contados de novo durante replays.
- Eventos brutos deletados ou sobrescritos. Se você mantém apenas um total corrido, não pode provar o que aconteceu, e reprocessamentos podem inflar totais.
- Ordem de webhooks assumida. Webhooks podem ser duplicados, fora de ordem ou representar estados parciais. Reconcile por IDs de objeto do Stripe e mantenha uma trava de “já processado”.
- Cancelamentos, reembolsos e créditos não modelados explicitamente. Se você só adiciona uso e nunca registra ajustes negativos, vai “consertar” totais com imports e contar de novo.
Exemplo: você registra “10 chamadas de API” e depois emite um crédito de 2 chamadas por uma queda. Se você fizer backfill reenviando todo o uso do dia e também aplicar o crédito, o cliente pode ver 18 chamadas (10 + 10 - 2) em vez de 8.
Checklist rápido antes de entrar em produção
Antes de ativar faturamento por uso para clientes reais, faça uma última verificação dos básicos que previnem bugs caros de faturamento. A maioria das falhas não é “problema do Stripe”. São problemas de dados: duplicatas, dias faltando e retries silenciosos.
Mantenha o checklist curto e aplicável:
- Imponha unicidade em eventos de uso (por exemplo, constraint única em
event_id) e adote uma estratégia única de ids. - Armazene todo webhook, verifique sua assinatura e processe idempotentemente.
- Trate o uso bruto como imutável. Corrija com ajustes (positivos ou negativos), não com edições.
- Rode um job diário de reconciliação que compare totais internos (por cliente, por meter, por dia) com o estado de faturamento do Stripe.
- Adicione alertas para lacunas e anomalias: dias faltando, totais negativos, picos repentinos ou grande diferença entre “eventos ingeridos” e “eventos faturados”.
Um teste simples: escolha um cliente, reexecute a ingestão dos últimos 7 dias e confirme que os totais não mudam. Se mudarem, ainda existe um problema de idempotência ou lifecycle-state.
Cenário de exemplo: um mês realista de uso e faturas
Uma pequena equipe de suporte usa um portal de cliente que cobra $0.10 por conversa atendida. Eles vendem como faturamento por uso com Stripe, mas a confiança vem do que acontece quando os dados ficam bagunçados.
Em 1º de março, o cliente inicia um novo período de faturamento. Cada vez que um agente encerra uma conversa, seu app emite um evento de uso:
event_id: um UUID estável do seu appcustomer_idesubscription_item_idquantity: 1 conversaoccurred_at: horário de fechamentoingested_at: quando você viu pela primeira vez
Em 3 de março, um worker de background retrya após um timeout e envia a mesma conversa novamente. Como event_id é único, o segundo insert vira um no-op e os totais não mudam.
No meio do mês, o Stripe envia webhooks de preview da invoice e depois a invoice finalizada. Seu handler de webhook armazena stripe_event_id, type e received_at, e marca como processado apenas depois que a transação do banco de dados é confirmada. Se o webhook for entregue duas vezes, a segunda entrega é ignorada porque stripe_event_id já existe.
Em 18 de março, você importa um lote tardio de um cliente móvel que estava offline. Ele contém 35 conversas de 17 de março. Esses eventos têm occurred_at mais antigos, mas ainda são válidos. Seu sistema os insere, recalcula totais diários de 17 de março e o uso extra é incluído na próxima fatura porque ainda está dentro do período de faturamento aberto.
Em 22 de março, você descobre que uma conversa foi registrada duas vezes devido a um bug que gerou dois event_id diferentes. Em vez de apagar o histórico, você escreve um evento de ajuste com quantity = -1 e uma razão como “duplicata detectada”. Isso mantém a trilha de auditoria intacta e torna a mudança na fatura explicável.
Próximos passos: implementar, monitorar e iterar com segurança
Comece pequeno: um meter, um plano, um segmento de clientes que você conhece bem. O objetivo é consistência simples — seus números batem com o Stripe mês após mês, sem surpresas.
Construa pequeno, depois endureça
Um rollout inicial prático:
- Defina um formato de evento (o que é contado, em qual unidade, em que tempo).
- Armazene cada evento com uma chave única de idempotência e um status claro.
- Agregue em totais diários (ou horários) para que faturas possam ser explicadas.
- Reconcilie contra webhooks do Stripe em uma rotina agendada, não apenas em tempo real.
- Depois de faturar, trate o período como fechado e encaminhe eventos tardios por um caminho de ajustes.
Mesmo com ferramentas no-code, você pode manter forte integridade de dados se tornar estados inválidos impossíveis: imponha constraints únicas para chaves de idempotência, exija foreign keys para customer e subscription e evite atualizar eventos brutos aceitos.
Monitoramento que te salva depois
Adicione telas de auditoria simples cedo. Elas se pagam na primeira vez que alguém pergunta “por que minha fatura está maior este mês?”. Visões úteis incluem: busca de eventos por cliente e período, ver totais por dia dentro do período, status de processamento de webhooks e revisão de backfills e ajustes com quem/quando/por quê.
Se você está implementando isso com AppMaster (appmaster.io), o modelo se encaixa naturalmente: defina eventos brutos, agregados e ajustes no Data Designer, então use Business Processes para ingestão idempotente, agregação agendada e reconciliação de webhooks. Você ainda ganha um razão real e uma trilha de auditoria, sem escrever todo o encanamento manualmente.
Quando seu primeiro meter estiver estável, adicione o próximo. Mantenha as mesmas regras de lifecycle, as mesmas ferramentas de auditoria e o mesmo hábito: mude uma coisa por vez e verifique de ponta a ponta.
FAQ
Trate isso como um pequeno livro-razão. A parte difícil não é cobrar o cartão; é manter um registro preciso e explicável do que foi contabilizado, mesmo quando eventos chegam atrasados, chegam em duplicidade ou precisam de correções.
Um padrão seguro é: seu banco de dados é a fonte de verdade para eventos brutos de uso e seu status, e o Stripe é a fonte de verdade para faturas e resultados de pagamento. Essa divisão mantém o faturamento rastreável enquanto o Stripe cuida de preços, impostos e cobranças.
Faça com que seja estável e determinístico para que retries produzam o mesmo identificador. Comumente é derivado da ação de negócio real, como customer_id + chave do meter + source_record_id, assim um envio duplicado vira um no-op inofensivo em vez de uso extra.
Não edite ou exclua eventos aceitos. Registre um evento de ajuste compensatório (incluindo quantidade negativa quando necessário) e mantenha o original intacto, para que você possa explicar o histórico depois sem adivinhar o que mudou.
Mantenha eventos brutos de uso como append-only e armazene agregados separadamente como dados derivados que você pode reconstruir. Agregados servem para velocidade e relatórios; eventos brutos servem para auditoria, disputas e reconstrução de totais após bugs ou backfills.
Armazene pelo menos dois timestamps: quando ocorreu e quando foi ingerido, e mantenha a fonte. Se a fatura não estiver finalizada, recalcule antes da finalização; se estiver finalizada, trate como correção clara (cobrança adicional ou crédito) em vez de mover silenciosamente uso para outro período.
Armazene todo payload de webhook que você receber e imponha processamento idempotente usando o event id do Stripe como chave única. Webhooks são frequentemente duplicados ou fora de ordem, então seu handler só deve aplicar mudanças de estado que façam os registros avançarem.
Use o billing period start e end do Stripe para a janela, e divida o uso quando o preço ativo mudar. O objetivo é que cada linha de fatura possa ser ligada a um intervalo de tempo e preço específicos para que os totais permaneçam explicáveis.
Faça com que sua lógica de agregação prove quais eventos brutos foram incluídos e armazene um identificador de execução de cálculo ou metadados equivalentes para poder reproduzir os totais depois. Se reexecutar a ingestão para a mesma janela muda os totais, provavelmente você tem um problema de idempotência ou estado de lifecycle.
Modele os eventos brutos de uso, agregados, ajustes e tabelas de caixa de entrada de webhooks no Data Designer, então implemente ingestão e reconciliação em Business Processes com constraints de unicidade para idempotência. Você pode construir um livro-razão auditável e reconciliação agendada sem escrever todo o encanamento à mão.


