Sessões de checkout
Uma Checkout Session (checkout.session) é um carrinho hospedado: você cria a sessão pela API,
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)
A sessão é um snapshot durável do carrinho/cliente/UTM. Os itens são fornecidos pelo comprador e
guardados como JSONB; o worker de pagamento é a autoridade que os valida contra o catálogo no
momento do submit (/pay) — não na criação da sessão. Cada lineItems[] é resolvido server-side
nesse momento:
- Catálogo — informe
idde uma variante (var_...). O backend lê o preço deproductVariants. 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).
Valores em centavos.
unitPrice/totals.*são inteiros (ex.:49700= R$ 497,00).
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": "[email protected]", "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" },
"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. Entregue ao comprador. Formato {domínio}/p/{ambient}/{id}. |
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. |
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). |
metadata / externalTransactionId | Ecoam o que você enviou. |
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. |
firstInitialization | true enquanto o comprador não abriu a url. |
expiresAt | Expiração informada por você. Se omitida, o cron expira a sessão após a idade máxima (24h a partir de createdAt, configurável). |
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 descem para colunas
próprias da transação e passam a alimentar 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.
No checkout hospedado nada disso precisa ser enviado: os
utm_*da URL de acesso são lidos e normalizados automaticamente na primeira inicialização da sessão.
Ciclo de Vida (status)
status | Significado | Terminal? |
|---|---|---|
active | Criada e válida; aguardando/recebendo o comprador. | não |
completed | Pagamento aprovado. | sim |
abandoned | Marcada como abandonada (cron de timeout); pode reabrir para active se o comprador retornar. | não |
cancelled | Cancelada via DELETE (soft-cancel). | sim |
expired | Passou de expiresAt. | sim |
O enum nativo de status é
active,completed,cancelled,abandoned,expired— não existe statuspending.Em status terminal a
urlredireciona para uma página de estado em vez de renderizar o checkout.
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) |
Filtros da listagem:
status(enum),source(texto, máx. 20),id(sess_/ckts_/ses_),daterange*e paginação (limit/offset/sort). Não há filtro porexternalTransactionId.
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": "[email protected]", "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.
Campos aceitos (todos opcionais):
items/lineItems(carrinho),customer,address,client(carrega UTM),coupons,paymentMethods,redirectUrls,shippingDelivery,source,language,timezone,totalAmount/totalDiscount/totalShipping/totalTax(centavos),utm,metadata,integrationType,integrationId,externalTransactionId,expiresAteonBehalfOf.onBehalfOf(publicIdde um sub-vendedor que você possui) faz o charge único da sessão ser liquidado no sub-vendedor (marketplace direct-charge) e você (plataforma) recebe a application fee; a posse + função de marketplace são validadas na criação (senãoonBehalfOfInvalid, 422). O campoambienté aceito por compatibilidade mas ignorado — o ambiente vem do principal (namespace da chave).
Preço ad-hoc (sem catálogo):
{ "lineItems": [{ "name": "Consultoria 1h", "quantity": 2, "unitPrice": 25000 }] }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 voo. Depois que um job de pagamento é criado para a sessão e
enquanto ele estiver
pending/processing/requiresAction, um PUT que toque os campos de dinheiro (items/lineItems,coupons,totalAmount,totalDiscount,totalShipping) responde 409sessionHasActivePayment, comparams[0].jobIdapontando o job que segura a sessão. Os demais campos continuam atualizáveis. O motivo é que o worker cobra o total congelado mas remonta a transação a partir do carrinho corrente — mexer nele nessa janela desalinharia o que foi cobrado do que foi vendido. Espere o job chegar a um estado terminal (ou abra uma nova sessão). - O preço cobrado é o congelado no job. Ao submeter o pagamento, os totais são gravados na row do
job; é essa a quantia cobrada — a coluna
totalAmountda sessão é apenas o espelho. Se as duas divergirem, vale a do job (e a divergência é registrada em log). Junto com o item acima: o selo trava a mercadoria, o congelamento 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 enfileira um
job assíncrono — não cobra inline. A cobrança é feita pelo worker, que lê o snapshot da 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 |
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).
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"].
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) |
insights: nullsignifica "não há telemetria desta sessão" — não "intenção zero". Não pintenullcomo0na tela.
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 | Cron de abandono marcou a sessão como abandoned. |
checkout.session.reactivated | Sessão abandoned reaberta (comprador retornou). |
checkout.session.expired | Sessão expirada (passou da idade máxima). |
O payload desses eventos é o objeto
checkout.sessioncompleto (mesma forma da leitura).O caminho recomendado de confirmação é webhook + a transação correspondente — veja Proibição de Polling e Verificando Assinaturas de Webhook.
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.
How is this guide?