docs

Assinaturas e billing

Subscription (plano) vs CustomerSubscription (ativa), trial, dunning e o lifecycle real exposto pela API V1.

A Zhex separa dois conceitos que muita plataforma confunde:

  • Subscription — o plano (template). Define preço, intervalo, trial. Vive no produto.
  • CustomerSubscription — a assinatura ativa de um cliente sobre aquele plano. Tem status, datas de billing, ciclo atual.

A analogia: Subscription é menu, CustomerSubscription é pedido em andamento. Você pode ter 1 menu (plano "Premium Mensal") com 10.000 pedidos ativos cobrando R$ 99 a cada 30 dias.

Confundir os dois é onde a maioria dos bugs de billing nasce — atualizar o plano não muda nenhum pedido em andamento, e cancelar um pedido não desativa o menu. Mantenha-os mentalmente separados.

Onde cada um vive

RecursoConfiguraçãoAcesso
Subscription (plano)Dashboard → Produtos → PagamentoHoje só pelo dashboard. Endpoint público no roadmap.
CustomerSubscription (instância)API V1 — GET e cancel/v1/customer_subscriptions/...

A CustomerSubscription é criada automaticamente quando o cliente paga um produto recorrente — você não precisa de endpoint "criar assinatura". Basta cobrar o Product no PaymentIntent; a assinatura nasce no sucesso.

Lifecycle do CustomerSubscription

StatusSignificadoAcesso?
INCOMPLETEPaymentIntent ainda não confirmouNão
TRIALINGEm período de trial gratuitoSim
ACTIVECobrando normalmenteSim
PAST_DUEÚltima cobrança falhou; em retrySim (grace period 7 dias)
UNPAIDEsgotou retriesBloqueie
CANCELEDEncerrada (final)Não

A diferença entre PAST_DUE e UNPAID é onde 30% do churn involuntário se decide. Trate ambos com email automático "atualize seu cartão" — em PAST_DUE, mantenha acesso; em UNPAID, bloqueie mas mantenha login para recuperar.

Plano (configurado no dashboard)

// representação interna
{
  id: 'sub_abc',
  productId: 'prd_xyz',
  name: 'Premium Mensal',
  billingInterval: 'MONTH',         // DAY | WEEK | MONTH | YEAR
  billingIntervalCount: 1,
  trialEnabled: true,
  trialPeriodDays: 7,
  isActive: true,
}

billingInterval × billingIntervalCount cobre todos os casos:

CobrançabillingIntervalbillingIntervalCount
DiáriaDAY1
SemanalWEEK1
QuinzenalWEEK2
MensalMONTH1
BimestralMONTH2
AnualYEAR1

Como nasce uma CustomerSubscription

Hoje, a partir de uma cobrança bem-sucedida em produto recorrente:

  1. Cliente clica em "Assinar Premium" — fluxo passa pelo checkout (hosted ou seu).
  2. Você cria PaymentIntent apontando para o Product recorrente.
  3. Cliente confirma com tok_* (cartão tokenizado).
  4. Quando o intent vira succeeded, a Zhex cria a CustomerSubscription automaticamente, herdando intervalo e trial do plano.
  5. Próximas cobranças acontecem nas datas calculadas pela Zhex.

Você não chama POST /v1/customer_subscriptions — esse endpoint não existe na API V1. A assinatura é side-effect da cobrança.

Trial

Trial é configurado no plano (dashboard). Quando o cliente assina um produto com trial:

  1. CustomerSubscription nasce em TRIALING com trial_start/trial_end.
  2. Sem cobrança durante o trial.
  3. No trial_end, a Zhex dispara o primeiro PaymentIntent automaticamente. Sucesso → ACTIVE. Falha → INCOMPLETE → retry → UNPAID.

trialPeriodDays: 7 é o sweet spot. Menor que 3 não converte; maior que 14 aumenta churn por "esqueci de cancelar" que vira chargeback.

Dunning (cobrança em PAST_DUE)

Quando uma cobrança recorrente falha, a Zhex faz retry automático nas próximas datas de tentativa configuradas para o adquirente. O detalhe exato (intervalos, número de tentativas) é gerenciado internamente; o que você vê é o CustomerSubscription transitando entre ACTIVEPAST_DUEACTIVE (retry funcionou) ou PAST_DUEUNPAID.

Seu trabalho é o lado humano da história: email para o cliente atualizar o cartão.

// no handler de webhook
if (event.type === 'payment_intent.payment_failed') {
  const intent = event.data.object;
  if (intent.customer) {
    await sendEmail(intent.customer, 'update-card', {
      update_url: `https://meusite.com/billing?intent=${intent.id}`,
    });
  }
}

A update_url deve abrir uma tela com @zhexio/zhex-js para tokenizar novo cartão. Bom UX aqui economiza 30%+ de churn involuntário.

Cancelamento

três modos:

Cancel imediato

// SDK ainda não cobre customerSubscriptions; use fetch:
await zhexFetch(`/v1/customer_subscriptions/${id}/cancel`, {
  method: 'POST',
  body: JSON.stringify({ reason: 'requested_by_customer' }),
});

Vira CANCELED na hora. Cliente perde acesso. Não há cobrança proporcional — se ele pagou R$ 99 e cancelou no dia 5, perdeu os outros 25 dias.

Cancel ao fim do ciclo

curl -X POST https://prometheus.zhex.io/v1/customer_subscriptions/csub_…/cancel \
  -H "Authorization: Bearer $ZHEX_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "cancel_at_period_end": true, "reason": "downsell" }'

Mantém ACTIVE até o current_period_end, depois vira CANCELED. É o default mais ético — cliente usa o que pagou.

Cancel em data específica

curl -X POST https://prometheus.zhex.io/v1/customer_subscriptions/csub_…/cancel \
  -H "Authorization: Bearer $ZHEX_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "cancel_at": 1730000000, "reason": "promo_window" }'

cancel_at em Unix timestamp. Mutuamente exclusivo com cancel_at_period_end.

Quem prefere "cancelei e quero refund" usa modo 1 + endpoint separado de refund.

Pause vs Cancel

Pause de assinatura (suspender cobrança por N ciclos sem cancelar) é roadmapPOST /v1/customer_subscriptions/:id/pause ainda não está disponível. Para "férias do cliente" hoje, o caminho é cancelar e recriar quando o cliente voltar.

Webhooks

Hoje a API V1 emite os events de PaymentIntent que são side-effect das cobranças recorrentes:

  • payment_intent.succeeded — cobrança do ciclo N foi bem-sucedida (assinatura entra/permanece em ACTIVE)
  • payment_intent.payment_failed — cobrança falhou (assinatura entra em PAST_DUE)
  • payment_intent.canceled — primeira cobrança cancelada antes da assinatura virar ativa

Para reagir a transições próprias da CustomerSubscription (activated, trial_will_end, canceled, payment_succeeded no ciclo N), consulte o estado via GET /v1/customer_subscriptions/:id no momento que precisar — vocabulário de events específico (customer_subscription.*) está no roadmap.

Idempotência em billing recorrente

Worker de billing vai rodar duas vezes algum dia (deploy, reschedule, retry de fila). A Zhex já gerencia o billing recorrente internamente — você não precisa fazer POST /v1/payment_intents em loop manual.

Quando você fizer cobrança ad-hoc no contexto de uma assinatura (ex.: cobrança extra fora do ciclo), use idempotency key determinística:

const key = `mensalidade-${customerId}-${cycleYear}-${cycleMonth}`;

await zhex.paymentIntents.create(
  {
    amount: 9700,
    currency: 'brl',
    customer: customerId,
    payment_method_types: ['card'],
  },
  { idempotencyKey: key },
);

Mesmo input + mesma key → resposta cacheada. Worker rodou 3x? Cliente cobrado 1x.

Boas práticas

  • payment_description curto e branded. Aparece todo mês no statement: MEUSITE*Premium > ZHX BR PAYMNT 8392.
  • Cancel auto-servido. Se cliente não consegue cancelar pelo seu painel, ele cancela via cartão = chargeback. Você perde a disputa e a fee.
  • PAST_DUE mantém acesso. Bloquear na 1ª falha é antipático — boa parte dos PAST_DUE resolvem na 2ª tentativa (saldo voltou, cartão atualizado). Bloqueie só em UNPAID.
  • Não delete CustomerSubscription. Sempre cancele. Histórico é receita LTV, contábil, reconciliação.

Próximos passos

Esta página foi útil?

Atualizado em

Nesta página