---
title: "Signature sessions - Rest PKI Core"
description: "Signature sessions let Rest PKI Core users sign on a hosted page: your app gets a redirectUrl, sends the user there and fetches the signed documents."
canonical: "https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/signature-sessions"
locale: "en"
product: "Rest PKI Core"
last_updated: "2026-10-19"
---

# Signature sessions - Rest PKI Core

**Signature sessions** are a [Rest PKI Core](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core) feature which enable your users to perform digital signatures without requiring you to implement a signature
web page on your app.

To create a signature session, your app specifies a `returnUrl` and receives back a `redirectUrl`. Your app then redirects the user to the `redirectUrl`, where he will 
digitally sign documents:

![Signature session start](https://docs.lacunasoftware.com/images/rest-pki/signature-session-start.png)

Once the process is complete, the user will be redirected back to your app on the *returnUrl* you specified when starting the session. A query parameter will be added
to the `returnUrl` containing the signature session ID (`?signatureSessionId=...`). Then, your app parses the signature session ID on the query string and uses it to
call APIs to fetch the documents signed by the user:

![Signature session end](https://docs.lacunasoftware.com/images/rest-pki/signature-session-end.png)

## Creating a signature session

> **Tip**
>
> From this point on we'll describe calls to Rest PKI Core. Make sure you've read the [Get Started](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/get-started) article to be able to keep up.

On .NET, call `IRestPkiService.CreateSignatureSessionAsync()` and use the `RedirectUrl` returned to redirect your user:

```cs
var response = await restPkiService.CreateSignatureSessionAsync(new CreateSignatureSessionRequest() {
	ReturnUrl = "http://localhost:8080/SessionCallback",
});
return Redirect(response.RedirectUrl);
```

See [session creation sample in .NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/SignatureSessionRestCoreController.cs).

On PHP, call `RestPkiServiceInterface.createSignatureSession()` and use the `$redirectUrl` returned to redirect your user:

```php
$request = new CreateSignatureSessionRequest();
$request->returnUrl = "http://localhost:8080/SessionCallback";

$response = $service->createSignatureSession($request);

header("Location: {$response->redirectUrl}");
```

See [session creation sample in PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/signature-session-rest-core/index.php).

On Java, call `RestPkiService.createSignatureSession()` and use the `RedirectUrl` returned to redirect your user:

```java
CreateSignatureSessionRequest request = new CreateSignatureSessionRequest();
request.setReturnUrl("http://localhost:60695/signature-session-rest-core/complete");

CreateSignatureSessionResponse response = restPkiService.createSignatureSession(request);

return "redirect:" + response.getRedirectUrl();
```

See [session creation sample in Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/SignatureSessionRestCoreController.java).

On Node.js, with the `client` created in [Get started](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/get-started#nodejs), call `createSignatureSession()` and use the `redirectUrl`
returned in `data` to redirect your user. On Express:

```js
router.get('/signature-session', async (req, res, next) => {
	try {
		const response = await client.createSignatureSession({
			returnUrl: 'http://localhost:3000/session-callback',
		});
		res.redirect(response.data.redirectUrl);
	} catch (err) {
		next(err);
	}
});
```

On Python, with the `client` created in [Get started](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/get-started#python), call `SignatureSessionsApi.api_signature_sessions_post()`
and use the `redirect_url` returned to redirect your user. On Flask:

```python
from flask import redirect
from restpki_ng_python_client import CreateSignatureSessionRequest, SignatureSessionsApi

signature_sessions_api = SignatureSessionsApi(client)

@app.route('/signature-session')
def signature_session():
    response = signature_sessions_api.api_signature_sessions_post(CreateSignatureSessionRequest(
        return_url='http://localhost:5000/session-callback',
    ))
    return redirect(response.redirect_url)
```

Our samples repository has no signature session sample in Node.js or Python.

If calling the API directly, do:

```plaintext
POST {endpoint}/api/signature-sessions
{
	"returnUrl": "http://localhost:8080/SessionCallback"
}
```

Let's drill down on the `returnUrl` used on the code snippets above:

```plaintext
http://localhost:8080/SessionCallback
^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
Your app's URL        Route to the callback handler
```

When adapting to your case, you must change the first portion to your own application's address and also to the route of your own "callback handler" (see next section).

## Implementing the callback handler

The `returnUrl` is the location to which Rest PKI Core will redirect the user back upon completion of the session.

> **Note**
>
> So far, we've used `/SessionCallback` as the relative URL of the callback handler, but feel free to use whatever makes sense for your app

On .NET, the implementation would look like this:

```cs
public class SessionCallbackController : Controller {
	...	
	public async Task<IActionResult> Index(Guid signatureSessionId) {
		var session = await restPkiService.GetSignatureSessionAsync(signatureSessionId);
		if (session.Status != SignatureSessionStatus.Completed) {
			return Redirect("/");
		}
		return View(session);
	}
}
```

See [callback handler sample in .NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/SignatureSessionRestCoreController.cs).

On PHP, the implementation would look like this:

```php
$sessionId = $_GET['signatureSessionId'];

$session = $service->getSignatureSession($sessionId);
if ($session->status != SignatureSessionStatus::COMPLETED) {
	header("Location: http://localhost:8000/");
	exit;
}
```

See [callback handler sample in PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/signature-session-rest-core/complete.php).

On Java, the implementation would look like this:

```java
@Controller
public class SessionCallbackController {
	...
	public String index(
		Model model, 
		@RequestParam(value="signatureSessionId") String sessionId
	) throws Exception {
		SignatureSession session = restPkiService.getSignatureSession(UUID.fromString(sessionId));
		if (session.getStatus() != SignatureSessionStatus.COMPLETED) {
			return "redirect:/";
		}
		// The session's documents give the signed file only as the deprecated "location";
		// the DocumentModel of each document gives it as "url".
		List<DocumentModel> documents = new ArrayList<>();
		for (SignatureSessionDocument document : session.getDocuments()) {
			documents.add(restPkiService.getDocumentModel(document.getId()));
		}
		model.addAttribute("documents", documents);
		return "signature-callback/index";
	}
}
```

See [callback handler sample in Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/SignatureSessionRestCoreController.java).

On Node.js, call `getSignatureSession()`. The session is in `data`, and its `status` is one of the values of `SignatureSessionStatus`,
exported by the package:

```js
const { SignatureSessionStatus } = require('restpki-core-client');

router.get('/session-callback', async (req, res, next) => {
	try {
		const response = await client.getSignatureSession(req.query.signatureSessionId);
		const session = response.data;
		if (session.status !== SignatureSessionStatus.Completed) {
			return res.redirect('/');
		}
		res.render('session-callback', { documents: session.documents });
	} catch (err) {
		next(err);
	}
});
```

On Python, call `SignatureSessionsApi.api_signature_sessions_id_get()`, which returns a `SignatureSessionModel` whose `status` is a
`SignatureSessionStatus`:

```python
from flask import redirect, render_template, request
from restpki_ng_python_client import SignatureSessionStatus

@app.route('/session-callback')
def session_callback():
    session = signature_sessions_api.api_signature_sessions_id_get(request.args['signatureSessionId'])
    if session.status != SignatureSessionStatus.COMPLETED:
        return redirect('/')
    return render_template('session_callback.html', documents=session.documents)
```

Notice that we check the `Status` of the session and send the user away to some default location if the status is not `Completed`.

> **Note**
>
> When a user is redirected back to your app, the session will have one of two possible statuses: `Completed` or `UserCancelled`. You should always check the
> status, since it is perfecly normal for a user to land back on your app by cancelling the session instead of going through the procedure.

Now, the Razor view:

```html
@using Lacuna.RestPki.Api
@using Lacuna.RestPki.Client
@model SignatureSession
<h2>Signed documents</h2>
<ul>
	@foreach (var document in Model.Documents)
	{
		<li>
			<a href="@document.SignedFile.Url">@document.SignedFile.Name</a>
		</li>
	}
</ul>
```

On PHP, the client side view:

```php
<h2>Signed documents</h2>
<ul>
	<?php for ($i = 0; $i < count($session->documents); $i++) { 
		$document = $session->documents[$i]; ?>

		<li>
			<a href="<?= $document->signedFile->location ?>"><?= $document->signedFile->name ?></a>
		</li>
	<?php } ?>
</ul>
```

On Java, the Thymeleaf view:

```html
<h2>Signed documents</h2>
<ul>
	<li th:each="document : ${documents}">
		<a th:href="${document.getSignedFile().getUrl()}">
			<span th:text="${document.getSignedFile().getName()}"/>
		</a>
	</li>
</ul>
```

On Node.js, the Pug view (Express renders Pug in our samples):

```pug
h2 Signed documents
ul
	each document in documents
		li
			a(href=document.signedFile.url)= document.signedFile.name
```

On Python, the Jinja template:

```html
<h2>Signed documents</h2>
<ul>
	{% for document in documents %}
	<li>
		<a href="{{ document.signed_file.url }}">{{ document.signed_file.name }}</a>
	</li>
	{% endfor %}
</ul>
```

The link to each signed file (`Url`) is temporary: it downloads the file without the API key and expires after a short time, normally one hour, so don't store it.
Older code reads the same link from `Location`, a deprecated name for this field. The PHP client exposes it only as `location`, and in Java the session's documents
expose it only as `getLocation()`, which is why the Java handler above fetches each document's `DocumentModel`. The Node.js and Python clients expose it as `url`
(`signed_file.url` in Python).

## Predefining documents

The sessions created so far require the user to upload the documents that will be signed. You can instead specify the documents that will be signed
when creating the session. In this case, the user will not be able to upload additional documents to the session.

On .NET:

```cs
string pdfPath = ...;
Stream xmlStream = ...;
var documents = new List<SignatureSessionDocumentToSign> {
	new SignatureSessionDocumentToSign {
		File = FileReference.FromFile(pdfPath),
	},
	new SignatureSessionDocumentToSign {
		File = FileReference.FromStream(xmlStream, "doc.xml"),
	},
	...
};
var response = await restPkiService.CreateSignatureSessionAsync(new CreateSignatureSessionRequest() {
	...
}, documents: documents);
return Redirect(response.RedirectUrl);
```

On Java:

```java
CreateSignatureSessionRequest request = new CreateSignatureSessionRequest();
String pdfPath = ...;
InputStream xmlStream = ...;

List<SignatureSessionDocumentToSign> documents = new ArrayList<>();

SignatureSessionDocumentToSign document = new SignatureSessionDocumentToSign();
document.setFile(FileReference.fromFile(pdfPath, "doc.pdf", "application/pdf"));
documents.add(document);

document = new SignatureSessionDocumentToSign();
document.setFile(FileReference.fromStream(xmlStream, "doc.xml", "application/xml"));
documents.add(document);

CreateSignatureSessionResponse response = restPkiService.createSignatureSession(request, null, null, documents);

return "redirect:" + response.getRedirectUrl();
```

The Node.js and Python clients have no helper to read a file into the request: give each document's `file` either its `content`, encoded in Base64, and
its `name`, or a `url` from which Rest PKI Core downloads it.

On Node.js:

```js
const fs = require('fs');

const response = await client.createSignatureSession({
	returnUrl: 'http://localhost:3000/session-callback',
	documents: [
		{ file: { content: fs.readFileSync(pdfPath).toString('base64'), name: 'doc.pdf' } },
		{ file: { url: 'https://yourapp.com/doc2.pdf' } },
	],
});
res.redirect(response.data.redirectUrl);
```

On Python, pass the content as a Base64 string: the library doesn't encode `bytes`.

```python
import base64
from restpki_ng_python_client import CreateSignatureSessionRequest, FileReferenceModel, SignatureSessionDocumentData

with open(pdf_path, 'rb') as f:
    pdf_content = base64.b64encode(f.read()).decode('ascii')

response = signature_sessions_api.api_signature_sessions_post(CreateSignatureSessionRequest(
    return_url='http://localhost:5000/session-callback',
    documents=[
        SignatureSessionDocumentData(file=FileReferenceModel(content=pdf_content, name='doc.pdf')),
        SignatureSessionDocumentData(file=FileReferenceModel(url='https://yourapp.com/doc2.pdf')),
    ],
))
return redirect(response.redirect_url)
```

Direct API integration:

```plaintext
POST {endpoint}/api/signature-sessions
{
	...
	"documents": [
		{
			"file": {
				"content": "Base64EncodedBytes==",
				"name": "doc1.pdf"
			}
		},
		{
			"file": {
				"url": "https://yourapp.com/doc2.pdf"
			}
		},
		...
	]
}
```

For more information see [Referencing files](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/file-referencing).

## Disabling downloads

If you don't receive webhook notifications of documents signed, you might feel like the signature session process is not working reliably because some users simply
download the signed documents and don't bother to stick around while they are redirected back to your app, denying it the chance to detect that documents have been
signed. If you feel this is a problem for your case, you can disable the downloading of signed documents by passing `DisableDownloads = true`.

On .NET:

```cs
var response = await restPkiService.CreateSignatureSessionAsync(new CreateSignatureSessionRequest() {
	...
	DisableDownloads = true,
});
return Redirect(response.RedirectUrl);
```

On PHP:

```php
$request = new CreateSignatureSessionRequest();
$request->returnUrl = "http://localhost:8080/session-callback";
$request->disableDownloads = true;

$response = $service->createSignatureSession($request);

header("Location: {$response->redirectUrl}");
```

On Java:

```java
CreateSignatureSessionRequest request = new CreateSignatureSessionRequest();
request.setReturnUrl("http://localhost:60695/signature-session-rest-core/complete");
request.setDisableDownloads(true);

CreateSignatureSessionResponse response = restPkiService.createSignatureSession(request);

return "redirect:" + response.getRedirectUrl();
```

On Node.js:

```js
const response = await client.createSignatureSession({
	returnUrl: 'http://localhost:3000/session-callback',
	disableDownloads: true,
});
res.redirect(response.data.redirectUrl);
```

On Python:

```python
response = signature_sessions_api.api_signature_sessions_post(CreateSignatureSessionRequest(
    return_url='http://localhost:5000/session-callback',
    disable_downloads=True,
))
return redirect(response.redirect_url)
```

Direct API integration:

```plaintext
POST {endpoint}/api/signature-sessions
{
	...
	"disableDownloads": true
}
```

Then, users will only be able to download documents through your app, and thus will be less inclined to quit the process prematurely.

> **Note**
>
> Another way of dealing with this issue is to opt for the **Webhook flow** instead (see next section)

## Webhook flow

On the standard integration flow described above, your app detects that documents have been signed when the user is redirect back with the `signatureSessionId`
query parameter appended to the `returnUrl`.

An alternative flow is to instead be notified of signed documents through [webhook notifications](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/webhooks). In this case, the signature page can be instead
opened on a new tab, which is closed by the end of the process. Hence, the user is never redirected back, and there is no need to implement the callback handler
(instead, you'll implement the webhook handler).

This flow has the advantage of not requiring the user to stick around while they are redirected by to your app for it to take notice of signed files. Also,
enabling [background processing](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/signature-sessions/background-processing), which greatly enhances the user's experience, is simpler in this flow.

To open the signature page on a new tab, use an anchor (`<a>` element) with `target="_blank"`:

```html
<a href="..." target="_blank">Sign documents</a>
```

> **Note**
>
> Another option is to open the tab with the `window.open()` function in Javascript

Furthermore, you must omit the `returnUrl` parameter when creating the session.

On .NET:

```cs
var response = await restPkiService.CreateSignatureSessionAsync(new CreateSignatureSessionRequest() {
	ReturnUrl = null,
	EnableBackgroundProcessing = true,
});
return Redirect(response.RedirectUrl);
```

See [session creation sample with webhook in .NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/SignatureSessionRestCoreController.cs).

On PHP:

```php
$request = new CreateSignatureSessionRequest();
$request->returnUrl = null;
$request->enableBackgroundProcessing = true;

$response = $service->createSignatureSession($request);

header("Location: {$response->redirectUrl}");
```

See [session creation sample with webhook in PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/signature-session-rest-core/using-webhook.php).

On Java:

```java
CreateSignatureSessionRequest request = new CreateSignatureSessionRequest();
request.setEnableBackgroundProcessing(true);

CreateSignatureSessionResponse response = restPkiService.createSignatureSession(request);

return "redirect:" + response.getRedirectUrl();
```

See [session creation sample with webhook in Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/SignatureSessionRestCoreController.java).

On Node.js, leave out `returnUrl`:

```js
const response = await client.createSignatureSession({
	enableBackgroundProcessing: true,
});
res.redirect(response.data.redirectUrl);
```

On Python, leave out `return_url`:

```python
response = signature_sessions_api.api_signature_sessions_post(CreateSignatureSessionRequest(
    enable_background_processing=True,
))
return redirect(response.redirect_url)
```

Direct API integration:

```plaintext
POST {endpoint}/api/signature-sessions
{
	"returnUrl": null,
	"enableBackgroundProcessing": true
}
```

Then, the tab will be closed by Rest PKI Core by the end of the session.

## Improving the user experience

You may use the following optional features to improve the user experience:

* [Certificate requirements](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/signature-sessions/certificate-requirements) may be used to restrict the certificates the user can choose
* [File validation](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/signature-sessions/file-validation)
* [Background processing](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/signature-sessions/background-processing)
* [Document metadata](https://docs.lacunasoftware.com/en-us/articles/rest-pki/core/integration/signature-sessions/document-metadata)

**Demos and samples**

- Live demo: [Sign a file with Rest PKI Core](https://try.lacunasoftware.com/en/pki-suite/sign-a-file-rest-pki-core)
- Sign a PDF with Rest PKI Core: [.NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/PadesSignatureRestCoreController.cs) · [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/PadesSignatureCoreController.java) · [Node.js](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/nodejs/expressmvc/routes/pades-signature-core.js) · [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/pades-signature-rest-core/index.php) · [Python](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/python/flask/sample/views/pades_signature_rest_core.py) · [Ruby](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/ruby/rails/app/controllers/pades_signature_rest_core_controller.rb)
- Signature session with Rest PKI Core: [.NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/SignatureSessionRestCoreController.cs) · [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/SignatureSessionRestCoreController.java) · [PHP](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/php/plain/public/signature-session-rest-core/index.php)
- Signature session with predefined documents using Rest PKI Core: [.NET](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/dotnet/mvc/PkiSuiteAspNetMvcSample/Controllers/SignatureSessionPreDefRestCoreController.cs) · [Java](https://github.com/LacunaSoftware/PkiSuiteSamples/blob/master/java/springmvc/src/main/java/com/lacunasoftware/pkisuite/controller/SignatureSessionPreDefRestCoreController.java)
