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:
| Tipo | Quando | O que acontece |
|---|---|---|
UPGRADE | O preço da nova oferta é maior que o atual. | A diferença é cobrada na hora. O plano muda quando o pagamento é confirmado. |
DOWNGRADE | O 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
iddela. 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:

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 responder403. Pare aqui ou ligue a opção no painel.optionstraz as ofertas do mesmo produto que estão ativas e marcadas como selecionáveis.optionssempre inclui a oferta atual, mesmo que ela não seja mais selecionável. Esconda do cliente a opção cujoidé igual acurrent_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"
}
}| Campo | O que significa |
|---|---|
type | UPGRADE ou DOWNGRADE. |
current_price e new_price | Preço da oferta atual e da nova, em reais. |
total_days | Dias do ciclo atual. |
remaining_days | Dias que faltam até a próxima cobrança. |
prorated_credit | Crédito pelos dias que o cliente já pagou e não vai usar. |
charge_amount | No upgrade, o valor que será cobrado agora. Mostre este valor ao cliente. |
effective_at | Quando a troca vale. No upgrade, é o momento do cálculo. No downgrade, é a data da próxima cobrança. |
current_payment_method | Meio 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.
| Campo | Obrigatório | O que enviar |
|---|---|---|
new_product_price_id | Sim | id da nova oferta, o mesmo do passo anterior. |
payment_choice | Não | Onde cobrar o upgrade. current (padrão): no meio de pagamento atual da assinatura. new_card: num cartão novo, informado em card. |
card | Só com new_card | Dados 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.status | O que significa | O que fazer |
|---|---|---|
paid | O cartão foi cobrado na hora. A assinatura já está no novo plano. | Siga para o próximo passo. |
pending | A 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é oidda nova oferta;next_billing_amounté o preço da nova oferta;next_billing_atfoi 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í,
offerpassa 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.
| Etapa | Conta | Exemplo |
|---|---|---|
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 casas | 15 × 49,90 ÷ 30 = 24,95 |
| Diferença | preço novo − crédito, nunca abaixo de zero | 99,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ção | Evento | O que fazer |
|---|---|---|
| Upgrade pendente e pago depois | TRANSACTION_PAID | Compare data.transaction.id com upgrade.transaction_id. Consulte a assinatura para ver o novo plano. |
| Upgrade pago na hora no cartão | Nenhum | Use a resposta 200. |
| Downgrade agendado | Nenhum | Guarde a troca no seu sistema. |
| Renovação em que o downgrade é aplicado | Os eventos de sempre da renovação | Veja 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:
| Passo | Situação | Resposta | Como resolver |
|---|---|---|---|
| Todos | A assinatura não existe na sua conta | 404 not_found com Assinatura não encontrada | Confira o id da assinatura. |
| 2 e 3 | new_product_price_id ausente ou fora do formato uuid | 400 invalid_request | Envie o id de uma oferta de options. |
| 2 e 3 | A oferta não existe ou é oculta | 404 not_found com Plano não encontrado. | Escolha uma oferta de options. |
| 2 | A oferta é de outro produto | 400 invalid_request com O plano selecionado não pertence a este produto. | Escolha uma oferta de options. |
| 3 | A oferta é de outro produto | 404 not_found com Plano não encontrado. | Escolha uma oferta de options. |
| 2 | A oferta está inativa | 400 invalid_request com O plano selecionado não está ativo. | Ative a oferta ou escolha outra. |
| 2 e 3 | A oferta não está marcada como selecionável | 403 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 3 | A nova oferta tem o mesmo preço da atual | 400 invalid_request com O novo plano possui o mesmo valor do plano atual. | Escolha uma oferta com preço diferente. |
| 2 e 3 | Upgrade num meio de pagamento que não pode ser cobrado nesta assinatura | 400 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. |
| 3 | O produto não permite troca | 403 forbidden com Este produto não permite troca de plano pelo cliente. | Ligue Cliente pode trocar de plano? no produto, no painel. |
| 3 | payment_choice: "new_card" sem card, ou card incompleto | 400 invalid_request | Envie todos os campos de card. |
| 3 | Upgrade com current numa assinatura no cartão sem cartão salvo | 400 invalid_request com Cartão da assinatura não encontrado. | Repita com payment_choice: "new_card" e uma nova Idempotency-Key. |
| 3 | Cartão recusado no upgrade | 400 invalid_request com a mensagem do gateway ou Cartão recusado. | Peça outro cartão ao cliente. Veja o aviso abaixo. |
| 3 | O gateway não criou o PIX ou o boleto do upgrade | 400 invalid_request com a mensagem do gateway ou Erro ao criar pedido no gateway | Espere alguns minutos e tente de novo. |
| 3 | Erro inesperado | 500 internal_error | Antes 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.