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:

Não há SDK obrigatório — a API é HTTP + JSON. Um SDK Node oficial (@nexsign/sdk-node) está planejado para Fase 1.


1. Conceitos

TermoO que é
AppAplicação cliente (seu produto). Possui um app_id (UUID) e uma ou mais API keys.
EnvelopeUnidade de assinatura. Agrega 1+ signatários, o documento (HTML → PDF), uma cláusula de aceite e a hash chain de eventos.
SignerSignatário do envelope. Identificado por email; opcionalmente correlacionado ao seu app via external_ref.
subject_type / subject_idDiscriminadores opacos que você escolhe. NexSign não os interpreta — servem para você correlacionar o envelope com o recurso original (contrato, NDA, aprovação).
subject_metadataJSONB 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_refString "humana" para correlação em logs (ex.: nexarq:contract:abc123).
Evidence packagePDF gerado on-demand contendo dossiê do envelope (eventos, IPs, TSA tokens, hash chain).
Sealed PDFPDF 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_••••••••••••••••

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 é:

  1. Seu backend cria o envelope. NexSign envia email com magic link ao signatário.

  2. Signatário clica no link → navegador carrega sua UI pública (/sign/<token>) → sua UI faz GET /api/v1/public/magic?token=....

  3. 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": "..."
    }
    
  4. UI guarda o JWT em memória (não em localStorage) e envia em Authorization: Bearer <token> para /v1/signing/*.

  5. JWT é específico do envelope/signer. Após otp/validate, recebe a flag otp_verified: true que destrava POST /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:

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ódigoHTTPQuando
unauthorized401API key inválida ou JWT expirado
not_found404Envelope inexistente ou de outro app
signer_not_in_envelope403Signer do JWT não pertence ao envelope
already_signed409Tentativa de re-assinar
otp_invalid / otp_expired / otp_too_many_attempts422Veja retry_after_ms
magic_*422Falhas no fluxo de magic link
missing_html_template422subject_metadata.html_template ausente no sign
internal_error500Erro 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:

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

EventoQuando
envelope.createdEnvelope criado via API.
envelope.activatedPrimeiro signer abriu o link (envelope sai de pending para active).
envelope.signer_signedUm 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.declinedAlgum signer recusou.
envelope.cancelledCancelado via API.
envelope.expiredAtingiu expires_at sem completar.
signer.otp_issuedOTP 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


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


7. Ambientes

AmbienteBase URLAPI key prefix
Desenvolvimento (local)http://localhost:3000/apilivre (NEXSIGN_DEV_API_KEY)
Sandbox 🚧https://sandbox.nexsign.nexunio.com/apinxs_test_...
Produção 🚧https://nexsign.nexunio.com/apinxs_live_...

8. Referências internas