Trocar de plano

Liste as ofertas disponíveis, calcule o valor e mude a assinatura para um plano mais caro ou mais barato sem cancelar.

Quando usar

Use este guia quando o cliente quer mudar a assinatura para outra oferta de plano do mesmo produto, sem cancelar. Exemplo: sair do plano mensal de R$ 49,90 para o de R$ 99,90.

A API decide o tipo da troca pelo preço:

TipoQuandoO que acontece
UPGRADEO preço da nova oferta é maior que o atual.A diferença é cobrada na hora. O plano muda quando o pagamento é confirmado.
DOWNGRADEO preço da nova oferta é menor que o atual.Nada é cobrado. A troca fica agendada para a próxima renovação.

Ofertas com o mesmo preço não podem ser trocadas entre si. A troca para uma oferta de outro produto também não é aceita.

Para trocar só o cartão, sem mudar o plano, use Trocar o cartão da assinatura.

Todos os valores deste guia estão em reais, como número: 99.9 = R$ 99,90. Veja Valores nas respostas.

Visão geral

O diagrama mostra as três chamadas da troca e os resultados possíveis.

Antes de começar

  • Um token de acesso. Veja Autenticação; os exemplos assumem a variável accessToken.
  • Uma assinatura e o id dela. Veja Assinar um plano.
  • O produto com a troca de plano ligada no painel. A opção Cliente pode trocar de plano? fica no cadastro do produto e nasce desligada.
  • Cada oferta de destino com a opção Plano selecionável pelo cliente? ligada no painel. Ela também nasce desligada.
  • A oferta de destino ativa e com preço diferente do plano atual.

As duas opções só existem no painel

A API não liga nem desliga Cliente pode trocar de plano? e Plano selecionável pelo cliente?. Ajuste as duas no painel antes de integrar.

A opção Cliente pode trocar de plano? fica na aba Geral do plano, em Meus produtos → Assinaturas:

Aba Geral do plano no painel, com a opção Cliente pode trocar de plano ligada e o aviso de que o cliente poderá trocar entre as ofertas marcadas como selecionáveis

Passo a passo

Liste as ofertas disponíveis

Chame GET /subscriptions/{id}/plan-options:

curl "https://api.pagpolar.com/v1/subscriptions/<ID_DA_ASSINATURA>/plan-options" \
  -H "Authorization: Bearer <SEU_TOKEN_DE_ACESSO>"
const response = await fetch(
  'https://api.pagpolar.com/v1/subscriptions/<ID_DA_ASSINATURA>/plan-options',
  {
    headers: { Authorization: `Bearer ${accessToken}` },
  },
);

console.log(response.status, await response.json());

Resposta resumida:

{
  "data": {
    "allow_client_plan_change": true,
    "current_product_price_id": "<ID_DA_OFERTA_ATUAL>",
    "options": [
      {
        "id": "<ID_DA_OFERTA_ATUAL>",
        "title": "Plano Básico",
        "price": 49.9,
        "is_active": true,
        "cycle": "MONTHLY",
        "cycle_interval": 1
      },
      {
        "id": "<ID_DA_NOVA_OFERTA>",
        "title": "Plano Premium",
        "price": 99.9,
        "is_active": true,
        "cycle": "MONTHLY",
        "cycle_interval": 1
      }
    ]
  }
}

Como ler a resposta:

  • allow_client_plan_change é false: o produto não permite troca. A execução vai responder 403. Pare aqui ou ligue a opção no painel.
  • options traz as ofertas do mesmo produto que estão ativas e marcadas como selecionáveis.
  • options sempre inclui a oferta atual, mesmo que ela não seja mais selecionável. Esconda do cliente a opção cujo id é igual a current_product_price_id.
  • A lista não é filtrada por preço. Uma oferta com o mesmo preço da atual aparece, mas a troca para ela responde 400.

Calcule o valor da troca

Mostre ao cliente quanto ele vai pagar antes de trocar. Chame POST /subscriptions/{id}/plan-change/preview com o id da oferta escolhida em new_product_price_id.

Esta chamada não muda nada na assinatura e não cobra nada.

curl -X POST "https://api.pagpolar.com/v1/subscriptions/<ID_DA_ASSINATURA>/plan-change/preview" \
  -H "Authorization: Bearer <SEU_TOKEN_DE_ACESSO>" \
  -H "Content-Type: application/json" \
  -d '{
    "new_product_price_id": "<ID_DA_NOVA_OFERTA>"
  }'
const response = await fetch(
  'https://api.pagpolar.com/v1/subscriptions/<ID_DA_ASSINATURA>/plan-change/preview',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      new_product_price_id: '<ID_DA_NOVA_OFERTA>',
    }),
  },
);

console.log(response.status, await response.json());

Resposta de um upgrade. Os números são ilustrativos:

{
  "data": {
    "type": "UPGRADE",
    "current_product_price_id": "<ID_DA_OFERTA_ATUAL>",
    "current_price": 49.9,
    "new_product_price_id": "<ID_DA_NOVA_OFERTA>",
    "new_price": 99.9,
    "total_days": 30,
    "remaining_days": 15,
    "prorated_credit": 24.95,
    "charge_amount": 74.95,
    "effective_at": "2026-09-15T14:00:00.000Z",
    "current_payment_method": "CREDIT_CARD"
  }
}
CampoO que significa
typeUPGRADE ou DOWNGRADE.
current_price e new_pricePreço da oferta atual e da nova, em reais.
total_daysDias do ciclo atual.
remaining_daysDias que faltam até a próxima cobrança.
prorated_creditCrédito pelos dias que o cliente já pagou e não vai usar.
charge_amountNo upgrade, o valor que será cobrado agora. Mostre este valor ao cliente.
effective_atQuando a troca vale. No upgrade, é o momento do cálculo. No downgrade, é a data da próxima cobrança.
current_payment_methodMeio de pagamento da assinatura: CREDIT_CARD, PIX ou BOLETO.

A conta do upgrade está em Como o valor do upgrade é calculado.

No downgrade, ignore charge_amount

Em DOWNGRADE, charge_amount pode vir maior que zero, mas nada é cobrado. Use só type e effective_at para explicar a troca ao cliente.

Execute a troca

Chame POST /subscriptions/{id}/plan-change. Envie o header Idempotency-Key, uma chave por troca. Veja Idempotência.

CampoObrigatórioO que enviar
new_product_price_idSimid da nova oferta, o mesmo do passo anterior.
payment_choiceNãoOnde cobrar o upgrade. current (padrão): no meio de pagamento atual da assinatura. new_card: num cartão novo, informado em card.
cardSó com new_cardDados do cartão novo. Veja o exemplo abaixo.

Em downgrade, não envie payment_choice nem card: nada é cobrado.

O exemplo cobra o upgrade no meio de pagamento atual. Ele também serve para downgrade:

curl -X POST "https://api.pagpolar.com/v1/subscriptions/<ID_DA_ASSINATURA>/plan-change" \
  -H "Authorization: Bearer <SEU_TOKEN_DE_ACESSO>" \
  -H "Idempotency-Key: 7c1e2f3a-4b5c-4d6e-8f90-1a2b3c4d5e6f" \
  -H "Content-Type: application/json" \
  -d '{
    "new_product_price_id": "<ID_DA_NOVA_OFERTA>"
  }'
import { randomUUID } from 'node:crypto';

const idempotencyKey = randomUUID();

const response = await fetch(
  'https://api.pagpolar.com/v1/subscriptions/<ID_DA_ASSINATURA>/plan-change',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Idempotency-Key': idempotencyKey,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      new_product_price_id: '<ID_DA_NOVA_OFERTA>',
    }),
  },
);

console.log(response.status, await response.json());

A resposta é 200. O conteúdo depende do tipo da troca.

Downgrade:

{
  "data": {
    "type": "DOWNGRADE"
  }
}

Upgrade pago na hora, no cartão:

{
  "data": {
    "type": "UPGRADE",
    "upgrade": {
      "transaction_id": "<ID_DA_VENDA_DA_DIFERENCA>",
      "charge_amount": 74.95,
      "status": "paid"
    }
  }
}

Upgrade pendente, em PIX:

{
  "data": {
    "type": "UPGRADE",
    "upgrade": {
      "transaction_id": "<ID_DA_VENDA_DA_DIFERENCA>",
      "charge_amount": 74.95,
      "status": "pending",
      "pix": {
        "qr_code": "<CODIGO_PIX_COPIA_E_COLA>"
      }
    }
  }
}

Em boleto, no lugar de pix vem boleto com barcode (o código do boleto como o gateway devolveu) e pdf_link (link do PDF).

upgrade.statusO que significaO que fazer
paidO cartão foi cobrado na hora. A assinatura já está no novo plano.Siga para o próximo passo.
pendingA cobrança da diferença foi criada e espera a confirmação do pagamento. A assinatura continua no plano atual.Em PIX, mostre pix.qr_code: ele vale por 1 hora. Em boleto, mostre boleto.pdf_link: o vencimento é em 5 dias. Em cartão, espere a confirmação e não cobre de novo.

Guarde upgrade.transaction_id. É o id da venda da diferença.

Cartão aprovado pode responder pending

Às vezes o cartão é aprovado, mas a troca não pode ser concluída na hora, por exemplo quando o gateway falha ao atualizar o plano da assinatura. Nesse caso a resposta não é erro: vem 200 com upgrade.status: "pending", e a venda da diferença continua em PROCESSING.

A troca é concluída quando o gateway avisa o pagamento: a venda passa para PAID, o plano muda e o webhook TRANSACTION_PAID é enviado. Trate como qualquer upgrade pending. Não peça outro cartão nem repita a troca com outra Idempotency-Key.

Confirme o resultado

Upgrade com status: "paid". A troca já foi feita. Consulte GET /subscriptions/{id} e confira:

  • offer.id é o id da nova oferta;
  • next_billing_amount é o preço da nova oferta;
  • next_billing_at foi recalculado: o novo ciclo começa no dia da troca;
  • status é ACTIVE.

Upgrade com status: "pending". Espere o webhook TRANSACTION_PAID com data.transaction.id igual ao upgrade.transaction_id. Quando ele chegar, consulte a assinatura e confira os mesmos campos do caso paid.

Sem webhook, consulte a venda em GET /sales/{identifier} usando o upgrade.transaction_id. Quando status for PAID, consulte a assinatura.

Se o cliente não pagar, a assinatura continua no plano atual.

Downgrade. Nada muda agora:

  • GET /subscriptions/{id} continua mostrando a oferta e o valor atuais até a renovação.
  • A troca é aplicada quando a cobrança da próxima renovação é paga. A partir daí, offer passa a ser a nova oferta.
  • Na assinatura no cartão, a PagPolar já informa o novo valor ao gateway no momento do agendamento.

A API não mostra o downgrade agendado

Nenhuma resposta da API traz a troca agendada. Guarde no seu sistema a nova oferta e a data de effective_at do passo 2 para mostrar ao cliente.

Duas regras sobre trocas agendadas:

  • Um novo downgrade substitui o downgrade agendado antes.
  • Um upgrade confirmado cancela o downgrade agendado.

Como o valor do upgrade é calculado

O cliente recebe crédito pelos dias do ciclo que já pagou e não vai usar. O crédito é descontado do preço da nova oferta.

EtapaContaExemplo
Dias do ciclo (total_days)Dias entre o início do ciclo atual e a próxima cobrança.30
Dias restantes (remaining_days)Dias entre hoje e a próxima cobrança.15
Crédito (prorated_credit)dias restantes × preço atual ÷ dias do ciclo, arredondado em 2 casas15 × 49,90 ÷ 30 = 24,95
Diferençapreço novo − crédito, nunca abaixo de zero99,90 − 24,95 = 74,95

Depois da diferença, duas regras podem mudar o valor final:

  • Valor mínimo por meio de pagamento. Se a diferença ficar abaixo do mínimo, a cobrança usa o mínimo. PIX: R$ 5,00. Boleto: R$ 10,00. Cartão: valor mínimo configurado pela PagPolar.
  • Taxas do meio de pagamento. O valor final é calculado com as regras de cobrança do meio de pagamento.

Por isso, mostre sempre o charge_amount do preview. Não refaça a conta no seu sistema.

Eventos de webhook deste fluxo

SituaçãoEventoO que fazer
Upgrade pendente e pago depoisTRANSACTION_PAIDCompare data.transaction.id com upgrade.transaction_id. Consulte a assinatura para ver o novo plano.
Upgrade pago na hora no cartãoNenhumUse a resposta 200.
Downgrade agendadoNenhumGuarde a troca no seu sistema.
Renovação em que o downgrade é aplicadoOs eventos de sempre da renovaçãoVeja Ciclo de vida da assinatura.

A criação da cobrança da diferença não envia TRANSACTION_CREATED.

Quando algo dá errado

Além dos erros comuns a todas as rotas, este fluxo pode responder:

PassoSituaçãoRespostaComo resolver
TodosA assinatura não existe na sua conta404 not_found com Assinatura não encontradaConfira o id da assinatura.
2 e 3new_product_price_id ausente ou fora do formato uuid400 invalid_requestEnvie o id de uma oferta de options.
2 e 3A oferta não existe ou é oculta404 not_found com Plano não encontrado.Escolha uma oferta de options.
2A oferta é de outro produto400 invalid_request com O plano selecionado não pertence a este produto.Escolha uma oferta de options.
3A oferta é de outro produto404 not_found com Plano não encontrado.Escolha uma oferta de options.
2A oferta está inativa400 invalid_request com O plano selecionado não está ativo.Ative a oferta ou escolha outra.
2 e 3A oferta não está marcada como selecionável403 forbidden com Este plano não está disponível para troca pelo cliente.Ligue Plano selecionável pelo cliente? na oferta, no painel.
2 e 3A nova oferta tem o mesmo preço da atual400 invalid_request com O novo plano possui o mesmo valor do plano atual.Escolha uma oferta com preço diferente.
2 e 3Upgrade num meio de pagamento que não pode ser cobrado nesta assinatura400 invalid_request com Método de pagamento não disponível para esta assinatura.Confira os meios de pagamento e as taxas da conta com o suporte.
3O produto não permite troca403 forbidden com Este produto não permite troca de plano pelo cliente.Ligue Cliente pode trocar de plano? no produto, no painel.
3payment_choice: "new_card" sem card, ou card incompleto400 invalid_requestEnvie todos os campos de card.
3Upgrade com current numa assinatura no cartão sem cartão salvo400 invalid_request com Cartão da assinatura não encontrado.Repita com payment_choice: "new_card" e uma nova Idempotency-Key.
3Cartão recusado no upgrade400 invalid_request com a mensagem do gateway ou Cartão recusado.Peça outro cartão ao cliente. Veja o aviso abaixo.
3O gateway não criou o PIX ou o boleto do upgrade400 invalid_request com a mensagem do gateway ou Erro ao criar pedido no gatewayEspere alguns minutos e tente de novo.
3Erro inesperado500 internal_errorAntes de repetir, consulte a assinatura e, se houver, a venda da diferença. Veja O erro não garante que nada foi criado.

Upgrade recusado deixa uma venda FAILED

Quando a cobrança do upgrade falha com 400, a venda da diferença pode ficar registrada com status FAILED. A assinatura continua no plano atual. A Idempotency-Key é liberada: repetir com a mesma chave tenta cobrar de novo.

Próximos passos