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 / utm / externalTransactionId | Ecoam o que você enviou. |
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). |
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) |
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).
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).
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?