Integrando com o NexSign
Status: este documento descreve o contrato-alvo da Fase 1. Algumas peças (provisionamento de API keys via painel, SDK Node oficial, webhook dispatcher) ainda estão em construção — onde houver divergência com a Fase 0 está marcado com
🚧 Fase 1.
NexSign é o serviço de assinatura digital da NexUnio. Aplicações clientes (seu produto) integram via:
- REST API autenticada por API key (lado servidor).
- Magic link + JWT efêmero para o signatário (lado navegador).
- Webhooks HTTP com assinatura HMAC para eventos do envelope.
Não há SDK obrigatório — a API é HTTP + JSON. Um SDK Node oficial
(@nexsign/sdk-node) está planejado para Fase 1.
1. Conceitos
| Termo | O que é |
|---|---|
| App | Aplicação cliente (seu produto). Possui um app_id (UUID) e uma ou mais API keys. |
| Envelope | Unidade de assinatura. Agrega 1+ signatários, o documento (HTML → PDF), uma cláusula de aceite e a hash chain de eventos. |
| Signer | Signatário do envelope. Identificado por email; opcionalmente correlacionado ao seu app via external_ref. |
subject_type / subject_id | Discriminadores opacos que você escolhe. NexSign não os interpreta — servem para você correlacionar o envelope com o recurso original (contrato, NDA, aprovação). |
subject_metadata | JSONB livre que viaja no create. Carrega title, html_template, template_vars, clause_html e qualquer extra que você queira ver de volta nos webhooks/evidence. |
external_subject_ref | String "humana" para correlação em logs (ex.: nexarq:contract:abc123). |
| Evidence package | PDF gerado on-demand contendo dossiê do envelope (eventos, IPs, TSA tokens, hash chain). |
| Sealed PDF | PDF final com selo PAdES B-LT + carimbo do tempo (TSA ICP-Brasil quando disponível). |
NexSign não tem users ou tenants — o cliente é responsável por
gerenciar identidade dos signatários no seu próprio domínio.
2. Autenticação
2.1. API key (servidor → NexSign)
Toda chamada server-side passa por header:
X-API-Key: nxs_live_••••••••••••••••
- Distribuída pelo painel admin (🚧 Fase 1) ou via env
NEXSIGN_DEV_API_KEYem ambiente local. - Escopos disponíveis (🚧 Fase 1, hoje aceita qualquer escopo):
envelopes:readenvelopes:writeenvelopes:cancel
- Nunca exponha a API key no navegador. Toda criação/leitura de envelope deve ocorrer no seu backend.
Resposta a falha:
HTTP/1.1 401 Unauthorized
{ "error": "unauthorized", "message": "Invalid or missing API key" }
2.2. JWT do signer (navegador → NexSign)
O signatário não usa API key. O fluxo é:
-
Seu backend cria o envelope. NexSign envia email com magic link ao signatário.
-
Signatário clica no link → navegador carrega sua UI pública (
/sign/<token>) → sua UI fazGET /api/v1/public/magic?token=.... -
NexSign valida o token, queima o nonce (cookie
nexsign_magic_nonce, httpOnly) e devolve um JWT efêmero:{ "token": "eyJhbGciOi...", "envelope_id": "...", "app_id": "...", "signer_email": "..." } -
UI guarda o JWT em memória (não em
localStorage) e envia emAuthorization: Bearer <token>para/v1/signing/*. -
JWT é específico do envelope/signer. Após
otp/validate, recebe a flagotp_verified: trueque destravaPOST /v1/signing/sign.
3. Endpoints REST
Base URL: https://nexsign.nexunio.com/api (ajuste para seu ambiente).
Todas as rotas estão sob /v1.
3.1. Envelopes (servidor, API key)
POST /v1/envelopes — criar envelope
POST /api/v1/envelopes
X-API-Key: nxs_live_...
Content-Type: application/json
{
"subject_type": "contract",
"subject_id": "0a0e6c5a-8c4d-4f1c-9b2e-2c3a8d9b1f01",
"subject_metadata": {
"title": "Contrato de prestação de serviços #4521",
"html_template": "<h1>Contrato {{numero}}</h1><p>Entre {{contratante}} e {{contratado}}...</p>",
"template_vars": {
"numero": "4521",
"contratante": "ACME Ltda",
"contratado": "João Silva"
},
"clause_html": "<p>Declaro ter lido e concordo com os termos acima...</p>",
"extras": { "internal_ref": "ORD-998" }
},
"external_subject_ref": "nexarq:contract:4521",
"signers": [
{
"name": "João Silva",
"email": "joao@example.com",
"role": "signer",
"external_ref": "user_123",
"order": 0
},
{
"name": "ACME Ltda",
"email": "juridico@acme.com",
"role": "witness",
"order": 1
}
],
"expires_at": "2026-06-01T23:59:59Z"
}
Regras:
signers: 1 a 20 itens.subject_metadata.html_templateé obrigatório no momento do sign — pode omitir no create se for setar via update futuro (🚧 Fase 1).expires_até opcional; sem isso o envelope vive até ser cancelado/assinado.
Resposta 201:
{
"envelope_id": "1f2c3a4b-...",
"status": "pending",
"magic_links_sent": 1
}
GET /v1/envelopes/:id — status + metadata
{
"envelope": {
"id": "...",
"status": "pending|active|signed|cancelled|declined|expired",
"subject_type": "contract",
"subject_id": "...",
"subject_metadata": { ... },
"external_subject_ref": "...",
"signers": [
{ "email": "...", "name": "...", "role": "...", "signed_at": "2026-05-09T13:42:11Z" }
],
"expires_at": "...",
"completed_at": "..."
}
}
GET /v1/envelopes/:id/events — hash chain
Retorna a cadeia de eventos do envelope, encadeada por prev_hash/this_hash.
Útil para auditoria/dossiê próprio.
POST /v1/envelopes/:id/cancel
{ "reason": "Cliente desistiu da contratação" }
POST /v1/envelopes/:id/resend-otp
Reenvia magic link para um signer específico.
{ "signer_email": "joao@example.com" }
GET /v1/envelopes/:id/evidence
Retorna PDF (application/pdf) com o dossiê de evidências: cadeia de
eventos, IPs, user agents, tokens TSA. Header X-Artifact-Sha256 traz o
hash do PDF para você guardar.
GET /v1/envelopes/:id/sealed-pdf
Retorna o PDF assinado mais recente (PAdES B-LT). Disponível depois que
ao menos um signer concluiu /sign.
3.2. Signing (navegador, JWT do signer)
Todas exigem Authorization: Bearer <jwt>.
POST /v1/signing/otp/request
Solicita reenvio de OTP + magic link para o signer do JWT.
{ "base_url": "https://app.exemplo.com" }
POST /v1/signing/otp/validate
{ "code": "123456" }
Resposta:
{ "ok": true, "otp_id": "...", "token": "<novo JWT com otp_verified:true>" }
Substitua o JWT em memória pelo novo retornado.
POST /v1/signing/sign
Exige JWT com otp_verified: true. Renderiza PDF, aplica TSA + PAdES, marca
o signer como assinado.
{
"cpf": "123.456.789-00",
"acceptance": {
"accepted_at": "2026-05-09T13:42:00Z",
"drawn_signature_base64": "data:image/png;base64,iVBORw0KGgo..."
}
}
Resposta:
{
"ok": true,
"envelope": { "...status atualizado..." },
"sealed_artifact_id": "..."
}
POST /v1/signing/decline
{ "reason": "Não concordo com a cláusula 5." }
3.3. Públicas (sem auth)
GET /v1/public/magic?token=...
Troca magic token + nonce cookie por JWT efêmero. Veja seção 2.2.
3.4. Erros
Padrão da resposta:
{ "error": "<codigo_snake_case>", "message": "...", "retry_after_ms": 3000 }
Códigos comuns:
| Código | HTTP | Quando |
|---|---|---|
unauthorized | 401 | API key inválida ou JWT expirado |
not_found | 404 | Envelope inexistente ou de outro app |
signer_not_in_envelope | 403 | Signer do JWT não pertence ao envelope |
already_signed | 409 | Tentativa de re-assinar |
otp_invalid / otp_expired / otp_too_many_attempts | 422 | Veja retry_after_ms |
magic_* | 422 | Falhas no fluxo de magic link |
missing_html_template | 422 | subject_metadata.html_template ausente no sign |
internal_error | 500 | Erro inesperado — reporte com X-Request-Id |
4. Webhooks
🚧 Fase 1. O contrato abaixo é o que vai ser entregue. Hoje os eventos existem como telemetria interna (
signature.*); o dispatcher HTTP está em implementação.
4.1. Configuração
No painel admin do app, registre um endpoint:
- URL
https://seu-app.example.com/webhooks/nexsign - Eventos a inscrever (lista abaixo).
- NexSign devolve um signing secret (
whsec_...). Guarde no cofre.
4.2. Formato
POST /webhooks/nexsign
Content-Type: application/json
NexSign-Signature: t=1715258521,v1=5257a8...
NexSign-Event-Id: evt_01HYZ...
NexSign-Event-Type: envelope.signed
{
"id": "evt_01HYZ...",
"type": "envelope.signed",
"created_at": "2026-05-09T13:42:11Z",
"app_id": "...",
"data": {
"envelope_id": "...",
"subject_type": "contract",
"subject_id": "...",
"external_subject_ref": "nexarq:contract:4521",
"signer_email": "joao@example.com",
"signer_external_ref": "user_123",
"status": "signed",
"sealed_artifact_id": "..."
}
}
4.3. Eventos
| Evento | Quando |
|---|---|
envelope.created | Envelope criado via API. |
envelope.activated | Primeiro signer abriu o link (envelope sai de pending para active). |
envelope.signer_signed | Um signer concluiu /sign. Para envelopes com vários signatários, dispara N vezes. |
envelope.signed | Último signer assinou. Envelope vai para signed. Use este para liberar fluxos no seu app. |
envelope.declined | Algum signer recusou. |
envelope.cancelled | Cancelado via API. |
envelope.expired | Atingiu expires_at sem completar. |
signer.otp_issued | OTP gerado/reemitido (informativo, baixa volumetria). |
4.4. Verificação de assinatura (HMAC)
import crypto from 'node:crypto';
function verify(rawBody: string, header: string, secret: string): boolean {
const [tPart, v1Part] = header.split(',');
const t = tPart.split('=')[1];
const v1 = v1Part.split('=')[1];
const signed = `${t}.${rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
// timing-safe + janela de 5 minutos
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Use o raw body (bytes exatos), não o JSON re-serializado.
4.5. Retries e idempotência
- Tentativas: 8x com backoff exponencial (até ~24h).
- Considere sucesso qualquer
2xxem ≤ 10s. - Use
NexSign-Event-Idcomo chave de idempotência — o mesmo evento pode chegar mais de uma vez após retry.
5. Fluxo end-to-end (resumo)
[Seu backend] [NexSign] [Signer browser]
| | |
| POST /v1/envelopes ----------> | |
| | -- email c/ magic link -----> |
| | | clica
| | <-- GET /v1/public/magic ---- |
| | -- JWT efêmero -------------> |
| | <-- POST /signing/otp/req --- |
| | -- email c/ código OTP -----> |
| | <-- POST /signing/otp/validate |
| | -- JWT (otp_verified=true) -> |
| | <-- POST /signing/sign ------ |
| | (renderiza PDF + TSA + PAdES) |
| | -- ok + sealed_artifact_id -> |
| <-- webhook envelope.signed --- | |
| GET /v1/envelopes/:id/sealed-pdf | |
| (arquiva no seu storage) | |
6. Boas práticas
- Idempotência no create: derive
subject_id(UUID v5) do recurso no seu app +subject_type. Se a request falhar/refazer, você reusa o mesmo envelope. - Não polle status. Inscreva-se em
envelope.signedeenvelope.declinede arquive o sealed-pdf no callback. - Guarde sempre
X-Artifact-Sha256ao baixar evidence/sealed-pdf — prova de integridade do que você arquivou. - CORS: a API é aberta por padrão hoje (Fase 0). Em Fase 1 seu app registra origens permitidas para o JWT do signer.
- Rotação de API keys: 🚧 Fase 1 permitirá emitir nova key e revogar a antiga sem downtime.
7. Ambientes
| Ambiente | Base URL | API key prefix |
|---|---|---|
| Desenvolvimento (local) | http://localhost:3000/api | livre (NEXSIGN_DEV_API_KEY) |
| Sandbox 🚧 | https://sandbox.nexsign.nexunio.com/api | nxs_test_... |
| Produção 🚧 | https://nexsign.nexunio.com/api | nxs_live_... |
8. Referências internas
- Schema do banco:
packages/db/src/schema.ts - Rotas Hono:
apps/app/src/hono/routes/*.ts - Núcleo (envelope, otp, sealing, TSA):
apps/app/src/lib/core/ - Eventos de telemetria que viram webhooks:
apps/app/src/lib/core/observability.ts