Preços (ProductPrice)
Modelo de preço da Zhex — base único em centavos, conversão automática BRL/USD/EUR, descrição de fatura e melhores práticas.
ProductPrice é o valor que o cliente vê. Cada Product tem um único ProductPrice ativo por vez, com um valor base e uma lista de moedas habilitadas para conversão automática em tempo de checkout.
Por que valor em centavos
base_amount é sempre inteiro em centavos da base_currency — nunca decimal.
| Display | Valor base_amount |
|---|---|
| R$ 497,00 | 49700 |
| US$ 49.99 | 4999 |
| € 9,90 | 990 |
Por quê? Floats em JavaScript fazem 0.1 + 0.2 = 0.30000000000000004. Em pagamentos isso é um chargeback esperando para acontecer. Toda math da Zhex é em inteiros até o último step de display.
Multi-currency: como funciona
Quando você define base_amount: 49700 + base_currency: "BRL" + enabled_currencies: ["BRL", "USD", "EUR"], a Zhex:
A taxa de câmbio é lockada no momento que o PaymentIntent é criado, não no clique de "Pagar". Isso evita arbitragem de FX com sessões longas e dá previsibilidade ao seu reconciliation.
A base_currency continua sendo a moeda do seu payout: se você é PJ no Brasil cobrando em BRL, o cliente americano paga em USD via cross-border, mas você recebe em BRL no payout (descontando o spread informado).
Buscar preço atual
Endpoint: GET /v1/products/:productId/price
const res = await fetch(
`https://prometheus.zhex.io/v1/products/${productId}/price`,
{
headers: { Authorization: `Bearer ${process.env.ZHEX_SECRET_KEY}` },
},
);
const price = await res.json();
// {
// id: "ppr_...",
// base_amount: 49700,
// base_currency: "BRL",
// enabled_currencies: ["BRL", "USD", "EUR"],
// payment_description: "Curso Node Avançado · Acesso 12m"
// }Atualizar preço
Endpoint: POST /v1/products/:productId/price
curl -X POST https://prometheus.zhex.io/v1/products/prd_xyz/price \
-H "Authorization: Bearer $ZHEX_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"base_amount": 59700,
"base_currency": "BRL",
"enabled_currencies": ["BRL", "USD"],
"payment_description": "Curso Node Avançado · Acesso 12m"
}'Campos
| Campo | Tipo | Notas |
|---|---|---|
base_amount | int | Centavos da base_currency. Mín 100 (R$ 1,00). |
base_currency | string | BRL, USD ou EUR. Define moeda do seu payout. |
enabled_currencies | string[] | Moedas exibidas no checkout. Sempre inclua a base_currency. |
payment_description | string? | Aparece no statement do cartão e na fatura por email. Limite 22 chars (limite Visa/Master). |
payment_description: o detalhe que evita chargeback
O statement do cartão é o que o cliente lê quando aparece a cobrança na fatura 30 dias depois. Se ele não reconhecer, o ticket vai pra dispute_chargeback.
Bom:
ZHEX*CursoNode12mMEUSITE*Premium
Ruim:
ZHX BR PAYMNT 8392← bandeira recusa, gera frictionPagamento← sem branding, leva a "que cobrança é essa?"
A Zhex prefixa automaticamente com ZHEX* se o seu MCC exigir, mas sua descrição vai depois.
Atualizações in-place
ProductPrice é singleton por produto — POST /v1/products/:productId/price muta o registro existente. Cobranças já registradas (PaymentIntent em curso, CustomerSubscription ativa) continuam com os valores que tinham na hora da criação — você sobe o preço sem afetar quem já está pagando.
// CustomerSubscription criada em janeiro com base_amount=49700
// Você sobe para 59700 em fevereiro
// → cobranças recorrentes da subscription antiga continuam em 49700
// até você fazer upgrade explícito (configurado pelo dashboard)Preço em produto recorrente
Quando o produto é RECURRING, o ProductPrice define o valor de cada ciclo. A configuração de intervalo (mensal, anual, lifetime) e trial vive no template de assinatura no dashboard — CustomerSubscription é a instância ativa que herda essas regras.
Melhores práticas
- Lock o FX cedo. Se seu funil tem upsell/downsell, sempre crie
PaymentIntents separados — não reutilize FX entre etapas distintas. - Não diminua
enabled_currenciesem produção sem um plano. Cliente que abriu checkout em USD e voltou 10 min depois pode receberMISMATCH_CURRENCY. payment_description≠Product.name. O nome pode ter 60 chars, a descrição é mais curta porque vai pro statement.
Atualizado em