# Trocar de plano

URL: https://staging.pagpolar.com/docs/guias/jornadas/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](/docs/guias/jornadas/trocar-cartao-da-assinatura).

Todos os valores deste guia estão em **reais**, como número: `99.9` = R$ 99,90. Veja [Valores nas respostas](/docs/guias/fundamentos/valores-datas-e-identificadores#valores-nas-respostas).

## Visão geral

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

```mermaid
sequenceDiagram
  autonumber
  participant I as Seu sistema
  participant A as API
  participant G as Gateway
  participant W as Seu servidor de webhook
  I->>A: GET /subscriptions/ID/plan-options
  A-->>I: 200 allow_client_plan_change e options
  I->>A: POST /subscriptions/ID/plan-change/preview
  A-->>I: 200 type, charge_amount e effective_at
  I->>A: POST /subscriptions/ID/plan-change com Idempotency-Key
  alt DOWNGRADE
    A-->>I: 200 type DOWNGRADE, troca agendada
  else UPGRADE pago na hora no cartão
    A->>G: cobra a diferença
    A-->>I: 200 upgrade.status paid, plano já trocado
  else UPGRADE pendente em PIX, boleto, cartão ou troca não concluída na hora
    A-->>I: 200 upgrade.status pending
    G-)A: pagamento confirmado
    A-)W: TRANSACTION_PAID e o plano é trocado
  end
```

## Antes de começar

* Um token de acesso. Veja [Autenticação](/docs/guias/fundamentos/autenticacao#obter-o-token); os exemplos assumem a variável `accessToken`.
* Uma assinatura e o `id` dela. Veja [Assinar um plano](/docs/guias/jornadas/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

1. **Liste as ofertas disponíveis**

   Chame [`GET /subscriptions/{id}/plan-options`](/docs/referencia/assinaturas/list-subscription-plan-options):

   #### cURL

   ```bash
           curl "https://api.pagpolar.com/v1/subscriptions/<ID_DA_ASSINATURA>/plan-options" \
             -H "Authorization: Bearer <SEU_TOKEN_DE_ACESSO>"
           ```

   #### Node.js

   ```js
           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:

       ```json
       {
         "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`.

2. **Calcule o valor da troca**

   Mostre ao cliente quanto ele vai pagar antes de trocar. Chame [`POST /subscriptions/{id}/plan-change/preview`](/docs/referencia/assinaturas/preview-subscription-plan-change) 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

   ```bash
           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>"
             }'
           ```

   #### Node.js

   ```js
           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:

       ```json
       {
         "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](#calculo).

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

3. **Execute a troca**

   Chame [`POST /subscriptions/{id}/plan-change`](/docs/referencia/assinaturas/change-subscription-plan). Envie o header `Idempotency-Key`, uma chave por troca. Veja [Idempotência](/docs/guias/fundamentos/idempotencia).

       | 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

   ```bash
           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>"
             }'
           ```

   #### Node.js

   ```js
           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());
           ```

   #### Cobrar o upgrade num cartão novo

   Envie `payment_choice: "new_card"` e os dados em `card`. Todos os campos de `card` são obrigatórios. `holder_document` é o CPF (11 dígitos) ou CNPJ (14 dígitos) do titular, com ou sem pontuação. Veja [Documento e telefone](/docs/guias/fundamentos/valores-datas-e-identificadores#documento-e-telefone).

           ```json
           {
             "new_product_price_id": "<ID_DA_NOVA_OFERTA>",
             "payment_choice": "new_card",
             "card": {
               "number": "<NUMERO_DO_CARTAO>",
               "holder_name": "MARIA SILVA",
               "holder_document": "<CPF_DO_TITULAR>",
               "exp_month": 12,
               "exp_year": 2030,
               "cvv": ""
             }
           }
           ```

           Numa assinatura no cartão, depois que o upgrade é pago, o cartão novo passa a ser o cartão da assinatura.

   > **Os nomes dos campos são diferentes na troca de cartão**
   >
   > Aqui o objeto se chama `card` e a validade vai em `exp_month` e `exp_year`. Na [troca de cartão](/docs/guias/jornadas/trocar-cartao-da-assinatura), o objeto se chama `credit_card` e a validade vai em `expiration_month` e `expiration_year`.

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

       **Downgrade:**

       ```json
       {
         "data": {
           "type": "DOWNGRADE"
         }
       }
       ```

       **Upgrade pago na hora, no cartão:**

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

       **Upgrade pendente, em PIX:**

       ```json
       {
         "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`.

4. **Confirme o resultado**

   **Upgrade com `status: "paid"`.** A troca já foi feita. Consulte [`GET /subscriptions/{id}`](/docs/referencia/assinaturas/get-subscription) 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`](/docs/webhooks/eventos/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}`](/docs/referencia/vendas/get-sale) 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.

| 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`](/docs/webhooks/eventos/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](/docs/guias/conceitos/ciclo-de-vida-da-assinatura).                   |

A criação da cobrança da diferença **não** envia [`TRANSACTION_CREATED`](/docs/webhooks/eventos/transaction-created).

## Quando algo dá errado

Além dos [erros comuns a todas as rotas](/docs/guias/fundamentos/erros#erros-comuns), 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](/docs/guias/fundamentos/idempotencia#erro-nao-garante). |

> **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

- [Trocar o cartão da assinatura](/docs/guias/jornadas/trocar-cartao-da-assinatura) — Troque o cartão sem mudar o plano.
- [Ciclo de vida da assinatura](/docs/guias/conceitos/ciclo-de-vida-da-assinatura) — Entenda os status e as renovações.
- [Idempotência](/docs/guias/fundamentos/idempotencia) — Repita a troca sem cobrar duas vezes.
