Sign inside your app under your own brand, without redirecting the user to any external site
Embed a complete signing page in your product, with your own logo, colors and domain. You issue a short-lived signing session from your server, then embed it in an 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.
Three ways to integrate signing — Choose what suits 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.
The hosted page
Redirect the user to signing_url which comes ready in the signer's data. It is the fastest way to launch: a complete signing page hosted by Wthaiq on your custom domain, with no front-end code.
- Zero code in the browser — a direct link
- runs on the custom domain
sign.yourbrand.com - Ideal for links sent by email or messaging
In-app embedding
Issue signing_session short-lived, then load it inside an iframe in your page. The user stays exactly where they are, and you receive the session events in real time via postMessage.
- The highest completion rate — without leaving the page
- Events
wthaiq:viewed / signed / declined - A short-lived client token — not a secret key
Redirect
Redirect 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.
- A guaranteed return to your application's route
- Suitable for environments that do not allow
iframe - The reference is returned in the URL parameter
Generate a short-lived signing session
Never 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.
Issues an embedded signing link valid for 30 minutes for a specific signer, as part of the branded signing pattern.
| Parameter | Description |
|---|---|
| return_url string Optional | A full https URL on a domain accepted in your account, to which the signer returns after signing; available on the Business plan and above. The session lifetime is fixed at 30 minutes. |
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 '{ "return_url": "https://app.acme-pay.com/signed" }'// Server-side only — the secret key never leaves your server
const r = await fetch('https://wthaiq.com/api/v1/signers/sgr_9fA2/signing_session', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WTHAIQ_SECRET_KEY}`,
'Wthaiq-Version': '2026-07-01',
'Content-Type': 'application/json'
},
body: JSON.stringify({ return_url: 'https://app.acme-pay.com/signed' })
});
const session = await r.json();
// Send only this to the browser — nothing else
res.json({ signing_url: session.signing_url });{
"object": "signing_session",
"signer": "sgr_9fA2",
"signing_url": "https://wthaiq.com/embed/sign/9aF2xQ7bV3..",
"expires_at": 1754001800
}Full brand customization
The signing page is entirely yours. Branding is set once at the account level from the “Branding” page in the developer console, is available on the “Business” plan and above, and is applied automatically to every signature request you create through the API; the request body carries no branding settings.
{
"brand_name": "Acme Pay",
"logo_url": "https://cdn.acme-pay.com/logo.svg",
"brand_color": "#0B5FFF",
"custom_domain": "sign.acme-pay.com",
"from_email": "sign@acme-pay.com",
"hide_powered": true,
"sign_welcome_ar": "مرحبًا بك في Acme Pay. راجع العقد ثم وقّعه.",
"sign_welcome_en": "Welcome to Acme Pay. Review the contract, then sign.",
"sign_button_color": "#0B5FFF"
}| The field | Description |
|---|---|
| brand_name | Your brand name shown on the signing page; it also becomes the sender name of invitation and reminder emails once your domain is verified. |
| logo_url | Your logo, shown at the top of the signing page. SVG or PNG with a transparent background is preferred. |
| brand_color | Your primary brand color (HEX); applied to buttons, links and progress indicators. |
| custom_domain | The custom signing domain, such as sign.acme-pay.com. The custom domain is activated by contacting our team. |
| from_email | The sender address on your domain, after configuring SPF and DKIM records to ensure delivery. |
| hide_powered | When on, the “via Wthaiq platform” line is hidden from the signing page for API requests. The signature certificate and the public verification page always name Wthaiq. |
| sign_welcome_ar sign_welcome_en | A welcome sentence shown to the signer at the top of the signing page in the page language: plain text, up to 240 characters. |
| sign_button_color | The colour of the primary buttons on the signing page, in HEX. If absent, the brand colour is used; a light colour is not accepted because the button text is white. |
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. Identity verification and QES — inside the same flow
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.
Identity verification before signing
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.
- Capturing the official documentThe iframe asks for a photograph of the ID card or the passport to be captured with the camera directly inside your page.
- Live face matchThe user is asked to move their face in front of the camera to confirm that they are a live person present, and to match them against the photograph on the document.
- "Verifying", then approval ofA processing indicator appears, then a green success mark — and the signer's status becomes
identity_verified. - Opening the signing pageOnly after approval does the signing area become available, so the user signs with their signature already tied to a verified identity.
Qualified signing with a token
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 without the private key ever leaving the token (deferred, detached PAdES).
- Agent and token detectionThe page checks that the local agent is running and the token is connected, and displays "Token detected".
- Preparing the signature on the serverThe server computes
signedAttributes(the document's SHA-256 hash) and sends only the hash — no private key is involved here. - Entering a PIN on the deviceThe agent asks for the token PIN, which the user enters to authorize a single signing operation on the device.
- Signing locally, then embedding the CMSThe token signs the hash and returns a structure
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 signingPage.prepareTokenSignature();
// 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 finalizes PAdES (LT/LTA)
await signingPage.completeTokenSignature({ cms: signed.cms });From the request to the signed document — step by step
One complete embedding journey: create the request on your server, embed the signing, wait for the trusted completion signal, then download the sealed document.
- Create an embedded signature request
On your server, create
POST /v1/signature_requestssignature_requestwith its signers, its source and its legal level. - Get the signing link
Get
signers[0].signing_urlsigning_urlfrom the signer's data, or issuesigning_sessionshort-lived, for embedding. - Embed it in your page
Load the URL in
iframeand listen for the eventspostMessageto update the interface. - The user completes signing
verifies their identity (AES) where required, then signs inside your frame without leaving your app.
- Receive the trusted notification
Your server receives the completion event — this is the definitive signal to trigger your post-signing logic.
signature_request.completed - Download the sealed document
Request 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}/download
curl -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-1A2B3C4D5E6F",
"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"
# Output: links to the final contract (JSON) · independent cryptographic proof: the evidence record via GET /v1/signature_requests/{id}/evidenceEmbedding security rules
Embedding is safe as long as you separate what stays on the server from what reaches the browser.
Never send sk_ to the browser, ever
The secret key stays exclusively on the server. The browser sees only the signing link and the short-lived session token.
A short token lifetime (TTL)
The session link expires 30 minutes after it is created, and an expired link is useless even if intercepted; so create the session at the moment the signer needs it.
An allowed-domains list
Set allowed_origins to your domains only. Any attempt to embed the session from an origin outside the list is rejected.
Verify events on the server
Do not trigger a sensitive action based on postMessage alone; rely on the Webhook signed withwhsec_ as the source of truth.
Developer questions about embedding
Does the user see any reference to Wthaiq while signing?
No, if you enable full White-label. The signing page appears with your logo and colors 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 hide_powered: true. The user stays inside your experience from start to finish.
Why do I mint 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.
What is the difference between the events 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.
How does QES qualified signing work inside the iframe?
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.
What if our environment does not allow the use of theiframe?
Use the pattern redirect: mint the session with success_url and cancel_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.
Sign inside your application — under your own brand.
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.