SDK JavaScript / TypeScript
npm install @veridia/sdk
O pacote é @veridia/sdk. Node 18.17 ou mais novo — o módulo de webhook usa node:crypto.
Escrito em TypeScript com strict: true. Publica builds ESM e CJS mais os arquivos de declaração.
Criar um cliente
Forneça exatamente um entre publishableKey ou secretKey. Passar ambos, ou nenhum, lança erro na construção.
import { VeridiaClient } from '@veridia/sdk';
// Navegador / cliente não confiável
const client = new VeridiaClient({ publishableKey: 'qv_pub_...' });
// Seu servidor
const server = new VeridiaClient({ secretKey: process.env.VERIDIA_SECRET_KEY! });
Outras opções: baseUrl (padrão https://api.xxuxe.online), timeoutMs (padrão 30000), retryOptions, circuitBreakerOptions, semaphoreOptions, logger, telemetry, fetchImpl.
Executar uma verificação
// 1. Inicie. Todo campo é opcional — o tenant vem da chave.
const init = await client.verify.init({
userRef: 'your-internal-user-id', // máx 128; ecoado de volta no webhook
country: 'PY', // ISO 3166-1 alpha-2, maiúsculas
documentType: 'dni', // apenas uma dica; o modelo de OCR decide
submittedFullName: 'Ada Lovelace', // máx 255; comparado de forma difusa com o documento
});
// init.verificationId → 'vf_...'
// init.uploads.docFront.url → faça o PUT dos bytes aqui
// init.uploads.docFront.key → devolva em submit({ keys })
// init.expiresAt → SEGUNDOS unix; os slots param de funcionar depois disso
// 2. Envie as capturas.
await client.verify.uploadDocFront(init.uploads, docFrontBlob);
await client.verify.uploadDocBack(init.uploads, docBackBlob);
await client.verify.uploadSelfie(init.uploads, selfieBlob);
// 3. Submeta. `keys` é obrigatório.
await client.verify.submit({
verificationId: init.verificationId,
keys: client.verify.keysFrom(init),
});
Para um passaporte não existe verso: pule uploadDocBack e chame client.verify.keysFrom(init, { docBack: false }). Enviar uma key docBack para bytes que você nunca enviou é rejeitado.
documentType, não docTypeAlguns comentários de código dentro do pacote ainda mostram docType. Essa grafia está errada, e ela falha silenciosamente: a API valida com Zod, que remove chaves desconhecidas antes de validar. Um campo não reconhecido não produz erro — ele é simplesmente descartado, e a dica de OCR que você achou que enviou nunca chega.
O mesmo vale para qualquer campo que você invente. tenantId, callbackUrl e metadata não existem no init; enviá-los retorna 200 e não muda nada.
Por que keys é obrigatório
O submit precisa saber quais bytes armazenados são a frente do documento e quais são a selfie. A key de cada slot de upload é a única coisa que diz isso. Omita-as e você recebe um 400 cuja mensagem não vai citar o campo faltante — o Zod remove as chaves desconhecidas primeiro, então um corpo cheio de URLs chega ao validador parecendo vazio.
keysFrom existe para que o caso comum não possa ser feito errado.
Uploads
uploadFile (e os três wrappers em volta dele) repassam presigned.headers literalmente e usam presigned.method. É isso que faz o upload funcionar: esses headers carregam X-Veridia-Upload-Token, uma credencial de vida curta por verificação, e o endpoint de upload se autentica com ela, não com sua chave de API.
Se você escrever o PUT você mesmo, repasse o objeto headers inteiro. Não o reconstrua, não envie só Content-Type, e não anexe um cabeçalho Authorization — ele não faz nada ali.
Os uploads contornam deliberadamente o cliente HTTP resiliente do SDK. Eles chamam fetch diretamente, então as camadas de retentativa, circuit breaker e idempotência descritas abaixo não se aplicam a eles, e uma resposta não-2xx lança um Error simples, não um VeridiaError.
Ler o resultado
O submit retorna assim que o job é enfileirado. O pipeline leva cerca de 15 segundos.
const result = await server.verify.getStatus(verificationId);
// result.status → 'queued' | 'processing' | 'completed' | 'failed'
// result.verdict → 'approved' | 'review' | 'rejected' (ausente até completar)
Isso exige uma chave secreta. Uma chave publicável retorna 401 secret_key_required.
poll(verificationId, options?) faz loop até o status ser terminal, com backoff de 1,5× a partir de intervalMs (padrão 1000) até maxIntervalMs (padrão 5000), desistindo após timeoutMs (padrão 120000) com um Error lançado. Aceita um AbortSignal.
const final = await server.verify.poll(verificationId, { timeoutMs: 60_000 });
switch (final.verdict) {
case 'approved': await activate(userId); break;
case 'rejected': await decline(userId); break;
case 'review': await queueForHuman(userId); break;
default: await handleNoDecision(final); // failed, ou sem veredicto
}
Ramifique pelo verdict, nunca pelo status — uma verificação rejeitada também é completed. Veja status não é verdict.
O polling nunca enxerga o desfecho de um caso revisado por um humano, que pode chegar horas depois. Os webhooks sim.
VerifyStatusResponse não batem com o que vem na redeVerifyStatusResponse declara breakdown?: ConfidenceBreakdown e flags?: readonly string[]. Nenhum dos dois corresponde ao que a API realmente retorna:
- A API retorna
scores, nãobreakdown— um objeto com chaves em snake_case:ocr_confidence,face_match,liveness,doc_quality,mrz_valid,name_match.result.breakdownéundefined.livenesspode sernull. flagsé uma lista de objetos{ level, text }, não de strings.
Ler qualquer um deles pelo tipo declarado não te dá nada, e um limiar escrito contra result.breakdown.faceMatch compara undefined — que é false em toda comparação, então a checagem nunca dispara e todo mundo passa. Leia-os da resposta com um cast explícito até os tipos serem corrigidos:
const raw = result as unknown as {
scores?: Record<string, number | null>;
flags?: Array<{ level: string; text: string }>;
};
const faceMatch = raw.scores?.face_match;
Os nomes dos campos e a semântica estão documentados na referência do endpoint de status.
VerifyInitResponse também não modela o bloco liveness retornado quando você passa activeLiveness: true. Esse bloco é exclusivo de navegador — ele carrega beacons de desafio a que um servidor não consegue responder — então leia-o da resposta bruta e entregue ao seu front end.
Webhooks
Importe o verificador do subcaminho /webhooks. A raiz do pacote reexporta apenas os tipos de webhook, não a função:
import { verifyWebhookSignature } from '@veridia/sdk/webhooks';
import { verifyWebhookSignature } from '@veridia/sdk' não compila e é undefined em tempo de execução.
A assinatura
verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string,
options?: { toleranceSec?: number; now?: number },
): WebhookEvent
Quatro argumentos posicionais — não um objeto de opções. Ela retorna o evento parseado e lança em caso de falha. Nunca retorna um booleano, então um if (!isValid) escrito contra ela não rejeita nada.
Falhas lançam um entre WebhookSignatureError, WebhookTimestampError ou WebhookPayloadError, todos exportados do mesmo subcaminho.
Express
import express from 'express';
import {
verifyWebhookSignature,
WebhookSignatureError,
WebhookTimestampError,
WebhookPayloadError,
} from '@veridia/sdk/webhooks';
const app = express();
// Corpo bruto, não express.json() — reserializar o JSON muda o digest.
app.post(
'/webhooks/veridia',
express.raw({ type: 'application/json' }),
async (req, res) => {
let event;
try {
event = verifyWebhookSignature(
req.body.toString('utf8'), // express.raw devolve um Buffer
req.header('Veridia-Signature') ?? '', // sem prefixo X-
process.env.VERIDIA_WEBHOOK_SECRET!,
);
} catch (err) {
if (
err instanceof WebhookSignatureError ||
err instanceof WebhookTimestampError ||
err instanceof WebhookPayloadError
) {
return res.sendStatus(400);
}
throw err;
}
// A entrega é ao menos uma vez. Deduplique por event.id, que é estável
// entre retentativas, e confirme a duplicata para que ela pare de ser reenviada.
if (await alreadyProcessed(event.id)) return res.sendStatus(200);
// Responda rápido, depois faça o trabalho lento: o dispatcher expira em 10s.
res.sendStatus(200);
switch (event.type) {
case 'verification.approved': await activate(event.verificationId); break;
case 'verification.rejected': await decline(event.verificationId); break;
case 'verification.review_required': await queueForHuman(event.verificationId); break;
}
await markProcessed(event.id);
},
);
Três coisas que isso corrige, cada uma capaz de quebrar um handler silenciosamente:
payloadprecisa ser uma string.express.raw()te entrega umBuffer; passá-lo lançaWebhookPayloadErrorem toda entrega. Chame.toString('utf8').- O cabeçalho é
Veridia-Signature.req.headers['x-veridia-signature']éundefined, e o verificador então lança por cabeçalho ausente. - O discriminador é
event.type. Não existeevent.event; um switch nele cai nodefaultpara sempre, retornando200sem processar nada.
Janela de replay
toleranceSec tem padrão 300 e esse é o valor certo. O dispatcher reassina a cada retentativa, então a sexta retentativa chega com um t fresco — você não precisa ampliar a janela para sobreviver ao cronograma de retentativas, e ampliá-la só alonga o período em que uma entrega capturada pode ser reproduzida contra você.
A verificação de assinatura usa uma checagem de comprimento antes de timingSafeEqual, então um v1= truncado retorna um WebhookSignatureError limpo em vez de lançar RangeError de dentro do seu handler.
Tratamento de erros
Todo erro da API é uma instância de VeridiaError ou de uma de suas subclasses. Restrinja com instanceof:
import {
VeridiaError,
VeridiaAuthError,
VeridiaValidationError,
VeridiaCreditError,
VeridiaResourceError,
VeridiaRateLimitError,
VeridiaServerError,
VeridiaNetworkError,
isVeridiaError,
} from '@veridia/sdk';
try {
await client.verify.init({ country: 'PY' });
} catch (err) {
if (err instanceof VeridiaRateLimitError) {
// 429 — err.retryAfter está em SEGUNDOS, vindo do cabeçalho Retry-After
} else if (err instanceof VeridiaAuthError) {
// 401 / 403 — chave inválida, revogada, ou da família errada
} else if (err instanceof VeridiaCreditError) {
// 402 — o tenant está sem créditos
} else if (err instanceof VeridiaServerError || err instanceof VeridiaNetworkError) {
// transitório; já foi retentado pelo SDK antes de chegar em você
} else if (isVeridiaError(err)) {
// qualquer outra coisa vinda da API
}
}
isAuthError e isRateLimitError não existem. O único type guard exportado é isVeridiaError, útil quando o instanceof falha através de fronteiras de bundle.
A classe é escolhida a partir do status HTTP: 401/403 → auth, 400/422 → validação, 402 → crédito, 404/410/423 → recurso, 429 → rate limit, 5xx → servidor.
err.code não é o código de erro da APIA camada HTTP procura um campo code no corpo do erro, mas a API retorna { error, message, requestId }. O code que ela procura nunca está lá, então err.code cai para 'unknown_error' em todo erro da API.
Ramifique pela classe do erro ou por err.statusCode. Um switch (err.code) contra os códigos documentados da API — secret_key_required, insufficient_credits, rate_limited — não casa com nada.
err.message de fato carrega a mensagem da API, e err.statusCode é preciso.
Resiliência
Retentativa com backoff exponencial e jitter, um circuit breaker e um semáforo de concorrência envolvem toda chamada à API. Os três são configuráveis na construção:
const client = new VeridiaClient({
secretKey: process.env.VERIDIA_SECRET_KEY!,
retryOptions: { maxAttempts: 3 },
circuitBreakerOptions: { threshold: 5, resetMs: 30_000 },
semaphoreOptions: { maxConcurrent: 5 },
telemetry: {
onRequest: (ctx) => metrics.increment('veridia.request', { method: ctx.method }),
onResponse: (ctx) => metrics.timing('veridia.duration', ctx.durationMs),
onError: (ctx) => Sentry.captureException(ctx.error),
},
logger: pino(),
});
client.getStats() retorna o estado atual do breaker e a contagem de requisições em voo.
Todo POST, PUT e PATCH recebe um cabeçalho Idempotency-Key gerado automaticamente, a não ser que você passe um, para que um submit retentado não enfileire o job duas vezes. Como observado acima, nada disso cobre os uploads de imagem.
VeridiaCircuitOpenError é lançado quando o breaker está aberto. Ele é exportado da raiz do pacote.
Tipos exportados
import type {
DocumentType, // 'dni' | 'passport' | 'drivers_license' | 'national_id' | 'other'
Verdict, // 'approved' | 'review' | 'rejected'
VerificationStatus, // 'queued' | 'processing' | 'completed' | 'failed'
VerifyInitParams,
VerifyInitResponse,
VerifySubmitParams,
VerifySubmitResponse,
VerifyStatusResponse,
PresignedUpload,
WebhookEvent,
WebhookEventType,
} from '@veridia/sdk';
VerificationResult e VerifyState não são exportados e nunca foram — os nomes que você quer são VerifyStatusResponse e VerificationStatus.
Para onde ir agora
- Webhooks — verificação de assinatura, tipos de evento, retentativas
- Referência da API — os endpoints que este SDK encapsula
- SDK Python · SDK PHP · SDK Flutter