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.
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
- 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.
- Assine na sessão, um documento por vez ou em lote. A assinatura inteira acontece dentro da requisição e normalmente leva alguns segundos.
- Busque o PDF assinado pelo id da assinatura. É também aí que se lê o resultado de uma assinatura.
- 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 | ||
|---|---|---|
cpf | obrigatório | Onze 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. |
pin | obrigatório | O PIN do certificado, qualquer que seja a custódia. Para um Certificado do Cofre, a senha que abre o arquivo dele. |
sessionDuration | opcional | Em segundos. Se omitido, vale a janela padrão do Servidor; acima do máximo, o valor é reduzido ao máximo, não recusado. |
email | opcional | Um 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'
| Status | Significado |
|---|---|
200 | O PDF assinado, como corpo da resposta. |
409 DOCUMENT_NOT_READY | Ainda está sendo assinado. Pergunte de novo. |
422 DOCUMENT_UNAVAILABLE | Terminou sem versão assinada: recusado, cancelado ou expirado. details diz qual. Não há o que esperar. |
404 DOCUMENT_NOT_FOUND | Nenhum documento tem esse id. |
502 SIGNER_ERROR | Nã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ódigo | HTTP | Significado |
|---|---|---|
INVALID_REQUEST | 400, 415 | Requisição malformada, ou um media type que a operação não lê. details indica o campo. |
INVALID_DOCUMENT | 400 | Nenhum documento chegou, ou ele estava vazio. |
UNAUTHORIZED | 401 | A chave de API não foi aceita. |
SESSION_EXPIRED | 401 | Nenhuma Sessão de Assinatura ativa tem o sessionId informado: expirada, desativada, substituída, esgotada ou interrompida. Crie uma nova sessão. |
INVALID_PIN | 401 | O 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_FOUND | 404 | Na 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_FOUND | 404 | Nenhum documento tem esse id de assinatura. |
ENDPOINT_NOT_FOUND | 404 | Nenhuma operação responde a esse método e caminho. Verifique o prefixo /v1. |
DOCUMENT_NOT_READY | 409 | Ainda está sendo assinado. Vale perguntar de novo. |
DOCUMENT_TOO_LARGE | 413 | O documento ou o corpo da requisição excede o limite do Servidor; details informa os dois tamanhos. |
CERT_NOT_ACTIVE | 422 | Todo certificado do CPF, ou o certificado vinculado, tem um veredito diferente de ativo (EXPIRED, REVOKED, UNTRUSTED, STALE). |
DOCUMENT_UNAVAILABLE | 422 | O documento terminou sem versão assinada. |
PIN_NOT_AVAILABLE | 422 | Nã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_OFFLINE | 422 | Todo 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_UNAVAILABLE | 422 | Os dados do próprio certificado não estão guardados para ele. |
CERT_INCOMPLETE_IDENTITY | 422 | O 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_DOCUMENT | 422 | Não é um PDF. |
SIGNER_REJECTED | 422 | O serviço de assinatura recusou o documento; details traz a explicação dele. |
SIGN_TIMEOUT | 422 | A assinatura não terminou dentro do prazo da requisição (60 s por padrão); nada foi assinado. |
SERVER_BUSY | 429 | O 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_ATTEMPTS | 429 | O 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_ERROR | 500 | Não foi possível concluir a requisição; é seguro tentar de novo. |
SIGNER_ERROR | 502 | Nã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 difere | Como um cliente percebe |
|---|---|---|
| 1 | expiresIn 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. |
| 2 | Assinar 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. |
| 3 | A 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. |
| 4 | A 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. |
| 5 | Um 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. |
| 6 | Uma 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. |
| 7 | Este 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_EXPIREDem 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
202diz que o documento foi aceito, não assinado. Consulte o endpoint do documento periodicamente enquanto receber409; pare em200ou422. - 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.codee 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
emailse 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.