02Operação5 min

O Stripe já quebrou meu build cinco vezes e eu já deixo isso na agenda

Subir a versão da API do Stripe não é um incidente. É manutenção recorrente, com danos colaterais bem previsíveis, e convém orçá-la.

Por Salva Castells

Imagem de capa: Bernd Schwabe in Hannover, CC BY-SA 4.0.
Neste post

O build cai com um erro de tipos em lib/stripe.ts. A linha é esta:

Type '"2025-06-30.basil"' is not assignable to type '"2025-07-30.basil"'.

Não toquei em nada de pagamentos. O que aconteceu é que o bun update trouxe uma versão nova do SDK do Stripe, e o pacote carrega dentro um literal com a versão de API que suporta. No dia em que esse literal muda, tudo o que passa apiVersion explicitamente deixa de compilar. Isso já me aconteceu cinco vezes em pouco mais de um ano, em quatro projetos diferentes.

A primeira vez foi um incidente. Na quinta já era algo que eu esperava.

O mesmo commit, em quatro repositórios, com um ano de distância

A sequência está escrita no histórico e é quase cômica de tão repetitiva. 2025-06-30.basil para 2025-07-30.basil em agosto. Um mês depois, 2025-07-30.basil para 2025-08-27.basil, em dois repositórios no mesmo dia. Meses mais tarde, 2025-11-17.clover para 2026-01-28.clover num SaaS de geração de imagens. E pouco depois, outra vez, no boilerplate que uso para começar projetos.

As mensagens de commit são idênticas: "update Stripe API version to X". O diff também. E aí está a primeira lição, que não tem nada a ver com o Stripe: a mesma string estava repetida em quatro lugares do mesmo repositório. Uma no webhook, e mais três em createCheckout, createCustomerPortal e findCheckoutSession, cada uma com seu próprio new Stripe(secretKey, { apiVersion, typescript: true }). Quatro linhas idênticas que é preciso mudar na mão a cada vez, e quatro lugares onde esquecer uma.

Tive a cara de pau de deixar um comentário ao lado de cada uma:

const stripe = new Stripe(secretKey, {
  apiVersion: "2026-01-28.clover", // TODO: update this when Stripe updates their API
  typescript: true,
});

Um TODO repetido quatro vezes é documentação de que você sabe que vai repetir o trabalho e mesmo assim não centralizou.

Os danos colaterais não estão na versão, estão nos tipos

A parte que quebra o build é trivial: você muda a string e compila. O que custa de verdade são as mudanças de forma que vêm escondidas na mesma subida, porque o SDK reescreve seus tipos ao mesmo tempo.

O caso concreto que me comeu uma tarde foi o webhook de invoice.paid. Eu pegava o price ID assim:

const priceIdIP = stripeIP.lines.data[0].pricing?.price_details?.price;

Com a versão nova, esse campo passa a ser string | Price | undefined conforme venha expandido ou não. O TypeScript deixa de engolir a comparação contra p.priceId e, o importante, o runtime começa a comparar um objeto contra uma string: o find sobre a tabela de preços não encontra nada, cai no if (!pricingIP || !priceIdIP) e o pagamento não é aplicado no banco de dados. A cobrança foi feita no Stripe e o usuário não tem seus créditos.

O primeiro conserto foi o óbvio:

const rawPrice = stripeIP.lines.data[0].pricing?.price_details?.price;
const priceIdIP = typeof rawPrice === "string" ? rawPrice : rawPrice?.id;

E tive que mexer nisso de novo no mesmo dia, porque rawPrice?.id continuava sem estreitar bem o tipo em todos os caminhos e compilava mal conforme o modo de checkout. A versão que ficou é mais feia e mais honesta:

const priceIdIP =
  typeof rawPrice === "string"
    ? rawPrice
    : typeof rawPrice === "object" && rawPrice
      ? (rawPrice as { id: string }).id
      : undefined;

O mesmo com os descontos. discounts: couponId ? [{ coupon: couponId }] : [] convive com allow_promotion_codes: true, e aí há uma interação que o Stripe não perdoa: se você passa um cupom fixo, os códigos promocionais do cliente deixam de ser aplicados. Dois commits no mesmo dia para deixar aquilo certo. Nenhum dos dois foi por causa da versão: foram por mexer no checkout.

Por que no fim parei de fixar a versão

Depois do quinto bump fiz o contrário do que diz a ortodoxia. Apaguei o apiVersion dos quatro lugares:

const stripe = new Stripe(secretKey);

Sem apiVersion, o SDK usa a versão que tem compilada dentro. E essa é exatamente a versão contra a qual estão gerados os tipos de TypeScript que você está usando. O descompasso que quebrava o build desaparece por construção: você não pode ter o SDK numa versão e o literal em outra se só há uma fonte.

O argumento clássico para fixá-la é que assim você controla quando o comportamento muda. É verdade, mas deixa de ser quando você fixa a versão e ao mesmo tempo atualiza o SDK todo mês. Ali você não tem duas alavancas: tem uma alavanca e uma string que você sincroniza na mão em quatro arquivos. O pin de verdade vem do lockfile, não do literal.

Se você quiser mesmo pinar a versão de API, o pin tem que estar num único lugar, exportado, e o SDK tem que estar cravado no package.json com versão exata, não com ^. Fixar uma e deixar a outra flutuando é o pior dos dois mundos.

O checklist de quando sai versão nova

  • Atualizar o SDK e o literal ao mesmo tempo, nunca separados. Se forem separados, o erro de tipos aparece num PR que não fala de pagamentos.
  • Buscar em todo o repositório, não no arquivo óbvio. Cinco vezes encontrei uma quinta instância de new Stripe( num lugar de que eu não lembrava.
  • Reler os handlers de webhook antes de dar o build verde por bom. O compilador te avisa do apiVersion, mas um campo que passa de string para objeto pode compilar e falhar em runtime se houver um ?. no meio.
  • Testar invoice.paid com o CLI, não só checkout.session.completed. Os eventos recorrentes usam outro caminho de dados e quebram sem que ninguém perceba até o mês seguinte.
  • Verificar o cupom e o código promocional juntos se a sessão os levar.

Meu erro foi tratar cada bump como uma interrupção. Não é: sai uma versão a cada dois ou três meses e custa meia hora se você lembrar de olhar os webhooks, uma tarde se não lembrar. Isso é manutenção, e manutenção se agenda. O que não se agenda se paga na forma de uma cobrança que entrou no Stripe e não chegou ao banco de dados.