Short answer. To add QES to your website you need neither your own cryptographic backend nor any PKI knowledge. You make one SDK import from https://dstucrypt.io/embed/dstucrypt-embed.mjs, call embed('sign', { mount: 'modal' }) — and a signing modal appears on your page. The user picks a key file and enters the password inside an isolated iframe, while your code receives a ready CMS signature: signature.base64 to send to your backend, or signature.download() to save it as a file. Below is the full path from zero to your first signed document.
What you get in the end — and what you won't have to do
After ten minutes of integration, real document signing works on your website: the user clicks a button, sees a modal, picks their file-based key, enters the password — and your JavaScript receives a promise with the result. The result contains the signature itself (bytes or base64), the signer's certificate identifier signerCertId, and the accredited field, which lets you distinguish a qualified signature (QES) from a merely valid one.
Just as important is what you do not have to do. You don't host the widget files, the core or the .wasm yourself — everything loads from the dstucrypt.com.ua origin, and the SDK builds the URLs itself. You don't write a single line of cryptography: the DSTU 4145 signature is computed by a proven crypto core compiled to WebAssembly. And you don't need to dig into PKI internals — timestamps (TSP), OCSP and CRL go through a built-in proxy with automatic fallback, with nothing for you to configure.
Why an iframe widget, specifically. You host nothing — not a single service file sits on your server. There is no cryptography on your side — hence nothing to certify or update. The widget version is always current: fixes and new formats arrive without rebuilding your site. And most importantly, the user's key and password are isolated inside an iframe on a different origin: even an XSS on your page cannot reach them.
Step 1. One import instead of a backend
The whole integration starts (and almost ends) with a single line — an ES module import right on your page:
step 1 — importing the SDK on the page
import { embed } from 'https://dstucrypt.io/embed/dstucrypt-embed.mjs';
No npm package, no build step, no API keys in the code. The SDK builds the widget URL from its own origin, so you never write file URLs by hand and keep no copies on your side (self-hosting is deliberately forbidden — key isolation only works with the service's origin). For local development there is a localhost mode — it is always free.
Step 2. Creating the widget: embed('sign', { mount: 'modal' })
Calling embed() creates a dedicated iframe for one widget and returns its instance. Together with step 3, the minimal working example looks like this:
steps 2–3 — the full minimal example
import { embed } from 'https://dstucrypt.io/embed/dstucrypt-embed.mjs';
const signer = await embed('sign', { mount: 'modal' });
// fileBytes: File | Blob | ArrayBuffer | Uint8Array | string
const { signature, signerCertId, accredited } = await signer.sign(fileBytes, {
format: 'CAdES-BES',
});
signature.download('document.p7s'); // save the signature as a file
On sign() the widget opens a modal: the user picks a key file, enters the password — and the promise resolves with the result. You pass your data as is: a File from an input, an ArrayBuffer, a Uint8Array or a string — no base64 conversion needed on input.
Step 3. Handling the signing result
The methods return bytes in a Bytes wrapper — convenient for getting any representation you need. The most typical scenario is sending the signature to your backend:
handling the result + errors
try {
const { signature, accredited } = await signer.sign(fileBytes);
await fetch('/api/documents/42/signature', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ signature: signature.base64, accredited }),
});
// or: signature.bytes (Uint8Array), signature.blob(), signature.size
} catch (e) {
if (e.code === 'not_licensed' || e.code === 'load_failed') return; // the modal has already been shown
showError('Signing failed: ' + e.message); // cancelled, wrong password, etc.
}
The accredited field is worth storing alongside the signature: true means the certificate was issued by an accredited provider — in other words, it is a genuine QES. And when the backend makes the acceptance decision, duplicate the check with the authoritative server-side POST /api/verify.
Which signature format to pick for a start
The format option accepts the full format string — CAdES, PAdES (PDF), XAdES (XML), ASiC. For a start the choice is simple:
| Format | What it is | When to use |
|---|---|---|
| CAdES-BES | basic CMS signature, a .p7s file ; the default | internal document workflow, first launch, offline scenarios |
| CAdES-T | the same + a qualified timestamp (TSP) | when proving the moment of signing matters; needs an online TSP, which the SDK fetches itself |
Start with CAdES-BES and switch to CAdES-T as soon as you need a timestamp — it is a one-line change. The remaining levels (CAdES-C, CAdES-XL, PAdES for PDF, XAdES for XML, ASiC containers) will be covered in a separate upcoming article on signature formats.
The default signature hash is DSTU GOST 34.311-95: this pairing with DSTU 4145 is what government validators accept today. The modern national standard — the Kupyna hash (DSTU 7564:2014) — is enabled with a single option, digest:'kupyna-256', wherever the receiving side supports it.
Which keys are supported
The widget opens all common file containers issued by Ukrainian providers: PKCS#12 (.p12/.pfx), JKS, PKCS#8 and Key-6.dat. The user doesn't need to know what kind of file they have: the widget opens the container itself and selects the signing key by keyUsage. If the container lacks a certificate, the widget will offer to upload one. Hardware tokens are not supported — file-based keys only.
Mount modes: modal or inline block
The mount option determines where the widget lives. 'modal' (the default) is a modal window over the page that opens for the duration of the operation; the iframe inside grows automatically to fit the content. The alternative is embedding the widget into your own page block by passing a CSS selector or an Element:
inline mode — the widget inside your own block
<!-- on the page: -->
<div id="sign-box"></div>
const signer = await embed('sign', { mount: '#sign-box' });
const { signature } = await signer.sign(fileBytes, { format: 'CAdES-T' });
Inline mode is convenient when signing is the central action of the page (a document workflow dashboard, a contract page), while the modal fits when signing is just one step in a flow. When the widget is no longer needed, call widget.destroy().
How much it costs
The license is tied to a domain. Every new domain automatically gets 7 days free — no registration, just connect the widget. After that, the base subscription is UAH 4,500/month or UAH 38,880/year per domain; pay online via LiqPay on the /buy page and manage domains in your account. localhost for development is always free.
What's next: verification, QES sign-in, encryption
Signing is just one of six widgets. The same embed() call connects verify (signature verification with details per signer), auth (website sign-in with a QES and server-side identity confirmation), encrypt/decrypt (CMS EnvelopedData encryption) and cert (certificate parsing and online status). They all start the same way — with the single import you have already made.
Frequently asked questions
Do I need to install or host anything to add QES to my website?
No. You make one SDK import from https://dstucrypt.io/embed/dstucrypt-embed.mjs — all widget files and the cryptographic core load from our origin. The SDK builds the widget URL itself, so you never write file URLs and host nothing on your side.
Does the user's key or password ever reach my server?
No. All cryptography runs in the user's browser (WebAssembly) inside an iframe on the dstucrypt.io origin. The private key and password never reach your page's code, your server, or DSTUcrypt's servers.
How much does the QES widget for a website cost?
Every new domain gets 7 days free automatically — just connect the widget. After that, the base subscription is UAH 4,500/month or UAH 38,880/year per domain, paid online via LiqPay.
Which signature format should I choose at the start?
For a start, CAdES-BES is enough — the basic default format (a .p7s file). If you need a qualified timestamp, specify format:'CAdES-T' — the SDK obtains the timestamp itself through the built-in proxy, with nothing to configure.
Read next
- Signing in to a website with a QES: user authentication via electronic signature
- Verifying an electronic signature on a website
- QES key files: PKCS#12/PFX, JKS, PKCS#8 and Key-6.dat
Try it: QES on your website today
Ready-made DSTU signing, verification and encryption widgets. The key and password never leave the user's browser. Every new domain gets 7 days free.
Live demoHow to integrate