Skip to content

Signing API - Especificação do Endpoint

Endpoints públicos para o fluxo de assinatura de contrato via link com JWT. O acesso é autenticado exclusivamente pelo token na URL (não usa header Authorization do login padrão) e é cross-tenant.


GET /api/v1/signing/:token

Retorna todos os dados necessários para a tela de assinatura.

Response

json
{
  "data": {
    "contractId": "841d1f47-c5b0-4aec-ab55-0b294abefb8d",
    "contractName": "Contrato multiplos templates",

    "borrower": {
      "id": "ea38bb68-cce8-4958-b408-982e00aadc00",
      "name": "Jão da Silva",
      "tradeName": "ACME"
    },

    "provider": {
      "id": "1e3eb94d-84cb-4c14-9d15-885b6a05f0b3",
      "name": "Maraca Acme",
      "tradeName": "Maraca Acme"
    },

    "documents": [],

    "contractPdfs": [
      {
        "id": "0f8c7d3d-e83a-45fe-af5f-83da0293a169",
        "fileName": "Contrato de prestação de serviços - Maraca Acme.pdf",
        "fileUrl": "https://s3.../contrato-prestacao.pdf",
        "fileSize": 166553,
        "contractTemplateId": "2e5c6995-2ab1-4975-945a-784a66368e42",
        "templateName": "Contrato de prestação de serviços"
      },
      {
        "id": "0bd0e481-8a5b-42d0-87f5-fad300be66a6",
        "fileName": "Contrato de confidencialidade - Maraca Acme.pdf",
        "fileUrl": "https://s3.../nda.pdf",
        "fileSize": 398132,
        "contractTemplateId": "f8b94f87-a1e3-405b-8906-7394623641b5",
        "templateName": "Contrato de confidencialidade"
      }
    ],

    "signers": [
      {
        "id": "7ee3f9cd-1fd7-4a41-a0d6-7d6ed4abd40d",
        "name": "Maria Nunes",
        "email": "maria.nunes@hotmail.com",
        "role": "Gestores",
        "status": "pending",
        "signedAt": null,
        "order": 1
      }
    ],

    "currentSigner": {
      "id": "983a66a9-0bd6-4099-8912-f419632fff20",
      "name": "Maraca Acme",
      "email": "contato@maraca.com",
      "role": "Partes",
      "status": "pending",
      "signedAt": null,
      "order": 0,
      "signatureFields": [
        {
          "id": "e9e0b128-5fa5-463f-b270-f5004bf1835d",
          "pdfId": "0f8c7d3d-e83a-45fe-af5f-83da0293a169",
          "type": "INITIALS",
          "page": 0,
          "x": 12.18,
          "y": 21.68,
          "width": 12.24,
          "height": 4.87,
          "required": true,
          "signedAt": null
        },
        {
          "id": "07a3945a-72d5-472f-9476-0e5064c59aa5",
          "pdfId": "0f8c7d3d-e83a-45fe-af5f-83da0293a169",
          "type": "SIGNATURE",
          "page": 0,
          "x": 53.30,
          "y": 77.20,
          "width": 22,
          "height": 6,
          "required": true,
          "signedAt": null
        }
      ]
    },

    "expiresAt": "2026-04-24T11:39:34.000Z",
    "requiresVerification": true,
    "isVerified": false,
    "signerEmail": "all***@hotmail.com"
  }
}

Campos

Raiz (data)

CampoTipoObrigatórioDescrição
contractIdstring (UUID)simID do contrato
contractNamestringsimNome do contrato
borrowerSigningPartysimDados do tomador
providerSigningPartysimDados do prestador
documentsSigningDocument[]simDocumentos de onboarding (pode ser [])
contractPdfsContractPdf[]simPDFs do contrato (1+ por template)
signersSigner[]simDemais assinantes (não inclui currentSigner)
currentSignerCurrentSignersimSignatário que acessou via token (inclui signatureFields)
expiresAtstring (ISO 8601)nãoData de expiração do JWT
requiresVerificationbooleansimSe o fluxo exige código 2FA
isVerifiedbooleansimSe o signer já verificou nesta sessão
signerEmailstring | nullnãoEmail mascarado (all***@hotmail.com)

Removido (2026-04-17): contractPdf (singular legado), signerPhone, signatureFields em signers não-current. O frontend consome apenas contractPdfs, e os campos do signer atual vêm em currentSigner.signatureFields com pdfId explícito.

SigningParty (borrower, provider)

CampoTipoObrigatório
idstring (UUID)sim
namestringsim
tradeNamestringnão

ContractPdf (contractPdfs[])

Um PDF por template que compõe o contrato.

CampoTipoObrigatórioDescrição
idstring (UUID)simID do PDF
fileNamestringsimNome do arquivo
fileUrlstring (URL)simURL do PDF no S3
fileSizenumbersimTamanho em bytes
contractTemplateIdstring (UUID)nãoTemplate de origem
templateNamestringnãoNome do template (exibido na lista)

Signer (signers[])

Não inclui o currentSigner. Tampouco inclui signatureFields (estes vêm apenas em currentSigner).

CampoTipoObrigatório
idstring (UUID)sim
namestringsim
emailstringsim
rolestringsim
status"pending" | "signed" | "rejected"sim
signedAtstring | nullnão
ordernumbersim

CurrentSigner (currentSigner)

Estende Signer e adiciona signatureFields.

CampoTipoObrigatório
...campos de Signer
signatureFieldsSignatureField[]sim

SignatureField (currentSigner.signatureFields[])

Cada campo pertence a um PDF específico via pdfId.

CampoTipoObrigatórioDescrição
idstring (UUID)simID do campo
pdfIdstring (UUID)simPDF a que este campo pertence (contractPdfs[].id)
type"SIGNATURE" | "INITIALS"simAssinatura ou rubrica
pagenumbersimPágina (0-indexed)
x, ynumbersimPosição em porcentagem (0 a 100)
width, heightnumbernãoTamanho em porcentagem
requiredbooleansimObrigatório para finalizar
signedAtstring | nullnãoTimestamp de quando foi assinado

Regras de UI (frontend)

  • Documentos sequenciais: a lista contractPdfs é percorrida em ordem. Um PDF só desbloqueia quando o anterior tem todos os signatureFields do currentSigner (filtrados por pdfId) assinados.
  • Documento sem campos: PDF sem nenhum signatureField associado conta como "completo" assim que visitado (usuário precisa apenas clicar "Próximo documento").
  • Finalizar: só habilitado quando todos os required de todos os PDFs estão signedAt !== null e o signer passou pelos documentos em ordem.

Fluxo 2FA

POST /api/v1/signing/:token/request-code

Dispara o envio do código de 6 dígitos para o email do signer.

  • Body: vazio. Canal SMS foi removido; o envio é sempre por email.
  • Rate limit: 3 requisições / minuto por IP (@Throttle).
  • Cooldown de reenvio: 60s entre códigos para o mesmo assignmentId. Tentativas antes desse prazo retornam 400.
  • TTL do código: 10 minutos.
  • Invalidação: cada novo código invalida todos os anteriores pendentes do mesmo signer.

Response:

json
{ "data": { "message": "Código enviado via email" } }

POST /api/v1/signing/:token/verify-code

Valida o código de 6 dígitos.

  • Body: { "code": "123456" } (string de 6 caracteres).
  • Rate limit: 5 requisições / minuto por IP.
  • Sucesso: marca a verificação como verified e permite a chamada a /sign.

Response:

json
{ "data": { "verified": true } }

POST /api/v1/signing/:token/sign

Requer verificação 2FA prévia (isVerified === true), senão retorna 400.

Request:

json
{
  "signatureImage": "data:image/png;base64,iVBORw0KGgo...",
  "latitude": -23.5505,
  "longitude": -46.6333
}

Response:

json
{ "data": { "message": "Contrato assinado com sucesso" } }

latitude e longitude são persistidos em ContractSignatureAssignment.signedLatitude/signedLongitude para rastreabilidade.


GET /api/v1/signing/:token/pdf/:pdfId/download-url

Retorna uma URL temporária para download do PDF com todas as assinaturas já aplicadas pelas partes que assinaram. Se nenhum signer assinou ainda, retorna a URL do PDF original (sem assinaturas).

  • Rate limit: 10 requisições / minuto por IP.
  • Validação: o pdfId precisa pertencer ao contractId do token, caso contrário 404.
  • Cacheamento: o snapshot é gravado em S3 com chave determinística (contracts/{contractId}/signed-snapshots/{pdfId}.pdf) e sobrescrito a cada chamada quando há assinaturas novas.

Response:

json
{ "data": { "url": "https://s3.../contracts/.../signed-snapshots/pdf-id.pdf" } }

Erros:

StatusSituação
404pdfId não pertence ao contrato do token
429Rate limit excedido

POST /api/v1/signing/:token/reject

Request:

json
{ "reason": "Motivo da recusa (opcional)" }

Response:

json
{ "data": { "message": "Assinatura rejeitada" } }

Segurança

ControleOnde
JWT exclusivo (type: "signing")SigningJwtGuard extrai o token da URL, valida com authConfig.jwt.secret (HS256), recusa JWTs de outros tipos
ExpiraçãoLida do claim exp do JWT; bloqueada em /signing/:token pelo guard
Rate limit 2FA@Throttle no controller (request-code 3/min, verify-code 5/min, por IP)
Rate limit download@Throttle no pdf/:pdfId/download-url (10/min por IP)
Isolamento de PDFService valida que pdf.contractId === ctx.contractId antes de gerar a URL, impedindo cross-contract
Cooldown reenvio60s por signer no service
TTL código10 minutos
Mascaramento de emailmaskEmail no service (server-side)
GeolocalizaçãoPersistida em signedLatitude/signedLongitude a cada /sign
Bloqueio contratos concluídosGuard recusa signing em contratos COMPLETED / FINISHED / OVERDUE / ACTIVE

Erros

StatusSituação
400Código inválido/expirado, cooldown ativo, código sem verificação prévia antes de /sign
401Token JWT inválido, expirado ou tipo incorreto
404Contrato ou signatário não encontrado
409Signatário já assinou ou rejeitou
429Rate limit excedido (request-code ou verify-code)