> ## Documentation Index
> Fetch the complete documentation index at: https://docs.z2pay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Trocar o plano da assinatura

> Muda o que a assinatura cobra. Para cima vale já, com a diferença rateada; para baixo, na virada do ciclo.

`POST /subscriptions/:id/change-plan`

Faz parte do recurso [Assinaturas](/pt-BR/subscriptions) — o conceito e os dez estados estão lá.

Troca o que a assinatura cobra: outro plano do catálogo em `planId`, ou um conjunto de itens avulsos em `items`. Pelo menos um dos dois — mandando os dois, `planId` vence e `items` é ignorado.

## Quando a troca passa a valer

Depende do preço do destino, e é o ponto que mais surpreende quem integra.

**Plano mais caro vale na hora.** O que resta do ciclo é acertado por rateio: uma fatura separada nasce em aberto, com o crédito do que sobrou do plano atual e o débito do novo. A fatura do período corrente não é tocada — ela já foi emitida.

**Plano mais barato é sempre agendado para a virada.** A assinatura segue no plano atual até o fim do período, e o novo entra no ciclo seguinte. Rebaixar agora exigiria mexer numa fatura que o cliente já recebeu.

<Warning>
  **Pedir `effectiveAt: "now"` num plano mais barato responde `409`.** Não é um caso a contornar: é a regra. **Omita o campo** e o rebaixamento é agendado sozinho — esse é o caminho normal.

  `effectiveAt` não tem valor padrão de propósito. `"now"` significa *"eu quero que valha agora"*, e é esse pedido que a rota recusa num rebaixamento. Se houvesse padrão, todo rebaixamento responderia `409`, inclusive o de quem só mandou `planId`.
</Warning>

## A conta do rateio

O cálculo é por dia, e o dia corrente conta como usado. Numa troca de R$ 100,00 para R$ 129,90 faltando 25 dias de um ciclo de 31:

|                        |                            |
| ---------------------- | -------------------------- |
| crédito do plano atual | −80,65  (`10000 × 25/31`)  |
| débito do plano novo   | +104,76  (`12990 × 25/31`) |
| **cobrado agora**      | **24,11**                  |

`netAmount` traz esse total em centavos. Numa troca agendada ele é zero — não há o que cobrar hoje.

<Warning>
  **A fatura da diferença ainda não existe quando a resposta chega.** O `prorationInvoiceId` é reservado na hora, e a fatura é gravada logo em seguida, de forma assíncrona — normalmente em 100ms a 2s.

  `GET /invoices/{id}` chamado na sequência responde `404`, e o mesmo vale para o `latestInvoiceId` da assinatura, que aponta para ela. Escute o webhook `invoice.created` em vez de ler logo depois da troca.
</Warning>

Para ver a conta **antes** de confirmar, use [simular a troca](/pt-BR/subscriptions/change-plan-preview). Ela devolve o mesmo valor e as duas pernas separadas.

<Note>
  **Rebaixamento que geraria crédito é recusado.** Quando o rateio daria saldo a favor do cliente, a troca responde `409`: crédito de assinatura ainda não existe. Como todo rebaixamento é agendado para a virada, o caso não aparece no fluxo normal.
</Note>

## Cortesia: trocar sem cobrar

`prorationBehavior: "none"` troca na hora e **não cobra a diferença**. Nenhuma fatura é emitida — `prorationInvoiceId` volta `null` e `netAmount`, zero.

Serve para cortesia comercial ou para corrigir um erro seu. Só vale junto de `effectiveAt: "now"` — pedida numa troca agendada, a chamada responde `409`.

## O plano novo vale antes de a fatura ser paga

A troca é aplicada no ato. A fatura da diferença segue a régua de cobrança normal — e **se ela não for paga, a troca não é desfeita**. Quem entra em inadimplência é a assinatura inteira, pelo caminho de sempre.

É deliberado: liberar acesso é reversível, cobrar não é. Segurar o plano novo até o pagamento cair deixaria sem o que comprou justamente quem já pagou, durante o processamento.

## O destino precisa ser compatível

O contrato vigente fixa três coisas, e o plano de destino tem de respeitá-las:

* **moeda** — a mesma da assinatura;
* **recorrência** — mensal continua mensal; não dá para migrar de mensal para anual por aqui;
* **momento de cobrança** — antecipado continua antecipado.

Qualquer divergência responde `409`. Assinatura que não esteja ativa também, assim como contrato com cobrança antecipada de todos os ciclos — nele as faturas futuras já foram emitidas.

<Note>
  **Simular recusa pelos mesmos motivos.** Use [`change-plan/preview`](/pt-BR/subscriptions/change-plan-preview) na tela onde o cliente escolhe o plano: o erro aparece ali, e não na hora de confirmar.
</Note>

## Como saber que há uma troca agendada

O campo `scheduledPlanChange` da assinatura traz `{ planId, effectiveDate }` enquanto houver troca marcada, e `null` quando não houver. Ele aparece no [`GET /subscriptions/{id}`](/pt-BR/subscriptions/get) e no corpo desta rota.

É por ele que se sabe se vale a pena chamar o [desfazer](/pt-BR/subscriptions/change-plan-delete) — e se ele funcionou.

## Desfazer

Enquanto a troca estiver **agendada**, [`DELETE /subscriptions/:id/change-plan`](/pt-BR/subscriptions/change-plan-delete) a apaga. Depois que ela entra em vigor, o caminho de volta é uma nova troca.


## OpenAPI

````yaml openapi/billing.json POST /subscriptions/{id}/change-plan
openapi: 3.0.3
info:
  title: Z2Pay Billing API
  version: 1.0.0
  description: Cobrança recorrente da Z2Pay — planos, assinaturas e faturas
servers:
  - url: https://api.sandbox.z2pay.com/v1
    description: Sandbox
  - url: https://api.z2pay.com/v1
    description: Produção
security: []
tags:
  - name: Invoices
    description: Faturas emitidas pelas assinaturas
  - name: Plans
    description: Planos de cobrança e versões de preço
  - name: Subscriptions
    description: Assinaturas recorrentes
paths:
  /subscriptions/{id}/change-plan:
    post:
      tags:
        - Subscriptions
      summary: Trocar o plano da assinatura
      description: >-
        Troca o que a assinatura cobra, para outro plano do catálogo (`planId`)
        ou para itens avulsos (`items`) — pelo menos um dos dois. Mandando os
        dois, `planId` vence e `items` é ignorado.


        **Quando a troca vale.** Para um plano mais caro ela vale na hora, e o
        que resta do ciclo é acertado por proporção: uma fatura separada nasce
        com o crédito do plano atual e o débito do novo. Para um plano mais
        barato ela é sempre agendada para a virada do ciclo — a fatura do
        período corrente já foi emitida, e rebaixar agora exigiria mexer num
        documento que o cliente já recebeu. Por isso pedir `effectiveAt: "now"`
        num plano mais barato responde `409`; omitir o campo agenda, e é o
        caminho normal.


        **A troca vale antes do pagamento.** O plano novo passa a valer no ato,
        e a fatura da diferença segue a régua de cobrança normal. Se ela não for
        paga, a troca não é desfeita — quem entra em inadimplência é a
        assinatura inteira.


        **A fatura da diferença ainda não existe quando esta resposta chega.** O
        `prorationInvoiceId` é reservado na hora e gravado por um handler
        assíncrono (100ms–2s); lê-lo em `GET /invoices/{id}` na sequência
        responde 404. Escute o webhook `invoice.created`.


        **Cortesia.** `prorationBehavior: "none"` troca na hora sem cobrar a
        diferença. Serve para cortesia comercial ou para corrigir um erro seu, e
        só vale junto de `effectiveAt: "now"` — pedida numa troca agendada,
        responde `409`.


        O destino precisa ser compatível com o contrato vigente: mesma moeda,
        mesma recorrência e mesmo momento de cobrança. Use `POST
        /subscriptions/{id}/change-plan/preview` para ver o valor e as recusas
        antes de confirmar.
      operationId: SubscriptionController_changePlan
      parameters:
        - name: id
          in: path
          required: true
          description: ID da assinatura
          schema:
            type: string
        - name: Idempotency-Key
          in: header
          required: false
          description: Chave única para garantir idempotência da requisição
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                planId:
                  type: string
                  minLength: 1
                  description: >-
                    Plano de destino. Ausente ⇒ o destino é avulso e `items` é
                    obrigatório.
                items:
                  type: array
                  items:
                    oneOf:
                      - type: object
                        properties:
                          priceVersionId:
                            type: string
                            minLength: 1
                            description: >-
                              Versão de preço (Price) que dita unitAmount,
                              currency e recurrence.
                          quantity:
                            type: integer
                            minimum: 0
                            exclusiveMinimum: true
                            default: 1
                            description: Quantidade do item (default 1).
                        required:
                          - priceVersionId
                      - type: object
                        properties:
                          description:
                            type: string
                            minLength: 1
                            maxLength: 200
                            description: Descrição do item avulso (inline).
                          unitAmount:
                            type: integer
                            minimum: 0
                            exclusiveMinimum: true
                            description: Valor unitário por ciclo, em centavos.
                          quantity:
                            type: integer
                            minimum: 0
                            exclusiveMinimum: true
                            default: 1
                            description: Quantidade do item (default 1).
                        required:
                          - description
                          - unitAmount
                  description: >-
                    Itens do destino avulso (referência ou inline). Ignorado
                    quando há planId.
                itemOverrides:
                  type: object
                  additionalProperties:
                    type: integer
                    minimum: 0
                    exclusiveMinimum: true
                  description: Override de quantidade por componente do plano destino.
                effectiveAt:
                  type: string
                  enum:
                    - now
                    - period_end
                  description: >-
                    Quando a troca vale. Ausente ⇒ imediata para plano mais
                    caro, na virada para mais barato. Pedir `now` num plano mais
                    barato é recusado.
                prorationBehavior:
                  type: string
                  enum:
                    - create_prorations
                    - none
                  default: create_prorations
                  description: >-
                    `none` troca agora sem cobrar a diferença (cortesia). Só
                    vale com effectiveAt=now.
      responses:
        '200':
          description: Plano trocado ou troca agendada para a virada
        '400':
          description: >-
            Requisição inválida — algum parâmetro ou campo do corpo não passou
            na validação
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        description: >-
                          Identificador estável do erro. É por ele que você deve
                          ramificar, não pela mensagem.
                      message:
                        type: string
                        description: >-
                          Descrição legível. Pode mudar sem aviso e não deve ser
                          usada em condicional.
                      issues:
                        type: array
                        description: >-
                          Um item por campo rejeitado. Nunca vem vazio: se há
                          400 de validação, há pelo menos um.
                        items:
                          type: object
                          properties:
                            path:
                              type: string
                              description: >-
                                Campo que falhou. Vem vazio quando o erro é do
                                corpo como um todo.
                            message:
                              type: string
                              description: O que há de errado com esse campo.
              example:
                error:
                  code: VALIDATION_ERROR
                  message: Validation failed
                  issues:
                    - path: status
                      message: >-
                        Status inválido. Valores aceitos: pending,
                        waiting_payment, paid, refused, canceled, refunded
                    - path: startDate
                      message: >-
                        Data deve ser ISO 8601 com timezone (ex.:
                        2026-06-24T00:00:00Z)
        '401':
          description: Chave de API ausente, malformada ou inválida
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        description: >-
                          Identificador estável do erro. É por ele que você deve
                          ramificar, não pela mensagem.
                      message:
                        type: string
                        description: >-
                          Descrição legível. Pode mudar sem aviso e não deve ser
                          usada em condicional.
              example:
                error:
                  code: UNAUTHORIZED
                  message: Invalid API key
        '404':
          description: Assinatura ou plano de destino não encontrado
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        description: >-
                          Identificador estável do erro. É por ele que você deve
                          ramificar, não pela mensagem.
                      message:
                        type: string
                        description: >-
                          Descrição legível. Pode mudar sem aviso e não deve ser
                          usada em condicional.
              example:
                error:
                  code: NOT_FOUND
                  message: Subscription not found
        '409':
          description: >-
            Assinatura não ativa, contrato com cobrança antecipada, destino
            incompatível, ou troca imediata pedida para plano mais barato
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        description: >-
                          Identificador estável do erro. É por ele que você deve
                          ramificar, não pela mensagem.
                      message:
                        type: string
                        description: >-
                          Descrição legível. Pode mudar sem aviso e não deve ser
                          usada em condicional.
              example:
                error:
                  code: CONFLICT
                  message: No refundable payment found
        '422':
          description: >-
            Idempotency-Key já usada com um corpo diferente. Use uma chave nova
            para uma operação diferente
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    description: >-
                      Mensagem do conflito de idempotência. Aqui `error` é
                      texto, não objeto — ramifique pelo status HTTP.
              example:
                error: Idempotency key already used with a different request body
      security:
        - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: API Key da Credential (gerada no Backoffice)

````