---
title: "API REST: o SignSession Contract"
description: "A API REST do TrustBridge que implementa o SignSession Contract: autenticação, ciclo de vida da sessão, operações, erros e diferenças em relação à referência."
canonical: "https://docs.lacunasoftware.com/articles/trustbridge/rest-api"
locale: "pt-BR"
product: "TrustBridge"
last_updated: "2026-10-12"
---

# 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

```text
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

```bash
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`. |

```json
{ "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.

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

### `POST /v1/sessions/deactivate`: encerrar uma sessão

```bash
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:

```bash
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):

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

Responde `202`:

```json
{ "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).

```bash
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**:

```json
{
  "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

```bash
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`.

```json
{ "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_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.
