Vitrin Digital — Guia de Integração

Integre pagamentos na sua aplicação em minutos. Checkout transparente, Pix, Pix Automático (recorrência via SPI/BC), boleto, cartão de crédito, assinaturas e produtos avulsos — tudo via API REST.

Recursos:

  • 📖 Swagger UI público — explore interativamente
  • 📦 Schema OpenAPI — importe direto no Postman ou Insomnia
  • 📜 Este guia — referência completa, passo a passo

⚡ Integre em 15 minutos

Os 5 passos abaixo cobrem o caso mais comum: receber um pagamento Pix e ser notificado quando ele for confirmado.

1. Cadastre-se e copie sua API key (humano, 2 min)

Cadastro, verificação documental e configuração do PIN são feitos no dashboard — não há cURL pra isso. Acesse:

vitrin.digital → Criar conta → preencha CNPJ, endereço, dados do sócio responsável, conta bancária para repasse e PIN → conclua o onboarding documental.

💡 Conta bancária no signup: o cadastro coleta sua conta de repasse já no formulário inicial (banco, agência, conta, dígito, tipo). Isso evita que seu primeiro recebível trave esperando essa info depois da aprovação. Pode editá-la depois em Conta → Configurações → Conta bancária (com PIN).

Como você recebe as chaves:

  • vd_test_* (sandbox) — gerada imediatamente no signup e exibida na tela final. Funciona já: pode criar cobranças sandbox, clientes, simular webhooks — tudo sem mover dinheiro real, sem aguardar nada.
  • vd_live_* (produção) — gerada apenas após aprovação do cadastro pela equipe Vitrin (1-2 dias úteis). Quando aprovada, a chave plaintext é enviada uma única vez no email de aprovação. Guarde em local seguro — depois disso, só dá pra regenerar (e a anterior expira).

Use a vd_test_* nos exemplos abaixo pra testar enquanto aguarda aprovação. Troque pra vd_live_* quando for pro ar.

2. Assine os 4 contratos comerciais (humano, 3 min)

A primeira chamada de cobrança retorna HTTP 403 enquanto os contratos não forem assinados. A assinatura é feita pelo dono da org no dashboard com PIN — não há endpoint REST pra isso (é exigido pela natureza da assinatura eletrônica).

Acesse /dashboard/contract, revise os 4 documentos e confirme com seu PIN.

Sua aplicação pode checar programaticamente se a org está liberada via GET /contracts/status/ antes de tentar cobrar — útil pra mostrar mensagem amigável no seu UI.

3. Crie um cliente (1 min)

curl -X POST https://api.vitrin.digital/api/v1/customers/ \
  -H "Authorization: Bearer vd_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Maria da Silva",
    "email": "maria@exemplo.com",
    "cpf_cnpj": "12345678909",
    "phone": "11988776655"
  }'

Retorna { "id": "<customer_id>", ... }.

4. Crie uma cobrança Pix (2 min)

curl -X POST https://api.vitrin.digital/api/v1/checkout/pix/ \
  -H "Authorization: Bearer vd_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "<customer_id>",
    "amount": "99.90",
    "description": "Pedido #1234"
  }'

Retorna o payment_id, o QR Code base64 (qr_code) e o copia-cola (copy_paste). Renderize isso na sua UI pro cliente final.

5. Configure o webhook e valide a assinatura (5 min)

Configure a URL HTTPS de webhook em /dashboard/webhooks (requer PIN). Quando o cliente pagar, a Vitrin envia:

{
  "event": "PAYMENT_RECEIVED",
  "payment": { "id": "pay_xxx", "status": "received", "amount": "99.90" }
}

Valide a assinatura HMAC no header X-Vitrin-Signature (SHA256 do body com seu webhook secret) — código de exemplo na seção 5. É só. Você está integrado.


Próximos passos

Depois desses 5 passos, você provavelmente vai querer:

  • Boleto ou cartão de crédito → seção 3.2 e 3.3
  • Cobrança recorrente → Pix Automático ou Assinaturas
  • Boletos futuros de uma vez (carnê) → Assinatura em carnê
  • Antecipar recebíveis → endpoints /anticipations/
  • Cronograma de recebíveis → GET /balance/scheduled/ (Pix D+1, Boleto D+2, Cartão Nx D+30·n)
  • Defender chargebacks → POST /payments/{id}/evidences/ pra anexar comprovantes

Casos de uso

Situações reais já resolvidas de ponta a ponta. Cada caso tem o passo a passo no painel no Manual da Plataforma, seção 20; aqui ficam os endpoints de cada um.

Caso (manual)EndpointsSeções deste guia
Clube de assinatura com valor por clientePOST /plans/, POST /customers/, POST /subscriptions/ (amount_override), POST /subscriptions/{id}/change-amount/Planos, Assinaturas, Alterar valor
Encomenda sob medida paga antes da produçãoPOST /payment-links/, POST /payment-links/{id}/send-email/, POST /orders/create/3.9, Pedidos
Assinante que prefere boleto: boleto mensal ou carnêPOST /subscriptions/ (BOLETO ou CARNE), POST /subscriptions/{id}/switch-to-carne/, POST /carnes/{id}/send-email/Assinaturas, Assinatura em carnê
Loja virtual própria integrada pela APIPOST /customers/, POST /charges/ + Idempotency-Key, PUT /webhooks/config/, GET /payments/{id}/status/Cobrança avulsa, 3.0, 5. Webhooks
Pagamento com cartão dentro do seu sitePOST /checkout/intents/, POST /checkout/intents/{id}/confirm/, GET /checkout/intents/{id}/status/Pagamento com cartão no navegador
Campanha sazonal com combo, oferta extra e cupomPOST /products/ (bundle), POST /bundle-items/, POST /order-bumps/, POST /coupons/Produtos, Bundle, Order Bumps, Cupons
Vendas pelo link da bio, sem sitePOST /products/ (thank_you_url), PUT /checkout/settings/, PATCH /settings/ (auto_receipt_enabled)Produtos, Configurações do Checkout, Atualizar (parcial)
Cliente fiel que recompra sem digitar o cartãoPOST /customers/{id}/one-click-link/, POST /charges/charge-saved/3.7
Associação ou clube com mensalidade por faixa e aprovaçãoPOST /plans/ (requires_approval), POST /subscriptions/bulk/, POST /subscriptions/{id}/approve/Planos, Assinaturas, Aprovar ou rejeitar
Doação ou contribuição com valor livrePOST /products/ (contribution), POST /plans/ (allow_customer_defined_amount)Produtos, Planos
Débito automático no Pix (Pix Automático)POST /subscriptions/ (PIX_AUTOMATIC), GET /settings/Assinaturas, 3.6
Receber o valor cheio: repasse da taxa e adicional de parcelamentoPATCH /settings/ (fee_passthrough_methods, installment_markup_pct_2_6, installment_markup_pct_7_12)Repasse da taxa, Atualizar (parcial)
Revendedoras e parceiras que ganham comissão (afiliados)PATCH /organization/affiliate-program/, PUT /products/{id}/affiliate-config/, POST /affiliate-applications/{id}/moderate/9.5

SDKs oficiais

Em vez de chamar a API com fetch/requests cru, instale o pacote da sua linguagem. Os dois SDKs cobrem charges, customers, plans, subscriptions, products, balance + verificação de webhook HMAC + retry automático seguro + erros tipados (VitrinAuthError, VitrinValidationError com fieldErrors, VitrinRefusedError, VitrinRateLimitError, etc).

VitrinRefusedError vs VitrinValidationError. As duas nascem de um HTTP 422, e a diferença importa: VitrinRefusedError é recusa do emissor — a cobrança chegou ao banco e voltou negada, com code (ABECS), customerMessage, retryable e transactionId. VitrinValidationError é payload rejeitado — nada foi enviado ao banco. Até o @vitrindigital/node 0.3.1 e o vitrin (Python) 0.2.2 as duas caíam na mesma classe; as versões 0.4.0 e 0.3.0 separam, e isso é uma mudança incompatível para quem tratava recusa dentro de um catch/except de VitrinValidationError. Um catch genérico de VitrinError segue pegando as duas.

Retry automático. GET (e demais métodos idempotentes) é repetido em 429, 5xx, tempo esgotado e erro de rede. Um POST ou PATCH só é repetido quando não há risco de cobrar duas vezes: resposta 429, resposta 5xx com retryable: true, ou conexão que nem abriu (DNS, conexão recusada). Tempo esgotado de leitura e 502/503/504 sem retryable: true não são repetidos. A exceção é o /charges/ e o /charges/charge-saved/ com Idempotency-Key, que a Vitrin deduplica: aí o SDK repete como antes. No 502 incerto de cartão (payment_status: "processing"), o SDK não repete: consulta a transação por até ~60 s e devolve o desfecho (opção uncertainWaitMs / uncertain_wait_seconds; 0 desliga). Até o @vitrindigital/node 0.4.0 e o vitrin (Python) 0.3.0 qualquer 5xx de POST era repetido, inclusive o 502 incerto de cartão.

Node.js

Pacote: @vitrindigital/node — Node 18+, zero deps de runtime.

npm install @vitrindigital/node
# ou: pnpm add @vitrindigital/node    yarn add @vitrindigital/node
import { Vitrin } from '@vitrindigital/node';

const vitrin = new Vitrin({ apiKey: process.env.VITRIN_API_KEY! });

const charge = await vitrin.charges.create({
  customer_id: 'cus_xxx',
  amount: 99.90,
  billing_type: 'PIX',
  idempotencyKey: `pedido-${orderId}`,  // evita duplo-débito em retry
});

Validação de webhook (Express):

import { Webhooks } from '@vitrindigital/node';

app.post('/webhooks/vitrin', express.raw({ type: 'application/json' }), (req, res) => {
  const event = Webhooks.constructEvent({
    payload: req.body,
    signature: req.header('x-vitrin-signature'),
    timestamp: req.header('x-vitrin-timestamp'),
    secret: process.env.VITRIN_WEBHOOK_SECRET!,
  });
  // process event.data ...
  res.sendStatus(200);
});

Detalhes completos em sdk/node/README.md.

Python

Pacote: vitrin — Python 3.10+, única dep requests.

pip install vitrin
import os
from vitrin import Vitrin

vitrin = Vitrin(api_key=os.environ["VITRIN_API_KEY"])

charge = vitrin.charges.create(
    customer_id="cus_xxx",
    amount=99.90,
    billing_type="PIX",
    idempotency_key=f"pedido-{order_id}",
)

Validação de webhook (Flask):

from flask import Flask, request
from vitrin import webhooks, VitrinError
import os

app = Flask(__name__)

@app.post("/webhooks/vitrin")
def handle():
    try:
        event = webhooks.construct_event(
            payload=request.get_data(),
            signature=request.headers.get("X-Vitrin-Signature"),
            timestamp=request.headers.get("X-Vitrin-Timestamp"),
            secret=os.environ["VITRIN_WEBHOOK_SECRET"],
        )
    except VitrinError as e:
        return str(e), 400
    # process event.data ...
    return "", 200

Detalhes completos em sdk/python/README.md.

Outras linguagens

Sem SDK oficial ainda. Use a API REST direto — exemplos em cURL ao longo deste guia.


Tipos de organização

A Vitrin suporta dois tipos de Organization, escolhidos automaticamente pelo entity_type no momento do signup:

  • Empresas brasileiras (CNPJ): Pix, Pix Automático, Boleto, cartão doméstico e antecipação de recebíveis, em BRL, usando rails domésticos brasileiros.
  • Empresas globais (US LLC): cartão por padrão e Pix cross-border (o cliente paga em BRL, a LLC recebe em USD), com os valores escriturados em USD (campo currency). Boleto não se aplica a LLC (é Brasil-only); antecipação e saque manual também não — a liquidação é automática na conta bancária da LLC. O onboarding inclui um passo de verificação de identidade (hospedado ou embedded na dashboard). LLC: Apple Pay e Google Pay disponíveis no checkout do integrador — veja api/wallets-llc.md para habilitar, registrar domínios e renderizar os botões. Veja api/onboarding-llc.md para o fluxo completo (incl. embedded) e api/differences-cnpj-llc.md para a tabela comparativa.

O roteamento interno é abstraído na API — os mesmos endpoints atendem ambos os tipos de org, e os webhooks usam o mesmo vocabulário canônico de eventos (PAYMENT_RECEIVED, PAYMENT_REFUSED, etc.). As diferenças visíveis ao integrador (campo currency = USD para LLC, métodos disponíveis em payment_method, o passo de onboarding/KYC, e a confirmação de cartão via client_secret/SCA para LLC) estão documentadas em api/differences-cnpj-llc.md. Endpoints de tesouraria (saldo, extrato, transferências, recebíveis) retornam, para LLC, uma resposta auto_payout em vez de saldo retido.


1. Primeiros Passos

Cadastro e API key (humano, no dashboard)

Cadastro, verificação documental, definição do PIN e geração da primeira API key são feitos no dashboard web — não há endpoint REST pra essas ações. Isso é proposital: o cadastro precisa de upload de documentos físicos, validação do CNPJ e definição do PIN que protege ações sensíveis. Tudo isso é interação humana.

Como obter suas chaves:

  1. Acesse vitrin.digital → Criar conta
  2. Preencha CNPJ, razão social, endereço, dados do sócio responsável e defina seu PIN de 6 dígitos
  3. Anote a vd_test_* que aparece na tela final — você já pode integrar e testar imediatamente
  4. Aguarde aprovação (1-2 dias úteis). Quando aprovado, a vd_live_* chega no seu email registrado — plaintext exibida uma única vez. Guarde em local seguro
  5. Se perder a vd_live_*, regenere pelo dashboard ou via POST /auth/api-key/regenerate/ (com PIN)

Autenticação

Cada organização recebe 2 chaves com momentos de geração diferentes:

ChavePrefixoQuando é geradaQuando funciona
Teste (sandbox)vd_test_*No signup — exibida na telaImediatamente após cadastro
Produçãovd_live_*No momento da aprovação — enviada por emailApós aprovação + contratos assinados

A vd_test_* permite cobranças sandbox, gestão de clientes, e teste de webhooks antes da aprovação. Cobranças sandbox são simulação (sem dinheiro real). Não existem em sandbox: link de pagamento (§3.9) e pedido com link (POST /orders/create/ sem billing_type, §7) — a página pública cobraria de verdade, então os dois respondem 400 com a chave de teste. Quando estiver pronto pro ar, troque pra vd_live_* em produção.

Todas as requisições usam a API key no header:

Authorization: Bearer vd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Ou pra sandbox:

Authorization: Bearer vd_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Ambas as chaves autenticam nos mesmos endpoints. A diferença é qual ambiente de processamento é usado (produção vs sandbox) e quais status de org são aceitos (vd_test_* aceita inclusive pending_review).

A chave secreta nunca vai ao navegador

vd_live_* e vd_test_* são secretas: quem tem uma delas lê clientes e transações, cria cobranças com qualquer valor e estorna. Guarde-as só no seu servidor (variável de ambiente, cofre de segredos) e nunca as coloque em HTML, JavaScript de página, app mobile ou repositório público.

Para o navegador existem as chaves publicáveis, uma por ambiente:

ChavePrefixoOnde verO que faz
Publicável de produçãovd_pk_live_*Conta → Configurações → Chaves de APISó POST /checkout/tokenize/, GET /checkout/config/, POST /checkout/3ds-skip/, POST /checkout/intents/{id}/confirm/ e GET /checkout/intents/{id}/status/
Publicável de testevd_pk_test_*Conta → Configurações → Chaves de APIO mesmo, no sandbox

A chave publicável é pública por natureza: pode ir na página. Em qualquer outro endpoint ela recebe 401, e ela não escolhe valor nem cliente: a cobrança no navegador só acontece por uma intenção de pagamento criada pelo seu servidor (§3.3, "Pagamento com cartão no navegador"). Se quiser trocá-la, use "Gerar nova" em Conta → Configurações → Chaves de API (pede o PIN); a antiga para de valer na hora.

Rotacionar/recuperar a API key

A vd_live_* é exibida apenas uma vez no email de aprovação. Se perder ou quiser rotacionar (boas práticas, CI/CD, key comprometida), regenere via API com PIN do dono:

POST /api/v1/auth/api-key/regenerate/

{ "pin": "123456", "key_type": "live" }

A resposta inclui a nova api_key plaintext (também só uma vez). Armazene imediatamente — a antiga é invalidada na hora e qualquer chamada subsequente retorna 401.

Pra rotacionar a chave de teste, use "key_type": "test". Default é live.

Por que o PIN está aqui? Rotação invalida a chave anterior. O PIN garante que o dono autorizou a operação, mesmo que alguém com acesso temporário à API key faça a chamada.


1.5. Status do onboarding (read-only)

Após o cadastro a org passa por verificação documental antes de poder cobrar e sacar. O upload dos documentos é feito no dashboard em /dashboard/onboarding — sem cURL, sem API. Sua aplicação pode, no entanto, consultar o status do onboarding pra mostrar mensagens contextuais ou habilitar funcionalidades só quando a org estiver liberada:

GET /api/v1/onboarding/status/

Resposta:

{
  "account_ready": true,
  "org_status": "onboarding",
  "document_status": "PENDING",
  "account_status": {
    "general": "PENDING",
    "documentation": "AWAITING_APPROVAL",
    "commercial_info": "APPROVED",
    "bank_account_info": "APPROVED"
  }
}

Quando account_status.general == "APPROVED", a org passa automaticamente para org_status='active' e libera saques. O dashboard reflete isso em tempo real.


1.6. Contratos eletrônicos

A Vitrin opera sob 4 contratos comerciais (Termos de Uso, Anexo Comercial, Política de Chargebacks e Política de Privacidade) que precisam estar assinados antes que sua org consiga gerar cobranças. Tentativas de cobrar antes da assinatura retornam HTTP 403:

{ "detail": "Voce precisa assinar todos os contratos comerciais antes de operar. Acesse /dashboard/contract para revisar e assinar." }

Como assinar (humano, no dashboard)

A assinatura é feita pelo dono da org em /dashboard/contract. Lá ele revisa cada contrato (com as taxas reais preenchidas no Anexo Comercial), faz scroll até o fim e confirma com PIN. Não há endpoint REST pra assinar — assinatura eletrônica avançada exige interação humana inequívoca.

Consultar status (programático)

Antes de cada chamada de cobrança, sua aplicação pode (opcionalmente) checar se está tudo em ordem:

GET /api/v1/contracts/status/

Resposta:

{
  "all_signed": true,
  "pending": [],
  "active": [
    {
      "id": "uuid",
      "contract_slug": "terms-of-use",
      "contract_title": "Termos de Uso",
      "contract_version": 1,
      "signed_at": "2026-04-10T12:34:56Z",
      "valid_until": null,
      "status": "active"
    }
  ]
}

Se all_signed == false, redirecione o operador para /dashboard/contract.

Re-assinatura quando o contrato muda

Se a Vitrin publicar uma nova versão de qualquer contrato (ex: atualização da Política de Chargebacks), o pending volta a ter itens com reason: "new_version" e a operação fica bloqueada até nova assinatura. Você é avisado por e-mail com 30 dias de antecedência.


2. Clientes

Antes de cobrar, cadastre o pagador.

Criar

POST /api/v1/customers/

{
  "name": "Maria Silva",
  "email": "maria@email.com",
  "cpf_cnpj": "12345678909",
  "phone": "11999887766"
}

Resposta: retorna id do cliente (UUID) para usar nas cobranças.

Campos opcionais — pessoais:

{
  "birth_date": "1990-03-15",        // data de nascimento (PF)
  "company_name": "Empresa LTDA",     // razão social (PJ)
  "notes": "Cliente VIP — desconto",  // observação interna, não visível ao cliente
  "custom_data": {                    // respostas dos campos customizados do checkout
    "como_conheceu": "Instagram",
    "tamanho_camiseta": "M"
  }
}

Campos opcionais — endereço (recomendado para produtos físicos e defesa de chargeback):

{
  "postal_code": "01001000",
  "address": "Rua das Flores",
  "address_number": "42",
  "complement": "Apto 12",
  "province": "Centro",
  "city": "São Paulo",
  "state": "SP"
}

O dashboard auto-preenche endereço via ViaCEP quando o pagador digita o CEP. Em integrações server-side você pode replicar essa lógica chamando https://viacep.com.br/ws/{cep}/json/.

Listar

GET /api/v1/customers/?page=1

Resposta paginada:

{
  "count": 142,
  "next": "https://api.vitrin.digital/api/v1/customers/?page=2",
  "previous": null,
  "results": [ { "id": "uuid", "name": "Maria Silva", ... } ]
}

Filtros aceitos via query string: email, cpf_cnpj, name__icontains.

Detalhes

GET /api/v1/customers/{id}/

Atualizar

PUT  /api/v1/customers/{id}/    # substitui todos os campos
PATCH /api/v1/customers/{id}/   # atualiza só os enviados

Excluir

DELETE /api/v1/customers/{id}/

Excluir um cliente que tem transações associadas é bloqueado — você recebe HTTP 409. Use para limpar cadastros que nunca pagaram.


3. Cobranças

Cobrança avulsa (POST /charges/)

Um endpoint para cobrar em qualquer meio: Pix, boleto ou cartão. É o que aceita Idempotency-Key (§3.0), então use-o quando o seu sistema cria a cobrança e pode repetir a requisição.

POST /api/v1/charges/
Idempotency-Key: pedido-4711

{
  "customer_id": "uuid-do-cliente",
  "amount": "89.90",
  "billing_type": "PIX",
  "description": "Pedido 4711"
}
CampoObrigatórioDescrição
customer_idsimCliente da sua organização (§2)
amountsimValor da cobrança
billing_typesimPIX, BOLETO ou CREDIT_CARD, entre os métodos ativos da conta
descriptionnãoAté 500 caracteres. No Pix, também distingue cobranças do mesmo valor (§3.0)
due_datenãoVencimento (AAAA-MM-DD)
installmentsnãoParcelas no cartão (padrão 1). Acima do teto da conta (org_max_installments, até 12x) devolve 400; as opções saem em GET /checkout/installments/ (§3.4)
credit_card_tokenno cartãoToken de POST /checkout/tokenize/ (§3.3, Passo 1)
authenticationnãoResultado do 3DS (§3.3, Passo 3)
installment_markup_pctnãoAdicional de parcelamento só desta cobrança, em %, no lugar do da conta. Só no cartão parcelado, com o adicional liberado para a conta e até o limite da faixa (2–6x ou 7–12x); fora disso, 400

A resposta é 201 com a transação: id, status, amount, net_amount, billing_type, installments, pix_qr_code e pix_copy_paste (Pix), boleto_url e boleto_barcode (boleto), due_date, card e refusal (cartão), entre outros. O mesmo objeto sai em GET /transactions/{id}/.

  • Repetir a mesma Idempotency-Key devolve a cobrança já criada, com o mesmo corpo (§3.0).
  • 409 charge_in_flight: uma cobrança idêntica ainda está sendo criada. Repita em alguns segundos (§3.0).
  • Cartão recusado: 422 (Recusas: como tratar). Cartão com resultado incerto: 502 (Processamento indisponível).
  • O pagamento confirmado chega pelo webhook PAYMENT_RECEIVED (§5). Para o Pix, dá para consultar o status enquanto o cliente olha o QR Code (§4).

3.0 Evitando cobrança duplicada

Duas proteções, uma que você controla e uma automática.

Idempotency-Key (recomendado)

Mande o header Idempotency-Key nos POSTs que criam cobrança. Repetir a mesma chave na mesma organização devolve a cobrança já criada — mesmo 201, mesmo corpo, sem cobrar de novo no processamento.

POST /api/v1/charges/
Idempotency-Key: pedido-4711

Use um identificador do seu lado (número do pedido, id do carrinho). A proteção vem justamente de a chave se repetir no retry — gerar um UUID novo a cada tentativa não protege de nada. Até 128 caracteres; o escopo é por organização, então sua chave nunca colide com a de outro lojista.

A chave deve identificar a intenção de compra, não o carregamento da página. Se ela for gerada a cada render, cada tentativa vira uma chave nova e nenhuma repetição é reconhecida. Amarre-a ao pedido/carrinho e reenvie a mesma em toda retentativa.

Uma chave nova que não corresponde a nenhuma cobrança não desativa o reaproveitamento automático descrito abaixo: se houver um Pix pendente idêntico e válido, ele é devolvido de qualquer forma. Mandar chave nunca deixa você menos protegido do que não mandar.

Vale em POST /charges/ e POST /charges/charge-saved/. Nos SDKs:

await vitrin.charges.create({ /* ... */, idempotencyKey: `pedido-${orderId}` });
vitrin.charges.create(..., idempotency_key=f"pedido-{order_id}")

Reaproveitamento automático de Pix pendente

Mesmo sem Idempotency-Key, um POST /api/v1/charges/ pode devolver uma cobrança que já existia em vez de criar outra. Acontece quando todos estes pontos batem:

  • método PIX e status pending;
  • mesmo cliente, mesmo valor e mesma description;
  • criada há menos de ~55 minutos (o QR Pix vale 1 hora);
  • a cobrança tem QR de fato — uma que falhou ao nascer nunca é reaproveitada.

A resposta é 201 com o corpo de sempre, então nada muda no seu código — mas não estranhe receber um id que você já tinha visto. É o comportamento correto: é o mesmo pagamento, e o cliente segue pagando o QR que já está na tela dele.

409 — cobrança idêntica em trânsito

Se você disparar dois pedidos idênticos quase ao mesmo tempo, o segundo pode receber 409 com error: "charge_in_flight":

{
  "error": "charge_in_flight",
  "detail": "Uma cobrança idêntica para este cliente está sendo criada agora. Tente novamente em alguns segundos — a cobrança existente será devolvida."
}

Significa que a primeira cobrança ainda está sendo processada e não tem QR para devolver. Trate como "tente de novo em instantes", nunca como erro definitivo — repetir o pedido devolve a cobrança que ficou pronta.

Antes de devolver 409 a API espera o QR por até 12 segundos — dimensionado para cobrir a cauda da latência de criação de cobrança. Na prática, portanto, este status quase não acontece: o normal é o segundo pedido receber 201 com a cobrança que ficou pronta.

Criar uma segunda cobrança nesse instante seria a duplicata que esta proteção existe para evitar — por isso o 409 em vez de uma cobrança nova.

Se você usa um cliente HTTP com retry automático, verifique se ele retenta em timeout e não só em 5xx. Timeout é justamente o caso em que o servidor recebeu e processou o pedido: sem Idempotency-Key, cada retry nasce como uma cobrança nova. Com a chave, todos convergem para a mesma.

Cobrança já paga nunca é reaproveitada: recompra do mesmo item continua gerando cobrança nova. Cartão e boleto também não são reaproveitados — cartão cobra na hora e boleto tem vencimento em dias.

Se a sua loja vende produtos diferentes pelo mesmo preço, garanta que a description seja diferente entre eles. É ela que distingue "10 músicas" de "12 músicas" a R$ 47,00.

3.1 Pix

POST /api/v1/checkout/pix/

{
  "customer_id": "uuid-do-cliente",
  "amount": 99.90,
  "description": "Plano Mensal"
}

Resposta:

{
  "payment_id": "uuid-da-transacao",
  "qr_code": "base64-do-qr-code",
  "copy_paste": "00020101021226880014br.gov.bcb...",
  "amount": 99.90,
  "status": "pending"
}

O frontend da sua app exibe o QR code (qr_code em base64) ou o código copia-e-cola (copy_paste).

3.2 Boleto

POST /api/v1/checkout/boleto/

{
  "customer_id": "uuid-do-cliente",
  "amount": 99.90,
  "description": "Plano Mensal"
}

Resposta:

{
  "payment_id": "uuid-da-transacao",
  "barcode": "23793.38128 60000.000003 00000.000406 1 84340000009990",
  "url": "https://vitrin.digital/b/pdf/xxxx",
  "pix_copy_paste": "00020101021226900014br.gov.bcb.pix2568…6304ABCD",
  "amount": 99.90,
  "status": "pending"
}

Boleto pago ou baixado confirmado depois: o pagamento ou a baixa de um boleto vencido às vezes só é confirmado alguns dias depois, numa conferência diária da Vitrin. Ele fecha aqui com o evento de sempre (PAYMENT_RECEIVED ou PAYMENT_DELETED), enviado no momento da conferência e não no do pagamento ou da baixa (veja Ciclo de vida e datas).

Boleto com Pix: o boleto também pode ser pago por Pix. pix_copy_paste é o Pix copia e cola do mesmo boleto: pagar por ele quita o boleto, com o mesmo valor, vencimento, multa e juros. Vem null quando o boleto não tem Pix. O campo está também em GET /transactions/{id}/, nos webhooks PAYMENT_* do boleto (§5) e em cada parcela de carnê. Para mostrar um QR Code, gere a imagem a partir desse texto.

Multa e juros: o boleto avulso (esta seção e POST /charges/ com BOLETO) e as parcelas de carnê saem com multa de 2% e juros de mora de 0,4% sobre o valor, cobrados pelo banco se o boleto for pago depois do vencimento. O boleto da assinatura no boleto mensal (billing_type: "BOLETO") não leva essas condições. O boleto impresso mostra as duas taxas no campo "(+) Juros / Multa" (0,400% / 2,000%). Ainda não são configuráveis por conta.

3.3 Cartão de Crédito

Passo 1: Tokenizar o cartão

POST /api/v1/checkout/tokenize/

{
  "customer_id": "uuid-do-cliente",
  "credit_card": {
    "holderName": "MARIA SILVA",
    "number": "5162306219378829",
    "expiryMonth": "01",
    "expiryYear": "2028",
    "ccv": "318"
  },
  "credit_card_holder_info": {
    "name": "Maria Silva",
    "email": "maria@email.com",
    "cpfCnpj": "12345678909",
    "postalCode": "01001000",
    "addressNumber": "42",
    "phone": "11999887766"
  }
}

Resposta:

{
  "credit_card_token": "tok_xxxxxxxxxxxxx",
  "credit_card_number": "5162 **** **** 8829",
  "credit_card_brand": "MASTERCARD",
  "three_ds": "try"
}

three_ds é a política 3DS da sua organização (off, try ou require) — ver o Passo 3.

No lugar de customer_id, o tokenize aceita intent_id + client_secret de uma intenção de pagamento: o cliente vem da intent. É o que o navegador usa com a chave publicável (ver "Pagamento com cartão no navegador" abaixo). Com customer_id nada muda. A Vitrin guarda a bandeira deste token por algumas horas para conferir o ECI da cobrança contra ela.

Passo 2: Cobrar com o token

POST /api/v1/checkout/pay/

{
  "customer_id": "uuid-do-cliente",
  "amount": 299.90,
  "billing_type": "CREDIT_CARD",
  "installments": 3,
  "credit_card_token": "tok_xxxxxxxxxxxxx",
  "description": "Pacote Premium"
}

Dados do cartão nunca passam pelo seu servidor. A tokenização é feita via API e o token é usado para cobranças futuras.

Passo 3: 3D Secure

Política 3DS (three_ds). A Vitrin decide por organização se o 3DS roda, e a decisão é a mesma no checkout hospedado, no link de pagamento e na API. Ela combina o modo do seu Anexo Comercial com a escolha da organização em Conta → Configurações → Confirmação do banco no cartão (3DS) — opção que só aparece no painel quando o 3DS está disponível para a conta (Anexo Comercial em optional ou required, e conta que não é LLC):

Modo do AnexoConfiguração da orgthree_dsCobrança de cartão
disableda opção não apareceoffauthentication é ignorado
optional (padrão)desligado (padrão)offauthentication é ignorado
optionalligadotryautenticação válida → cobra com 3DS; ausente ou inválida → cobra sem 3DS. Nunca recusa por 3DS
requiredtravado ligadorequiresem autenticação válida → 422 (abaixo), com as isenções
conta LLC (EUA)a opção não apareceoffa autenticação é feita automaticamente; authentication não é usado

Como saber a política: o campo three_ds vem na resposta do POST /checkout/tokenize/ (Passo 1) e em GET /api/v1/checkout/config/:

GET /api/v1/checkout/config/
Authorization: Bearer vd_live_xxxxx

{ "three_ds": "try" }

Regra para a sua integração: com off, não rode o 3DS (não adianta mandar). Com try, rode e, se não autenticar, cobre sem authentication. Com require, rode e não cobre se a autenticação falhar — a Vitrin recusaria com 422.

Por que autenticar. A venda com 3DS válido tem condições melhores de antecipação do que a venda sem 3DS, conforme as condições da sua conta. A antecipação é sempre sujeita a análise e aprovação da Vitrin e às condições do seu Anexo Comercial, e só existe se estiver liberada na sua conta. O 3DS também transfere ao emissor o risco da contestação por fraude ("transação não reconhecida") em compra autenticada, conforme a regra de cada bandeira; as demais contestações (produto não entregue, desacordo comercial, duplicidade) continuam com a loja.

O antifraude, se ligado, continua valendo com 3DS. Com o antifraude da conta ligado, toda cobrança de cartão passa por ele, com ou sem 3DS. Veja Antifraude abaixo.

O que conta como autenticação. Envie o authentication exatamente como o 3DS devolveu (o vitrin.js já monta no formato certo). A Vitrin decide se ele indica autenticação para a bandeira do cartão; o que não indica é tratado como cobrança sem 3DS. Mesmo autenticada, a cobrança pode ser rebaixada pelo emissor na autorização: confira is_3ds_authenticated na Transaction.

Onde enviar authentication. POST /checkout/pay/, POST /charges/ (cartão), POST /orders/create/ (cartão) e POST /checkout/guest/ (cartão). Todos seguem a política acima.

Checkout hospedado e link de pagamento. O checkout /c/... e o link /p/... seguem a política sozinhos: com try/require, antes de pagar com cartão o comprador passa pela tela "Confirmando com o seu banco" (e pelo código do banco, quando ele pedir), com o mesmo valor e parcelas da cobrança. Com try, se o banco não confirmar a venda segue sem 3DS; com require, o cartão não é cobrado e o comprador é orientado a tentar outro cartão ou pagar com Pix ou boleto. Pix e boleto não passam pelo 3DS. No checkout de plano, o 3DS só roda com require (a cobrança recorrente não leva a autenticação ao processamento). Você não precisa fazer nada além de escolher a política.

Recusas por 3DS (política require). A cobrança não chega a ser processada. A tentativa é persistida como Transaction com status='failed' (GET /api/v1/transactions/?status=failed e o painel mostram) e a resposta é HTTP 422, sem retryable:

return_codeQuandoerror
VITRIN_3DS_MISSINGSem authentication (ou sem eci)"Cobrança recusada — 3DS obrigatório."
VITRIN_3DS_NOT_AUTHENTICATEDauthentication presente, mas não indica autenticação para a bandeira"Cobrança recusada — autenticação 3DS não concluída."
{
  "error": "Cobrança recusada — autenticação 3DS não concluída.",
  "description": "Autenticação 3DS obrigatória não concluída ou não aceita. Refaça a autenticação ou use outro cartão.",
  "customer_message": "Não foi possível processar este cartão. Por favor, use outro cartão ou tente novamente.",
  "retryable": false,
  "return_code": "VITRIN_3DS_NOT_AUTHENTICATED",
  "three_ds_mode": "required",
  "transaction_id": "757b6c0c-323f-4dda-9089-c823fe3179f3"
}

O customer_message é neutro — não expõe a política ao pagador. No checkout hospedado e no link, a recusa usa "Não foi possível confirmar este cartão com o seu banco. Tente outro cartão ou pague com Pix ou boleto.". Tentativas de cartão recusadas por 3DS no checkout hospedado e no link têm teto por item + pagador e por IP; acima dele o cartão fica indisponível por uma hora (HTTP 429, blocked_method: "CREDIT_CARD") e Pix/boleto seguem.

Isenções na política require:

  • Sem o comprador presente: cobranças e renovações de assinatura (/subscriptions/ e as ações approve, change_amount e bulk), cartão salvo cobrado pela sua API (/charges/charge-saved/) e one-click.
  • Bandeiras sem 3DS (JCB, Aura, Diners, Discover): isentas quando a Vitrin identificou a bandeira na tokenização — no checkout hospedado e no link (a Vitrin tokeniza) e nas cobranças por token gerado no /checkout/tokenize/ desta organização. A bandeira informada pelo cliente não é aceita; token de origem desconhecida segue a regra geral.

Hipercard não é aceito. O cartão Hipercard é recusado já na tokenização, com uma mensagem ao comprador para usar outro cartão.

Modo recomendado — via vitrin.js (SDK oficial, 1.3.0):

O SDK lê a política sozinho (do Vitrin.tokenize ou de GET /checkout/config/, também exposto como Vitrin.getConfig()) e ajusta o Vitrin.authenticate3DS:

three_dsVitrin.authenticate3DS
offresolve null na hora, sem carregar o script de 3DS e sem desafio
tryresolve com a autenticação; resolve null em cartão não inscrito, bandeira sem 3DS, falha, erro, timeout ou comprador que desiste
requireresolve com a autenticação; resolve null em cartão não inscrito ou bandeira sem 3DS; rejeita em falha (3ds_failed), erro (3ds_error), falha ao iniciar (3ds_mpi_init_failed — recarregue a página), timeout (3ds_timeout) ou valor alterado (3ds_amount_changed)

Sem conseguir ler a política, o SDK age como require. Integrações que não chamam o SDK continuam funcionando como antes (tokenize + pay).

Cobrança feita no navegador? Use Vitrin.confirmCardPayment com uma intenção de pagamento (seção seguinte): ele já tokeniza, roda o 3DS pela política e confirma. O exemplo abaixo é para quem cobra pelo servidor (/checkout/pay/) e só usa o navegador para tokenizar e autenticar.

<script src="https://vitrin.digital/vitrin.js"></script>
<script>
// Chave PUBLICÁVEL: tokeniza e autentica. A cobrança sai do SEU servidor,
// com a chave secreta guardada nele.
Vitrin.init({ publishableKey: 'vd_pk_live_xxxxx' });

async function pagar(card, pedido) {
  // 1. Tokeniza o cartão (a resposta traz `three_ds`; o SDK guarda)
  const { token } = await Vitrin.tokenize({
    customerId: pedido.customerId,
    holderName: card.holderName,
    number: card.number,
    expiryMonth: card.expiryMonth,
    expiryYear: card.expiryYear,
    cvv: card.cvv,
  });

  // 2. Autentica via 3DS no navegador, seguindo a política da org. O script
  //    de 3DS precisa do número do cartão (não do token).
  let auth = null;
  try {
    auth = await Vitrin.authenticate3DS({
      card: {
        number: card.number,
        expiryMonth: card.expiryMonth,
        expiryYear: card.expiryYear,
        holderName: card.holderName,
      },
      amount: pedido.amount,          // o mesmo valor da cobrança
      installments: pedido.installments,
      cardToken: token,               // só telemetria
    });
  } catch (err) {
    // Só acontece com `require`: mostre ao comprador e não cobre.
    mostrarErro('Seu banco não confirmou a compra. Tente outro cartão.');
    return;
  }

  // 3. Manda token + auth ao SEU servidor, que chama /checkout/pay/.
  await fetch('/meu-backend/pagar', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ pedidoId: pedido.id, cardToken: token, authentication: auth }),
  });
}
</script>

O SDK trata automaticamente:

  • Carregamento lazy do script de 3DS (só baixa quando a política pede)
  • Sequência do script: inicialização com o valor, início da autenticação quando ele fica pronto e desafio do emissor quando houver
  • Timeout (padrão 3 minutos, ajustável com timeoutMs)
  • Resultado já no formato do authentication (eci com 2 dígitos, cavv, xid, ds_trans_id, protocol_version, reference_id)

O script de 3DS considera só o valor da primeira autenticação da página. Uma nova autenticação com outro valor é recusada com 3ds_amount_changed (em try, resolve null): recarregue a página.

O 3DS só funciona em produção — o ambiente de testes (vd_test_*) não autentica.

Exemplo — cobrança com 3DS seguindo three_ds (servidor):

// Node, no SEU servidor. `vd_live_*` nunca vai ao navegador.
async function cobrarCartao({ customerId, amount, installments, cardToken, authentication, threeDs }) {
  const body = { customer_id: customerId, amount, installments,
                 billing_type: 'CREDIT_CARD', credit_card_token: cardToken };
  // off: não adianta mandar. try/require: manda o que o navegador obteve.
  if (threeDs !== 'off' && authentication) body.authentication = authentication;

  const r = await fetch('https://api.vitrin.digital/api/v1/checkout/pay/', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.VITRIN_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const tx = await r.json();
  if (r.status === 422 && tx.return_code?.startsWith('VITRIN_3DS_')) {
    // `require` sem autenticação válida: peça outro cartão ou Pix/boleto.
  }
  return tx; // tx.is_3ds_authenticated = o que a confirmação trouxe
}

Modo manual (app mobile ou MPI próprio): monte o authentication com o resultado do seu MPI e envie no POST /checkout/pay/ (ou /charges/, /orders/create/, /checkout/guest/):

POST /api/v1/checkout/pay/

{
  "customer_id": "uuid-do-cliente",
  "amount": 299.90,
  "billing_type": "CREDIT_CARD",
  "installments": 3,
  "credit_card_token": "tok_xxxxxxxxxxxxx",
  "description": "Pacote Premium",

  "authentication": {
    "eci": "05",
    "cavv": "AAABBBCCCDDDEEEFFFGGG=",
    "xid": null,
    "ds_trans_id": null,
    "protocol_version": "2",
    "reference_id": "973cf83d-b378-43d5-84b6-ce1531475f2a"
  }
}

Bandeiras com 3DS: Visa e Mastercard (3DS 2.2), Elo e Amex (3DS 2.1). JCB, Aura, Diners e Discover não têm 3DS — cobre sem authentication. Hipercard não é aceito (veja as isenções acima).

Pagamento com cartão no navegador (chave publicável + intent)

Quando o comprador paga numa página sua, o cartão é digitado no navegador. Para cobrar ali sem expor a chave secreta e sem deixar o navegador escolher o valor, o fluxo tem duas metades:

  1. O seu servidor cria a intenção de pagamento com a chave secreta. O valor, o cliente e o teto de parcelas ficam fixos nela.
  2. A página confirma com a chave publicável e o client_secret da intent. O Vitrin.confirmCardPayment tokeniza o cartão, roda o 3DS pela política da org e confirma.

Regra: a chave secreta (vd_live_*/vd_test_*) nunca vai ao navegador. Na página, só a publicável (vd_pk_live_*/vd_pk_test_*) e o client_secret da intent daquela compra.

1. Servidor: criar a intent

POST /api/v1/checkout/intents/
Authorization: Bearer vd_live_xxxxx

{
  "customer_id": "uuid-do-cliente",
  "amount": "299.90",
  "description": "Pedido 4711",
  "max_installments": 6,
  "external_reference": "pedido-4711",
  "metadata": { "pedido": 4711 }
}
{
  "id": "8b0e5c1e-1111-4222-8333-944455556666",
  "client_secret": "8b0e5c1e-1111-4222-8333-944455556666_secret_Xy...",
  "status": "requires_payment",
  "amount": "299.90",
  "customer_id": "uuid-do-cliente",
  "max_installments": 6,
  "expires_at": "2026-09-25T16:00:00-03:00",
  "...": "..."
}
  • O client_secret aparece só nesta resposta. Entregue à página só id e client_secret daquela compra; não o guarde em log.
  • A intent vale por 60 minutos e é de uso único. Crie uma por tentativa de compra.
  • installments no confirm fica limitado ao menor entre max_installments e o teto de parcelas da conta (acima disso, 400).
  • metadata (até 20 chaves, 4 KB) e external_reference voltam no webhook de pagamento, no bloco payment_intent.
  • Intent criada com vd_test_* confirma com vd_pk_test_*; com vd_live_*, com vd_pk_live_*.

2. Navegador: confirmar com o SDK

<script src="https://vitrin.digital/vitrin.js"></script>
<script>
Vitrin.init({ publishableKey: 'vd_pk_live_xxxxx' });

async function pagar(intent, card, parcelas) {
  try {
    const r = await Vitrin.confirmCardPayment(intent.id, intent.client_secret, {
      installments: parcelas,           // até o teto da intent
      card: {
        holderName: card.holderName,    // como impresso no cartão
        number: card.number,
        expiryMonth: card.expiryMonth,
        expiryYear: card.expiryYear,
        cvv: card.cvv,
      },
    });
    if (r.status === 'succeeded') return mostrarSucesso();
    if (r.status === 'processing') {
      // A cobrança PODE ter sido feita. Não cobre de novo: aguarde o webhook.
      return mostrarAguardando('Estamos confirmando seu pagamento com o banco.');
    }
    if (r.status === 'failed') {
      // Desfecho final: a intent esgotou as 3 tentativas e não aceita outra.
      // Peça ao seu servidor uma intent nova (ou ofereça Pix/boleto).
      return mostrarErro('Não foi possível pagar com cartão. Tente Pix ou boleto.');
    }
    // 'requires_payment': a tentativa não foi cobrada; pode tentar de novo
    // com a mesma intent. `r.transaction_id` pode vir preenchido: é o da
    // tentativa liberada, que não foi cobrada.
    return mostrarErro('Não foi possível concluir. Tente novamente.');
  } catch (err) {
    if (!(err instanceof Vitrin.Error)) throw err;
    if (err.code === 'card_declined') {
      mostrarErro(err.details.customer_message);   // retryable, attempts_remaining
    } else if (err.code === '3ds_required' || err.code.startsWith('3ds_')) {
      mostrarErro('Seu banco não confirmou a compra. Tente outro cartão ou Pix.');
    } else if (err.code === 'intent_expired' || err.code === 'intent_unavailable') {
      // Peça ao seu servidor uma intent nova (ou confira se já foi paga).
    } else {
      mostrarErro('Não foi possível concluir o pagamento agora.');
    }
  }
}
</script>

O confirmCardPayment faz, nesta ordem:

  1. lê a intent em GET /checkout/intents/{id}/status/ (valor e teto de parcelas);
  2. confere installments contra o teto, sem chamar a rede de novo se passar dele;
  3. tokeniza o cartão pela intent (intent_id + client_secret; o cliente vem dela);
  4. roda o 3DS pela política da org, com o valor da intent;
  5. confirma.

O valor e o cliente cobrados são sempre os da intent: a página não os informa, e o confirm não aceita valor. customerId ou amount passados ao SDK são ignorados, com um aviso no console.

O que confirmCardPayment devolve

ResultadoQuandoO que fazer
{ status: 'succeeded', transaction_id }Cobrança aceita no processamentoMostre sucesso. A liquidação segue pelo webhook de pagamento
{ status: 'processing', transaction_id }A cobrança está em análise no processamento (HTTP 202), ou o confirm ficou sem desfecho claro (ver "Resposta ambígua" abaixo) e a consulta de status não o resolveu em ~60 sNão cobre de novo. O desfecho chega pelo webhook e por GET /checkout/intents/{id}/ no seu servidor
{ status: 'requires_payment', transaction_id }Depois de uma resposta ambígua, a intent foi liberada sem cobrança. transaction_id é o da tentativa liberada (pode vir preenchido) e não quer dizer cobrançaPode tentar de novo com a mesma intent
{ status: 'failed', transaction_id }Depois de uma resposta ambígua, a consulta encontrou a 3ª recusa: a intent esgotou as tentativasDesfecho final, não tente de novo com esta intent: crie outra no servidor ou ofereça Pix/boleto
Vitrin.Error card_declined (422)Recusa do emissorMostre details.customer_message. A intent aceita 3 tentativas; na 3ª recusa vira failed (details.attempts_remaining)
Vitrin.Error 3ds_required (422)A org exige 3DS e a autenticação não valeuOutro cartão, Pix ou boleto
Vitrin.Error 3ds_failed, 3ds_timeout, 3ds_error, 3ds_mpi_init_failedPolítica require e o 3DS falhou no navegadorO confirm não é enviado; outro cartão ou recarregue a página
Vitrin.Error intent_not_found (404)id/client_secret errados ou de outra conta, ou a intent deixou de aceitar pagamento durante o tokenizeConfira o que o servidor entregou à página
Vitrin.Error intent_unavailable (409, ou lida antes do tokenize)Intent já paga, em processamento ou failed (details.intent_status)Consulte a intent no servidor; crie outra se preciso
Vitrin.Error intent_expired (410, ou lida antes do tokenize)Passaram 60 minutosCrie outra intent
Vitrin.Error invalid_request (400)Parcelas acima do teto (details.max_installments), ou chave de outro ambienteAjuste as parcelas / a chave
Vitrin.Error provider_unavailable (5xx com retryable: true)Processamento indisponível, sem cobrançaPode tentar de novo em alguns segundos
Vitrin.Error invalid_intentA leitura da intent veio sem valor ou sem teto de parcelasNada foi cobrado; tente de novo ou fale com o suporte
Vitrin.Error timeout30 s sem resposta na leitura da intent ou no tokenizeNada foi cobrado; pode tentar de novo
Vitrin.Error rate_limited (429)Muitas tentativasAguarde

Todo Vitrin.Error que vem da API traz status (HTTP) e details (o corpo da resposta).

Status da intent no navegador.

GET /api/v1/checkout/intents/{id}/status/
Authorization: Bearer vd_pk_live_xxxxx
X-Intent-Secret: <client_secret>

{ "status": "requires_payment", "transaction_id": null, "amount": "299.90", "max_installments": 6 }
  • Traz status, transaction_id, amount e max_installments. transaction_id é o da tentativa mais recente: depois de uma liberação ele continua preenchido, com status: requires_payment, e não quer dizer cobrança. Este último é o teto efetivo: o menor entre o max_installments da intent e o teto de parcelas da conta. O comprador já vê o valor, e só quem tem o segredo (ou a chave secreta) o lê.
  • Nunca traz o cliente, metadata, external_reference nem description.
  • Com a chave publicável, o client_secret vai só no header X-Intent-Secret: na URL ele acabaria em logs de acesso. Sem ele, com ele errado, de outra conta ou de outro ambiente: 404.
  • Com a chave secreta, o header não é necessário.

Resposta ambígua. Se o confirm fica sem desfecho claro, a cobrança pode ter acontecido. Isso vale para:

  • um 5xx sem retryable: true, que inclui o 502 com intent_status: "processing" e retryable: false e também o 502/504 do proxy, sem corpo;
  • a rede cair depois do envio;
  • 90 s sem resposta.

Nesses casos o SDK nunca reenvia o confirm: consulta o status da intent por até ~60 s e devolve succeeded, requires_payment, failed ou processing. Quem não usa o SDK consulta o mesmo endpoint. Um 5xx com retryable: true garante que nada foi cobrado.

Servidor: acompanhar a intent

GET /api/v1/checkout/intents/{id}/ (chave secreta) devolve a intent completa, com status e transaction_id. Status: requires_payment, processing, succeeded (cobrança aceita; a liquidação segue na Transaction e no webhook), failed (3 recusas) ou expired. Libere o pedido pelo webhook de pagamento (PAYMENT_CONFIRMED/PAYMENT_RECEIVED), que traz o bloco payment_intent com id, external_reference e metadata.

Sem o SDK (outra biblioteca ou app): com a chave publicável, leia o valor em /status/, tokenize pela intent (o cliente vem dela), rode o 3DS se a política pedir e confirme:

POST /api/v1/checkout/tokenize/
Authorization: Bearer vd_pk_live_xxxxx

{
  "intent_id": "<id>",
  "client_secret": "<client_secret>",
  "credit_card": { "holderName": "MARIA SILVA", "number": "5162306219378829",
                   "expiryMonth": "01", "expiryYear": "2028", "ccv": "318" }
}

Segredo errado, intent de outra conta, de outro ambiente ou que já não está em requires_payment (inclusive vencida): 404, sem tokenizar.

POST /api/v1/checkout/intents/{id}/confirm/
Authorization: Bearer vd_pk_live_xxxxx

{
  "client_secret": "<client_secret>",
  "credit_card_token": "tok_xxxxxxxxxxxxx",
  "installments": 3,
  "authentication": { "eci": "05", "cavv": "...", "reference_id": "..." }
}

Com a chave publicável, o 200 traz só o desfecho, sem dados do cliente nem taxas:

{ "id": "<id>", "status": "succeeded", "transaction_id": "<uuid>",
  "card": { "brand": "Visa", "last4": "1111" }, "installments": 3 }

Um 202 com os mesmos campos e "status": "processing" quer dizer cobrança em análise no processamento: não confirme de novo e aguarde o webhook. Com a chave secreta, o 200 traz também transaction (a Transaction completa).

Quem já integra pelo servidor (/checkout/tokenize/ + /checkout/pay/ com a chave secreta) não precisa mudar nada. Vitrin.init({ apiKey }) (SDK 1.2) continua funcionando, com um aviso no console: troque por publishableKey e, se a chave secreta já esteve numa página pública, rotacione-a (POST /auth/api-key/regenerate/). O Vitrin.charge só funciona com a chave secreta, ou seja, no seu servidor.

Recusas: como tratar

Quando o emissor (banco do cliente) recusa a cobrança de cartão, a Vitrin devolve HTTP 422 com um payload pronto pra você decidir o próximo passo:

{
  "error": "Pagamento não autorizado.",
  "description": "Transação não permitida",
  "return_code": "GD",
  "return_code_description": "Transação não permitida",
  "customer_message": "Pagamento não autorizado pelo emissor. Use outro cartão para finalizar a compra.",
  "retryable": false,
  "transaction_id": "757b6c0c-323f-4dda-9089-c823fe3179f3"
}

422 não quer dizer que a cobrança não foi enviada. A presença de return_code significa o contrário: houve resposta de autorização — a cobrança saiu daqui, chegou ao emissor e voltou negada. Um 422 sem return_code é que é payload rejeitado (aí sim nada foi enviado ao banco). Se você ramifica pelo status HTTP, use o return_code como discriminador; se usa os SDKs oficiais, a distinção já vem em classes separadas (VitrinRefusedError vs VitrinValidationError — vide §SDKs oficiais).

Os campos chave são:

  • customer_message: texto pronto para exibir ao cliente final no checkout. Já vem em PT-BR e adaptado ao motivo da recusa (saldo insuficiente vs cartão bloqueado vs erro transitório).
  • retryable:
    • true → emissor admite nova tentativa com o mesmo cartão (ex.: timeout, sistema indisponível, parcelamento inválido). Você pode oferecer "Tentar novamente" ao cliente.
    • false → recusa definitiva para esse cartão. Não tente de novo com o mesmo cartão — a próxima tentativa é recusada de imediato no nosso processamento (sem chegar ao emissor) e fica registrada no histórico do cliente. Ofereça troca de cartão.
  • return_code / return_code_description: código ABECS oficial e descrição interna. Útil pra logs e suporte; não recomendamos exibir ao cliente final (use customer_message).
  • transaction_id: id da Transaction com status='failed' que registra a tentativa. Toda recusa é persistida, inclusive a da primeira cobrança de uma assinatura de cartão — você encontra em GET /api/v1/transactions/?status=failed, em GET /api/v1/payments/{id}/ e no painel, com o bloco refusal carregando o código. Você não precisa guardar o corpo do erro para ter histórico.

Exemplo de UX recomendado:

const resp = await vitrin.charges.create({ /* ... */ });

if (resp.status === 422) {
  const body = await resp.json();
  // Mostre a customer_message ao usuário
  showError(body.customer_message);

  // Se retryable, ofereça botão "Tentar de novo"
  if (body.retryable) {
    showRetryButton();
  } else {
    // Recusa definitiva: ofereça outro cartão
    showCardSwitcher();
  }
}

Importante: mesmo com 3DS autenticado (Is3DSAuthenticated: true), o emissor pode recusar — o 3DS dá liability shift, mas não garante autorização. Recusas após 3DS geralmente vêm de checagens de risco do banco emissor (fraude, saldo, divergência de dados do titular).

Processamento indisponível (502) nas cobranças de cartão

Vale para todos os caminhos que cobram cartão na hora: POST /checkout/pay/, POST /charges/, POST /charges/charge-saved/, POST /orders/create/, POST /checkout/guest/, POST /subscriptions/ com cartão e o pagamento one-click (POST /public/oneclick/pay/). Em todos eles, retryable: true quer dizer que nada foi cobrado.

Um HTTP 502 nesses endpoints tem dois significados, e o campo retryable separa os dois:

  • retryable: true: a cobrança não chegou a ser processada (limite de requisições, credencial recusada, conexão que nem abriu, rejeição no processamento). Nada foi cobrado, e você pode tentar de novo em alguns segundos.
  • retryable: false com payment_status: "processing": a resposta do processamento se perdeu no caminho (tempo esgotado, rede caída no meio, erro sem corpo). A cobrança pode ter sido feita.
{
  "error": "Provedor de pagamentos indisponível no momento.",
  "description": "…",
  "customer_message": "Não conseguimos confirmar o pagamento agora. Não tente de novo: aguarde a confirmação da loja antes de pagar outra vez.",
  "retryable": false,
  "payment_status": "processing",
  "transaction_id": "757b6c0c-323f-4dda-9089-c823fe3179f3"
}

502 com payment_status: "processing" = não cobre de novo. Aguarde o webhook de pagamento da transaction_id ou consulte GET /api/v1/transactions/{transaction_id}/. Se a cobrança foi efetivada, a Vitrin recebe a confirmação, a transação vira received e você recebe o PAYMENT_RECEIVED normalmente. Cobrar de novo nesse intervalo gera cobrança dupla no cartão do cliente.

No caso incerto, o customer_message não pede nova tentativa: diz ao comprador que o pagamento está em confirmação. Pode exibir tal qual.

A transação no GET. Enquanto a confirmação não chega, essa transação aparece com status: "failed" e um bloco refusal próprio:

"refusal": {
  "code": "VITRIN_PROVIDER_UNCERTAIN",
  "description": "Resultado em confirmação; não cobre de novo.",
  "message": "Resultado em confirmação; não cobre de novo.",
  "customer_message": "Não conseguimos confirmar o pagamento agora. Não tente de novo: aguarde a confirmação da loja antes de pagar outra vez.",
  "retryable": false
}

VITRIN_PROVIDER_UNCERTAIN não é recusa do emissor: é o registro de que a resposta do processamento se perdeu. Ele se resolve sozinho:

  • a cobrança foi efetivada: a transação vira received, o refusal some (null) e você recebe o PAYMENT_RECEIVED;
  • a cobrança foi recusada: a transação segue failed, mas com o código real do emissor no refusal (e o retryable dele), e você recebe o PAYMENT_REFUSED;
  • a cobrança nunca chegou a ser processada: a Vitrin consulta o processamento pela referência da tentativa (a partir de 2 min, depois em 5, 15 e 30 min e então de hora em hora). Se ela não existe lá 15 min depois, a transação segue failed com refusal.code = "VITRIN_PROVIDER_NOT_CHARGED" e retryable: true: nada foi cobrado, e você pode cobrar de novo (com uma Idempotency-Key nova, porque a antiga aponta para esta tentativa).

A mesma consulta resolve o caso em que o webhook se perdeu no caminho: a transação vira paga ou recusada como se o webhook tivesse chegado, e você recebe o PAYMENT_RECEIVED ou o PAYMENT_REFUSED normalmente. Sem desfecho em 24 h, a Vitrin para de consultar e a transação segue VITRIN_PROVIDER_UNCERTAIN: fale com o suporte antes de cobrar de novo.

refusal.codeO que aconteceuretryable
VITRIN_PROVIDER_UNCERTAINA resposta do processamento se perdeu; a cobrança pode ter sido feita. Em confirmaçãofalse
VITRIN_PROVIDER_NOT_CHARGEDA cobrança não chegou a ser processada; nada foi cobradotrue
VITRIN_PROVIDER_UNCONFIRMEDSó no rastro da assinatura de cartão: 24 h sem confirmação. Pode ter sido cobrada; confirme com o suportefalse

No polling de status (GET /api/v1/payments/{payment_id}/status/), a transação incerta aparece com status: "processing" e payment_status: "processing", não failed: não ofereça "pagar de novo" enquanto ela estiver assim.

Os campos error, description, customer_message e retryable continuam em todas as respostas 502. payment_status e transaction_id só aparecem no caso incerto.

Diferenças por endpoint:

EndpointCaso incertoDemais falhas de processamento
/checkout/pay/, /charges/502 com o corpo acima502 com o mesmo corpo e retryable: true
/orders/create/, /checkout/guest/502 com o corpo acima (antes era 504 no tempo esgotado); o pedido não é criadocomo antes: 503 (limite de requisições) ou 502/504 com error e retryable
/charges/charge-saved/502 com o corpo acima (antes era 504 com retryable: true no tempo esgotado)como antes: 503 ou 502/504 com error e retryable
/subscriptions/ com cartão502 com o corpo acima; a assinatura não é criada e a transaction_id é o rastro da tentativa (antes era 504 com retryable: true, sem transação)como antes: 503 ou 502/504 com error e retryable
one-click502 com error: "provider_error", retryable: false, payment_status e transaction_id; o link já foi consumido502 provider_error com retryable: true

Só cartão. Pix e boleto não mudam: um Pix ou boleto repetido gera outro QR ou boleto, não um débito duplo, e seguem com retryable: true e refusal.code = "VITRIN_PROVIDER_ERROR".

Com Idempotency-Key no /charges/ e no /charges/charge-saved/, repetir o pedido depois de um 502 incerto é seguro: a Vitrin acha a transação pela chave e não repete o processamento. Enquanto não há desfecho, a repetição devolve na hora o mesmo 502 incerto (retryable: false, payment_status: "processing", a mesma transaction_id); depois do webhook, 201 com a mesma transação, paga ou recusada.

Na assinatura, a primeira cobrança acontece dentro da criação da recorrência no processamento. No caso incerto, a assinatura pode existir lá sem existir aqui: não crie outra antes de confirmar com o suporte. A transaction_id do 502 é só o rastro da tentativa: ela não vira received (se a assinatura nasceu, as cobranças chegam pelo webhook da recorrência) e, depois de 24 h sem confirmação, passa de VITRIN_PROVIDER_UNCERTAIN para VITRIN_PROVIDER_UNCONFIRMED. A Vitrin é avisada na hora e confere o caso.

Os SDKs oficiais (@vitrindigital/node 0.5.0 e vitrin 0.4.0 em diante) seguem essa regra: um POST só é repetido em 429, em 5xx com retryable: true, quando a conexão nem chegou a abrir, ou no /charges/ e /charges/charge-saved/ com Idempotency-Key; nunca no 502 com retryable: false. Nesse 502, em /charges/, /charges/charge-saved/ e /checkout/pay/, o SDK consulta GET /transactions/{transaction_id}/ por até ~60 s e devolve o desfecho: a transação paga, a recusa do emissor (VitrinRefusedError) ou, sem desfecho no prazo, o erro com payment_status: "processing". O POST nunca é reenviado. Veja SDKs oficiais.

Antifraude

A Vitrin oferece análise antifraude opcional para cobranças de cartão. Você liga e desliga em Conta → Configurações, na opção Antifraude, que mostra o custo por cobrança. Não há parâmetro por API: o toggle só liga ou desliga.

  • Ligado: toda cobrança de cartão passa pela análise, com ou sem 3DS. A resposta pode demorar mais.
  • Desligado: nenhuma passa. Sem 3DS, a loja assume o risco de contestação por transação não reconhecida.

Como tratar na integração:

  • Cobrança em análise (HTTP 202, status: "processing"): não cobre de novo. Aguarde o webhook de pagamento (PAYMENT_RECEIVED ou PAYMENT_REFUSED) ou consulte a transação.
  • Recusa: mostre o customer_message ao comprador e ofereça outro cartão, Pix ou boleto (ver Recusas: como tratar).

Quando ligar: se você integra server-only (sem checkout web), aceita bandeiras sem suporte 3DS (JCB, Aura, Diners, Discover), tem cartões não inscritos no 3DS chegando com frequência ou usa a política 3DS try (as vendas em que o 3DS não autentica seguem sem 3DS).

3.4 Consultar Parcelamento

Antes de cobrar, consulte as opções de parcelamento que o cliente final vê:

GET /api/v1/checkout/installments/?amount=299.90

Resposta:

{
  "amount": 299.90,
  "installments": [
    { "installment_count": 1, "installment_value": 299.90, "total_value": 299.90 },
    { "installment_count": 2, "installment_value": 154.46, "total_value": 308.92 },
    { "installment_count": 3, "installment_value": 103.99, "total_value": 311.97 }
  ]
}

A resposta é desenhada pra alimentar diretamente o frontend de checkout do cliente final: o que ele paga por parcela (installment_value) e o total (total_value). Os juros embutidos no parcelamento ficam transparentes pro pagador (consolidados no total_value) e não são expostos como campo separado.

Para conhecer o custo aplicado a cada cobrança da sua conta, consulte os termos comerciais ativos em GET /api/v1/commercial-terms/active/ ou a aba "Comissões" do dashboard. Os valores líquidos por transação aparecem em Transaction.net_amount quando a cobrança é criada.

3.5 Guest Checkout (sem cadastro prévio)

Cria o cliente e cobra em um único passo:

POST /api/v1/checkout/guest/

{
  "customer_name": "João Santos",
  "customer_email": "joao@email.com",
  "customer_cpf_cnpj": "98765432100",
  "customer_phone": "11988776655",
  "amount": 49.90,
  "billing_type": "PIX",
  "description": "Ingresso Evento"
}

3.6 Pix Automático (recorrência via SPI/BC)

Para cobranças recorrentes sem cartão. O pagador escaneia um QR Code uma única vez, autoriza a primeira cobrança + futuras recorrências via app bancário, e a partir daí você gera as cobranças periódicas.

Requisitos: o método precisa estar habilitado pelo admin (PIX_AUTOMATIC em allowed_payment_methods) e pela própria org em enabled_payment_methods. Sem isso, a criação responde com erro, e o método aparece esmaecido no painel com o selo Não liberado; fale com o suporte para pedir a liberação.

Sem sandbox: o Pix Automático não tem ambiente de teste. Com a chave vd_test_*, criar, alterar, cobrar, sincronizar e cancelar respondem 403 e nada é criado; as consultas (GET) continuam funcionando. Use a vd_live_*.

Passo 1: Criar a autorização (gera QR Code de aceite)

POST /api/v1/pix-automatic/authorizations/

{
  "customer_id": "uuid-do-cliente",
  "contract_id": "ASSINATURA-001",
  "frequency": "MONTHLY",
  "start_date": "2026-05-01",
  "value": 99.90,
  "description": "Mensalidade Plus",
  "first_charge_value": 99.90,
  "first_charge_description": "Primeira mensalidade"
}

Campos:

  • frequency: WEEKLY, MONTHLY, QUARTERLY, SEMIANNUALLY, ANNUALLY. O Pix Automático não tem ciclo quinzenal nem bimestral: assinatura de plano com billing_cycle biweekly ou bimonthly e billing_type: "PIX_AUTOMATIC" é recusada com HTTP 400 (billing_cycle), sem criar a assinatura.
  • start_date: obrigatório por compatibilidade, mas ignorado. A cobrança imediata (first_charge_value) é o 1º ciclo, e a recorrência começa um período depois dela: hoje + 1 semana, + 1 mês etc. Essa data é a âncora da série e vem no start_date da resposta.
  • value: valor fixo cobrado a cada ciclo. Obrigatório — sem value (ou min_limit_value) o BACEN força o pagador a definir um teto no app do banco, comportamento que normalmente não é o desejado para assinatura de valor fixo. value e min_limit_value são mutuamente exclusivos.
  • min_limit_value (alternativa a value): piso de valor variável. O pagador define um teto no app que precisa ser ≥ esse piso, e a org pode cobrar qualquer valor ≤ teto a cada ciclo.
  • finish_date (opcional): se omitido, prazo indeterminado.
  • first_charge_value: valor da primeira cobrança (vinculada ao QR Code de aceite — obrigatório). Renderiza no QR Code que o cliente escaneia ao aceitar a autorização.

Endereço e telefone do Customer: o BACEN exige que o pagador veja todos os dados no app do banco antes de aceitar. Antes de criar a autorização, garanta que o Customer tem phone, postal_code, address, address_number, province, city e state preenchidos — se faltar algum, a API retorna 400 com missing_fields.

Resposta:

{
  "id": "uuid-da-autorizacao",
  "provider_authorization_id": "pix_auto_xxx",
  "status": "CREATED",
  "immediate_qr_code": "<base64 PNG do QR Code>",
  "immediate_payload": "00020101021226880014br.gov.bcb...",
  "frequency": "MONTHLY",
  "value": "99.90"
}

Renderize o QR Code para o cliente:

<img src="data:image/png;base64,<immediate_qr_code>" alt="QR Code Pix Automatico" />

Ou ofereça o copia-e-cola via immediate_payload.

Passo 2: Aguardar o aceite + 1ª cobrança (via webhook)

Quando o cliente escaneia, aceita no app bancário e paga a primeira cobrança, dois sinais chegam:

  1. A autorização vai de CREATED para ACTIVE. Você recebe o evento:
    PIX_AUTOMATIC_RECURRING_AUTHORIZATION_ACTIVATED
    
  2. A Transaction da primeira cobrança (criada automaticamente no momento da autorização, com status='pending') vai para received quando o cliente paga, via webhook PIX padrão. Ela aparece em GET /api/v1/transactions/ com billing_type='PIX_AUTOMATIC' e provider_payment_id igual ao identificador da transação no processamento.

Toda cobrança Pix Automático paga (a primeira e as recorrentes) dispara dois eventos canônicos para o mesmo pagamento — PAYMENT_RECEIVED e PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_PAID. Reconcilie de forma idempotente pelo payment.id. Detalhes em Eventos suportados.

Passo 3: Cobranças recorrentes

A Vitrin gera as cobranças de cada ciclo. Com a autorização ACTIVE, a Vitrin cria a cobrança de cada ciclo cerca de 3 dias úteis antes do vencimento e a envia ao banco do pagador. A cobrança aparece em GET /api/v1/transactions/ com status='pending' e o webhook PAYMENT_RECEIVED chega quando o débito acontece. Você não precisa chamar nada.

  • Datas dos ciclos: contadas a partir da âncora (start_date da autorização), no mesmo dia da semana (semanal) ou do mês (mensal e maiores). Em mês sem o dia da âncora, vale o último dia do mês, e a série segue a partir dele (âncora dia 31 → 30/09 → 30/10 → 30/11).
  • Fim de semana e feriado nacional: o débito do Pix Automático só acontece em dia útil. O vencimento que cair em fim de semana ou feriado vai para o próximo dia útil, só naquele ciclo: a série não muda de dia. Exemplo semanal com âncora na sexta: 20/11/2026 (feriado) → segunda 23/11; o ciclo seguinte vence na sexta 27/11, normalmente. Idem 25/12 → 28/12 e 01/01 → 04/01.
  • Uma cobrança por ciclo e por autorização: um cliente pode ter mais de uma autorização (um semanal e um mensal, por exemplo) e cada uma cobra o seu ciclo.
  • Falta de saldo: quando a retentativa automática está habilitada na plataforma, a Vitrin tenta o débito de novo até 3 vezes em 7 dias corridos (sempre em dia útil e antes do vencimento do ciclo seguinte), e a cobrança fica pending enquanto houver tentativa. Recusa do banco do pagador não é retentada. Sem retentativa possível, a cobrança vai para overdue.

Cobrança avulsa na autorização (opcional). Se precisar gerar uma cobrança fora da recorrência automática:

POST /api/v1/pix-automatic/authorizations/{id}/charges/

{
  "value": 99.90,
  "due_date": "2026-06-01",
  "description": "Mensalidade junho"
}

Janela do BC: o banco do pagador aceita o agendamento entre 2 e 10 dias úteis antes do vencimento (dias úteis descontam fins de semana e feriados nacionais).

  • due_date entre 2 e 10 dias úteis: enviada na hora, HTTP 201, com provider_payment_id preenchido.
  • due_date a mais de 10 dias úteis: não é erro. A Vitrin guarda a cobrança e devolve HTTP 202 com provider_payment_id vazio; ela é enviada automaticamente quando faltarem cerca de 3 dias úteis para o vencimento.
  • due_date a menos de 2 dias úteis, no passado ou em fim de semana/feriado: HTTP 400 (o de fim de semana/feriado sugere o próximo dia útil em suggested_due_date).

Se a autorização tem value fixo, você precisa enviar exatamente esse valor.

Com a recorrência automática ativa, uma cobrança avulsa com vencimento perto do ciclo (até 5 dias no mensal, 2 no semanal) ocupa o ciclo: a Vitrin não gera outra para ele.

Resposta: retorna a Transaction criada (mesmo formato do /checkout/pix/).

Outras operações

GET    /api/v1/pix-automatic/authorizations/                      # Listar autorizações
GET    /api/v1/pix-automatic/authorizations/{id}/                 # Detalhes
PATCH  /api/v1/pix-automatic/authorizations/{id}/                 # Alterar Customer/start_date (limitado pelo processamento)
GET    /api/v1/pix-automatic/authorizations/{id}/instructions/    # Listar cobranças geradas
POST   /api/v1/pix-automatic/authorizations/{id}/sync/            # Forçar sync com o processamento
DELETE /api/v1/pix-automatic/authorizations/{id}/                 # Cancelar (PIN obrigatório)
GET    /api/v1/pix-automatic/instructions/                        # Todas as instruções da org
GET    /api/v1/pix-automatic/instructions/{id}/                   # Detalhe da instrução
POST   /api/v1/pix-automatic/instructions/{id}/sync/              # Forçar sync da instrução
DELETE /api/v1/pix-automatic/instructions/{id}/                   # Cancelar agendamento (PIN obrigatório)

PATCH é limitado pelo processamento a alterar dados do Customer e start_date. Mudanças de valor (value/min_limit_value), finish_date, frequency, customer ou contract_id exigem criar nova autorização (cancelar a atual e emitir um novo QR de aceite). Status terminal (CANCELLED/REFUSED/EXPIRED) bloqueia qualquer alteração.

DELETE /instructions/{id}/ cancela um agendamento ainda não pago. Status terminal (DONE/CANCELLED/REFUSED) bloqueia o cancelamento. A Transaction pendente associada é marcada como canceled automaticamente.

Status possíveis

AutorizaçãoSignificado
CREATEDAguardando aceite do pagador
ACTIVEAceita — cobranças podem ser geradas
CANCELLEDCancelada (pela org ou pelo pagador)
REFUSEDRecusada pelo banco do pagador
EXPIREDExpirada sem aceite
Instrução (cobrança)Significado
AWAITING_REQUESTCriada localmente
SCHEDULEDAgendada no SPI
DONEPagamento confirmado
CANCELLEDCancelada antes do débito
REFUSEDRecusada (saldo insuficiente, divergência etc.)

3.7 Cartões Salvos e One-Click Buy

Permite cobrar um cliente recorrente sem pedir os dados do cartão de novo. O fluxo é:

  1. Cliente preenche o cartão uma vez com consentimento explícito
  2. Você guarda o saved_card_token (a Vitrin persiste a referência; o número do cartão fica no cofre PCI do processamento)
  3. Cobranças futuras chamam /charges/charge-saved/ informando só o customer_id e o valor

Um cartão preferido por cliente. Salvar um novo cartão substitui o anterior. Cliente final pode remover via link assinado que você gera pelo painel ou pela API.

Salvar cartão (consentimento obrigatório — LGPD)

POST /api/v1/customers/{customer_id}/saved-card/

{
  "credit_card": {
    "holder_name": "JOAO DA SILVA",
    "number": "4000000000001000",
    "expiry_month": "12",
    "expiry_year": "2030",
    "cvv": "123"
  },
  "consent": {
    "accepted_at": "2026-05-16T12:00:00Z",
    "ip": "189.10.20.30"
  }
}

Resposta 201 Created:

{
  "brand": "visa",
  "last4": "1000",
  "exp_month": 12,
  "exp_year": 2030,
  "holder_name": "JOAO DA SILVA",
  "consent_at": "2026-05-16T12:00:00Z"
}

O bloco consent é obrigatório — sem accepted_at e ip o cartão não é persistido (400). Use o timestamp em que o cliente final aceitou o checkbox "Salvar cartão para próximas compras" e o IP capturado no mesmo request.

Cobrar com o cartão salvo (one-click)

POST /api/v1/charges/charge-saved/

{
  "customer_id": "5e3a0a1b-9b1f-4f87-9b6b-1a2b3c4d5e6f",
  "amount": 99.90,
  "description": "Recompra plano Pro",
  "installments": 1,
  "external_reference": "reorder-2026-05-16"
}

Retorna o mesmo Transaction que /checkout/pay/ retorna. O cliente paga sem digitar nada — você cobra com 1 request.

ErroQuando
404 customer_not_foundcustomer_id não existe ou pertence a outra org
422 no_saved_cardCliente sem cartão salvo
422 card_expiredMês/ano de expiração já passou — peça novo cartão
422 <ABECS code>Recusa do emissor (vide tabela em 3.3)

Consultar / remover (lado da org)

GET    /api/v1/customers/{customer_id}/saved-card/   # 404 se não houver
DELETE /api/v1/customers/{customer_id}/saved-card/   # 204

A resposta do GET nunca inclui o token — só brand, last4, expiração, nome e timestamp do consentimento.

Você pode gerar uma URL que o cliente final abre para ver/remover o próprio cartão, sem precisar do seu painel (ex: enviar por email após assinatura de plano, página de minha-conta no site da loja):

POST /api/v1/customers/{customer_id}/saved-card/management-link/

Resposta:

{
  "url": "https://vitrin.digital/customer/cards/eyJhbGciOi...",
  "token": "eyJhbGciOi...",
  "expires_at": "2026-05-17T12:00:00Z"
}

O token é um JWT de 24h. A página servida em url permite ao cliente final:

  • GET /api/v1/public/saved-card/?token=<jwt> — ver bandeira + últimos 4 + nome da loja
  • DELETE /api/v1/public/saved-card/?token=<jwt> — remover o próprio cartão

LGPD: o link assinado é o mecanismo recomendado para atender o direito de revogação do consentimento. Inclua-o em emails transacionais relevantes e na sua página de minha-conta.

Enquanto charge-saved é servidor pra servidor (você cobra direto via API), o link de recompra entrega o controle pro cliente final: você gera uma URL assinada, manda por email/WhatsApp, e o cliente confirma a compra com 1 clique numa página hospedada pela Vitrin.

POST /api/v1/customers/{customer_id}/one-click-link/

{
  "product_id": "5e3a0a1b-9b1f-4f87-9b6b-1a2b3c4d5e6f",
  "amount": "99.90",         // opcional — só pra produto customer_defined ou plano com valor flexível
  "installments": 3,         // opcional, default 1
  "coupon_code": "OFF10",    // opcional — só pra produto; valida targeting na emissão
  "bump_ids": ["..."],       // opcional — só pra produto; bumps pré-aceitos vinculados ao parent
  "expires_in": 900          // opcional (segundos), default 900 = 15min, máx 3600
}

Use plan_id no lugar de product_id pra gerar link de assinatura. Exatamente um dos dois. Cupom e bumps só funcionam pra produto — planos rejeitam (400).

Resposta:

{
  "url": "https://vitrin.digital/c/loja/produto-x/oneclick?t=eyJhbGciOi...",
  "token": "eyJhbGciOi...",
  "expires_at": "2026-05-16T15:15:00Z"
}

O cliente abre a url, vê o card do cartão salvo (bandeira + ••••1234), valor e parcelas, e clica "Pagar — 1 clique". A Vitrin valida o JWT, cobra usando o token salvo (mesma engine do checkout normal — cria Order/Subscription, calcula fees, executa a divisão de comissão, dispara webhook) e redireciona pro thank_you_url configurado no item.

Segurança:

  • JWT HS256 com TTL curto (15min default, 1h máx) — limita janela de fraude
  • Uso único: depois do primeiro clique, o token é consumido (nonce Redis) — segundo POST retorna 401 token_consumed
  • Nonce só é consumido após validações reversíveis (cartão removido / expirado / item inativo / subscription duplicada) — falhas reversíveis preservam o link; cliente pode retentar
  • Cartão precisa estar salvo e válido no momento do clique. Se foi removido entre emissão e uso → 422 card_removed
  • Item precisa estar is_active=True na sua org — mudanças refletem em tempo real
  • Rate limit: 10 req/min por IP no endpoint público (/public/oneclick/), abaixo do checkout normal por cobrar sem CVV
ErroQuando
400 coupon_not_found / coupon_invalid / coupon_not_applicableCupom inexistente, inválido (expirado/inativo) ou não aplicável ao produto (na emissão)
400 bumps_not_foundAlgum bump_id não pertence ao parent product (na emissão)
422 no_saved_cardCustomer sem cartão salvo (na emissão)
422 card_expiredCartão com mês/ano expirado (na emissão ou no clique)
422 card_removedCartão foi deletado entre emissão e clique
422 provider_refusedCartão recusado (customer_message pronto pra exibir)
502 provider_errorProcessamento instável. Com retryable: true, nada foi cobrado; com retryable: false e payment_status: "processing", a cobrança pode ter saído: não emita outro link antes do webhook (veja Processamento indisponível (502))
409 subscription_already_activeCliente já tem subscription vigente desse plano (preserva nonce)
404 item_not_foundProduto/plano não existe ou ficou inativo
401 token_expiredTTL estourou
401 token_consumedCliente já usou o link
401 token_invalidAssinatura inválida / scope errado / tampering
GET /api/v1/customers/{customer_id}/one-click-history/?limit=20

Retorna items[] com status (paid / pending / expired / consumed_failed), jti, issued_at, paid_at + paid_resource_id quando aplicável. Útil pra reconciliar campanhas de email e ver quem clicou.

Conformidade

  • O número do cartão nunca toca o servidor da Vitrin — só o token retornado pelo cofre PCI do processamento
  • Cada token é exclusivo da sua organização e não pode ser usado por outra org
  • O texto de consentimento sugerido está em docs/MANUAL-ORG.md — cole na sua política de privacidade
  • Cobranças com cartão salvo não pedem CVV. Isso é parte do one-click. Para fluxos sensíveis (valor alto, antifraude), prefira /checkout/pay/ com cartão fresco + 3DS

SDKs

// Node
await vitrin.customers.savedCard.create(customer.id, {
  credit_card: { holder_name, number, expiry_month, expiry_year, cvv },
  consent: { accepted_at, ip },
})

await vitrin.charges.createWithSavedCard({
  customer_id: customer.id,
  amount: 99.90,
  description: 'Recompra plano Pro',
})

await vitrin.customers.savedCard.createManagementLink(customer.id)
# Python
vitrin.customers.save_card(
    customer.id,
    credit_card={"holder_name": ..., "number": ..., "expiry_month": ..., "expiry_year": ..., "cvv": ...},
    consent={"accepted_at": ..., "ip": ...},
)

vitrin.charges.create_with_saved_card(
    customer_id=customer.id, amount="99.90", description="Recompra plano Pro",
)

vitrin.customers.saved_card_management_link(customer.id)

# Link de recompra one-click (cliente clica e paga)
vitrin.customers.one_click_link(
    customer.id, product_id=product.id, amount="99.90", installments=3,
)
// Node — link de recompra
await vitrin.customers.oneClickLink(customer.id, {
  product_id: product.id,
  amount: "99.90",
  installments: 3,
})

3.8 Cobrança das taxas

Por padrão, as taxas da plataforma são descontadas automaticamente do valor líquido recebido. O valor que você passa em amount é o valor que o cliente paga; você recebe amount − taxa.

Exemplo: numa cobrança de R$ 100 com taxa de R$ 5,49, o cliente paga R$ 100 e você recebe R$ 94,51 líquido na sua conta. A taxa sai do seu repasse, não é somada à fatura do pagador.

Esse comportamento é uniforme para Pix, Boleto e Cartão. Confira a taxa exata aplicada à sua org no simulador ou em GET /org/settings/.

Repasse da taxa ao cliente final (opcional)

A Vitrin pode liberar, organização a organização, a opção de repassar a taxa ao cliente em vez de descontá-la do seu líquido. Quando liberada, a org liga o repasse por meio de pagamento (Cartão, Pix — inclui Pix Automático —, Boleto) em Conta → Configurações ou via API, com PATCH /api/v1/settings/ e o campo fee_passthrough_methods (lista de chaves: "CREDIT_CARD", "PIX", "BOLETO"; [] desliga tudo). Sem essa liberação do admin Vitrin (fee_passthrough_allowed, somente leitura em GET /api/v1/settings/), a seção não aparece no painel e o PATCH com qualquer método responde HTTP 400. Também dá 400 um método que não esteja em enabled_payment_methods ou um valor que não seja lista de strings.

PATCH /api/v1/settings/

{ "fee_passthrough_methods": ["CREDIT_CARD", "PIX"] }

Com o repasse ligado para o método da cobrança:

  • amount no request continua sendo o preço que você quer receber — nada muda no que você envia.
  • A cobrança sai para o pagador pelo valor bruto: o menor valor possível, ao centavo, tal que, depois de descontada a taxa, sobra amount inteiro para você. Juros de parcelamento (cartão) e o seu markup de parcelamento continuam incidindo por cima do valor bruto, como hoje. Comissão de afiliado/coprodutor continua calculada sobre o amount original, não sobre o valor bruto.
  • A resposta da cobrança e os serializers de Transaction trazem amount (o valor efetivamente cobrado do pagador, já com o repasse aplicado) e fee_passthrough_amount (a diferença entre o valor cobrado e o preço original; 0 quando não há repasse).
  • O webhook de pagamento (payment nos eventos PAYMENT_*, §5) também traz fee_passthrough_amount, no mesmo formato numérico de amount e net_amount (0 sem repasse). Campo aditivo — integrações que não o leem não mudam.
  • Reaproveitamento de Pix pendente (§3.0): o reuso compara o preço que você enviou em amount, não o valor bruto — repetir o mesmo pedido devolve o mesmo Pix, com repasse ligado ou não.
  • GET /api/v1/checkout/installments/ já devolve os totais com o repasse aplicado quando o método está ligado — você não precisa recalcular nada para exibir o valor certo ao pagador.
  • Cobranças de R$ 0 nunca são "brutas" (não há taxa a repassar). Organizações do tipo LLC (conta global, cobrança em USD) não têm essa opção.
  • Assinaturas: o estado do repasse (ligado ou desligado) e o valor cobrado por ciclo são congelados no momento em que a assinatura é criada. Ligar ou desligar o repasse depois só vale para assinaturas novas — as existentes continuam cobrando como antes, mesmo que a configuração da org mude. Mudança de taxa também não recalcula o valor de uma assinatura existente. No Pix Automático, reajuste do preço do plano (ou do valor da assinatura) chega ao próximo ciclo com o repasse recalculado sobre o preço novo.
  • Pix Automático: o repasse vale para assinaturas de Pix Automático criadas a partir de um plano (POST /api/v1/subscriptions/ com billing_type: "PIX_AUTOMATIC" ou checkout público do plano). Não vale para autorizações e instruções de Pix Automático avulsas criadas pela API com valor explícito (§3.6) — nelas o valor informado é o valor debitado, e a taxa sai do seu líquido como sempre.

Cobrança com valor definido por você, num link compartilhável de uso único — sem cadastrar produto. O cliente paga pelo funil normal (taxas, juros, repasses, webhooks, estorno e repasse da taxa se ligado, §3.8), não por um checkout hospedado fora da Vitrin.

Substitui o antigo POST /checkout/sessions/. Esse endpoint gerava um link hospedado externo, fora do funil (sem Transaction local), e não está mais na oferta — responde 422 {"error": "payment_link_unavailable"} em qualquer caso. Use /payment-links/ para um link avulso, ou o checkout de produto/plano (/c/<org>/<item>, §14) quando o item já existe.

Indisponível para organizações LLC (conta global) — a confirmação do link depende do pipeline de pagamentos local, que o webhook da conta global não usa.

Indisponível em sandbox. Criar link com chave de teste (vd_test_*) responde 400 {"detail": "Link de pagamento indisponível em sandbox."} — a página pública cobra de verdade, então um link de teste viraria cobrança real. Crie links só com a chave de produção.

Criar:

POST /api/v1/payment-links/

{
  "amount": "150.00",
  "description": "Consultoria de setembro",
  "customer_id": "<uuid-opcional>",
  "allowed_methods": ["PIX", "CREDIT_CARD"],
  "max_installments": 6,
  "expires_at": "2026-10-15T00:00:00Z",
  "success_url": "https://sualoja.com/obrigado"
}
  • amount: obrigatório, > 0. Precisa alcançar o piso da plataforma de pelo menos um dos métodos efetivos do link — o link continua válido se algum método ainda cobrir o valor, mesmo que outro fique abaixo do piso.
  • description: obrigatória.
  • customer_id: opcional, precisa ser cliente da sua organização (outra org → 400). Com cliente, a página pública só pede a forma de pagamento; sem cliente, ela pede os dados do pagador (como o checkout público) e cria/atualiza o Customer.
  • allowed_methods: subconjunto de enabled_payment_methods, sem PIX_AUTOMATIC (cobrança avulsa não é recorrência). Vazio = todos os métodos habilitados na conta.
  • max_installments: só junto de CREDIT_CARD entre os métodos efetivos; de 1 até o teto da conta (org_max_installments, capado em 12x). Vazio = usa o teto da conta.
  • expires_at: padrão 7 dias a partir de agora quando omitido; máximo 90 dias. Precisa ser no futuro.
  • success_url: opcional, até 500 caracteres, precisa ser https (formato completo — só o esquema ou com espaço é rejeitado). Vazio = ao confirmar o pagamento, o cliente vê a página de obrigado padrão da Vitrin (sua marca, valor pago, meio, data, nº da transação — mesma página que o checkout de produto/plano mostra quando não há thank_you_url configurado) em vez de ser redirecionado.

Resposta (201):

{
  "id": "<uuid>",
  "url": "https://vitrin.digital/p/<token>",
  "amount": "150.00",
  "description": "Consultoria de setembro",
  "customer_id": null,
  "customer_name": null,
  "allowed_methods": ["PIX", "CREDIT_CARD"],
  "max_installments": 6,
  "success_url": "https://sualoja.com/obrigado",
  "expires_at": "2026-10-15T00:00:00Z",
  "status": "open",
  "paid_transaction_id": null,
  "paid_at": null,
  "canceled_at": null,
  "created_at": "2026-09-23T12:00:00Z"
}

url traz o token opaco (secrets.token_urlsafe), nunca o id — não dá pra adivinhar outro link da mesma org pela URL. status é sempre o status efetivo: open, paid, canceled ou expired (expired é derivado — open com expires_at no passado — e nunca é um valor persistido).

Listar / detalhe / cancelar:

GET  /api/v1/payment-links/?status=open      # open | paid | canceled | expired
GET  /api/v1/payment-links/{id}/
POST /api/v1/payment-links/{id}/cancel/

cancel/ só funciona com o link open; num link já paid ou canceled responde 409:

{ "error": "payment_link_not_open", "detail": "Link não pode ser cancelado (status atual: paid).", "status": "paid" }

Um Pix ou boleto já emitido não é cancelado no processamento — se o cliente pagar mesmo assim, cai no caso de pagamento duplicado abaixo (L8).

Se o link pertence a um pedido (POST /orders/create/ sem billing_type) ainda pending, cancelar o link cancela o pedido junto (canceled).

Enviar por e-mail:

POST /api/v1/payment-links/{id}/send-email/

Manda "Seu link de pagamento — <sua loja>" pro cliente do link, assinado com a marca da sua org — sem "Vitrin" em lugar nenhum visível ao cliente: descrição, valor, validade e um botão para a URL do link. Exige um customer_id cadastrado no link com e-mail — sem isso, 400. Só funciona com o link open (paid/canceled/vencido → 409, mesmo formato do cancel/ acima). Limite de 3 envios por hora por link — o 4º responde:

{ "error": "payment_link_email_rate_limited", "detail": "Limite de 3 envios por hora atingido para este link." }

Há também um teto de 30 envios por hora pela conta inteira, somando todos os links — o 31º responde 429 com "error": "payment_link_email_org_rate_limited". Resposta (200): { "sent_to": "cliente@exemplo.com" }. O envio em si é assíncrono — o 200 confirma que foi aceito, não a entrega.

Página pública (sem autenticação) — o que seu integrador não precisa chamar, mas ajuda a saber o que o cliente vê:

GET  /api/v1/public/payment-links/<token>/
POST /api/v1/public/payment-links/<token>/pay/

O GET devolve a marca da sua org, descrição, valor, status efetivo, os métodos que o link oferece e aceita (métodos do link cujo piso da plataforma o valor alcança) e o preço por método (quote, a mesma função de precificação do checkout — o que o GET mostra é exatamente o que o pay/ cobra). Token inexistente → 404.

Uso único e reaproveitamento (o que evita cobrar duas vezes por engano):

  • Pix ou boleto já emitidos e ainda pagáveis são devolvidos em nova chamada de pay/ no mesmo meio, em vez de gerar cobrança nova — Pix por ~55 minutos (validade do QR menos uma folga), boleto até a data de vencimento.
  • Link sem cliente: o Pix/boleto pendente reaproveitado é o de quem o gerou primeiro — quem abrir o link depois e escolher o mesmo meio recebe o mesmo QR/boleto, emitido em nome de quem o gerou (os dados preenchidos no novo formulário não são usados). Para cobrar pessoas diferentes, crie um link por pessoa (ou um link com customer_id).
  • Cartão com uma tentativa em análise (3DS/antifraude) bloqueia nova tentativa de cartão por um tempo: aguarde o desfecho (webhook). Cartão recusado não bloqueia — nova tentativa é legítima.
  • Limite de tentativas com cartão: depois de várias recusas de cartão no mesmo link, nova tentativa de cartão responde 429 {"error": "Limite de tentativas com cartão atingido neste link. Escolha outra forma de pagamento.", "effective_status": "open", "blocked_method": "CREDIT_CARD"} sem chegar a ser processada (defesa contra teste de cartão na URL pública). Pix e boleto seguem disponíveis no link; se o cliente precisar pagar com cartão, crie um link novo.
  • Duas tentativas de pay/ ao mesmo tempo no mesmo link: a segunda recebe 409 {"error": "Pagamento em andamento neste link. Tente novamente em instantes."}.
  • Link paid, canceled ou expired → pay/ responde 409 com effective_status.

Confirmação — quando o link vira paid. Só na confirmação: cartão aprovado na hora (resposta síncrona do pay/), ou Pix/boleto pela confirmação do processamento (webhook, ou a reconciliação periódica da Vitrin quando a notificação se perde). Reentrega é idempotente (não desfaz nem duplica). Se dois pagamentos do mesmo link forem confirmados (ex.: Pix e boleto pagos por engano) — caso raro, não bloqueado na origem — o primeiro confirmado mantém o link paid; o outro fica registrado normalmente (Transaction própria) e dispara um alerta interno para a Vitrin revisar o estorno.

paid é definitivo. Se a transação que quitou o link for depois estornada, cancelada ou sofrer chargeback, o link continua paid — ele não reabre. Para cobrar de novo, crie um link novo.

O webhook canônico de pagamento (§5) traz payment_link_id — string do link que originou a cobrança, ou null fora dele.


4. Polling de Status (Pix)

Para Pix, faça polling até o pagamento ser confirmado:

GET /api/v1/payments/{payment_id}/status/

Resposta:

{
  "payment_id": "uuid",
  "status": "pending",
  "confirmed": false,
  "pix_copy_paste": "00020101..."
}

Faça polling a cada 3-5 segundos. Quando confirmed: true, o pagamento foi recebido.

Cartão com resultado incerto (o 502 com payment_status: "processing" da seção 3) aparece aqui com status: "processing" e payment_status: "processing", não failed: a cobrança pode ter sido feita, então não ofereça nova tentativa enquanto ela estiver assim. As demais transações não mudam.


5. Webhooks

Configurar

Configure a URL onde sua aplicação recebe os eventos. Pode ser via dashboard ou API:

Via dashboard: em Conta → Configurações → Webhook, informe a URL HTTPS e clique em Salvar webhook (requer PIN). O secret HMAC é exibido na tela após salvar — guarde-o.

Via API:

PUT /api/v1/webhooks/config/

{
  "webhook_url": "https://suaapp.com/webhooks/vitrin",
  "pin": "123456"
}

O secret HMAC é gerado automaticamente e retornado no response. Você pode regenerá-lo a qualquer momento (e qualquer requisição feita à URL com signature antiga ficará inválida).

Sem URL configurada, a Vitrin processa o evento internamente (atualizando suas Transactions) mas não repassa ao seu backend. Configure a URL antes de ir pro ar.

Receber eventos

A Vitrin envia um POST para a sua URL quando eventos ocorrem:

POST https://suaapp.com/webhooks/vitrin

Headers:
  X-Vitrin-Signature: sha256=abc123...
  X-Vitrin-Event: PAYMENT_RECEIVED
  X-Vitrin-Event-Id: PAYMENT_RECEIVED:pay_xxx:2026-04-09
  Content-Type: application/json

Body:
{
  "event": "PAYMENT_RECEIVED",
  "payment": {
    "id": "<uuid-da-transacao>",
    "status": "received",
    "billing_type": "PIX",
    "amount": 99.90,
    "net_amount": 96.41,
    "fee_passthrough_amount": 0,
    "due_date": "2026-05-07",
    "paid_at": "2026-05-07T12:34:56Z",
    "customer_id": "<uuid-do-cliente>",
    "subscription_id": null,
    "description": "Mensalidade",
    "installments": 1,
    "payment_link_id": null,
    "boleto_url": null,
    "digitable_line": null,
    "carne_id": null,
    "installment_number": null
  }
}

payment_link_id é o id do link de pagamento avulso que originou a cobrança (§3.9), ou null fora dele. Campo aditivo, presente em todo evento de pagamento.

Boleto e parcela de carnê. Campos aditivos. Os quatro abaixo estão em todo evento de pagamento e vêm null fora do caso; pix_copy_paste só existe no evento de boleto:

  • boleto_url e digitable_line: link e linha digitável de qualquer boleto, seja avulso, de assinatura no boleto mensal ou parcela de carnê. Na assinatura no boleto mensal, eles só vêm quando o boleto é informado no aviso do ciclo. Se não vierem, ficam null; o assinante recebe o boleto por e-mail do mesmo jeito.
  • pix_copy_paste: o Pix copia e cola do mesmo boleto (boleto com Pix, §3.2). Presente só quando billing_type é BOLETO, e null no boleto sem Pix. Nos outros meios o campo não vem — na cobrança Pix avulsa, o copia e cola dela está em GET /transactions/{id}/.
  • carne_id e installment_number: o carnê e o número da parcela (1 a N), quando a cobrança é uma parcela de carnê.
{
  "event": "PAYMENT_RECEIVED",
  "payment": {
    "id": "<uuid-da-transacao>",
    "status": "received",
    "billing_type": "BOLETO",
    "amount": 1240.20,
    "due_date": "2026-11-10",
    "subscription_id": "<uuid-da-assinatura>",
    "description": "Plano Pro - parcela 2/12",
    "boleto_url": "https://…",
    "digitable_line": "23790448095616862379336011058009740430000124020",
    "pix_copy_paste": "00020101021226900014br.gov.bcb.pix2568…6304ABCD",
    "carne_id": "<uuid-do-carnê>",
    "installment_number": 2
  }
}

Cobranças de cartão trazem também o snapshot da bandeira/últimos 4 (em qualquer status) e, no caso de recusa, um bloco refusal com o motivo:

{
  "event": "PAYMENT_REFUSED",
  "payment": {
    "id": "<uuid-da-transacao>",
    "status": "failed",
    "billing_type": "CREDIT_CARD",
    "amount": 133.32,
    "installments": 12,
    "customer_id": "<uuid-do-cliente>",
    "card": { "brand": "Mastercard", "last4": "0523" },
    "refusal": {
      "code": "51",
      "description": "Saldo/limite insuficiente",
      "message": "Autorizacao negada",
      "customer_message": "Cartão sem saldo/limite. Use outro cartão ou tente menos parcelas.",
      "retryable": false
    }
  }
}
  • card aparece em qualquer cobrança CC (autorizada ou recusada).
  • refusal só vem com status='failed' e contém:
    • code: ABECS oficial (ex: "51", "GD")
    • description: descrição interna em português
    • message: mensagem original do emissor
    • customer_message: mensagem pronta pra mostrar ao cliente final no checkout
    • retryable: false significa que tentar de novo no mesmo cartão vai falhar — peça outro cartão ou outra forma de pagamento.

Pagamentos via checkout público (ou via API de Orders) trazem também o bloco order enriquecido — use para provisionar acesso/entrega sem chamada GET extra:

{
  "event": "PAYMENT_RECEIVED",
  "payment": {
    "id": "<uuid-tx>",
    "status": "received",
    "amount": 75.00,
    "order": {
      "id": "<uuid-order>",
      "status": "pending",
      "unit_price": 75.00,
      "total_amount": 75.00,
      "quantity": 1,
      "product": {
        "id": "<uuid-product>",
        "slug": "doacao-vitrin",
        "name": "Apoie o canal",
        "type": "contribution",
        "thank_you_url": "https://..."
      },
      "coupon": { "id": "<uuid>", "code": "PROMO10", "discount_amount": 10.00 },
      "bump": { "id": "<uuid>", "product_id": "<uuid>", "product_name": "E-book bônus", "amount": 19.00 },
      "customer_chosen_amount": 75.00
    }
  }
}
  • order só aparece quando há um Order vinculado (compra via checkout público ou POST /orders/create/).
  • coupon / bump / customer_chosen_amount só aparecem quando aplicáveis.

Cobranças com atribuição de afiliado trazem o bloco affiliate com o slug, valor da comissão e status de liquidação. Use para provisionar comissão no seu sistema interno (ex: gamificação, ranking) ou para notificar o afiliado fora da Vitrin:

{
  "event": "PAYMENT_RECEIVED",
  "payment": {
    "id": "<uuid-tx>",
    "status": "received",
    "amount": 100.00,
    "affiliate": {
      "slug": "joao-silva",
      "commission_amount": 30.00,
      "status": "settled",
      "is_recurring": false,
      "settled_at": "2026-05-19T22:34:56Z"
    }
  }
}
  • affiliate só aparece quando a venda foi atribuída a um afiliado (cliente entrou no checkout pelo link dele dentro da janela de atribuição configurada na sua loja).
  • status reflete o estado da comissão na carteira do afiliado:
    • pending: comissão registrada mas ainda não liquidada (cobrança ainda não confirmou).
    • settled: comissão liquidada na carteira do afiliado — vai pra ele no próximo repasse.
    • reversed: comissão estornada (chargeback ou refund).
  • is_recurring: true em renovações de assinatura/Pix Automático; false na primeira venda ou produto avulso.
  • settled_at / reversed_at aparecem conforme o estado.

Quando há coprodutores vinculados ao produto, vem também o bloco coproducers:

{
  "payment": {
    "coproducers": [
      { "name": "Editora Parceira", "amount": 20.00, "mode": "pct" }
    ]
  }
}
  • mode é pct ou fixed; o amount em reais é o valor que foi destacado pra cada coprodutor.

Importante: a fatia da Vitrin não aparece no webhook canônico — amount já é o que sua loja vende; net_amount já está líquido de taxas. Comissão de afiliado e repasses ao coprodutor são debitados desse líquido (não da Vitrin).

Eventos sintéticos da Vitrin (ciclo de vida de assinaturas):

EventoQuandoCampos no topo do corpo
subscription.dunning_warningAssinatura overdue há ≥ 7 diassubscription_id, plan_id, customer_id, overdue_since
subscription.canceled_for_dunningAssinatura overdue há ≥ 30 dias — a Vitrin cancelou a assinaturaidem acima
subscription.canceledA org cancelou a assinatura via POST /subscriptions/{id}/cancel/ (ou pelo dashboard)subscription_id, plan_id, customer_id, billing_type, canceled_at, reason

O nome do evento chega em minúsculas, exatamente como na tabela, no campo event e no header X-Vitrin-Event. Além dos campos do topo, o corpo traz o bloco subscription: a assinatura no mesmo formato de GET /subscriptions/{id}/, lida no momento da entrega.

{
  "event": "subscription.canceled",
  "subscription_id": "<uuid-da-assinatura>",
  "plan_id": "<uuid-do-plano>",
  "customer_id": "<uuid-do-cliente>",
  "billing_type": "CREDIT_CARD",
  "canceled_at": "2026-10-02T17:03:11.120482+00:00",
  "reason": "cancelado pela org via API",
  "subscription": {
    "id": "<uuid-da-assinatura>",
    "customer": "<uuid-do-cliente>",
    "customer_name": "Maria Souza",
    "plan": "<uuid-do-plano>",
    "plan_name": "Plano Pro",
    "plan_price": "179.90",
    "amount_override": null,
    "effective_amount": "179.90",
    "status": "canceled",
    "billing_type": "CREDIT_CARD",
    "provider_subscription_id": "…",
    "current_period_start": "2026-09-10T00:00:00-03:00",
    "current_period_end": "2026-10-10T00:00:00-03:00",
    "canceled_at": "2026-10-02T14:03:11.120482-03:00",
    "created_at": "2026-03-10T09:12:44.018311-03:00",
    "updated_at": "2026-10-02T14:03:11.121907-03:00"
  }
}

Nos dois eventos de inadimplência, o topo traz overdue_since (início da inadimplência) no lugar de billing_type/canceled_at/reason:

{
  "event": "subscription.dunning_warning",
  "subscription_id": "<uuid-da-assinatura>",
  "plan_id": "<uuid-do-plano>",
  "customer_id": "<uuid-do-cliente>",
  "overdue_since": "2026-11-15T14:00:04.310027+00:00",
  "subscription": { "id": "<uuid-da-assinatura>", "status": "overdue", "…": "…" }
}

Se a assinatura não existir mais no momento da entrega, o bloco subscription não vem.

Se você encerra assinatura no seu lado a partir de webhook, mapeie os três. subscription.canceled cobre o cancelamento que a própria org pediu (a resposta HTTP já traz o objeto, mas a integração só sabe pelo evento); os dois de dunning cobrem o encerramento automático por inadimplência.

Correção de setembro/2026: estes três eventos agora chegam de verdade. Até esta correção, eles saíam da Vitrin com o nome errado: chegavam à sua URL como {"event": "PAYMENT_CREATED", "payment": {}} (header X-Vitrin-Event: PAYMENT_CREATED), sem nenhum dado da assinatura. Agora chegam com o nome da tabela e o corpo acima. O que isso muda para você:

  • Você mapeou os três nomes desta seção: nada a alterar. Seu handler passa a ser chamado, e as assinaturas canceladas pela org ou por inadimplência começam a ser encerradas no seu lado. Confira que é o comportamento que você quer antes da primeira entrega.
  • Você tratava PAYMENT_CREATED sem payment.id: esses "fantasmas" param de chegar. Remova o tratamento especial, se houver.
  • Eventos antigos podem chegar agora. Um aviso ou cancelamento que ainda estava na fila de reentrega sai com o nome e o corpo novos, mesmo que seja de semanas atrás. Decida pelo estado atual em subscription.status, não só pelo nome do evento. Por exemplo, um dunning_warning antigo de uma assinatura que já voltou a active não pede ação.
  • Trate os três de forma idempotente por subscription_id: o mesmo cancelamento não deve encerrar nada duas vezes.

Nota: o payment.id é o UUID da Transaction na Vitrin — use ele para reconciliar contra GET /payments/{id}/. Em raros casos onde o webhook chega antes da Transaction ser persistida internamente (condição de corrida), o body vem com payment.provider_payment_id_legacy no lugar de payment.id — você pode reconciliar via lookup posterior.

Validar assinatura

import hmac, hashlib

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

Eventos suportados

EventoQuando
PAYMENT_RECEIVEDPagamento confirmado
PAYMENT_CONFIRMEDPagamento confirmado (cartão)
PAYMENT_OVERDUEPagamento vencido. Na parcela de carnê, sai no 5º dia após o vencimento (carência)
PAYMENT_REFUSEDPagamento recusado (cartão) — body inclui refusal
PAYMENT_REFUNDEDReembolso processado
PAYMENT_REFUND_PENDINGEstorno solicitado; o banco do pagador ainda não confirmou (Pix)
PAYMENT_REFUND_FAILEDEstorno não se concretizou; a cobrança volta a valer
PAYMENT_CHARGEBACK_REQUESTEDChargeback aberto
PAYMENT_DELETEDPagamento cancelado
CARNE_CREATEDCarnê emitido: assinatura nova em carnê ou troca para carnê. Corpo em Assinatura em carnê
CARNE_RENEWEDCarnê seguinte emitido pela renovação automática
CARNE_CANCELEDParcelas em aberto do carnê canceladas, porque a assinatura foi cancelada
CARNE_RENEWAL_BLOCKEDRenovação do carnê barrada porque o boleto não está mais habilitado na conta. Uma vez por carnê
subscription.canceled / subscription.canceled_for_dunning / subscription.dunning_warningCiclo de vida da assinatura (tabela acima)
SUBSCRIPTION_CREATED⚠️ Legado, não emitido. Vocabulário legado; a Vitrin nunca emitiu. Não use para criar assinatura no seu lado — a criação é síncrona, o objeto vem na resposta do POST /subscriptions/.
SUBSCRIPTION_DELETED⚠️ Legado, não emitido. Para encerrar assinatura use subscription.canceled / subscription.canceled_for_dunning (tabela acima) ou, no Pix Automático, os eventos de autorização abaixo.
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CREATEDAutorização Pix Automático criada
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_ACTIVATEDPagador aceitou — autorização ativa
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_REFUSEDAutorização recusada pelo banco
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CANCELLEDMandato encerrado — cliente cancelou no app do banco, a org cancelou via API, ou detectamos cancelamento no processamento
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_EXPIREDMandato encerrado — expirou sem aceite do pagador
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_CREATEDInstrução de cobrança criada
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_SCHEDULEDInstrução agendada no SPI
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_PAIDInstrução de cobrança paga (acompanha um PAYMENT_RECEIVED — ver nota abaixo)
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_REFUSEDInstrução recusada (ver refusalReason)
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_CANCELLEDInstrução cancelada

Pix Automático — o mandato morre em silêncio se você não escutar. O AUTHORIZATION_CANCELLED/_EXPIRED é o único aviso de que a recorrência acabou: quando o cliente cancela o débito automático no app do banco, não há cobrança recusada, não há PAYMENT_*, não há nada além destes eventos. Se sua integração só reage a eventos de pagamento, a assinatura fica ativa pra sempre no seu lado. O corpo é:

{
  "event": "PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CANCELLED",
  "authorization": {
    "id": "<UUID Vitrin da autorização>",
    "status": "CANCELLED",
    "customer_id": "<UUID Vitrin>",
    "subscription_id": "<UUID Vitrin ou null>",
    "contract_id": "...",
    "frequency": "MONTHLY",
    "refusal_reason": null
  }
}

subscription_id pode vir null quando a autorização foi criada direto por POST /pix-automatic/authorizations/ sem assinatura vinculada. Nesse caso reconcilie por authorization.id (o mesmo que você recebeu na criação) ou por customer_id. Não assuma que subscription_id sempre existe.

Eventos desconhecidos: o vocabulário canônico cresce ao longo do tempo. Trate qualquer event que sua integração não reconheça como no-op — nunca como erro. PAYMENT_REFUND_PENDING e PAYMENT_REFUND_FAILED, por exemplo, passaram a existir em julho/2026 para o estorno de Pix: entre a solicitação e a confirmação do banco a cobrança fica em refund_pending, e se a devolução não se concretizar ela volta para received.

Pix Automático — dois eventos por cobrança paga. Quando uma cobrança Pix Automático é paga, você recebe dois eventos canônicos para o mesmo pagamento: PAYMENT_RECEIVED (dinheiro recebido — idêntico a qualquer outra cobrança) e PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_PAID (qual instrução da recorrência foi quitada). Ambos apontam para a mesma Transaction (mesmo payment.id). São event distintos, com X-Vitrin-Event-Id distintos — não são entrega duplicada. Trate seu handler de forma idempotente por payment.id: registre "a cobrança X foi paga" uma única vez e ignore notificações repetidas; não assuma "um evento = um pagamento".


6. Assinaturas

Recorrência via cartão de crédito, boleto mensal (um boleto por ciclo, enviado ao e-mail do assinante), Pix Automático (§3.6) ou carnê (boletos futuros emitidos de uma vez). Cada plano define a periodicidade e o valor; cada assinatura vincula um cliente a um plano. Assinatura no Pix comum (billing_type: "PIX") não é criada: para Pix, use PIX_AUTOMATIC.

Planos

Criar:

POST /api/v1/plans/

{
  "name": "Plano Pro",
  "price": 179.90,
  "billing_cycle": "monthly",
  "description": "Acesso completo",
  "trial_days": 7,
  "trial_price": "0.00",
  "max_cycles": 0,
  "setup_fee": "0.00",
  "cover_image_url": "https://..."
}

Ciclos: weekly, biweekly, monthly, bimonthly, quarterly, semiannually, yearly.

Campos opcionais (catálogo expandido):

  • trial_days (int, default 0) — dias de teste antes da primeira cobrança real. 0 desativa. Em contas onde o período de teste não está disponível no cartão ou boleto mensal (GET /settings/ → trial_somente_carne: true), com teste a assinatura só é criada no carnê (CARNE, 1º vencimento no fim do teste), por POST /subscriptions/: no cartão, no boleto mensal e no Pix Automático a 1ª cobrança sai logo no início. Nessas contas, o checkout público de plano com teste também não aceita assinatura (enabled_payment_methods vazio e unavailable_message).
  • trial_price (decimal, default 0) — só 0 é aceito (trial gratuito); valor maior devolve 400.
  • max_cycles (int, default 0) — encerra a assinatura após N cobranças. 0 = indeterminado.
  • setup_fee (decimal, default 0) — só 0 é aceito; taxa de adesão não é suportada e valor maior devolve 400.
  • cover_image_url — capa exibida no checkout público.
  • thank_you_url — link de entrega do plano (para onde o cliente vai depois de pagar).
  • allow_customer_defined_amount (bool, default false) — "Pagador define o valor": o checkout público pede o valor ao cliente, e ele vira o amount_override da assinatura. No POST .../pay/ do checkout, envie o valor em amount_override; o link do plano aceita ?suggested=150,00 para pré-preencher.
  • min_amount / max_amount (decimal, opcionais) — piso e teto do valor escolhido pelo cliente. Só são aceitos com allow_customer_defined_amount=true (senão, 400). Valor fora da faixa no checkout devolve 400 com "Valor mínimo aceito: R$ …" ou "Valor máximo aceito: R$ …".
  • requires_approval (bool, default false) — "Exige aprovação manual": a assinatura feita pelo checkout público nasce pending_approval, sem cobrar nada, até você aprovar ou rejeitar (ver Aprovar ou rejeitar).

Listar / Detalhes / Atualizar / Excluir:

GET    /api/v1/plans/
GET    /api/v1/plans/{id}/
PUT    /api/v1/plans/{id}/        # substitui
PATCH  /api/v1/plans/{id}/        # parcial
DELETE /api/v1/plans/{id}/        # bloqueado se houver assinaturas ativas

Assinaturas

Criar:

POST /api/v1/subscriptions/

{
  "customer_id": "uuid-do-cliente",
  "plan_id": "uuid-do-plano",
  "billing_type": "CREDIT_CARD",
  "credit_card_token": "tok_xxx"
}

billing_type: CREDIT_CARD, BOLETO (boleto mensal), PIX_AUTOMATIC ou CARNE. O método precisa estar habilitado na conta; PIX_AUTOMATIC precisa também estar liberado (§3.6). Para carnê, veja Assinatura em carnê.

  • BOLETO: o 1º boleto é gerado na criação. Em contas em que o boleto mensal chega por e-mail, o cliente precisa ter e-mail cadastrado; sem ele, 400 em billing_type: {"billing_type": ["Para assinatura no boleto mensal, o cliente precisa ter e-mail cadastrado: é por ele que o boleto de cada mês chega."]}. Cada cobrança também aparece em GET /transactions/ e chega pelos webhooks de pagamento.

  • PIX responde 400 em billing_type, sem criar nada, com a alternativa que a conta tem. Ex.: {"billing_type": ["Assinatura no Pix comum não está disponível: use Pix Automático."]} (sem Pix Automático na conta, a mensagem aponta cartão, boleto ou carnê, conforme o que a conta tem).

  • Plano com período de teste (trial_days > 0), em conta com trial_somente_carne: true, aceita só CARNE; outro meio responde 400 com "Este plano tem período de teste: a assinatura só pode ser feita no carnê.". Em conta com trial_somente_carne: false, o teste não restringe o meio.

  • amount_override (decimal, opcional, >= 0): preço personalizado desta assinatura, no lugar do preço do plano. Vazio ou null = preço do plano. Para mudar depois, use Alterar valor.

  • credit_card_token (opcional, só cartão): sem ele, a resposta traz charge_url, o link de ativação em que o cliente cadastra o cartão; o mesmo link fica em activation_url enquanto a assinatura estiver pending.

  • Com PIX_AUTOMATIC, a resposta traz pix_automatic com qr_code e qr_payload (copia e cola): o QR Code de autorização, que já inclui a 1ª cobrança. O cliente precisa ter CPF/CNPJ, nome e endereço completo cadastrados.

  • POST /subscriptions/ não aceita cupom: para um valor diferente, use amount_override.

  • payer_ip (opcional): IP do pagador no momento da adesão. Em assinatura no cartão, o IP de quem paga é exigido. Se você chama a API pelo seu servidor, envie o IP do navegador do cliente; sem o campo, usamos o IP da requisição, que será o do seu servidor.

Listar:

GET /api/v1/subscriptions/?page=1

Resposta paginada. Filtros aceitos:

  • status: pending_approval, pending, active, overdue, canceled, rejected, expired;
  • plan: id do plano;
  • billing_type;
  • has_override: true (só com preço personalizado) ou false (só no preço do plano);
  • search: nome, e-mail ou CPF/CNPJ do cliente.

Cada item traz amount_override (ou null), effective_amount (o valor cobrado por ciclo), activation_url (só enquanto pending, no cartão) e pending_amount_change (ver Alterar valor).

Detalhe:

GET /api/v1/subscriptions/{id}/
GET /api/v1/subscriptions/{id}/detail/

O primeiro devolve o mesmo objeto da lista. O /detail/ devolve um resumo (id, customer_name, plan_name, plan_price, plan_cycle, status, billing_type, created_at) e, quando a assinatura está active ou overdue, tenta acrescentar next_due_date, cycle, value e provider_status. Esses campos extras podem faltar e não são a data oficial da próxima cobrança: para a data de uma mudança de valor, use pending_amount_change.effective_date. As cobranças de cada ciclo estão em GET /transactions/ (filtre pelo cliente) e chegam pelos webhooks de pagamento.

Exportar (CSV):

GET /api/v1/subscriptions/export/

Aceita os mesmos filtros da lista. Colunas: subscription_id, customer_cpf_cnpj, customer_name, customer_email, plan_slug, plan_name, plan_price, amount_override, effective_amount, billing_type, status, created_at, canceled_at, provider_subscription_id.

Importar em lote:

POST /api/v1/subscriptions/bulk/

{
  "rows": [
    {
      "cpf_cnpj": "15150016608",
      "name": "Membro Um",
      "email": "membro1@x.com",
      "phone": "11999990000",
      "plan_slug": "mensalidade",
      "billing_type": "CREDIT_CARD",
      "amount_override": "473.00"
    }
  ]
}
  • Até 500 linhas por lote (acima disso, ou lista vazia, 400).
  • Obrigatórios por linha: cpf_cnpj, name, email, plan_slug, billing_type. phone e amount_override são opcionais.
  • billing_type: CREDIT_CARD ou BOLETO (boleto mensal); na conta global, PIX no lugar de BOLETO. Fora da conta global, PIX vai para errors com a mesma mensagem da criação; carnê e Pix Automático não entram na importação. No cartão, cada assinatura nasce sem cartão, com o link de ativação em activation_url (em GET /subscriptions/{id}/) para enviar ao cliente. No boleto, o boleto de cada ciclo vai para o email da linha.
  • Plano com período de teste, em conta com trial_somente_carne: true, não entra na importação: a linha vai para errors com a mesma mensagem da criação.
  • O cliente é criado ou atualizado pelo cpf_cnpj. Se ele já tem assinatura active, pending ou overdue no mesmo plano, a linha é pulada.
  • Cada linha é independente: uma falha não desfaz as outras.
  • Resposta: {"created": N, "skipped": N, "errors": [{"row", "cpf_cnpj", "error"}], "total_rows": N}, com 200 sem erros e 207 quando alguma linha falhou.

Aprovar ou rejeitar inscrição

Assinaturas de plano com requires_approval=true feitas pelo checkout público nascem pending_approval, sem nada cobrado. O checkout responde com "status": "pending_approval" e "requires_approval": true.

POST /api/v1/subscriptions/{id}/approve/
POST /api/v1/subscriptions/{id}/reject/      { "reason": "opcional" }
  • Aprovar começa a cobrança como numa assinatura nova daquele meio. A resposta é a assinatura:
    • CREDIT_CARD: traz charge_url, o link de ativação para enviar ao cliente;
    • PIX_AUTOMATIC: traz pix_automatic (qr_code, qr_payload) para enviar ao cliente. Se o Pix Automático não estiver mais liberado na conta, 400 e a inscrição continua pending_approval;
    • BOLETO: o 1º boleto é gerado. Em contas em que ele chega por e-mail, sem e-mail cadastrado, 400 (mesma mensagem de POST /subscriptions/) e a inscrição continua pending_approval até você completar o cadastro do cliente;
    • Inscrição em PIX (antiga), ou em plano que ganhou período de teste depois da inscrição (em conta com trial_somente_carne: true): 400 com o motivo e "Rejeite esta inscrição e crie a assinatura de novo com outra forma de pagamento."; a inscrição continua pending_approval.
    • Se o cliente usou cupom no checkout (só PIX_AUTOMATIC), o desconto é aplicado agora.
  • Rejeitar move para rejected e não cobra nada. reason fica só no histórico.
  • Nos dois, assinatura fora de pending_approval devolve 400.

Cancelar:

POST /api/v1/subscriptions/{id}/cancel/

Em assinatura em carnê, o cancelamento também cancela as parcelas em aberto (ver Ciclo de vida e datas). O cancelamento também descarta uma mudança de valor agendada.

Alterar valor

POST /api/v1/subscriptions/{id}/change-amount/

{ "new_amount_override": "473.00", "reason": "Reajuste anual" }

new_amount_override com "" ou null remove o valor próprio da assinatura, e ela volta ao preço do plano. reason é opcional e fica no histórico. Valor igual ao atual devolve 400.

Assinatura que já cobrou (active ou overdue) no cartão ou no boleto mensal (ou no Pix comum, em assinatura antiga): o novo valor vale a partir da próxima cobrança. O pedido fica agendado e nada muda antes disso. O cliente nunca paga duas vezes o mesmo período. A resposta traz a data em scheduled_for e a mudança em pending_amount_change:

{
  "id": "5b0c...",
  "status": "active",
  "amount_override": "250.00",
  "effective_amount": "250.00",
  "activation_url": "",
  "scheduled_for": "2026-10-26",
  "requires_customer_action": false,
  "pending_amount_change": {
    "id": "9f1e...",
    "new_amount": "473.00",
    "new_amount_override": "473.00",
    "effective_date": "2026-10-26",
    "status": "scheduled",
    "reason": "Reajuste anual",
    "requested_at": "2026-09-27T14:02:11-03:00",
    "origin": "org"
  }
}
  • amount_override e effective_amount continuam com o valor atual até a data.
  • pending_amount_change também aparece em GET /subscriptions/ e GET /subscriptions/{id}/. É null quando não há mudança agendada.
  • pending_amount_change.status:
    • scheduled: aguardando a data.
    • applying: sendo aplicada, na madrugada da data.
    • needs_review: nossa equipe está conferindo. Não faça outro pedido; aguarde o contato. Até a conclusão, a assinatura segue no valor atual.
  • pending_amount_change.origin: org (pedido da loja). coupon só aparece em assinatura antiga no Pix ou no boleto mensal que usou cupom: a volta ao valor normal depois da 1ª cobrança com cupom.
  • Pedir de novo substitui a mudança agendada. Durante applying, o pedido devolve 409.
  • Se a cobrança daquela data já tiver saído no valor antigo, a mudança passa para a cobrança seguinte e effective_date avança.
  • Sem próxima cobrança programada, o pedido devolve 409. Se não foi possível consultar a data, devolve 502 e nada é registrado; tente de novo.

Cancelar a mudança agendada (antes da data):

POST /api/v1/subscriptions/{id}/change-amount/cancel/

Devolve a assinatura com pending_amount_change: null. Sem mudança agendada, devolve 404. Durante applying, devolve 409. Mudança com origin: "coupon" também devolve 409: ela não se cancela, só se substitui por um novo change-amount.

No dia da cobrança:

  • Boleto mensal (e Pix comum, em assinatura antiga): a cobrança do ciclo já sai no valor novo, sem ação do cliente. O boleto no valor novo chega ao e-mail dele como os anteriores.
  • Cartão: a assinatura passa a pending e o cliente precisa cadastrar o cartão de novo pelo link de ativação. O link aparece em activation_url no detalhe enquanto a assinatura estiver pending, e a loja também o recebe por e-mail. A cobrança do ciclo sai quando o cliente cadastra o cartão, e a assinatura volta a active.

Quando a mudança é imediata:

  • Pix Automático: a próxima cobrança já sai no valor novo, sem ação do cliente. A resposta vem com scheduled_for: null.
  • Assinatura pending (nada cobrado ainda): a mudança vale na hora, e scheduled_for vem null. No cartão, a resposta traz requires_customer_action: true e o link em charge_url, que também fica em activation_url no detalhe. Envie o link ao cliente. Nos outros meios, requires_customer_action vem false.
  • Carnê: o pedido devolve 409, porque as parcelas já foram emitidas.

Comportamento por meio de pagamento

CREDIT_CARDBOLETOPIX_AUTOMATICCARNE
Requisito do cliente—Em contas em que o boleto mensal chega por e-mail: e-mail cadastrado — sem ele, 400 na criação/aprovação——
1ª cobrançaCom credit_card_token (ou cartão no checkout), cobrada na criação. Sem token, quando o cliente cadastra o cartão pelo charge_urlBoleto gerado na criaçãoJunto com a autorização: pix_automatic.qr_code / qr_payload na resposta1ª parcela do carnê, emitido na criação
Ciclos seguintesCobrados no cartão, na data do cicloUm boleto por cicloCriados pela Vitrin e debitados na data (§3.6)As outras parcelas, já emitidas
change-amountAgendado para a próxima cobrança; na data, o cliente cadastra o cartão de novo (activation_url)Agendado para a próxima cobrança; o boleto seguinte sai no valor novo, sem ação do clienteImediato409
Cupom (checkout)Com PIX_AUTOMATIC na conta: 400 "Nesta assinatura, o cupom não vale no cartão." Sem: 400 "Cupom não disponível para este plano."Com PIX_AUTOMATIC na conta: 400 "Nesta assinatura, o cupom não vale nesta forma de pagamento." Sem: 400 "Cupom não disponível para este plano."Só na 1ª cobrança; nada agendadoNão se aplica (sem checkout)
Trial grátis (em conta com trial_somente_carne: true)400: a 1ª cobrança sai logo depois do cadastro do cartão400: o 1º boleto sai na criação400: a 1ª cobrança vem com a autorização1º vencimento padrão no fim do trial. Único meio aceito nessas contas
approveDevolve charge_urlGera o 1º boletoDevolve pix_automaticNão se aplica

PIX comum não cria assinatura por nenhuma porta (POST /subscriptions/, bulk, approve, checkout público): exigiria um Pix novo do assinante a cada ciclo. Use PIX_AUTOMATIC (débito automático). Para quem prefere boleto, há BOLETO (um boleto por ciclo, no e-mail) e CARNE (todos os boletos de uma vez, com PDF e envio por e-mail pela Vitrin).

Assinatura em carnê

O carnê é um meio de pagamento da assinatura (billing_type: "CARNE"). A Vitrin emite de uma vez N boletos com vencimento futuro, um por ciclo do plano, e os entrega num PDF com a marca da sua loja. Não há recorrência no processamento: cada parcela é um boleto avulso comum, com o mesmo funil de taxa, repasse e webhooks de qualquer cobrança.

Quando usar:

  • O assinante não tem cartão ou não quer deixá-lo salvo, e prefere pagar boleto.
  • O assinante quer receber os boletos do período de uma vez, para imprimir, agendar no banco ou pagar adiantado.
  • O plano tem período de teste, e a conta é uma daquelas em que o cartão ou o boleto mensal não respeitam o teste (trial_somente_carne: true): nela, o carnê é o meio que respeita (1º vencimento no fim do teste).

Para um boleto por ciclo, enviado ao e-mail do assinante, use billing_type: "BOLETO" (boleto mensal).

Requisitos: BOLETO em enabled_payment_methods, valor da assinatura maior que zero e conta que não seja LLC (conta global). Fora disso, a resposta é 400.

Criar assinatura em carnê

POST /api/v1/subscriptions/
Idempotency-Key: assinatura-4711

{
  "customer_id": "uuid-do-cliente",
  "plan_id": "uuid-do-plano",
  "billing_type": "CARNE",
  "amount_override": "150.00",
  "carne": {
    "installments": 12,
    "first_due_date": "2026-10-10",
    "send_email": true
  }
}

O objeto carne é opcional, e todos os campos dele também:

  • installments: número de parcelas, de 1 a 12. Padrão: 12.
  • first_due_date: vencimento da 1ª parcela (AAAA-MM-DD). Padrão: hoje + 3 dias, no dia de Brasília; se o plano tem trial (trial_days) maior que 3, hoje + trial_days. Data no passado responde 400. As demais parcelas vencem um ciclo do plano depois, sempre contadas a partir da 1ª: com 1º vencimento em 31/01, as próximas vencem em 28/02, 31/03, 30/04 e 31/05. O dia 31 não vira 30 pelo caminho.
  • send_email: padrão false. Com true, a Vitrin envia o carnê por e-mail ao assinante na emissão e em cada renovação, se ele tiver e-mail cadastrado. Veja send-email/.

Outros pontos:

  • Valor da parcela: é o valor da assinatura (amount_override ou o preço do plano). Com o repasse da taxa de boleto ligado (§3.8), cada parcela sai pelo valor bruto, e o estado do repasse fica congelado na assinatura, como em qualquer meio.
  • Campos ignorados: credit_card_token e next_due_date não valem para carnê.
  • Trial: sem first_due_date, a 1ª parcela vence no fim do trial do plano (hoje + trial_days), nunca antes de hoje + 3 dias. O trial_price não vale no carnê: toda parcela tem o valor da assinatura. Com first_due_date explícito, vale a data informada.
  • Status: a assinatura nasce pending e vira active quando a 1ª parcela for paga.
  • Emissão tudo ou nada: se algum boleto for recusado, os já emitidos são cancelados e a assinatura não é criada. A resposta é 422 na recusa (mesmo formato de §3.3, "Recusas") ou 502 com o processamento indisponível.
  • Sandbox: criada com a chave de teste (vd_test_*), a assinatura fica marcada como de teste e o carnê sai com is_sandbox: true.

Tempo de resposta: a emissão é síncrona. A Vitrin emite um boleto por parcela, em sequência, antes de responder: são até 12 chamadas ao processamento, e cada uma leva alguns segundos, podendo chegar a 18 segundos quando o processamento está lento. Com 12 parcelas, a resposta pode passar de 1 minuto no pior caso. A troca soma a isso a consulta e o encerramento da recorrência anterior. Por isso:

  • configure o timeout do seu cliente HTTP em pelo menos 120 segundos para POST /subscriptions/ com CARNE e para switch-to-carne/;
  • mande sempre Idempotency-Key na criação e, se a conexão cair ou der timeout, repita com a mesma chave (veja abaixo). Sem a chave, repetir cria outra assinatura com outros boletos.

Idempotency-Key (recomendado): vale só com billing_type: "CARNE". Nos outros meios, o header é ignorado. Use uma chave estável do seu lado, de até 128 caracteres. O que acontece ao repetir a mesma chave:

  • O pedido original terminou bem: o mesmo 201, com a mesma assinatura e o mesmo carnê, sem emitir boletos de novo. É a resposta a esperar num retry depois de timeout.
  • O pedido original ainda está emitindo: 409 carne_in_flight. Espere alguns segundos e repita com a mesma chave.
    { "error": "carne_in_flight", "detail": "Um pedido com esta Idempotency-Key ainda está emitindo o carnê. Repita em alguns segundos.", "subscription_id": "<uuid>" }
    
  • O pedido original falhou e tudo foi desfeito (boleto recusado, processamento fora, com todos os boletos emitidos cancelados): a assinatura não ficou gravada, então a mesma chave emite de novo, como um pedido novo.
  • O pedido original foi interrompido no meio (queda do servidor, por exemplo): enquanto a varredura não passa, 409 carne_in_flight; depois dela (até ~2 horas, veja "Emissão interrompida" abaixo), a mesma chave emite de novo (201, com assinatura nova).
  • O pedido original falhou e deixou um boleto que o processamento não deixou cancelar (a assinatura fica canceled, dona desse boleto): 409 carne_failed. Use outra chave para tentar de novo.
    { "error": "carne_failed", "detail": "O pedido com esta Idempotency-Key falhou ao emitir o carnê. Use outra chave para tentar de novo.", "subscription_id": "<uuid>" }
    

Emissão interrompida: se a emissão for interrompida no meio (queda do servidor durante um deploy, por exemplo), as parcelas já emitidas ficam sem carnê. Uma varredura de hora em hora trata as que têm mais de 1 hora:

  • parcela com boleto emitido: o boleto é cancelado no processamento e a parcela passa a canceled;
  • parcela cuja emissão foi cortada no meio da chamada ao processamento (sem id do boleto na Vitrin): passa a failed. A Vitrin não consulta o processamento para achar esse boleto. Custo: se ele chegou a ser criado lá, ninguém recebeu o link dele, e ele fica sem cobrança até vencer; se mesmo assim for pago, o pagamento é reconhecido na parcela e você recebe o PAYMENT_RECEIVED normal.

Depois disso, o vencimento fica livre para uma nova emissão (a troca na mesma data funciona). Na criação, a assinatura que ficou pending sem carnê passa a canceled e libera a Idempotency-Key: repetir o pedido com a mesma chave emite de novo.

Resposta (201): a assinatura, com o carnê no campo carne.

{
  "id": "<uuid-da-assinatura>",
  "customer": "<uuid-do-cliente>",
  "customer_name": "Maria Souza",
  "plan": "<uuid-do-plano>",
  "plan_name": "Plano Pro",
  "plan_price": "179.90",
  "amount_override": "150.00",
  "effective_amount": "150.00",
  "status": "pending",
  "billing_type": "CARNE",
  "provider_subscription_id": "",
  "current_period_start": "2026-09-24T10:15:02.114305-03:00",
  "current_period_end": null,
  "canceled_at": null,
  "created_at": "2026-09-24T10:15:02.114411-03:00",
  "updated_at": "2026-09-24T10:15:02.114422-03:00",
  "carne": {
    "id": "<uuid-do-carnê>",
    "subscription_id": "<uuid-da-assinatura>",
    "status": "active",
    "number_of_installments": 12,
    "first_due_date": "2026-10-10",
    "send_email": true,
    "is_sandbox": false,
    "pdf_url": "https://api.vitrin.digital/api/v1/public/carnes/<token>.pdf",
    "renewed_by_id": null,
    "installments": [
      {
        "number": 1,
        "amount": "150.00",
        "due_date": "2026-10-10",
        "status": "pending",
        "boleto_url": "https://…",
        "digitable_line": "23790448095616862379336011058009740430000124020",
        "pix_copy_paste": "00020101021226900014br.gov.bcb.pix2568…6304ABCD",
        "transaction_id": "<uuid-da-transacao>"
      },
      { "number": 2, "amount": "150.00", "due_date": "2026-11-10", "status": "pending", "…": "…" }
    ],
    "created_at": "2026-09-24T10:15:09.870112-03:00",
    "updated_at": "2026-09-24T10:15:09.870120-03:00"
  }
}

Trocar uma assinatura existente para carnê

POST /api/v1/subscriptions/{id}/switch-to-carne/

{
  "carne": {
    "installments": 12,
    "send_email": true
  }
}

O corpo carne é o mesmo da criação, e também é opcional. A assinatura mantém id, cliente, plano, valor e histórico de cobranças. Só o meio de pagamento muda.

O que a troca aceita:

  • Status: a assinatura precisa estar active ou overdue. Depois da troca ela fica active, mesmo se estava overdue: a inadimplência era da cobrança anterior, e daqui em diante quem decide são as parcelas. Se uma parcela vencer sem pagamento (passada a carência), a assinatura volta a overdue e a política de inadimplência corre a partir dali. Nenhum aviso nem cancelamento por inadimplência sai antes disso.
  • Ambiente: a chave precisa ser do mesmo ambiente da assinatura. A chave de teste (vd_test_*) numa assinatura real, ou a de produção numa assinatura criada com a chave de teste, responde 400 antes de emitir qualquer boleto: { "detail": "Esta assinatura é real; a chave de teste (vd_test_*) não a troca para carnê. Use a chave de produção." }. Assinaturas criadas antes da chegada do carnê à API contam como reais.
  • Meio: cartão (CREDIT_CARD) ou boleto recorrente (BOLETO).

Recusas com 409, todas com o mesmo formato:

{ "error": "carne_conflict", "detail": "A assinatura já está em carnê ativo." }
detailMotivo
Só assinatura ativa ou inadimplente troca para carnê (status atual: …).Assinatura pending, pending_approval, canceled, etc.
A assinatura já está em carnê ativo.Já existe um carnê active nessa assinatura.
Assinatura em Pix Automático não troca para carnê.Encerrar o mandato do Pix Automático encerra a assinatura inteira. Para passar um assinante de Pix Automático para carnê, cancele a assinatura e crie outra em carnê.
A forma de pagamento da assinatura mudou durante a troca.Outro pedido mudou a assinatura ao mesmo tempo. Os boletos emitidos por este pedido foram cancelados.

Vencimento da 1ª parcela:

  • Padrão: sem first_due_date, a 1ª parcela vence na próxima cobrança da assinatura, segundo o processamento. Se o processamento não informar a data, vale o fim do período atual (current_period_end). Nunca vence antes de amanhã. Assim, o ciclo já pago no cartão ou no boleto não é cobrado de novo.
  • Data dentro do período já pago: um first_due_date explícito anterior ao fim do período já pago responde 400:
    { "first_due_date": ["O primeiro vencimento (2026-10-01) cai dentro do período já pago, que vai até 2026-10-15. Use uma data a partir do fim do período para não cobrar o mesmo ciclo duas vezes."] }
    
  • Outros 400: opções inválidas (installments fora de 1–12, data no passado), boleto não habilitado, valor zero ou chave de outro ambiente.

O que acontece com a recorrência anterior:

  1. A Vitrin emite as parcelas.
  2. Encerra a recorrência anterior no processamento, do mesmo jeito que o cancelamento faz.
  3. Grava o carnê.

Se a emissão ou o encerramento falharem, os boletos emitidos são cancelados, a assinatura fica exatamente como estava e a resposta é de erro (409, 422 ou 502). Depois da troca:

  • billing_type passa a CARNE;
  • provider_subscription_id fica vazio, porque a recorrência anterior não existe mais;
  • não há mais cobrança no cartão nem boleto novo da recorrência.

Cobrança tardia da recorrência antiga: uma cobrança que já tinha sido disparada antes da troca e que só chega depois (por exemplo, o boleto do ciclo corrente pago depois da troca) continua sendo reconhecida. Ela gera os PAYMENT_* normais, com o subscription_id da assinatura e carne_id: null.

  • Paga: conta como pagamento da assinatura.
  • Recusada ou vencida: fica registrada, mas não deixa a assinatura inadimplente.

A troca não aceita Idempotency-Key. Um segundo pedido igual recebe 409 ("já está em carnê ativo"): num retry depois de timeout, leia esse 409 como sucesso do primeiro pedido e confira com GET /carnes/?subscription_id=. Como na criação, a troca pode passar de 1 minuto; use timeout de pelo menos 120 segundos.

Resposta (200): a assinatura já com billing_type: "CARNE" e o carnê no campo carne, no mesmo formato da criação.

Alterar valor: POST /subscriptions/{id}/change-amount/ em assinatura em carnê responde 409, porque as parcelas já foram emitidas. Para mudar o valor, cancele a assinatura e crie outra.

Carnês

GET  /api/v1/carnes/?subscription_id=<uuid>
GET  /api/v1/carnes/{id}/
POST /api/v1/carnes/{id}/send-email/

Listar e detalhar:

  • A listagem é paginada (count/next/previous/results, 25 por página, ?page_size= até 100) e vem do mais novo para o mais antigo.
  • Com subscription_id, a listagem traz só os carnês daquela assinatura. Um id que não é UUID responde 400.
  • Carnê de outra organização responde 404.

Campos do carnê (mesmo corpo na criação, na troca, na listagem e nos webhooks CARNE_*):

CampoDescrição
idId do carnê
subscription_idAssinatura do carnê
statusactive (há parcela em aberto), completed (todas pagas) ou canceled
number_of_installmentsN, de 1 a 12
first_due_dateVencimento da 1ª parcela
send_emailSe o carnê vai por e-mail ao assinante na emissão e na renovação
is_sandboxtrue se o carnê foi emitido com a chave de teste (vd_test_*): boletos do ambiente de teste, que não cobram de verdade
pdf_urlPDF público do carnê (ver abaixo)
renewed_by_idCarnê seguinte, emitido na renovação. null até lá
installmentsAs parcelas, em ordem
created_at / updated_atDatas do carnê

Campos de cada parcela: number, amount (string decimal), due_date, status, boleto_url, digitable_line, pix_copy_paste e transaction_id. pix_copy_paste é o Pix copia e cola do boleto da parcela: pagar por ele quita a parcela. Vem null quando o boleto não tem Pix (parcelas emitidas antes desta versão, de setembro/2026, por exemplo). Os status são os de qualquer cobrança:

  • pending: em aberto;
  • overdue: vencida, passada a carência;
  • received ou confirmed: paga;
  • canceled: cancelada (por nós, com a assinatura, ou baixada no processamento — veja Ciclo de vida).

transaction_id é o payment.id dos webhooks e o id de GET /payments/{id}/.

Enviar por e-mail:

POST /api/v1/carnes/{id}/send-email/ manda ao assinante o e-mail "Seu carnê — <sua loja>", com a marca da sua org e sem "Vitrin" em lugar nenhum visível a ele. O e-mail traz as parcelas em aberto (valor e vencimento) e um botão para o PDF.

Resposta (200): { "sent_to": "assinante@exemplo.com" }. O envio é assíncrono: o 200 confirma que o pedido foi aceito, não a entrega.

Na ordem em que são checados:

StatusQuandoCorpo
404Carnê inexistente ou de outra org—
409Carnê completed ou canceled: não há o que enviar{ "error": "carne_not_active", "detail": "Carnê não está ativo (status atual: completed).", "status": "completed" }
400Assinante sem e-mail cadastrado{ "error": "Assinante sem e-mail cadastrado." }
4294º envio na mesma hora para o mesmo carnê (limite de 3 por hora por carnê){ "error": "carne_email_rate_limited", "detail": "Limite de 3 envios por hora atingido para este carnê." }
42931º envio de carnê na mesma hora pela conta inteira (limite de 30 por hora){ "error": "carne_email_org_rate_limited", "detail": "…" }

O envio automático (send_email: true na emissão, na troca e na renovação) não conta nesses limites.

PDF público (pdf_url):

GET /api/v1/public/carnes/<token>.pdf
  • Acesso: sem autenticação. O token é opaco e não adivinhável (não é o id). É esse link que você manda ao assinante, e o e-mail e o WhatsApp do dashboard usam o mesmo.
  • Conteúdo:
    • a marca da sua loja (logo ou nome, e a cor do checkout);
    • um canhoto por parcela em aberto (pending ou overdue), com valor, vencimento, pagador, linha digitável e código de barras;
    • no canhoto, "Pague com Pix": o QR Code do Pix copia e cola da parcela, quando o boleto tem Pix. O QR é gerado por nós a partir do pix_copy_paste, sem logo e sem link para fora;
    • as parcelas já pagas não ganham canhoto, porque um canhoto de parcela quitada convida a pagar de novo; o topo do PDF diz quais foram pagas;
    • nada de "Vitrin" nem de qualquer marca de terceiros.
  • Atualização: o PDF é gerado a cada abertura, então sempre reflete o status atual das parcelas.
  • Respostas:
    • carnê completed abre com o aviso de que não há parcelas em aberto;
    • carnê canceled, token desconhecido ou conta suspensa respondem 404, com o mesmo corpo nos três casos;
    • limite de 10 aberturas por minuto por IP; acima disso, 429 com Retry-After.
  • Privacidade: o PDF traz o nome do assinante. Trate o link como dado dele e compartilhe só com ele.

Ciclo de vida e datas

Um job diário roda às 11:00 (horário de Brasília) e cuida de vencimento, renovação e cancelamento pendente. A política de inadimplência roda às 12:00, a mesma de todas as assinaturas. As datas abaixo seguem o dia de Brasília.

Parcela paga (PAYMENT_RECEIVED, com carne_id e installment_number):

  • A assinatura pending ou overdue vira active.
  • O período (current_period_start → current_period_end) passa a ser o ciclo daquela parcela: do vencimento dela ao vencimento da seguinte.
  • Pagamento antecipado é aceito. Pagar a parcela 3 antes da 2 leva o período até o fim do ciclo da 3, e o período nunca volta atrás: pagar a 2 depois não o encurta.
  • Se outra parcela já estiver vencida (passada a carência), a assinatura continua overdue até essa também ser paga.
  • Com todas as parcelas pagas, o carnê vira completed.

Vencimento, com carência de 4 dias: o boleto pago no dia do vencimento pode levar alguns dias para compensar. Por isso, uma parcela não paga tem 4 dias corridos de carência e só depois é tratada como vencida. No 5º dia após o vencimento:

  • a parcela passa a overdue, e você recebe PAYMENT_OVERDUE dela, uma única vez por parcela, com carne_id e installment_number;
  • a assinatura passa a overdue, e o relógio da inadimplência começa.

O boleto continua pagável depois do vencimento, com multa e juros. Se for pago, a parcela vai a received e a assinatura volta a active, desde que não haja outra parcela vencida.

Parcela baixada no processamento: se um boleto do carnê for cancelado ou baixado direto no processamento (no painel dele, por exemplo), você recebe PAYMENT_DELETED da parcela (com carne_id e installment_number), e ela passa a canceled. Como ela não foi paga e não pode mais ser, conta como vencida depois da mesma carência de 4 dias: a assinatura passa a overdue e a inadimplência corre normalmente. Pagar as outras parcelas não a tira de overdue. Para regularizar, cancele a assinatura e crie outra, ou combine outra cobrança com o assinante. Os cancelamentos feitos pela Vitrin (cancelamento da assinatura, inadimplência, suporte) não contam como baixa.

Conferência diária. O pagamento ou a baixa de um boleto em aberto (avulso ou parcela de carnê) que já passou do vencimento mais a carência de 4 dias às vezes só é confirmado depois, numa conferência diária da Vitrin. Se o boleto foi pago, ele vai a received com paid_at na data real do pagamento, e você recebe PAYMENT_RECEIVED. Se foi baixado ou cancelado no processamento, ele vai a canceled e você recebe PAYMENT_DELETED (na parcela de carnê, vale a regra da baixa acima). Os eventos são os mesmos de sempre e saem durante a própria conferência, no dia seguinte (ou depois) ao pagamento ou à baixa, e não no momento em que aconteceram. Um boleto que segue em aberto continua sendo conferido. Trate-os de forma idempotente por payment.id, como qualquer webhook.

Inadimplência: é a política de todas as assinaturas, contada a partir do dia em que a assinatura ficou overdue:

  • aviso subscription.dunning_warning aos 7 dias, por volta do 12º dia após o vencimento da parcela;
  • cancelamento aos 30 dias, por volta do 35º dia: a assinatura é cancelada, você recebe subscription.canceled_for_dunning, as parcelas em aberto são canceladas e sai CARNE_CANCELED.

Exemplo com uma parcela que vence em 10/11/2026 e não é paga:

DataO que acontece
10/11A parcela vence
11/11 a 14/11Carência: nada muda
15/11Parcela overdue + PAYMENT_OVERDUE; assinatura overdue
22/11subscription.dunning_warning
15/12Assinatura cancelada: subscription.canceled_for_dunning + parcelas em aberto canceladas + CARNE_CANCELED

Cancelamento: quando a assinatura é cancelada, pela org (POST /subscriptions/{id}/cancel/ ou dashboard) ou por inadimplência:

  • as parcelas em aberto (pending e overdue) são canceladas no processamento e passam a canceled;
  • as parcelas já pagas não mudam;
  • quando todas as parcelas em aberto estiverem canceladas, o carnê vira canceled e você recebe CARNE_CANCELED;
  • se o processamento falhar em alguma parcela, o job diário tenta de novo, e o CARNE_CANCELED só sai quando todas estiverem canceladas;
  • depois do cancelamento, o pdf_url responde 404.

Renovação automática: 30 dias antes do vencimento da última parcela, a Vitrin emite o próximo carnê. As condições são duas: a assinatura está active e o carnê ainda não foi renovado. Assinatura inadimplente não renova. O carnê novo:

  • tem o mesmo N e a mesma opção de e-mail;
  • começa no ciclo seguinte à última parcela e continua a série de vencimentos (o dia 31 continua 31).

O que sai na renovação:

  • o webhook CARNE_RENEWED, com o carnê novo (não sai CARNE_CREATED);
  • no carnê anterior, renewed_by_id aponta para o novo;
  • um e-mail de aviso ao dono da conta;
  • o e-mail "Seu novo carnê — <sua loja>" ao assinante, se send_email estiver ligado.

Exemplo: 12 parcelas mensais de 10/01/2027 a 10/12/2027 renovam em 10/11/2027 (30 dias antes da última). O carnê novo vence de 10/01/2028 a 10/12/2028.

  • Renovação atrasada: se a assinatura volta a active depois da janela e o ciclo seguinte já passou, a 1ª parcela do carnê novo vence no dia seguinte.
  • Falha na renovação: se a emissão falhar, os boletos emitidos naquela tentativa são cancelados e o job do dia seguinte tenta de novo. Na 3ª falha, o dono da conta recebe um e-mail ("Falha na renovação do carnê"). Não há webhook para a falha, e as tentativas diárias continuam.
  • Boleto desabilitado na conta: a renovação exige o carnê disponível, como a criação (BOLETO em enabled_payment_methods e conta que não seja LLC). Sem isso, a Vitrin não renova e avisa uma única vez por carnê: webhook CARNE_RENEWAL_BLOCKED (com o carnê atual) e e-mail ao dono da conta ("Carnê não renovado: boleto desabilitado"). As parcelas já emitidas continuam valendo. Sem renovação, a assinatura deixa de ser cobrada depois da última parcela, a menos que o boleto seja reativado ou a assinatura passe para outro meio de pagamento. Se o boleto for reativado enquanto o carnê ainda está na janela de 30 dias, a renovação volta a acontecer no job seguinte. Isso não conta como falha de renovação.

Webhooks do carnê

CARNE_CREATED (emissão e troca), CARNE_RENEWED (renovação), CARNE_CANCELED (cancelamento) e CARNE_RENEWAL_BLOCKED (renovação barrada por boleto desabilitado) trazem o carnê completo, no mesmo formato da API:

{
  "event": "CARNE_CREATED",
  "carne": {
    "id": "<uuid-do-carnê>",
    "subscription_id": "<uuid-da-assinatura>",
    "status": "active",
    "number_of_installments": 12,
    "first_due_date": "2026-10-10",
    "send_email": true,
    "is_sandbox": false,
    "pdf_url": "https://api.vitrin.digital/api/v1/public/carnes/<token>.pdf",
    "renewed_by_id": null,
    "installments": [
      { "number": 1, "amount": "150.00", "due_date": "2026-10-10", "status": "pending", "boleto_url": "https://…", "digitable_line": "…", "pix_copy_paste": "…", "transaction_id": "<uuid>" }
    ],
    "created_at": "2026-09-24T10:15:09.870112-03:00",
    "updated_at": "2026-09-24T10:15:09.870120-03:00"
  }
}
  • Um evento por carnê: cada carnê gera no máximo um evento de cada tipo, e o X-Vitrin-Event-Id é estável (carne-created-<id>, carne-renewed-<id>, carne-canceled-<id>, carne-renewal-blocked-<id>). Trate o evento de forma idempotente por carne.id.
  • Momento da leitura: o carnê é lido no momento da entrega. Numa reentrega, as parcelas podem vir com status mais novo que o do momento do evento.
  • Parcelas: cada parcela segue gerando seus próprios PAYMENT_* (PAYMENT_RECEIVED, PAYMENT_OVERDUE…), com carne_id e installment_number (§5).

Multa, juros e taxa

  • Multa e juros: as parcelas têm multa de 2% e juros de mora de 0,4% sobre o valor depois do vencimento, as mesmas de todo boleto emitido pela Vitrin (§3.2). As taxas saem impressas no boleto, no campo "(+) Juros / Multa". Ainda não são configuráveis por conta.
  • Taxa: cada parcela é um boleto. A parcela paga tem a taxa de boleto da sua conta, descontada do líquido daquela parcela, ou repassada ao assinante se o repasse de boleto estiver ligado (§3.8). O carnê não tem taxa própria.
  • Sandbox: com a chave de teste (vd_test_*), as parcelas são emitidas no ambiente de teste e a renovação daquele carnê também. O carnê traz is_sandbox: true na API e nos webhooks. O PDF traz a faixa "Ambiente de teste", e o e-mail ao assinante leva o mesmo aviso no assunto. O assinante recebe o e-mail de verdade, então use um e-mail seu nos testes.

7. Produtos e Pedidos

Para vendas avulsas (eventos, pacotes, créditos, ingressos, infoprodutos).

Produtos

Criar:

POST /api/v1/products/

{
  "name": "Evento Premium",
  "product_type": "one_time",
  "price": 347.00,
  "description": "Acesso a 1 evento com recursos Pro",
  "long_description": "Texto rico em markdown para a página pública",
  "cover_image_url": "https://...",
  "video_url": "https://www.youtube.com/embed/...",
  "thank_you_url": "https://...",
  "allow_installments": true,
  "max_installments": 12
}

Tipos (product_type):

  • one_time — pagamento único (ingresso, infoproduto, serviço)
  • pack — pacote com N unidades (curso com aulas, conjunto de itens)
  • credits — créditos (saldo pré-pago para uso futuro)
  • contribution — doação, gorjeta, apoio voluntário; pagador define o valor
  • bundle — combo agrupando outros produtos (ver seção "Bundle")

Modo de preço (price_mode):

ValorComportamento
fixed (default)Cliente paga exatamente price
customer_definedCliente digita o valor no checkout. price vira sugestão pré-preenchida.

Para customer_defined, configure piso/teto:

  • min_price (decimal, opcional) — valor mínimo aceito
  • max_price (decimal, opcional) — teto opcional

O tipo contribution automaticamente força price_mode=customer_defined. Para um produto regular ("Pague quanto quiser pelo curso"), use one_time com price_mode=customer_defined.

Listar / Detalhes / Atualizar / Excluir:

GET    /api/v1/products/
GET    /api/v1/products/{id}/
PUT    /api/v1/products/{id}/
PATCH  /api/v1/products/{id}/
DELETE /api/v1/products/{id}/

Filtros: active, product_type, name__icontains.

Excluir ou arquivar (DELETE /api/v1/products/{id}/): o resultado depende do histórico de pedidos do produto.

SituaçãoRespostaO que acontece
Nenhum pedido204Produto excluído de verdade.
Algum pedido pending400Nada muda: o pedido ainda pode ser pago. error: "Há N pedido(s) pendente(s) deste produto. Cancele-os antes de excluir."
Faz parte de um pacote combinado400Nada muda. Remova-o do pacote antes.
Qualquer outro histórico (pago, entregue, cancelado, reembolsado, expirado)200Produto arquivado (corpo abaixo).
{
  "archived": true,
  "detail": "Produto arquivado: ele sai do catálogo e do checkout, e os pedidos continuam no histórico."
}

O produto arquivado sai da listagem e do detalhe (GET, PUT, PATCH e um novo DELETE dão 404), do checkout público, do one-click, da criação de pedidos (POST /orders/create/ dá 404) e dos order bumps. O slug fica livre, então um produto novo pode usar o mesmo. Pedidos, transações e relatórios continuam mostrando o produto e o nome dele. Um produto oferecido como order bump também é arquivado, mesmo sem pedidos, porque o bump o referencia.

Bundle (combo de produtos)

Agrupa N produtos em uma única venda com preço promocional.

GET    /api/v1/bundle-items/?bundle=<uuid>
POST   /api/v1/bundle-items/
PATCH  /api/v1/bundle-items/{id}/
DELETE /api/v1/bundle-items/{id}/

POST /api/v1/bundle-items/

{
  "bundle": "uuid-do-produto-tipo-bundle",
  "item": "uuid-do-produto-incluido",
  "quantity": 1,
  "position": 0
}

Restrições:

  • O produto pai precisa ter product_type='bundle'.
  • Bundle aninhado (bundle dentro de bundle) é rejeitado.
  • Item precisa pertencer à mesma organização.

Comportamento no checkout: o pagador paga bundle.price e o backend cria 1 Order parent (com a receita) + N child Orders com unit_price=0 (marcadas em metadata.bundle_parent_id). Cada child Order tem o produto incluído + quantidade — use isso pra provisionar a entrega item por item via webhook.

O webhook canônico inclui payment.order.bundle_items[] com {order_id, product_id, product_name, quantity} para todos os itens incluídos.

Cupons

Códigos de desconto aplicáveis no checkout público.

GET    /api/v1/coupons/
POST   /api/v1/coupons/
GET    /api/v1/coupons/{id}/
PATCH  /api/v1/coupons/{id}/
DELETE /api/v1/coupons/{id}/

POST /api/v1/coupons/

{
  "code": "BLACKFRIDAY",
  "discount_type": "percent",     // "percent" | "fixed"
  "discount_value": "20.00",       // 20% ou R$ 20,00 conforme o tipo
  "valid_until": "2026-12-31T23:59:59Z",
  "usage_limit": 100,              // 0 = ilimitado
  "min_amount": "50.00",
  "is_active": true,
  "plans": [],                     // lista de UUIDs (vazio = todos os planos)
  "products": []                   // lista de UUIDs (vazio = todos os produtos)
}

plans e products juntos definem os alvos: com os dois vazios, o cupom vale para qualquer plano ou produto da org; com algum preenchido, só para os itens listados. UUID de plano/produto de outra org → 400.

Cupom em plano — só a 1ª cobrança. Em planos, o desconto vale só na 1ª cobrança da assinatura; as seguintes saem pelo valor normal (preço do plano, ou o amount_override no plano de valor livre). Regras:

  • Aceito no checkout público (.../pay/) só com billing_type PIX_AUTOMATIC, desde que ele esteja liberado e ativo na sua conta. No cartão, 400 {"error": "Nesta assinatura, o cupom não vale no cartão."} — nada é criado e o cupom não é gasto. Sem Pix Automático na conta: 400 {"error": "Cupom não disponível para este plano."}. Os meios aceitos vêm em methods no coupon/validate/.
  • Só a cobrança da autorização sai com desconto; as seguintes já saem pelo valor normal, sem mudança agendada.
  • Não vale em plano com período de teste grátis (Cupom não disponível para este plano.) nem quando zeraria a 1ª cobrança (Este cupom zeraria a 1ª cobrança e não vale para assinatura.).
  • Conta um uso por assinatura, quando a assinatura é criada; falha na criação não gasta o cupom. Em plano com aprovação manual (requires_approval), o cupom fica na inscrição e o desconto é aplicado no POST /subscriptions/{id}/approve/.
  • usage_limit, valid_from/valid_until, is_active e min_amount (comparado ao valor de cada ciclo) valem como no produto.
  • POST /subscriptions/ (API) não aceita cupom: para um valor diferente, use amount_override.

Order Bumps

Ofertas adicionais exibidas no checkout do produto principal.

GET    /api/v1/order-bumps/?parent_product=<uuid>
POST   /api/v1/order-bumps/
PATCH  /api/v1/order-bumps/{id}/
DELETE /api/v1/order-bumps/{id}/

POST /api/v1/order-bumps/

{
  "parent_product": "uuid-produto-principal",
  "bump_product": "uuid-produto-adicional",
  "title": "Adicione o e-book complementar",
  "description": "Material extra para aprofundar",
  "discount_percent": "30.00"      // 30% off sobre o preço do bump_product
}

Quando o cliente aceita um ou mais bumps no checkout, é criado 1 Order parent (com o total cobrado) + N child Orders (metadata.via='order_bump', metadata.parent_order_id=<parent.id>). O webhook PAYMENT_RECEIVED inclui:

  • payment.order.bump — primeiro bump (compat).
  • payment.order.bumps[] — todos os bumps aceitos com {order_id, product_id, product_name, amount}.

Checkout público (sem auth)

Endpoints sem API key — alimentam a página https://vitrin.digital/c/<org-slug>/<item-slug>.

GET  /api/v1/public/checkout/<org_slug>/<item_slug>/
POST /api/v1/public/checkout/<org_slug>/<item_slug>/quote/
POST /api/v1/public/checkout/<org_slug>/<item_slug>/pay/
POST /api/v1/public/checkout/<org_slug>/coupon/validate/
GET  /api/v1/public/checkout/payments/<tx_id>/status/

GET /<item_slug>/ — retorna produto/plano + branding da org + bumps disponíveis (order_bumps[]). Slug pode apontar tanto pra Plan quanto pra Product (resolução 1:1; em conflito Product ganha).

Em plano, enabled_payment_methods traz só os meios que assinam aquele plano: PIX_AUTOMATIC (quando liberado), CREDIT_CARD e BOLETO (boleto mensal), os que estiverem ativos — Pix comum não assina. Em conta com trial_somente_carne: true, plano com trial_days > 0 vem com a lista vazia, porque com teste a assinatura só pode ser feita no carnê, que não passa pelo checkout. Lista vazia vem com unavailable_message (ex.: "Este plano tem período de teste e não está disponível para assinatura por aqui. Fale com a loja."); mostre-a no lugar do formulário. Com meios, unavailable_message vem "". Em conta com trial_somente_carne: false, o teste não esvazia a lista.

POST .../quote/ — preço por meio de pagamento, para montar o seletor de forma de pagamento antes do pagador escolher (ex.: "Pix R$ 100,00 · Cartão R$ 104,49"). Sem dados do pagador — não cria Customer nem cobra:

{
  "custom_amount": "75.00",
  "coupon_code": "PROMO10",
  "bump_ids": ["uuid-bump-1"]
}

Resposta:

{
  "base_amount": "297.00",
  "methods": {
    "PIX": { "total": "297.00" },
    "BOLETO": { "total": "297.00" },
    "CREDIT_CARD": {
      "total": "297.00",
      "installments": [
        { "n": 1, "installment_value": "297.00", "total": "297.00" },
        { "n": 2, "installment_value": "153.00", "total": "306.00" }
      ]
    }
  }
}

Aceita os mesmos parâmetros de preço que .../pay/ (amount_override/custom_amount, coupon_code, bump_ids) e usa a mesma resolução de preço/cupom/bumps — os totais aqui são exatamente o que .../pay/ cobraria para os mesmos dados, já com juros de parcelamento e, quando ligado, o repasse da taxa (§3.8). Só devolve os métodos habilitados para a org (enabled_payment_methods); em plano, os mesmos do GET acima.

POST .../pay/ — cria customer guest e cobra:

{
  "customer_name": "Maria Silva",
  "customer_cpf_cnpj": "12345678900",
  "customer_email": "maria@exemplo.com",
  "customer_phone": "11999998888",
  "customer_birth_date": "1990-03-15",
  "customer_company_name": "Empresa LTDA",
  "customer_postal_code": "01310100",
  "customer_address": "Av Paulista",
  "customer_address_number": "1000",
  "customer_complement": "Apto 12",
  "customer_province": "Bela Vista",
  "customer_city": "São Paulo",
  "customer_state": "SP",
  "custom_data": { "como_conheceu": "Instagram" },
  "billing_type": "CREDIT_CARD",
  "installments": 3,
  "credit_card_token": "tok_...",
  "custom_amount": "75.00",
  "coupon_code": "PROMO10",
  "bump_ids": ["uuid-bump-1", "uuid-bump-2"]
}
  • Sistema (sempre obrigatórios): customer_name, customer_cpf_cnpj.
  • Toggleáveis: os demais customer_* são exigidos conforme field_config da org (retornado em branding.field_config no GET acima). Se um campo enabled=true, required=true vem vazio, a resposta é 400 {error, missing: [...]}.
  • custom_data aceita as chaves definidas em branding.field_config.custom_fields — campos required vazios também caem na mesma validação.
  • custom_amount obrigatório quando product.price_mode=customer_defined.
  • coupon_code valida e aplica desconto se válido. Em plano, o desconto vale só na 1ª cobrança, e só no PIX_AUTOMATIC — não no cartão nem no boleto (ver Cupons).
  • Em plano, billing_type PIX responde 400 com "Assinatura no Pix comum não está disponível: ...", e em plano com trial, em conta com trial_somente_carne: true, qualquer meio responde 400 {"error": "Este plano tem período de teste e não está disponível para assinatura por aqui. Fale com a loja."} — nada é criado, nem a inscrição com aprovação manual.
  • Em plano no BOLETO, a assinatura nasce e o 1º boleto é enviado ao e-mail do pagador (customer_email); a resposta não traz o boleto. A página hospedada mostra "Assinatura criada", avisando que o boleto de cada cobrança vai para o e-mail.
  • bump_ids[] cria N child Orders adicionais vinculados via metadata.parent_order_id (até 10 por checkout). bump_id singular continua aceito por compat.
  • Cartão: envie credit_card_token (tokenizado por você) ou credit_card (number, holder, expiry_month, expiry_year, cvv) — a página hospedada manda o cartão digitado, e a Vitrin o tokeniza no processamento antes de cobrar; o número nunca é gravado. Se o cartão não puder ser registrado, a resposta é 422 com customer_message e nenhuma transação é criada. save_card: true + card_consent.accepted_at salva o cartão para one-click, só depois de o pagamento ser aprovado.
  • Plano no cartão com charge_url preenchido: a assinatura nasceu sem o cartão, e charge_url é o link em que o cliente o cadastra: mostre-o ao comprador. A página hospedada mostra "Falta cadastrar o cartão" com o botão para esse link.
  • Limite de tentativas com cartão: contam só recusas do cartão (cartão não registrado, recusa do emissor), não erros de configuração. Depois de várias recusas seguidas (do mesmo pagador, do mesmo IP ou na loja), o cartão responde 429 com customer_message e blocked_method: "CREDIT_CARD" por um tempo. Mostre o customer_message e ofereça Pix ou boleto, que seguem disponíveis.

Resposta inclui transaction_id, order_id/subscription_id, pix_qr_code/boleto_url quando aplicável, e thank_you_url para redirecionamento pós-pagamento. Em plano no cartão ou no boleto: kind: "plan", subscription_id, status, billing_type, charge_url (vazio quando o cartão já foi cobrado), trial_days e thank_you_url. No boleto, não redirecione ao thank_you_url antes do pagamento.

POST .../coupon/validate/ — útil para validação em tempo real no front:

{ "code": "PROMO10", "item_slug": "curso-pro", "amount": "297.00" }

Resposta: {valid, code, discount_type, discount_value, discount, final_amount, message}. Em plano, amount só é considerado no plano de valor livre (é o valor de cada ciclo) e a resposta traz também:

{
  "valid": true,
  "discount": "10.00",
  "final_amount": "90.00",          // 1ª cobrança
  "first_charge_only": true,
  "recurring_amount": "100.00",     // cobranças seguintes
  "methods": ["PIX_AUTOMATIC"]   // meios em que o cupom vale (vazio sem Pix Automático na conta)
}

No .../quote/ de um plano, coupon_code não muda methods (valor de cada ciclo) e acrescenta a 1ª cobrança com o desconto:

"first_charge": {
  "code": "BEMVINDO",
  "discount": "10.00",
  "methods": {
    "PIX_AUTOMATIC": { "total": "90.00" }
  }
}

Na página hospedada, o cliente vê junto do preço: "Cupom aplicado: R$ 10,00 de desconto na 1ª cobrança; as seguintes saem por R$ 100,00."

Configurações do Checkout (org)

Branding e campos do formulário usados pelas páginas públicas:

GET  /api/v1/checkout/settings/
PUT  /api/v1/checkout/settings/

{
  "logo_url": "https://...",
  "primary_color": "#00BCB5",
  "accent_color": "#390043",
  "show_org_name": true,
  "support_email": "ajuda@minha-org.com",
  "support_whatsapp": "+5511...",
  "default_thank_you_url": "https://...",
  "terms_text": "Ao comprar, você concorda com...",
  "field_config": {
    "fields": {
      "email":          {"enabled": true,  "required": true},
      "phone":          {"enabled": true,  "required": false},
      "birth_date":     {"enabled": false, "required": false},
      "address":        {"enabled": true,  "required": true},
      "address_number": {"enabled": true,  "required": true},
      "complement":     {"enabled": true,  "required": false},
      "postal_code":    {"enabled": true,  "required": true},
      "city":           {"enabled": true,  "required": true},
      "state":          {"enabled": true,  "required": true}
    },
    "custom_fields": [
      {
        "key": "como_conheceu",
        "label": "Como nos conheceu?",
        "type": "select",
        "required": true,
        "options": ["Instagram", "Indicação", "Google"]
      }
    ]
  }
}

Sobre field_config:

  • Campos sistema (name, cpf_cnpj) sempre aparecem e são obrigatórios — não toggleáveis.
  • Toggleáveis: email, phone, birth_date, address, address_number, complement, postal_code, city, state. Cada um tem enabled e required.
  • custom_fields[]: tipos suportados — text, email, tel, select, textarea. Para select, popule options[].
  • O backend sempre devolve field_config mesclado com defaults — campos não enviados em PUT continuam com o valor anterior; chaves desconhecidas são ignoradas.
  • O endpoint público GET /public/checkout/<org>/<slug>/ devolve branding.field_config só com a configuração da org (sem os mínimos dos métodos embutidos) — a UI precisa mesclar branding.method_requirements[<método escolhido>] por cima antes de renderizar/validar (mesmo contrato do GET /public/payment-links/<token>/, ver §3.9). Ver "Requisitos automáticos por método de pagamento" logo abaixo.

Requisitos automáticos por método de pagamento. Independente do field_config configurado, cada método tem campos mínimos obrigatórios — e a Vitrin força esses campos como enabled=true, required=true quando o método correspondente é o escolhido na cobrança:

Método (billing_type)Campos exigidos automaticamente
PIX / PIX_AUTOMATICemail
CREDIT_CARDemail, phone
BOLETOemail, postal_code, address, address_number, city, state

O endpoint público também devolve branding.method_requirements — um mapa {billing_type: [field_keys]} — para o frontend ajustar dinamicamente o que mostrar como obrigatório quando o usuário troca de método. A validação no POST .../pay/ aplica os requisitos do método selecionado (não a união de todos): pagador pode pagar via Pix sem informar endereço, e via Boleto será obrigado.

Métodos disponíveis (enabled_payment_methods). A organização escolhe quais métodos expor no checkout entre os que o admin Vitrin liberou (allowed_payment_methods). O endpoint GET /public/checkout/<org>/<slug>/ devolve apenas os métodos enabled_payment_methods. Tentar pagar com método não habilitado retorna 400.

Pedidos

Criar:

POST /api/v1/orders/create/

{
  "customer_id": "uuid-do-cliente",
  "product_id": "uuid-do-produto",
  "billing_type": "PIX",
  "quantity": 1
}

billing_type é opcional:

  • PIX/BOLETO: cobra na hora, como sempre — o pedido nasce pending já com a transação vinculada, e quando o webhook PAYMENT_RECEIVED chega, o pedido muda para paid.
  • CREDIT_CARD: exige credit_card_token (o dashboard não coleta cartão direto) — sem ele, 400.
  • Ausente: não cobra na hora. Gera um link de pagamento de uso único (o mesmo objeto de POST /payment-links/) e o pedido nasce pending sem transação. A resposta (201) traz payment_link {id, url, status, expires_at, ...} — mande esse url ao cliente. O cliente escolhe o meio (inclusive cartão) e as parcelas (respeitando o teto do produto e da conta) na página pública. Só quando ele paga o pedido ganha a transação e vira paid; uma tentativa que expira ou é recusada não muda o pedido. Aceita os mesmos campos opcionais do link: allowed_methods (subconjunto dos meios habilitados da conta, sem PIX_AUTOMATIC) e expires_at (padrão: 7 dias). Indisponível com chave de sandbox (vd_test_*): responde 400 {"detail": "Link de pagamento indisponível em sandbox."} e nada é criado — a página pública do link cobraria de verdade.
  • allowed_methods e expires_at só valem sem billing_type. Enviados junto com billing_type, são ignorados (sem erro).
  • Em qualquer caminho com cobrança imediata (PIX/BOLETO/CREDIT_CARD), installments (cartão) respeita o teto efetivo do produto — o menor entre o limite do produto e o da conta.

Listar:

GET /api/v1/orders/?status=paid&page=1

Status possíveis: pending, paid, fulfilled, refunded, canceled, expired. Filtros: status, product_id, customer_id.

O status do pedido não acompanha sozinho o vencimento do link: um pedido com link vencido continua pending (e payment_link.status vem expired) até você gerar um link novo ou cancelar o pedido — ver abaixo.

Detalhes:

GET /api/v1/orders/{id}/

Inclui o cliente, produto, transação vinculada (quando houver), payment_link (objeto ou null — presente quando o pedido foi criado sem billing_type) e timestamps de cada transição de status.

Marcar como entregue (fulfill):

POST /api/v1/orders/{id}/fulfill/

Use depois que você entregou o produto (mandou ingresso por e-mail, liberou acesso ao curso, despachou o item físico). Marca fulfilled_at e expõe ao cliente final via dashboard.

Gerar novo link de pagamento:

POST /api/v1/orders/{id}/payment-link/renew/

{ "expires_at": "2026-10-10T23:59:59-03:00" }   # opcional; padrão: 7 dias

Para pedido pending cujo link não está aberto (vencido ou cancelado). Cria um link novo com o mesmo cliente, valor, descrição, meios e teto de parcelas do anterior, e o pedido passa a apontar para ele. Resposta (200): o pedido, com o payment_link novo. Se o cliente ainda pagar um Pix/boleto emitido pelo link antigo, o pedido é quitado mesmo assim e o link novo é cancelado.

  • 409 order_not_pending — o pedido não está pending;
  • 409 order_without_payment_link — o pedido foi criado com billing_type (sem link);
  • 409 payment_link_open — o link atual ainda está aberto: reenvie esse mesmo link;
  • 400 — expires_at inválido, meios do link anterior que não estão mais habilitados na conta, ou chave de sandbox.

Cancelar pedido:

POST /api/v1/orders/{id}/cancel/

Só para pedido pending. Se o link do pedido estiver aberto, ele é cancelado junto e deixa de aceitar pagamento. Resposta (200): o pedido com status: "canceled". Formato dos erros:

{ "error": "order_not_pending", "detail": "Só é possível cancelar pedidos pendentes (status atual: paid).", "status": "paid" }
  • 409 order_not_pending — o pedido não está pending;
  • 409 order_has_transaction — pedido criado com billing_type PIX/BOLETO: a cobrança já existe e seguiria pagável. O corpo traz transaction_id; cancele a cobrança em POST /payments/{transaction_id}/cancel/ e o pedido é cancelado junto.

Um Pix ou boleto já emitido pelo link não é cancelado no processamento: se o cliente pagar mesmo assim, o pedido passa de canceled a paid.

Pedidos não têm PUT/PATCH/DELETE direto. Para modificar, cancele e crie um novo.


8. Reembolso e Cancelamento

Reembolso (requer PIN)

POST /api/v1/payments/{id}/refund/

{
  "pin": "123456",
  "amount": 50.00
}

Omita amount para reembolso total.

⚠️ 202 não é reembolso concluído

Este endpoint devolve dois status diferentes, e tratar os dois como sucesso é o erro mais caro dessa integração:

StatusSignificaO que mostrar ao usuário
200Reembolso concluído. O dinheiro voltou."Reembolso efetuado"
202Reembolso solicitado, ainda não confirmado."Reembolso em processamento"

No 202 o corpo já avisa:

{
  "status": "refund_pending",
  "detail": "Estorno solicitado; será confirmado em instantes."
}

Isso acontece no Pix: quando o banco do pagador não confirma a devolução na hora, a transação fica reservada e o desfecho vem depois. Ele pode ser qualquer um dos dois:

  • confirmou → a cobrança vira refunded e você recebe PAYMENT_REFUNDED;
  • não confirmou → a cobrança volta a valer e você recebe PAYMENT_REFUND_FAILED.

Ou seja, um 202 pode terminar sem nenhum dinheiro devolvido. Nunca escreva "reembolso processado" na tela a partir de um 202 — espere o webhook. Consulte Eventos suportados e trate PAYMENT_REFUND_FAILED desfazendo o que a tela tiver mostrado.

Se você não tratar esse evento, o cliente vê "reembolsado" numa cobrança que segue valendo, e só descobre quando o dinheiro não chega.

Cancelar cobrança pendente

POST /api/v1/payments/{id}/cancel/

Só funciona para cobranças com status pending ou confirmed.

Parcela de carnê não se cancela sozinha: a resposta é 409 {"error": "carne_installment", "detail": "Parcelas de carnê são canceladas pelo carnê: cancele a assinatura."}. Cancelar a assinatura cancela todas as parcelas em aberto (Assinatura em carnê). As cobranças (GET /payments/) trazem carne (id do carnê) e carne_installment (número da parcela), nulos fora do carnê.


9. Saldo e Transferências

Consultar saldo

GET /api/v1/balance/

Resposta:

{
  "available": 1500.00,
  "total": 2300.00,
  "pending": 800.00,
  "withdrawal_fees": { "pix": 3.49, "ted": 9.49 }
}
  • available: pronto para saque
  • pending: aguardando compensação (D+1 Pix, D+2 Boleto, D+30 Crédito)
  • total: available + pending

Cronograma de recebíveis

GET /api/v1/balance/scheduled/?days_ahead=90

Lista quando cada cobrança paga vai cair na conta. Útil pra projeção de fluxo de caixa.

Resposta:

{
  "horizon_days": 90,
  "total_scheduled": 12500.0,
  "count": 18,
  "by_day": [
    { "date": "2026-04-29", "total": 350.0, "count": 2 },
    { "date": "2026-04-30", "total": 199.9, "count": 1 }
  ],
  "items": [
    {
      "release_date": "2026-04-29",
      "amount": 99.9,
      "method": "PIX",
      "installment": null,
      "transaction_id": "tx_xxx",
      "anticipated": false
    },
    {
      "release_date": "2026-05-15",
      "amount": 100.0,
      "method": "CREDIT_CARD",
      "installment": "1/3",
      "transaction_id": "tx_yyy",
      "anticipated": false
    }
  ]
}

Regras de release: PIX = paid_at + 1d; Boleto = paid_at + 2d; Cartão Nx = uma linha por parcela em paid_at + 30·i. Cobrança antecipada vem com anticipated: true.

Limitação importante: /balance/scheduled/ é uma projeção baseada em paid_at + prazo do método. Ela não considera horários de processamento, retenções, falhas de repasse, fins de semana ou feriados. Para previsão fiel do que vai cair na sua conta bancária hoje (ou nos próximos dias úteis), use /treasury/calendar/, que devolve o estado oficial de cada repasse (Previsto, Depositado, Rejeitado).

Calendário de saques (fonte de verdade)

GET /api/v1/treasury/calendar/?start=YYYY-MM-DD&end=YYYY-MM-DD

Reflete o calendário oficial de repasses — esse é o endpoint que sua app deve consultar para mostrar "Caindo hoje", "Próximos saques", "Já depositado", "Recusado". Cada item corresponde a um repasse concreto que vai sair (ou já saiu) para a conta bancária cadastrada da sua loja, com os horários de processamento já aplicados.

Resposta:

{
  "range": { "start": "2026-05-18", "end": "2026-05-22" },
  "totals": {
    "previsto":   3890.29,
    "depositado": 3249.30,
    "disponivel":    0.00,
    "rejeitado":     0.00,
    "indefinido":    0.00
  },
  "count": 5,
  "items": [
    {
      "status": "Depositado",
      "amount": 3249.30,
      "release_date": "2026-05-20T00:00:00",
      "is_today": true,
      "is_transferred": false,
      "message": "Pagamento Efetivado."
    },
    {
      "status": "Previsto",
      "amount": 3890.29,
      "release_date": "2026-05-21T00:00:00",
      "is_today": false,
      "is_transferred": false,
      "message": ""
    }
  ]
}

Status possíveis em items[].status:

StatusSignificado
PrevistoRepasse agendado pra essa data (ainda não saiu).
DepositadoRepasse efetivado — o valor já caiu (ou está caindo) na sua conta bancária.
DisponívelSaldo acumulado disponível pra saque (orgs com auto-repasse desligado).
RejeitadoRepasse falhou — message traz o motivo (dados bancários ausentes, conta inativa, etc.).
IndefinidoValor provisório antes da consolidação do dia. Ignore no agregado.

Como usar nas suas telas:

  • "Caindo hoje" = items onde release_date == today AND status == 'Depositado' → some amount. Esse é o número que vai bater exato com o extrato bancário da sua loja no dia.
  • "Próximos saques" = items onde status == 'Previsto', agrupado por release_date.
  • "Saldo disponível pra saque" = totals.disponivel (só pra orgs com auto-repasse pausado).
  • "Repasses falhos" = items onde status == 'Rejeitado' — mostre o message ao operador da loja.

Não some Transaction.net_amount localmente para estimar saque do dia. Esse cálculo ignora:

  • Horários de processamento (cobrança paga no fim do dia pode entrar no repasse do dia seguinte).
  • Reservas e retenções de saldo.
  • Fins de semana e feriados (repasse roda em dia útil).
  • Recusas de TED/Pix (conta destino inativa, divergência de titular, etc.).

A soma local pode divergir do valor real. Use /treasury/calendar/ sempre que precisar do número que vai bater com o extrato bancário.

Descritor do Pix de repasse (limitação de white-label)

Quando o repasse cai na conta bancária da sua loja, o descritor da transação no extrato bancário do destinatário carrega o nome legal da instituição de pagamento que executou o Pix — não o nome da Vitrin nem o da sua loja. Essa é uma exigência regulatória do SPI/BACEN: o ordenador da transferência precisa identificar quem está mandando o dinheiro.

Pra você como operador da loja, isso aparece no seu próprio extrato (R$ X recebido de "<nome da instituição de pagamento>") e não há configuração para mudar — vale tanto pra Vitrin quanto pra qualquer outro intermediador no Brasil.

Esse vazamento de marca é restrito ao extrato bancário do repasse. Em toda outra superfície (checkout, e-mails transacionais, webhooks, dashboard) a Vitrin se mantém white-label e a sua loja aparece como a marca-fim.

Transferir via Pix (requer PIN)

POST /api/v1/transfers/pix/

{
  "value": 500.00,
  "pix_key": "12345678909",
  "pix_key_type": "CPF",
  "pin": "123456"
}

Tipos de chave: CPF, CNPJ, EMAIL, PHONE, EVP (aleatória).

Taxa de saque retida do valor: pediu R$500, R$500 são debitados da sua conta de pagamentos, R$500 − fee vai pro destino. Resposta carrega requested, withdrawal_fee e net_to_destination pra contabilização. Saques ≤ taxa retornam HTTP 400.

Transferir via dados bancários (requer PIN)

POST /api/v1/transfers/bank/

{
  "value": 500.00,
  "bank_code": "237",
  "agency": "0001",
  "account": "12345",
  "account_digit": "6",
  "account_type": "CONTA_CORRENTE",
  "owner_name": "Maria Silva",
  "cpf_cnpj": "12345678909",
  "pin": "123456"
}

Histórico de transferências

GET /api/v1/transfers/

Combina as transferências solicitadas pela sua org com o status real de cada uma (falhas, datas de efetivação).


9.5. Programa de Afiliados

A Vitrin oferece um programa de afiliação: terceiros se cadastram como afiliados na plataforma, sua loja libera produtos e planos para afiliação com uma comissão definida, eles compartilham um link rastreado e cada venda atribuída paga uma comissão automática no afiliado. Toda a infraestrutura de atribuição, ledger, repasse e estorno é da Vitrin — sua API só precisa ligar o programa, configurar comissões e (opcional) receber os blocos affiliate / coproducers no webhook.

9.5.1. Como funciona, em uma frase

Quando o cliente chega no seu checkout via ?aff=<slug-do-afiliado>, a Vitrin grava um cookie de 1ª parte com validade attribution_window_days (padrão 30 dias, configurável de 7 a 90). Se o pagamento acontecer dentro dessa janela, a comissão configurada no produto/plano (ou o default do programa) é debitada do líquido da sua venda e creditada na carteira do afiliado. Sua taxa Vitrin continua a mesma — afiliado não sai da fatia da plataforma.

9.5.2. Ligar o programa

PATCH /api/v1/organization/affiliate-program/

{
  "enabled": true,
  "attribution_window_days": 30,
  "requires_approval": true,
  "default_commission_pct": "20.00",
  "default_commission_recurring_pct": "10.00",
  "public_marketplace": false
}
  • requires_approval=true (recomendado): cada afiliado que pede para promover seus produtos fica em pending até você aprovar. false libera afiliação automática — use só em programas abertos.
  • default_commission_pct é a comissão default aplicada quando o produto/plano não tem comissão própria.
  • default_commission_recurring_pct é a comissão default em renovações (assinatura/Pix Automático). Em branco usa a mesma da primeira venda.
  • public_marketplace=true lista seus produtos no marketplace público da Vitrin (qualquer afiliado descobre). false mantém privado — só quem você convidar e for aprovado consegue ver e promover.

Para ler o estado atual:

GET /api/v1/organization/affiliate-program/

9.5.3. Comissão por produto / plano

Cada produto e plano pode sobrescrever o default do programa.

PUT /api/v1/products/{product_id}/affiliate-config/

{
  "enabled": true,
  "commission_pct": "30.00",
  "auto_approve": false,
  "max_affiliates": null
}
PUT /api/v1/plans/{plan_id}/affiliate-config/

{
  "enabled": true,
  "commission_pct": "30.00",
  "commission_recurring_pct": "15.00",
  "affiliate_recurring_months": null,
  "auto_approve": false,
  "max_affiliates": null
}
  • enabled=false retira o item do marketplace e bloqueia novos pedidos. Comissões já liquidadas não são afetadas.
  • auto_approve=true faz com que pedidos pra este item específico virem aprovados sem moderação (útil pra produtos de baixo risco mesmo com requires_approval=true no programa).
  • max_affiliates: limite opcional de quantos afiliados podem estar simultaneamente aprovados no item.
  • Para planos (recorrência):
    • commission_recurring_pct: comissão paga nas renovações. Em branco usa commission_pct.
    • affiliate_recurring_months: cap em N renovações. Em branco = vitalícia enquanto a assinatura existir.

Importante — A comissão é calculada sobre o preço do produto, não sobre o valor cobrado do cliente. Em cartão parcelado, os juros e o adicional de parcelamento da loja não entram na base da comissão.

9.5.4. Moderar pedidos de afiliação

Quando um afiliado solicita seu produto via marketplace, ele aparece em:

GET /api/v1/organization/affiliate-applications/?status=pending

Resposta:

[
  {
    "id": "<uuid-app>",
    "affiliate_slug": "joao-silva",
    "product_slug": "ebook-marketing",
    "plan_slug": null,
    "target_name": "E-book Marketing",
    "status": "pending",
    "created_at": "2026-05-19T22:34:56Z"
  }
]

Para aprovar, rejeitar ou revogar:

POST /api/v1/affiliate-applications/{app_id}/moderate/

{
  "action": "approve",
  "commission_pct_override": "35.00",
  "commission_recurring_pct_override": "18.00"
}
  • action: approve, reject ou revoke.
  • commission_pct_override / commission_recurring_pct_override: opcional — define comissão personalizada só pra este afiliado nesse produto/plano. Use pra parceiros estratégicos. Em branco, herda do produto/plano/programa.
  • rejection_reason: usado em action=reject, mostrado ao afiliado.

9.5.5. Tracking no checkout

Se você usa o checkout público da Vitrin (/c/{org}/{item}), não precisa fazer nada — a captura de ?aff= e o cookie de atribuição já são tratados pelo frontend hospedado.

Se você roda checkout próprio chamando /api/v1/checkout/ diretamente:

  1. Capture ?aff= na URL de chegada do cliente.
  2. Guarde em cookie/localStorage de 1ª parte (TTL ≤ attribution_window_days).
  3. (Opcional, mas recomendado) chame:
    POST /api/v1/public/affiliate-track/
    
    {
      "affiliate_slug": "joao-silva",
      "org_slug": "minha-loja",
      "product_slug": "ebook-marketing"
    }
    
    pra registrar o clique antes da conversão (alimenta o histórico do afiliado).
  4. No POST do checkout, envie no body:
    {
      "affiliate_slug": "joao-silva",
      "affiliate_attribution_ts": 1716234567890
    }
    
    affiliate_attribution_ts é o epoch em milissegundos do clique original — usado pra rechecar a janela no servidor. A Vitrin valida o slug, a janela e a permissão do afiliado no item; em qualquer falha, a venda segue sem atribuição (falha-soft, nunca quebra o checkout).

9.5.6. Relatório de comissões

GET /api/v1/organization/affiliate-report/

Resposta:

{
  "totals": {
    "settled": "1234.56",
    "pending": "78.90",
    "reversed": "12.00"
  },
  "recent": [
    {
      "id": "<uuid-comissao>",
      "org_slug": "minha-loja",
      "product_slug": "ebook-marketing",
      "is_recurring": false,
      "sale_amount": "100.00",
      "commission_pct": "30.00",
      "commission_amount": "30.00",
      "status": "settled",
      "settled_at": "2026-05-19T22:34:56Z",
      "created_at": "2026-05-19T22:30:00Z"
    }
  ]
}

totals agrega todas as comissões da loja por status. recent traz as 200 últimas. Use pra reconciliação e relatórios próprios — o webhook (§5) já entrega o detalhe transação a transação.

9.5.7. Pontos importantes

  • Comissão de afiliado é debitada do líquido da sua loja, não da taxa Vitrin.
  • Estorno proporcional: quando uma cobrança com comissão é estornada (refund ou chargeback), a comissão é revertida proporcionalmente. Se o afiliado já tinha sacado, o saldo dele fica negativo e a Vitrin compensa no próximo repasse — você não precisa cobrar do afiliado.
  • Renovações de assinatura/Pix Automático propagam o afiliado automaticamente. O cap (affiliate_recurring_months) limita por quantas renovações o afiliado continua ganhando.
  • Coprodutores (repasse fixo, sem janela de atribuição) são entidade separada de afiliado. A configuração é feita por produto no painel.
  • White-label: o afiliado nunca vê dados da sua loja além do que você publica (nome, descrição, preço, comissão). Pra ele, a Vitrin é a plataforma. Pra você, ele aparece só como slug no webhook.

10. Antecipação de Recebíveis

Antecipação te permite receber agora recebíveis de cartão que cairiam no futuro. A seleção é por data de recebimento: você simula, escolhe os dias e confirma com PIN.

Pré-requisito: a org precisa ter allow_anticipation=true (ativado pelo admin). Com a feature desabilitada, as rotas respondem 403 (o GET responde 200 com {"allow_anticipation": false}).

Resumo e antecipação em processamento

GET /api/v1/anticipations/
{
  "allow_anticipation": true,
  "available_gross": 250.00,
  "available_net": 240.00,
  "max_percent": 25.0,
  "total_receivables_gross": 1000.00,
  "total_receivables_net": 960.00,
  "display_rate": 2.99,
  "is_exempt": false,
  "expected_payment_date": "2026-10-01",
  "history": [{ "date": "2026-09-24", "org_fee": 7.47, "release_dates": ["2026-10-24"] }],
  "in_progress": {
    "id": "uuid", "expected_payment_date": "2026-10-01", "gross": 250.00, "release_dates": ["2026-10-24"],
    "complement_release_dates": [], "complement_until": null
  }
}
  • available_gross/available_net — disponível para antecipação, já com o limite da sua conta aplicado e descontado o que você já antecipou e ainda não venceu. Use esses campos para saber quanto pode pedir; não calcule o limite por conta própria. max_percent é o limite da sua conta, o mesmo que a tela Antecipação mostra.
  • in_progress — antecipação ainda não creditada, ou null. Enquanto existir, uma nova solicitação responde 409 (abaixo). expected_payment_date é a data até a qual ela bloqueia.
  • complement_release_dates/complement_until — preenchidos só quando a antecipação em processamento foi efetivada parcialmente (veja "Efetivação parcial" abaixo).

Simular

POST /api/v1/anticipations/simulate/

{ "start_date": "2026-10-01", "end_date": "2026-12-31" }   // ambos opcionais (padrão: hoje → +365 dias)

Devolve session_guid (use na solicitação), expected_payment_date, a quebra por dia em days (release_date, gross, discount, net, count) e os mesmos available_gross, available_net, max_percent e in_progress do resumo. O filtro de datas muda só a tabela days e o total do período; o disponível vale para todos os recebíveis.

Solicitar (requer PIN)

POST /api/v1/anticipations/request/

{
  "session_guid": "da-simulacao",
  "release_dates": ["2026-10-24", "2026-11-24"],
  "pin": "123456"
}

Resposta 200: anticipated, org_fee, net_received, expected_payment_date, is_exempt, partial e remaining_release_dates.

Horário. A solicitação só é aceita em horário comercial, em dias úteis. Fora dele, a resposta é 400 com o motivo em error, sem nada reservado: mostre a mensagem e tente de novo no próximo horário aceito. A simulação funciona a qualquer hora.

Efetivação parcial. Alguns lançamentos podem ficar indisponíveis no momento da efetivação: a resposta vem com partial: true e os dias que ficaram de fora em remaining_release_dates. Você pode fazer um complemento com esses dias (e só eles) até o horário de in_progress.complement_until: simule de novo e solicite com eles. O complemento passa pelo mesmo limite de antecipação.

Uma solicitação por vez. Depois de uma antecipação aceita, uma nova só é aceita após o crédito da anterior. Selecione todos os dias desejados numa única solicitação. Se a antecipação anterior não for aprovada, uma nova solicitação é liberada automaticamente.

StatusQuandoCorpo
409 anticipation_in_progressHá antecipação em processamento (crédito previsto para hoje ou depois){"error": "anticipation_in_progress", "detail": "Há uma antecipação em processamento. Uma nova solicitação fica disponível após o crédito, em 01/10.", "available_after": "2026-10-01"}
400Soma dos dias selecionados acima do disponível{"error": "O total selecionado (R$ …) ultrapassa o disponível para antecipação (R$ …). Selecione menos dias.", "available_gross": 250.0, "selected_gross": 500.0}
400Disponível zerado (limite já usado){"error": "Não há valor disponível para antecipação no momento.", "available_gross": 0.0}
400Fora do horário aceito{"error": "…"} — mostre a mensagem e tente mais tarde
400Nenhum dia selecionado está disponível / simulação já usada{"error": "…"} — simule de novo
502A antecipação foi recusada no processamento{"error": "…", "description": "…", "retryable": true} — nada foi antecipado; pode tentar de novo
502Não foi possível confirmar o resultado (instabilidade no meio da solicitação){"error": "Não foi possível confirmar a antecipação agora. Verifique em alguns minutos antes de tentar de novo.", "retryable": false} — a antecipação pode ter acontecido: consulte GET /anticipations/ antes de tentar de novo; enquanto não houver confirmação, ela conta como em processamento

Leia available_after em vez de interpretar o texto. Pedidos simultâneos são serializados: se dois chegarem juntos, só um é efetivado e o outro recebe 409.


11. Defesa de Chargeback

Disputa é sempre manual. O lojista sustenta a defesa diretamente com a operadora do cartão. Não há endpoint de submissão automática. A Vitrin é o organizador: você anexa evidências aqui, baixa o dossiê pré-montado e encaminha pra operadora pelo canal manual.

Listar disputas em aberto

GET /api/v1/chargebacks/

Retorna transações com status chargeback ou disputed, com evidence_count por transação.

Anexar evidências

POST /api/v1/payments/{id}/evidences/
Content-Type: multipart/form-data

  file: <arquivo>            # PDF, imagem, etc — máx 25 MB
  evidence_type: receipt     # taxonomia abaixo
  description: ""            # opcional

Tipos válidos: delivery_proof, communication, contract, receipt, refund_attempt, ip_log, other.

GET    /api/v1/payments/{id}/evidences/             # listar
DELETE /api/v1/payments/{id}/evidences/{ev_id}/     # remover (bloqueia se já submetida)

Storage é privado (URLs presigned com expiração de 1h). Pode coletar evidências preventivamente em transações ainda não disputadas (status received/confirmed) — útil pra ter o material pronto antes de qualquer disputa surgir.

Dossiê de defesa pré-montado

A Vitrin compila automaticamente o dossiê com tudo que ela tem da transação:

GET /api/v1/payments/{id}/chargeback-defense/

Resposta:

{
  "transaction": {
    "id": "uuid",
    "amount": 99.90,
    "billing_type": "CREDIT_CARD",
    "installments": 1,
    "paid_at": "2026-04-09T15:30:00Z",
    "payer_ip": "189.6.x.y"
  },
  "customer": {
    "name": "Maria Silva",
    "cpf_cnpj": "12345678909",
    "email": "maria@email.com",
    "phone": "11988776655",
    "address": "Rua das Flores, 42 - Centro",
    "city_state": "São Paulo/SP"
  },
  "order": {
    "product_name": "Curso Premium",
    "fulfilled_at": "2026-04-09T16:00:00Z"
  },
  "subscription": null,
  "previous_transactions": [
    { "id": "uuid", "amount": 49.90, "paid_at": "2026-03-15T..." }
  ],
  "webhook_events": [
    { "event_type": "PAYMENT_CONFIRMED", "created_at": "..." },
    { "event_type": "PAYMENT_RECEIVED", "created_at": "..." }
  ],
  "audit_trail": [
    { "action": "transaction.created", "timestamp": "...", "ip_address": "..." }
  ]
}

Use esses dados pra montar a contestação junto à bandeira (Visa/Mastercard) ou pelo nosso suporte. O ideal é guardar tudo: comprovante de entrega, IP do pagador, transações anteriores do mesmo cliente bem-sucedidas, fulfillment do pedido, eventos de webhook que comprovam a confirmação do pagamento.

Boas práticas para reduzir chargebacks: cobre só após confirmar identidade, marque pedidos como fulfilled assim que entregar, mantenha cadastro de cliente completo (endereço!), responda chargebacks rapidamente.


12. Configurações da Org

Self-service de tudo que você pode customizar pelo dashboard /dashboard/settings/.

Ler configurações atuais

GET /api/v1/settings/

Resposta:

{
  "enabled_payment_methods": ["PIX", "CREDIT_CARD", "BOLETO"],
  "org_max_installments": 12,
  "org_min_installment_value": "10.00",
  "installment_markup_pct_2_6": "0.00",
  "installment_markup_pct_7_12": "0.00",
  "allow_installment_markup": false,
  "installment_markup_max_pct_2_6": "0.00",
  "installment_markup_max_pct_7_12": "0.00",
  "auto_anticipation_enabled": false,
  "checkout_logo": "https://...",
  "checkout_primary_color": "#00BCB5",
  "webhook_url": "https://suaapp.com/webhooks/vitrin",
  "antifraud_enabled": false,
  "payout_frequency": "monthly",

  "allowed_payment_methods": ["PIX", "CREDIT_CARD", "BOLETO"],
  "max_installments": 12,
  "min_installment_value": "10.00",
  "allow_anticipation": true,
  "allow_auto_anticipation": false,

  "trial_somente_carne": false,

  "fee_percentage_pix": "5.49",
  "fee_fixed_pix": "3.49",
  "fee_percentage_credit": "5.49",
  "anticipation_rate_credit_vista": "2.99",
  "withdrawal_fee_fixed_pix": "3.49"
}

Os campos dividem-se em três blocos:

BlocoEditável pela org?
Preferências da org (enabled_payment_methods, org_max_installments, org_min_installment_value, installment_markup_pct_2_6, installment_markup_pct_7_12, auto_anticipation_enabled, antifraud_enabled, auto_receipt_enabled, checkout_logo, checkout_primary_color)✅ via PATCH
Limites da plataforma (allowed_payment_methods, max_installments, allow_anticipation)❌ definido pelo admin no onboarding
Frequência de repasse (payout_frequency, payout_monthly_day, payout_weekly_day)❌ só leitura: muda por renegociação das condições comerciais
Tabela de taxas (fee_*, anticipation_*, withdrawal_*)❌ congelada no Anexo Comercial assinado
Capacidade de teste (trial_somente_carne)❌ só leitura: depende da conta. Quando true, um plano com trial_days só cria assinatura em CARNE (§ Assinaturas).

Atualizar (parcial)

PATCH /api/v1/settings/

{
  "enabled_payment_methods": ["PIX", "CREDIT_CARD"],
  "checkout_primary_color": "#FD8000",
  "auto_anticipation_enabled": true
}

A enabled_payment_methods precisa ser um subconjunto de allowed_payment_methods. Tentar habilitar um método não-permitido retorna HTTP 400.

  • org_max_installments: teto de parcelas da conta, até max_installments (capado em 12x). POST /charges/, POST /checkout/pay/ e POST /checkout/guest/ recusam installments acima desse teto com 400 {"installments": ["Número de parcelas acima do permitido para esta conta (máximo: Nx)."]} — o mesmo teto de GET /checkout/installments/ (§3.4) e do parcelamento em produto/link de pagamento (§7).
  • installment_markup_pct_2_6 / installment_markup_pct_7_12: adicional de parcelamento ("Adicional de parcelamento" na tela), em % sobre o preço de venda, por faixa de parcelas. Só com o adicional liberado para a conta (allow_installment_markup, somente leitura) e até o limite da faixa (installment_markup_max_pct_2_6 / installment_markup_max_pct_7_12, somente leitura); fora disso, 400. É um acréscimo cobrado do cliente no parcelado que fica para a sua loja. Já vem somado no installment_value/total do checkout e no valor cobrado, e não entra na base da comissão de afiliados e coprodutores. Vale para cobranças novas.
  • auto_receipt_enabled (bool, padrão true): "Recibo automático" na tela. Ligado, o cliente com e-mail recebe o comprovante quando o pagamento é confirmado, com o botão do link de entrega (thank_you_url) do produto ou plano.

Configurar webhook (já documentado em §5)

PUT /api/v1/webhooks/config/ — requer PIN.

Conta bancária para repasse

GET /api/v1/settings/bank-account/
PUT /api/v1/settings/bank-account/        (requer PIN)

{
  "bank_code": "237",
  "bank_agency": "1234",
  "bank_agency_digit": "0",
  "bank_account": "987654",
  "bank_account_digit": "5",
  "bank_account_type": "CHECKING",
  "pin": "123456"
}

bank_account_type: CHECKING (corrente) ou SAVINGS (poupança). O titular precisa estar no mesmo CPF/CNPJ da org — o processamento rejeita repasse pra conta de terceiros.

Checklist pós-aprovação

GET /api/v1/onboarding/checklist/

Retorna uma lista de 4 passos (conta bancária, webhook, primeiro plano/produto, primeira cobrança) com status completed: true|false cada. Usado pelo dashboard pra renderizar o widget "ative sua conta" — útil também pra mostrar progresso ao usuário no seu próprio painel.

{
  "is_approved": true,
  "completed_count": 2,
  "total_count": 4,
  "all_completed": false,
  "steps": [
    { "key": "bank_account", "completed": true, ... },
    { "key": "webhook", "completed": false, "cta_label": "Configurar", "cta_href": "/dashboard/webhooks" },
    { "key": "product_or_plan", "completed": true, ... },
    { "key": "first_charge", "completed": false, "cta_label": "Ver integração", "cta_href": "/docs/integracao" }
  ]
}

Export LGPD dos dados da org

A org pode pedir um snapshot completo dos próprios dados (clientes, transações, assinaturas, planos, produtos) atendendo ao direito de portabilidade da LGPD.

POST /api/v1/export/

{ "format": "json" }     # ou "csv" (zip com 5 tabelas)

Cria um export pending, agenda processamento async, e retorna o registro. O processamento leva alguns segundos. Quando completa, a org recebe um email com link de download (URL presigned, 1h de validade).

GET  /api/v1/export/                       # histórico + status atuais
GET  /api/v1/export/{id}/download/         # gera URL presigned fresca

Limites: 1 export pendente/processando por org (HTTP 429 se já tem um em andamento), retenção de 90 dias após conclusão.


13. Relatórios

Relatórios agregados sobre a operação da sua org. Todos retornam dados restritos à org autenticada.

Overview do dashboard

GET /api/v1/reports/overview/?days=30

Resposta:

{
  "total_revenue": 28900.50,
  "total_transactions": 142,
  "active_subscriptions": 23,
  "active_customers": 89,
  "anticipated_value": 8500.00,
  "by_method": {
    "PIX": 18200.00,
    "CREDIT_CARD": 9100.50,
    "BOLETO": 1600.00
  }
}

Receita por dia

GET /api/v1/reports/revenue/?days=30

Retorna uma série temporal com receita bruta e líquida por dia, pronta para gráficos.

Receita mensal

GET /api/v1/reports/revenue/monthly/?months=12

Transações com filtros

GET /api/v1/reports/transactions/?status=received&billing_type=PIX&from=2026-04-01&to=2026-04-30

Filtros: status, billing_type, customer_id, from, to, min_amount, max_amount.

Churn de assinaturas

GET /api/v1/reports/churn/

Retorna taxa de cancelamento por mês, MRR perdido e principais motivos.

Relatório de antecipações

GET /api/v1/reports/anticipation/?from=2026-04-01&to=2026-04-30

Resumo de antecipações solicitadas no período: total antecipado, taxa total paga, economia média de tempo.


14. Referência Rápida de Endpoints

Checkout

MétodoEndpointDescrição
POST/charges/Cobrança avulsa em Pix, boleto ou cartão (aceita Idempotency-Key)
POST/charges/charge-saved/Cobrar com o cartão salvo do cliente (§3.7)
POST/checkout/pix/Cobrar via Pix
POST/checkout/boleto/Cobrar via Boleto
POST/checkout/tokenize/Tokenizar cartão
POST/checkout/pay/Cobrar com token
POST/checkout/intents/Criar intenção de pagamento (chave secreta)
GET/checkout/intents/{id}/Consultar a intent (chave secreta)
POST/checkout/intents/{id}/confirm/Confirmar com cartão (chave publicável + client_secret)
GET/checkout/intents/{id}/status/Status para o navegador (chave publicável + X-Intent-Secret)
GET/checkout/config/Política 3DS da org
POST/checkout/guest/Checkout sem cadastro prévio
GET/checkout/installments/?amount=XConsultar parcelamento
MétodoEndpointDescrição
POST/payment-links/Criar link de pagamento avulso
GET/payment-links/?status=Listar (paginado; open|paid|canceled|expired)
GET/payment-links/{id}/Detalhe
POST/payment-links/{id}/cancel/Cancelar (só open; 409 fora disso)
POST/payment-links/{id}/send-email/Enviar por e-mail ao cliente do link (só open com cliente+e-mail; 429 no 4º envio/hora do link ou no 31º/hora da conta)
GET/public/payment-links/{token}/Info pública + quote (sem auth)
POST/public/payment-links/{token}/pay/Pagar (sem auth)

Pix Automático

MétodoEndpointDescrição
GET/pix-automatic/authorizations/Listar autorizações
POST/pix-automatic/authorizations/Criar (gera QR Code de aceite)
GET/pix-automatic/authorizations/{id}/Detalhes
PATCH/pix-automatic/authorizations/{id}/Alterar (value/min_limit_value/finish_date/description)
DELETE/pix-automatic/authorizations/{id}/Cancelar (PIN)
POST/pix-automatic/authorizations/{id}/charges/Criar cobrança recorrente (2-10 dias úteis)
GET/pix-automatic/authorizations/{id}/instructions/Listar instruções da autorização
POST/pix-automatic/authorizations/{id}/sync/Forçar sync com o processamento
GET/pix-automatic/instructions/Todas as instruções da org
GET/pix-automatic/instructions/{id}/Detalhe da instrução
POST/pix-automatic/instructions/{id}/sync/Forçar sync da instrução
DELETE/pix-automatic/instructions/{id}/Cancelar agendamento (PIN)

Transações

MétodoEndpointDescrição
GET/transactions/Listar transações
GET/payments/{id}/status/Status (polling Pix)
POST/payments/{id}/refund/Reembolsar (PIN)
POST/payments/{id}/cancel/Cancelar pendente
GET/payments/{id}/chargeback-defense/Dossiê de defesa

Clientes

MétodoEndpointDescrição
GET/customers/Listar clientes (paginado)
POST/customers/Criar cliente
GET/customers/{id}/Detalhes
PUT/PATCH/customers/{id}/Atualizar
DELETE/customers/{id}/Excluir (bloqueado se houver transações)

Assinaturas

MétodoEndpointDescrição
GET/subscriptions/Listar assinaturas
POST/subscriptions/Criar assinatura
POST/subscriptions/{id}/cancel/Cancelar (em carnê, cancela as parcelas em aberto)
POST/subscriptions/{id}/switch-to-carne/Trocar para carnê (só active/overdue; 409 fora disso, em carnê ativo ou Pix Automático)
GET/subscriptions/{id}/Detalhe (mesmo objeto da lista)
GET/subscriptions/{id}/detail/Resumo com próxima cobrança estimada
POST/subscriptions/{id}/change-amount/Alterar valor (agendado para a próxima cobrança; imediato no Pix Automático e em pending; 409 no carnê)
POST/subscriptions/{id}/change-amount/cancel/Cancelar a mudança de valor agendada
POST/subscriptions/{id}/approve/Aprovar inscrição pending_approval
POST/subscriptions/{id}/reject/Rejeitar inscrição pending_approval
POST/subscriptions/bulk/Importar até 500 assinaturas (CREDIT_CARD ou BOLETO)
GET/subscriptions/export/Exportar CSV (mesmos filtros da lista)

Carnês

MétodoEndpointDescrição
GET/carnes/?subscription_id=Listar (paginado)
GET/carnes/{id}/Detalhe com as parcelas
POST/carnes/{id}/send-email/Enviar ao assinante por e-mail (só active; 429 no 4º envio/hora do carnê ou no 31º/hora da conta)
GET/public/carnes/{token}.pdfPDF do carnê (sem auth; 10/min por IP)

Planos

MétodoEndpointDescrição
GET/plans/Listar planos
POST/plans/Criar plano
PUT/plans/{id}/Atualizar
DELETE/plans/{id}/Excluir

Produtos e Pedidos

MétodoEndpointDescrição
GET/products/Listar produtos
POST/products/Criar produto
GET/products/{id}/Detalhes
PUT/PATCH/products/{id}/Atualizar
DELETE/products/{id}/Excluir (sem pedidos) ou arquivar (com histórico)
POST/orders/create/Criar pedido (link de pagamento, ou cobrança imediata com billing_type)
GET/orders/Listar pedidos
GET/orders/{id}/Detalhes
POST/orders/{id}/fulfill/Marcar entregue
POST/orders/{id}/payment-link/renew/Novo link para pedido pending com link vencido/cancelado (409 se o link ainda está aberto)
POST/orders/{id}/cancel/Cancelar pedido pending (cancela o link aberto junto; 409 fora disso)

Contratos eletrônicos (read-only)

MétodoEndpointDescrição
GET/contracts/status/Visão geral (pending + active + all_signed)
GET/contracts/pending/Apenas contratos a assinar
GET/contracts/active/Apenas vigentes
GET/contracts/{id}/preview/Render do template para leitura
GET/contracts/history/Histórico de aceitações
GET/contracts/acceptances/{id}/Detalhe de uma aceitação

Assinar é ação humana — feita em /dashboard/contract com PIN.

Onboarding (read-only)

MétodoEndpointDescrição
GET/onboarding/status/Status do compliance interno
GET/onboarding/checklist/Checklist pós-aprovação (4 passos)

Upload de documentos é ação humana — feita em /dashboard/onboarding.

Auth (rotação de chave)

MétodoEndpointDescrição
POST/auth/api-key/regenerate/Rotaciona a API key (requer PIN)

Cadastro, login, PIN e password reset são ações humanas — feitas no dashboard.

Financeiro

MétodoEndpointDescrição
GET/balance/Saldo (disponível + pendente)
GET/balance/scheduled/Cronograma de recebíveis (projeção heurística)
GET/treasury/calendar/Calendário oficial de saques (Previsto/Depositado/Rejeitado — use pra "Caindo hoje")
GET/finance/statement/Extrato financeiro
POST/transfers/pix/Transferir via Pix (PIN)
POST/transfers/bank/Transferir via banco (PIN)
GET/transfers/Histórico de transferências

Antecipação

MétodoEndpointDescrição
GET/anticipations/Listar transações elegíveis
POST/anticipations/simulate/Simular antecipação
POST/anticipations/request/Solicitar (PIN)

Configurações

MétodoEndpointDescrição
GET/settings/Configurações da org
PATCH/settings/Atualizar configurações
GET/settings/bank-account/Conta bancária de repasse
PUT/settings/bank-account/Atualizar conta bancária (PIN)

Defesa de chargeback

MétodoEndpointDescrição
GET/chargebacks/Lista de transações em disputa
GET/payments/{id}/chargeback-defense/Dossiê pré-montado
GET/payments/{id}/evidences/Evidências anexadas
POST/payments/{id}/evidences/Upload de evidência (multipart, máx 25 MB)
DELETE/payments/{id}/evidences/{ev_id}/Remover evidência

Export LGPD

MétodoEndpointDescrição
GET/export/Histórico de exports
POST/export/Solicitar export (format: json ou csv)
GET/export/{id}/download/URL presigned de download
PUT/webhooks/config/Configurar webhook (PIN)
GET/onboarding/status/Status do onboarding

Relatórios

MétodoEndpointDescrição
GET/reports/overview/Dashboard overview
GET/reports/revenue/?days=30Receita por período
GET/reports/transactions/Transações com filtros
GET/reports/churn/Churn de assinaturas
GET/reports/anticipation/Relatório de antecipações

Programa de Afiliados

MétodoEndpointDescrição
GET/organization/affiliate-program/Config do programa da loja
PATCH/organization/affiliate-program/Ligar/desligar + janela + comissão default
GET/products/{id}/affiliate-config/Comissão configurada no produto
PUT/products/{id}/affiliate-config/Atualizar comissão do produto
GET/plans/{id}/affiliate-config/Comissão configurada no plano
PUT/plans/{id}/affiliate-config/Atualizar comissão do plano + cap renovações
GET/organization/affiliate-applications/Pedidos de afiliação (filtro ?status=pending)
POST/affiliate-applications/{id}/moderate/Aprovar / rejeitar / revogar
GET/products/{id}/affiliates/Afiliados ativos no produto
GET/organization/affiliate-report/Relatório de comissões pagas
POST/public/affiliate-track/Registrar clique (sem auth; checkout próprio)

15. Códigos de Erro

StatusSignificado
200Sucesso
201Criado com sucesso
400Dados inválidos (verifique o corpo da requisição)
401API key inválida ou expirada
403Sem permissão (org suspensa ou feature desabilitada)
404Recurso não encontrado
429Rate limit excedido (100 req/min padrão)
502Erro no processamento de pagamentos (indisponível)

Todos os erros retornam JSON:

{ "error": "Descrição do erro." }

16. Prazos de Recebimento

Cada método tem seu próprio prazo de liquidação, sujeito às condições da sua conta — não fixe esse prazo no seu código. Para a data real de cada cobrança, use GET /api/v1/balance/scheduled/ (release_date por item, §9); com antecipação liberada na conta, a data prevista de crédito vem em expected_payment_date (§10), sujeita a análise e aprovação.


17. Limites e Rate Limiting

RecursoLimite
API key (geral)100 req/min
Checkout30 req/min
Webhooks500 req/min
PDF público do carnê10 req/min por IP

Header Retry-After é retornado quando o limite é excedido.


Suporte