Skip to content

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.


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 }
}
OptionDescription
languageLanguage of the document version to show.
metadataKey/value pairs stored with the record (merged with CMP.setMetadata).
approvalTokenOne-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' }
});
OptionSent asDescription
version_iddocument_version_idVersion to approve. If omitted, the SDK resolves the active version for language.
version—Version label returned in the result when you pass version_id.
languagelanguageLanguage of the version.
decisiondecision'accepted' (default) or 'rejected'.
visitor_idvisitor_idYour reference for the person (max. 100 characters). Ignored when an approval token is used — the token’s subject is stored instead.
approvalTokenapproval_tokenOne-time approval token.
document_content_base64custom_document_contentContent of a document generated by your system (HTML, plain text or PDF; up to about 500 KB decoded).
document_mime_typecustom_document_mime_typetext/html, text/plain or application/pdf.
signaturetop-level fieldsSee Signature fields.
metadatametadataKey/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.


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:

  1. Your backend — which knows the logged-in user — calls POST /tool/v1/s2s/approval-tokens (see REST API) and receives a token (hat_…).
  2. You pass the token to the page and hand it to the SDK as approvalToken.
  3. 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 with identity_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.

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',
signer_email: '[email protected]',
declaration_text: 'I have read and accept the service contract.',
input_type: 'touch',
stroke_count: 14,
drawing_duration_ms: 3200,
screen_resolution: '390x844'
}
});
FieldSent asNotes
image_base64signature_imagePNG, JPEG or WebP; up to about 150 KB decoded.
mime_typesignature_mime_type
signer_name, signer_email, signer_phone, signer_id_numbersame namesWhat the signer states about themselves, unless vouched for by an approval token.
declaration_textdeclaration_textThe sentence the person agreed to (max. 1000 characters).
input_typeinput_typetouch, mouse or pen.
stroke_count, drawing_duration_ms, strokes_datasame namesDrawing telemetry.
screen_resolutionscreen_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:
ValueMeaning
self_declaredThe signer details came from the browser. Nobody verified them.
tenant_attestedYour 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.


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: false with error.code = "DOCUMENT_VERSION_CHANGED", and document:error fires.

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.


CMP.on('document:approved', ({ slug, version, consentId }) => {});
CMP.on('document:rejected', ({ slug, version }) => {});
CMP.on('document:error', ({ slug, version, code, status, message }) => {});
error.codeWhat to do
MODULE_LOAD_FAILEDThe module file could not be loaded (network, ad blocker, CSP). Call again to retry.
DOCUMENT_NOT_FOUND, VERSION_NOT_FOUNDCheck the slug, that the document is attached to the site and that it has an active version in the requested language.
APPROVAL_TOKEN_REQUIREDMint a token on your backend and pass it.
APPROVAL_TOKEN_INVALID, APPROVAL_TOKEN_CONFLICTMint a new token; make sure the signer fields match the token.
DOCUMENT_VERSION_CHANGEDShow the document again.
CONFIG_OUTDATEDThe version you passed is not active and could not be re-checked.
NETWORK_ERROR, TIMEOUT, RATE_LIMIT_EXCEEDED, QUEUE_OVERLOADEDTemporary. 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.