Signing via API · White-label

Sign inside your app — Under your own brand, without redirecting the user to any external site

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.

iframe embed or redirect Your own signing domain Identity verification and QES inside the flow
wthaiq:signed signing_session app.yourbrand.com Account activation Review the agreement and sign it to complete registration Embedded · under your brand Your logo here Sign here Signature confirmation
Integration models

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.

1
hosted

The hosted page

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.

  • Zero code in the browser — a direct link
  • runs on the custom domain sign.yourbrand.com
  • Ideal for links sent by email or messaging
2
embedded / iframe

In-app embedding

Issue 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.

  • The highest completion rate — without leaving the page
  • Events wthaiq:viewed / signed / declined
  • A short-lived client token — not a secret key
3
redirect

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 allowiframe
  • The reference is returned in the URL parameter
The signing session

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.

POST /v1/signers/{id}/signing_session

Mints an embedded signing link and a short-lived client token for a specific signer, in White-label mode.

ParameterDescription
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.
Why a short-lived session instead of the secret key? Key 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 Node Response
signing_session.sh
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
  }'
server.js
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 });
200 OK · signing_session
{
  "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
}
White-label

Full brand customisation

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.json
{
  "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 fieldDescription
logo_urlYour logo, displayed at the top of the signing page and in the header of email messages. SVG or a transparent PNG is preferred.
brand_colorYour primary brand colour (HEX); applied to buttons, links and progress indicators.
signing_domainThe custom signing domain, such as sign.acme-pay.com. It is verified through a CNAME record and a TLS certificate issued automatically.
email_from_nameThe sender name shown in invitation and reminder messages — it appears under your brand, not under Wthaiq.
email_from_addressThe sender address on your domain, after configuring SPF and DKIM records to ensure delivery.
remove_wthaiq_brandingat true The "Powered by Wthaiq" line is removed from the page and the emails (available on the plans that support full white-label).
localeThe signer's default interface language, such as ar.
support_emailThe support address shown to the signer when they need help.
The custom domain makes the address bar itself display 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.
Embedding

Embedding in your page — iframe and postMessage events

Download signing_url produced by the session inside iframe, then listen for the window events through postMessage to update your interface in real time.

embed.html
<iframe
  id="wthaiq-frame"
  src="https://sign.acme-pay.com/s/uZ8xR2Kq?t=cst_live_9aF2xQ7bV3"
  allow="camera; microphone"
  style="width:100%;height:760px;border:0;border-radius:16px">
</iframe>

&lt;!-- allow: camera/microphone required for the identity verification step inside the iframe --&gt;
listen.js
window.addEventListener('message', function (e) {
  // Always verify the origin of the message before trusting it
  if (e.origin !== 'https://sign.acme-pay.com') return;

  const evt = e.data;
  switch (evt.type) {
    case 'wthaiq:viewed':
      // The signer opened the document
      console.log('viewed', evt.signer);
      break;
    case 'wthaiq:signed':
      // Signing completed — close the frame and start activation
      onSigned(evt.signature_request);
      break;
    case 'wthaiq:declined':
      // The signer declined — log the reason and redirect
      onDeclined(evt.reason);
      break;
  }
});
wthaiq:viewed

Fired when the signer opens the document for the first time inside the iframe — use it to track the start of the journey.

wthaiq:signed

Fired as soon as signing completes successfully, and carries signature_request andsigner.

wthaiq:declined

Fired when the signer declines or cannot complete, and carries the field reason.

Browser events versus Webhooks. Events postMessage are ideal for updating the interface immediately, but they are not the source of truth — the user may close the tab before they arrive. Always rely on the Webhook signature_request.completed on your server as the final trusted signal before triggering any sensitive action.

Redirection instead of embedding

If you would rather not use theiframe, mint the session using the redirect 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 parameter.

redirect.sh
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": "redirect",
    "success_url": "https://app.acme-pay.com/onboarding/done?sr={signature_request}",
    "cancel_url": "https://app.acme-pay.com/onboarding/canceled"
  }'
Identity and qualified signing

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.

AES · Didit

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.

What the user sees
  • 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.
QES · token agent

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 — the private key never leaves the token (deferred/detached PAdES).

What the user sees
  • 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 authorise 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.
qes-deferred.js
// 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 });
Both steps appear with your own branding and language inside the iframe. See The legal standing page for a breakdown of the SES / AES / QES levels and the ladder of PAdES formats from B to LTA.
The full flow

From creation 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.

  1. Create an embedded signature request

    On your server, create signature_request with its signers, its source and its legal level.

    POST /v1/signature_requests
  2. Get the signing link

    Take signing_url from the signer, or mint signing_session short-lived, for embedding.

    signers[0].signing_url
  3. Embed it in your page

    Load the URL in iframe and listen for the events postMessage to update the interface.

  4. The user completes signing

    verifies their identity (AES) where required, then signs inside your frame without leaving your app.

  5. Receive the trusted Webhook

    Your server receives the completion event — this is the definitive signal to trigger your post-signing logic.

    signature_request.completed
  6. 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
1 · Create 5 · Webhook 6 · Download
create.sh
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"}
  }'
webhook.json
{
  "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"
    }
  }
}
download.sh
# 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>
Security

Embedding 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)

Make expires_in for the shortest sufficient period (300 to 3600 seconds). An expired token is harmless even if it is intercepted.

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.

FAQs

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 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.

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 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.

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.