docs

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

State machine

PaymentIntent lifecycle

Cada transição emite um evento. Você nunca precisa fazer polling — escute via webhook.

cartão sem 3DSrequires_payment_methodrequires_confirmationrequires_actionprocessingsucceededcanceledfailed

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.

StatusSignificadoO que faz a seguir
requires_payment_methodRecurso criado, sem método anexado aindaConfirmar com payment_method: tok_*
requires_confirmationTem método anexado; falta confirmar a cobrançaPOST /v1/payment_intents/:id/confirm
requires_actionPrecisa do cliente — 3DS challenge, redirect bancário, ou QR PIXEsperar webhook (3DS / liquidação assíncrona)
processingAdquirente recebeu, aguardando liquidaçãoEsperar webhook (cartão em segundos, PIX/boleto até D+0)
succeededLiquidado — você pode liberar produtoIdempotente: trate como evento único no consumer
canceledCancelado 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

CampoTipoNotas
amountintCentavos. R$ 497,00 = 49700. Nunca float.
currencystringISO 4217: brl, usd, eur.
customerstringcus_* — obrigatório. Crie um Customer antes (veja Customers).
payment_method_typesstring[]?Métodos aceitos: card, pix, boleto. Default ['card'].
descriptionstring?Aparece no dashboard. Até 255 chars.
statement_descriptorstring?Aparece no statement do cartão. Até 22 chars.
utmobject?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:

CampoTipoUso
utm_sourcestring?Origem (google, facebook, email_newsletter)
utm_mediumstring?Canal (cpc, email, social)
utm_campaignstring?Nome da campanha
utm_termstring?Palavra-chave paga (search ads)
utm_contentstring?Variante de criativo / A/B test
utm_referrerstring?URL de referrer no momento do landing
utm_landing_pagestring?URL do landing page completa
srcstring?Slot de afiliado (UTMify, etc)
sckstring?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 | canceled

A 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 novo tok_*).

Quando o intent fica em requires_action, o body traz next_action com o que o cliente precisa fazer:

typeQuandoConteúdo
pix_display_qr_codePIX gerado, aguardando pagamentoqr_code_data (BR Code) + qr_code_url opcional + expires_at
boleto_display_detailsBoleto registradoline_code (47 dígitos IPTE) + pdf_url/barcode_url opcionais + expires_at
redirect_to_url3DS challenge ou redirect bancáriourl 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:

EventoQuando
payment_intent.succeededLiquidação confirmada — libere produto aqui
payment_intent.payment_failedÚltima tentativa falhou (recusado, expirou, network)
payment_intent.canceledCancelado 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

codeCausaO que fazer
card_declinedEmissor recusouPedir outro cartão
insufficient_fundsSem saldoIdem
expired_cardValidade vencidaIdem
incorrect_cvcCVV erradoPedir confirmação
processing_errorErro temporário do adquirenteRetry com mesma idempotency key
payment_intent_not_foundID não existe nesse modo (test/live mismatch)Confirme livemode da chave
customer_not_foundCustomer não existe nesse modoIdem
token_unusableToken expirou, foi consumido ou é de outra partiçãoRe-tokenizar via zhex.js
payment_intent_canceledTentativa de confirm em intent já canceladoCrie novo intent
payment_intent_expiredTentativa 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

Esta página foi útil?

Atualizado em

Nesta página