Pular para o conteúdo principal

API REST: o SignSession Contract

O Servidor implementa o SignSession Contract, a API que uma aplicação cliente chama para abrir uma Sessão de Assinatura vinculada a um CPF e assinar documentos nela. Esta página trata da autenticação, do ciclo de vida da sessão, de cada operação, do envelope de erro e dos pontos em que este Servidor responde, de propósito, de forma diferente da outra implementação do contrato.

dica

Com o Servidor em execução, o contrato legível por máquina é servido em /openapi/v1.json (OpenAPI 3.1) e uma referência navegável em /scalar. Os dois respondem sem credencial. Esta página é o guia conceitual; o documento publicado é a fonte de verdade para os detalhes de cada campo.

Versão do contrato e caminho base​

Toda operação é servida sob /v1, e nada mais responde: uma chamada a uma rota sem o prefixo é recusada com 404 ENDPOINT_NOT_FOUND. v1 é a versão do contrato, a única à qual um cliente deve se fixar. O campo version do documento OpenAPI é a versão do build do Servidor e não diz nada sobre compatibilidade de protocolo.

Autenticação​

Toda operação exige o header X-API-Key, com uma chave de API emitida para uma Aplicação Cliente no console administrativo. Qualquer uma das chaves de uma aplicação autentica toda a superfície; a chave não restringe nada dentro dela. Chaves ausentes, desconhecidas, revogadas, expiradas e de aplicações desativadas são todas recusadas da mesma forma, com 401 UNAUTHORIZED.

Trate um sessionId e um signatureId como tokens de portador (bearer tokens) daquilo que identificam: qualquer chave aceita pode usar uma sessão ou buscar um documento cujo id ela tenha.

Toda resposta, inclusive as de erro, traz um X-Correlation-Id gerado pelo Servidor. Informe-o ao relatar um problema; ele leva o operador a todo o rastro da requisição.

O ciclo de vida da sessão​

POST /v1/sessions (cpf, pin) ──▶ 201 { sessionId, expiresIn, expiresAt }
│
├──▶ GET /v1/sessions/status?sessionId=… is it still active? (never extends it)
├──▶ GET /v1/certificates/info?sessionId=… which certificate, which verdict
├──▶ POST /v1/signatures (sessionId, document) ──▶ 202 { signatureId }
├──▶ POST /v1/signatures/batch (sessionId, files[]) ──▶ 202 { results[] }
│ └──▶ GET /v1/signatures/{signatureId}/document ──▶ 200 PDF | 409 | 422
└──▶ POST /v1/sessions/deactivate (sessionId) ──▶ 200
  1. Crie uma sessão com o CPF e o PIN do titular. O Servidor tenta primeiro os Certificados do Cofre guardados para esse CPF; se nenhum servir, oferece o PIN às estações de trabalho que têm um certificado do CPF, onde ele é comparado com o PIN em uso ali e nunca apresentado ao token. Se nenhuma estação de trabalho tiver esse PIN em cache, o titular recebe uma solicitação no Tray App e esta requisição fica aberta até que ele responda ou a solicitação expire (60 s por padrão). Leve isso em conta no timeout do seu cliente. O certificado que confirmar o PIN é o que fica vinculado à sessão.
  2. Assine na sessão, um documento por vez ou em lote. A assinatura inteira acontece dentro da requisição e normalmente leva alguns segundos.
  3. Busque o PDF assinado pelo id da assinatura. É também aí que se lê o resultado de uma assinatura.
  4. Desative a sessão quando o trabalho terminar, ou deixe a janela de inatividade se esgotar.

Há no máximo uma sessão ativa por CPF; criar outra substitui a anterior. Assinar estende a janela de inatividade; consultar o status, não. Uma sessão também termina quando a Franquia de Assinaturas opcional da implantação se esgota.

Operações​

POST /v1/sessions: criar uma Sessão de Assinatura​

curl -s https://trustbridge.example.com/v1/sessions \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{ "cpf": "12345678909", "pin": "1234", "sessionDuration": 3600, "email": "holder@example.com" }'
Campo
cpfobrigatórioOnze dígitos, sem pontuação. Os dígitos verificadores são conferidos, e os onze valores de dígitos repetidos são recusados, com 400 INVALID_REQUEST.
pinobrigatórioO PIN do certificado, qualquer que seja a custódia. Para um Certificado do Cofre, a senha que abre o arquivo dele.
sessionDurationopcionalEm segundos. Se omitido, vale a janela padrão do Servidor; acima do máximo, o valor é reduzido ao máximo, não recusado.
emailopcionalUm endereço alternativo do signatário, usado apenas quando o certificado vinculado não traz nenhum. Sem ele, um certificado assim é recusado no momento da assinatura com CERT_INCOMPLETE_IDENTITY.
{ "sessionId": "8f3c…", "cpf": "12345678909", "expiresIn": 3600, "expiresAt": "2026-09-21T15:04:05Z" }

expiresIn é sempre a janela efetivamente concedida. Recusas: 404 CERT_NOT_FOUND (nenhum certificado para o CPF em lugar nenhum), 422 CERT_NOT_ACTIVE (todo certificado do CPF tem um veredito diferente de ativo), 401 INVALID_PIN, 422 PIN_NOT_AVAILABLE (a solicitação expirou, foi recusada ou não pôde ser exibida), 422 WORKSTATION_OFFLINE (toda máquina com um certificado ativo está offline), 429 TOO_MANY_PIN_ATTEMPTS (bloqueio por CPF).

GET /v1/sessions/status?sessionId=…: verificar uma sessão​

Responde o estado da sessão e quanto tempo ela ainda tem. Um id que não identifica nada ativo ainda responde 200 com active: false; omitir o id resulta em 400. Pode ser consultado periodicamente sem risco; nunca estende a janela. Não há como perguntar se um CPF tem uma sessão: um cliente que perdeu o id cria uma nova sessão.

{ "active": true, "sessionId": "8f3c…", "expiresIn": 3412, "expiresAt": "2026-09-21T15:04:05Z" }

POST /v1/sessions/deactivate: encerrar uma sessão​

curl -s https://trustbridge.example.com/v1/sessions/deactivate \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" -d '{ "sessionId": "8f3c…" }'

Encerra a sessão imediatamente e descarta o segredo em que ela se apoiava (o PIN que uma estação de trabalho tinha em cache para ela, ou a senha que o Servidor guardava para abrir um Certificado do Cofre). Não é idempotente: sem nada ativo, responde 401 SESSION_EXPIRED. A resposta informa o CPF lido da sessão, o id, active: false e endedAt.

GET /v1/certificates/info?sessionId=…: descrever o certificado vinculado​

O certificado ao qual a sessão está vinculada, como a plataforma o registrou pela última vez: cpf (o titular do certificado, que não precisa ser igual ao CPF da sessão), subjectName, issuer, serialNumber, notBefore, notAfter e status. Verifique status antes de assinar: um veredito pode mudar enquanto a sessão está ativa, e só ACTIVE pode assinar. Exige uma sessão ativa; nunca a estende.

POST /v1/signatures: assinar um documento​

Há dois formatos de corpo. JSON, com o documento codificado em base64:

curl -s https://trustbridge.example.com/v1/signatures \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d "{ \"sessionId\": \"8f3c…\", \"document\": \"$(base64 -w0 contract.pdf)\", \"fileName\": \"contract.pdf\" }"

Ou multipart, com um campo sessionId e o PDF como parte de arquivo (o nome do arquivo é tirado da parte):

curl -s https://trustbridge.example.com/v1/signatures \
-H "X-API-Key: $API_KEY" \
-F "sessionId=8f3c…" -F "document=@contract.pdf"

Responde 202:

{ "signatureId": "a1b2…", "status": "PROCESSING", "acceptedAt": "2026-09-21T14:10:00Z" }

Um 202 não significa "deu certo". Depois que o documento chega ao serviço de assinatura, esta operação responde o id aconteça o que acontecer em seguida, porque o id é o único caminho de volta a um documento que pode ter sido assinado. O resultado é lido na busca. signatureId é o id do documento no próprio Lacuna Signer. Só PDFs são assinados (422 UNSUPPORTED_DOCUMENT); um documento acima de Signing:MaxDocumentBytes (10 MB por padrão, medidos sobre os bytes decodificados) resulta em 413 DOCUMENT_TOO_LARGE; nenhum documento resulta em 400 INVALID_DOCUMENT; um sessionId ausente resulta em 400 INVALID_REQUEST.

POST /v1/signatures/batch: assinar vários documentos​

Somente multipart/form-data: um campo sessionId e uma parte de arquivo por documento, até Signing:MaxBatchItems (50 por padrão).

curl -s https://trustbridge.example.com/v1/signatures/batch \
-H "X-API-Key: $API_KEY" \
-F "sessionId=8f3c…" -F "files=@a.pdf" -F "files=@b.pdf" -F "files=@c.pdf"

Responde 202 com um resultado por documento, na ordem em que as partes chegaram:

{
"results": [
{ "fileName": "a.pdf", "signatureId": "a1b2…", "status": "PROCESSING", "error": null },
{ "fileName": "b.pdf", "signatureId": null, "status": "ERROR",
"error": { "code": "DOCUMENT_TOO_LARGE", "message": "…" } },
{ "fileName": "c.pdf", "signatureId": "c3d4…", "status": "PROCESSING", "error": null }
],
"acceptedAt": "2026-09-21T14:10:00Z"
}

Cada documento é independente: um que falhou traz status: "ERROR" e um código do mesmo vocabulário, enquanto os demais trazem ids. Leia os resultados, não o código de status. A chamada inteira só falha por algo que valha para todos eles: nenhuma sessão ativa, um certificado que não pode assinar, mais documentos que o limite (400 INVALID_REQUEST) ou nenhum (400 INVALID_DOCUMENT). Os documentos são assinados vários de cada vez, até Signing:SignerConcurrency, e a chamada inteira é aguardada, então um lote grande leva o tempo de assinar todos os documentos dele. Um lote conta como uma chamada para o throttle e para a Franquia de Assinaturas.

GET /v1/signatures/{signatureId}/document: buscar o documento assinado​

curl -s https://trustbridge.example.com/v1/signatures/a1b2…/document \
-H "X-API-Key: $API_KEY" -o signed.pdf -w '%{http_code}\n'
StatusSignificado
200O PDF assinado, como corpo da resposta.
409 DOCUMENT_NOT_READYAinda está sendo assinado. Pergunte de novo.
422 DOCUMENT_UNAVAILABLETerminou sem versão assinada: recusado, cancelado ou expirado. details diz qual. Não há o que esperar.
404 DOCUMENT_NOT_FOUNDNenhum documento tem esse id.
502 SIGNER_ERRORNão foi possível consultar o serviço de assinatura; não diz nada sobre o documento.

O Servidor não armazena nada, então o tempo durante o qual um documento pode ser buscado é a retenção do Lacuna Signer. Qualquer chave de API válida pode buscar qualquer documento cujo id conheça, inclusive um criado por outra Aplicação Cliente, igualando de propósito a implementação de referência.

O envelope de erro​

Todo erro chega em um único envelope. code é uma string, não um conjunto fechado: trate um código desconhecido como uma falha inesperada, e não como um erro de parsing. Decida com base em code, nunca em message.

{ "error": { "code": "CERT_NOT_ACTIVE", "message": "…", "details": "…" } }
CódigoHTTPSignificado
INVALID_REQUEST400, 415Requisição malformada, ou um media type que a operação não lê. details indica o campo.
INVALID_DOCUMENT400Nenhum documento chegou, ou ele estava vazio.
UNAUTHORIZED401A chave de API não foi aceita.
SESSION_EXPIRED401Nenhuma Sessão de Assinatura ativa tem o sessionId informado: expirada, desativada, substituída, esgotada ou interrompida. Crie uma nova sessão.
INVALID_PIN401O PIN não abriu nenhum Certificado do Cofre do CPF e é diferente do PIN em uso em todas as estações de trabalho que têm um certificado dele.
CERT_NOT_FOUND404Na criação: nenhum certificado para o CPF em lugar nenhum. Na leitura ou na assinatura: o certificado vinculado não está mais disponível.
DOCUMENT_NOT_FOUND404Nenhum documento tem esse id de assinatura.
ENDPOINT_NOT_FOUND404Nenhuma operação responde a esse método e caminho. Verifique o prefixo /v1.
DOCUMENT_NOT_READY409Ainda está sendo assinado. Vale perguntar de novo.
DOCUMENT_TOO_LARGE413O documento ou o corpo da requisição excede o limite do Servidor; details informa os dois tamanhos.
CERT_NOT_ACTIVE422Todo certificado do CPF, ou o certificado vinculado, tem um veredito diferente de ativo (EXPIRED, REVOKED, UNTRUSTED, STALE).
DOCUMENT_UNAVAILABLE422O documento terminou sem versão assinada.
PIN_NOT_AVAILABLE422Não foi possível fazer nenhuma comparação: uma solicitação expirou, foi recusada ou não pôde ser exibida, ou um Certificado do Cofre não pôde ser aberto.
WORKSTATION_OFFLINE422Todo certificado ativo do CPF está em uma estação de trabalho, e todas elas estão offline (depois da tolerância de reconexão, em uma assinatura).
CERT_DATA_UNAVAILABLE422Os dados do próprio certificado não estão guardados para ele.
CERT_INCOMPLETE_IDENTITY422O certificado não identifica o titular o suficiente para assinar; details indica o campo. Um endereço de e-mail é o único campo que o email da sessão pode suprir.
UNSUPPORTED_DOCUMENT422Não é um PDF.
SIGNER_REJECTED422O serviço de assinatura recusou o documento; details traz a explicação dele.
SIGN_TIMEOUT422A assinatura não terminou dentro do prazo da requisição (60 s por padrão); nada foi assinado.
SERVER_BUSY429O Servidor está executando tantas chamadas do contrato quanto aceita e a fila dele está cheia. Nada na requisição foi recusado: tente de novo com um backoff próprio, com jitter. Nenhum Retry-After é enviado.
TOO_MANY_PIN_ATTEMPTS429O bloqueio por PINs incorretos do CPF está em vigor. Refere-se a um CPF, em todas as chaves de API; tentar de novo piora a situação. Some à medida que as falhas mais antigas saem da janela (15 min por padrão).
INTERNAL_ERROR500Não foi possível concluir a requisição; é seguro tentar de novo.
SIGNER_ERROR502Não foi possível consultar o serviço de assinatura. Em uma assinatura, só chega antes de o documento ter chegado ao serviço, então nada foi assinado.

Dois 429 diferentes. SERVER_BUSY diz respeito ao Servidor naquele momento e se resolve sozinho; TOO_MANY_PIN_ATTEMPTS diz respeito ao certificado de uma pessoa e não se resolve com novas tentativas. Um cliente que decide só pelo status vai tratá-los da mesma forma, e é por isso que são códigos separados.

Onde este Servidor difere da implementação de referência​

A outra implementação do contrato, o CertSession, está em produção e tem integrações feitas contra ela, e este Servidor a acompanha em quase tudo. Sete respostas diferem de propósito. Duas são silenciosas: produzem um comportamento diferente sem nenhum erro que o indique.

#O que difereComo um cliente percebe
1expiresIn nunca é nulo. Um sessionDuration omitido concede a janela padrão do Servidor, e não uma ilimitada. Não existe sessão ilimitada.Silencioso. Leia expiresIn e expiresAt na resposta, em vez de supor a janela que você pediu.
2Assinar não encerra a sessão; estende-a, a menos que a implantação tenha definido uma Franquia de Assinaturas. A referência encerra a sessão após cada assinatura, então um cliente escrito para ela cria uma sessão por documento.Silencioso. Um cliente assim não percebe nada, exceto que as sessões extras custam ao titular mais uma confirmação de PIN. Para encerrar a sessão junto com o documento, chame POST /v1/sessions/deactivate. Uma franquia de 1 no Servidor reproduz a referência.
3A validação do certificado verifica a cadeia e a revogação, não só a data de expiração.Recusado com CERT_NOT_ACTIVE onde a referência assinaria. Um certificado que assina no CertSession pode estar UNTRUSTED aqui se as âncoras de confiança da implantação não cobrirem o emissor dele.
4A busca de um documento assinado não é restrita à Aplicação Cliente que o criou.Nada muda no protocolo; a referência se comporta da mesma forma. Está listado porque a paridade foi escolhida em vez do isolamento, conscientemente, para esta geração do contrato.
5Um cpf precisa ser um CPF real: dígitos verificadores conferidos, valores de dígitos repetidos recusados.Recusado com 400 INVALID_REQUEST onde a referência criaria uma sessão. Valide os dígitos verificadores também no seu formulário. O pattern do schema continua ^[0-9]{11}$, porque um cálculo de módulo não é uma expressão regular.
6Uma requisição de assinatura sem sessionId é malformada, e não expirada.400 INVALID_REQUEST em vez de 401 SESSION_EXPIRED. Um 401 significa criar uma sessão e tentar de novo; este 400 significa que a requisição foi montada errado.
7Este Servidor descarta carga. Uma chamada que chega enquanto o Servidor já executa tantas quanto aceita é recusada, em vez de ficar na fila indefinidamente.429 SERVER_BUSY, que a referência nunca responde. Tente de novo após um backoff próprio. Usar lotes reduz a frequência com que isso acontece.

Onde este Servidor responde mais do que a referência descreve (sete campos em certificates/info, um acceptedAt no lote, um vocabulário de erros mais amplo), nada do que um cliente espera está faltando.

O Cofre de Certificados é uma capacidade que a referência não tem, e não uma resposta que difere: POST /v1/sessions testa o PIN contra os Certificados do Cofre antes de qualquer máquina, então uma sessão pode ser vinculada sem nenhuma estação de trabalho envolvida e sem nenhuma solicitação exibida. A única assimetria visível: uma sessão vinculada ao cofre se apoia em um segredo guardado apenas na memória do Servidor, então reiniciar o Servidor a encerra, e o cliente recebe SESSION_EXPIRED na chamada seguinte, recuperando-se ao criar uma nova sessão com o mesmo PIN. Uma sessão vinculada a uma estação de trabalho sobrevive ao mesmo reinício.

Como escrever um cliente robusto​

  • Guarde o sessionId. Nenhuma chamada o recupera a partir de um CPF.
  • Esteja pronto para SESSION_EXPIRED em qualquer chamada e recupere-se criando uma nova sessão. Você não tem como saber se uma implantação definiu uma Franquia de Assinaturas, nem deveria.
  • Leia o resultado na busca. Um 202 diz que o documento foi aceito, não assinado. Consulte o endpoint do documento periodicamente enquanto receber 409; pare em 200 ou 422.
  • Defina um timeout generoso em POST /v1/sessions. Uma criação que vai para uma solicitação no Tray aguarda uma pessoa, até o timeout da solicitação mais alguns segundos.
  • Decida com base em error.code e trate os dois 429 de forma diferente.
  • Use lotes quando tiver muitos documentos. Uma chamada, uma vaga no throttle, um débito na franquia.
  • Envie email se algum certificado com que você assina puder não trazer endereço.

Testando o contrato manualmente​

A Lacuna Software pode fornecer uma coleção do Postman com o contrato como este Servidor o implementa, com um environment que traz um pequeno PDF codificado em base64. Importe os dois, cole em apiKey uma chave de API emitida na tela Aplicações Cliente e execute as requisições em ordem: crie uma sessão (acompanhe a solicitação no Tray App se não houver PIN em cache), assine um documento e busque-o. A coleção grava sessionId e signatureId no environment, então não é preciso colar nada entre um passo e outro. Docs/openapi, no repositório do produto, contém o contrato versionado e as notas de divergência a partir das quais a coleção foi construída.

Ver em Markdown