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, optionally on behalf of a Company, and sign documents under it. This page covers authentication, the session lifecycle, Per-Transaction PIN and Sessions without a PIN, documents signed with REST PKI Core, every operation (Hash Signing included), the error envelope, and which document is the contract's specification.

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, with the error's code; it leads an operator to the request's whole trail, where the Audit Log records the code each call was answered with (Server 1.24.0).

The session lifecycle​

POST /v1/sessions (cpf, pin[, cnpj]) ──▶ 201 { sessionId, expiresIn, transactionPin, certificate, chain, … }
│
├──▶ 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[, pin]) ──▶ 202 { signatureId, fileName }
├──▶ POST /v1/signatures/batch (sessionId, files[][, pin]) ──▶ 202 { results[] }
│ └──▶ GET /v1/signatures/{signatureId}/document ──▶ 200 PDF | 409 | 422
├──▶ POST /v1/signatures/hashes (sessionId, digestAlgorithm, hashes[][, pin]) ──▶ 200 { signatures[] }
│ (Client Applications granted Hash Signing only)
└──▶ POST /v1/sessions/deactivate (sessionId) ──▶ 200
  1. Create a session with the holder's CPF and PIN, and a CNPJ to sign on behalf of a Company. The Server tries the Vault Certificates held for that CPF first. Failing that, the workstations holding a certificate for the CPF are asked to prove they hold the PIN in use there, and the Server checks each proof against the PIN you sent. The PIN goes no further than the Server: no workstation is sent it, and it is 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. A Client Application an administrator switched to Sessions without a PIN may leave the PIN out, and then nobody is asked anything at creation.
  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, or per CPF and CNPJ for a session for a Company; creating another for the same one replaces it. Signing extends the idle window; reading status does not. A session also ends when its Signature Allowance is spent: the number of signing calls an administrator of the Server may set for the Client Application that created the session (Server 1.17.0; before it, one figure for the whole deployment). Nothing in a response says whether a session has one, so be ready for SESSION_EXPIRED before expiresAt.

A session bound to a Vault Certificate rests on a secret held only in the Server's memory, so a Server restart ends it: the client meets SESSION_EXPIRED on its next call and recovers by creating a new session with the same PIN. A session bound to a workstation survives the same restart.

A session bound to a workstation that an administrator removes from the platform (Server 1.21.0) never signs again: every signing call under it answers 422 WORKSTATION_OFFLINE with details workstation removed, and it expires on its own, so create a new one. A call already under way when the machine is removed may answer without those details; the next one carries them. A machine that is only disconnected answers the same code without them, and its session signs again once it is back.

Workstation releases and session creation

From Server 1.15.0 a workstation needs Workstation 1.5.0 or later to connect at all, and Workstation 1.6.0 to serve a session that asks for the PIN on every signing call (Per-Transaction PIN). For a Client Application with that mode on, a machine still on 1.5.0 counts as offline, so a holder whose machines all run it is answered 422 WORKSTATION_OFFLINE until one of them is updated. Every other session is served by 1.5.0 as before. See Workstation installation.

Per-Transaction PIN​

A Client Application can be made to ask for the holder's PIN on every signing call, not only when the session is created. It is a TrustBridge extension (Server 1.15.0), and it is off until an administrator of the Server turns it on for your Client Application: ask the deployment's operator, naming the application. It is set per Client Application, never per API key, and it applies only to sessions created after it is turned on. Turning it off again likewise changes only the sessions created after that.

Session creation does not change: POST /v1/sessions still takes the holder's pin and confirms it as before, unless the application also has Sessions without a PIN. What changes is signing. Every session answers transactionPin, from POST /v1/sessions and from GET /v1/sessions/status while it is active, and its value decides what your screen shows:

transactionPinWhere the certificate isWhat your application does on each signing call
nullAnywhereNothing new. The session authorizes its own signatures, as every session does without the switch. Send no pin.
"workstation"On the holder's machineTell your user to look at their Tray App, and send no pin. Each call waits while the Tray shows your Client Application's name and what the call signs (the documents' names, or the number of digests) and asks for the PIN. A batch asks once for the whole call.
"client"In the Server's Certificate Vault, where nobody can be promptedShow a PIN field for every transaction and send what is typed as pin on each signing call: a JSON member, or a pin form field beside a batch's documents. It is checked once per call, before anything is signed, and kept for nothing afterwards.

Under "workstation", the wait for the holder is bounded by the Server (Signing:AuthorizationTimeout, 60 s by default) and is not counted against the time the signing itself is given, so allow for both in your client's timeout. A machine that is away when the call arrives is first waited for briefly (Signing:WorkstationReconnectGrace, 15 s by default).

The refusals of this mode are answered before anything is signed: nothing is created at the signature service, and the same request can be sent again once its cause is dealt with.

AnswerMeaningWhat to do
400 INVALID_REQUESTA pin was sent on a session whose transactionPin is null or "workstation", or none on a "client" session's call.Fix the client. A pin on an ordinary session used to be ignored and is now refused, so that a wiring mistake shows at once.
422 PIN_NOT_AVAILABLE, details declinedThe holder refused the call at the Tray.Do not resend it unasked.
422 PIN_NOT_AVAILABLE, details timed outNobody answered in time.Ask your user to be at their machine, and try again.
422 PIN_NOT_AVAILABLE, details no Tray at the consoleNothing at the machine's console could show the prompt: the Tray App is not running for whoever is signed in there, is too old to show a signing call, or the machine was starting an update.Ask your user to sign in there and open TrustBridge from the Start menu (Workstation 1.9.0), or update the Tray App. These details do not say which cause it was; the Server's operator sees that on the admin console.
422 PIN_NOT_AVAILABLE, details token lockedThe token's PIN is blocked; only its PUK unlocks it.Resending changes nothing.
422 PIN_NOT_AVAILABLE, details no A1 PIN at the workstationThe certificate is an A1 file and the machine has no A1 PIN set for it, or cannot read the ones it has.Ask your user to set one in the Tray App.
422 PIN_NOT_AVAILABLE, details too many calls waitingThe holder's machine already has as many calls waiting at its Tray as the Server lets it hold (two by default), so this one was not shown.Send one call at a time per holder, and send this one again once those are answered.
422 PIN_NOT_AVAILABLE under "client"The certificate could not be opened: it is resting after too many failed PINs, which clears with time, or it cannot be opened on this Server, which is for the operator. details says which.As details says.
422 WORKSTATION_OFFLINEThe holder's machine is not connected, runs a release too old to ask, or dropped while its holder was at the prompt.The session stays valid; it signs again once the machine reconnects.
401 INVALID_PIN under "client"The pin was wrong.Let your user type it again. It counts against the CPF's failed-PIN lockout, as a wrong pin at creation does.
429 TOO_MANY_PIN_ATTEMPTS under "client"That lockout is in force.details gives the allowance and the window; no PIN is tried until the oldest failures age out.

One case is answered later: a call that loses its authorization part of the way through, because the holder's machine dropped and came back, or its token refused the PIN typed for the call when it came to sign. That is PIN_NOT_AVAILABLE too, but answered where any failure at that point is: after the signature service has the document (a single document is then answered 202 with its id, and a batch item carries the error), or after some digests were signed, which the refusal says. Its details is the machine's own sentence.

Keep one long-lived session per user in this mode, rather than one per transaction. Ask for a sessionDuration up to the deployment's maximum (Sessions:MaxDuration, 24 hours by default; read the window granted off expiresIn), and create a new session only when a call answers SESSION_EXPIRED. The session no longer authorizes anything on its own, so a long one leaves nothing more open than a short one would. A short one costs the holder instead: under "workstation", creating a session on a token whose PIN the machine has not cached prompts for the PIN as well, so a session per transaction can mean two prompts for one signature. A session created without a PIN prompts nobody at creation, so there the cost is only the creation call itself.

Sessions without a PIN​

A Client Application with Per-Transaction PIN on may also be switched to create its sessions without a PIN (Server 1.18.0). It is a second switch beside the first, also a TrustBridge extension, also set by an administrator of the Server for your Client Application, and only ever on while Per-Transaction PIN is: turning Per-Transaction PIN off clears it. With it, pin on POST /v1/sessions may be left out, so for a certificate on the holder's machine your application collects the PIN only where more than one certificate could serve the session (below); otherwise the holder types it at their Tray App for each signing call, and nowhere else. For every other application, a request without pin is refused 400 INVALID_REQUEST, pin is required., as it always was.

A creation without pin binds to the single certificate that could serve the session, counting only the machines connected at that moment, without asking anybody anything: nothing is prompted, challenged or opened, and nothing is counted against the failed-PIN lockout. It answers transactionPin like any other session, "workstation" or "client", and a "client" session still needs pin on every signing call. Its refusals come in the order they always did: 429 TOO_MANY_PIN_ATTEMPTS while the CPF is locked out, with or without a pin, then 404 CERT_NOT_FOUND, 422 CERT_NOT_ACTIVE and 422 WORKSTATION_OFFLINE. 401 INVALID_PIN and 422 PIN_NOT_AVAILABLE are answered only to a request that sent a pin.

Where more than one certificate could serve the session, the request is refused 400 INVALID_REQUEST with these details:

pin is required. More than one certificate could serve this session, and the one the PIN opens is the one it binds to.

That happens when the holder's certificate is on two machines, when the Server's Certificate Vault holds one beside one on a machine, or when an old card and its renewal are both still valid. Handle it by asking: ask your user for the PIN and send the same request with it, and the certificate it opens is the one the session binds to. Expect it at least while a holder's old and renewed cards are both valid. A pin that is sent is checked exactly as for any other application. Leave the field out rather than sending an empty one, which is refused.

A session bound this way can still fail on its first signing call where nothing at the machine can authorize it, for example 422 PIN_NOT_AVAILABLE with details no A1 PIN at the workstation or no Tray at the console; the refusal names what to fix. Note what the switch accepts: a key of such an application can create a session for any CPF that has a certificate on the platform, read that certificate through GET /v1/certificates/info, and replace that CPF's active session, all without the PIN, though it cannot sign anything without the holder. See Security.

Documents signed with REST PKI Core​

An administrator of the Server may set a Client Application to have its documents signed with REST PKI Core, Lacuna's PDF signing service, instead of Lacuna Signer (Server 1.18.0). It is off until the administrator switches it on for that application. The wire is the same either way, and no field says which service signed: the same 202, the same batch results and the same fetch. What changes is behind it:

  • Which service signs is read from the application whose API key makes each signing call, not from the one that created the session, so a switch takes effect from that application's next signing call. It governs POST /v1/signatures and POST /v1/signatures/batch; Hash Signing is unaffected.
  • The signatureId is REST PKI Core's own id for the signed document, a GUID like the Signer's, and REST PKI Core keeps that document: the fetch reads it from there, so how long it stays fetchable is REST PKI Core's retention. Ids the Signer produced, before or after a switch, are fetched from the Signer as before.
  • The holder's key makes exactly one signature, as before, under the same custody, PIN and Tray App rules, and one call spends one Signature Allowance charge. No e-mail address is needed: a certificate that names none signs, and one that names no holder at all is refused 422 CERT_INCOMPLETE_IDENTITY.
  • REST PKI Core validates the certificate too, under its own trust settings, before the holder is asked anything. A certificate it does not validate is 422 CERT_NOT_ACTIVE, with details REVOKED, EXPIRED or UNTRUSTED; nothing is signed and no charge is spent. Its verdict can disagree with the platform's.
  • Which signature policy is applied, and whether marks are placed on the pages, are settings of the REST PKI Core the deployment uses, not of TrustBridge.

The id exists only once REST PKI Core has answered the signed document, so an id is never answered for a document it did not sign, and every failure before then is a refusal rather than a 202, even one that comes after the holder's key signed:

AnswerMeaning
422 SIGN_TIMEOUTTime ran out.
422 CERT_NOT_ACTIVEREST PKI Core did not validate the certificate: before the holder is asked anything, or, rarely, for one whose standing changed while the holder's key was signing.
422 UNSUPPORTED_DOCUMENTA PDF REST PKI Core cannot process: malformed, protected by a password, already signed where it is set to refuse one, or one it cannot make PDF/A where it is set to. details says which in REST PKI Core's own words.
502 SIGNER_ERRORREST PKI Core unreachable, failing, rate-limiting or out of its subscription's quota.
500 INTERNAL_ERRORA fault of the Server's own, or of how it is set up to reach REST PKI Core.

A refusal after the holder's key signed promises nothing either way: the call's Signature Allowance charge is spent, and REST PKI Core may still have kept a signed document that no id names, so sending the document again signs it again. An INTERNAL_ERROR says so in its message when the key had already signed. In a batch each of these is that item's ERROR, except a failure before anything is signed (the certificate refused, REST PKI Core failing or out of time while it validates the certificate, or not reachable the way the Server is set up to reach it), which refuses the whole call. A batch item reads PROCESSING only once REST PKI Core has answered its signed document. A fetch of a document REST PKI Core no longer has answers 422 DOCUMENT_UNAVAILABLE with no details.

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. For a session for a Company, still the person's CPF: the holder of the Company's e-CNPJ.
pinrequired*The certificate's PIN, whichever custody it is under. For a Vault Certificate, the password that opens its file. *May be left out only by a Client Application with Sessions without a PIN on; never sent empty.
cnpjoptionalThe Company to sign on behalf of, with its e-CNPJ. Fourteen characters, unpunctuated and uppercase: twelve digits or letters (the alphanumeric CNPJ is accepted as well as the numeric one) followed by two check digits. Check digits are verified, and twelve repetitions of one character are refused, with 400 INVALID_REQUEST. Omitted, the session is the person's, as before.
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",
"transactionPin": null, "certificate": "MIIH…", "chain": ["MIIG…", "MIIF…"]
}

expiresIn is always the window actually granted. transactionPin says whether every signing call needs the PIN for that call (Per-Transaction PIN); it is always present, as null where it does not apply. certificate is the bound certificate's DER encoding in base64, and chain its issuers, nearest first, up to and including the root, as the platform recorded them when it last validated the certificate (Server 1.14.0). They let a client build a signed structure before it signs anything. certificate is null when the platform does not hold the certificate's encoding, and signing under such a session is then refused with CERT_DATA_UNAVAILABLE. chain is never omitted or null: it is [] when no chain is recorded, which today is always the case for a certificate held on a workstation. Refusals:

  • 400 INVALID_REQUEST, details beginning pin is required.: no pin, from an application without Sessions without a PIN, or, from one with it, more than one certificate could serve the session.
  • 404 CERT_NOT_FOUND: no certificate for the CPF (and CNPJ) anywhere.
  • 422 CERT_NOT_ACTIVE: every certificate for the CPF (and CNPJ) carries a non-active verdict.
  • 401 INVALID_PIN: every candidate certificate was tried and none accepted the PIN. Only where a pin was sent.
  • 422 PIN_NOT_AVAILABLE: no comparison could be made. A prompt timed out, was declined or could not be shown; a Vault Certificate could not be opened; or too many recent attempts against the certificate did not match, so it is not asked again until they age out. details says which. Only where a pin was sent: a creation without one compares nothing.
  • 422 WORKSTATION_OFFLINE: every machine holding an active certificate is offline, or, for a Client Application with Per-Transaction PIN on, runs a Workstation release older than 1.6.0. With details workstation removed, the machine chosen was removed from the platform while the session was being created: no session was created or replaced, and any session the CPF (and CNPJ) already had stands.
  • 429 TOO_MANY_PIN_ATTEMPTS: the per-CPF lockout, with or without a pin. A creation without one never counts against it.

A session for a Company​

Send cnpj beside cpf to sign with a Company's e-CNPJ. cpf is still the person's: the holder of that e-CNPJ, whose PIN this is.

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" }'
{ "sessionId": "c71d…", "cpf": "12345678909", "cnpj": "12ABC34501DE35", "expiresIn": 3600, "expiresAt": "2026-09-21T15:04:05Z" }
  • Matching is exact. With a cnpj, only an e-CNPJ naming both that CNPJ and that CPF can bind the session, never the person's own e-CPF. Without one, only the person's e-CPF can, never an e-CNPJ, even one naming the same CPF.
  • One active session per CPF and CNPJ. A person's own session and the one for their Company coexist, and so do two people's sessions for one Company. Ending one does not affect the other.
  • The PIN lockout stays per CPF, whichever cnpj was sent, or none: one card usually holds both certificates behind one PIN, so failures on either count together.
  • Answers name the cnpj back wherever they name the session's CPF: creation, deactivation and the certificate description. A session without one answers with no cnpj member at all (not null), so an integration that never sends one sees nothing new.

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", "transactionPin": null }

transactionPin is the value creation answered. It is present whenever the session is active, and absent when the id names nothing active.

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 (and its cnpj, for a session for a Company), 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; for an e-CNPJ, the CPF of the person who holds it on the Company's behalf), cnpj (only for an e-CNPJ), subjectName, issuer, serialNumber, notBefore, notAfter and status, and, from Server 1.14.0, the certificate's DER encoding as certificate and its issuers as chain, as session creation answers them. 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. 404 CERT_NOT_FOUND means the bound certificate is no longer held: a token was removed, the certificate was deleted, or the workstation that held it was removed from the platform, which a signing call under the same session answers WORKSTATION_OFFLINE instead.

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"

Any file part is read as the document, whatever its field is called. Only one is signed: where a request carries several, document wins, then file, then whichever arrived first.

Answers 202:

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

fileName is the name the Server signed the document under, which is not always the one sent: the request's fileName, or the file part's own name (filename* where the part carries one); document.pdf where that name is absent or empty; and cut at 260 characters, which can take the extension with it. Where a multipart request carried several file parts, it says which one was signed.

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, or, for a Client Application whose documents are signed with REST PKI Core, REST PKI Core's, answered only once REST PKI Core has answered the signed document. 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.

Under a per-transaction session the call also carries pin where transactionPin is "client" (a JSON member, or a pin field beside the multipart document), or waits for the holder at the Tray where it is "workstation", before the document reaches the signature service.

Two answers from the signature service mean opposite things. 422 SIGNER_REJECTED is the service refusing the request (usually the document, occasionally something else the request named; details says which): resending it unchanged gets the same answer. 502 SIGNER_ERROR is the service being unreachable, rate-limiting or failing before the document reached it: nothing was signed, and the same document may be sent again once the service is back. That last promise is the Signer's: for a document signed with REST PKI Core, SIGNER_ERROR can come after the holder's key signed, and then promises nothing either way.

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. Every item, failed or not, is named by the same rule as the single-document fileName. The whole call fails only for something true of all of them: no sessionId (400 INVALID_REQUEST), no live session, a certificate that cannot sign, more documents than the limit (400 INVALID_REQUEST), or none (400 INVALID_DOCUMENT), and, for documents signed with REST PKI Core, a failure before anything is signed (above). 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.

Under a per-transaction session a batch is authorized once for the whole call: by a pin form field where transactionPin is "client", or by one Tray prompt naming the documents where it is "workstation". A document refused on its own, such as a non-PDF, is its item's ERROR as always, and is neither counted nor named in the prompt. A prompt that is declined or unanswered refuses the whole call with 422 PIN_NOT_AVAILABLE, not one ERROR per item, and nothing is created at the signature service. acceptedAt is then the moment the holder authorized the batch.

POST /v1/signatures/hashes: sign digests (Hash Signing)​

(TrustBridge extension, Server 1.14.0.) Signs digests the client computed itself, with the certificate the session is bound to, and answers the signatures synchronously, in the same order. No document is sent and nothing is fetched afterwards. It is for an integrator that assembles its own signed structure (CAdES, XAdES, or a PAdES signature built client-side) around the certificate and chain that session creation and GET /v1/certificates/info answer.

Only a Client Application an administrator has granted Hash Signing may call it. A digest carries nothing that says what it is a digest of, so this hands the holder's key whatever the client chooses. It is off for every Client Application until an administrator turns it on for that application on the admin console's Client Applications screen. An ungranted caller is refused 403 HASH_SIGNING_NOT_GRANTED before the session is looked up.

curl -s https://trustbridge.example.com/v1/signatures/hashes \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{ "sessionId": "8f3c…", "digestAlgorithm": "SHA-256", "hashes": ["uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="] }'
{ "signatures": ["Kx9f…"], "signedAt": "2026-10-18T14:10:00Z" }
Field
sessionIdrequiredThe active session whose certificate signs.
digestAlgorithmrequiredSHA-256, SHA-384 or SHA-512, one per call.
hashesrequiredThe digests, each base64 encoded and exactly as long as the algorithm's output (32, 48 or 64 bytes). At least one, and at most Signing:MaxBatchItems (50 by default).
pin"client" sessions onlyThe holder's PIN for this call, under a per-transaction session whose transactionPin is "client". Refused on any other session.
  • Each signature is raw RSASSA-PKCS1-v1_5 over a DigestInfo naming digestAlgorithm, not a CMS structure, made by the certificate GET /v1/certificates/info describes.
  • A digest that is not base64 or is the wrong length refuses the whole call with 400 INVALID_REQUEST before anything is signed, and details names its index (hashes[2] for the third).
  • All or nothing. The digests are signed in order, and the first one that fails ends the call with that refusal. Signatures already made are not answered (the message says how many there were), so a client holds every signature or none, and sends the whole call again.
  • One call counts once against the Signature Allowance however many digests it carries, and extends the session's window, both only if the key was used. It is bounded as a whole by Signing:RequestBudget (60 s by default). Each digest is recorded in the Audit Log.
  • Under a "workstation" per-transaction session the holder authorizes the call at the Tray, which shows the Client Application and the number of digests, before any digest is signed.

Refusals beside the usual ones: 403 HASH_SIGNING_NOT_GRANTED; 422 CERT_DATA_UNAVAILABLE when the platform does not hold the certificate's encoding (certificate: null); 422 CERT_INCOMPLETE_IDENTITY when the certificate names no holder's CPF, which signing digests requires even where a document would not; 422 WORKSTATION_OFFLINE when the holding machine is not reachable, or went away while it had a digest, or was removed from the platform (details workstation removed); 422 PIN_NOT_AVAILABLE when the machine could not use its PIN (none held, refused, or the token locked) or the Server no longer holds the secret the session was created with; and 422 SIGN_TIMEOUT, after which the key may have signed some of the digests although none is answered.

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: details says Refused, Canceled or Expired. Nothing to wait for. For a document signed with REST PKI Core, REST PKI Core no longer has it, with no details.
404 DOCUMENT_NOT_FOUNDNo document goes by this id, including one the signature service does not hold.
502 SIGNER_ERRORThe signature service could not be asked (unreachable, rate-limiting or faulting), or, for a document signed with REST PKI Core, REST PKI Core could not be asked for it; the document is unaffected. Ask again shortly.

Nothing is stored by the Server, so how long a document stays fetchable is Lacuna Signer's retention, or REST PKI Core's for a document signed there. Any valid API key may fetch any document whose id it knows, including one created by a different Client Application. That is the contract, not an oversight: treat a signatureId as a bearer token for its document.

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

"The CPF" below means the CPF and CNPJ together for a session for a Company. The PIN lockout is the one exception, and stays per CPF.

CodeHTTPMeaning
INVALID_REQUEST400, 415Malformed request, or a body in a media type the operation does not read. details names what was wrong: for example a missing sessionId, or a CPF or CNPJ whose check digits do not agree. Creating a session without pin, details begins pin is required.: the application does not create sessions without a PIN, or more than one certificate could serve the session (Sessions without a PIN).
INVALID_DOCUMENT400No document to sign 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_PIN401Creating with a pin: the PIN opened no Vault Certificate held for the CPF, and differs from the one in use at every workstation holding one: every candidate was tried, and none matched. Signing under a "client" per-transaction session: the call's pin did not open the certificate. Either way it counts against the CPF's lockout.
HASH_SIGNING_NOT_GRANTED403This Client Application has not been granted Hash Signing. Answered before the session is looked up, so it says nothing about the sessionId.
CERT_NOT_FOUND404Creating: no certificate for the CPF anywhere, neither in the vault nor on a workstation. Reading or signing: the bound certificate is no longer held. Reading also answers this once the workstation that held it was removed; signing answers that case WORKSTATION_OFFLINE.
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 the size and the limit.
CERT_NOT_ACTIVE422Every certificate for the CPF, or the bound one, carries a non-active verdict (EXPIRED, REVOKED, UNTRUSTED, STALE). Signing a document with REST PKI Core: REST PKI Core also did not validate the certificate.
DOCUMENT_UNAVAILABLE422The document ended without a signed version: refused, cancelled or expired, or, signed with REST PKI Core, REST PKI Core no longer has it. Nothing to wait for.
PIN_NOT_AVAILABLE422Creating with a pin: no comparison could be made: a prompt timed out, was declined or could not be shown, a Vault Certificate could not be opened, or too many recent attempts against the certificate did not match. Signing digests: the key could not be used. Signing under a per-transaction session: the holder did not authorize the call, or its pin could not be checked (see Per-Transaction PIN). details says which.
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), or, for a Client Application with Per-Transaction PIN on, too old to ask the holder for each call. With details workstation removed, the session's workstation was removed from the platform: nothing signs under that session again, and it expires on its own. A creation whose chosen workstation was removed meanwhile answers the same, and then no session was created or replaced.
CERT_DATA_UNAVAILABLE422The certificate's own data, which signing needs, is not held for it.
CERT_INCOMPLETE_IDENTITY422The certificate does not name its holder well enough to sign; details names the missing field. An address is the one field the session's email can supply. Signing digests, and signing a document with REST PKI Core, ask only for the holder, and refuse a certificate that names none.
UNSUPPORTED_DOCUMENT422Not a PDF, or, for a document signed with REST PKI Core, a PDF REST PKI Core cannot process.
SIGNER_REJECTED422The signature service refused the request, usually the document itself; details carries its account and says which. Retrying unchanged reproduces it.
SIGN_TIMEOUT422The signature did not finish inside the time one request is given (60 s by default). Signing a document, it arrives only while no signature id exists: nothing was signed, except for a document signed with REST PKI Core, where the holder's key may already have signed and a signed document may have been kept. Signing digests, no signature is answered, but the key may have signed some of them.
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, whichever cnpj was sent with the CPF, or none: at session creation, and at every signing call under a "client" per-transaction session, which share the one count. 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, except signing a document with REST PKI Core, where it can arrive after the holder's key signed, as its message then says, and sending the document again signs it again.
SIGNER_ERROR502The signature service could not be asked: unreachable, rate-limiting, or answering a fault of its own. On a sign this only arrives before the document reached it, so nothing was signed; on a fetch it says nothing about the document. For a document signed with REST PKI Core, the service is REST PKI Core, out of its subscription's quota included, and on a sign it can arrive after the holder's key 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.

The specification​

The contract's normative description is the SignSession reference specification (SignSession — Documentação de referência), written in Portuguese from this Server's implementation and adopted by the contract's specification author in place of the earlier integration document. Ask Lacuna Software for a copy. The OpenAPI document a running Server serves at /openapi/v1.json is generated from the Server itself and kept identical to what it answers. If the two ever disagree, one of them is wrong: report the difference rather than assume it is deliberate.

Codes the OpenAPI document marks as TrustBridge extensions are ones the original specification did not define. The current specification carries them with the same mark; they are part of the contract like any other. Three extensions are not in the specification at all, so the OpenAPI document is where they are written down: Hash Signing (POST /v1/signatures/hashes and HASH_SIGNING_NOT_GRANTED), Per-Transaction PIN (transactionPin and the pin on signing calls), which mints no code, and Sessions without a PIN (a pin left out of POST /v1/sessions). Documents signed with REST PKI Core are another: they change no wire shape and mint no code, so the descriptions of the two document-signing operations and of the fetch are where they are written down.

Coming from the earlier integration document​

A client written against the earlier integration document, or against the contract's other implementation (CertSession), meets these answers. Every one of them is the contract now, and two are silent: they behave differently without any error saying so.

What a client meetsWhat to do
Every route carries /v1. A bare route answers 404 ENDPOINT_NOT_FOUND.Prefix every call. There is no version header or media type.
A session is named by its sessionId, not by a CPF. Only creation takes a CPF; status, certificate info, signing and deactivation take the id.Store the sessionId. No call recovers it from a CPF.
expiresIn is never null. An omitted sessionDuration grants the default window, not an unlimited one.Silent. Read expiresIn and expiresAt off the response rather than assuming the window you asked for.
Signing extends the session rather than ending it, unless an administrator set a Signature Allowance for your Client Application (an allowance of 1 gives single-use sessions).Silent. A client that creates a session per document still works, at the cost of another PIN confirmation each time. To end a session with its document, call POST /v1/sessions/deactivate.
A cpf has to be a real CPF: check digits verified, repeated-digit values refused.400 INVALID_REQUEST. Validate check digits in your form too; the schema's pattern (^[0-9]{11}$) cannot express them.
A signing call without sessionId is malformed, single or batch.400 INVALID_REQUEST: fix the request. A 401 SESSION_EXPIRED means create a session and retry.
Certificates are validated for chain and revocation, not only expiry.CERT_NOT_ACTIVE with UNTRUSTED, REVOKED or STALE where an expiry-only check would sign.
The Server sheds load.429 SERVER_BUSY. Retry after a backoff of your own; batching reduces how often you meet it.
The outcome is read at the fetch. There is no webhook to register.Poll GET /v1/signatures/{signatureId}/document until 200 or 422.
Any valid API key fetches any document whose signatureId it knows.Treat the id as a bearer token: do not log it or hand it to parties who should not have the document.

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 your Client Application has 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. Do not resend a document refused with SIGNER_REJECTED unchanged; do resend one answered SIGNER_ERROR, unless your documents are signed with REST PKI Core, where a refusal after the holder's key signed means that resending signs again.
  • 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.
  • Send cnpj only to sign for a Company. A session without one never binds an e-CNPJ, and one with it never binds the person's e-CPF.
  • Read transactionPin on every session you create, and send pin on a signing call only when it is "client". Under "workstation", give the signing call time for a person to answer the Tray.
  • Under Sessions without a PIN, handle 400 INVALID_REQUEST at creation by asking for the PIN and sending the same request with it: more than one certificate could serve that holder.

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 and a sample cnpj. 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. A second creation request does the same for a Company, sending the environment's cnpj. One request signs digests (Hash Signing), and a Per-Transaction PIN folder covers both kinds of per-transaction session; its first request says how to leave pin out under Sessions without a PIN. The collection captures sessionId and signatureId into the environment so nothing needs pasting between steps. Where the collection and the Server's published /openapi/v1.json disagree, the published document wins.

View as Markdown