Checkouts

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 id de uma variante (var_...). O backend lê o preço de productVariants. 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).

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"
}
CampoDescrição
idIdentificador público (sess_*). Use em GET/PUT/DELETE e na reconciliação.
urlA URL hospedada. Entregue ao comprador. Formato {domínio}/p/{ambient}/{id}.
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.
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).
metadata / utm / externalTransactionIdEcoam o que você enviou.
firstInitializationtrue enquanto o comprador não abriu a url.
expiresAtExpiraçã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)

statusSignificadoTerminal?
activeCriada e válida; aguardando/recebendo o comprador.não
completedPagamento aprovado.sim
abandonedMarcada como abandonada (cron de timeout); pode reabrir para active se o comprador retornar.não
cancelledCancelada via DELETE (soft-cancel).sim
expiredPassou de expiresAt.sim

O enum nativo de status é active, completed, cancelled, abandoned, expirednão existe status pending.

Em status terminal a url redireciona para uma página de estado em vez de renderizar o checkout.

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-cancelstatus=cancelled (preserva a linha e o histórico)
PagarPOST/v1/checkouts/sessions/{id}/paySubmete 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 por externalTransactionId.

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, expiresAt e onBehalfOf. onBehalfOf (publicId de 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ão onBehalfOfInvalid, 422). O campo ambient é 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.
  • 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/expiredcheckoutSessionNotUpdatable (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 } }
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

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

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.abandonedCron de abandono marcou a sessão como abandoned.
checkout.session.reactivatedSessão abandoned reaberta (comprador retornou).
checkout.session.expiredSessão expirada (passou da idade máxima).

O payload desses eventos é o objeto checkout.session completo (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

  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 / VariantslineItems[].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.
  • Webhookscheckout.session.completed para confirmação assíncrona.

How is this guide?

On this page