---
title: "Sign a PDF (PAdES) - PKI Express"
description: "How to sign a PDF (PAdES) with PKI Express: the two-step signature with the user's certificate and Web PKI, the one-step signature with a server key (PKCS #12), policies, visual representation and batches."
canonical: "https://docs.lacunasoftware.com/en-us/articles/pki-express/pades-signature"
locale: "en"
product: "PKI Express"
last_updated: "2026-10-13"
---

# Sign a PDF (PAdES) - PKI Express

[PKI Express](https://docs.lacunasoftware.com/en-us/articles/pki-express) signs PDFs (PAdES) on your server in two ways: 

* **With the user's certificate**, in two steps: PKI Express computes the hash to sign, [Web PKI](https://docs.lacunasoftware.com/en-us/articles/web-pki) signs it in the
  browser with the user's key, and PKI Express writes the signed PDF.
* **With a server key**, in one step and no browser: PKI Express signs with a key that your server holds, such as a PKCS #12 (.pfx) file.

Both use the `start-pades`, `complete-sig` and `sign-pades` commands, present since PKI Express 1.0.0; signing with a PKCS #12 file needs
1.3.0 or later. 

The code is Node.js, from the [PKI Suite samples](https://github.com/LacunaSoftware/PkiSuiteSamples), with `pki-express` (1.16.2); the
Java, PHP, Python and Ruby samples do the same. 
For a line-by-line walkthrough of the Java sample, see [Signature sample explained](https://docs.lacunasoftware.com/en-us/articles/pki-express/java/how-it-works).

## Flow with the user's certificate

1. **Browser**: Web PKI lists the user's certificates and reads the one they pick (`readCertificate()`); the page posts its content to the server.
1. **Server**: `PadesSignatureStarter.start()` returns the hash to sign, its digest algorithm and a transfer file.
1. **Browser**: Web PKI signs the hash with `signHash()` and posts the signature.
1. **Server**: `SignatureFinisher.complete()` with the same PDF, the transfer file and the signature writes the signed PDF.

## 1. Start the signature

```js
const signatureStarter = new PadesSignatureStarter();
Util.setPkiDefaults(signatureStarter);
signatureStarter.signaturePolicy = StandardSignaturePolicies.PADES_BASIC_WITH_LTV;
signatureStarter.setPdfToSignFromPathSync(StorageMock.getDataPath(fileId));
signatureStarter.setCertificateFromBase64Sync(certContent);
signatureStarter.addFileReferenceSync('stamp', StorageMock.getPdfStampPath());
signatureStarter
	.setVisualRepresentationSync(PadesVisualElementsExpress.getVisualRepresentation());
signatureStarter.start()
	.then((response) => {
		const { toSignHash } = response;
		const { digestAlgorithm } = response;
		const { transferFile } = response;
		// render the page that signs the hash, with these values in hidden fields
	}).catch((err) => next(err));
```

* `certContent` is the user's certificate, Base64-encoded, as Web PKI's `readCertificate()` returns it. 
* `start()` resolves to `toSignHash`, `digestAlgorithm`, `digestAlgorithmOid` and `transferFile`, the name of a file that PKI Express keeps in
  its transfer data folder until the signature is completed. 
* `Util.setPkiDefaults()` applies the samples' trust settings and timestamp authority; see [Validate signatures](https://docs.lacunasoftware.com/en-us/articles/pki-express/validate-signatures#trust)
  and [Timestamp a PDF](https://docs.lacunasoftware.com/en-us/articles/pki-express/timestamp-pdf#timestamp-authority). 
* If the PDF is already signed, the same flow adds a signature to it. 

## 2. Sign the hash in the browser

```js
pki.signHash({
	thumbprint: formElements.certThumbField.val(),
	hash: formElements.toSignHashField.val(),
	digestAlgorithm: formElements.digestAlgorithmField.val()
}).success(function (signature) {
	formElements.signatureField.val(signature);
	formElements.form.submit();
});
```

The thumbprint is the one of the certificate read in the first step; the page keeps it, with the transfer file, in hidden fields. 
For Web PKI itself, see [Web PKI](https://docs.lacunasoftware.com/en-us/articles/web-pki).

## 3. Complete the signature

```js
const signatureFinisher = new SignatureFinisher();
Util.setPkiDefaults(signatureFinisher);
signatureFinisher.setFileToSignFromPathSync(StorageMock.getDataPath(fileId));
signatureFinisher.setTransferFileFromPathSync(transferFile);
signatureFinisher.signature = signature;
const outputFile = `${uuidv4()}.pdf`;
signatureFinisher.outputFile = path.join(APP_ROOT, 'app-data', outputFile);
signatureFinisher
	.complete(true)
	.then((certificate) => { /* signed PDF at outputFile */ })
	.catch((err) => next(err));
```

* Give `SignatureFinisher` the same PDF as in the first step. 
* With `complete(true)`, the promise resolves to the signer's certificate; this needs PKI Express 1.8 or later. Without it, nothing is
  returned. 
* `SignatureFinisher` also completes CAdES and XML signatures. 

## Sign with a server key

`PadesSigner` signs in one call, with no browser. The sample signs with a test PKCS #12 file, password `1234`, or with one generated from
a certificate issued by the Amplia sample. 

```js
const signer = new PadesSigner();
Util.setPkiDefaults(signer);
signer.signaturePolicy = StandardSignaturePolicies.PADES_BASIC_WITH_LTV;
await signer.setPdfToSignFromPath(StorageMock.getDataPath(fileId));
await signer.setPkcs12FromPath(StorageMock.getSamplePkcs12Path());
signer.certPassword = '1234';
await signer.addFileReference('stamp', StorageMock.getPdfStampPath());
await signer.setVisualRepresentation(PadesVisualElementsExpress.getVisualRepresentation());
const outputFile = `${uuidv4()}.pdf`;
signer.outputFile = path.join(APP_ROOT, 'app-data', outputFile);
let signerCert = await signer.sign(true);
```

* The PKCS #12 can also be given as bytes (`setPkcs12FromRaw`) or Base64 (`setPkcs12FromBase64`). 
* Instead of a PKCS #12 file, the key can be: 
  * a certificate installed on the server, by its thumbprint (`certThumb`; PKI Express 1.3 or later);
  * a key by name (`keyName`) with its certificate file (`setCertFileFromPath`), PKI Express 1.14 or later, which added keys stored on
    Azure Key Vault (see [Azure](https://docs.lacunasoftware.com/en-us/articles/pki-express/config/azure)); 
  * a cloud certificate, through a trust service session: see [Cloud certificate services](https://docs.lacunasoftware.com/en-us/articles/pki-express/config/trust-services).
* Set `overwriteOriginalFile` instead of `outputFile` to sign the PDF in place. 
* TODO(docs): which certificate store PKI Express searches for `certThumb` on Linux, and whether the Java `setUseMachine` option applies there.
* TODO(docs): how to use a key on a PKCS #11 token or HSM; PKI Express 1.38.0 added `pkcs11LibraryPath`, which no page describes yet. 

## Signature policies

The policy is a `StandardSignaturePolicies` value; it decides what goes into the signature. The samples use `PADES_BASIC_WITH_LTV`. 

| Constant (Node.js, Python) | Value | Notes |
|---|---|---|
| `PADES_BASIC` | `pades` | |
| `PADES_BASIC_WITH_LTV` | `pades-ltv` | |
| `PADES_T` | `pades-t` | needs a timestamp authority |
| `PKI_BRAZIL_PADES_ADR_BASICA` | `adrb` | ICP-Brasil, PKI Express 1.12.0 or later |
| `PKI_BRAZIL_PADES_ADR_BASICA_WITH_LTV` | `adrb-ltv` | ICP-Brasil, PKI Express 1.12.0 or later |
| `PKI_BRAZIL_PADES_ADR_TEMPO` | `adrt` | ICP-Brasil, PKI Express 1.12.0 or later; needs a timestamp authority |

* With a policy that needs a timestamp, the Node.js library refuses to run if no timestamp authority is set; PKI Express itself returns code 32
  (`TimestampOptionsRequired`). See [Timestamp a PDF](https://docs.lacunasoftware.com/en-us/articles/pki-express/timestamp-pdf#timestamp-authority). 
* Java names the constants in camel case (`PadesBasicWithLTV`, `PkiBrazilPadesAdrBasica`, `PkiBrazilPadesAdrTempo`) and adds `PkiBrazilPadesAdrCompleta`
  (`adrc`) and `PkiBrazilPadesAdrArquivamento` (`adra`); PKI Express 1.34.0 added the ICP-Brasil AD-RC and AD-RA PAdES policies. 
* TODO(docs): the PHP library names the PAdES AD-RA policy `pades-ad-ra` (`PADES_AD_RA`), unlike Java's `adra`; which value PKI Express 1.34.0
  expects is not documented. 
* The policy names differ from Rest PKI's: see the [equivalence table](https://docs.lacunasoftware.com/en-us/articles/pki-express/nodejs/convert-from-restpki).

## Visual representation and other options

* The visual representation (the stamp drawn on the page) is a JSON model given with `setVisualRepresentation()`, or a file with
  `setVisualRepresentationFile()`. Images it references can be added with `addFileReference(alias, path)` and used as `fref://alias`.
  See [Visual representation](https://docs.lacunasoftware.com/en-us/articles/pki-express/nodejs/visual-rep) and the [tags for the text](https://docs.lacunasoftware.com/en-us/articles/pki-express/pades-tags). 
* Other `PadesSignatureStarter` and `PadesSigner` options, each with the PKI Express version it needs: 

| Option | Node.js property | PKI Express |
|---|---|---|
| Signer's reason | `reason` | 1.13.0 |
| Hide the default visual representation | `suppressDefaultVisualRep` | 1.13.1 |
| Name of the signature field | `customSignatureFieldName` | 1.15.0 |
| Certification level | `certificationLevel` | 1.16.0 |
| Signer name and location | Java only: `setSignerName`, `setLocation` | 1.27.0 |
| PDF metadata | PHP only: `addMetadata(key, value)` | 1.38.0 |

## In each language

The classes have the same names in every client library; the setters differ: 

| Library | PDF and certificate (start) | Transfer file and signature (complete) | PKCS #12 (server key) |
|---|---|---|---|
| Node.js | `setPdfToSignFromPath`, `setCertificateFromBase64` | `setTransferFileFromPath`, `signature` | `setPkcs12FromPath`, `certPassword` |
| Java | `setPdfToSign`, `setCertificateBase64` | `setTransferFilePath`, `setSignature` | `setPkcs12`, `setCertPassword` |
| PHP | `setPdfToSign`, `setCertificateBase64` | `setTransferFile`, `setSignature` | `setPkcs12`, `setCertPassword` |
| Python | `set_pdf_to_sign_from_path`, `set_certificate_from_base64` | `set_transfer_file_from_path`, `signature` | `set_pkcs12_from_path`, `cert_password` |
| Ruby | `pdf_to_sign_path=`, `certificate_base64=` | `transfer_file_id=`, `signature=` | `pkcs12_path=`, `cert_password=` |

In Node.js, `start()`, `complete()` and `sign()` return promises; the `...Sync` setters are synchronous versions of the promise-returning ones. 

## Batches

To sign many PDFs with the same certificate, the batch sample runs the same start and complete steps once per document, called with AJAX.
In the browser, Web PKI's `preauthorizeSignatures()` asks the user once, then `signHash()` signs each hash. 

## Medical prescriptions

The prescription sample generates a PDF that carries the ICP-Brasil prescription metadata (OIDs such as `2.16.76.1.12.1.1` and the CRM),
then signs it with the user's certificate. Its signature field name must end with the keyword `Emitente`. 

```js
signatureStarter.customSignatureFieldName = "Signature1 Emitente";
signatureStarter.signaturePolicy = StandardSignaturePolicies.PADES_BASIC;
```

TODO(docs): which ICP-Brasil document defines the prescription metadata and the `Emitente` keyword.

## Errors

A failed command ends with a PKI Express return code: for example 14 (`ValidationFailed`) when the signer's certificate is not trusted or
not valid, or 32 (`TimestampOptionsRequired`). In Node.js, the promise is rejected with a `CommandError`. See [Return codes](https://docs.lacunasoftware.com/en-us/articles/pki-express/return-codes). 

## See also

* [Sign with CAdES](https://docs.lacunasoftware.com/en-us/articles/pki-express/cades-signature) and [Sign an XML document](https://docs.lacunasoftware.com/en-us/articles/pki-express/xml-signature)
* [Validate signatures](https://docs.lacunasoftware.com/en-us/articles/pki-express/validate-signatures)
* [Printer-friendly version](https://docs.lacunasoftware.com/en-us/articles/pki-express/printer-friendly-version)
* [PKI Express quick reference](https://docs.lacunasoftware.com/en-us/articles/pki-express/quick-reference)

**Demos and samples**

- Live demo: [Sign a file with your certificate](https://try.lacunasoftware.com/en/pki-suite/sign-a-file)
- Sign a PDF with PKI Express: [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/PadesSignatureExpressController.java) · [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/pades-signature-express.js) · [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/pades-signature-express/index.php) · [Python](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/python/flask/sample/views/pades_signature_express.py) · [Ruby](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/ruby/rails/app/controllers/pades_signature_express_controller.rb)
- Sign a PDF with a server key using PKI Express: [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/PadesServerKeyExpressController.java) · [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/pades-server-key-express.js) · [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/pades-server-key-express/index.php) · [Python](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/python/flask/sample/views/pades_server_key_express.py) · [Ruby](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/ruby/rails/app/controllers/pades_server_key_express_controller.rb)
- Sign multiple PDFs with PKI Express: [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/BatchPadesSignatureExpressController.java) · [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/batch-pades-signature-express.js) · [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/batch-pades-signature-express/index.php) · [Python](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/python/flask/sample/views/batch_pades_express.py) · [Ruby](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/ruby/rails/app/controllers/batch_pades_express_controller.rb)
- Sign a medical prescription (Receita Simples) using PKI Express: [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/ReceitaSimplesExpressController.java) · [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/receita-simples-express.js) · [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/receita-simples-express/index.php) · [Ruby](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/ruby/rails/app/controllers/receita_simples_express_controller.rb)
