Integration Guide
Signer is a web portal of documents that enables digital and electronic signatures as well as the creation of complex document flows. This guide presents the integration options so that your application can make the most of Signer's potential.
If this is your first integration, start with Getting started, which takes you from zero to your first signed document in about 15 minutes. This guide serves as a reference for the use cases.
Authentication
All API calls demand one API Key in order to communicate with Signer. This key must be placed in the header of all requests:
X-Api-Key: your-application|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Every API Key is associated with an Organization previously registered in Signer. Therefore, the scope of this key is restricted to that organization, that key will only allow you to access/create documents from that same organization.
To get an API key: if you have permission to create applications in the organization, create an application under Applications and generate the key with its Keys button, as shown in Getting started. Otherwise, ask your instance administrator for one.
Client libraries
Lacuna publishes a client library of the API for .NET, Java, Node.js and PHP. You create the client with the URL of your Signer instance and the API key, and call the APIs through its methods. You can also call the REST API directly from any language, as Getting started does with curl.
| Language | Package | Version | Install |
|---|---|---|---|
| .NET (.NET Framework 4.5 or later, .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 |
The samples on GitHub use these libraries, with one scenario per use case of this guide. To embed the signing screen in your web page, use the lacuna-signer-widget JavaScript library: see Embedded signature.
Features newer than the client libraries
The client libraries were released before Signer 3.x, so they lack some of its calls and fields. A method or field missing from a library does not mean that Signer lacks it: check the API reference.
| Feature | .NET | Java | Node.js | PHP |
|---|---|---|---|---|
Pause and resume, and the Paused status | No | No | No | No |
DocumentPaused and DocumentResumed webhook events | No | No | No | No |
DocumentDeleted webhook event | With Lacuna.Signer.Api 2.1.0 | Yes | Yes | No |
disableRefusal | No | No | No | No |
notificationLanguage of a participant | No | Yes | Yes | No |
notifiedEmailsData, timestampEnabled | No | No | No | No |
signerPositioningEnabled, automaticSignaturePositioning | No | No | No | No |
convertToPdfA, pdfAProfile | No | Yes | No | No |
Lacuna.Signer.Client 1.24.0 installs Lacuna.Signer.Api 1.67.0. To get the newer models, add Lacuna.Signer.Api 2.1.0 to your project as well, as the .NET samples do. Even then, no library's DocumentStatus has Paused, and the .NET GetDocumentDetailsAsync fails on a paused document: read it with a direct call.
For these features, call the REST API directly with the same URL and API key. The samples add a small helper to their base class: it sends JSON with the X-Api-Key header and throws on an answer other than 2xx, with the body, which for a 422 holds Signer's error code. Upload the file with the library as usual, and use the helper for the call the library lacks. For example, to pause and resume a document:
- C#
- Java
- Node.js
- PHP
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
private static readonly HttpClient HttpClient = new HttpClient();
// signerEndpoint and apiKey: the same values you give to the 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);
}
}
}
// Pause: the reason is optional
await CallSignerApiAsync(HttpMethod.Post, $"/api/documents/{documentId}/pause", new { reason = "Waiting for the contract review" });
var document = await CallSignerApiAsync(HttpMethod.Get, $"/api/documents/{documentId}");
// document["status"] is "Paused"
// Resume: no body
await CallSignerApiAsync(HttpMethod.Post, $"/api/documents/{documentId}/resume");
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
// signerEndpoint and apiKey: the same values you give to the 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)) {
// A POST without a body still sends 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);
}
// Pause: the reason is optional
JsonObject pauseRequest = new JsonObject();
pauseRequest.addProperty("reason", "Waiting for the contract review");
callSignerApi("POST", "/api/documents/" + documentId + "/pause", pauseRequest);
JsonObject document = callSignerApi("GET", "/api/documents/" + documentId, null).getAsJsonObject();
// document.get("status").getAsString() is "Paused"
// Resume: no body
callSignerApi("POST", "/api/documents/" + documentId + "/resume", null);
// Node.js 18 or later, for the global fetch.
// basePath and apiKey: the same values you give to the client's Configuration
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;
}
// Pause: the reason is optional
await callSignerApi("POST", `/api/documents/${documentId}/pause`, { reason: "Waiting for the contract review" });
const document = await callSignerApi("GET", `/api/documents/${documentId}`);
// document.status is "Paused"
// Resume: no body
await callSignerApi("POST", `/api/documents/${documentId}/resume`);
// A method of your class; $this->endPoint and $this->apiKey: the same values you give to the SignerClient
protected function callSignerApi($method, $path, $body = null)
{
$options = [
'headers' => ['X-Api-Key' => $this->apiKey],
'http_errors' => false, // read the error body instead of letting Guzzle throw
'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);
}
// Pause: the reason is optional
$this->callSignerApi("POST", "/api/documents/$documentId/pause", ['reason' => "Waiting for the contract review"]);
$document = $this->callSignerApi("GET", "/api/documents/$documentId");
// $document['status'] is "Paused"
// Resume: no body
$this->callSignerApi("POST", "/api/documents/$documentId/resume");
To create a document with a new field, send the whole POST /api/documents body through the helper, with the id, name and contentType of the upload in files. The samples linked from each feature's page do that.
A Signer instance older than a feature does not reject its field: it creates the document without it. Read the document back (GET /api/documents/{id}) and check that the field is there, as the samples do.
How the flow works
In practice, an integration with Signer follows four stages. Each stage has a dedicated page in this guide:
- Create documents: upload the file, assemble the participant flow and (optionally) sign inside your own application.
- Track documents: check the status, send reminders and receive webhook notifications.
- Download and list: get the document versions and list documents by different criteria.
- Validate signatures: validate signatures using a validation key or the signed file.
And throughout the document's life you may need lifecycle operations: cancel, refuse, pause and resume, submit a new version, delete or restore.
Use cases
| Use case | What you do |
|---|---|
| Create documents | Upload, flow creation, embedded signature, pre-positioning signatures and envelopes (multiple files) |
| Document field reference | Look up the participant fields, the identifier types and the participant and document options |
| Track documents | Check document and participant status, send reminders and set up webhooks |
| Download and list | Download the different document versions and list documents |
| Validate signatures | Validate signatures by key or by signed file |
| Document lifecycle | Cancel, refuse, pause and resume, submit a new version, delete and restore |
| Contacts and identifiers | Manage the organization's contacts and a user's identifiers |
Error responses
The APIs answer with these HTTP status codes:
| Code | Meaning |
|---|---|
200 (OK) | Request processed. The response body depends on the API: see its entry in the API reference. |
400 (Bad Request) | Syntax error, for instance a required field that was not sent. |
401 (Unauthorized) | The API key was not sent or is invalid. |
403 (Forbidden) | The API key is valid, but its application does not have permission for the requested operation. |
422 (Unprocessable Entity) | API error, for instance an entity ID that is not found. The body is an ErrorModel. |
An ErrorModel has three fields:
| Field | Type | Content |
|---|---|---|
code | string | The error code. Test this field in your code, not the message. |
message | string | A description of the error. |
details | object, with string values | Additional information about the error. It may be null. |
{
"code": "DocumentNotFound",
"message": "...",
"details": null
}
The API reference lists the main codes of a 422 response: CertificateNotFound, DocumentNotFound, FolderNotFound, CpfMismatch, CpfNotExpected, InvalidFlowAction and DocumentInvalidKey. The list is not comprehensive, and new codes may be added at any time without notice, so handle codes you do not know. Other pages describe the codes of their operations, such as DocumentHasNoSignatures when downloading a version.