Click-to-sign on HTML
Prerequisites
A pair of API key ID and API key secret.
Some credits for the Webpage Forensic API workflow.
The what? And the why?
Generally, a click-to-sign signature is a type of electronic signature where a user demonstrates their intent to sign a document or agree to terms by clicking a button, checkbox, or link (e.g., "I agree" or "Accept"). This method is widely used in online transactions and agreements due to its simplicity and speed.
Right there you can find the first caveat: due to its simplicity, there are key elements missing to really prove that the signer effectively did sign. Some implementations may use the log files and other very similar mechanisms. However, that interaction can be repudiated by the alleged signer.
The Webpage Forensic API patented solution proposed in Web Trust fills the gap left in the click-to-sign eco-system. This solution leverages digital forensic techniques, following the standard ISO/IEC 27037:2012, to help prove that an interaction took place from a certain IP address using a device with certain properties.
One of the goals of our solution is to make the signer experience as seamless as possible. Of course, there is a price to pay; integrating this solution requires a little bit more effort since the signature interaction takes place inside a web page that must be rendered by the company before being presented to the signer.
An Example
Let’s assume there is a company with an online onboarding process, and at some point of that process it wants the clients to accept certain conditions and would like having that part with a value of a signature. However, the client should not get out of the process to make the signature; instead, it would be enough to tick some checkboxes and click an Accept button.
This example is ideal for the click-to-sign signature.
Let’s assume that this is the page, terms.html

<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Company - Terms</title>
<style>html {
background-color: #fff;
}
body {
font-family: sans-serif;
font-size: 14px;
padding: 0;
margin: 0;
color: #303030;
}
form > ol > li > strong:first-child {
display: block;
margin: 10px 0;
font-weight: bold;
font-size: 16px;
color: #11254b;
}
ol, ul {
padding-left: 16px;
}
h1 {
margin: 20px 0 0;
font-size: 20px;
color: #5198f4;
}
h2 {
margin: 5px 0 20px;
font-size: 16px;
color: #5198f4;
}
form {
display: block;
max-width: 660px;
margin: 0 auto;
padding: 20px 20px 120px;
background-color: #f9f9f9;
}
#preferences {
padding: 1em;
margin: 2em 0;
background-color: #fff;;
}
#preferences .checkbox {
margin: 1em 0;
}
#preferences .checkbox::after {
content: '';
display: table;
clear: both;
}
label {
display: block;
position: relative;
padding-left: 2em;
cursor: pointer;
}
label > input[type='checkbox'] {
display: block;
position: absolute;
left: 0;
cursor: pointer;
}
input[type='submit'] {
min-width: 100px;
display: inline-block;
appearance: none;
outline: none;
cursor: pointer;
margin: 20px 10px;
border: 0;
border-radius: 3px;
padding: 0 10px;
height: 40px;
font-size: 14px;
font-weight: bold;
line-height: 40px;
color: #fff;
text-align: center;
text-decoration: none;
text-transform: uppercase;
background-color: #5198f4;
box-shadow: 0 0 5px #00000033, 0 5px 5px -5px #00000033;
}
</style>
</head>
<body>
<form action='/terms/accept' method='post'><h1>Service Terms</h1>
<h2>that the client must accept</h2>
<ol>
<li><strong>SOME TERMS</strong>
<ol>
<li>Term1.
</li>
<li>Term2</li>
</ol>
</li>
<li><strong>OTHER STUFF</strong>
<ul>
<li>Info here
</li>
<li>Also here
</li>
</ul>
</li>
</ol>
<div id='preferences'>
<div class='checkbox'><label for='mandatory'><input id='mandatory' name='mandatory' type='checkbox' required><strong>I hereby declare something important<span style='color:red'> *</span></strong></label>
</div>
<div class='checkbox'><label for='optional'><input id='optional' name='optional' type='checkbox'>This is optional instead.</label></div>
</div>
<div><input type='hidden' name='contractID' value='23442'><input type='submit' name='accept' value='Accept' id='accept'>
</div>
</form>
<script>
var privacy = document.getElementById('privacy');
privacy.required = false;
var accept = document.getElementById('accept');
accept.addEventListener('click', function (e) {
if (!privacy.checked) e.preventDefault(true);
window.scrollTo(0, document.documentElement.scrollHeight);
});
</script>
</body>
</html>As you can see, that’s a fairly common page. Just a form with some text, some checkboxes and a button.
Pay attention to the form’s action property. In this example, action is an endpoint /processes/accept. This endpoint is called using a POST once the form is submitted.
Let’s also assume that this page is served using ExpressJS.
Exampleimport express from "express";
import {join} from "path";
const app = express();
const port = 3000;
app.get("/terms", (req, res) => {
res.sendFile(path.join(__dirname, "/terms.html"));
});
app.post("/terms/accept", (req, res) => {
// Process the form submission
// ...
res.sendFile(path.join(__dirname, "/outcome.html"));
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
});For the sake of simplicity, we just created two controller methods: a GET to load terms.html and a POST to process the form submission.
Using Webpage Forensic API
To use our click-to-sign solution (Webpage Forensic API), you will need to create an interaction passing a modified version of terms.html, instead of serving it yourself and redirecting the client to an URL we give you in response.
First, let’s create a minimal payload:
{
"oURL": "https://company.com/terms/accept",
"prerenderedHtml": "THE HTML WHERE THE SIGNATURE TAKES PLACE",
"variation": "full",
}Here:
oURLis the original URL where the form is submitted. Take a look at how to protect your oURL endpoint.prerenderedHtmlis a string containing the HTML that Web Trust should serve, in which you need to replace the original URL where the form is submitted with the placeholder<%= e_url %>. In this example,/terms/acceptmust be replaced with<%= e_url %>.variationshould always be“full“for this workflow.
Now we need to create the interaction and redirect the client to the returned fUrl.
Exampleimport express from "express";
import {join} from "path";
import {readFile}
const app = express();
const port = 3000;
app.get("/terms", async (req, res) => {
let prerenderedHtml = await readFile(path.join(__dirname, "/terms.html"));
prerenderedHtml = prerenderedHtml.replace(/\/terms\/accept/, "<%= e_url %>");
const res = await fetch("https://api.webtrust.kopjra.com/v1/interactions", {
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Basic NjhiOTkxMjEtYWIxNC00YzUwLWFlMzItNDgzZmQ4MWVkNWJkOnlBRG1YTDZ6IzNjVm4tWURPNmJsTWkybGY0MEpNMEdUcFpLVH5+Ujc1eUNEN3ZZVGU2fmJTdTJEOUZwNHFEVjl4RWhtVmRZSw==",
},
method: "POST",
body: JSON.stringify({
"oURL": "https://company.com/terms/accept",
"prerenderedHtml": prerenderedHtml,
"variation": "full",
}),
});
// Avoiding error management
res.redirect((await res.json()).fUrl);
});
app.post("/terms/accept", (req, res) => {
// Process the form submission
// ...
res.sendFile(path.join(__dirname, "/outcome.html"));
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
});As you can see, we are practically implementing some sort of man in the middle. Instead of serving terms.html directly, we are delegating Web Trust to do this.
And guess what the client will see,

Exactly the same! Or so she will think, under the hood, Web Trust will gather key information to build a proof of the signature.
When the client clicks on the ACCEPT button, Web Trust will redirect her browser to oURL with exactly the same request; this way the app.post method can do what it did before.
Using the Proxy functionality (recommended)
If we add to the interaction creation payload actAsProxy: true , Web Trust will proxy the request to oURL instead of doing the redirect using 307 and will show the client the response of the oURL call.
Even though this seems very similar to what we did before, proxying the oURL request gives more information to build the forensic proof, making it stronger. Also, it solves a problem in Safari with a redirect using 307; essentially, Safari transforms a 307 redirect of a POST into a GET, which might be a problem.