Cobrança (implantações hospedadas)
Como planos, Stripe Checkout e o webhook se encaixam em uma implantação hospedada do MillionSend, e como provisionar uma conta Stripe para ela.
A cobrança só existe quando IS_CLOUD=true. Uma instância auto-hospedada
não tem planos, limites de envio, aba de Cobrança nem rota de webhook — nunca
precisa de uma chave do Stripe. Esta página é para operar uma implantação
hospedada.
Planos
Uma tabela de planos, do mais barato ao mais caro. Free e Starter limitam os envios por
dia UTC; Pro e Scale incluem um volume mensal por período de cobrança do
Stripe e podem cobrar excedente além dele. O Free guarda até 1.000 contatos;
segmentos e tópicos são ilimitados em todos os planos, e contatos também a
partir do Starter. Domínios de envio são limitados a 3 no Free e 10 no
Starter, ilimitados acima. Em qualquer um desses limites a API responde
403 plan_limit_reached ("Your plan allows up to 1000 contacts") e o painel
mostra a mesma frase. Uma equipe adiciona até 10 domínios por hora, ou 100 no
Scale (DOMAIN_CREATES_PER_HOUR); a contagem zera a cada hora cheia (UTC) e,
passando disso, o POST /domains responde 429 rate_limit_exceeded com
retry-after. Apagar um domínio não devolve a vaga dele na hora. Caixas de entrada são limitadas a 3 no Free e 25 no
Starter, ilimitadas acima (veja Caixas de entrada).
Uma pessoa pode ser dona de até 3 equipes no Free, 5 no Starter, 10 no Pro e
15 no Scale, pelo melhor plano entre as equipes de que ela é dona
(PLAN_TEAM_LIMIT); participar da equipe de outra pessoa não conta. No Free,
toda equipe que a pessoa criou nos últimos 30 dias
(TEAM_CREATE_WINDOW_DAYS) também conta, inclusive as excluídas, então
excluir uma equipe não libera vaga. Acima do limite, o painel recusa a nova
equipe e diz o motivo.
| Faixa | Plano | Preço | Incluído | Limite | Excedente por 1.000 |
|---|---|---|---|---|---|
free | Free | US$ 0 | 100 | por dia | — |
starter | Starter | US$ 9 | 1.500 | por dia | — |
pro_100k | Pro | US$ 20 | 100.000 | por mês | US$ 0,30 |
pro_200k | Pro | US$ 35 | 200.000 | por mês | US$ 0,30 |
scale_500k | Scale | US$ 75 | 500.000 | por mês | US$ 0,25 |
scale_1m | Scale | US$ 140 | 1.000.000 | por mês | US$ 0,20 |
scale_1_5m | Scale | US$ 200 | 1.500.000 | por mês | US$ 0,18 |
scale_2_5m | Scale | US$ 330 | 2.500.000 | por mês | US$ 0,16 |
A tabela de planos é PLAN_RUNGS em packages/core/src/plans.ts; Checkout, painel,
GET /usage e os e-mails de conta leem tudo de lá. O relatório de migração da
CLI mantém uma cópia em packages/cli/src/report.ts (ela roda sozinha contra
qualquer instância), então uma mudança na tabela de planos é espelhada lá à mão. A linha da equipe
carrega plan e, em plano mensal, plan_quota (o volume incluído que ela
comprou); juntos, eles nomeiam a faixa.
Limites diários (Free, Starter)
O contador é o dia UTC. Os envios continuam passando até 50% além do limite
antes de ficarem em espera, para um dia movimentado não ser cortado no limite; os
e-mails acima desse teto ficam em espera como queued_quota e o job
quota.drain, a cada 15 minutos, libera-os após a meia-noite UTC. A API
responde 429 daily_quota_exceeded só quando a fila de espera está cheia.
Os proprietários recebem quota.warning em 80% do limite, quota.reached no limite
e quota.paused quando os envios começam a ficar em espera, uma vez por dia UTC;
um upgrade de plano libera o que estava em espera em poucos minutos. Um
envio agendado conta no dia UTC em que sai: o próprio dia, ou hoje para um
scheduled_at já passado (ele sai na hora). Reagendá-lo para outro dia conta
também nesse dia, e o dia que ele deixa mantém a contagem; se esse dia estiver
cheio, o e-mail fica em espera até o drain achar espaço.
No Free, o limite é da pessoa, não da equipe: todas as equipes Free que uma
pessoa criou dividem uma única contagem diária, guardada pela pessoa que as
criou (teams.pool_owner_id, gravado na criação da equipe) em
free_pool_usage, então ela continua em 100 por dia, com a mesma tolerância de
50%, não importa quantas equipes a pessoa tenha. Os usage_counters de cada
equipe continuam contando para as métricas dela. Os envios de uma equipe
excluída continuam contados, e os avisos de cota saem uma vez só para todas.
As mesmas equipes dividem um só histórico de bounces e reclamações (veja
Proteções), que conta também os que
chegam depois de uma equipe ser excluída, cada reclamação no dia em que o
e-mail foi enviado. Equipes criadas antes de
a pessoa ser gravada recebem esse dado quando o worker inicia: o usuário
indicado na linha team.created do log de auditoria delas ou, sem ela, o
primeiro proprietário. Nessa hora, uma equipe Free leva para a contagem
compartilhada os números dos últimos 7 dias, então o histórico de bounces e
reclamações dela e a contagem do dia continuam valendo.
No Free, os e-mails para provedores que informam reclamações de spam também
saem para no máximo 10 desses destinatários de uma vez e depois 10 por hora,
somando todas as equipes Free que a pessoa criou (FREE_BURST, guardado por
pessoa em free_pool_bursts). As reclamações chegam de minutos a dias depois
do envio, então uma pausa pode agir logo nas primeiras, antes de todos os
e-mails do dia saírem. Cada destinatário desses conta, inclusive os em Cco. Um
e-mail esperando a vez fica queued até o horário que ele reservou e confere
de novo o plano da equipe pelo menos uma vez por hora, então, se a equipe sair
do Free, ele sai na próxima verificação; um e-mail que chega à vez dele com
mais de uma hora de atraso, porque ficou parado numa pausa ou foi reagendado,
reserva uma nova vez. A vez segue a ordem em que os e-mails chegam ao worker,
então um e-mail enviado depois de uma transmissão espera atrás dos e-mails
dela. Destinatários no
Gmail, no iCloud e na T-Online, que não informam reclamações, e no simulador
de caixas do SES nunca contam, então um e-mail só para eles nunca espera, e os
e-mails de conta também não.
Volumes mensais (Pro, Scale)
O contador é o período de cobrança do Stripe — a tabela usage_periods,
chaveada pelo current_period_start da equipe — sem tolerância. O que acontece
no volume incluído depende da chave excedente em Cobrança, ligada por
padrão (o cliente a desliga lá):
- Excedente desligado: a API recusa com
429 monthly_quota_exceeded("Monthly sending quota exceeded with overage off or paused for a past-due payment; the period renews on<data>"); nada fica em espera pela API. As transmissões ainda deixam o que sobra em espera comoqueued_quota, e o drain o reconfere contra o período a cada rodada: sai quando o período renova, quando o excedente é ligado ou quando o plano sobe. - Excedente ligado: os envios além do volume incluído são reportados a um
medidor do Stripe e cobrados por 1.000 na taxa da faixa, na próxima fatura
(veja o cron de excedente) — até um teto rígido de
5× o volume incluído (
OVERAGE_HARD_CAP), para uma integração descontrolada ou uma chave roubada nunca gerar uma conta sem fim. Nesse teto a API recusa com o mesmo429 monthly_quota_exceeded("Monthly sending quota exceeded: sends stop at 5 times the included volume even with overage on; the period renews on<data>") até o período renovar.
Enquanto o pagamento de uma renovação está pendente, o excedente fica pausado, seja qual for a posição da chave: a equipe mantém o volume incluído durante as novas tentativas do Stripe, e o que passar dele é recusado ou fica em espera como com o excedente desligado, até a fatura em aberto ser paga. O e-mail de pagamento recusado, a página de cobrança e os e-mails de cota avisam disso, e a fatura paga libera as transmissões em espera em poucos minutos.
Os proprietários recebem quota.warning em 80% e quota.reached em 100% do volume
incluído, uma vez por período; o e-mail de limite atingido diz se os envios
agora cobram excedente ou são recusados. Não há quota.paused em planos
mensais. Um envio agendado conta no período em que foi aceito.
Em todo plano, um domínio de envio com menos de 30 dias também tem o seu
próprio limite diário (veja Domínios).
Uma mudança de plano nunca o libera, e o que ele segura fica em espera como
queued_quota sem contar no plano até o dreno liberá-lo.
E-mails recebidos
Equipes que recebem e-mail são contadas uma vez por
mensagem, não por destinatário, em usage_counters.received e
usage_counters_hourly.received e, em plano mensal, em
usage_periods.received. O custo vem de
receivingRule em packages/core/src/receiving/rcpt.ts:
- Free: o e-mail recebido gasta o contador de envios do dia
(
usage_counters.accepted, e a contagem compartilhada da pessoa que criou a equipe emfree_pool_usage), então envios e e-mails recebidos dividem a cota e a tolerância de 50% em todas as equipes Free que essa pessoa criou, e os avisos de cota contam os dois. No teto, o MX adia e-mails novos com452 4.2.2até a meia-noite UTC. - Starter: grátis até o dobro da cota diária (3.000) por dia UTC, contado
à parte dos envios; além disso,
452. - Pro, Scale: grátis até o dobro do volume incluído por período. Além
disso, cada décimo e-mail recebido soma um e-mail a
usage_periods.accepted, que o cron de excedente cobra como um envio. Depois da franquia, o e-mail recebido é adiado quandoacceptedchega ao teto do período: 5× o volume com o excedente ligado, o próprio volume com ele desligado ou pausado por um pagamento pendente.
O MX pergunta a cada RCPT TO, então, se a equipe estiver sem espaço, a
mensagem é adiada antes de ser enviada; o remetente tenta de novo. Instâncias
auto-hospedadas contam os e-mails recebidos e nunca os limitam.
Caixas de entrada
As caixas de entrada em si não custam nada; o que
elas recebem é contado como acima. O plano limita quantas uma equipe mantém e
com que rapidez as cria, a partir de PLAN_INBOX_LIMIT e
INBOX_CREATES_PER_HOUR em packages/core/src/inboxes/limits.ts:
| Plano | Caixas | Criadas por hora | Pega-tudo | Endereços compartilhados |
|---|---|---|---|---|
| Free | 3 | 10 | Não | Só respondem |
| Starter | 25 | 60 | Sim | Começam conversas |
| Pro, Scale | Ilimitadas | 600 | Sim | Começam conversas |
- No limite de caixas,
POST /inboxesresponde403 validation_errorcom a mensagem do Resend, "You have reached the inbox limit of your plan. Upgrade to add more.", e o painel desativa Criar caixa de entrada com o mesmo conselho. Acima da taxa por hora, responde429 rate_limit_exceededcomretry-after. A taxa conta as caixas criadas, então excluí-las não zera a conta. - O recebimento pega-tudo (
receiving_catch_allnum domínio) exige o Starter ou acima: no Free, ligá-lo responde403 plan_limit_reached("Catch-all receiving needs a paid plan"). Ele continua ligado num downgrade, e foi ligado em todo domínio que já recebia e-mail antes de as caixas existirem. - Os endereços do
domínio compartilhado
começam conversas num plano pago, quando a instância permite, e no Free só
respondem dentro das próprias conversas. O plano é lido a cada envio:
depois de um downgrade, uma regra
opensalva vale como só respostas, sem ser reescrita, e volta a valer depois de um upgrade. As caixas compartilhadas contam no limite de caixas até serem excluídas, por você ou depois de 90 dias sem uso. GET /usageinforma o limite emlimits.inboxes(nullquando é ilimitado), e o cartão de cobrança o mostra como "6 de 25".- A análise de injeção de prompt dos e-mails recebidos tem contagem própria
(
usage_counters.inbound_judged), separada das amostras do monitoramento de conteúdo dos envios, com os mesmos valores diários: 600 por equipe e 50.000 por instância, mais 200 por caixa. O e-mail recebido nunca gasta as cotas de amostragem dos envios.
Instâncias auto-hospedadas não têm limite de caixas nem taxa de criação, o pega-tudo vale para qualquer domínio, e toda equipe conta como paga para os endereços compartilhados: o que eles enviam depende das configurações de quem opera a instância.
O modelo no Stripe
- Um produto por plano pago (Starter, Pro, Scale), localizado por
metadata.millionsend_plan. - Um preço recorrente por faixa, lookup key
millionsend_<faixa>_monthly(millionsend_pro_100k_monthly, …), commetadata.millionsend_rung = <faixa>mais plano, volume incluído, período e taxa de excedente. - Um medidor, evento
emails_over_quota, que somavalueporstripe_customer_id. - Um preço de excedente medido por faixa mensal, lookup key
millionsend_<faixa>_overage, nesse medidor, cobrado por 1.000 e-mails arredondando para cima (transform_quantity: { divide_by: 1000, round: "up" }).
Os preços são localizados por lookup key, nunca por id de preço, então o
mesmo build roda em qualquer conta Stripe (teste ou produção) sem configurar
preços por ambiente. Uma assinatura em uma faixa mensal carrega o preço da
faixa e o preço medido dela como segundo item desde o Checkout (o id do item
fica em teams.stripe_overage_item_id); o item medido só cobra o que o worker
reporta, então a chave de excedente do cliente é uma simples flag na
linha, teams.overage_enabled, que é o que toda superfície de envio lê,
pausada enquanto teams.plan_status é past_due.
O fluxo
-
Um proprietário ou administrador abre Configurações → Cobrança e escolhe uma faixa. O servidor cria o Customer no Stripe para a equipe (uma única vez, guardado na equipe com
metadata.team_id) e redireciona para o Stripe Checkout com o preço dessa faixa. -
O Checkout coleta pagamento, endereço e id fiscal (imposto automático ligado). O Stripe redireciona de volta para
/settings/billing. O redirecionamento não muda nada — a página só consulta por alguns segundos. -
O Stripe entrega
checkout.session.completed,customer.subscription.*einvoice.*emPOST /api/billing/webhook. O handler verifica a assinatura no corpo bruto, registra o id do evento (duplicatas são confirmadas e ignoradas), busca a assinatura de novo no Stripe e só então gravateams.plan,plan_quota,current_period_start,current_period_end,stripe_overage_item_id,pending_rungemove_up_pending_until. -
Trocar de faixa acontece no painel (
billing.changePlan), e a direção decide quando:- Subir espera o próprio pagamento: os itens da assinatura são
atualizados — o item do plano para o preço da nova faixa, o item medido
reprecificado para uma faixa mensal depois de reportar o uso — com
proration_behavior: always_invoiceepayment_behavior: pending_if_incomplete, então a diferença proporcional e o excedente até ali são faturados e cobrados na hora, e o novo volume só vale depois que essa fatura é paga. Quando o banco recusa ou pede autenticação a faixa continua a mesma, mas uma descida agendada antes já foi cancelada: o Stripe segura a atualização por 23 horas, o painel leva o cliente à página hospedada da fatura, e pagar lá aplica a troca (o webhook grava a linha); senão o Stripe anula a fatura. Outra subida, ou excluir a equipe, anula uma fatura em espera antes de reportar qualquer coisa, para que ela não possa ser paga depois. Os envios já aceitos dentro do volume antigo são marcados como acertados na linha do período, então a nova faixa nunca os cobra como excedente. - Descer vale ao fim do período, sem rateio e sem reembolso: um
subscription schedule do Stripe é criado a partir da assinatura (ou o
pendente é reaproveitado) com duas fases — os itens atuais até
current_period_end, depois os itens da nova faixa — e a linha do plano não muda até o webhook aplicar a troca de fase. Até lá, a página de cobrança mostra "Muda para X em<data>" com um botão Manter<atual>: escolher a faixa atual libera o schedule, e uma subida posterior também.
A chave de excedente (
billing.setOverage) viraoverage_enabled; desligar reporta antes o que ainda não foi reportado. Uma assinatura anterior à tabela de planos atual não tem item medido; ligar o excedente o adiciona (desligar e ligar de novo, já que a chave começa ligada). - Subir espera o próprio pagamento: os itens da assinatura são
atualizados — o item do plano para o preço da nova faixa, o item medido
reprecificado para uma faixa mensal depois de reportar o uso — com
-
Gerenciar cobrança abre o Customer Portal do Stripe para forma de pagamento, faturas, id fiscal e cancelamento ao fim do período (o portal pede o motivo do cancelamento). Trocas de plano não são oferecidas lá: o portal do Stripe não consegue atualizar uma assinatura com mais de um item, e uma faixa mensal tem dois.
Excluir uma equipe cancela a assinatura na hora com uma fatura final
(invoice_now): o excedente do período é reportado antes, e a fatura o cobra
junto com qualquer valor proporcional ainda pendente. O Stripe deixa essa
fatura em rascunho por cerca de uma hora, então um evento de medidor ainda em
processamento entra nela, e tenta de novo uma cobrança recusada no calendário
de sempre. O tempo não usado não é reembolsado. A exceção é a equipe suspensa
por fraude: a assinatura termina sem fatura final, já que tudo o que ela
pagou nos últimos 120 dias é reembolsado.
Uma equipe não pode comprar nem subir de plano enquanto estiver suspensa
(por qualquer motivo) ou tiver uma sinalização aberta em Confiança e
segurança que um operador abriu ou reabriu, de qualquer tipo:
billing.checkout, uma subida em billing.changePlan e ligar o excedente
respondem FORBIDDEN (upgrades_held), e a página de cobrança diz que não
é possível subir de plano, sem dizer por quê. Uma sinalização automática
não bloqueia nada: ela só pede que um operador olhe, e o bloqueio do
monitor de conteúdo por abuso é uma suspensão. Reativar a equipe encerra a
sinalização que a suspensão abriu e deixa qualquer outra sinalização aberta
como está. Descer, manter a faixa atual, desligar o
excedente e Gerenciar cobrança (onde fica o cancelamento) continuam
abertos. A configuração que o --portal cria não oferece troca de plano; a
configuração padrão da conta pode oferecer, então defina
STRIPE_PORTAL_CONFIG.
As colunas de plano são escritas a partir de uma assinatura buscada no Stripe — pelo handler do webhook e pelos dois procedimentos do painel acima — nunca por um redirecionamento, uma chamada do cliente ou um payload de evento aceito sem verificação.
Regras de direito ao plano
A faixa deriva da assinatura buscada de novo no Stripe no momento do webhook, então entregas fora de ordem convergem para o estado atual do Stripe. O item não medido da assinatura nomeia a faixa, nesta ordem:
- o
metadata.millionsend_rungdo preço; - a lookup key do preço (
millionsend_<faixa>_monthly); - o
metadata.millionsend_plando produto, caindo na primeira faixa desse plano — é assim que os dois preços vendidos antes da tabela de planos atual resolvem (millionsend_pro_monthly→pro_100k,millionsend_scale_monthly→scale_500k).
| Status da assinatura buscada de novo | plan, plan_quota | plan_status |
|---|---|---|
active, trialing | O plano e o volume incluído da faixa (plan_quota é nulo em uma faixa diária). Preço desconhecido: registrado em log, nada muda. | o mesmo |
past_due | Inalterado (carência de pagamento; o Stripe continua tentando); o excedente fica pausado até a fatura ser paga | past_due |
unpaid, canceled, incomplete, incomplete_expired, qualquer outro | free, nulo | unpaid / canceled / incomplete / canceled |
Regras adicionais:
- Um status sem direito para uma assinatura diferente da guardada na equipe é ignorado, então o fim de uma assinatura substituída nunca revoga a atual.
- Eventos de um customer sem equipe, ou tipos que o handler não consome, são
registrados e respondidos com
200para o Stripe parar de reenviar. - Stripe inacessível ou falha no banco lançam erro; a linha do evento é desfeita e a nova tentativa do Stripe é processada normalmente.
billing.reconcilebusca de novo no Stripe a assinatura de toda equipe assinante uma vez por dia, e mais uma vez a cada boot do worker: um deploy que reinicia o processo com um evento em andamento é alcançado na hora, não horas depois. Um plano que o reconcile move é reportado aos proprietários como o webhook teria feito.stripe_customer_id,stripe_subscription_id,current_period_start,current_period_end,stripe_overage_item_idepending_rung(a faixa da última fase de um schedule pendente, quando difere da atual) emove_up_pending_until(quando o Stripe descarta uma subida que ainda espera o pagamento) são guardados junto com o plano. Um item medido precificado para outra faixa é reapontado para o preço medido da faixa ao ser aplicado.
O cron de excedente
billing.overage roda no worker a cada 10 minutos. Para cada linha de
período de uma equipe com item medido que tem mais envios além do volume
incluído do que o medidor já conhece (accepted − incluído − reported_overage), envia um evento de medidor por equipe e período, em três
comandos, para uma queda em qualquer ponto não custar nada:
- a linha fixa o contador até onde o evento vai avançar:
pending_overage = toondereported_overage = frome não há fixação (uma linha que outra rodada fixou antes é pulada); - o evento de medidor sai com o identificador
<time>:<início do período>:<from>:<to>(o início do período em milissegundos de época) e valorto − from; - a linha alcança:
reported_overage = to, pending_overage = null.
Uma queda entre os dois últimos deixa a fixação, então a próxima rodada
reenvia o mesmo to com o mesmo identificador e o Stripe o descarta como
duplicata; uma falha do Stripe também deixa a fixação para a próxima rodada.
Com o excedente desligado nada passa do volume incluído, então não há o que
reportar; com ele ligado nada passa de 5× o volume, então um período cobra no
máximo quatro volumes de excedente. O uso de um período que já terminou é
carimbado um segundo dentro desse período, onde o Stripe o fatura (a fatura
fica em rascunho por cerca de uma hora após o fim do período; linhas com mais
de 35 dias não podem mais ser medidas e vão para o log). Enquanto uma subida
espera o pagamento, o período em curso da equipe fica de fora (a fatura de um
período encerrado não pode esperar): o Stripe não cobra nada reportado
no preço antigo entre o pedido e o pagamento, então os envios da espera são
reportados quando a subida se aplica (contra o volume novo) ou quando o
Stripe a descarta sem pagamento após 23 horas (contra o antigo). O mesmo
relato roda antes de a chave desligar,
antes de uma subida (os envios feitos na faixa antiga acertam na taxa dela),
antes de excluir uma equipe (o cancelamento leva uma fatura final)
e quando o item medido sai da assinatura; os três últimos rodam também
durante uma espera, já que cada um a encerra. O Stripe descarta um evento de
medidor que ainda não contou quando uma subida reprecifica o item medido, e a
contagem leva cerca de um minuto, então uma subida compara antes o excedente
reportado no período com os resumos do medidor no Stripe: enquanto mais de 1%
do volume (no mínimo 1.000 e-mails) ainda não foi contado, ou um passo que o
medidor rejeitou ainda espera novo envio, billing.changePlan recusa com
CONFLICT (overage_in_flight) e o painel pede para tentar de novo em um
minuto. Cada tentativa reporta antes o que foi enviado desde o último relato,
então, enquanto uma equipe envia mais que essa margem por minuto, a subida
espera o envio desacelerar. Excluir uma equipe recusa do mesmo jeito quando o
medidor rejeita o relato que ela faz, já que o cancelamento apagaria as linhas
que o devem.
Ambiente
IS_CLOUD=true exige todas estas no boot (caso contrário o processo se
recusa a iniciar):
| Variável | Propósito |
|---|---|
STRIPE_SECRET_KEY | Chave secreta da API do Stripe (sk_test_… / sk_live_…). |
STRIPE_WEBHOOK_SECRET | Segredo de assinatura do endpoint apontado para /api/billing/webhook (whsec_…). |
STRIPE_PORTAL_CONFIG | Opcional. Id da configuração do Customer Portal (bpc_…); sem valor usa o padrão da conta. |
APP_BASE_URL | URL pública do painel; Checkout e Portal retornam para {APP_BASE_URL}/settings/billing. |
KMS_KEY_ID | Chave AWS KMS para segredos dos tenants (o modo hospedado criptografa com KMS em vez de MASTER_ENCRYPTION_KEY). |
Provisionando uma conta Stripe
Um script idempotente cria tudo que a API consegue criar. Os valores vêm da tabela de planos, não de flags:
STRIPE_SECRET_KEY=sk_test_… pnpm --filter @millionsend/billing provision \
--webhook-url https://app.example.com/api/billing/webhook \
--portal --app-url https://app.example.com| Flag | Efeito |
|---|---|
--webhook-url | Localiza ou cria o endpoint de webhook para essa URL com exatamente os eventos que o handler consome. Omita em desenvolvimento local. |
--portal | Localiza ou cria a configuração do Customer Portal e imprime o id para STRIPE_PORTAL_CONFIG. |
--app-url | Origem do painel: a URL de retorno padrão do portal passa a ser <app-url>/settings/billing. Omita para usar o padrão do Stripe. |
--move-legacy | Move toda assinatura ainda em um preço anterior à tabela de planos atual para a sua faixa (Pro 100K, Scale 500K) na hora, sem rateio, adicionando o item medido da faixa; um desconto na assinatura permanece. Idempotente. |
--dry-run | Lê a conta e imprime o que seria escrito, sem escrever. |
O que ele faz, e por que rodar de novo é seguro:
- Produtos são localizados por
metadata.millionsend_plan(starter/pro/scale), criados com o código do Stripe Tax para SaaS de uso empresarial. - O medidor é localizado pelo nome do evento,
emails_over_quota. - Preços são localizados por lookup key. Um valor diferente na tabela de planos
cria um novo preço, move a lookup key para ele e arquiva o antigo;
assinaturas existentes mantêm o preço antigo (ainda resolvido pelo
metadata), novos checkouts recebem o novo. Só o metadata é atualizado no
lugar. Os preços são
tax_behavior: exclusive. - Preços legados
millionsend_pro_monthlyemillionsend_scale_monthlysão arquivados, não excluídos: assinaturas ainda neles continuam funcionando, resolvidas para a primeira faixa do plano pelo metadata do produto, até cada uma ser movida; só os novos checkouts deixam de vê-los. - Endpoint de webhook é localizado pela URL; listas de eventos que
divergiram são ressincronizadas. O segredo de assinatura é impresso uma
única vez, na criação — o Stripe nunca o devolve de novo. Para rotacionar,
gere um novo no painel do Stripe (Developers → Webhooks → o endpoint → Roll secret)
e copie o novo valor em
STRIPE_WEBHOOK_SECRET. - Configuração do portal é localizada por metadata e tem as
configurações atualizadas: os recursos (histórico de faturas, forma de
pagamento, dados do cliente incluindo id fiscal e cancelamento ao fim do
período com coleta do motivo; atualização de assinatura fica desligada,
veja o fluxo), os links de termos e privacidade do perfil da
empresa (
millionsend.com/terms,/privacy) e, com--app-url, a URL de retorno.
Teste e produção são contas Stripe separadas: rode uma vez com cada chave.
Checklist só pelo painel do Stripe
O script termina imprimindo estes itens; a API não consegue fazê-los:
- Stripe Tax: ative e adicione os registros fiscais das jurisdições em que você vende (Settings → Tax). O Checkout liga o imposto automático, que falha sem isso.
- Perfil da empresa: razão social, e-mail/URL de suporte e o descritor de fatura que aparece no extrato do cartão (Settings → Public details).
- Marca: logo, ícone e cores do Checkout, do portal, das faturas e dos e-mails (Settings → Branding).
- E-mails ao cliente: recibos de pagamento aprovado e avisos de pagamento recusado (Settings → Emails).
- Assinaturas legadas: uma assinatura em um preço arquivado o mantém; mova cada uma para o preço da sua faixa na página da assinatura (sem rateio, ao fim do período).
Migrando uma implantação existente
A migração 0035_pricing_ladder adiciona o valor de plano starter, as
colunas de teams plan_quota, current_period_start,
stripe_overage_item_id, overage_enabled (padrão true) e pending_rung,
e a tabela usage_periods (accepted, reported_overage,
pending_overage). Equipes scale existentes passam a Scale 500K
(plan_quota 500000) e equipes pro a Pro 100K (100000);
current_period_start é preenchido como current_period_end − 1 mês. As
assinaturas delas ficam nos preços legados, resolvidos pelo metadata do
produto, até serem movidas, e não têm item medido, então os envios delas além
do volume incluído passam (a chave começa ligada) mas não cobram nada até um
ser adicionado: desligar e ligar o excedente de novo em Cobrança o adiciona,
e uma subida de faixa também. O contador do período começa vazio: envios
aceitos antes da migração contam no dia em que saíram, não no período.
Testando localmente
Rode o painel com IS_CLOUD=true e a chave secreta de modo de teste, e
encaminhe os eventos do Stripe para ele com a Stripe CLI:
stripe listen --forward-to localhost:3009/api/billing/webhookO stripe listen imprime um segredo whsec_… próprio — coloque-o em
STRIPE_WEBHOOK_SECRET do processo local (não precisa de --webhook-url ao
provisionar). Use o cartão 4242 4242 4242 4242 no Checkout e stripe trigger customer.subscription.deleted para exercitar um downgrade. A rota de
webhook responde 404 quando IS_CLOUD não é true, 400 para assinatura
inválida e 200 para tudo que ela verificou.