Guia de Integração
O Signer é um portal de documentos que permite a realização de assinaturas digitais e eletrônicas, bem como a criação de fluxos complexos. Este guia apresenta as opções de integração para que sua aplicação aproveite ao máximo o potencial do Signer.
Se é a sua primeira integração, comece pelos Primeiros passos, que levam você do zero ao primeiro documento assinado em cerca de 15 minutos. Este guia serve como referência dos casos de uso.
Autenticação
Todas as chamadas de API exigem uma chave de acesso de API (API Key). Essa chave deve ser colocada no cabeçalho de todas as requisições:
X-Api-Key: sua-aplicacao|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Toda chave de API está associada a uma Organização previamente cadastrada no Signer. Sendo assim, o escopo dessa chave fica restrito àquela organização, isto é, aquela chave só permitirá acessar/criar documentos daquela organização.
Para obter uma chave de API: se você tem permissão para criar aplicações na organização, crie uma aplicação em Aplicações e gere a chave com o botão Chaves dela, como mostram os Primeiros passos. Caso contrário, peça uma chave ao administrador da sua instância.
Bibliotecas cliente
A Lacuna publica uma biblioteca cliente da API para .NET, Java, Node.js e PHP. Você cria o cliente com a URL da sua instância do Signer e a chave de API, e chama as APIs pelos métodos dele. Também é possível chamar a API REST diretamente de qualquer linguagem, como fazem os Primeiros passos com curl.
| Linguagem | Pacote | Versão | Instalação |
|---|---|---|---|
| .NET (.NET Framework 4.5 ou posterior, .NET Standard 2.0) | Lacuna.Signer.Client | 1.24.0 | dotnet add package Lacuna.Signer.Client |
| Java | com.lacunasoftware.signer:signer-client | 2.14.0 | Gradle: implementation 'com.lacunasoftware.signer:signer-client:2.14.0' |
| Node.js | signer-node-client | 1.1.0 | npm install signer-node-client |
| PHP | lacuna/signer-client | 1.4.10 | composer require lacuna/signer-client |
Os exemplos no GitHub usam essas bibliotecas, com um cenário por caso de uso deste guia. Para embutir a tela de assinatura na sua página web, use a biblioteca JavaScript lacuna-signer-widget: veja Assinatura embutida.
Recursos mais novos que as bibliotecas cliente
As bibliotecas cliente foram publicadas antes do Signer 3.x, por isso não têm algumas das chamadas e dos campos dele. Um método ou campo que falta numa biblioteca não quer dizer que o Signer não o tenha: confira a referência da API.
| Recurso | .NET | Java | Node.js | PHP |
|---|---|---|---|---|
Pausar e retomar, e o status Paused | Não | Não | Não | Não |
Eventos de webhook DocumentPaused e DocumentResumed | Não | Não | Não | Não |
Evento de webhook DocumentDeleted | Com o Lacuna.Signer.Api 2.1.0 | Sim | Sim | Não |
disableRefusal | Não | Não | Não | Não |
notificationLanguage de um participante | Não | Sim | Sim | Não |
notifiedEmailsData, timestampEnabled | Não | Não | Não | Não |
signerPositioningEnabled, automaticSignaturePositioning | Não | Não | Não | Não |
convertToPdfA, pdfAProfile | Não | Sim | Não | Não |
O Lacuna.Signer.Client 1.24.0 instala o Lacuna.Signer.Api 1.67.0. Para ter os modelos mais novos, adicione também o Lacuna.Signer.Api 2.1.0 ao seu projeto, como fazem os exemplos em .NET. Mesmo assim, o DocumentStatus de nenhuma biblioteca tem Paused, e o GetDocumentDetailsAsync do .NET falha num documento pausado: leia-o com uma chamada direta.
Para esses recursos, chame a API REST diretamente, com a mesma URL e a mesma chave de API. Os exemplos acrescentam à sua classe base uma pequena função auxiliar: ela envia JSON com o header X-Api-Key e lança uma exceção numa resposta diferente de 2xx, com o corpo, que num 422 traz o código de erro do Signer. Faça o upload do arquivo com a biblioteca, como de costume, e use a função auxiliar na chamada que falta na biblioteca. Por exemplo, para pausar e retomar um documento:
- C#
- Java
- Node.js
- PHP
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
private static readonly HttpClient HttpClient = new HttpClient();
// signerEndpoint e apiKey: os mesmos valores que você passa ao SignerClient
private async Task<JToken> CallSignerApiAsync(HttpMethod method, string path, object body = null)
{
using (var request = new HttpRequestMessage(method, signerEndpoint.TrimEnd('/') + path))
{
request.Headers.TryAddWithoutValidation("X-Api-Key", apiKey);
if (body != null)
{
request.Content = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
}
using (var response = await HttpClient.SendAsync(request))
{
var content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"{method} {path} answered {(int)response.StatusCode}: {content}");
}
return string.IsNullOrEmpty(content) ? null : JToken.Parse(content);
}
}
}
// Pausar: o motivo é opcional
await CallSignerApiAsync(HttpMethod.Post, $"/api/documents/{documentId}/pause", new { reason = "Aguardando a revisão do contrato" });
var document = await CallSignerApiAsync(HttpMethod.Get, $"/api/documents/{documentId}");
// document["status"] é "Paused"
// Retomar: sem corpo
await CallSignerApiAsync(HttpMethod.Post, $"/api/documents/{documentId}/resume");
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
// signerEndpoint e apiKey: os mesmos valores que você passa ao SignerClient
JsonElement callSignerApi(String method, String path, JsonElement body) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(signerEndpoint.replaceAll("/$", "") + path).openConnection();
connection.setConnectTimeout(30000);
connection.setReadTimeout(100000);
connection.setRequestMethod(method);
connection.setRequestProperty("X-Api-Key", apiKey);
if (!"GET".equals(method)) {
// Um POST sem corpo também envia Content-Length: 0
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
try (OutputStream output = connection.getOutputStream()) {
if (body != null) {
output.write(body.toString().getBytes(StandardCharsets.UTF_8));
}
}
}
int status = connection.getResponseCode();
InputStream stream = status < 400 ? connection.getInputStream() : connection.getErrorStream();
String content = "";
if (stream != null) {
try (Scanner scanner = new Scanner(stream, "UTF-8")) {
scanner.useDelimiter("\\A");
content = scanner.hasNext() ? scanner.next() : "";
}
}
if (status < 200 || status >= 300) {
throw new IOException(String.format("%s %s answered %d: %s", method, path, status, content));
}
return content.isEmpty() ? null : new JsonParser().parse(content);
}
// Pausar: o motivo é opcional
JsonObject pauseRequest = new JsonObject();
pauseRequest.addProperty("reason", "Aguardando a revisão do contrato");
callSignerApi("POST", "/api/documents/" + documentId + "/pause", pauseRequest);
JsonObject document = callSignerApi("GET", "/api/documents/" + documentId, null).getAsJsonObject();
// document.get("status").getAsString() é "Paused"
// Retomar: sem corpo
callSignerApi("POST", "/api/documents/" + documentId + "/resume", null);
// Node.js 18 ou posterior, pelo fetch global.
// basePath e apiKey: os mesmos valores que você passa à Configuration do cliente
async function callSignerApi(method: string, apiPath: string, body?: object): Promise<any> {
const response = await fetch(basePath.replace(/\/$/, "") + apiPath, {
method: method,
headers: { "X-Api-Key": apiKey, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const content = await response.text();
if (!response.ok) {
throw new Error(`${method} ${apiPath} answered ${response.status}: ${content}`);
}
return content ? JSON.parse(content) : null;
}
// Pausar: o motivo é opcional
await callSignerApi("POST", `/api/documents/${documentId}/pause`, { reason: "Aguardando a revisão do contrato" });
const document = await callSignerApi("GET", `/api/documents/${documentId}`);
// document.status é "Paused"
// Retomar: sem corpo
await callSignerApi("POST", `/api/documents/${documentId}/resume`);
// Um método da sua classe; $this->endPoint e $this->apiKey: os mesmos valores que você passa ao SignerClient
protected function callSignerApi($method, $path, $body = null)
{
$options = [
'headers' => ['X-Api-Key' => $this->apiKey],
'http_errors' => false, // ler o corpo do erro em vez de o Guzzle lançar a exceção
'connect_timeout' => 30,
'timeout' => 100,
];
if ($body !== null) {
$options['json'] = $body;
}
$response = (new \GuzzleHttp\Client())->request($method, rtrim($this->endPoint, '/') . $path, $options);
$content = (string)$response->getBody();
$status = $response->getStatusCode();
if ($status < 200 || $status >= 300) {
throw new \RuntimeException("$method $path answered $status: $content");
}
return $content === '' ? null : json_decode($content, true);
}
// Pausar: o motivo é opcional
$this->callSignerApi("POST", "/api/documents/$documentId/pause", ['reason' => "Aguardando a revisão do contrato"]);
$document = $this->callSignerApi("GET", "/api/documents/$documentId");
// $document['status'] é "Paused"
// Retomar: sem corpo
$this->callSignerApi("POST", "/api/documents/$documentId/resume");
Para criar um documento com um campo novo, envie o corpo inteiro de POST /api/documents pela função auxiliar, com o id, o name e o contentType do upload em files. Os exemplos indicados na página de cada recurso fazem isso.
Uma instância do Signer mais antiga que um recurso não rejeita o campo dele: cria o documento sem ele. Leia o documento de volta (GET /api/documents/{id}) e confira que o campo está lá, como fazem os exemplos.
Como o fluxo funciona
Na prática, uma integração com o Signer segue quatro etapas. Cada etapa tem uma página dedicada neste guia:
- Criar documentos: envie o arquivo, monte o fluxo de participantes e (opcionalmente) assine dentro da sua própria aplicação.
- Acompanhar documentos: verifique o status, envie lembretes e receba notificações por webhook.
- Baixar e listar: obtenha as versões do documento e liste documentos por diferentes critérios.
- Validar assinaturas: valide as assinaturas por chave de validação ou pelo arquivo assinado.
E, ao longo da vida do documento, você pode precisar de operações de ciclo de vida: cancelar, recusar, pausar e retomar, enviar nova versão, excluir ou restaurar.
Casos de uso
| Caso de uso | O que você faz |
|---|---|
| Criar documentos | Upload, criação do fluxo, assinatura embutida, pré-posicionamento de assinaturas e envelopes (múltiplos arquivos) |
| Referência dos campos do documento | Consultar os campos do participante, os tipos de identificador e as opções do participante e do documento |
| Acompanhar documentos | Verificar status do documento e dos participantes, enviar lembretes e configurar webhooks |
| Baixar e listar | Baixar as diferentes versões do documento e listar documentos |
| Validar assinaturas | Validar assinaturas por chave ou por arquivo assinado |
| Ciclo de vida do documento | Cancelar, recusar, pausar e retomar, enviar nova versão, excluir e restaurar |
| Contatos e identificadores | Gerenciar os contatos da organização e os identificadores de um usuário |
Respostas de erro
As APIs respondem com estes códigos de status HTTP:
| Código | Significado |
|---|---|
200 (OK) | Requisição processada. O corpo da resposta depende da API: veja a entrada dela na referência da API. |
400 (Bad Request) | Erro de sintaxe, por exemplo um campo obrigatório que não foi enviado. |
401 (Unauthorized) | A chave de API não foi enviada ou é inválida. |
403 (Forbidden) | A chave de API é válida, mas a aplicação dela não tem permissão para a operação pedida. |
422 (Unprocessable Entity) | Erro da API, por exemplo um ID de entidade que não foi encontrado. O corpo é um ErrorModel. |
Um ErrorModel tem três campos:
| Campo | Tipo | Conteúdo |
|---|---|---|
code | string | O código do erro. Teste este campo no seu código, e não a mensagem. |
message | string | Uma descrição do erro. |
details | objeto, com valores string | Informações adicionais sobre o erro. Pode ser null. |
{
"code": "DocumentNotFound",
"message": "...",
"details": null
}
A referência da API lista os principais códigos de uma resposta 422: CertificateNotFound, DocumentNotFound, FolderNotFound, CpfMismatch, CpfNotExpected, InvalidFlowAction e DocumentInvalidKey. A lista não é completa, e novos códigos podem ser incluídos a qualquer momento sem aviso, então trate também os códigos que você não conhece. Outras páginas descrevem os códigos das suas operações, como DocumentHasNoSignatures ao baixar uma versão.