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 é plano — event.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​