Integração com a Paytix

A cada acontecimento de uma venda (pedido criado, pagamento aprovado, reembolso, carrinho abandonado, assinatura renovada), a Paytix envia um POST com JSON assinado para a URL que você cadastrar. Esta página documenta o que o seu sistema precisa para receber esses eventos: o formato de cada payload, os cabeçalhos, a verificação de assinatura e a política de retentativa.

Comece aqui

  1. Publique um endpoint https que aceite POST e responda 200. Só HTTPS é aceito.
  2. Na tela Webhooks do app, cadastre a URL e escolha os eventos que interessam.
  3. Copie o segredo de assinatura (whsec_…) que aparece no webhook criado e guarde-o como variável de ambiente no seu servidor.
  4. Verifique a assinatura de toda chamada antes de confiar no conteúdo: código pronto na seção Assinatura e segurança.
  5. Clique em Testar e confirme que chegou um evento com "test": true.

O mesmo formato serve n8n, Make, Zapier e qualquer sistema próprio. Todos recebem o payload canônico descrito aqui. As integrações nativas (Utmify, Meta, TikTok) ficam em Integrações e não precisam de webhook.

A entrega

Cada evento é um POST com corpo JSON (UTF-8) e estes cabeçalhos:

CampoTipoDescrição
X-Paytix-EventstringO nome do evento (ex.: order.paid). Dá para rotear sem abrir o corpo.
X-Paytix-Event-IduuidId único do evento, igual ao campo id do corpo. Repete em toda retentativa: use-o para descartar duplicata.
X-Paytix-SignaturestringAssinatura HMAC no formato t=<unix>,v1=<hex>, detalhado na seção seguinte.
Content-Typestringapplication/json
User-AgentstringPaytix-Webhooks/2.0

Regras da entrega

  • Responda 2xx em até 10 segundos. O que passar disso conta como falha. Grave o evento e responda na hora; processe depois, fora da requisição.
  • Redirecionamento (3xx) não é seguido e conta como falha: cadastre a URL final.
  • A resposta do seu servidor fica no log da tela Webhooks, truncada em 2 KB.

Retentativas

Falhou, a Paytix tenta de novo com espera crescente: 10 s, 1 min, 5 min, 30 min, 2 h, 6 h e 12 h (±20% de variação), num total de 7 tentativas em cerca de 21 horas. Erro 4xx não é retentado (endpoint que responde 400 vai responder 400 de novo), com duas exceções: 408 e 429. 5xx, timeout e falha de rede sempre retentam. Esgotadas as tentativas, a entrega aparece como “desistiu” na tela de webhooks, com botão de reenviar.

Saúde do destino: 10 falhas seguidas marcam o webhook como “falhando”; 30 falhas seguidas o desligam e o vendedor recebe um e-mail. Reative-o na tela de webhooks depois de corrigir o endpoint.

Duplicatas e ordem

A mesma entrega pode chegar mais de uma vez, e eventos diferentes podem chegar fora de ordem: uma retentativa de order.created pode aterrissar depois do order.paid. Deduplique pelo id e decida o estado final pelo par occurred_at + status, nunca pela ordem de chegada.

Assinatura e segurança

Toda entrega é assinada com o segredo (whsec_…) do seu webhook, no mesmo formato que a Stripe usa. O cabeçalho X-Paytix-Signature traz t=<timestamp unix>,v1=<assinatura>, onde v1 é o HMAC-SHA256 da string {t}.{corpo cru}. Para verificar:

  1. Separe t e v1 do cabeçalho.
  2. Calcule o HMAC-SHA256 de {t}.{corpo} com o segredo, sobre o corpo cru, byte a byte, antes de qualquer parse.
  3. Compare com v1 em tempo constante.
  4. Recuse se t estiver a mais de 5 minutos do seu relógio. Isso protege contra reenvio de uma chamada capturada.

Node.js

const crypto = require("crypto");

// segredo: o que aparece em "Segredo de assinatura", na tela de webhooks
function confere(corpoCru, cabecalho, segredo) {
  const partes = Object.fromEntries(
    cabecalho.split(",").map((p) => p.split("="))
  );
  const esperado = crypto
    .createHmac("sha256", segredo)
    .update(partes.t + "." + corpoCru)
    .digest("hex");

  // fora de 5 minutos, recuse: protege contra reenvio do mesmo pedido
  if (Math.abs(Date.now() / 1000 - Number(partes.t)) > 300) return false;

  return crypto.timingSafeEqual(
    Buffer.from(esperado), Buffer.from(partes.v1)
  );
}

// no Express, use express.raw(). O corpo tem que ser o texto original
app.post("/paytix", express.raw({ type: "*/*" }), (req, res) => {
  const ok = confere(
    req.body.toString(),
    req.header("X-Paytix-Signature"),
    process.env.PAYTIX_WEBHOOK_SECRET
  );
  if (!ok) return res.sendStatus(401);
  res.sendStatus(200); // responda 2xx rápido; processe depois
});

PHP

<?php
$corpoCru = file_get_contents("php://input");
$cabecalho = $_SERVER["HTTP_X_PAYTIX_SIGNATURE"] ?? "";
$segredo = getenv("PAYTIX_WEBHOOK_SECRET");

parse_str(str_replace(",", "&", $cabecalho), $partes);
$esperado = hash_hmac("sha256", $partes["t"] . "." . $corpoCru, $segredo);

// fora de 5 minutos, recuse
if (abs(time() - (int) $partes["t"]) > 300) { http_response_code(401); exit; }
if (!hash_equals($esperado, $partes["v1"])) { http_response_code(401); exit; }

http_response_code(200);

As chamadas não saem de um IP fixo: a autenticidade vem da assinatura, não da origem. Endereço novo no seu firewall não é sinal de problema.

O envelope

Todo evento tem a mesma casca. O que muda é o conteúdo de data.

CampoTipoDescrição
iduuidIdentificador único do evento. Igual ao X-Paytix-Event-Id e estável entre retentativas.
eventstringO nome do evento. O catálogo está na seção seguinte.
versionnúmeroVersão do formato. Hoje, sempre 1; mudança incompatível virá com número novo.
occurred_atdata UTCQuando o fato aconteceu. Não muda entre retentativas.
sent_atdata UTCQuando esta entrega saiu. Muda a cada retentativa.
testbooleanotrue quando o evento veio do botão “Testar”. Não dispare efeito real para ele.
dataobjetoO conteúdo: eventos order.* preenchem data.order; cart.abandoned preenche data.cart; subscription.* preenche data.subscription. Os demais blocos (items, buyer, seller, coupon, tracking) existem em todos.

Datas são sempre UTC, em ISO 8601. Valores monetários são números JSON em reais (ex.: 297, 267.3), não em centavos. Campo sem valor vem null, nunca some do payload.

Catálogo de eventos

EventoConteúdoQuando dispara
order.createdpedidoPedido criado aguardando pagamento: o Pix ou boleto foi gerado. Cartão aprovado na hora não passa por aqui.
order.paidpedidoPagamento aprovado pelo gateway. É o evento para entregar o produto.
order.refusedpedidoPagamento recusado (cartão). O código da recusa vai em refused_reason.
order.refundedpedidoReembolso feito. Revogue o acesso ao produto.
order.chargebackpedidoChargeback: o comprador contestou a compra no banco.
cart.abandonedcarrinhoO comprador preencheu o e-mail e ficou ~30 minutos sem pagar.
subscription.createdassinaturaAssinatura criada, com a primeira cobrança confirmada pelo gateway.
subscription.renewedassinaturaCobrança recorrente aprovada. A assinatura segue ativa.
subscription.charge_failedassinaturaCobrança recorrente falhou. A assinatura entra em carência (past_due). Não é cancelamento.
subscription.canceledassinaturaAssinatura cancelada. É este evento que revoga o acesso.

Ao cadastrar o webhook você escolhe quais desses eventos recebe. Evento fora da sua lista não é enviado.

Eventos de pedido (order.*)

Um pedido é uma cobrança no gateway: o produto principal e os order bumps são linhas do mesmo pedido, em items. Upsell é uma cobrança separada, então chega como um pedido novo (com items[].kind = "upsell") que herda o tracking do pedido de origem. O data.order.id é estável: o order.refunded de amanhã traz o mesmo id do order.paid de hoje.

data.order

CampoTipoDescrição
iduuidId do pedido na Paytix.
statusstringpending | paid | failed | refunded | chargeback. Acompanha o evento: order.created chega com pending, order.paid com paid, e assim por diante.
payment_methodstringpix | credit_card | boleto
installmentsnúmeroParcelas. 1 em Pix e boleto.
currencystringSempre "BRL".
amounts.subtotalnúmeroValor antes do cupom (total + discount).
amounts.discountnúmeroDesconto do cupom aplicado ao pedido.
amounts.totalnúmeroO que o comprador pagou: principal + order bumps, já com desconto.
amounts.gateway_feenúmero | nullTaxa do gateway, em reais. Vem null enquanto o gateway não informa: a taxa costuma nascer segundos depois do pagamento, então no order.paid o comum é null; o valor aparece nos eventos seguintes do pedido.
amounts.seller_netnúmerototal − gateway_fee. Enquanto a taxa não é conhecida, é igual ao total.
created_atdata UTCQuando o pedido foi criado.
paid_atdata UTC | nullQuando foi pago. Preenchido também em order.refunded e order.chargeback.
refused_atdata UTC | nullQuando a última tentativa foi recusada.
refunded_atdata UTC | nullQuando foi reembolsado.
refused_reasonstring | nullCódigo da recusa do cartão (ex.: insufficient_funds). Só em order.refused.
gatewaystringpagarme | mercadopago. O gateway do vendedor que processou.
gateway_order_idstring | nullId do pedido no gateway, para conciliação.
checkout_urlstringA página de pagamento do produto.

data.items[]

CampoTipoDescrição
kindstringmain | bump | upsell. Bump é item do mesmo pedido; upsell chega num pedido próprio.
sale_iduuidId da linha de venda. Cada item tem o seu.
product_id / product_nameuuid / stringO produto vendido.
offer_id / offer_nameuuid | null / string | nullA oferta aplicada, quando houver.
quantitynúmeroHoje, sempre 1.
unit_price / totalnúmeroEm reais, já com o desconto do cupom aplicado.

data.buyer, data.seller e data.coupon

CampoTipoDescrição
buyer.name / buyer.emailstringO comprador, como preencheu no checkout.
buyer.phonestring | nullTelefone com DDD, só dígitos.
buyer.documentstring | nullCPF, só dígitos.
buyer.ipstring | nullIP do comprador na página de pagamento.
buyer.countrystringHoje, sempre "BR".
seller.id / seller.nameuuid / stringA conta Paytix e o nome de fornecedor exibido no checkout.
couponobjeto | null{ code, discount } quando um cupom foi usado.

data.tracking

A atribuição capturada na primeira visita à página de pagamento. Campo que não veio na URL chega null.

CampoTipoDescrição
session_idstring | nullId da sessão de checkout. Liga o pedido aos eventos de funil.
utm_source … utm_termstring | nullOs cinco parâmetros UTM da URL.
src / sckstring | nullParâmetros de rastreio da Utmify.
fbclid / gclid / ttclidstring | nullIds de clique de Meta, Google e TikTok.
fbp / fbcstring | nullCookies do pixel da Meta. São eles que casam a conversão com o clique.
referrer / landing_urlstring | nullDe onde o comprador veio e a URL exata em que caiu.
device_typestring | nullmobile | desktop
user_agentstring | nullO navegador do comprador.

Exemplo de order.paid

{
  "id": "9d3f1c8a-1b2c-4d5e-8f90-1a2b3c4d5e6f",
  "event": "order.paid",
  "version": 1,
  "occurred_at": "2026-08-25T14:32:11Z",
  "sent_at": "2026-08-25T14:32:12Z",
  "test": false,
  "data": {
    "order": {
      "id": "8c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
      "status": "paid",
      "payment_method": "pix",
      "installments": 1,
      "currency": "BRL",
      "amounts": {
        "subtotal": 346.9,
        "discount": 29.7,
        "total": 317.2,
        "gateway_fee": null,
        "seller_net": 317.2
      },
      "created_at": "2026-08-25T14:30:07+00:00",
      "paid_at": "2026-08-25T14:32:11+00:00",
      "refused_at": null,
      "refunded_at": null,
      "refused_reason": null,
      "gateway": "pagarme",
      "gateway_order_id": "or_x9k2m4n8",
      "checkout_url": "https://paytix.com.br/pay/curso-de-trafego"
    },
    "items": [
      {
        "kind": "main",
        "sale_id": "8c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
        "product_id": "1f2e3d4c-5b6a-7988-9a0b-1c2d3e4f5a6b",
        "product_name": "Curso de Tráfego",
        "offer_id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
        "offer_name": "Turma de agosto",
        "quantity": 1,
        "unit_price": 267.3,
        "total": 267.3
      },
      {
        "kind": "bump",
        "sale_id": "4e5f6a7b-8c9d-0e1f-2a3b-4c5d6e7f8a9b",
        "product_id": "0b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
        "product_name": "Planilha de Métricas",
        "offer_id": null,
        "offer_name": null,
        "quantity": 1,
        "unit_price": 49.9,
        "total": 49.9
      }
    ],
    "buyer": {
      "name": "João Silva",
      "email": "[email protected]",
      "phone": "11999999999",
      "document": "12345678900",
      "ip": "189.10.20.30",
      "country": "BR"
    },
    "seller": {
      "id": "2b3c4d5e-6f70-8192-a3b4-c5d6e7f8a9b0",
      "name": "Minha Marca"
    },
    "coupon": {
      "code": "PROMO10",
      "discount": 29.7
    },
    "tracking": {
      "session_id": "s_1756130000_ab12cd34",
      "utm_source": "facebook",
      "utm_medium": "cpc",
      "utm_campaign": "lancamento",
      "utm_content": "criativo-3",
      "utm_term": null,
      "src": null,
      "sck": null,
      "fbclid": "IwAR123",
      "gclid": null,
      "ttclid": null,
      "fbp": "fb.1.1756130000.123456",
      "fbc": "fb.1.1756130000.IwAR123",
      "referrer": "https://facebook.com/",
      "landing_url": "https://paytix.com.br/pay/curso-de-trafego?utm_source=facebook",
      "device_type": "mobile",
      "user_agent": "Mozilla/5.0 …"
    },
    "subscription": null
  }
}

Carrinho abandonado (cart.abandoned)

Dispara quando o comprador preenche o e-mail no checkout e passa cerca de 30 minutos sem pagar. Não existe pedido ainda: data.order vem null e o conteúdo fica em data.cart. Se ele pagar dentro da janela, o evento não dispara.

data.cart

CampoTipoDescrição
iduuidId da sessão de checkout abandonada.
statusstringSempre "abandoned".
payment_methodstring | nullO meio de pagamento selecionado quando o comprador parou.
amountsobjetosubtotal, discount e total, sem gateway_fee: não houve cobrança.
initiated_atdata UTCQuando o comprador começou a preencher.
abandoned_atdata UTCQuando foi marcado como abandonado.
checkout_urlstringO link para levar o comprador de volta ao checkout.

Diferenças em relação aos eventos de pedido: items[].sale_id vem null (não existe venda), buyer.ip vem null, e no tracking os campos fbp, fbc, referrer e user_agent vêm null.

Exemplo

{
  "id": "9d3f1c8a-1b2c-4d5e-8f90-1a2b3c4d5e6f",
  "event": "cart.abandoned",
  "version": 1,
  "occurred_at": "2026-08-25T14:32:11Z",
  "sent_at": "2026-08-25T14:32:12Z",
  "test": false,
  "data": {
    "order": null,
    "cart": {
      "id": "6d7e8f9a-0b1c-2d3e-4f5a-6b7c8d9e0f1a",
      "status": "abandoned",
      "payment_method": "pix",
      "currency": "BRL",
      "amounts": {
        "subtotal": 297,
        "discount": 0,
        "total": 297
      },
      "initiated_at": "2026-08-25T13:58:20+00:00",
      "abandoned_at": "2026-08-25T14:32:11+00:00",
      "checkout_url": "https://paytix.com.br/pay/curso-de-trafego"
    },
    "items": [
      {
        "kind": "main",
        "sale_id": null,
        "product_id": "1f2e3d4c-5b6a-7988-9a0b-1c2d3e4f5a6b",
        "product_name": "Curso de Tráfego",
        "offer_id": null,
        "offer_name": null,
        "quantity": 1,
        "unit_price": 297,
        "total": 297
      }
    ],
    "buyer": {
      "name": "João Silva",
      "email": "[email protected]",
      "phone": "11999999999",
      "document": "12345678900",
      "ip": null,
      "country": "BR"
    },
    "seller": {
      "id": "2b3c4d5e-6f70-8192-a3b4-c5d6e7f8a9b0",
      "name": "Minha Marca"
    },
    "coupon": null,
    "tracking": {
      "session_id": "s_1756130000_ab12cd34",
      "utm_source": "facebook",
      "utm_medium": "cpc",
      "utm_campaign": "lancamento",
      "utm_content": "criativo-3",
      "utm_term": null,
      "src": null,
      "sck": null,
      "fbclid": "IwAR123",
      "gclid": null,
      "ttclid": null,
      "fbp": null,
      "fbc": null,
      "referrer": null,
      "landing_url": "https://paytix.com.br/pay/curso-de-trafego?utm_source=facebook",
      "device_type": "mobile",
      "user_agent": null
    },
    "subscription": null
  }
}

Eventos de assinatura (subscription.*)

Nesses eventos data.order vem null e o conteúdo fica em data.subscription. O subscription.id é estável entre criar, renovar e cancelar: é a chave certa para liberar e revogar acesso. Importante: subscription.charge_failed abre a carência (past_due), não é cancelamento; quem revoga é o subscription.canceled. O tracking vem vazio nesses eventos, e buyer traz só nome e e-mail.

data.subscription

CampoTipoDescrição
iduuidId da assinatura na Paytix. É o mesmo em todos os eventos dela.
statusstringtrialing | active | past_due | cancelled. O estado no momento do evento.
gateway / gateway_subscription_idstring / string | nullOnde a recorrência roda e o id dela lá.
frequencystringweekly | monthly | quarterly | semiannual | annual
payment_methodstringHoje, credit_card.
amountnúmeroValor de cada cobrança, em reais.
current_period_start / current_period_enddata UTCO período vigente já pago.
trial_ends_atdata UTC | nullFim do período de teste, quando houver.
cancelled_atdata UTC | nullQuando foi cancelada.
cardobjeto | null{ brand, last4 } do cartão salvo. Nunca o número.

Exemplo de subscription.renewed

{
  "id": "9d3f1c8a-1b2c-4d5e-8f90-1a2b3c4d5e6f",
  "event": "subscription.renewed",
  "version": 1,
  "occurred_at": "2026-08-25T14:32:11Z",
  "sent_at": "2026-08-25T14:32:12Z",
  "test": false,
  "data": {
    "order": null,
    "items": [
      {
        "kind": "main",
        "sale_id": null,
        "product_id": "1f2e3d4c-5b6a-7988-9a0b-1c2d3e4f5a6b",
        "product_name": "Mentoria Mensal",
        "offer_id": "9a2b3c4d-5e6f-7081-9a2b-3c4d5e6f7081",
        "offer_name": "Pro",
        "quantity": 1,
        "unit_price": 97,
        "total": 97
      }
    ],
    "buyer": {
      "name": "João Silva",
      "email": "[email protected]",
      "phone": null,
      "document": null,
      "ip": null,
      "country": "BR"
    },
    "seller": {
      "id": "2b3c4d5e-6f70-8192-a3b4-c5d6e7f8a9b0",
      "name": "Minha Marca"
    },
    "coupon": null,
    "tracking": {
      "session_id": null,
      "utm_source": null,
      "utm_medium": null,
      "utm_campaign": null,
      "utm_content": null,
      "utm_term": null,
      "src": null,
      "sck": null,
      "fbclid": null,
      "gclid": null,
      "ttclid": null,
      "fbp": null,
      "fbc": null,
      "referrer": null,
      "landing_url": null,
      "device_type": null,
      "user_agent": null
    },
    "subscription": {
      "id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
      "status": "active",
      "gateway": "pagarme",
      "gateway_subscription_id": "sub_x1y2z3w4",
      "frequency": "monthly",
      "payment_method": "credit_card",
      "amount": 97,
      "currency": "BRL",
      "current_period_start": "2026-08-25T14:32:11+00:00",
      "current_period_end": "2026-09-25T14:32:11+00:00",
      "trial_ends_at": null,
      "cancelled_at": null,
      "plan": {
        "offer_id": "9a2b3c4d-5e6f-7081-9a2b-3c4d5e6f7081",
        "name": "Pro",
        "slug": "mentoria-pro",
        "code": "visitas_50k"
      },
      "card": {
        "brand": "visa",
        "last4": "4242"
      }
    }
  }
}

Eventos de teste

O botão Testar da tela Webhooks envia um evento com a forma exata do real, mas com "test": true, valor de R$ 1 e o pedido de id fixo paytix-teste-conexao. Testar de novo repete o mesmo id, para ferramenta que evolui pedido por id atualizar a mesma linha em vez de criar outra. Trate test antes de qualquer efeito: evento de teste não deve entregar produto nem disparar automação real.

API de leitura

O webhook avisa quando algo acontece. A API responde quando você pergunta, e é o caminho mais curto para um SaaS: no login do seu sistema, pergunte se aquele e-mail tem assinatura ativa e em qual plano, sem guardar estado nenhum do lado de cá.

Autenticação

Crie uma chave em Configurações → Chaves de API e mande em Authorization: Bearer ptx_…. Ela aparece uma única vez: a Paytix guarda só o SHA-256. Chame do servidor: a API não responde a requisição de navegador, de propósito, para a chave não vazar na aba de rede de quem abrir a sua página.

GET /subscriptions

curl "https://qefuqiimonxnutinyvcu.supabase.co/functions/v1/api-v1/[email protected]" \
  -H "Authorization: Bearer ptx_sua_chave"
ParâmetroTipoDescrição
emailstringE-mail do comprador, como ele digitou no checkout.
product_iduuidSó as assinaturas deste produto.
product_slugstringO mesmo, pelo endereço do checkout.
statusstringUm ou mais, separados por vírgula: trialing, active, past_due, pending, cancelled.
limitnúmeroAté 100. O padrão é 50, do mais novo para o mais antigo.

A resposta:

{
  "data": [
    {
      "id": "3f1c…",
      "status": "active",
      "active": true,
      "plan": {
        "offer_id": "9a2b…",
        "code": "visitas_10k",
        "name": "Starter",
        "slug": "linkboom-starter"
      },
      "product": {
        "id": "7d4e…",
        "name": "Linkboom",
        "slug": "linkboom"
      },
      "buyer": {
        "email": "[email protected]",
        "name": "Cliente Exemplo"
      },
      "amount": 47,
      "currency": "BRL",
      "frequency": "monthly",
      "interval_count": 1,
      "payment_method": "credit_card",
      "card": {
        "brand": "visa",
        "last4": "4242"
      },
      "trial_ends_at": null,
      "current_period_start": "2026-08-01T12:00:00Z",
      "current_period_end": "2026-09-01T12:00:00Z",
      "cancel_at_period_end": false,
      "cancelled_at": null,
      "activated_at": "2026-08-01T12:00:04Z",
      "created_at": "2026-08-01T12:00:00Z"
    }
  ]
}
CampoTipoDescrição
activebooleanoSe o cliente pode usar o software hoje. Verdadeiro em trialing, active e past_due. A carência de uma cobrança recusada vai até o cancelamento, e cortar no primeiro erro derruba cliente bom por cartão vencido.
plan.codestringO código que você escreveu na aba Planos (visitas_10k). É por ele que se aplica a cota: não muda se você renomear o plano.
plan.offer_iduuidA oferta. Use quando não houver código.
current_period_enddataAté quando o ciclo pago vale.

POST /subscriptions/{id}/upgrade

"Esse cliente estourou a cota, sobe para o próximo plano." Quem sabe que o limite acabou é o seu software; a Paytix executa: acha o degrau seguinte da escada que você ordenou em Produtos → Planos, troca o plano e cobra a diferença proporcional aos dias que faltam do ciclo.

curl -X POST "https://qefuqiimonxnutinyvcu.supabase.co/functions/v1/api-v1/subscriptions/3f1c.../upgrade" \
  -H "Authorization: Bearer ptx_sua_chave" \
  -H "Content-Type: application/json" \
  -d '{"plan_code": "visitas_50k"}'   # opcional: sem isso, sobe um degrau
RespostaTipoDescrição
plan.codestringO plano em que o cliente ficou.
prorated_amountnúmeroO que foi cobrado agora. Zero quando não houve cobrança; veja Mercado Pago abaixo.
effective_atdataQuando o plano novo passou a valer: agora.
next_charge_atdataQuando entra o valor cheio.

Os "não" possíveis, todos com corpo JSON:

ErroHTTPO que fazer
auto_upgrade_disabled409Desligado no produto (scope: "product") ou pelo cliente na área de compras dele (scope: "buyer"). Você decide: travar, avisar ou degradar.
no_next_plan409Ele já está no plano mais alto da escada.
plan_not_found404Nenhum plano ativo com esse código neste produto.
not_an_upgrade409O plano pedido não é mais caro. Auto-upgrade não desce de plano.
subscription_inactive409A assinatura está cancelada ou ainda pendente.
upgrade_failed502O gateway recusou, e a mensagem vem junto (cartão sem limite, por exemplo). O plano não mudou.

No Mercado Pago não há cobrança da diferença: todo cartão salvo lá exige o CVV a cada cobrança, e ninguém digita nada num upgrade automático. O plano sobe na hora e o valor cheio entra na renovação, e o prorated_amount volta zero.

Cobrar mais caro sem autorização prévia é venda casada: a regra aparece no checkout antes da compra e o cliente pode desligar em /compras. Diga isso também na sua página de preços.

Limites

120 chamadas por minuto, por chave. Passou disso, 429. Chave inválida ou revogada responde 401. Guarde o resultado em cache pelo tempo da sessão do seu usuário em vez de consultar a cada página.

Para o que esta página não respondeu, a tela Webhooks mostra cada entrega feita: payload enviado, resposta recebida e a próxima tentativa.