Appearance
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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
contractId | string (UUID) | sim | ID do contrato |
contractName | string | sim | Nome do contrato |
borrower | SigningParty | sim | Dados do tomador |
provider | SigningParty | sim | Dados do prestador |
documents | SigningDocument[] | sim | Documentos de onboarding (pode ser []) |
contractPdfs | ContractPdf[] | sim | PDFs do contrato (1+ por template) |
signers | Signer[] | sim | Demais assinantes (não inclui currentSigner) |
currentSigner | CurrentSigner | sim | Signatário que acessou via token (inclui signatureFields) |
expiresAt | string (ISO 8601) | não | Data de expiração do JWT |
requiresVerification | boolean | sim | Se o fluxo exige código 2FA |
isVerified | boolean | sim | Se o signer já verificou nesta sessão |
signerEmail | string | null | não | Email mascarado (all***@hotmail.com) |
Removido (2026-04-17):
contractPdf(singular legado),signerPhone,signatureFieldsem signers não-current. O frontend consome apenascontractPdfs, e os campos do signer atual vêm emcurrentSigner.signatureFieldscompdfIdexplícito.
SigningParty (borrower, provider)
| Campo | Tipo | Obrigatório |
|---|---|---|
id | string (UUID) | sim |
name | string | sim |
tradeName | string | não |
ContractPdf (contractPdfs[])
Um PDF por template que compõe o contrato.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string (UUID) | sim | ID do PDF |
fileName | string | sim | Nome do arquivo |
fileUrl | string (URL) | sim | URL do PDF no S3 |
fileSize | number | sim | Tamanho em bytes |
contractTemplateId | string (UUID) | não | Template de origem |
templateName | string | não | Nome do template (exibido na lista) |
Signer (signers[])
Não inclui o currentSigner. Tampouco inclui signatureFields (estes vêm apenas em currentSigner).
| Campo | Tipo | Obrigatório |
|---|---|---|
id | string (UUID) | sim |
name | string | sim |
email | string | sim |
role | string | sim |
status | "pending" | "signed" | "rejected" | sim |
signedAt | string | null | não |
order | number | sim |
CurrentSigner (currentSigner)
Estende Signer e adiciona signatureFields.
| Campo | Tipo | Obrigatório |
|---|---|---|
...campos de Signer | ||
signatureFields | SignatureField[] | sim |
SignatureField (currentSigner.signatureFields[])
Cada campo pertence a um PDF específico via pdfId.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id | string (UUID) | sim | ID do campo |
pdfId | string (UUID) | sim | PDF a que este campo pertence (contractPdfs[].id) |
type | "SIGNATURE" | "INITIALS" | sim | Assinatura ou rubrica |
page | number | sim | Página (0-indexed) |
x, y | number | sim | Posição em porcentagem (0 a 100) |
width, height | number | não | Tamanho em porcentagem |
required | boolean | sim | Obrigatório para finalizar |
signedAt | string | null | não | Timestamp 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 ossignatureFieldsdocurrentSigner(filtrados porpdfId) assinados. - Documento sem campos: PDF sem nenhum
signatureFieldassociado conta como "completo" assim que visitado (usuário precisa apenas clicar "Próximo documento"). - Finalizar: só habilitado quando todos os
requiredde todos os PDFs estãosignedAt !== nulle 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 retornam400. - 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
verifiede 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
pdfIdprecisa pertencer aocontractIddo token, caso contrário404. - 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:
| Status | Situação |
|---|---|
404 | pdfId não pertence ao contrato do token |
429 | Rate limit excedido |
POST /api/v1/signing/:token/reject
Request:
json
{ "reason": "Motivo da recusa (opcional)" }Response:
json
{ "data": { "message": "Assinatura rejeitada" } }Segurança
| Controle | Onde |
|---|---|
JWT exclusivo (type: "signing") | SigningJwtGuard extrai o token da URL, valida com authConfig.jwt.secret (HS256), recusa JWTs de outros tipos |
| Expiração | Lida 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 PDF | Service valida que pdf.contractId === ctx.contractId antes de gerar a URL, impedindo cross-contract |
| Cooldown reenvio | 60s por signer no service |
| TTL código | 10 minutos |
| Mascaramento de email | maskEmail no service (server-side) |
| Geolocalização | Persistida em signedLatitude/signedLongitude a cada /sign |
| Bloqueio contratos concluídos | Guard recusa signing em contratos COMPLETED / FINISHED / OVERDUE / ACTIVE |
Erros
| Status | Situação |
|---|---|
400 | Código inválido/expirado, cooldown ativo, código sem verificação prévia antes de /sign |
401 | Token JWT inválido, expirado ou tipo incorreto |
404 | Contrato ou signatário não encontrado |
409 | Signatário já assinou ou rejeitou |
429 | Rate limit excedido (request-code ou verify-code) |