vitrin (Python)
SDK oficial Python para a API da Vitrin Digital.
pip install vitrin
Python 3.10+ requerido. Única dependência runtime:
requests.
Setup
import os
from vitrin import Vitrin
vitrin = Vitrin(
api_key=os.environ["VITRIN_API_KEY"],
# opcionais:
# base_url="https://api.vitrin.digital/api/v1",
# timeout=30.0,
# max_retries=3,
)
Use vd_test_* em desenvolvimento, vd_live_* em produção.
Organizações LLC (US)
Se a org é uma US LLC (entity_type == "llc"), os valores são em USD
(campo currency nas transações); os métodos default são cartão e Pix
cross-border (o cliente paga em BRL, a LLC recebe em USD). A liquidação é
automática: os endpoints de saldo/recebíveis/transferências retornam
{ "auto_payout": true } em vez de saldo retido (não há saque/antecipação
manual; o cronograma "PIX D+1 / Boleto D+2 / Cartão Nx D+30" abaixo é só para
CNPJ). O onboarding inclui verificação de identidade (KYC). Veja
docs/api/onboarding-llc.md e docs/api/differences-cnpj-llc.md.
Recursos
Clientes
customer = vitrin.customers.create(
name="Maria Silva",
email="maria@example.com",
cpf_cnpj="12345678901",
)
vitrin.customers.list(page=1)
vitrin.customers.update(customer["id"], phone="11987654321")
vitrin.customers.delete(customer["id"])
Cobranças
charge = vitrin.charges.create(
customer_id=customer["id"],
amount=99.90,
billing_type="PIX",
description="Mensalidade abril",
idempotency_key=f"mensalidade-{customer['id']}-2026-04", # evita duplo-débito em retry
)
print(charge["pix_qr_code"])
print(charge["pix_copy_paste"])
# Reembolso parcial
vitrin.charges.refund(charge["id"], amount=50.0, pin="123456")
Planos & Assinaturas
plan = vitrin.plans.create(name="Pro Mensal", price=99.0, billing_cycle="monthly")
sub = vitrin.subscriptions.create(
customer_id=customer["id"],
plan_id=plan["id"],
billing_type="CREDIT_CARD",
credit_card_token="tok_xxx",
)
vitrin.subscriptions.cancel(sub["id"], pin="123456")
Saldo & Recebíveis
vitrin.balance.retrieve()
# → { "available": ..., "total": ..., "pending": ..., "withdrawal_fees": {...} }
vitrin.balance.scheduled(90)
# → cronograma 90d: PIX D+1, Boleto D+2, Cartão Nx D+30·n
Webhooks
from flask import Flask, request, abort
from vitrin import webhooks, VitrinError
import os
app = Flask(__name__)
@app.post("/webhooks/vitrin")
def handle_webhook():
try:
event = webhooks.construct_event(
payload=request.get_data(), # body cru, NÃO parsed
signature=request.headers.get("X-Vitrin-Signature"),
timestamp=request.headers.get("X-Vitrin-Timestamp"),
event_type=request.headers.get("X-Vitrin-Event"),
event_id=request.headers.get("X-Vitrin-Event-Id"),
secret=os.environ["VITRIN_WEBHOOK_SECRET"],
)
print(event.type, event.id, event.data)
return "", 200
except VitrinError as e:
return str(e), 400
Tratamento de erros
from vitrin import (
VitrinError, VitrinAuthError, VitrinValidationError, VitrinRefusedError,
VitrinRateLimitError, VitrinNotFoundError, VitrinServerError,
)
try:
vitrin.charges.create(...)
except VitrinRefusedError as e:
# A cobrança CHEGOU ao emissor e ele negou.
print("Recusado:", e.code, e.customer_message)
except VitrinValidationError as e:
# A cobrança NÃO saiu daqui — o payload foi rejeitado.
print("Campos inválidos:", e.field_errors)
except VitrinAuthError:
print("Chave inválida ou sem permissão")
except VitrinRateLimitError:
print("Aguarde antes de tentar de novo")
except VitrinError as e:
print(f"Erro Vitrin: {e.status_code} {e.request_id} {e.message}")
Retry automático com backoff exponencial (default: 3 tentativas), mas só quando repetir não arrisca cobrar duas vezes:
| Situação | GET (e PUT/DELETE) |
POST/PATCH |
|---|---|---|
429 |
repete | repete (o limite barra o pedido antes de qualquer efeito) |
5xx com retryable: true |
repete | repete (nada foi cobrado) |
5xx com retryable: false |
repete | não repete |
5xx sem retryable (ex.: 502/504 do proxy) |
repete | só no /charges/ e /charges/charge-saved/ com idempotency_key |
| conexão que nem abriu (DNS, conexão recusada) | repete | repete (nada saiu daqui) |
| tempo esgotado, conexão caída no meio | repete | só no /charges/ e /charges/charge-saved/ com idempotency_key |
4xx (exceto 429) |
não repete | não repete |
retryable: true num 5xx quer dizer nada foi cobrado, em todos os caminhos de cartão da API: /checkout/pay/, /charges/, /charges/charge-saved/, /orders/create/, /checkout/guest/, /subscriptions/ e one-click.
O 502 com retryable: false e payment_status: "processing" é o resultado incerto de uma cobrança de cartão: a resposta do processador se perdeu e a cobrança pode ter sido feita. O SDK nunca reenvia esse POST. Em charges.create(), charges.create_with_saved_card() e /checkout/pay/, ele consulta GET /transactions/{transaction_id}/ por até ~60 s (após 2, 4, 8, 15 e 30 s) e devolve o desfecho:
| Desfecho da transação | O que a chamada faz |
|---|---|
paga (received/confirmed) |
devolve a transação: o mesmo objeto do sucesso |
| recusada pelo emissor | levanta VitrinRefusedError, como o 422 |
| ainda incerta no fim do prazo (ou consulta falhando) | levanta VitrinServerError com payment_status: "processing" e transaction_id no body |
O prazo é configurável: Vitrin(api_key=..., uncertain_wait_seconds=30); uncertain_wait_seconds=0 desliga a consulta e levanta o erro na hora. Uma falha na consulta não conta como "não cobrado": o SDK segue consultando até o prazo.
Em /orders/create/, /checkout/guest/ e /subscriptions/ o objeto do sucesso (pedido, assinatura) não existe quando o pagamento se confirma depois, então o erro é levantado na hora, sem consulta. Nos dois casos, não cobre de novo: aguarde o webhook ou consulte a transação.
try:
vitrin.charges.create(...)
except VitrinServerError as e:
if isinstance(e.body, dict) and e.body.get("payment_status") == "processing":
# NÃO cobre de novo: aguarde o webhook de pagamento desta transação.
vitrin.charges.retrieve(e.body["transaction_id"])
No /charges/ e no /charges/charge-saved/, a Vitrin acha a cobrança pela idempotency_key antes de ir ao processador, então ali o SDK repete com segurança: a repetição devolve a mesma cobrança (ou o mesmo 502 incerto enquanto ela não tem desfecho), nunca uma segunda. Em qualquer outro endpoint a chave não impede o segundo efeito, e o SDK não repete.
Recusa não é erro de validação
A API usa 422 para duas situações opostas, e o SDK as separa em classes diferentes:
| Classe | O que aconteceu | O que fazer |
|---|---|---|
VitrinValidationError |
O payload foi rejeitado. Nada foi enviado ao banco. | Corrigir os campos (field_errors) e repetir. |
VitrinRefusedError |
A cobrança chegou ao emissor e ele negou. | Exibir customer_message; oferecer retry só se retryable. |
O discriminador é o código de retorno no corpo (return_code / abecs_code): ele só existe porque houve resposta de autorização.
except VitrinRefusedError as e:
mostrar_erro(e.customer_message) # texto pronto, em PT-BR
if e.retryable:
oferecer_nova_tentativa() # ex.: 51 saldo, 91 emissor fora do ar
else:
oferecer_outro_cartao() # ex.: 82 negada — o mesmo cartão vai negar de novo
log.info("recusa %s tx=%s", e.code, e.transaction_id)
transaction_id aponta para a Transaction failed que registra a tentativa — ela também aparece em GET /transactions/?status=failed e no painel.
Idempotência
Inclua idempotency_key em POSTs sensíveis. Se o request chegar duas vezes (retry de rede, deploy etc), a Vitrin reconhece pela chave e devolve a mesma resposta — sem cobrar duas vezes.
Vale em charges.create() e charges.create_with_saved_card(). A chave é escopada por organização (a sua nunca colide com a de outro lojista) e aceita até 128 caracteres.
Use um identificador seu e estável — número do pedido, id do carrinho. A proteção vem de a chave se repetir no retry; um UUID novo a cada tentativa não protege de nada.
vitrin.charges.create(
customer_id="cus_1",
amount=100,
billing_type="PIX",
idempotency_key=f"pedido-{order_id}", # único por pedido
)
Acesso bruto
data = vitrin.request("/some/path/", method="POST", body={"foo": "bar"})
Licença
MIT
Release files for vitrin 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vitrin-0.4.0.tar.gz | 40.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vitrin-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 59.6 kB
Release files / vitrin-0.4.0.tar.gz
| Download URL | vitrin-0.4.0.tar.gz |
|---|---|
| Size | 40.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
01104c6a3836b3fca42ff5bee5ee0461e536dcb88049f1f88bf057dc14fc0cd8
|
|
BLAKE2b-256 checksum How to use checksums |
8a1ba795510738339bc4d674efc17fb53d394540f3a7f9d855a8c2f438629e95
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / vitrin-0.4.0-py3-none-any.whl
| Download URL | vitrin-0.4.0-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f97b296a4dfe1c8a7e04f758b327a082d7ed585467e02a1ca1595d5758ef95e9
|
|
BLAKE2b-256 checksum How to use checksums |
13f3a589abec661f335e6be72eab504cac79a65f2a46ccd1b7d0ba1b384ff271
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|