SelectwinSelectwin

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 id de uma variante (var_...). O preço vem do seu catálogo. Envie só id + quantity; opcionalmente sobrescreva com unitPrice.
  • Ad-hoc — sem variante de catálogo: informe name e unitPrice (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

JSON
{
  "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"
}
CampoDescrição
idIdentificador público (sess_*). Use em GET/PUT/DELETE e na reconciliação.
urlA 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.
statusEstado da sessão (ver Ciclo de Vida).
ambientlive | sandbox.
lineItemsItens já com preço resolvido pelo backend.
totalsTotais 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.
couponsCupons aplicados/validados.
customer / addressSnapshot do comprador (parcial até ele preencher; objeto JSON, não cria linha ainda).
paymentMethodsMétodos habilitados (do corpo, ou do template global da empresa).
redirectUrlsURLs 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 / externalTransactionIdEcoam 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.
utmAtribuiçã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.
cookiesCookies 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.
firstInitializationtrue enquanto o comprador não abriu a url.
onlineO 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.
expiresAtExpiraçã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.

JSON
{ "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 --> [*]
statusSignificadoTerminal?
activeCriada e válida; aguardando/recebendo o comprador.não
completedPagamento aprovado.sim
abandonedMarcada como abandonada por inatividade; pode reabrir para active se o comprador retornar.não
cancelledCancelada via DELETE (soft-cancel).sim
expiredPassou de expiresAt.sim

Recursos Disponíveis

RecursoMétodoEndpointDescrição
CriarPOST/v1/checkouts/sessionsCria a sessão e devolve a url hospedada
ListarGET/v1/checkouts/sessionsLista paginada (filtros por status, source, id, datas)
ConsultarGET/v1/checkouts/sessions/{id}Recupera a sessão atual (reconciliação)
AtualizarPUT/v1/checkouts/sessions/{id}Atualiza itens/cliente/endereço/cupons/metadata (só em status não-terminal)
CancelarDELETE/v1/checkouts/sessions/{id}Soft-cancel → status=cancelled (preserva a linha e o histórico)
PagarPOST/v1/checkouts/sessions/{id}/paySubmete a sessão para pagamento (assíncrono → job 202)
Token de telemetriaPOST/v1/checkouts/sessions/{id}/telemetry-tokenEmite a capability que o front-end do checkout usa para enviar telemetria de interatividade (só sessão active)
ComportamentoGET/v1/checkouts/sessions/{id}/behaviorRoll-up de engajamento + diagnóstico da sessão (intenção, risco de abandono, atrito, ação recomendada)
EventosGET/v1/checkouts/sessions/{id}/eventsTraç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.

FilaConsulta 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:

HTTP
GET /v1/checkouts/sessions?abandonedrangegte=2026-09-09T00:00:00Z&sort=-intentScore

A 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

HTTP
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):

JSON
{ "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:

JSON
{
  "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[] com name + unitPrice (centavos inteiros) e sem id. Ela recebe um id line_… na resposta, é exibida e somada na página com o preço que você mandou, e chega à transação como item (com externalReference/metadata, se enviados). Um desconto da sua loja entra como preço já descontado nas linhas, nunca como valor negativo.
  • Frete da loja — shippingDelivery com external: true, name e price (centavos; 0 = grátis). Entra no total como frete, é congelado com esse nome na cobrança (shipping da 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 o address para 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 — um template_id na 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: confira amount na 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 — paymentMethods restringe 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 se nupay estiver 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 fica completed quando o link de aprovação é exibido — como no Pix, quem diz que foi pago é o checkout.session.completed / transaction.approved. Se o comprador desistir no app, a NuPay vencer (30 min) ou for recusada, você recebe o transaction.canceled/transaction.failed dessa 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 (id de 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.
  • lineItems enviado → re-precifica itens + totais e zera os cupons (reenvie coupons no mesmo PUT para reaplicar).
  • coupons enviado sem lineItems → re-precifica os itens atuais para recalcular o desconto.
  • O corpo aceita também status, restrito a active ou abandoned (um abandoned voltando para active reativa a mesma sessão e emite checkout.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 409 sessionHasActivePayment. 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:

JSON
{ "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.

JSON
{ "payment": { "method": "credit", "cardId": "card_01hqzv...", "installments": 3, "capture": true } }
CampoTipoObrigatórioDescrição
payment.methodenumSimcredit, pix ou billet
payment.cardIdstringcondicionalToken de cartão salvo (card_...). Obrigatório quando method=credit
payment.installmentsintNão1–21 (default 1)
payment.capturebooleanNãodefault 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):

JSON
{ "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

SinalQuandoO que trazer
transaction.approved (e failed, canceled, refunded, chargeback, dispute)a cobrança da sessão muda de estadoexternalReference = 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.completeda 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 criadao 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 pontualstatus, 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.

JSON
{
  "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):

JSON
{
  "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" }
}
CampoDescrição
engagement.totalTimeSpentMs / activeTimeMsTempo total desde a abertura e tempo com a aba focada/interagindo
engagement.maxStepReached / visitCount / eventCountEtapa máxima alcançada, visitas e eventos recebidos
insights.intentScore / abandonmentRisk / automationScoreEscores 0–100: intenção de compra, risco de abandono, probabilidade de automação (bot)
insights.confidenceConfiança 0–100 do diagnóstico — exiba sempre; abaixo de 35% a ação recomendada é wait
insights.stagebrowsing, filling, paying, stuck, leaving ou converted
insights.frictionPoints[]Onde o comprador travou (area, severity low/medium/high, evidence)
insights.recommendedActionO 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).

JSON
{
  "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

EventoQuando
checkout.session.createdSessão recém-criada (auditoria).
checkout.session.completedO comprador concluiu o pagamento na sessão (use isto, não polling, para liberar o pedido).
checkout.session.abandonedA sessão passou a abandoned por inatividade do comprador.
checkout.session.reactivatedSessão abandoned reaberta (comprador retornou).
checkout.session.expiredSessão expirada (passou da idade máxima).

Melhores Práticas

  1. Resolva preço pelo catálogo (var_*) sempre que possível; use ad-hoc só para itens fora do catálogo.
  2. Use externalTransactionId para reconciliar (campo ecoado em cada sessão; a listagem filtra por status/source/id/datas, não por externalTransactionId).
  3. Reaja a checkout.session.completed em vez de fazer polling no GET.
  4. Reenvie coupons ao alterar lineItems num PUT, já que mudar o carrinho zera os descontos.
  5. Defina expiresAt se a idade máxima padrão (24h) não fizer sentido para o seu fluxo.

Integração com Outros Recursos

  • Products / Variants — lineItems[].id referencia 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.completed para confirmação assíncrona.

Atualizado em 21 de outubro de 2026

Preferências de cookies

Hoje o site usa só cookies essenciais: nenhuma ferramenta de medição ou de anúncios está ligada, e nada é compartilhado com terceiros. Se isso mudar, você decide aqui antes de qualquer coisa ser carregada.

  • Mantêm o site funcionando: segurança, a sua escolha sobre cookies e preferências como a versão da documentação. Não dependem de consentimento.

    Sempre ativos