Embed a complete signing page inside your product: with your own logo, colours and domain. From your server you issue a short-lived signing session, which you embed in iframe or redirect to it, and receive events in real time via postMessage and Webhooks — while the secret key stays on your server alone. Signing, identity verification and qualified signing all sit within a single flow that never leaves your application.
The foundation is the same in all three: you create signature_request on your server, then you get a signing link for the signer. The model differs in How The user reaches that link.
Redirect the user to signing_url provided in the signer field. The fastest way to launch: a complete signing page hosted by Wthaiq on your custom domain, with no interface code.
sign.yourbrand.comIssue signing_session short-lived and load it inside iframe in your page. The user stays exactly where they are, and you receive their events in real time via postMessage.
wthaiq:viewed / signed / declinedRedirect the browser to the signing page, then return it automatically to success_url after signing or cancel_url on cancellation. A middle ground between simplicity and control.
iframeNever send your secret key to the browser. Instead, your server calls this endpoint to mint a temporary session token scoped to a single signer, which is safe to send to the front end.
Mints an embedded signing link and a short-lived client token for a specific signer, in White-label mode.
| Parameter | Description |
|---|---|
| mode string Optional | Session pattern: embedded (default, for embedding in iframe) or redirect (for redirection). |
| allowed_origins string[] Optional | The list of domains allowed to embed the session. Any origins outside it are rejected. Effectively required with embedded. |
| success_url string Optional | The return URL after signing in redirect. The template supports {signature_request}. |
| cancel_url string Optional | The return URL when the user cancels or the session expires. |
| expires_in integer Optional | The token's lifetime in seconds (between 300 and 3600, default 3600). The shorter it is, the safer. |
sk_ has full authority over your account — creating requests, downloading signed documents, managing Webhooks. If it leaked from the browser, any party could take control of it. The session token, by contrast, is restricted to a single signer, to the "signing" scope only, to a few minutes, and to specific domains — so even if it is intercepted, it opens only what it was minted for, and then it expires. curl -X POST https://wthaiq.com/api/v1/signers/sgr_9fA2/signing_session \
-H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Wthaiq-Version: 2026-07-01" \
-H "Content-Type: application/json" \
-d '{
"mode": "embedded",
"allowed_origins": ["https://app.acme-pay.com"],
"expires_in": 900
}'import Wthaiq from '@wthaiq/node';
const wt = new Wthaiq(process.env.WTHAIQ_SECRET_KEY);
// Server-side only — the secret key never leaves your server
const session = await wt.signers.createSigningSession('sgr_9fA2', {
mode: 'embedded',
allowed_origins: ['https://app.acme-pay.com'],
expires_in: 900
});
// Send only this to the browser — nothing else
res.json({ signing_url: session.signing_url });{
"object": "signing_session",
"signer": "sgr_9fA2",
"signature_request": "sr_3n8Kd2Qa1V",
"signing_url": "https://sign.acme-pay.com/s/uZ8xR2Kq?t=cst_live_9aF2xQ7bV3",
"client_token": "cst_live_9aF2xQ7bV3",
"mode": "embedded",
"expires_at": 1754000900,
"created_at": 1754000000
}The signing page is entirely yours. Brand settings are configured at account level by default, and can be overridden per signature request through the object branding inside the request body.
{
"branding": {
"logo_url": "https://cdn.acme-pay.com/logo.svg",
"brand_color": "#0B5FFF",
"signing_domain": "sign.acme-pay.com",
"email_from_name": "Acme Pay",
"email_from_address": "sign@acme-pay.com",
"remove_wthaiq_branding": true,
"locale": "ar",
"support_email": "support@acme-pay.com"
}
}| The field | Description |
|---|---|
| logo_url | Your logo, displayed at the top of the signing page and in the header of email messages. SVG or a transparent PNG is preferred. |
| brand_color | Your primary brand colour (HEX); applied to buttons, links and progress indicators. |
| signing_domain | The custom signing domain, such as sign.acme-pay.com. It is verified through a CNAME record and a TLS certificate issued automatically. |
| email_from_name | The sender name shown in invitation and reminder messages — it appears under your brand, not under Wthaiq. |
| email_from_address | The sender address on your domain, after configuring SPF and DKIM records to ensure delivery. |
| remove_wthaiq_branding | at true The "Powered by Wthaiq" line is removed from the page and the emails (available on the plans that support full white-label). |
| locale | The signer's default interface language, such as ar. |
| support_email | The support address shown to the signer when they need help. |
sign.acme-pay.com instead of the Wthaiq domain — so the user sees no reference to an external provider at any point in the signing journey. The user never leaves your iframe to verify their identity or to sign with a qualified certificate. Both steps appear embedded immediately before signing, and this is what the user sees in each of them.
When you enable require_identity on the signer, the frame shows a full verification step before the signing page opens — and the verification decision is tied to the signature itself.
identity_verified.for qualified signing (method: "token"), signing with the hardware certificate is carried out through a local agent on the user's device at 127.0.0.1:8899 — the private key never leaves the token (deferred/detached PAdES).
signedAttributes (the document's SHA-256 hash) and sends only the hash — no private key is involved here.CMS, and the server embeds it in the document and completes PAdES up to LTA.// 1) The server prepares signedAttributes (the document hash) — no private key
const prep = await wt.signers.prepareTokenSignature('sgr_9fA2');
// prep.digest = SHA-256 of signedAttributes (ESS signingCertificateV2)
// 2) In the browser: the local agent signs on the device
const signed = await fetch('https://127.0.0.1:8899/sign', {
method: 'POST',
body: JSON.stringify({ digest: prep.digest, alg: 'SHA256withRSA' })
}).then(r => r.json());
// The key never leaves the token; the agent returns a CMS structure (adbe.pkcs7.detached)
// 3) The server embeds the CMS in the document and finalises PAdES (LT/LTA)
await wt.signers.completeTokenSignature('sgr_9fA2', { cms: signed.cms });One complete embedding journey: create the request on your server, embed the signing, wait for the trusted completion signal, then download the sealed document.
On your server, create signature_request with its signers, its source and its legal level.
Take signing_url from the signer, or mint signing_session short-lived, for embedding.
Load the URL in iframe and listen for the events postMessage to update the interface.
verifies their identity (AES) where required, then signs inside your frame without leaving your app.
Your server receives the completion event — this is the definitive signal to trigger your post-signing logic.
signature_request.completedRequest the final PDF once the request is complete; its independent cryptographic proof is the sealed evidence record (Ed25519 + an RFC 3161 timestamp).
GET /v1/signature_requests/{id}/downloadcurl -X POST https://wthaiq.com/api/v1/signature_requests \
-H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Wthaiq-Version: 2026-07-01" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f3c9c2e-2b7a-4a1e-9c2f-1d3e4f5a6b7c" \
-d '{
"title": "Merchant agreement — Acme Pay",
"source": {"type":"template","template_id":"tpl_merchant_agreement"},
"legal_level": "aes",
"signers": [
{"name":"Salma Hassan","email":"salma@example.com","type":"individual",
"method":"draw","require_identity":true}
],
"metadata": {"merchant_id":"M-88213"}
}'{
"id": "evt_2M8pQ",
"object": "event",
"type": "signature_request.completed",
"created_at": 1754500000,
"livemode": true,
"data": {
"object": {
"id": "sr_3n8Kd2Qa1V",
"object": "signature_request",
"status": "completed",
"reference": "WTQ-000123",
"download_url": "https://wthaiq.com/api/v1/signature_requests/sr_3n8Kd2Qa1V/download"
}
}
}# Available only once the status becomes completed
curl https://wthaiq.com/api/v1/signature_requests/sr_3n8Kd2Qa1V/download \
-H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Wthaiq-Version: 2026-07-01" \
-o agreement-signed.pdf
# Output: a final sealed application/pdf · Independent cryptographic proof: the evidence record via GET /api/evidence.php?scope=api&doc=<id>Embedding is safe as long as you separate what stays on the server from what reaches the browser.
sk_ to the browser, everThe secret key stays exclusively on the server. The browser sees only the signing link and the short-lived session token.
Make expires_in for the shortest sufficient period (300 to 3600 seconds). An expired token is harmless even if it is intercepted.
Set allowed_origins to your domains only. Any attempt to embed the session from an origin outside the list is rejected.
Do not trigger a sensitive action based on postMessage alone; rely on the Webhook signed withwhsec_ as the source of truth.
No, if you enable full White-label. The signing page appears with your logo and colours and on your custom domain sign.yourbrand.com, emails are sent under your own brand, and the "Powered by Wthaiq" line is removed once you set remove_wthaiq_branding: true. The user stays inside your experience from start to finish.
signing_session instead of passing the secret key? because sk_ has full privileges over your account and must never reach the browser. The short-lived session token is restricted to a single signer, to the signing permission only, to specific domains and to a few minutes — so even if it is intercepted, it opens only what it was minted for and then expires automatically.
postMessage and Webhooks? Browser events such as wthaiq:signed to update the interface in real time, but it may not arrive if the user closes the tab. The Webhook is like signature_request.completed reaches your server reliably and signed withwhsec_, which is the source of truth for any sensitive action such as activation.
through a local signing agent on the user's device at 127.0.0.1:8899. The server prepares the hash signedAttributes and sends only the hash, so the user enters the token code and the device signs locally — the private key never leaves the token — and then it is embedded CMS in the document to complete PAdES up to LTA. This is deferred/detached signing.
iframe? Use the pattern redirect: mint the session with success_url andcancel_url, then send the browser to signing_url. Wthaiq returns the user to your route automatically with the reference in the URL. Or use the hosted page directly via signing_url on your custom domain.
Start with a single signing session in minutes, then embed a complete signing experience under your own brand and domain without sending any user to an external site.