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
Metadata
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 | Provenance | |
|---|---|---|---|---|
| vitrin-0.4.0.tar.gz | 40.9 kB | Without provenance | 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
|
Security for vitrin 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.