Pular para o conteúdo principal

REST API: the SignSession Contract

The Server implements the SignSession Contract, the API a client application calls to open a CPF-bound Signing Session and sign documents under it. This page covers authentication, the session lifecycle, every operation, the error envelope, and where this Server deliberately answers differently from the contract's other implementation.

tip

While the Server is running, the machine-readable contract is served at /openapi/v1.json (OpenAPI 3.1) and a browsable reference at /scalar. Both answer without a credential. This page is the conceptual guide; the published document is the source of truth for field-level detail.

Contract version and base path​

Every operation is served under /v1, and nothing else answers: a call to a bare route is refused with 404 ENDPOINT_NOT_FOUND. v1 is the contract version, the only version a client should pin to. The version field in the OpenAPI document is the Server's build version and says nothing about wire compatibility.

Authentication​

Every operation requires the X-API-Key header, carrying an API key issued to a Client Application in the admin console. Any of an application's keys authenticates the full surface; the key scopes nothing within it. Missing, unknown, revoked, expired and disabled-application keys are all rejected identically with 401 UNAUTHORIZED.

Treat a sessionId and a signatureId as bearer tokens for the things they name: any accepted key can use a session or fetch a document whose id it holds.

Every response, errors included, carries a Server-minted X-Correlation-Id. Quote it when reporting a problem; it leads an operator to the request's whole trail.

The session lifecycle​

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. Create a session with the holder's CPF and PIN. The Server tries the Vault Certificates held for that CPF first; failing that, it offers the PIN to the workstations holding a certificate for the CPF, where it is compared against the PIN in use there and never presented to the token. If no workstation has that PIN cached, the holder is prompted at their Tray App and this request stays open until they answer or the prompt times out (60 s by default). Allow for that in your client's timeout. Whichever certificate confirms the PIN binds the session.
  2. Sign under the session, one document at a time or in a batch. The whole signature runs inside the request and typically takes a few seconds.
  3. Fetch the signed PDF by the signature id. This is also where a signature's outcome is read.
  4. Deactivate the session when the work is done, or let its idle window lapse.

At most one session is active per CPF; creating another replaces it. Signing extends the idle window; reading status does not. A session also ends when the deployment's optional Signature Allowance is spent.

Operations​

POST /v1/sessions: create a Signing Session​

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" }'
Field
cpfrequiredEleven digits, no punctuation. Check digits are verified, and the eleven repeated-digit values are refused, with 400 INVALID_REQUEST.
pinrequiredThe certificate's PIN, whichever custody it is under. For a Vault Certificate, the password that opens its file.
sessionDurationoptionalSeconds. Omitted grants the Server's default window; above the maximum is clamped, not refused.
emailoptionalA fallback address for the signer, used only where the bound certificate names none. Without it such a certificate is refused at signing time with CERT_INCOMPLETE_IDENTITY.
{ "sessionId": "8f3c…", "cpf": "12345678909", "expiresIn": 3600, "expiresAt": "2026-09-21T15:04:05Z" }

expiresIn is always the window actually granted. Refusals: 404 CERT_NOT_FOUND (no certificate for the CPF anywhere), 422 CERT_NOT_ACTIVE (every certificate for the CPF carries a non-active verdict), 401 INVALID_PIN, 422 PIN_NOT_AVAILABLE (prompt timed out, declined or could not be shown), 422 WORKSTATION_OFFLINE (every machine holding an active certificate is offline), 429 TOO_MANY_PIN_ATTEMPTS (per-CPF lockout).

GET /v1/sessions/status?sessionId=…: check a session​

Answers the session's state and how long it has left. An id that names nothing active still answers 200 with active: false; omitting the id is 400. Safe to poll; never extends the window. There is no way to ask whether a CPF has a session: a client that lost the id creates a new session.

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

POST /v1/sessions/deactivate: end a session​

curl -s https://trustbridge.example.com/v1/sessions/deactivate \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" -d '{ "sessionId": "8f3c…" }'

Ends the session immediately and drops the secret it rested on (the PIN a workstation had cached for it, or the password the Server held to open a Vault Certificate). Not idempotent: with nothing active it answers 401 SESSION_EXPIRED. The response reports the CPF read off the session, the id, active: false and endedAt.

GET /v1/certificates/info?sessionId=…: describe the bound certificate​

The certificate the session is bound to, as the platform last recorded it: cpf (the certificate's subject, which need not equal the session's CPF), subjectName, issuer, serialNumber, notBefore, notAfter and status. Check status before signing: a verdict can turn while a session is live, and only ACTIVE can sign. Requires an active session; never extends it.

POST /v1/signatures: sign a document​

Two body shapes. JSON with the document base64-encoded:

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\" }"

Or multipart, with a sessionId field and the PDF as a file part (the file name is taken from the part):

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

Answers 202:

{ "signatureId": "a1b2…", "status": "PROCESSING", "acceptedAt": "2026-09-21T14:10:00Z" }

A 202 is not "it worked". Once the document has reached the signature service this operation answers the id whatever happens afterwards, because the id is the only way back to a document that may have been signed. The fetch is where the outcome is read. signatureId is Lacuna Signer's own document id. Only PDFs are signed (422 UNSUPPORTED_DOCUMENT); a document over Signing:MaxDocumentBytes (10 MB by default, measured on decoded bytes) answers 413 DOCUMENT_TOO_LARGE; no document at all is 400 INVALID_DOCUMENT; a missing sessionId is 400 INVALID_REQUEST.

POST /v1/signatures/batch: sign several documents​

multipart/form-data only: a sessionId field and one file part per document, up to Signing:MaxBatchItems (50 by default).

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"

Answers 202 with one result per document, in the order the parts arrived:

{
"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"
}

Each document is independent: one that failed carries status: "ERROR" and a code from the same vocabulary while its siblings carry ids. Read the results, not the status code. The whole call fails only for something true of all of them: no live session, a certificate that cannot sign, more documents than the limit (400 INVALID_REQUEST), or none (400 INVALID_DOCUMENT). Documents are signed several at a time up to Signing:SignerConcurrency, and the whole call is awaited, so a large batch takes as long as signing every document in it. A batch is one call for the throttle and for the Signature Allowance.

GET /v1/signatures/{signatureId}/document: fetch the signed document​

curl -s https://trustbridge.example.com/v1/signatures/a1b2…/document \
-H "X-API-Key: $API_KEY" -o signed.pdf -w '%{http_code}\n'
StatusMeaning
200The signed PDF, as the response body.
409 DOCUMENT_NOT_READYStill being signed. Ask again.
422 DOCUMENT_UNAVAILABLEEnded without a signed version: refused, cancelled or expired. details says which. Nothing to wait for.
404 DOCUMENT_NOT_FOUNDNo document goes by this id.
502 SIGNER_ERRORThe signature service could not be asked; says nothing about the document.

Nothing is stored by the Server, so how long a document stays fetchable is Lacuna Signer's retention. Any valid API key may fetch any document whose id it knows, including one created by a different Client Application, matching the reference implementation deliberately.

The error envelope​

Every error arrives in one envelope. code is a string, not a closed set: treat an unrecognised one as a failure you did not expect rather than a parse error. Branch on code, never on message.

{ "error": { "code": "CERT_NOT_ACTIVE", "message": "…", "details": "…" } }
CodeHTTPMeaning
INVALID_REQUEST400, 415Malformed request, or a media type the operation does not read. details names the field.
INVALID_DOCUMENT400No document arrived, or it was empty.
UNAUTHORIZED401The API key was not accepted.
SESSION_EXPIRED401No active Signing Session goes by the sessionId named: expired, deactivated, replaced, exhausted or stranded. Create a new session.
INVALID_PIN401The PIN opened no Vault Certificate for the CPF and differs from the one in use at every workstation holding one.
CERT_NOT_FOUND404Creating: no certificate for the CPF anywhere. Reading or signing: the bound certificate is no longer held.
DOCUMENT_NOT_FOUND404No document goes by this signature id.
ENDPOINT_NOT_FOUND404No operation answers this method and path. Check the /v1 prefix.
DOCUMENT_NOT_READY409Still being signed. Worth asking again.
DOCUMENT_TOO_LARGE413The document or the request body exceeds the Server's limit; details gives both sizes.
CERT_NOT_ACTIVE422Every certificate for the CPF, or the bound one, carries a non-active verdict (EXPIRED, REVOKED, UNTRUSTED, STALE).
DOCUMENT_UNAVAILABLE422The document ended without a signed version.
PIN_NOT_AVAILABLE422No comparison could be made: a prompt timed out, was declined or could not be shown, or a Vault Certificate could not be opened.
WORKSTATION_OFFLINE422Every active certificate for the CPF is on a workstation, and every one of those is offline (after the reconnect grace, on a sign).
CERT_DATA_UNAVAILABLE422The certificate's own data is not held for it.
CERT_INCOMPLETE_IDENTITY422The certificate does not name its holder well enough to sign; details names the field. An address is the one field the session's email can supply.
UNSUPPORTED_DOCUMENT422Not a PDF.
SIGNER_REJECTED422The signature service refused the document; details carries its account.
SIGN_TIMEOUT422The signature did not finish inside the request budget (60 s by default); nothing was signed.
SERVER_BUSY429The Server is running as many contract calls as it accepts and its queue is full. Nothing about the request was refused: retry with a backoff of your own, with jitter. No Retry-After is sent.
TOO_MANY_PIN_ATTEMPTS429The per-CPF failed-PIN lockout is in force. About one CPF, across every API key; retrying makes it worse. Clears as the oldest failures age out (15 min window by default).
INTERNAL_ERROR500The request could not be completed; safe to retry.
SIGNER_ERROR502The signature service could not be asked. On a sign this only arrives before the document reached it, so nothing was signed.

Two different 429s. SERVER_BUSY is about the Server at this moment and clears on its own; TOO_MANY_PIN_ATTEMPTS is about one person's certificate and does not clear by retrying. A client that branches on the status alone will treat them alike, which is why they are separate codes.

Where this Server differs from the reference implementation​

The contract's other implementation, CertSession, is deployed and integrated against, and this Server matches it almost everywhere. Seven answers differ on purpose. Two are silent: they produce different behaviour without any error saying so.

#What differsHow a client meets it
1expiresIn is never null. An omitted sessionDuration grants the Server's default window rather than an unlimited one. There is no unlimited session.Silent. Read expiresIn and expiresAt off the response rather than assuming the window you asked for.
2Signing does not end the session; it extends it, unless the deployment set a Signature Allowance. The reference ends a session after every signature, so a client written against it creates a session per document.Silent. Such a client notices nothing except that its extra sessions cost the holder another PIN confirmation. To end a session with the document, call POST /v1/sessions/deactivate. An allowance of 1 on the Server reproduces the reference.
3Certificate validation checks the chain and revocation, not only the expiry date.Refused with CERT_NOT_ACTIVE where the reference would sign. A certificate that signs against CertSession can be UNTRUSTED here if the deployment's anchors do not cover its issuer.
4Fetching a signed document is not scoped to the creating Client Application.Nothing on the wire; the reference behaves identically. Listed because parity was chosen over isolation, knowingly, for this contract generation.
5A cpf has to be a real CPF: check digits verified, repeated-digit values refused.Refused with 400 INVALID_REQUEST where the reference would create a session. Validate check digits in your form too. The schema's pattern is still ^[0-9]{11}$, because a modulus is not a regular expression.
6A signing request with no sessionId is malformed, not expired.400 INVALID_REQUEST rather than 401 SESSION_EXPIRED. A 401 means create a session and retry; this 400 means the request was built wrong.
7This Server sheds load. A call arriving while the Server is already running as many as it accepts is refused rather than queued indefinitely.429 SERVER_BUSY, which the reference never answers. Retry after a backoff of your own. Batching reduces how often you meet it.

Where this Server answers more than the reference describes (seven fields on certificates/info, an acceptedAt on the batch, a wider error vocabulary), nothing a client expects is missing.

The Certificate Vault is a capability the reference lacks rather than an answer that differs: POST /v1/sessions tries the PIN against Vault Certificates before any machine, so a session can bind with no workstation involved and no prompt shown. The one visible asymmetry: a vault-bound session rests on a secret held only in the Server's memory, so a Server restart ends it and the client meets SESSION_EXPIRED on its next call, recovering by creating a new session with the same PIN. A workstation-bound session survives the same restart.

Writing a robust client​

  • Store the sessionId. No call recovers it from a CPF.
  • Be ready for SESSION_EXPIRED on any call, and recover by creating a new session. You cannot tell whether a deployment set a Signature Allowance, and are not meant to.
  • Read the outcome at the fetch. A 202 says the document was accepted, not signed. Poll the document endpoint on 409; stop on 200 or 422.
  • Set a generous timeout on POST /v1/sessions. A creation that goes to a Tray prompt waits for a human, up to the prompt timeout plus a few seconds.
  • Branch on error.code, and handle the two 429s differently.
  • Batch when you have many documents. One call, one throttle slot, one allowance charge.
  • Send email if any certificate you sign with might carry no address.

Trying the contract by hand​

Lacuna Software can provide a Postman collection of the contract as this Server implements it, with an environment carrying a small base64-encoded PDF. Import both, paste an API key issued on the Client Applications screen into apiKey, and run the requests in order: create a session (watch the Tray App prompt if no PIN is cached), sign a document, fetch it. The collection captures sessionId and signatureId into the environment so nothing needs pasting between steps. Docs/openapi in the product repository holds the committed contract and the divergence notes the collection was built from.

View as Markdown