Pular para o conteúdo principal

SDK Python

pip install veridia

O pacote é veridia. Python 3.11 ou mais novo. Construído sobre httpx (com HTTP/2 habilitado) e Pydantic v2, e checado por tipos sob mypy --strict.

A superfície do SDK é snake_case (verification_id, fields_extracted) enquanto a API HTTP é camelCase. A conversão acontece na fronteira, então você escreve Python e o que trafega segue válido. Se você colar um dict camelCase direto da referência HTTP, isso também valida.

Criar um cliente

from veridia import VeridiaClient

client = VeridiaClient(api_key="qv_sec_...")

Todos os argumentos são keyword-only. Além de api_key: base_url (padrão https://api.xxuxe.online), timeout_s (30.0), user_agent, retry_policy, concurrency_limiter, circuit_breaker, telemetry, logger.

O cliente é um context manager, e fechá-lo libera o pool de conexões subjacente do httpx:

with VeridiaClient(api_key="qv_sec_...") as client:
...

Executar uma verificação

from veridia import VeridiaClient, keys_from

client = VeridiaClient(api_key="qv_sec_...")

# 1. Abra uma verificação e receba um slot de upload por imagem.
# Todo campo é opcional — o tenant vem da chave de API.
init = client.verify.init({
"user_ref": "user_42", # máx 128; ecoado de volta no webhook
"country": "PY", # ISO 3166-1 alpha-2, maiúsculas
"document_type": "dni", # apenas uma dica; o modelo de OCR decide
"submitted_full_name": "Ada Lovelace", # máx 255; comparação difusa
})

# init.verification_id → 'vf_...'
# init.expires_at → SEGUNDOS unix (um int), não uma string de data

# 2. Faça o PUT dos bytes. Precisa ser JPEG de verdade, no máximo 8 MB cada.
client.verify.upload(init.uploads.doc_front, Path("front.jpg").read_bytes())
client.verify.upload(init.uploads.doc_back, Path("back.jpg").read_bytes())
client.verify.upload(init.uploads.selfie, Path("selfie.jpg").read_bytes())

# 3. Enfileire. `keys` é obrigatório.
client.verify.submit(init.verification_id, keys_from(init))

Para um passaporte ou qualquer documento de face única, pule o verso e diga ao keys_from que não existe:

client.verify.upload(init.uploads.doc_front, front_bytes)
client.verify.upload(init.uploads.selfie, selfie_bytes)
client.verify.submit(init.verification_id, keys_from(init, doc_back=False))

O init sempre emite os três slots. Enviar uma key doc_back para um slot em que você nunca escreveu é rejeitado — que é exatamente o que doc_back=False existe para evitar, já que quem copia as keys à mão naturalmente copia as três.

Assinaturas dos métodos

verify.init(params: VerifyInitParams | None = None) -> VerifyInitResult
verify.upload(slot: PresignedUpload, content: bytes) -> None
verify.submit(
verification_id: str,
keys: VerifySubmitKeys,
*,
liveness_score: float | None = None,
) -> VerifySubmitResult
verify.status(verification_id: str) -> VerifyStatusResult

client.verify.init() sem argumentos é uma chamada válida.

O init aceita chaves desconhecidas sem reclamar, e o servidor também: a API valida com Zod, que remove chaves desconhecidas antes de validar. Um campo escrito errado não produz erro — ele é descartado silenciosamente. Não existe tenant_id, nem callback_url, nem metadata no init.

Por que keys é obrigatório

O submit precisa saber quais bytes armazenados são o documento e quais são a selfie. A key de cada slot é a única coisa que diz isso, e esquecê-las é a forma mais fácil de tomar um 400 do submit — um cuja mensagem não vai citar o que falta, porque o Zod removeu os campos desconhecidos primeiro e o corpo chegou parecendo vazio.

keys_from(init) as junta para que o caso comum não possa ser feito errado.

Uploads

upload() envia slot.headers literalmente. Esses headers não são decoração: eles carregam X-Veridia-Upload-Token, uma credencial de vida curta por verificação, e o endpoint responde 400 missing_upload_token sem ela. Sua chave de API não autentica esse endpoint de forma alguma.

upload() chama o httpx diretamente em vez de passar pelo cliente do SDK. Isso é deliberado — anexar sua chave de API não traz benefício nenhum e só amplia por onde ela circula, e a política de retentativa e idempotência ajustada para pequenas chamadas JSON é a política errada para um PUT binário de vários megabytes. Uma resposta não-2xx levanta RuntimeError, não um VeridiaError.

Ler o resultado

O submit retorna assim que o job é enfileirado; o pipeline leva cerca de 15 segundos. Prefira o webhook. Faça polling apenas se você não puder receber uma requisição de entrada.

status = client.verify.status(init.verification_id) # exige chave secreta

status.status # "queued" | "processing" | "completed" | "failed"
status.verdict # "approved" | "review" | "rejected" — None até completar

Uma chave publicável aqui retorna 401 secret_key_required, exposto como AuthError.

Ramifique pelo verdict, nunca pelo status:

if status.status in ("completed", "failed"):
match status.verdict:
case "approved": activate(user_id)
case "rejected": decline(user_id)
case "review": queue_for_human(user_id)
case None: handle_no_decision(status)

completed significa que o pipeline rodou, não que a pessoa passou — uma verificação rejeitada também é completed. Um veredicto None não é uma rejeição; significa que nenhuma decisão foi alcançada. E review é final: um humano precisa olhar. Fazer polling em um review esperando que ele se resolva espera para sempre.

Campos de VerifyStatusResult: verification_id, status, verdict, confidence, scores, flags, submitted_at, completed_at.

  • scores é um dict com chaves em snake_case: ocr_confidence, face_match, liveness, doc_quality, mrz_valid, name_match. Leia com .get() — o conjunto é aberto e liveness em particular pode estar ausente quando o pipeline não teve sinal de prova de vida (liveness).
  • flags é uma lista de dicts {"level": ..., "text": ...}, não uma lista de strings.

VerifySubmitResult carrega status_url, a URL absoluta do endpoint de status. Lê-la ainda exige uma chave secreta.

VerifyInitResult.liveness é um dict bruto, presente apenas quando você passou active_liveness: True. Ele é deixado sem modelagem de propósito: contém beacons de desafio a que um navegador precisa reagir em tempo real, o que um processo de servidor não consegue fazer. Repasse-o intacto para o front end que vai executá-lo.

Async

API idêntica com await na frente. Todo método tem um gêmeo assíncrono.

import asyncio
from veridia import AsyncVeridiaClient, keys_from

async def main() -> None:
async with AsyncVeridiaClient(api_key="qv_sec_...") as client:
init = await client.verify.init({"country": "PY", "user_ref": "user_42"})
await client.verify.upload(init.uploads.doc_front, front_bytes)
await client.verify.upload(init.uploads.selfie, selfie_bytes)
await client.verify.submit(
init.verification_id, keys_from(init, doc_back=False)
)

asyncio.run(main())

Webhooks

from veridia import VeridiaClient, WebhookError

event = VeridiaClient.verify_webhook(
payload, # bytes BRUTOS — não json.loads(), não recodificados
signature_header, # valor do cabeçalho Veridia-Signature
secret,
tolerance_s=300, # padrão; não mexa
)

verify_webhook é um staticmethod — não precisa de instância do cliente. veridia.verify_signature é a mesma função sob seu nome de nível de módulo, e veridia.construct_event é um alias dela para quem está acostumado com o SDK da Stripe. As três levantam WebhookError diante de um cabeçalho malformado, de uma assinatura que não bate, de um timestamp expirado ou de um corpo impossível de parsear.

O segredo de assinatura não tem prefixo obrigatório. É o que você definir no painel (mínimo 24 caracteres), ou uma string hexadecimal de 48 caracteres se você deixar a Veridia gerar uma. Não vá procurar um prefixo whsec_ — isso é uma convenção da Stripe que aparece em alguns valores de exemplo aqui, mas não faz parte do formato.

FastAPI

from fastapi import FastAPI, Request, Response
from veridia import VeridiaClient, WebhookError

app = FastAPI()

@app.post("/webhooks/veridia")
async def veridia_webhook(request: Request) -> Response:
try:
event = VeridiaClient.verify_webhook(
await request.body(), # bytes brutos
request.headers.get("Veridia-Signature", ""),
settings.VERIDIA_WEBHOOK_SECRET,
)
except WebhookError:
return Response(status_code=400)

# A entrega é ao menos uma vez. Deduplique por event.id — ele é estável
# entre retentativas — e confirme a duplicata para que ela pare de ser reenviada.
if already_processed(event.id):
return Response(status_code=200)

if event.type == "verification.approved":
activate_account(event.user_ref)
elif event.type == "verification.review_required":
queue_for_manual_review(event.user_ref)
elif event.type == "verification.rejected":
decline(event.user_ref)

mark_processed(event.id)
return Response(status_code=200)

Use await request.body(). request.json() te dá um objeto já parseado, e reserializá-lo para verificar o MAC reordena chaves e muda o espaçamento — o digest não vai bater.

O dispatcher dá 10 segundos ao seu endpoint. Responda 2xx rapidamente e faça o trabalho lento depois; qualquer outra coisa é retentada 6 vezes ao longo de cerca de 12,6 minutos, e depois fica parada como failed para um operador reenfileirar pelo painel.

Campos do evento

O payload é planoevent.verdict, não event.data["verdict"].

CampoTipoNotas
idstrevt_<hex>. Estável entre retentativas — deduplique por este
typestrUm de exatamente três valores (abaixo)
created_atintSegundos unix, não uma string ISO
tenant_idstr
verification_idstr
verdictstrapproved / review / rejected
confidencefloat | None
user_refstr | NoneO que você passou no init; None se nunca definiu um
scoresdict[str, float]chaves em snake_case, como acima
flagslist[dict]
fields_extracteddictPII de identidade — veja abaixo
latency_msint | None

Exatamente três tipos de evento: verification.approved, verification.review_required, verification.rejected. Não existe evento created nem expired — um integrador esperando por um deles espera para sempre.

event.fields_extracted contém nome completo, número do documento e data de nascimento. É por isso que a URL do webhook precisa ser https, e por que o payload não deve ser escrito literalmente nos logs da aplicação.

Se você nunca definiu user_ref no init, só o verification_id correlaciona o evento de volta a um usuário — e o caminho de polling não retorna user_ref de jeito nenhum, então guarde você mesmo o mapeamento.

Janela de replay

tolerance_s tem padrão 300 e está correto. O dispatcher reassina a cada retentativa, então até a última retentativa chega com um t fresco. Ampliar a janela não traz benefício nenhum e alonga o período em que uma entrega capturada pode ser reproduzida contra você.

Tratamento de erros

Toda exceção herda de VeridiaError.

from veridia import (
VeridiaError,
AuthError, # 401, 403
ValidationError, # 400, 422
RateLimitError, # 429 — tem .retry_after_ms
ServerError, # 5xx (retentado automaticamente)
NetworkError, # problemas de conexão (retentado automaticamente)
TimeoutError, # timeout da requisição (retentado automaticamente)
CircuitBreakerOpenError, # o breaker está OPEN
WebhookError, # problemas de assinatura / payload
)

try:
init = client.verify.init({"country": "PY"})
except RateLimitError as e:
time.sleep((e.retry_after_ms or 1000) / 1000)
except AuthError:
... # chave inválida, revogada, ou da família errada
except VeridiaError as e:
log.error("veridia failed: %s (request_id=%s)", e.message, e.request_id)

Cite e.request_id nos tickets de suporte.

veridia.TimeoutError sombreia o builtin de mesmo nome se você o importar puro. Importe o módulo ou use um alias se isso for relevante na sua base de código.

Resiliência

Quatro camadas envolvem toda chamada à API, todas substituíveis:

from veridia import (
VeridiaClient, RetryPolicy, CircuitBreaker, ConcurrencyLimiter, Telemetry, StdLogger,
)

client = VeridiaClient(
api_key="qv_sec_...",
retry_policy=RetryPolicy(
max_attempts=5, base_delay_ms=500, max_delay_ms=10_000, factor=2.5, jitter=True,
),
circuit_breaker=CircuitBreaker(threshold=10, reset_ms=60_000),
concurrency_limiter=ConcurrencyLimiter(max_concurrent=20),
telemetry=Telemetry(hooks={"on_request": on_request, "on_error": on_error}),
logger=StdLogger(),
)

Padrões: ConcurrencyLimiter(10), CircuitBreaker(5, 30_000), telemetria e logger no-op. jitter=True é o padrão full-jitter da AWS, que evita uma debandada sincronizada de retentativas entre seus workers.

Todo POST/PUT/PATCH recebe um Idempotency-Key gerado, para que um submit retentado não enfileire o job duas vezes. Como observado acima, nada disso cobre o upload().

Para onde ir agora