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) | Endpoints | Seções deste guia |
|---|---|---|
| Clube de assinatura com valor por cliente | POST /plans/, POST /customers/, POST /subscriptions/ (amount_override), POST /subscriptions/{id}/change-amount/ | Planos, Assinaturas, Alterar valor |
| Encomenda sob medida paga antes da produção | POST /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 API | POST /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 site | POST /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 cupom | POST /products/ (bundle), POST /bundle-items/, POST /order-bumps/, POST /coupons/ | Produtos, Bundle, Order Bumps, Cupons |
| Vendas pelo link da bio, sem site | POST /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ão | POST /customers/{id}/one-click-link/, POST /charges/charge-saved/ | 3.7 |
| Associação ou clube com mensalidade por faixa e aprovação | POST /plans/ (requires_approval), POST /subscriptions/bulk/, POST /subscriptions/{id}/approve/ | Planos, Assinaturas, Aprovar ou rejeitar |
| Doação ou contribuição com valor livre | POST /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 parcelamento | PATCH /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).
VitrinRefusedErrorvsVitrinValidationError. 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, comcode(ABECS),customerMessage,retryableetransactionId.VitrinValidationErroré payload rejeitado — nada foi enviado ao banco. Até o@vitrindigital/node0.3.1 e ovitrin(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 umcatch/exceptdeVitrinValidationError. Umcatchgenérico deVitrinErrorsegue pegando as duas.
Retry automático.
GET(e demais métodos idempotentes) é repetido em 429, 5xx, tempo esgotado e erro de rede. UmPOSTouPATCHsó é repetido quando não há risco de cobrar duas vezes: resposta 429, resposta 5xx comretryable: true, ou conexão que nem abriu (DNS, conexão recusada). Tempo esgotado de leitura e 502/503/504 semretryable: truenão são repetidos. A exceção é o/charges/e o/charges/charge-saved/comIdempotency-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çãouncertainWaitMs/uncertain_wait_seconds;0desliga). Até o@vitrindigital/node0.4.0 e ovitrin(Python) 0.3.0 qualquer 5xx dePOSTera 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:
- Acesse vitrin.digital → Criar conta
- Preencha CNPJ, razão social, endereço, dados do sócio responsável e defina seu PIN de 6 dígitos
- Anote a
vd_test_*que aparece na tela final — você já pode integrar e testar imediatamente - 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 - Se perder a
vd_live_*, regenere pelo dashboard ou viaPOST /auth/api-key/regenerate/(com PIN)
Autenticação
Cada organização recebe 2 chaves com momentos de geração diferentes:
| Chave | Prefixo | Quando é gerada | Quando funciona |
|---|---|---|---|
| Teste (sandbox) | vd_test_* | No signup — exibida na tela | Imediatamente após cadastro |
| Produção | vd_live_* | No momento da aprovação — enviada por email | Apó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 inclusivepending_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:
| Chave | Prefixo | Onde ver | O que faz |
|---|---|---|---|
| Publicável de produção | vd_pk_live_* | Conta → Configurações → Chaves de API | Só POST /checkout/tokenize/, GET /checkout/config/, POST /checkout/3ds-skip/, POST /checkout/intents/{id}/confirm/ e GET /checkout/intents/{id}/status/ |
| Publicável de teste | vd_pk_test_* | Conta → Configurações → Chaves de API | O 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"
}
| Campo | Obrigatório | Descrição |
|---|---|---|
customer_id | sim | Cliente da sua organização (§2) |
amount | sim | Valor da cobrança |
billing_type | sim | PIX, BOLETO ou CREDIT_CARD, entre os métodos ativos da conta |
description | não | Até 500 caracteres. No Pix, também distingue cobranças do mesmo valor (§3.0) |
due_date | não | Vencimento (AAAA-MM-DD) |
installments | não | Parcelas 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_token | no cartão | Token de POST /checkout/tokenize/ (§3.3, Passo 1) |
authentication | não | Resultado do 3DS (§3.3, Passo 3) |
installment_markup_pct | não | Adicional 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-Keydevolve 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
PIXe statuspending; - 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: semIdempotency-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
descriptionseja 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 Anexo | Configuração da org | three_ds | Cobrança de cartão |
|---|---|---|---|
disabled | a opção não aparece | off | authentication é ignorado |
optional (padrão) | desligado (padrão) | off | authentication é ignorado |
optional | ligado | try | autenticação válida → cobra com 3DS; ausente ou inválida → cobra sem 3DS. Nunca recusa por 3DS |
required | travado ligado | require | sem autenticação válida → 422 (abaixo), com as isenções |
| conta LLC (EUA) | a opção não aparece | off | a 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_code | Quando | error |
|---|---|---|
VITRIN_3DS_MISSING | Sem authentication (ou sem eci) | "Cobrança recusada — 3DS obrigatório." |
VITRIN_3DS_NOT_AUTHENTICATED | authentication 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çõesapprove,change_amountebulk), 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_ds | Vitrin.authenticate3DS |
|---|---|
off | resolve null na hora, sem carregar o script de 3DS e sem desafio |
try | resolve com a autenticação; resolve null em cartão não inscrito, bandeira sem 3DS, falha, erro, timeout ou comprador que desiste |
require | resolve 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.confirmCardPaymentcom 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(ecicom 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(emtry, resolvenull): 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:
- 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.
- A página confirma com a chave publicável e o
client_secretda intent. OVitrin.confirmCardPaymenttokeniza 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 oclient_secretda 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_secretaparece só nesta resposta. Entregue à página sóideclient_secretdaquela compra; não o guarde em log. - A intent vale por 60 minutos e é de uso único. Crie uma por tentativa de compra.
installmentsno confirm fica limitado ao menor entremax_installmentse o teto de parcelas da conta (acima disso, 400).metadata(até 20 chaves, 4 KB) eexternal_referencevoltam no webhook de pagamento, no blocopayment_intent.- Intent criada com
vd_test_*confirma comvd_pk_test_*; comvd_live_*, comvd_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:
- lê a intent em
GET /checkout/intents/{id}/status/(valor e teto de parcelas); - confere
installmentscontra o teto, sem chamar a rede de novo se passar dele; - tokeniza o cartão pela intent (
intent_id+client_secret; o cliente vem dela); - roda o 3DS pela política da org, com o valor da intent;
- 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
| Resultado | Quando | O que fazer |
|---|---|---|
{ status: 'succeeded', transaction_id } | Cobrança aceita no processamento | Mostre 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 s | Nã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ça | Pode 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 tentativas | Desfecho 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 emissor | Mostre 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 valeu | Outro cartão, Pix ou boleto |
Vitrin.Error 3ds_failed, 3ds_timeout, 3ds_error, 3ds_mpi_init_failed | Política require e o 3DS falhou no navegador | O 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 tokenize | Confira 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 minutos | Crie outra intent |
Vitrin.Error invalid_request (400) | Parcelas acima do teto (details.max_installments), ou chave de outro ambiente | Ajuste as parcelas / a chave |
Vitrin.Error provider_unavailable (5xx com retryable: true) | Processamento indisponível, sem cobrança | Pode tentar de novo em alguns segundos |
Vitrin.Error invalid_intent | A leitura da intent veio sem valor ou sem teto de parcelas | Nada foi cobrado; tente de novo ou fale com o suporte |
Vitrin.Error timeout | 30 s sem resposta na leitura da intent ou no tokenize | Nada foi cobrado; pode tentar de novo |
Vitrin.Error rate_limited (429) | Muitas tentativas | Aguarde |
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,amountemax_installments.transaction_idé o da tentativa mais recente: depois de uma liberação ele continua preenchido, comstatus: requires_payment, e não quer dizer cobrança. Este último é o teto efetivo: o menor entre omax_installmentsda 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_referencenemdescription. - Com a chave publicável, o
client_secretvai só no headerX-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 comintent_status: "processing"eretryable: falsee 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_codesignifica o contrário: houve resposta de autorização — a cobrança saiu daqui, chegou ao emissor e voltou negada. Um 422 semreturn_codeé que é payload rejeitado (aí sim nada foi enviado ao banco). Se você ramifica pelo status HTTP, use oreturn_codecomo discriminador; se usa os SDKs oficiais, a distinção já vem em classes separadas (VitrinRefusedErrorvsVitrinValidationError— 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 (usecustomer_message).transaction_id: id daTransactioncomstatus='failed'que registra a tentativa. Toda recusa é persistida, inclusive a da primeira cobrança de uma assinatura de cartão — você encontra emGET /api/v1/transactions/?status=failed, emGET /api/v1/payments/{id}/e no painel, com o blocorefusalcarregando 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: falsecompayment_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 datransaction_idou consulteGET /api/v1/transactions/{transaction_id}/. Se a cobrança foi efetivada, a Vitrin recebe a confirmação, a transação virareceivede você recebe oPAYMENT_RECEIVEDnormalmente. 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, orefusalsome (null) e você recebe oPAYMENT_RECEIVED; - a cobrança foi recusada: a transação segue
failed, mas com o código real do emissor norefusal(e oretryabledele), e você recebe oPAYMENT_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
failedcomrefusal.code = "VITRIN_PROVIDER_NOT_CHARGED"eretryable: true: nada foi cobrado, e você pode cobrar de novo (com umaIdempotency-Keynova, 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.code | O que aconteceu | retryable |
|---|---|---|
VITRIN_PROVIDER_UNCERTAIN | A resposta do processamento se perdeu; a cobrança pode ter sido feita. Em confirmação | false |
VITRIN_PROVIDER_NOT_CHARGED | A cobrança não chegou a ser processada; nada foi cobrado | true |
VITRIN_PROVIDER_UNCONFIRMED | Só no rastro da assinatura de cartão: 24 h sem confirmação. Pode ter sido cobrada; confirme com o suporte | false |
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:
| Endpoint | Caso incerto | Demais falhas de processamento |
|---|---|---|
/checkout/pay/, /charges/ | 502 com o corpo acima | 502 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 é criado | como 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ão | 502 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-click | 502 com error: "provider_error", retryable: false, payment_status e transaction_id; o link já foi consumido | 502 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_RECEIVEDouPAYMENT_REFUSED) ou consulte a transação. - Recusa: mostre o
customer_messageao 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 emTransaction.net_amountquando 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_AUTOMATICemallowed_payment_methods) e pela própria org emenabled_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 respondem403e nada é criado; as consultas (GET) continuam funcionando. Use avd_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 combilling_cyclebiweeklyoubimonthlyebilling_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 nostart_dateda resposta.value: valor fixo cobrado a cada ciclo. Obrigatório — semvalue(oumin_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.valueemin_limit_valuesão mutuamente exclusivos.min_limit_value(alternativa avalue): 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
Customertemphone,postal_code,address,address_number,province,cityestatepreenchidos — se faltar algum, a API retorna 400 commissing_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:
- A autorização vai de
CREATEDparaACTIVE. Você recebe o evento:PIX_AUTOMATIC_RECURRING_AUTHORIZATION_ACTIVATED - A
Transactionda primeira cobrança (criada automaticamente no momento da autorização, comstatus='pending') vai parareceivedquando o cliente paga, via webhook PIX padrão. Ela aparece emGET /api/v1/transactions/combilling_type='PIX_AUTOMATIC'eprovider_payment_idigual 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_RECEIVEDePIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_PAID. Reconcilie de forma idempotente pelopayment.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_dateda 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
pendingenquanto houver tentativa. Recusa do banco do pagador não é retentada. Sem retentativa possível, a cobrança vai paraoverdue.
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_dateentre 2 e 10 dias úteis: enviada na hora, HTTP 201, comprovider_payment_idpreenchido.due_datea mais de 10 dias úteis: não é erro. A Vitrin guarda a cobrança e devolve HTTP 202 comprovider_payment_idvazio; ela é enviada automaticamente quando faltarem cerca de 3 dias úteis para o vencimento.due_datea 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 emsuggested_due_date).Se a autorização tem
valuefixo, 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 doCustomerestart_date. Mudanças de valor (value/min_limit_value),finish_date,frequency,customeroucontract_idexigem 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. ATransactionpendente associada é marcada comocanceledautomaticamente.
Status possíveis
| Autorização | Significado |
|---|---|
CREATED | Aguardando aceite do pagador |
ACTIVE | Aceita — cobranças podem ser geradas |
CANCELLED | Cancelada (pela org ou pelo pagador) |
REFUSED | Recusada pelo banco do pagador |
EXPIRED | Expirada sem aceite |
| Instrução (cobrança) | Significado |
|---|---|
AWAITING_REQUEST | Criada localmente |
SCHEDULED | Agendada no SPI |
DONE | Pagamento confirmado |
CANCELLED | Cancelada antes do débito |
REFUSED | Recusada (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 é:
- Cliente preenche o cartão uma vez com consentimento explícito
- Você guarda o
saved_card_token(a Vitrin persiste a referência; o número do cartão fica no cofre PCI do processamento) - Cobranças futuras chamam
/charges/charge-saved/informando só ocustomer_ide 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.
| Erro | Quando |
|---|---|
404 customer_not_found | customer_id não existe ou pertence a outra org |
422 no_saved_card | Cliente sem cartão salvo |
422 card_expired | Mê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.
Link assinado para o cliente final gerenciar
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 lojaDELETE /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.
Link de recompra one-click (cliente final clica e paga)
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=Truena 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
| Erro | Quando |
|---|---|
400 coupon_not_found / coupon_invalid / coupon_not_applicable | Cupom inexistente, inválido (expirado/inativo) ou não aplicável ao produto (na emissão) |
400 bumps_not_found | Algum bump_id não pertence ao parent product (na emissão) |
422 no_saved_card | Customer sem cartão salvo (na emissão) |
422 card_expired | Cartão com mês/ano expirado (na emissão ou no clique) |
422 card_removed | Cartão foi deletado entre emissão e clique |
422 provider_refused | Cartão recusado (customer_message pronto pra exibir) |
502 provider_error | Processamento 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_active | Cliente já tem subscription vigente desse plano (preserva nonce) |
404 item_not_found | Produto/plano não existe ou ficou inativo |
401 token_expired | TTL estourou |
401 token_consumed | Cliente já usou o link |
401 token_invalid | Assinatura inválida / scope errado / tampering |
Histórico de links emitidos
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:
amountno 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
amountinteiro 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 oamountoriginal, não sobre o valor bruto. - A resposta da cobrança e os serializers de
Transactiontrazemamount(o valor efetivamente cobrado do pagador, já com o repasse aplicado) efee_passthrough_amount(a diferença entre o valor cobrado e o preço original;0quando não há repasse). - O webhook de pagamento (
paymentnos eventosPAYMENT_*, §5) também trazfee_passthrough_amount, no mesmo formato numérico deamountenet_amount(0sem 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/combilling_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.
3.9 Link de pagamento avulso
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 (semTransactionlocal), e não está mais na oferta — responde422 {"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 oCustomer.allowed_methods: subconjunto deenabled_payment_methods, semPIX_AUTOMATIC(cobrança avulsa não é recorrência). Vazio = todos os métodos habilitados na conta.max_installments: só junto deCREDIT_CARDentre 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_urlconfigurado) 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 recebe409 {"error": "Pagamento em andamento neste link. Tente novamente em instantes."}. - Link
paid,canceledouexpired→pay/responde409comeffective_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_urledigitable_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, ficamnull; 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ó quandobilling_typeéBOLETO, enullno boleto sem Pix. Nos outros meios o campo não vem — na cobrança Pix avulsa, o copia e cola dela está emGET /transactions/{id}/.carne_ideinstallment_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
}
}
}
cardaparece em qualquer cobrança CC (autorizada ou recusada).refusalsó vem comstatus='failed'e contém:code: ABECS oficial (ex:"51","GD")description: descrição interna em portuguêsmessage: mensagem original do emissorcustomer_message: mensagem pronta pra mostrar ao cliente final no checkoutretryable:falsesignifica 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
}
}
}
ordersó aparece quando há um Order vinculado (compra via checkout público ouPOST /orders/create/).coupon/bump/customer_chosen_amountsó 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"
}
}
}
affiliatesó 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).statusreflete 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:trueem renovações de assinatura/Pix Automático;falsena primeira venda ou produto avulso.settled_at/reversed_ataparecem 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épctoufixed; oamountem reais é o valor que foi destacado pra cada coprodutor.
Importante: a fatia da Vitrin não aparece no webhook canônico —
amountjá é o que sua loja vende;net_amountjá 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):
| Evento | Quando | Campos no topo do corpo |
|---|---|---|
subscription.dunning_warning | Assinatura overdue há ≥ 7 dias | subscription_id, plan_id, customer_id, overdue_since |
subscription.canceled_for_dunning | Assinatura overdue há ≥ 30 dias — a Vitrin cancelou a assinatura | idem acima |
subscription.canceled | A 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.canceledcobre 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": {}}(headerX-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_CREATEDsempayment.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, umdunning_warningantigo de uma assinatura que já voltou aactivenã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
| Evento | Quando |
|---|---|
PAYMENT_RECEIVED | Pagamento confirmado |
PAYMENT_CONFIRMED | Pagamento confirmado (cartão) |
PAYMENT_OVERDUE | Pagamento vencido. Na parcela de carnê, sai no 5º dia após o vencimento (carência) |
PAYMENT_REFUSED | Pagamento recusado (cartão) — body inclui refusal |
PAYMENT_REFUNDED | Reembolso processado |
PAYMENT_REFUND_PENDING | Estorno solicitado; o banco do pagador ainda não confirmou (Pix) |
PAYMENT_REFUND_FAILED | Estorno não se concretizou; a cobrança volta a valer |
PAYMENT_CHARGEBACK_REQUESTED | Chargeback aberto |
PAYMENT_DELETED | Pagamento cancelado |
CARNE_CREATED | Carnê emitido: assinatura nova em carnê ou troca para carnê. Corpo em Assinatura em carnê |
CARNE_RENEWED | Carnê seguinte emitido pela renovação automática |
CARNE_CANCELED | Parcelas em aberto do carnê canceladas, porque a assinatura foi cancelada |
CARNE_RENEWAL_BLOCKED | Renovaçã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_warning | Ciclo 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_CREATED | Autorização Pix Automático criada |
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_ACTIVATED | Pagador aceitou — autorização ativa |
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_REFUSED | Autorização recusada pelo banco |
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_CANCELLED | Mandato encerrado — cliente cancelou no app do banco, a org cancelou via API, ou detectamos cancelamento no processamento |
PIX_AUTOMATIC_RECURRING_AUTHORIZATION_EXPIRED | Mandato encerrado — expirou sem aceite do pagador |
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_CREATED | Instrução de cobrança criada |
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_SCHEDULED | Instrução agendada no SPI |
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_PAID | Instrução de cobrança paga (acompanha um PAYMENT_RECEIVED — ver nota abaixo) |
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_REFUSED | Instrução recusada (ver refusalReason) |
PIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_CANCELLED | Instruçã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_idpode virnullquando a autorização foi criada direto porPOST /pix-automatic/authorizations/sem assinatura vinculada. Nesse caso reconcilie porauthorization.id(o mesmo que você recebeu na criação) ou porcustomer_id. Não assuma quesubscription_idsempre existe.
Eventos desconhecidos: o vocabulário canônico cresce ao longo do tempo. Trate qualquer
eventque sua integração não reconheça como no-op — nunca como erro.PAYMENT_REFUND_PENDINGePAYMENT_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 emrefund_pending, e se a devolução não se concretizar ela volta parareceived.
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) ePIX_AUTOMATIC_RECURRING_PAYMENT_INSTRUCTION_PAID(qual instrução da recorrência foi quitada). Ambos apontam para a mesmaTransaction(mesmopayment.id). Sãoeventdistintos, comX-Vitrin-Event-Iddistintos — não são entrega duplicada. Trate seu handler de forma idempotente porpayment.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.0desativa. 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), porPOST /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_methodsvazio eunavailable_message).trial_price(decimal, default 0) — só0é aceito (trial gratuito); valor maior devolve400.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 devolve400.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, defaultfalse) — "Pagador define o valor": o checkout público pede o valor ao cliente, e ele vira oamount_overrideda assinatura. NoPOST .../pay/do checkout, envie o valor emamount_override; o link do plano aceita?suggested=150,00para pré-preencher.min_amount/max_amount(decimal, opcionais) — piso e teto do valor escolhido pelo cliente. Só são aceitos comallow_customer_defined_amount=true(senão,400). Valor fora da faixa no checkout devolve400com"Valor mínimo aceito: R$ …"ou"Valor máximo aceito: R$ …".requires_approval(bool, defaultfalse) — "Exige aprovação manual": a assinatura feita pelo checkout público nascepending_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,400embilling_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 emGET /transactions/e chega pelos webhooks de pagamento. -
PIXresponde400embilling_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 comtrial_somente_carne: true, aceita sóCARNE; outro meio responde400com"Este plano tem período de teste: a assinatura só pode ser feita no carnê.". Em conta comtrial_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 ounull= preço do plano. Para mudar depois, use Alterar valor. -
credit_card_token(opcional, só cartão): sem ele, a resposta trazcharge_url, o link de ativação em que o cliente cadastra o cartão; o mesmo link fica emactivation_urlenquanto a assinatura estiverpending. -
Com
PIX_AUTOMATIC, a resposta trazpix_automaticcomqr_codeeqr_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, useamount_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) oufalse(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.phoneeamount_overridesão opcionais. billing_type:CREDIT_CARDouBOLETO(boleto mensal); na conta global,PIXno lugar deBOLETO. Fora da conta global,PIXvai paraerrorscom 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 emactivation_url(emGET /subscriptions/{id}/) para enviar ao cliente. No boleto, o boleto de cada ciclo vai para oemailda linha.- Plano com período de teste, em conta com
trial_somente_carne: true, não entra na importação: a linha vai paraerrorscom a mesma mensagem da criação. - O cliente é criado ou atualizado pelo
cpf_cnpj. Se ele já tem assinaturaactive,pendingouoverdueno 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}, com200sem erros e207quando 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: trazcharge_url, o link de ativação para enviar ao cliente;PIX_AUTOMATIC: trazpix_automatic(qr_code,qr_payload) para enviar ao cliente. Se o Pix Automático não estiver mais liberado na conta,400e a inscrição continuapending_approval;BOLETO: o 1º boleto é gerado. Em contas em que ele chega por e-mail, sem e-mail cadastrado,400(mesma mensagem dePOST /subscriptions/) e a inscrição continuapending_approvalaté 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 comtrial_somente_carne: true):400com o motivo e"Rejeite esta inscrição e crie a assinatura de novo com outra forma de pagamento."; a inscrição continuapending_approval. - Se o cliente usou cupom no checkout (só
PIX_AUTOMATIC), o desconto é aplicado agora.
- Rejeitar move para
rejectede não cobra nada.reasonfica só no histórico. - Nos dois, assinatura fora de
pending_approvaldevolve400.
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_overrideeeffective_amountcontinuam com o valor atual até a data.pending_amount_changetambém aparece emGET /subscriptions/eGET /subscriptions/{id}/. Énullquando 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).couponsó 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 devolve409. - Se a cobrança daquela data já tiver saído no valor antigo, a mudança passa para a cobrança seguinte e
effective_dateavança. - Sem próxima cobrança programada, o pedido devolve
409. Se não foi possível consultar a data, devolve502e 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
pendinge o cliente precisa cadastrar o cartão de novo pelo link de ativação. O link aparece emactivation_urlno detalhe enquanto a assinatura estiverpending, 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 aactive.
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, escheduled_forvemnull. No cartão, a resposta trazrequires_customer_action: truee o link emcharge_url, que também fica emactivation_urlno detalhe. Envie o link ao cliente. Nos outros meios,requires_customer_actionvemfalse. - Carnê: o pedido devolve
409, porque as parcelas já foram emitidas.
Comportamento por meio de pagamento
CREDIT_CARD | BOLETO | PIX_AUTOMATIC | CARNE | |
|---|---|---|---|---|
| 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ça | Com credit_card_token (ou cartão no checkout), cobrada na criação. Sem token, quando o cliente cadastra o cartão pelo charge_url | Boleto gerado na criação | Junto com a autorização: pix_automatic.qr_code / qr_payload na resposta | 1ª parcela do carnê, emitido na criação |
| Ciclos seguintes | Cobrados no cartão, na data do ciclo | Um boleto por ciclo | Criados pela Vitrin e debitados na data (§3.6) | As outras parcelas, já emitidas |
change-amount | Agendado 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 cliente | Imediato | 409 |
| 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 agendado | Nã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ão | 400: o 1º boleto sai na criação | 400: a 1ª cobrança vem com a autorização | 1º vencimento padrão no fim do trial. Único meio aceito nessas contas |
approve | Devolve charge_url | Gera o 1º boleto | Devolve pix_automatic | Nã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 responde400. 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ãofalse. Comtrue, a Vitrin envia o carnê por e-mail ao assinante na emissão e em cada renovação, se ele tiver e-mail cadastrado. Vejasend-email/.
Outros pontos:
- Valor da parcela: é o valor da assinatura (
amount_overrideou 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_tokenenext_due_datenã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. Otrial_pricenão vale no carnê: toda parcela tem o valor da assinatura. Comfirst_due_dateexplícito, vale a data informada. - Status: a assinatura nasce
pendinge viraactivequando 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 é
422na recusa (mesmo formato de §3.3, "Recusas") ou502com o processamento indisponível. - Sandbox: criada com a chave de teste (
vd_test_*), a assinatura fica marcada como de teste e o carnê sai comis_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/comCARNEe paraswitch-to-carne/; - mande sempre
Idempotency-Keyna 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 oPAYMENT_RECEIVEDnormal.
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
activeouoverdue. Depois da troca ela ficaactive, mesmo se estavaoverdue: 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 aoverduee 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, responde400antes 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." }
detail | Motivo |
|---|---|
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_dateexplícito anterior ao fim do período já pago responde400:{ "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 (installmentsfora 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:
- A Vitrin emite as parcelas.
- Encerra a recorrência anterior no processamento, do mesmo jeito que o cancelamento faz.
- 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_typepassa aCARNE;provider_subscription_idfica 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 responde400. - Carnê de outra organização responde
404.
Campos do carnê (mesmo corpo na criação, na troca, na listagem e nos webhooks CARNE_*):
| Campo | Descrição |
|---|---|
id | Id do carnê |
subscription_id | Assinatura do carnê |
status | active (há parcela em aberto), completed (todas pagas) ou canceled |
number_of_installments | N, de 1 a 12 |
first_due_date | Vencimento da 1ª parcela |
send_email | Se o carnê vai por e-mail ao assinante na emissão e na renovação |
is_sandbox | true se o carnê foi emitido com a chave de teste (vd_test_*): boletos do ambiente de teste, que não cobram de verdade |
pdf_url | PDF público do carnê (ver abaixo) |
renewed_by_id | Carnê seguinte, emitido na renovação. null até lá |
installments | As parcelas, em ordem |
created_at / updated_at | Datas 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;receivedouconfirmed: 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:
| Status | Quando | Corpo |
|---|---|---|
404 | Carnê inexistente ou de outra org | — |
409 | Carnê completed ou canceled: não há o que enviar | { "error": "carne_not_active", "detail": "Carnê não está ativo (status atual: completed).", "status": "completed" } |
400 | Assinante sem e-mail cadastrado | { "error": "Assinante sem e-mail cadastrado." } |
429 | 4º 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ê." } |
429 | 31º 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 (
pendingouoverdue), 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ê
completedabre com o aviso de que não há parcelas em aberto; - carnê
canceled, token desconhecido ou conta suspensa respondem404, com o mesmo corpo nos três casos; - limite de 10 aberturas por minuto por IP; acima disso,
429comRetry-After.
- carnê
- 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
pendingouoverdueviraactive. - 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
overdueaté 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ê recebePAYMENT_OVERDUEdela, uma única vez por parcela, comcarne_ideinstallment_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_warningaos 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 saiCARNE_CANCELED.
Exemplo com uma parcela que vence em 10/11/2026 e não é paga:
| Data | O que acontece |
|---|---|
| 10/11 | A parcela vence |
| 11/11 a 14/11 | Carência: nada muda |
| 15/11 | Parcela overdue + PAYMENT_OVERDUE; assinatura overdue |
| 22/11 | subscription.dunning_warning |
| 15/12 | Assinatura 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 (
pendingeoverdue) são canceladas no processamento e passam acanceled; - as parcelas já pagas não mudam;
- quando todas as parcelas em aberto estiverem canceladas, o carnê vira
cancelede você recebeCARNE_CANCELED; - se o processamento falhar em alguma parcela, o job diário tenta de novo, e o
CARNE_CANCELEDsó sai quando todas estiverem canceladas; - depois do cancelamento, o
pdf_urlresponde404.
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 saiCARNE_CREATED); - no carnê anterior,
renewed_by_idaponta para o novo; - um e-mail de aviso ao dono da conta;
- o e-mail "Seu novo carnê —
<sua loja>" ao assinante, sesend_emailestiver 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
activedepois 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 (
BOLETOemenabled_payment_methodse conta que não seja LLC). Sem isso, a Vitrin não renova e avisa uma única vez por carnê: webhookCARNE_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 porcarne.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…), comcarne_ideinstallment_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ê trazis_sandbox: truena 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 valorbundle— combo agrupando outros produtos (ver seção "Bundle")
Modo de preço (price_mode):
| Valor | Comportamento |
|---|---|
fixed (default) | Cliente paga exatamente price |
customer_defined | Cliente digita o valor no checkout. price vira sugestão pré-preenchida. |
Para customer_defined, configure piso/teto:
min_price(decimal, opcional) — valor mínimo aceitomax_price(decimal, opcional) — teto opcional
O tipo
contributionautomaticamente forçaprice_mode=customer_defined. Para um produto regular ("Pague quanto quiser pelo curso"), useone_timecomprice_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ção | Resposta | O que acontece |
|---|---|---|
| Nenhum pedido | 204 | Produto excluído de verdade. |
Algum pedido pending | 400 | Nada 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 combinado | 400 | Nada muda. Remova-o do pacote antes. |
| Qualquer outro histórico (pago, entregue, cancelado, reembolsado, expirado) | 200 | Produto 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ó combilling_typePIX_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 emmethodsnocoupon/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 noPOST /subscriptions/{id}/approve/. usage_limit,valid_from/valid_until,is_activeemin_amount(comparado ao valor de cada ciclo) valem como no produto.POST /subscriptions/(API) não aceita cupom: para um valor diferente, useamount_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 conformefield_configda org (retornado embranding.field_configno GET acima). Se um campoenabled=true, required=truevem vazio, a resposta é400 {error, missing: [...]}. custom_dataaceita as chaves definidas embranding.field_config.custom_fields— campos required vazios também caem na mesma validação.custom_amountobrigatório quandoproduct.price_mode=customer_defined.coupon_codevalida e aplica desconto se válido. Em plano, o desconto vale só na 1ª cobrança, e só noPIX_AUTOMATIC— não no cartão nem no boleto (ver Cupons).- Em plano,
billing_typePIXresponde400com"Assinatura no Pix comum não está disponível: ...", e em plano com trial, em conta comtrial_somente_carne: true, qualquer meio responde400 {"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 viametadata.parent_order_id(até 10 por checkout).bump_idsingular continua aceito por compat.- Cartão: envie
credit_card_token(tokenizado por você) oucredit_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 é422comcustomer_messagee nenhuma transação é criada.save_card: true+card_consent.accepted_atsalva o cartão para one-click, só depois de o pagamento ser aprovado. - Plano no cartão com
charge_urlpreenchido: a assinatura nasceu sem o cartão, echarge_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
429comcustomer_messageeblocked_method: "CREDIT_CARD"por um tempo. Mostre ocustomer_messagee 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 temenablederequired. custom_fields[]: tipos suportados —text,email,tel,select,textarea. Paraselect, populeoptions[].- O backend sempre devolve
field_configmesclado 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>/devolvebranding.field_configsó com a configuração da org (sem os mínimos dos métodos embutidos) — a UI precisa mesclarbranding.method_requirements[<método escolhido>]por cima antes de renderizar/validar (mesmo contrato doGET /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_AUTOMATIC | email |
CREDIT_CARD | email, phone |
BOLETO | email, 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 nascependingjá com a transação vinculada, e quando o webhookPAYMENT_RECEIVEDchega, o pedido muda parapaid.CREDIT_CARD: exigecredit_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 nascependingsem transação. A resposta (201) trazpayment_link {id, url, status, expires_at, ...}— mande esseurlao 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 virapaid; 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, semPIX_AUTOMATIC) eexpires_at(padrão: 7 dias). Indisponível com chave de sandbox (vd_test_*): responde400 {"detail": "Link de pagamento indisponível em sandbox."}e nada é criado — a página pública do link cobraria de verdade. allowed_methodseexpires_atsó valem sembilling_type. Enviados junto combilling_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 combilling_type(sem link);409 payment_link_open— o link atual ainda está aberto: reenvie esse mesmo link;400—expires_atinvá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 combilling_typePIX/BOLETO: a cobrança já existe e seguiria pagável. O corpo traztransaction_id; cancele a cobrança emPOST /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:
| Status | Significa | O que mostrar ao usuário |
|---|---|---|
200 | Reembolso concluído. O dinheiro voltou. | "Reembolso efetuado" |
202 | Reembolso 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
refundede você recebePAYMENT_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 empaid_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:
| Status | Significado |
|---|---|
Previsto | Repasse agendado pra essa data (ainda não saiu). |
Depositado | Repasse efetivado — o valor já caiu (ou está caindo) na sua conta bancária. |
Disponível | Saldo acumulado disponível pra saque (orgs com auto-repasse desligado). |
Rejeitado | Repasse falhou — message traz o motivo (dados bancários ausentes, conta inativa, etc.). |
Indefinido | Valor provisório antes da consolidação do dia. Ignore no agregado. |
Como usar nas suas telas:
- "Caindo hoje" =
itemsonderelease_date == today AND status == 'Depositado'→ someamount. Esse é o número que vai bater exato com o extrato bancário da sua loja no dia. - "Próximos saques" =
itemsondestatus == 'Previsto', agrupado porrelease_date. - "Saldo disponível pra saque" =
totals.disponivel(só pra orgs com auto-repasse pausado). - "Repasses falhos" =
itemsondestatus == 'Rejeitado'— mostre omessageao operador da loja.
Não some
Transaction.net_amountlocalmente 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_feeenet_to_destinationpra 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 empendingaté você aprovar.falselibera 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=truelista seus produtos no marketplace público da Vitrin (qualquer afiliado descobre).falsemanté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=falseretira o item do marketplace e bloqueia novos pedidos. Comissões já liquidadas não são afetadas.auto_approve=truefaz com que pedidos pra este item específico virem aprovados sem moderação (útil pra produtos de baixo risco mesmo comrequires_approval=trueno 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 usacommission_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,rejectourevoke.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 emaction=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:
- Capture
?aff=na URL de chegada do cliente. - Guarde em cookie/localStorage de 1ª parte (TTL ≤
attribution_window_days). - (Opcional, mas recomendado) chame:
pra registrar o clique antes da conversão (alimenta o histórico do afiliado).POST /api/v1/public/affiliate-track/ { "affiliate_slug": "joao-silva", "org_slug": "minha-loja", "product_slug": "ebook-marketing" } - 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
slugno 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 respondem403(oGETresponde200com{"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, ounull. Enquanto existir, uma nova solicitação responde409(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.
| Status | Quando | Corpo |
|---|---|---|
409 anticipation_in_progress | Há 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"} |
400 | Soma 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} |
400 | Disponível zerado (limite já usado) | {"error": "Não há valor disponível para antecipação no momento.", "available_gross": 0.0} |
400 | Fora do horário aceito | {"error": "…"} — mostre a mensagem e tente mais tarde |
400 | Nenhum dia selecionado está disponível / simulação já usada | {"error": "…"} — simule de novo |
502 | A antecipação foi recusada no processamento | {"error": "…", "description": "…", "retryable": true} — nada foi antecipado; pode tentar de novo |
502 | Nã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_afterem vez de interpretar o texto. Pedidos simultâneos são serializados: se dois chegarem juntos, só um é efetivado e o outro recebe409.
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
fulfilledassim 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:
| Bloco | Editá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_methodsprecisa ser um subconjunto deallowed_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/ePOST /checkout/guest/recusaminstallmentsacima desse teto com400 {"installments": ["Número de parcelas acima do permitido para esta conta (máximo: Nx)."]}— o mesmo teto deGET /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 noinstallment_value/totaldo 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ãotrue): "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étodo | Endpoint | Descriçã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=X | Consultar parcelamento |
Links de pagamento
| Método | Endpoint | Descriçã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étodo | Endpoint | Descriçã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étodo | Endpoint | Descriçã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étodo | Endpoint | Descriçã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étodo | Endpoint | Descriçã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étodo | Endpoint | Descriçã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}.pdf | PDF do carnê (sem auth; 10/min por IP) |
Planos
| Método | Endpoint | Descrição |
|---|---|---|
| GET | /plans/ | Listar planos |
| POST | /plans/ | Criar plano |
| PUT | /plans/{id}/ | Atualizar |
| DELETE | /plans/{id}/ | Excluir |
Produtos e Pedidos
| Método | Endpoint | Descriçã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étodo | Endpoint | Descriçã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/contractcom PIN.
Onboarding (read-only)
| Método | Endpoint | Descriçã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étodo | Endpoint | Descriçã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étodo | Endpoint | Descriçã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étodo | Endpoint | Descrição |
|---|---|---|
| GET | /anticipations/ | Listar transações elegíveis |
| POST | /anticipations/simulate/ | Simular antecipação |
| POST | /anticipations/request/ | Solicitar (PIN) |
Configurações
| Método | Endpoint | Descriçã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étodo | Endpoint | Descriçã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étodo | Endpoint | Descriçã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étodo | Endpoint | Descrição |
|---|---|---|
| GET | /reports/overview/ | Dashboard overview |
| GET | /reports/revenue/?days=30 | Receita 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étodo | Endpoint | Descriçã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
| Status | Significado |
|---|---|
| 200 | Sucesso |
| 201 | Criado com sucesso |
| 400 | Dados inválidos (verifique o corpo da requisição) |
| 401 | API key inválida ou expirada |
| 403 | Sem permissão (org suspensa ou feature desabilitada) |
| 404 | Recurso não encontrado |
| 429 | Rate limit excedido (100 req/min padrão) |
| 502 | Erro 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
| Recurso | Limite |
|---|---|
| API key (geral) | 100 req/min |
| Checkout | 30 req/min |
| Webhooks | 500 req/min |
| PDF público do carnê | 10 req/min por IP |
Header Retry-After é retornado quando o limite é excedido.
Suporte
- Dashboard: https://vitrin.digital/dashboard
- Swagger / API Docs: https://api.vitrin.digital/api/docs/
- Email: contato@vitrin.digital