Skip to content
For developers · quickstart guide

From zero to your first signed contract in minutes.

This guide walks you through five direct steps, from creating an API key to getting a legally valid signed contract and verifying it. Examples are ready to copy in cURL, Node, Python and PHP. Start with a test key: nothing is charged and no message is sent to anyone; then switch to the live key at launch.

A free test mode within a daily limit Official libraries: PHP · Node.js · Python · iOS · Android A sealed, verifiable evidence record
201 Created A signed contract
create-signature-request.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-..." \
  -d '{
    "title":"Employment Contract — Ahmed M.",
    "source":{"type":"template","template_id":"tpl_employment"},
    "legal_level":"aes",
    "ordered":true,
    "signers":[
      {"name":"Ahmed Mohamed","email":"ahmed@example.com","method":"draw","require_identity":true}
    ]
  }'
Before you begin

Three requirements to get started.

Simple preparation you can finish in a minute, then move straight to your first API call.

Developer account

Create a developer account in the developer console with your work email. From there you manage your keys, your templates, your signature requests and your event logs. A developer account is separate from Wthaiq platform accounts, so if your email is already registered on the platform, use another one.

A secret API key

Create a key with the prefix sk_ from the “Integration keys” page in the developer console. It gives you full access to the API and is active from the moment it is created, so every signature request you send with a live key is real and counts against your plan allowance.

A plan for live sending

Test mode is free within a daily limit on the trial plan, while live sending requires a paid yearly developer plan and a verified company. See the Pricing page to choose the plan that suits your usage volume.

Start with a test key. Test key sk_test_ charges no balance, sends no invitation or reminder to anyone, accepts your account email as the signer, and goes no further than a simple signature. The live key sk_ so it really sends messages and issues binding, legally valid contracts. Every signer counts as one send from your annual plan balance, then from your wallet at the overage rate once it runs out, plus $1.50 from the wallet for each signer who requires identity verification.
A paid plan and a verified company before live sending. Live requests require an active paid plan and a company verified from the “Company verification” page in the developer console; otherwise the error returned is 402 plan_required or 403 kyb_required. The test key is free within a daily limit.
Five steps

From the key to the signed contract — step by step.

Follow the steps in order. Each step stands on its own and comes with copy-ready examples in four languages.

1 Authentication

Get your API key

From the developer console, open Integration keys and create a new key; choose “Test key” while you are building your integration. Keys come in two types:

KeyDescription
sk_...A secret key for server-side use only, with full permissions. The live one actually sends messages, issues legally valid contracts and its sends are billed; the test version (sk_test_) does none of that.
pk_...A publishable key, safe to expose in the browser, locked to your domains (allowed_origins), and limited to a set of paths (templates, signature requests, signers, flows).

Authentication is performed through the header Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx with every request. Also pin the API version through the header Wthaiq-Version: 2026-07-01 so that the behavior of the API stays stable across updates.

Never expose the secret key in client-side code. The secret key (sk_) is used on the server only. Never place it in a web or mobile app or in a public repository. If it leaks, revoke it immediately from the developer console and create a new one. See Security guide for the details.
2 Configuration

Set up the API connection

Call the API directly with cURL or any HTTP client without installing a package, or use one of the official server libraries (PHP · Node.js · Python) as the following tabs show.

cURLNodePythonPHP
shell
# cURL needs no SDK — just set your key
export WTHAIQ_API_KEY="sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Check the connection by fetching your account's templates
curl https://wthaiq.com/api/v1/templates \
  -H "Authorization: Bearer $WTHAIQ_API_KEY" \
  -H "Wthaiq-Version: 2026-07-01"
terminal + index.js
npm install wthaiq
import Wthaiq from 'wthaiq';
const wt = new Wthaiq('sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
terminal + main.py
pip install wthaiq
import wthaiq
wt = wthaiq.Client('sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
terminal + index.php
composer require wthaiq/wthaiq-php
$wt = new \Wthaiq\Client('sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
See the full list of libraries, including the two in-app signing libraries (iOS · Android), on The SDK page. All of them follow semantic versioning (SemVer), and the server libraries pin the API version header automatically.
3 Creation

Create your first signature request

Now create a signature request from a ready-made template. In the following example we use the employment contract template (tpl_employment), and a single signer who signs by drawing their signature (method: draw) after identity verification is passed (require_identity: true), which raises the request to the advanced signature level (AES).

POST /v1/signature_requests
cURLNodePythonPHP
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-..." \
  -d '{
    "title":"Employment Contract — Ahmed M.",
    "source":{"type":"template","template_id":"tpl_employment"},
    "legal_level":"aes",
    "ordered":true,
    "signers":[
      {"name":"Ahmed Mohamed","email":"ahmed@example.com","type":"individual","method":"draw","require_identity":true,
       "fields":{"job_title":"Software Engineer","salary":"25000"}}
    ],
    "reminders":{"enabled":true,"interval_hours":48,"max":3},
    "metadata":{"order_id":"A-1024"}
  }'
create.js
const sr = await wt.signatureRequests.create({
  title: 'Employment contract — Ahmed M.',
  source: { type: 'template', template_id: 'tpl_employment' },
  legal_level: 'aes',
  ordered: true,
  signers: [
    { name: 'Ahmed Mohamed', email: 'ahmed@example.com', type: 'individual',
      method: 'draw', require_identity: true,
      fields: { job_title: 'Software engineer', salary: '25000' } }
  ],
  reminders: { enabled: true, interval_hours: 48, max: 3 },
  metadata: { order_id: 'A-1024' }
});

console.log(sr.signers[0].signing_url);
create.py
sr = wt.signature_requests.create(
    title='Employment contract — Ahmed M.',
    source={'type': 'template', 'template_id': 'tpl_employment'},
    legal_level='aes',
    ordered=True,
    signers=[
        {'name': 'Ahmed Mohamed', 'email': 'ahmed@example.com', 'type': 'individual',
         'method': 'draw', 'require_identity': True,
         'fields': {'job_title': 'Software engineer', 'salary': '25000'}}
    ],
    reminders={'enabled': True, 'interval_hours': 48, 'max': 3},
    metadata={'order_id': 'A-1024'},
)

print(sr.signers[0].signing_url)
create.php
$sr = $wt->signatureRequests->create([
    'title' => 'Employment contract — Ahmed M.',
    'source' => ['type' => 'template', 'template_id' => 'tpl_employment'],
    'legal_level' => 'aes',
    'ordered' => true,
    'signers' => [
        ['name' => 'Ahmed Mohamed', 'email' => 'ahmed@example.com', 'type' => 'individual',
         'method' => 'draw', 'require_identity' => true,
         'fields' => ['job_title' => 'Software engineer', 'salary' => '25000']],
    ],
    'reminders' => ['enabled' => true, 'interval_hours' => 48, 'max' => 3],
    'metadata' => ['order_id' => 'A-1024'],
]);

echo $sr->signers[0]->signing_url;

Response is an object signature_request with status sent, and contains the signer together with the signing link signing_url ready to open or embed:

201 Created · application/json
{
  "id": "sr_3n8Kd2Qa1V",
  "object": "signature_request",
  "livemode": true,
  "status": "sent",
  "title": "Employment contract — Ahmed M.",
  "legal_level": "aes",
  "format": "pades-lt",
  "source": { "type": "template", "template_id": "tpl_employment" },
  "ordered": true,
  "require_identity": true,
  "reference": null,
  "reminders": { "enabled": true, "interval_hours": 48, "max": 3 },
  "expires_at": 1755000000,
  "completed_at": null,
  "download_url": null,
  "metadata": { "order_id": "A-1024" },
  "created_at": 1754000000,
  "signers": [
    {
      "id": "sgr_9fA2",
      "object": "signer",
      "name": "Ahmed Mohamed",
      "email": "ahmed@example.com",
      "type": "individual",
      "method": "draw",
      "require_identity": true,
      "order": 1,
      "status": "sent",
      "signing_url": "https://wthaiq.com/sign.php?token=uZ8..",
      "identity": { "status": "required", "provider": "external", "level": "aes" }
    }
  ]
}
idThe request ID, with the prefix sr_. Use it in all subsequent calls (tracking, download, cancellation).
statusThe status of the request. It starts as sent after sending, and passes through viewed and partially_signed up to completed.
livemodetrue because the request was created with a live key and was actually sent, and a valid certificate will be issued when signing completes. With a test key the value is false and nothing is sent.
signers[].signing_urlThe URL of the hosted signing page. Send the signer to it directly, or embed it in your application through a short-lived signing session.
signers[].identityThe identity verification status with a specialized identity-verification provider (official document and live face match). It must be approved before the signer can sign, because the request includes require_identity.
referencenull now. The public verification reference is assigned WTQ-... automatically when signing completes.

A shorter path — a session from a workflow: If you have set the document, the signature method and the send channel in a published workflow in the developer console, your server needs just three things: the workflow ID, the secret key and the webhook signing secret. The request names only the workflow and the signers, and the result comes back to your server in a signed webhook.

POST /v1/sessions
RequestWebhookVerification
session.sh
curl -X POST https://wthaiq.com/api/v1/sessions \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Wthaiq-Version: 2026-07-01" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f3c9c2e-..." \
  -d '{
    "workflow_id":"wf_8f3a1c9e2b",
    "vendor_data":"A-1024",
    "signers":[
      {"name":"Ahmed Mohamed","email":"ahmed@example.com"}
    ]
  }'
# {"id":"sr_3n8Kd2Qa1V","object":"session","status":"sent","workflow_id":"wf_8f3a1c9e2b","workflow_version":1,"vendor_data":"A-1024","sign_url":"https://wthaiq.com/sign.php?token=uZ8..",…}
signature_request.completed · POST body
{
  "id": "evt_2M",
  "object": "event",
  "type": "signature_request.completed",
  "created_at": 1754500000,
  "livemode": true,
  "data": {
    "object": {
      "id": "sr_3n8Kd2Qa1V",
      "object": "signature_request",
      "status": "completed",
      "reference": "WTQ-1A2B3C4D5E6F",
      "workflow_id": "wf_8f3a1c9e2b",
      "workflow_version": 1,
      "vendor_data": "A-1024"
    }
  }
}
verify.js
const crypto = require('crypto');

// rawBody: the raw body exactly as received · header: the Wthaiq-Signature header value · secret: whsec_...
function verify(rawBody, header, secret) {
  const p = Object.fromEntries(header.split(',').map(x => x.split('=')));
  if (!p.t || Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(p.t + '.' + rawBody).digest('hex');
  return expected.length === (p.v1 || '').length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
}

// After verification succeeds: event.data.object.vendor_data is your reference, and workflow_id is your workflow
wf_…The ID of the published workflow in your account, sent in the field workflow_id.
sk_…The secret key, sent only from your server in the header Authorization.
whsec_…The webhook signing secret, used to verify the header Wthaiq-Signature before you rely on any webhook.

If you set a return URL in the workflow, the signer comes back to your site after signing or declining. You will find all request fields, what each request may change, and the errors on the the Signing via API page.

4 Tracking

Track the status

There are two ways to know when the other party signs. Choose manual polling for quick experiments, and Webhooks for production.

Method one — polling: Query the request whenever you like. The status status reflects the latest state.

GET /v1/signature_requests/{id}
poll.sh
curl https://wthaiq.com/api/v1/signature_requests/sr_3n8Kd2Qa1V \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Wthaiq-Version: 2026-07-01"
The recommended method: Webhooks. Instead of repeated polling, let Wthaiq notify your server in real time. Register an endpoint and listen for the event signature_request.completed, so you react as soon as signing completes, with no manual polling. Details of registration and signature verification are in The Webhooks page.

Method two — Webhook: The full event body reaches you through POST to your endpoint, and the affected resource is wrapped under data.object:

signature_request.completed · POST body
{
  "id": "evt_2M",
  "object": "event",
  "type": "signature_request.completed",
  "created_at": 1754500000,
  "livemode": true,
  "data": {
    "object": {
      "id": "sr_3n8Kd2Qa1V",
      "object": "signature_request",
      "status": "completed",
      "legal_level": "aes",
      "reference": "WTQ-1A2B3C4D5E6F",
      "completed_at": 1754500000,
      "download_url": "https://wthaiq.com/api/v1/signature_requests/sr_3n8Kd2Qa1V/download"
    }
  }
}

Verify the signature header Wthaiq-Signature on every incoming request, and return the response 2xx quickly, and run the heavy work asynchronously.

5 Download and verification

Download the signed contract and verify it

As soon as the status becomes completed, request the final signed contract data. This endpoint is available for completed requests only and returns JSON containing pdf_url (the final PDF with the verification certificate and its reference, downloaded with a secret key) and final_document_url (the final document page). The independent cryptographic proof for this path is the the sealed evidence record (JSON), not a PAdES signature embedded inside the PDF — download it through GET /v1/signature_requests/{id}/evidence.

GET /v1/signature_requests/{id}/download
download.sh
curl https://wthaiq.com/api/v1/signature_requests/sr_3n8Kd2Qa1V/download \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# {"object":"download","final_document_url":"https://wthaiq.com/final/…","reference":"WTQ-1A2B3C4D5E6F","pdf_url":null,…}

To verify the contract's integrity publicly, use Reference code WTQ-... assigned on completion. Any party holding the reference can confirm the document's authenticity and integrity — programmatically or through the public page ‎/verify‎.

GET /v1/verifications/{reference}
verify.sh
curl https://wthaiq.com/api/v1/verifications/WTQ-1A2B3C4D5E6F \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Response
{
  "object": "verification",
  "reference": "WTQ-1A2B3C4D5E6F",
  "found": true,
  "status": "completed",
  "integrity": "intact",
  "document": { "title": "Employment contract", "sha256": "a3f1..", "hash_alg": "hmac-sha256", "format": "platform-evidence", "legal_level": "aes" }
}
How is integrity guaranteed? The hash is computed HMAC-SHA256 of the document text at sealing time (not of the downloaded PDF), and the request is sealed with an authenticated evidence record bearing a signature of type Ed25519 (which any party can verify with the public key at /trust) and a certified timestamp to RFC 3161. Any later change to the stored document text changes the hash, turning integrity to modified and tampering is detected immediately. More details in The legal standing page.
FAQs

Developer questions before you start.

Is there a test environment?

Yes. Create a test key sk_test_ from the developer dashboard with the “Test key” option: it charges no balance, sends no invitation or reminder to anyone, accepts your account email as the signer, goes no further than a simple signature, and its objects carry livemode: false. For use in the browser without exposing full permissions, use a publishable key pk_ — limited to specific paths and locked to your domains.

What is the difference between SES, AES and QES for me as a developer?

You control the level through the two fields legal_level and method. The simple level, SES, is a drawn signature with a verification code sent by email. The advanced level, AES, adds documented identity verification (an official document and a live face match through a specialized identity-verification provider) that ties the signature to a real person — enable it by setting require_identity: true. The qualified QES level uses a certificate held on a hardware token (method: token) from a licensed certification authority, and carries the highest legal standing under Egyptian Electronic Signature Law No. 15 of 2004. Fuller details in The legal standing page.

Does signing happen inside my application?

Both options are open to you. The easiest is to use signing_url hosted, which is returned with every signer. And if you want an embedded experience under your own brand without leaving your app, request a short-lived signing session via POST /v1/signers/{id}/signing_session and embed it in your interface. See API reference for the details of embedded signing (White-label).

How do I secure my keys?

Use the secret key on the server only, and never place it in web or mobile client code or in a public repository. Store it in environment variables or a secrets vault, restrict it to the least possible privilege, and rotate it regularly. If you suspect it has leaked, revoke it immediately from the developer console and create a new one. Also secure your Webhook endpoints by verifying the header Wthaiq-Signature. See Security guide.

Which SDK languages are supported?

Five official libraries: three for server integration (PHP · Node.js · Python) and two for in-app signing (iOS · Android). Their repositories live on the github.com/wthaiq organization. They follow semantic versioning (SemVer), and the server libraries pin the API version header automatically. The full list and the install commands are on The SDK page.

Ready for your first signed contract?

Start now with a free test key and run the full flow in minutes, with your own email as the recipient of the first test.