Trocar o plano da assinatura
curl --request POST \
--url https://api.sandbox.z2pay.com/v1/subscriptions/{id}/change-plan \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"planId": "<string>",
"items": [
{
"priceVersionId": "<string>",
"quantity": 1
}
],
"itemOverrides": {},
"prorationBehavior": "create_prorations"
}
'const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
planId: '<string>',
items: [{priceVersionId: '<string>', quantity: 1}],
itemOverrides: {},
prorationBehavior: 'create_prorations'
})
};
fetch('https://api.sandbox.z2pay.com/v1/subscriptions/{id}/change-plan', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.sandbox.z2pay.com/v1/subscriptions/{id}/change-plan"
payload = {
"planId": "<string>",
"items": [
{
"priceVersionId": "<string>",
"quantity": 1
}
],
"itemOverrides": {},
"prorationBehavior": "create_prorations"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"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)"
}
]
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key"
}
}{
"error": {
"code": "NOT_FOUND",
"message": "Subscription not found"
}
}{
"error": {
"code": "CONFLICT",
"message": "No refundable payment found"
}
}{
"error": "Idempotency key already used with a different request body"
}Troca de plano
Trocar o plano da assinatura
Troca o plano de uma assinatura ativa, para outro plano do catálogo ou para um conjunto de itens avulsos.
POST
/
subscriptions
/
{id}
/
change-plan
Trocar o plano da assinatura
curl --request POST \
--url https://api.sandbox.z2pay.com/v1/subscriptions/{id}/change-plan \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"planId": "<string>",
"items": [
{
"priceVersionId": "<string>",
"quantity": 1
}
],
"itemOverrides": {},
"prorationBehavior": "create_prorations"
}
'const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
planId: '<string>',
items: [{priceVersionId: '<string>', quantity: 1}],
itemOverrides: {},
prorationBehavior: 'create_prorations'
})
};
fetch('https://api.sandbox.z2pay.com/v1/subscriptions/{id}/change-plan', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.sandbox.z2pay.com/v1/subscriptions/{id}/change-plan"
payload = {
"planId": "<string>",
"items": [
{
"priceVersionId": "<string>",
"quantity": 1
}
],
"itemOverrides": {},
"prorationBehavior": "create_prorations"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"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)"
}
]
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key"
}
}{
"error": {
"code": "NOT_FOUND",
"message": "Subscription not found"
}
}{
"error": {
"code": "CONFLICT",
"message": "No refundable payment found"
}
}{
"error": "Idempotency key already used with a different request body"
}POST /subscriptions/:id/change-plan
Faz parte do recurso Assinaturas — 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
O preço do destino determina o momento. 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 é alterada. Plano mais barato é agendado para a virada do ciclo. A assinatura segue no plano atual até o fim do período, e o novo entra no ciclo seguinte.Pedir
effectiveAt: "now" num plano mais barato responde 409. Omita o campo: sem ele, o rebaixamento é agendado para a virada.A conta do rateio
O cálculo é por dia, e o dia corrente conta como usado. Numa troca de R100,00paraR 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.
A fatura da diferença pode não estar disponível imediatamente. O
prorationInvoiceId vem nesta resposta, mas GET /invoices/{id} pode responder 404 nos instantes seguintes — o mesmo vale para o latestInvoiceId da assinatura, que aponta para ela. Escute o webhook invoice.issued para saber quando a fatura existe.Rateio a favor do cliente responde
409. Como todo rebaixamento é agendado para a virada, e uma troca agendada não cobra nada hoje, o caso não aparece no fluxo normal.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.
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, 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, pelo caminho de sempre.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.
409. Assinatura que não esteja ativa também, assim como contrato com cobrança antecipada de todos os ciclos, cujas faturas futuras já foram emitidas.
A simulação recusa pelos mesmos motivos. Use
change-plan/preview na tela onde o cliente escolhe o plano: o erro aparece ali, e não na confirmação.Como saber que há uma troca agendada
O camposcheduledPlanChange da assinatura traz { planId, effectiveDate } enquanto houver troca marcada, e null quando não houver. Ele aparece no GET /subscriptions/{id} e no corpo desta rota.
É por ele que se sabe se há o que desfazer — e se o desfazer funcionou.
Desfazer
Enquanto a troca estiver agendada,DELETE /subscriptions/:id/change-plan a apaga. Depois que ela entra em vigor, o caminho de volta é uma nova troca.Authorizations
API Key da Credential (gerada no Backoffice)
Headers
Chave única para garantir idempotência da requisição
Path Parameters
ID da assinatura
Body
application/json
Plano de destino. Ausente ⇒ o destino é avulso e items é obrigatório.
Minimum string length:
1Itens do destino avulso (referência ou inline). Ignorado quando há planId.
- Option 1
- Option 2
Show child attributes
Show child attributes
Override de quantidade por componente do plano destino.
Show child attributes
Show child attributes
Quando a troca vale. Ausente ⇒ imediata para plano mais caro, na virada para mais barato. Pedir now num plano mais barato é recusado.
Available options:
now, period_end none troca agora sem cobrar a diferença (cortesia). Só vale com effectiveAt=now.
Available options:
create_prorations, none Response
Plano trocado ou troca agendada para a virada