MillionSend Docs

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.

FaixaPlanoPreçoIncluídoLimiteExcedente por 1.000
freeFreeUS$ 0100por dia—
starterStarterUS$ 91.500por dia—
pro_100kProUS$ 20100.000por mêsUS$ 0,30
pro_200kProUS$ 35200.000por mêsUS$ 0,30
scale_500kScaleUS$ 75500.000por mêsUS$ 0,25
scale_1mScaleUS$ 1401.000.000por mêsUS$ 0,20
scale_1_5mScaleUS$ 2001.500.000por mêsUS$ 0,18
scale_2_5mScaleUS$ 3302.500.000por mêsUS$ 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 como queued_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 mesmo 429 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 em free_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 com 452 4.2.2 até 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 quando accepted chega 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:

PlanoCaixasCriadas por horaPega-tudoEndereços compartilhados
Free310NãoSó respondem
Starter2560SimComeçam conversas
Pro, ScaleIlimitadas600SimComeçam conversas
  • No limite de caixas, POST /inboxes responde 403 validation_error com 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, responde 429 rate_limit_exceeded com retry-after. A taxa conta as caixas criadas, então excluí-las não zera a conta.
  • O recebimento pega-tudo (receiving_catch_all num domínio) exige o Starter ou acima: no Free, ligá-lo responde 403 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 open salva 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 /usage informa o limite em limits.inboxes (null quando é 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, …), com metadata.millionsend_rung = <faixa> mais plano, volume incluído, período e taxa de excedente.
  • Um medidor, evento emails_over_quota, que soma value por stripe_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

  1. 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.

  2. 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.

  3. O Stripe entrega checkout.session.completed, customer.subscription.* e invoice.* em POST /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 grava teams.plan, plan_quota, current_period_start, current_period_end, stripe_overage_item_id, pending_rung e move_up_pending_until.

  4. 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_invoice e payment_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) vira overage_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).

  5. 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:

  1. o metadata.millionsend_rung do preço;
  2. a lookup key do preço (millionsend_<faixa>_monthly);
  3. o metadata.millionsend_plan do 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 novoplan, plan_quotaplan_status
active, trialingO 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_dueInalterado (carência de pagamento; o Stripe continua tentando); o excedente fica pausado até a fatura ser pagapast_due
unpaid, canceled, incomplete, incomplete_expired, qualquer outrofree, nulounpaid / 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 200 para 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.reconcile busca 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_id e pending_rung (a faixa da última fase de um schedule pendente, quando difere da atual) e move_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:

  1. a linha fixa o contador até onde o evento vai avançar: pending_overage = to onde reported_overage = from e não há fixação (uma linha que outra rodada fixou antes é pulada);
  2. 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 valor to − from;
  3. 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ávelPropósito
STRIPE_SECRET_KEYChave secreta da API do Stripe (sk_test_… / sk_live_…).
STRIPE_WEBHOOK_SECRETSegredo de assinatura do endpoint apontado para /api/billing/webhook (whsec_…).
STRIPE_PORTAL_CONFIGOpcional. Id da configuração do Customer Portal (bpc_…); sem valor usa o padrão da conta.
APP_BASE_URLURL pública do painel; Checkout e Portal retornam para {APP_BASE_URL}/settings/billing.
KMS_KEY_IDChave 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
FlagEfeito
--webhook-urlLocaliza ou cria o endpoint de webhook para essa URL com exatamente os eventos que o handler consome. Omita em desenvolvimento local.
--portalLocaliza ou cria a configuração do Customer Portal e imprime o id para STRIPE_PORTAL_CONFIG.
--app-urlOrigem 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-legacyMove 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-runLê 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_monthly e millionsend_scale_monthly sã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/webhook

O 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.

Nesta página