Document Approval & Signature
Besides cookie consent, the SDK can record that a person approved a specific version of a document — terms of service, a distance sales contract, a privacy notice, or a document generated by your own system.
The approval code is not part of cmp.js. It is loaded automatically the first time you call one of the two methods below (or up front with data-modules="documents"); see Bundles and on-demand modules.
Two ways to collect an approval
Section titled “Two ways to collect an approval”1. CMP.showDocumentApproval(slug, options) — HashEntry’s dialog
Section titled “1. CMP.showDocumentApproval(slug, options) — HashEntry’s dialog”Loads the active version of the document, shows it in a dialog with “Accept” and “Decline” buttons and records the approval when the person accepts.
const result = await CMP.showDocumentApproval('distance-sales-agreement', { language: 'en', // optional, defaults to the SDK language metadata: { order_id: 'ORD-54321' }, // optional approvalToken: tokenFromYourBackend // optional / required, see below});
if (result.success && result.decision === 'accepted') { continueCheckout(result.consent_token);} else if (result.decision === 'rejected') { // the person declined} else { showError(result.error); // { code, status, message }}| Option | Description |
|---|---|
language | Language of the document version to show. |
metadata | Key/value pairs stored with the record (merged with CMP.setMetadata). |
approvalToken | One-time approval token minted by your backend. |
For backward compatibility, a plain object without any of these keys is treated as metadata.
Declining closes the dialog and fires document:rejected; no record is sent to the server. If saving fails with a temporary error, the dialog stays open and the person can press “Accept” again — the retry uses the same idempotency key (and the same token).
2. CMP.approveDocument(slug, options) — your own UI
Section titled “2. CMP.approveDocument(slug, options) — your own UI”Records an approval without showing anything, for a checkbox in your form, your own contract viewer or a signature pad you render yourself.
const result = await CMP.approveDocument('terms-of-service', { approvalToken: tokenFromYourBackend, metadata: { order_id: 'ORD-54321' }});| Option | Sent as | Description |
|---|---|---|
version_id | document_version_id | Version to approve. If omitted, the SDK resolves the active version for language. |
version | — | Version label returned in the result when you pass version_id. |
language | language | Language of the version. |
decision | decision | 'accepted' (default) or 'rejected'. |
visitor_id | visitor_id | Your reference for the person (max. 100 characters). Ignored when an approval token is used — the token’s subject is stored instead. |
approvalToken | approval_token | One-time approval token. |
document_content_base64 | custom_document_content | Content of a document generated by your system (HTML, plain text or PDF; up to about 500 KB decoded). |
document_mime_type | custom_document_mime_type | text/html, text/plain or application/pdf. |
signature | top-level fields | See Signature fields. |
metadata | metadata | Key/value pairs stored with the record. |
Two calls for the same document, version and decision that overlap in time share one request and one result, so a double click does not create two records.
One-time approval tokens
Section titled “One-time approval tokens”The browser SDK signs its requests with the site API key, which is visible in your page source. A document approval sent from a browser therefore proves that someone with access to your page sent it; the name or e-mail address in it is whatever was typed in.
An approval token lets your backend vouch for who is approving:
- Your backend — which knows the logged-in user — calls
POST /tool/v1/s2s/approval-tokens(see REST API) and receives a token (hat_…). - You pass the token to the page and hand it to the SDK as
approvalToken. - The SDK sends it as top-level
approval_token. The server consumes it atomically, takes the person’s identity from the token rather than from the browser, and stores the record withidentity_assurance: tenant_attested.
Rules:
- A token is valid for one site and one document (optionally one version), for a limited time (default 10 minutes, at most 1 hour), and once.
- A document can be configured in the dashboard to require a token. For such a document the SDK fails immediately, without sending a request, when no token is given:
error.code = "APPROVAL_TOKEN_REQUIRED". The server enforces the same rule. - Server codes are passed through unchanged:
APPROVAL_TOKEN_REQUIRED,APPROVAL_TOKEN_INVALID(unknown, expired, already used, or issued for another site/document),APPROVAL_TOKEN_CONFLICT(a signer field or the document content sent by the browser contradicts what the token vouches for). - On a network failure or a 5xx response the SDK retries with the same token and the same idempotency key; the server treats the repeat as the same request. After a definitive 4xx answer nothing is retried, and approvals are never stored in the offline queue. Mint a new token to try again.
Signature fields
Section titled “Signature fields”approveDocument accepts a signature object for approvals collected with a drawn signature or an explicit declaration. The SDK does not include a signature pad: you render the pad (or the declaration checkbox) and pass the result.
await CMP.approveDocument('service-contract', { approvalToken: tokenFromYourBackend, signature: { image_base64: canvas.toDataURL('image/png'), mime_type: 'image/png', signer_name: 'Jane Doe', declaration_text: 'I have read and accept the service contract.', input_type: 'touch', stroke_count: 14, drawing_duration_ms: 3200, screen_resolution: '390x844' }});| Field | Sent as | Notes |
|---|---|---|
image_base64 | signature_image | PNG, JPEG or WebP; up to about 150 KB decoded. |
mime_type | signature_mime_type | |
signer_name, signer_email, signer_phone, signer_id_number | same names | What the signer states about themselves, unless vouched for by an approval token. |
declaration_text | declaration_text | The sentence the person agreed to (max. 1000 characters). |
input_type | input_type | touch, mouse or pen. |
stroke_count, drawing_duration_ms, strokes_data | same names | Drawing telemetry. |
screen_resolution | screen_resolution |
All of these are sent as top-level fields of the request, not inside metadata. The fields otp_verified / otp_reference are not accepted from a browser; they are honoured only on the server-to-server route or through an approval token.
What “simple electronic signature” means here
Section titled “What “simple electronic signature” means here”A record created this way is a simple electronic signature: data in electronic form (the drawn image, the declaration, the technical context) that is attached to a specific document version and that the signer used to sign. It is not an advanced or qualified electronic signature — no certificate is issued to the signer, and HashEntry does not verify the signer’s identity.
What the evidence shows:
- which document version (by content hash) was approved, when, from which IP address and user agent, and with which declared signer details;
- that the record has not been altered since it was written (see Verifying evidence offline);
- how strongly the identity is backed, in the field
identity_assurance:
| Value | Meaning |
|---|---|
self_declared | The signer details came from the browser. Nobody verified them. |
tenant_attested | Your backend vouched for the person, either with a one-time approval token or by declaring a completed OTP check on the server-to-server route. The assurance is yours: HashEntry records that you attested it, not that HashEntry checked it. |
Whether a simple electronic signature is sufficient for a given contract depends on the applicable law and on the kind of transaction; some transactions require a specific form. Ask your legal counsel which level you need.
When the document version changes
Section titled “When the document version changes”If the server answers 409 CONFIG_OUTDATED, the SDK re-reads the active version of the document:
- unchanged (the server’s cache was cold) → the same request is sent once more;
- changed → the approval is not re-sent. The result is
success: falsewitherror.code = "DOCUMENT_VERSION_CHANGED", anddocument:errorfires.
In the second case show the document again (the person has not seen the new version) and collect a new approval — with a new token if you use tokens.
Results, events and errors
Section titled “Results, events and errors”CMP.on('document:approved', ({ slug, version, consentId }) => {});CMP.on('document:rejected', ({ slug, version }) => {});CMP.on('document:error', ({ slug, version, code, status, message }) => {});error.code | What to do |
|---|---|
MODULE_LOAD_FAILED | The module file could not be loaded (network, ad blocker, CSP). Call again to retry. |
DOCUMENT_NOT_FOUND, VERSION_NOT_FOUND | Check the slug, that the document is attached to the site and that it has an active version in the requested language. |
APPROVAL_TOKEN_REQUIRED | Mint a token on your backend and pass it. |
APPROVAL_TOKEN_INVALID, APPROVAL_TOKEN_CONFLICT | Mint a new token; make sure the signer fields match the token. |
DOCUMENT_VERSION_CHANGED | Show the document again. |
CONFIG_OUTDATED | The version you passed is not active and could not be re-checked. |
NETWORK_ERROR, TIMEOUT, RATE_LIMIT_EXCEEDED, QUEUE_OVERLOADED | Temporary. Let the person try again. |
VALIDATION_ERROR and size errors (SIGNATURE_IMAGE_TOO_LARGE, CUSTOM_DOCUMENT_TOO_LARGE, …) | Fix the request. |
Treat an approval as given only when result.success === true. Keep result.consent_token: it identifies the record for later verification and evidence export.