SDK PHP
composer require veridia/veridia-php
O pacote é veridia/veridia-php. PHP 8.2 ou mais novo, com ext-curl, ext-json, ext-openssl, ext-mbstring e ext-hash. O Composer traz junto Guzzle 7, psr/log, psr/http-message e ramsey/uuid.
A análise estática roda no nível 9 do PHPStan com regras estritas. Os tipos são classes readonly e enums com valor de apoio em todo lugar.
Criar um cliente
use Veridia\VeridiaClient;
// $secretKey vem do seu cofre de segredos — nunca do código-fonte.
$client = new VeridiaClient(apiKey: $secretKey);
O construtor também recebe options (um HttpClientOptions), baseUrl (padrão VeridiaClient::DEFAULT_BASE_URL) e guzzle (injete um cliente Guzzle pré-configurado).
Este SDK roda em um servidor, então os exemplos usam uma chave secreta. Ele funciona com uma chave publicável até o getStatus(), que exige uma secreta — veja tipos de chave.
Executar uma verificação
O fluxo é init → PUT de cada imagem → submit → descobrir o desfecho.
O init te entrega três slots de upload. Cada slot carrega uma key opaca; o submit recebe essas keys de volta, e essa é a única coisa que liga os bytes armazenados à verificação.
1. Init
use Veridia\Types\DocumentType;
use Veridia\Types\VerifyInitParams;
$init = $client->verify->init(new VerifyInitParams(
documentType: DocumentType::DNI, // apenas uma dica; o OCR decide sozinho
userRef: 'user_42', // SEU id de usuário — ecoado de volta no webhook
country: 'PY', // ISO 3166-1 alpha-2, MAIÚSCULAS
submittedFullName: 'Ada Lovelace', // comparado de forma difusa com o documento
));
echo $init->verificationId; // vf_xxxxxxxxxxxxxxxx
echo date('c', $init->expiresAt); // expiresAt é em SEGUNDOS UNIX, não uma string de data
Todo parâmetro é opcional — $client->verify->init() é uma chamada válida, porque o tenant vem da chave de API, não do corpo. Não existe tenantId, nem callbackUrl, nem metadata no init; a URL do webhook é configurada uma vez por tenant no painel.
Defina userRef se você usa webhooks. É o único campo que liga um evento de volta a um usuário no seu próprio sistema.
2. Envie as imagens
$client->verify->upload($init->uploads->docFront, '/tmp/dni-front.jpg');
$client->verify->upload($init->uploads->docBack, '/tmp/dni-back.jpg');
$client->verify->upload($init->uploads->selfie, '/tmp/selfie.jpg');
upload() faz streaming a partir do disco. Para bytes que você já tem em memória — um frame enviado por POST pelo navegador, digamos — use uploadBytes($slot, $bytes).
Ambos repassam os headers do slot literalmente, e é isso que faz o upload funcionar: esses headers carregam X-Veridia-Upload-Token, uma credencial de vida curta por verificação com a qual o endpoint se autentica, mais o Content-Type que ele valida. Os bytes precisam ser um JPEG de verdade (o endpoint checa o número mágico) e ter no máximo 8 MB.
Os uploads contornam deliberadamente o cliente HTTP do SDK. O endpoint de upload aceita o token de upload, e não sua chave de API, então anexar um cabeçalho Authorization não traz benefício nenhum e só amplia por onde sua chave 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. Um não-2xx vindo do host de armazenamento lança NetworkException; um arquivo ilegível lança InvalidArgumentException.
3. Submit
use Veridia\Types\VerifySubmitKeys;
use Veridia\Types\VerifySubmitParams;
$submitted = $client->verify->submit(new VerifySubmitParams(
verificationId: $init->verificationId,
keys: VerifySubmitKeys::fromInit($init),
));
echo $submitted->status->value; // "queued" — o job foi enfileirado, nada além disso
VerifySubmitKeys::fromInit() junta as keys a partir do resultado do init para que você não possa esquecê-las. Omiti-las é a forma mais fácil de tomar um 400 do submit, e o erro não vai citá-las: o servidor valida com Zod, que remove chaves desconhecidas antes de validar, então um corpo que falava de URLs chega parecendo simplesmente vazio.
VerifySubmitParams aceita um array metadata, mas ele é descartado na borda — não chega ao pipeline e não está presente no payload do webhook. Use userRef para correlação em vez disso.
O submit retorna assim que o job é enfileirado. O pipeline leva cerca de 15 segundos depois disso, e nada nesta resposta diz coisa alguma sobre a pessoa.
4. Descobrir o desfecho
Os webhooks são o canal recomendado. A Veridia empurra o desfecho no momento em que ele existe: nenhuma requisição de polling, nenhum timeout para ajustar, e nenhum intervalo de polling que possa ser mais lento que a resposta. Veja Webhooks abaixo.
O polling é o plano B para ambientes que não conseguem receber uma requisição de entrada. Ele precisa de uma chave secreta:
use Veridia\Types\VerifyVerdict;
$final = $client->verify->waitForTerminal(
verificationId: $init->verificationId,
pollIntervalMs: 2_000,
timeoutSeconds: 120,
);
match ($final->verdict) {
VerifyVerdict::APPROVED => admitUser($final),
VerifyVerdict::REVIEW => queueForHumanReview($final),
VerifyVerdict::REJECTED => declineUser($final),
null => declineUser($final), // pipeline falhou: nenhuma decisão alcançada
};
waitForTerminal() lança uma RuntimeException se o timeout se esgotar antes. Uma leitura acontece antes da primeira checagem de prazo, então mesmo um timeout de um segundo devolve uma resposta em vez de lançar imediatamente. Para uma única leitura não bloqueante, use $client->verify->getStatus($verificationId).
status não é verdict
Esta é a única coisa que você precisa acertar. Uma verificação tem dois eixos independentes:
| Campo | Pergunta que responde | Valores |
|---|---|---|
$status | O pipeline rodou? | queued processing completed failed |
$verdict | A pessoa passou? | approved review rejected — null até completar |
completed significa que o pipeline chegou a uma conclusão. Não significa que a pessoa passou: uma verificação aprovada, uma que exige revisão e uma rejeitada são todas completed.
// ERRADO — isto admite todo candidato rejeitado.
if (VerifyState::COMPLETED === $result->status) {
admitUser($user);
}
// Correto — faça polling no status, decida pelo verdict.
if ($result->status->isTerminal()) {
match ($result->verdict) { /* ... */ };
}
Mais duas armadilhas na mesma área:
- Um veredicto
nullnão é uma rejeição, e certamente não é uma aprovação. Significa que nenhuma decisão foi alcançada — ou o pipeline ainda está rodando, ou elefailed. reviewé final. Significa que um humano precisa olhar, não que o resultado ainda está se assentando. Fazer polling em um veredictoreviewesperando que ele se resolva espera para sempre. Escreva um ramo explícito para ele; umif/elsede dois braços o dobra silenciosamente para o lado em que oelsecalhar de estar.
VerifyStatusResult também carrega $confidence, $scores (um array<string, float> aberto com chaves em snake_case — ocr_confidence, face_match, liveness, doc_quality, mrz_valid, name_match), $flags (uma lista de objetos {level, text}, não de strings), $submittedAt e $completedAt.
Os dois enums falham ruidosamente diante de um valor que não reconhecem, em vez de degradar para null. Para o verdict essa é a direção segura: null legitimamente significa "ainda rodando", então coagir silenciosamente um desfecho desconhecido para ele te entregaria um valor que se lê como "ainda não decidido" para uma verificação que, na verdade, foi decidida.
Documentos de face única (passaporte)
O init sempre emite os três slots, mas um passaporte não tem verso. Envie duas imagens e diga ao fromInit() que não há verso:
$init = $client->verify->init(new VerifyInitParams(
documentType: DocumentType::PASSPORT,
userRef: 'user_42',
));
$client->verify->upload($init->uploads->docFront, '/tmp/passport.jpg');
$client->verify->upload($init->uploads->selfie, '/tmp/selfie.jpg');
// $init->uploads->docBack simplesmente fica sem ser escrito.
$submitted = $client->verify->submit(new VerifySubmitParams(
verificationId: $init->verificationId,
keys: VerifySubmitKeys::fromInit($init, docBack: false),
));
As keys que você envia precisam descrever exatamente os bytes que você de fato enviou. Mandar uma key docBack para um slot em que você nunca fez PUT é rejeitado — que é o que docBack: false existe para evitar, já que quem copia as keys à mão naturalmente copia as três.
Webhooks
A Veridia assina toda entrega. Verifique a assinatura antes de confiar em qualquer coisa do corpo.
Os cabeçalhos
Veridia-Signature: t=1753000000,v1=<hex minúsculo de 64 caracteres>
Veridia-Event: verification.approved
Existe um único cabeçalho de assinatura e o timestamp vive dentro dele. Não existe X-Veridia-Signature nem um X-Veridia-Timestamp separado; código que lê esses está lendo cabeçalhos que nunca são enviados. Em PHP o cabeçalho chega como $_SERVER['HTTP_VERIDIA_SIGNATURE'].
O MAC é HMAC-SHA256 sobre os bytes "<t>." + rawBody. Passe o corpo bruto — file_get_contents('php://input'), nunca um recodificado. Reserializar JSON reordena chaves e muda o espaçamento, o que muda o digest.
Os três tipos de evento
Exatamente estes, e nenhum outro:
verification.approvedverification.review_requiredverification.rejected
Não existe verification.created nem verification.expired — um webhook dispara apenas quando há um desfecho a reportar. Cada evento carrega um campo verdict com a mesma informação, então ramificar por qualquer um dos dois está bem aqui (diferente de ramificar pelo status do pipeline).
A entrega é ao menos uma vez — deduplique por id
A Veridia retenta até 6 vezes ao longo de cerca de 12,6 minutos. As retentativas não são só para handlers que falharam: o caso comum é um handler que teve sucesso e cujo 200 se perdeu num timeout. Ou seja, você vai ver o mesmo evento duas vezes. O id (evt_<hex>) é estável entre retentativas justamente para que você possa deduplicar por ele; sem essa checagem, uma aprovação provisiona o mesmo usuário várias vezes.
use Veridia\Errors\WebhookException;
use Veridia\Types\EventType;
use Veridia\Webhooks\WebhookHandler;
$handler = new WebhookHandler(secret: $webhookSigningSecret);
try {
$event = $handler->verify(
payload: file_get_contents('php://input') ?: '',
signatureHeader: $_SERVER['HTTP_VERIDIA_SIGNATURE'] ?? '',
);
} catch (WebhookException $e) {
http_response_code(400);
exit;
}
// Deduplique ANTES de agir, e confirme a duplicata com um 200 para que ela pare de ser reenviada.
if ($store->alreadyProcessed($event->id)) {
http_response_code(200);
exit;
}
match ($event->type) {
EventType::VERIFICATION_APPROVED => admitUser($event->userRef),
EventType::VERIFICATION_REVIEW_REQUIRED => queueForHumanReview($event->userRef),
EventType::VERIFICATION_REJECTED => declineUser($event->userRef),
};
$store->markProcessed($event->id);
http_response_code(200);
O dispatcher dá 10 segundos ao seu endpoint. Responda 2xx rapidamente e faça o trabalho lento depois; qualquer outra coisa é retentada, e depois fica parada como failed para um operador reenfileirar pelo painel.
O payload
Plano — $event->verdict, $event->verificationId, $event->userRef, $event->confidence, $event->scores, $event->flags, $event->fieldsExtracted. Não existe envelope data para desembrulhar. $event->createdAt é em segundos unix como inteiro, e $event->latencyMs é quanto tempo o pipeline levou.
$event->userRef é null se você nunca definiu um no init, e nesse caso só o $verificationId correlaciona de volta a um usuário.
$event->fieldsExtracted contém nome, número do documento e data de nascimento lidos do documento de identidade. Esses são exatamente os dados que seus usuários confiaram a você, então o endpoint precisa ser https://, e o payload não deve ser escrito literalmente nos logs da aplicação.
Janela de replay
Assinaturas são rejeitadas assim que ficam mais velhas que toleranceSeconds, cujo padrão é 300. Deixe assim. O dispatcher reassina a cada retentativa, então até a última retentativa chega com um t fresco — o padrão cobre com folga todo o cronograma de retentativas. Ampliá-lo não traz benefício nenhum e alonga a janela em que uma entrega capturada pode ser reproduzida contra você.
A checagem de frescor é unilateral de propósito: só um timestamp antigo é risco de replay. Um receptor com relógio atrasado em relação ao do emissor é rotina em VMs sem sincronização, e rejeitar ali culparia "velho demais" por um problema de relógio, apontando a depuração exatamente para a direção errada.
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.
Tratamento de erros
use Veridia\Errors\AuthException;
use Veridia\Errors\CircuitBreakerOpenException;
use Veridia\Errors\NetworkException;
use Veridia\Errors\RateLimitException;
use Veridia\Errors\ServerException;
use Veridia\Errors\TimeoutException;
use Veridia\Errors\ValidationException;
use Veridia\Errors\VeridiaException; // classe base, estende RuntimeException
try {
$status = $client->verify->getStatus($verificationId);
} catch (AuthException $e) {
// 401 / 403. No getStatus especificamente, cheque o corpo da resposta:
// $e->details['code'] === 'secret_key_required' significa que a chave é válida
// mas publicável, e este endpoint precisa de uma qv_sec_.
} catch (ValidationException $e) {
// 400 / 422 — o corpo da requisição estava errado
} catch (RateLimitException $e) {
// 429 — retryAfterMs vem do cabeçalho Retry-After, quando o servidor envia um
usleep(($e->retryAfterMs ?? 1000) * 1000);
} catch (CircuitBreakerOpenException $e) {
// breaker acionado: a Veridia está falhando e o SDK parou de tentar. Falhe esta
// requisição rápido em vez de enfileirar atrás de uma indisponibilidade.
} catch (VeridiaException $e) {
// pega-tudo
}
Toda exceção carrega $e->requestId (cite-o nos tickets de suporte), $e->statusCode, $e->details (o corpo da resposta parseado) e $e->toArray() para logging estruturado.
Resiliência
Retentativa, circuit breaker e limite de concorrência vêm ligados por padrão (HttpClientOptions::defaults()). Substitua qualquer um deles, ou passe null para desativar uma camada:
use Veridia\Http\HttpClientOptions;
use Veridia\Resilience\CircuitBreaker;
use Veridia\Resilience\ConcurrencyLimiter;
use Veridia\Resilience\RetryPolicy;
$opts = new HttpClientOptions(
apiKey: $secretKey,
baseUrl: VeridiaClient::DEFAULT_BASE_URL,
timeoutSeconds: 60.0,
connectTimeoutSeconds: 15.0,
retryPolicy: new RetryPolicy(
maxAttempts: 5,
baseDelayMs: 100,
maxDelayMs: 10_000,
factor: 2.0,
jitter: true, // full-jitter da AWS — evita debandada sincronizada de retentativas
),
circuitBreaker: new CircuitBreaker(threshold: 10, resetMs: 60_000),
limiter: new ConcurrencyLimiter(maxConcurrent: 25),
);
$client = new VeridiaClient(apiKey: $secretKey, options: $opts);
Todo POST recebe um X-Idempotency-Key novo (idem_<32 hex>) a menos que você forneça um, para que um submit retentado não enfileire o job duas vezes.
Essas camadas cobrem apenas as chamadas à API da Veridia. upload() e uploadBytes() ficam fora dessa maquinaria, como descrito acima.
Telemetria e logging
Implemente TelemetryHook e registre-o em um TelemetryDispatcher passado como telemetry: em HttpClientOptions. Eventos emitidos: request.started, request.succeeded, request.failed, retry.scheduled, circuit.state_changed. Um hook que lança exceção é capturado e logado como aviso, nunca relançado — um backend de métricas quebrado não pode derrubar uma verificação.
O logging é PSR-3: passe qualquer LoggerInterface como logger:. Funciona com Monolog, Symfony Logger, Laravel Log.
Referência de chamadas
| Chamada do SDK | HTTP | Chave necessária |
|---|---|---|
verify->init(?VerifyInitParams) | POST /v1/verify/init | publicável ou secreta |
verify->upload(PresignedUpload, string $path) | PUT <url do slot> | nenhuma (o token está no slot) |
verify->uploadBytes(PresignedUpload, string $bytes) | PUT <url do slot> | nenhuma |
verify->submit(VerifySubmitParams) | POST /v1/verify/submit | publicável ou secreta |
verify->getStatus(string $id) | GET /v1/verify/{id} | somente secreta |
verify->waitForTerminal(string $id, ...) | GET /v1/verify/{id} (com polling) | somente secreta |
Para onde ir agora
- Webhooks — verificação de assinatura, tipos de evento, retentativas
- Referência da API — os endpoints que este SDK encapsula
- SDK JavaScript · SDK Python · SDK Flutter