Sessões de checkout
Introdução
Uma Checkout Session (checkout.session) é um carrinho hospedado: você cria a sessão pela API,
recebe uma URL de checkout pronta e a entrega ao comprador (redirect, e-mail, SMS ou QR code).
O comprador preenche os dados e paga na página hospedada da Selectwin; você acompanha o resultado por
webhook (checkout.session.completed + a transação correspondente).
Diferente do Payment Link (reutilizável, sem comprador fixo), a session representa
uma intenção de compra específica — com itens já precificados pelo backend, cliente/endereço
opcionais e um externalTransactionId para reconciliação com o seu ERP.
Modelo de precificação (autoritativo no backend)
Quem decide o preço é a Selectwin, não o navegador do comprador. Os itens enviados são validados
contra o seu catálogo no momento do submit (/pay) — não na criação da sessão. Cada lineItems[]
é resolvido nesse momento:
- Catálogo — informe
idde uma variante (var_...). O preço vem do seu catálogo. Envie sóid+quantity; opcionalmente sobrescreva comunitPrice. - Ad-hoc — sem variante de catálogo: informe
nameeunitPrice(centavos). Útil para serviços avulsos.
O carrinho é payment-type-agnostic: pode misturar itens oneTime e variantes recurring (a sessão
vira assinatura no momento da cobrança, lendo billingType/cycles/trialInterval da variante — você
não envia esses campos).
O que é cobrado no pagamento (desde 2026-10-15): o comprador paga na hora exatamente o total que o checkout
mostrou. Num carrinho só de variantes recorrentes sem trial, esse total é a 1ª cobrança da assinatura —
itens + frete − cupons − desconto do método, numa transação (tra_…) ligada à assinatura, com o Pix/boleto na
tela e a recusa do cartão na hora; as cobranças seguintes são o valor dos itens (menos os cupons recorrentes), sem
frete nem desconto do método. Se essa 1ª cobrança é recusada, ou o Pix/boleto vence sem pagamento, a assinatura é
cancelada. Num carrinho só em trial, nada é cobrado no pagamento (nem o frete): a 1ª cobrança é no fim do trial,
no mesmo horário da compra.
Estrutura do Objeto checkout.session
{
"object": "checkout.session",
"id": "sess_a1b2c3d4e5f6789012ab",
"url": "https://checkout.selectwin.io/p/live/sess_a1b2c3d4e5f6789012ab",
"status": "active",
"ambient": "live",
"source": "api",
"currency": "BRL",
"lineItems": [
{ "id": "var_main_product", "name": "Curso Premium", "quantity": 1, "unitPrice": 49700,
"currency": "BRL", "isOrderbump": false, "isUpsell": false,
"metadata": { "sku": "ABC-123" }, "externalReference": "erp-ref-9" }
],
"totals": { "amount": 46523, "discount": 5167, "shipping": 0, "tax": 0 },
"coupons": [{ "code": "DESCONTO10", "type": "percentage", "value": 10 }],
"customer": {
"email": "maria@example.com", "fullName": "Maria Silva",
"document": { "number": "12345678909", "type": "cpf" },
"telephone": { "countryCode": "55", "areaCode": "11", "number": "999998888" }
},
"address": {
"postcode": "01310-100", "street": "Av. Paulista", "number": "1000",
"district": "Bela Vista", "city": "São Paulo", "state": "SP", "country": "BR"
},
"paymentMethods": ["pix", "credit", "billet"],
"paymentSelection": null,
"redirectUrls": {
"paid": "https://seller.com/obrigado",
"refused": "https://seller.com/recusado",
"pending": "https://seller.com/processando"
},
"utm": { "source": "newsletter", "campaign": "junho" },
"cookies": { "_fbp": "fb.1.1790000000000.1234567890", "_fbc": "fb.1.1790000000000.IwAR1x...", "_ga": "GA1.1.1234567890.1790000000", "_gcl_au": "1.1.1234567890.1790000000" },
"metadata": { "erpOrderId": "9921" },
"customInputs": null,
"integrationType": "shopify",
"integrationId": "cmp_shop_123",
"externalTransactionId": "pedido-7781",
"firstInitialization": true,
"online": false,
"expiresAt": "2026-06-12T12:00:00.000Z",
"createdAt": "2026-06-08T12:00:00.000Z",
"updatedAt": "2026-06-08T12:00:00.000Z"
}| Campo | Descrição |
|---|---|
id | Identificador público (sess_*). Use em GET/PUT/DELETE e na reconciliação. |
url | A URL hospedada, assinada. Entregue ao comprador (redirect, e-mail, QR). Formato {host}/p/{ambient}?session_id={id}&session_token=… — o host é o seu domínio de checkout quando você tem um pronto, senão pay.selectwin.io. O token prova que o portador recebeu a URL de quem criou a sessão: é ele que faz o checkout retomar esta sessão, com o comprador e o carrinho que você mandou, e vale enquanto a sessão vive (a mesma URL abre de novo se o comprador voltar ao pedido). Só a primeira abertura vem com os dados do comprador preenchidos; as seguintes retomam a sessão sem devolver o que foi digitado (quem tem a URL não é necessariamente quem digitou — 2026-10-10). E depois de uma segunda abertura, nenhuma tela do checkout devolve os dados digitados — nem ao recarregar a página (2026-10-12): quem abriu primeiro também não lê o que outra pessoa digitar depois. Os dados continuam na sessão e valem para o pagamento; você os lê por GET /v1/checkouts/sessions/{id}. null só em sessão terminal ou quando a conta não tem segredo de assinatura. |
status | Estado da sessão (ver Ciclo de Vida). |
ambient | live | sandbox. |
lineItems | Itens já com preço resolvido pelo backend. |
totals | Totais recalculados pelo backend (centavos). amount já é líquido do desconto. Os quatro campos são anuláveis: null significa esta sessão nunca teve um carrinho precificado — não R$ 0,00. O espelho nasce no GET /checkout/v1/checkout/init quando a sessão é criada com carrinho (itens do catálogo + cupons do link, método pré-selecionado se houver, sem frete), é reescrito pelo POST /checkout/v1/totals sempre que ele recebe um sessionId, e congelado pelo /pay. Assim o checkout.session.abandoned de quem abre o link e sai antes de a página calcular já leva o valor do carrinho. Só uma sessão sem itens continua null, para a tela poder mostrar — em vez de afirmar um zero. |
coupons | Cupons aplicados/validados. |
customer / address | Snapshot do comprador (parcial até ele preencher; objeto JSON, não cria linha ainda). |
paymentMethods | Métodos habilitados (do corpo, ou do template global da empresa). |
redirectUrls | URLs de pós-pagamento (do corpo, ou do template global). Cada valor é uma URL absoluta https:// (ou ""/null para não definir); javascript:, data: e http:// são recusados com 400. |
metadata / externalTransactionId | Ecoam o que você enviou — e descem para a cobrança: a transação (ou a assinatura) que a sessão produz nasce com externalReference = externalTransactionId e com o seu metadata (mais cookies). É por eles que o seu sistema fecha o pedido ao receber transaction.approved. O comprador não altera o seu metadata (2026-10-12): a página do checkout só escreve as chaves reservadas à plataforma — consent (o registro de consentimento do comprador) e attribution —, nunca troca nem apaga uma chave sua. Não use consent, attribution nem cookies para dados seus: são da plataforma e não descem para a cobrança. Na cobrança vale o limite de Metadata (50 chaves, chave até 40 caracteres, valor até 500, objeto até 8 KiB): uma chave que não cabe fica só na sessão, e a cobrança sai sem ela — mantenha a chave de reconciliação curta. |
utm | Atribuição da venda, normalizada (ver abaixo). Capturada uma única vez e imutável — e é ela que vira a dimensão de canal da transação cobrada. |
cookies | Cookies de rastreio do comprador, com o nome do cookie como chave e o valor como veio do navegador: _fbp e _fbc (Meta), _ga e _ga_<stream> (GA4), _gcl_au (Google Ads)… É com eles que você casa, no seu CAPI/Conversions API, um carrinho abandonado com o clique do anúncio. Capturados pela página do checkout e mesclados a cada autosave (o _fbc só existe depois do clique e pode chegar depois do _fbp). null quando nada foi capturado — a página não chegou a rodar, ou o comprador não consentiu. Apagados da sessão 90 dias após a última atividade. Na venda aprovada, os mesmos valores descem para transaction.metadata.cookies. |
firstInitialization | true enquanto o comprador não abriu a url. |
online | O comprador está com o checkout aberto agora. Sessão active cujo último sinal de presença (heartbeat, telemetria ou autosave) tem menos de 90s. É calculado na leitura, nunca um estado guardado: não existe "evento de despedida" quando alguém fecha a aba. |
expiresAt | Expiração informada por você. Se omitida, a sessão expira automaticamente após a idade máxima (24h a partir de createdAt). |
Atribuição (utm)
De onde veio a venda. Capturada uma vez, na criação, e imutável: um PUT posterior com outra utm
é ignorado enquanto a sessão já tiver atribuição — o afiliado que trouxe o comprador não pode ser
sobrescrito no meio do checkout. Quando a sessão é cobrada, os cinco campos UTM passam para a
transação e alimentam os relatórios de canal.
{ "utm": { "source": "newsletter", "medium": "email", "campaign": "junho", "content": "banner-topo", "term": "curso", "gclid": "Cj0KCQ...", "fbclid": "IwAR1x..." } }Duas grafias entram, uma sai. source, utm_source e utmSource são todas aceitas — a URL do
anúncio carimba utm_source, e repassá-la crua funciona. O que fica armazenado e é devolvido na leitura
é sempre a forma curta (source, medium, campaign, content, term), acompanhada dos
identificadores de clique que vierem (gclid, fbclid, ttclid, msclkid, twclid, wbraid,
gbraid, irclickid, li_fat_id, epik).
Chave desconhecida é descartada. Valor vazio, null, ou o texto literal "null"/"undefined" conta
como ausente — então um objeto só de nulos equivale a não enviar utm, e não consome a captura
única: a campanha real ainda pode ser gravada depois.
Ciclo de Vida (status)
stateDiagram-v2
state "active" as active
state "completed" as completed
state "abandoned" as abandoned
state "cancelled" as cancelled
state "expired" as expired
[*] --> active: POST /v1/checkouts/sessions
active --> completed: pagamento aprovado
active --> abandoned: inatividade do comprador
abandoned --> active: comprador retorna (heartbeat / reativação)
active --> cancelled: DELETE (soft-cancel)
active --> expired: passou de expiresAt
completed --> [*]
cancelled --> [*]
expired --> [*]status | Significado | Terminal? |
|---|---|---|
active | Criada e válida; aguardando/recebendo o comprador. | não |
completed | Pagamento aprovado. | sim |
abandoned | Marcada como abandonada por inatividade; pode reabrir para active se o comprador retornar. | não |
cancelled | Cancelada via DELETE (soft-cancel). | sim |
expired | Passou de expiresAt. | sim |
Recursos Disponíveis
| Recurso | Método | Endpoint | Descrição |
|---|---|---|---|
| Criar | POST | /v1/checkouts/sessions | Cria a sessão e devolve a url hospedada |
| Listar | GET | /v1/checkouts/sessions | Lista paginada (filtros por status, source, id, datas) |
| Consultar | GET | /v1/checkouts/sessions/{id} | Recupera a sessão atual (reconciliação) |
| Atualizar | PUT | /v1/checkouts/sessions/{id} | Atualiza itens/cliente/endereço/cupons/metadata (só em status não-terminal) |
| Cancelar | DELETE | /v1/checkouts/sessions/{id} | Soft-cancel → status=cancelled (preserva a linha e o histórico) |
| Pagar | POST | /v1/checkouts/sessions/{id}/pay | Submete a sessão para pagamento (assíncrono → job 202) |
| Token de telemetria | POST | /v1/checkouts/sessions/{id}/telemetry-token | Emite a capability que o front-end do checkout usa para enviar telemetria de interatividade (só sessão active) |
| Comportamento | GET | /v1/checkouts/sessions/{id}/behavior | Roll-up de engajamento + diagnóstico da sessão (intenção, risco de abandono, atrito, ação recomendada) |
| Eventos | GET | /v1/checkouts/sessions/{id}/events | Traço bruto de interatividade, evento a evento (limit 1–500, cursor afterSeq) |
Listar: scores e ordenação
Cada item da lista traz os três scores do motor de telemetria, de 0 a 100: intentScore (intenção de
compra), abandonRiskScore (risco de abandono) e automationScore (indício de automação, apenas
consultivo). null significa que nenhum evento de telemetria chegou para aquela sessão. Os valores são
os do último evento recebido: numa sessão já encerrada, descrevem como ela estava quando parou. O
diagnóstico completo (estágio, pontos de atrito, razões, ação recomendada) fica em
GET /v1/checkouts/sessions/{id}/behavior.
sort aceita createdAt, intentScore, abandonRiskScore ou abandonedAt. O prefixo - inverte para
decrescente.
Qualquer outro valor responde 400 invalidParameters. ascending e descending continuam valendo e
ordenam por createdAt. Sem sort, a lista vem da mais recente para a mais antiga.
Ao ordenar por score — ou por abandonedAt — as sessões sem valor (null) vão para o fim nas duas
direções, e o desempate é pela mais recente.
| Fila | Consulta sugerida |
|---|---|
| Carrinhos abandonados: de quem vale correr atrás primeiro | ?abandonedrangegte=2026-09-09T00:00:00Z&sort=-intentScore |
| Os mesmos, na ordem em que caíram | ?abandonedrangegte=2026-09-09T00:00:00Z&sort=-abandonedAt |
| Sessões abertas: quem está prestes a sair | ?status=active&sort=-abandonRiskScore |
Numa sessão já abandonada, abandonRiskScore é um risco que já se concretizou. Para essa fila, ordene
por intentScore.
Listar: a fila de recuperação recorta por abandonedAt, não por status
O abandono não fica em abandoned para sempre. A sessão sem pagamento passa a expired 24 horas
depois de criada, inclusive a abandonada — enquanto o link de recuperação vale 7 dias. Do 2º ao 7º dia
o carrinho tem link vivo, é recuperável, e status=abandoned já não o encontra.
Por isso a fila de recuperação se monta com abandonedrangegte/abandonedrangelte, que recortam por
quando o carrinho foi abandonado:
GET /v1/checkouts/sessions?abandonedrangegte=2026-09-09T00:00:00Z&sort=-intentScoreA presença de qualquer um dos dois já implica abandonedAt preenchido — a lista devolve só o que de
fato virou carrinho abandonado, em qualquer status de agora (abandoned, expired, e também o
completed de quem voltou e comprou). O status continua em cada linha: expirado ≠ abandonado é
informação real, e o filtro não a esconde.
Os dois recortes de data convivem e respondem a perguntas diferentes: daterange* é quando a sessão
nasceu, abandonedrange* é quando o carrinho caiu.
Criar
POST /v1/checkouts/sessions
SelectKey: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"externalTransactionId": "pedido-7781",
"lineItems": [
{ "id": "var_main_product", "quantity": 1 },
{ "id": "var_orderbump_book", "quantity": 1, "isOrderbump": true }
],
"customer": { "email": "maria@example.com", "fullName": "Maria Silva" },
"coupons": [{ "code": "DESCONTO10" }],
"redirectUrls": { "paid": "https://seller.com/obrigado" },
"metadata": { "erpOrderId": "9921" }
}Resposta 201 = o objeto checkout.session acima. Redirecione o comprador para url.
Preço ad-hoc (sem catálogo):
{ "lineItems": [{ "name": "Consultoria 1h", "quantity": 2, "unitPrice": 25000 }] }Pedido precificado fora: linhas manuais, frete da loja, carrinho travado
É o caso de uma loja (WooCommerce, Shopify, um ERP) que já calculou o pedido — cupom próprio, frete por CEP e peso, taxa de embalagem — e só quer que o comprador pague na página hospedada:
{
"externalTransactionId": "7781",
"lineItems": [
{ "id": "var_camiseta_m", "quantity": 2 },
{ "name": "Embalagem para presente", "unitPrice": 990, "quantity": 1, "externalReference": "fee:gift" },
{ "name": "Desconto BEMVINDO10", "unitPrice": 0, "quantity": 1 }
],
"shippingDelivery": { "external": true, "name": "SEDEX", "price": 2490, "deliveryTime": { "min": 2, "max": 5 }, "carrier": "Correios" },
"customer": { "fullName": "Maria Silva", "email": "maria@example.com", "document": { "type": "cpf", "number": "12345678909" } },
"address": { "postcode": "01310-100", "street": "Av. Paulista", "number": "1000", "city": "São Paulo", "state": "SP", "country": "BR" },
"paymentMethods": ["pix", "credit"],
"redirectUrls": { "paid": "https://loja.com/obrigado/7781" },
"metadata": { "orderKey": "wc_order_abc" }
}- Linha manual — uma
lineItems[]comname+unitPrice(centavos inteiros) e semid. Ela recebe um idline_…na resposta, é exibida e somada na página com o preço que você mandou, e chega à transação como item (comexternalReference/metadata, se enviados). Um desconto da sua loja entra como preço já descontado nas linhas, nunca como valor negativo. - Frete da loja —
shippingDeliverycomexternal: true,nameeprice(centavos;0= grátis). Entra no total como frete, é congelado com esse nome na cobrança (shippingda transação) e dispensa o passo de frete: a página não pede CEP nem opção do seu catálogo de entregas. Mande oaddresspara o boleto. - Carrinho travado — toda sessão criada por esta API é o pedido da sua loja: não tem stepper, "remover",
campo de cupom nem pop-up de cupom de saída, e o servidor recusa (
422 checkoutCartNotEditable) qualquer mudança de quantidade, item, cupom ou do frete da loja pelo comprador (a prévia de total também só considera os cupons que a sua loja gravou na sessão). O template que precifica é o global vigente da sua conta — umtemplate_idna URL de pagamento é ignorado. Os únicos ajustes de valor que podem acontecer são os descontos por método (Pix/cartão) do template global e o juro do parcelamento: confiraamountna transação contra o total do pedido menos o desconto do método escolhido; qualquer diferença além disso é anomalia — deixe o pedido em espera e fale com o suporte (antes de 10/10/2026 esta página mandava tratar QUALQUER diferença como desconto; a 20ª varredura de segurança fechou os caminhos que a produziam). - Métodos —
paymentMethodsrestringe os métodos do template (nunca liga um desligado). Ausente, vazia ou sem interseção com os trilhos (credit/pix/billet) ligados no template: a lista não restringe e valem todos os do template — inclusive o NuPay, como qualquer outro método. Ex.:["nupay"]sozinho, ou["billet", "nupay"]com o boleto desligado no template, servem o template inteiro. Numa lista que restringe (algum trilho dela está ligado), o NuPay só aparece senupayestiver nela (["pix", "nupay"]→ Pix e NuPay;["pix", "credit"]→ sem NuPay). Em todos os casos o NuPay só aparece se a sua conta estiver habilitada para ele (o comprador aprova no app do Nubank, à vista, sem cartão; carrinho com assinatura não oferece). A sessão ficacompletedquando o link de aprovação é exibido — como no Pix, quem diz que foi pago é ocheckout.session.completed/transaction.approved. Se o comprador desistir no app, a NuPay vencer (30 min) ou for recusada, você recebe otransaction.canceled/transaction.faileddessa cobrança e a sessão não volta: como depois de um Pix vencido, o comprador paga num checkout novo — crie outra sessão (ou mande o link de pagamento de novo) para o mesmo pedido. - Itens do catálogo (
idde variante) e linhas manuais convivem na mesma sessão; uma linha manual não exige CPF nem endereço por si (o Pix e o boleto continuam exigindo o CPF do pagador).
Atualizar
A semântica do PUT é de merge parcial, com recompute seguro de totais:
- Campos simples (
customer,address,metadata,redirectUrls,externalTransactionId,expiresAt) — atualizam sem recalcular o carrinho. lineItemsenviado → re-precifica itens + totais e zera os cupons (reenviecouponsno mesmo PUT para reaplicar).couponsenviado semlineItems→ re-precifica os itens atuais para recalcular o desconto.- O corpo aceita também
status, restrito aactiveouabandoned(umabandonedvoltando paraactivereativa a mesma sessão e emitecheckout.session.reactivated). - Só funciona em status não-terminal; em
completed/cancelled/expired→checkoutSessionNotUpdatable(409). - Selada enquanto há cobrança em andamento. Depois que o pagamento é submetido e enquanto ele não
chega a um estado terminal, um PUT que toque os campos de dinheiro (
items/lineItems,coupons,totalAmount,totalDiscount,totalShipping) responde 409sessionHasActivePayment. Os demais campos continuam atualizáveis. Espere o pagamento terminar (ou abra uma nova sessão). - O preço cobrado é o do momento do submit. Ao submeter o pagamento, os totais são fixados; é essa a quantia cobrada, mesmo que a sessão mude depois. O selo trava a mercadoria, o momento do submit trava o preço.
Cancelar
DELETE é soft-cancel: marca status=cancelled e preserva a linha (os dados de funil/abandono vivem
nela); sessões já completed não são afetadas. Resposta 200:
{ "id": "sess_a1b2c3d4e5f6789012ab", "resource": "checkoutSession", "deleted": true }Se a sessão não existir → checkoutSessionNotFound (404).
Pagar (assíncrono)
POST /v1/checkouts/sessions/{id}/pay registra a seleção de pagamento do comprador e devolve
202 — a cobrança é processada de forma assíncrona, sobre os dados já fixados na sessão.
{ "payment": { "method": "credit", "cardId": "card_01hqzv...", "installments": 3, "capture": true } }| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
payment.method | enum | Sim | credit, pix ou billet |
payment.cardId | string | condicional | Token de cartão salvo (card_...). Obrigatório quando method=credit |
payment.installments | int | Não | 1–21 (default 1) |
payment.capture | boolean | Não | default true. false pré-autoriza o cartão: o job fica em processing até você capturar a transação (POST /v1/transactions/{id}/capture) ou cancelá-la. Vale só para esta rota do lojista — o /pay do checkout hospedado sempre captura |
Resposta 202 Accepted (envelope de job, sem _links):
{ "jobId": "payjob_01hqzv...", "status": "pending", "sessionId": "sess_a1b2c3d4e5f6789012ab" }status do job pode ser pending, processing, requiresAction, completed ou failed. Só sessões
active são pagáveis; senão checkoutSessionNotPayable (409). Sessão inexistente → checkoutSessionNotFound (404).
Um pagamento por vez por sessão. Enquanto um pagamento da sessão está em andamento (job pending,
processing ou requiresAction — criado por esta rota ou pelo checkout hospedado), um novo submit responde
409 sessionHasActivePayment com params[0].jobId = o job que segura a sessão, e nada é cobrado de novo.
Uma Idempotency-Key nova não abre outra cobrança: acompanhe o job em andamento e, se ele terminar failed
(cartão recusado), submeta de novo. Os totais cobrados são os da sessão no instante do submit.
Uma compra, um pagamento. Normalmente a sessão de uma compra paga ou aguardando o comprador já está completed
(o 409 acima). Se mesmo assim ela estiver active com outro pagamento dela aprovado ou aguardando (Pix/boleto gerado,
NuPay aguardando aprovação, cartão pré-autorizado), o submit responde 409 sessionAlreadyPaid com
params[0].transactionId = essa cobrança, e nada é cobrado. Acompanhe-a em vez de cobrar de novo.
Configuração (paymentMethods / redirectUrls)
Você pode enviá-los no corpo da sessão. Quando omitidos, o backend usa o checkout template global
da empresa (isGlobal) para os métodos habilitados e as URLs de redirect; o redirectUrls do corpo
sobrescreve por sessão. Sem template e sem corpo, o default é ["pix", "credit", "billet"]. Um
paymentMethods enviado só restringe o template (interseção) — ver a seção do pedido precificado fora.
Reconciliação: o que volta para o seu sistema
| Sinal | Quando | O que trazer |
|---|---|---|
transaction.approved (e failed, canceled, refunded, chargeback, dispute) | a cobrança da sessão muda de estado | externalReference = o seu externalTransactionId; metadata = o seu metadata + cookies; items[] com os externalReference de cada linha; shipping com o frete da loja; payment.cardId quando um cartão ficou salvo no cliente (para cobrar renovações por POST /v1/transactions com payment.card.id) |
checkout.session.completed | a compra da sessão foi paga (cartão aprovado, Pix/boleto pago — inclusive a 1ª cobrança de uma assinatura sem trial) ou, num carrinho só em trial, a assinatura foi criada | o objeto checkout.session inteiro (lineItems, customer, address, totals, externalTransactionId, metadata) mais transactionId/subscriptionId (os dois, quando a compra criou uma assinatura e cobrou uma transação) |
GET /v1/checkouts/sessions/{id} | reconciliação pontual | status, transactionId |
Cupons
Envie coupons por código — aceita tanto a forma de string (["DESCONTO10"]) quanto de objeto
([{ "code": "DESCONTO10" }]); ambas são guardadas verbatim e ecoadas como { code, type, value }. A
elegibilidade (datas, limite de uso, mínimo de carrinho, itens permitidos) e o desconto são validados
na cobrança (no /pay), não na criação da sessão. Cupom inexistente → couponNotFound; inelegível
→ couponExpired / couponMinCartAmount / etc. (catálogo de Coupons).
Telemetria de interatividade
Enquanto o comprador está na página, o checkout hospedado envia telemetria de interatividade — forma e tempo da interação (etapa atual, tempo ativo, edições por campo, tentativas de pagamento, trocas de parcelamento), nunca o valor digitado em campo nenhum. Sobre esse traço o backend calcula, por sessão, um diagnóstico de intenção e abandono. Você lê o resultado por dois endpoints; o terceiro só interessa a quem renderiza o próprio front-end do checkout.
Token de telemetria — POST /v1/checkouts/sessions/{id}/telemetry-token
Emite a capability que o front-end usa para enviar os ticks de telemetria. Só serve para escrever
telemetria nessa sessão e só é emitida para sessão active (409 checkoutSessionNotTrackable nas
demais). O checkout hospedado pela Selectwin já faz isso sozinho — você só precisa deste endpoint se
renderiza o checkout por conta própria em cima de uma sessão.
{
"object": "checkout.session.telemetryToken",
"id": "sess_abc123",
"token": "eyJhbGciOiJSUzI1NiIs...",
"expiresAt": "2026-07-15T12:00:00.000Z",
"endpoint": "/core/checkout/v1/sessions/sess_abc123/telemetry",
"intervalMs": 10000
}Comportamento — GET /v1/checkouts/sessions/{id}/behavior
O roll-up da sessão (engagement) e o diagnóstico calculado sobre ele (insights):
{
"object": "checkout.session.behavior",
"id": "sess_abc123",
"status": "active",
"engagement": {
"totalTimeSpentMs": 384000,
"activeTimeMs": 250000,
"maxStepReached": 4,
"visitCount": 2,
"eventCount": 137,
"lastTelemetryAt": "2026-07-14T12:06:11.000Z"
},
"insights": {
"intentScore": 88,
"abandonmentRisk": 64,
"automationScore": 5,
"confidence": 92,
"stage": "stuck",
"frictionPoints": [
{ "area": "pagamento", "severity": "high", "evidence": "2 tentativa(s) de pagamento recusada(s)." }
],
"recommendedAction": "offerHelp",
"reasons": ["2 tentativa(s) de pagamento registrada(s) — a intenção de compra é concreta."],
"summary": "Comprador travado há ~6min, na etapa 4 de 4. Intenção 88/100, risco de abandono 64/100.",
"computedAt": "2026-07-14T12:06:11.000Z"
},
"behavior": { "...": "o roll-up cru" }
}| Campo | Descrição |
|---|---|
engagement.totalTimeSpentMs / activeTimeMs | Tempo total desde a abertura e tempo com a aba focada/interagindo |
engagement.maxStepReached / visitCount / eventCount | Etapa máxima alcançada, visitas e eventos recebidos |
insights.intentScore / abandonmentRisk / automationScore | Escores 0–100: intenção de compra, risco de abandono, probabilidade de automação (bot) |
insights.confidence | Confiança 0–100 do diagnóstico — exiba sempre; abaixo de 35% a ação recomendada é wait |
insights.stage | browsing, filling, paying, stuck, leaving ou converted |
insights.frictionPoints[] | Onde o comprador travou (area, severity low/medium/high, evidence) |
insights.recommendedAction | O que fazer agora (ex.: none, wait, offerHelp) |
Eventos — GET /v1/checkouts/sessions/{id}/events
O traço evento a evento, em ordem, para "replay" da sessão. Paginação por cursor: limit (1–500, default
200) e afterSeq (devolve só eventos com seq maior).
{
"object": "list",
"id": "sess_abc123",
"total": 137,
"hasMore": false,
"data": [
{ "seq": 42, "type": "field.paste", "step": 4, "payload": { "field": "cardNumber" },
"occurredAt": "2026-07-14T12:00:03.100Z", "receivedAt": "2026-07-14T12:00:05.000Z" }
]
}O vocabulário de type é fechado (session.view, step.enter, field.edit, field.paste,
payment.submit, payment.result, click.rage, exit.intent…) e o payload carrega forma e
tempo, jamais conteúdo: quantas correções, se houve colagem, qual validação falhou — nunca o que foi
digitado.
Webhooks
| Evento | Quando |
|---|---|
checkout.session.created | Sessão recém-criada (auditoria). |
checkout.session.completed | O comprador concluiu o pagamento na sessão (use isto, não polling, para liberar o pedido). |
checkout.session.abandoned | A sessão passou a abandoned por inatividade do comprador. |
checkout.session.reactivated | Sessão abandoned reaberta (comprador retornou). |
checkout.session.expired | Sessão expirada (passou da idade máxima). |
Melhores Práticas
- Resolva preço pelo catálogo (
var_*) sempre que possível; use ad-hoc só para itens fora do catálogo. - Use
externalTransactionIdpara reconciliar (campo ecoado em cada sessão; a listagem filtra porstatus/source/id/datas, não porexternalTransactionId). - Reaja a
checkout.session.completedem vez de fazer polling noGET. - Reenvie
couponsao alterarlineItemsnum PUT, já que mudar o carrinho zera os descontos. - Defina
expiresAtse a idade máxima padrão (24h) não fizer sentido para o seu fluxo.
Integração com Outros Recursos
- Products / Variants —
lineItems[].idreferencia variantes (var_*); o preço vem da variante. - Coupons — aplicados por código; validados/precificados no create/update.
- Transactions — a sessão concluída gera uma transação (cobrança).
- Customers / Addresses — o snapshot da sessão é resolvido em cliente/endereço no momento do pagamento.
- Webhooks —
checkout.session.completedpara confirmação assíncrona.
Atualizado em 21 de outubro de 2026