PaymentIntent
O recurso central da Zhex — máquina de estados de cobrança do requires_payment_method ao succeeded, com confirm headless e webhooks por transição.
PaymentIntent é o recurso central da Zhex. Ele representa uma intenção de cobrar e percorre uma máquina de estados explícita até succeeded ou canceled.
A diferença entre PaymentIntent e modelos antigos de "Charge": cada PaymentIntent é uma jornada inteira com retentativas e múltiplos métodos de pagamento sob o mesmo id. Você cria uma vez, acompanha por status, e não precisa lidar com IDs intermediários.
Lifecycle
PaymentIntent lifecycle
Cada transição emite um evento. Você nunca precisa fazer polling — escute via webhook.
Linha sólida = caminho feliz · linha tracejada = transição alternativa ou cancelamento · cor azul = transição válida · cor vermelha = caminho de erro.
Cada transição emite um evento webhook. Não faça polling — escute via webhook e use a API só para confirmar o estado atual em rotas de retry.
| Status | Significado | O que faz a seguir |
|---|---|---|
requires_payment_method | Recurso criado, sem método anexado ainda | Confirmar com payment_method: tok_* |
requires_confirmation | Tem método anexado; falta confirmar a cobrança | POST /v1/payment_intents/:id/confirm |
requires_action | Precisa do cliente — 3DS challenge, redirect bancário, ou QR PIX | Esperar webhook (3DS / liquidação assíncrona) |
processing | Adquirente recebeu, aguardando liquidação | Esperar webhook (cartão em segundos, PIX/boleto até D+0) |
succeeded | Liquidado — você pode liberar produto | Idempotente: trate como evento único no consumer |
canceled | Cancelado por você ou por timeout (PIX expira, 3DS abandonado) | Estado final |
Criar PaymentIntent
curl https://prometheus.zhex.io/v1/payment_intents \
-H "Authorization: Bearer $ZHEX_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Zhex-Version: 2026-04-25" \
-H "Content-Type: application/json" \
-d '{
"amount": 49700,
"currency": "brl",
"customer": "cus_…",
"payment_method_types": ["pix", "card", "boleto"],
"description": "Curso Node Avançado · Acesso 12m"
}'Campos aceitos
| Campo | Tipo | Notas |
|---|---|---|
amount | int | Centavos. R$ 497,00 = 49700. Nunca float. |
currency | string | ISO 4217: brl, usd, eur. |
customer | string | cus_* — obrigatório. Crie um Customer antes (veja Customers). |
payment_method_types | string[]? | Métodos aceitos: card, pix, boleto. Default ['card']. |
description | string? | Aparece no dashboard. Até 255 chars. |
statement_descriptor | string? | Aparece no statement do cartão. Até 22 chars. |
utm | object? | Atribuição de marketing. Persistida no TransactionUTM e propagada para integrações (Utmify) no momento do confirm. Veja abaixo. |
Atribuição (UTM tracking)
Para correlacionar tráfego pago com receita sem montar pipeline próprio, mande os parâmetros de atribuição no payload do PaymentIntent. Vale para qualquer método — cartão, PIX, boleto. A Zhex armazena junto com a transação e, no confirm, dispara o webhook de integração (Utmify, etc.) com o payload enriquecido.
const intent = await zhex.paymentIntents.create({
amount: 49700,
currency: 'brl',
customer: 'cus_…',
payment_method_types: ['card'],
utm: {
utm_source: 'facebook',
utm_medium: 'cpc',
utm_campaign: 'black_friday_2026',
utm_content: 'video_ad_v3',
utm_term: 'curso node',
utm_referrer: 'https://www.facebook.com/...',
utm_landing_page: 'https://meusite.com/?utm_source=facebook&...',
src: 'aff_marketing_pro', // afiliado / partner — opcional
sck: 'sub_001', // sub-campanha do afiliado — opcional
},
});Campos aceitos no utm — todos opcionais, mande só o que tem:
| Campo | Tipo | Uso |
|---|---|---|
utm_source | string? | Origem (google, facebook, email_newsletter) |
utm_medium | string? | Canal (cpc, email, social) |
utm_campaign | string? | Nome da campanha |
utm_term | string? | Palavra-chave paga (search ads) |
utm_content | string? | Variante de criativo / A/B test |
utm_referrer | string? | URL de referrer no momento do landing |
utm_landing_page | string? | URL do landing page completa |
src | string? | Slot de afiliado (UTMify, etc) |
sck | string? | Sub-campanha — pareado com src |
Como entra na Utmify
Quando o payment_intent confirma com sucesso (ou falha), a Zhex dispara PURCHASE_APPROVED (ou PURCHASE_REFUSED) para todas as integrações ativas da sua conta — incluindo a Utmify. O payload já leva os campos UTM no formato esperado, então o pedido aparece no dashboard da Utmify com a atribuição correta sem código adicional do seu lado.
Capture os UTMs no front, mande no back
Os UTMs vêm da query string da landing page do cliente (?utm_source=...). O fluxo recomendado: 1) frontend lê window.location.search no primeiro acesso, 2) guarda em localStorage, 3) envia pro seu backend junto com o resto do pedido, 4) seu backend repassa pro paymentIntents.create. Não confie no front pra criar o intent — sempre passa pelo seu servidor com a zsk_*.
Resposta
{
"id": "pi_3MtwBwLkdIwHu7ix28a3tqPa",
"object": "payment_intent",
"livemode": false,
"amount": 49700,
"currency": "brl",
"status": "requires_payment_method",
"customer": "cus_…",
"description": "Curso Node Avançado · Acesso 12m",
"payment_method_types": ["pix", "card", "boleto"],
"last_payment_error": null,
"charges": { "latest": null },
"next_action": null,
"utm": {
"utm_source": "facebook",
"utm_medium": "cpc",
"utm_campaign": "black_friday_2026",
"utm_term": null,
"utm_content": null,
"utm_referrer": null,
"utm_landing_page": null,
"src": null,
"sck": null
},
"created": 1714057200
}charges.latest recebe o ID do charge na adquirente quando a confirmação roda. last_payment_error é populado em canceled quando a transação falhou (com code e message).
utm vem populado em retrieve, confirm e cancel quando o create incluiu atribuição. Em list é sempre null — buscar UTM por intent na lista geraria N+1, então pra ler atribuição em massa use o dashboard ou query direta no BD via export. Quando precisar de UTM por intent específico, faz GET /v1/payment_intents/:id.
Confirmar (anexar token)
Toda confirmação consome um tok_* produzido pelo zhex.js (cartão) ou é gerada server-side em fluxos PIX/boleto. Hoje a API V1 expõe o caminho card → token:
const confirmed = await zhex.paymentIntents.confirm(intent.id, {
payment_method: 'tok_…', // do zhex.js
});
// confirmed.status pode ser: succeeded | requires_action | processing | canceledA resposta traz o status atualizado. Os mais comuns após confirm:
succeeded— fim feliz, sem 3DS, cartão aprovado direto.requires_action— precisa do cliente (3DS challenge, QR PIX, boleto baixar PDF). Para cartão, o 3DS é orquestrado pela própria Zhex; o estado terminal chega via webhook.processing— assíncrono (boleto/PIX): cliente já viu o que precisa, esperando pagamento.canceled— método foi recusado terminantemente, peça outro (gere novotok_*).
Quando o intent fica em requires_action, o body traz next_action com o que o cliente precisa fazer:
type | Quando | Conteúdo |
|---|---|---|
pix_display_qr_code | PIX gerado, aguardando pagamento | qr_code_data (BR Code) + qr_code_url opcional + expires_at |
boleto_display_details | Boleto registrado | line_code (47 dígitos IPTE) + pdf_url/barcode_url opcionais + expires_at |
redirect_to_url | 3DS challenge ou redirect bancário | url para redirecionar + return_url opcional |
Veja exemplos completos por método em PIX, Boleto e Cartão.
Cancel
const canceled = await zhex.paymentIntents.cancel(intent.id, {
cancellation_reason: 'requested_by_customer',
});cancellation_reason (opcional, livre) vai no audit trail e no webhook.
Você só pode cancelar em requires_payment_method, requires_confirmation ou requires_action. Em processing o cancelamento depende do método (PIX/boleto não confirmado expira sozinho). Em succeeded, use refund.
Retrieve e list
// retrieve
const intent = await zhex.paymentIntents.retrieve('pi_…');
// list (cursor-based)
for await (const intent of zhex.paymentIntents.list({
customer: 'cus_…',
limit: 100,
}).autoPagingEach()) {
/* ... */
}Use auto-paginação para iterar páginas inteiras com for await.
Webhooks emitidos
Os events que a API hoje dispara para PaymentIntent:
| Evento | Quando |
|---|---|
payment_intent.succeeded | Liquidação confirmada — libere produto aqui |
payment_intent.payment_failed | Última tentativa falhou (recusado, expirou, network) |
payment_intent.canceled | Cancelado manualmente ou por timeout |
Sempre escute payment_intent.succeeded como fonte da verdade — não confie no body do POST /v1/payment_intents/:id/confirm que parecia succeeded. Network pode mentir, webhook não.
Veja Webhooks para o handler canônico com verificação HMAC.
Idempotência sem armadilha
Use a mesma Idempotency-Key em todas as tentativas da mesma operação lógica. Em billing recorrente, o padrão mensalidade-{customer}-{cycle} é robusto:
await zhex.paymentIntents.create(
{ amount: 9700, currency: 'brl', customer: 'cus_…' },
{ idempotencyKey: `mensalidade:${customerId}:2026-04` },
);Se o worker rodar duas vezes (deploy, reschedule), a Zhex devolve a mesma resposta cacheada dentro de 24h. Se o body mudar com a mesma key, recebe 422 idempotency_key_in_use.
Erros comuns
code | Causa | O que fazer |
|---|---|---|
card_declined | Emissor recusou | Pedir outro cartão |
insufficient_funds | Sem saldo | Idem |
expired_card | Validade vencida | Idem |
incorrect_cvc | CVV errado | Pedir confirmação |
processing_error | Erro temporário do adquirente | Retry com mesma idempotency key |
payment_intent_not_found | ID não existe nesse modo (test/live mismatch) | Confirme livemode da chave |
customer_not_found | Customer não existe nesse modo | Idem |
token_unusable | Token expirou, foi consumido ou é de outra partição | Re-tokenizar via zhex.js |
payment_intent_canceled | Tentativa de confirm em intent já cancelado | Crie novo intent |
payment_intent_expired | Tentativa de confirm em intent expirado (TTL PIX) | Crie novo intent |
Toda resposta de erro tem o shape:
{
"error": {
"type": "card_error",
"code": "card_declined",
"message": "Sua cobrança foi recusada.",
"request_id": "req_xyz"
}
}request_id é o que você manda pro suporte se algo estiver bizarro.
Próximos passos
Tokenização
Como tok_* nasce — zhex.js Elements e PCI-SAQ-A
Webhooks
Tratar payment_intent.succeeded sem duplicar entrega
Test mode
Cartões 4242, simular 3DS, fluxos de erro
Atualizado em