---
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, sessões em nome de uma Empresa, operações, erros e a especificação do contrato."
canonical: "https://docs.lacunasoftware.com/articles/trustbridge/rest-api"
locale: "pt-BR"
product: "TrustBridge"
last_updated: "2026-10-13"
---

# 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, opcionalmente em nome de uma Empresa, 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 de qual documento é a especificaçã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[, cnpj])  ──▶  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, fileName }
        ├──▶ 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, e com um CNPJ para assinar em nome de uma
   Empresa. O Servidor tenta primeiro os Certificados do Cofre guardados para esse CPF. Se nenhum
   servir, as estações de trabalho que têm um certificado do CPF são solicitadas a **provar** que têm
   o PIN em uso ali, e o Servidor confere cada prova contra o PIN que você enviou. O PIN não vai além
   do Servidor: nenhuma estação de trabalho o recebe, e ele 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, ou por CPF e CNPJ numa sessão em nome de uma Empresa; criar
outra para o mesmo par 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.

Uma sessão vinculada a um **Certificado do Cofre** se apoia em um segredo guardado apenas na memória
do Servidor, então reiniciar o Servidor a encerra: o cliente recebe `SESSION_EXPIRED` na chamada
seguinte e se recupera criando uma nova sessão com o mesmo PIN. Uma sessão vinculada a uma estação de
trabalho sobrevive ao mesmo reinício.

> **Observação: Workstation 1.5.0 e Servidor 1.13.0**
>
> A partir do Servidor 1.13.0, só estações de trabalho com o **Workstation 1.5.0** ou posterior recebem
> pedidos de prova de PIN ou solicitações de PIN. Uma máquina com uma versão anterior continua assinando
> nas sessões já vinculadas a ela, mas, na criação de sessão, conta como offline: um titular cujas
> máquinas tenham todas uma versão anterior recebe `422 WORKSTATION_OFFLINE` até que uma delas seja
> atualizada. Veja [Instalação da estação de trabalho](https://docs.lacunasoftware.com/articles/trustbridge/workstation-installation).

## 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`. Numa sessão em nome de uma Empresa, continua sendo o CPF da pessoa: o responsável pelo e-CNPJ da Empresa. |
| `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. |
| `cnpj` | opcional | A Empresa em nome da qual assinar, com o e-CNPJ dela. Catorze caracteres, sem pontuação e em maiúsculas: doze dígitos ou letras (o CNPJ alfanumérico é aceito tanto quanto o numérico) seguidos de dois dígitos verificadores. Os dígitos verificadores são conferidos, e doze repetições de um mesmo caractere são recusadas, com `400 INVALID_REQUEST`. Se omitido, a sessão é da pessoa, como antes. |
| `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 (e CNPJ) em lugar nenhum.
- `422 CERT_NOT_ACTIVE`: todo certificado do CPF (e CNPJ) tem um veredito diferente de ativo.
- `401 INVALID_PIN`: todo certificado candidato foi tentado e nenhum aceitou o PIN.
- `422 PIN_NOT_AVAILABLE`: não foi possível fazer nenhuma comparação. Uma solicitação expirou, foi
  recusada ou não pôde ser exibida; um Certificado do Cofre não pôde ser aberto; ou tentativas
  recentes demais contra o certificado não conferiram, e ele só volta a ser consultado quando elas
  saírem da janela. `details` diz qual.
- `422 WORKSTATION_OFFLINE`: toda máquina com um certificado ativo está offline, ou com uma versão do
  Workstation anterior à 1.5.0.
- `429 TOO_MANY_PIN_ATTEMPTS`: bloqueio por CPF.

#### Sessão em nome de uma Empresa

Envie `cnpj` junto com `cpf` para assinar com o **e-CNPJ** de uma Empresa. O `cpf` continua sendo o da
pessoa: o responsável por esse e-CNPJ, cujo PIN é este.

```bash
curl -s https://trustbridge.example.com/v1/sessions \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -d '{ "cpf": "12345678909", "cnpj": "12ABC34501DE35", "pin": "1234" }'
```

```json
{ "sessionId": "c71d…", "cpf": "12345678909", "cnpj": "12ABC34501DE35", "expiresIn": 3600, "expiresAt": "2026-09-21T15:04:05Z" }
```

- **A correspondência é exata.** Com `cnpj`, só um e-CNPJ que traga esse CNPJ e esse CPF pode vincular
  a sessão, nunca o e-CPF da própria pessoa. Sem ele, só o e-CPF da pessoa pode, nunca um e-CNPJ,
  mesmo que traga o mesmo CPF.
- **Uma sessão ativa por CPF e CNPJ.** A sessão da pessoa e a da Empresa dela convivem, e as de dois
  responsáveis pela mesma Empresa também. Encerrar uma não afeta a outra.
- **O bloqueio por PIN continua por CPF**, qualquer que seja o `cnpj` enviado, ou nenhum: um mesmo
  cartão costuma guardar os dois certificados atrás de um só PIN, então as falhas em qualquer um deles
  contam juntas.
- **As respostas devolvem o `cnpj`** sempre que trazem o CPF da sessão: na criação, na desativação e
  na descrição do certificado. Uma sessão sem ele responde sem nenhum membro `cnpj` (nem com `null`),
  então uma integração que nunca o envia não vê nada de novo.

### `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 (e o `cnpj` dela, numa sessão em nome de uma Empresa), 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; num e-CNPJ, o CPF do
responsável pelo certificado em nome da Empresa), `cnpj` (só num e-CNPJ), `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"
```

Qualquer parte de arquivo é lida como o documento, seja qual for o nome do campo. Só uma é assinada:
quando a requisição traz várias, vence `document`, depois `file`, depois a primeira que chegou.

Responde `202`:

```json
{ "signatureId": "a1b2…", "fileName": "contract.pdf", "status": "PROCESSING", "acceptedAt": "2026-09-21T14:10:00Z" }
```

`fileName` é o nome com que o Servidor assinou o documento, que nem sempre é o enviado: o `fileName`
da requisição, ou o nome da própria parte de arquivo (`filename*` quando a parte traz um);
`document.pdf` quando esse nome está ausente ou vazio; e cortado em 260 caracteres, o que pode levar a
extensão junto. Quando uma requisição multipart trouxe várias partes de arquivo, ele indica qual foi
assinada.

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

Duas respostas do serviço de assinatura significam coisas opostas. `422 SIGNER_REJECTED` é o serviço
recusando a requisição (em geral o documento, às vezes outra coisa que a requisição indicou; `details`
diz qual): reenviá-la sem mudança traz a mesma resposta. `502 SIGNER_ERROR` é o serviço inacessível,
limitando requisições ou falhando antes de o documento chegar a ele: nada foi assinado, e o mesmo
documento pode ser reenviado quando o serviço voltar.

### `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.** Cada item, com falha ou
não, recebe o nome pela mesma regra do `fileName` da assinatura individual. A chamada inteira só falha
por algo que valha para todos eles: nenhum `sessionId` (`400 INVALID_REQUEST`), 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: `details` diz `Refused`, `Canceled` ou `Expired`. Não há o que esperar. |
| `404 DOCUMENT_NOT_FOUND` | Nenhum documento tem esse id, inclusive um que o serviço de assinatura não guarda. |
| `502 SIGNER_ERROR` | Não foi possível consultar o serviço de assinatura (inacessível, limitando requisições ou com falha); o documento não foi afetado. Pergunte de novo em instantes. |

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. Isso é o contrato, e não um descuido: trate um
`signatureId` como um token de portador do documento dele.

## 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": "…" } }
```

"O CPF", abaixo, significa o CPF e o CNPJ juntos numa sessão em nome de uma Empresa. O bloqueio por
PIN é a única exceção, e continua por CPF.

| Código | HTTP | Significado |
|---|---|---|
| `INVALID_REQUEST` | 400, 415 | Requisição malformada, ou um corpo em um media type que a operação não lê. `details` diz o que estava errado: por exemplo, um `sessionId` ausente, ou um CPF ou CNPJ com dígitos verificadores errados. |
| `INVALID_DOCUMENT` | 400 | Nenhum documento a assinar 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: todo candidato foi tentado, e nenhum conferiu. |
| `CERT_NOT_FOUND` | 404 | Na criação: nenhum certificado para o CPF em lugar nenhum, nem no cofre nem em uma estação de trabalho. 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 o tamanho e o limite. |
| `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: recusado, cancelado ou expirado. Não há o que esperar. |
| `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, um Certificado do Cofre não pôde ser aberto, ou tentativas recentes demais contra o certificado não conferiram. `details` diz qual. |
| `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, necessários para assinar, 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 que falta. 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 a requisição, em geral o próprio documento; `details` traz a explicação dele e diz qual. Repetir sem mudança dá o mesmo resultado. |
| `SIGN_TIMEOUT` | 422 | A assinatura não terminou dentro do tempo dado a uma 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, qualquer que seja o `cnpj` enviado com o CPF, ou nenhum. 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: inacessível, limitando requisições ou respondendo com uma falha própria. Em uma assinatura, só chega antes de o documento ter chegado ao serviço, então nada foi assinado; em uma busca, não diz nada sobre o documento. |

**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.

## A especificação

A descrição normativa do contrato é a **especificação de referência do SignSession**
(*SignSession — Documentação de referência*), escrita em português a partir da implementação deste
Servidor e adotada pelo autor da especificação do contrato no lugar do documento de integração
anterior. Peça uma cópia à Lacuna Software. O documento OpenAPI que um Servidor em execução serve em
`/openapi/v1.json` é gerado a partir do próprio Servidor e mantido idêntico ao que ele responde. Se os
dois discordarem, um deles está errado: informe a diferença em vez de supor que ela é intencional.

Os códigos que o documento OpenAPI marca como *TrustBridge extension* são os que a especificação
original não definia. A especificação atual os traz com a mesma marca; eles fazem parte do contrato
como qualquer outro.

### Vindo do documento de integração anterior

Um cliente escrito com base no documento de integração anterior, ou na outra implementação do
contrato (o CertSession), encontra estas respostas. Todas elas agora são o contrato, e duas são
silenciosas: produzem um comportamento diferente sem nenhum erro que o indique.

| O que um cliente encontra | O que fazer |
|---|---|
| **Toda rota tem o prefixo `/v1`.** Uma rota sem o prefixo responde `404 ENDPOINT_NOT_FOUND`. | Use o prefixo em todas as chamadas. Não há header nem media type de versão. |
| **Uma sessão é identificada pelo `sessionId`**, e não por um CPF. Só a criação recebe um CPF; status, informações do certificado, assinatura e desativação recebem o id. | Guarde o `sessionId`. Nenhuma chamada o recupera a partir de um CPF. |
| **`expiresIn` nunca é nulo.** Um `sessionDuration` omitido concede a janela padrão, e não uma ilimitada. | Silencioso. Leia `expiresIn` e `expiresAt` na resposta, em vez de supor a janela que você pediu. |
| **Assinar estende a sessão em vez de encerrá-la**, a menos que a implantação tenha definido uma Franquia de Assinaturas (uma franquia de 1 dá sessões de uso único). | Silencioso. Um cliente que cria uma sessão por documento continua funcionando, ao custo de mais uma confirmação de PIN a cada vez. Para encerrar a sessão junto com o documento, chame `POST /v1/sessions/deactivate`. |
| **Um `cpf` precisa ser um CPF real**: dígitos verificadores conferidos, valores de dígitos repetidos recusados. | `400 INVALID_REQUEST`. Valide os dígitos verificadores também no seu formulário; o `pattern` do schema (`^[0-9]{11}$`) não consegue expressá-los. |
| **Uma chamada de assinatura sem `sessionId` é malformada**, individual ou em lote. | `400 INVALID_REQUEST`: corrija a requisição. Um `401 SESSION_EXPIRED` significa criar uma sessão e tentar de novo. |
| **Os certificados são validados quanto à cadeia e à revogação**, não só quanto à expiração. | `CERT_NOT_ACTIVE` com `UNTRUSTED`, `REVOKED` ou `STALE` onde uma verificação só de expiração assinaria. |
| **O Servidor descarta carga.** | `429 SERVER_BUSY`. Tente de novo após um backoff próprio; usar lotes reduz a frequência com que isso acontece. |
| **O resultado é lido na busca.** Não há webhook a registrar. | Consulte `GET /v1/signatures/{signatureId}/document` até receber `200` ou `422`. |
| **Qualquer chave de API válida busca qualquer documento cujo `signatureId` conheça.** | Trate o id como um token de portador: não o registre em log nem o entregue a quem não deve ter o documento. |

## 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. Não reenvie sem mudança
  um documento recusado com `SIGNER_REJECTED`; reenvie um que recebeu `SIGNER_ERROR`.
- **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.
- **Envie `cnpj` só para assinar em nome de uma Empresa.** Uma sessão sem ele nunca se vincula a um
  e-CNPJ, e uma com ele nunca se vincula ao e-CPF da pessoa.

## 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 e um `cnpj` de exemplo. 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. Uma segunda requisição de criação faz o mesmo para uma
Empresa, enviando o `cnpj` do environment. A coleção grava `sessionId` e `signatureId` no
environment, então não é preciso colar nada entre um passo e outro. Quando a coleção e o
`/openapi/v1.json` publicado pelo Servidor discordarem, vale o documento publicado.
