AI Face Match
Compares the selfie with the ID photo using biometric analysis with a confidence score.
Didit.me is an AI-powered Know Your Customer (KYC) identity verification platform. When integrated with WPsigner, signers must prove their identity using a government-issued ID document, a selfie, and a real-time liveness check before they can sign a document.
Identity verification strengthens the evidence associated with a signing event, but it does not by itself guarantee legal validity or replace advice about the requirements in your jurisdiction.
AI Face Match
Compares the selfie with the ID photo using biometric analysis with a confidence score.
Liveness Detection
Prevents spoofing attacks with real-time liveness checks — no photos or videos accepted.
ID Document OCR
Extracts and validates data from passports, national IDs, and driver’s licenses from 190+ countries.
Audit Trail
Verification decisions and relevant metadata are recorded in the WPsigner Activity Timeline; identity images remain with Didit.
The integration follows a two-step activation model:
Once enabled, this is the end-to-end flow:
Signer opens signing page ↓WPsigner detects KYC is required ↓Signer clicks "Verify Identity" ↓WPsigner calls Didit API → creates a verification session ↓Signer is redirected to Didit's verification page (new tab) ↓Signer completes: ID scan → Selfie → Liveness check ↓Didit sends webhook to WPsigner with result (Approved / Declined) ↓Callback page closes → signing page unlocks automatically ↓Verified signer can now sign the documentThe signing page polls for status in the background using BroadcastChannel and localStorage as cross-tab communication mechanisms, meaning the signer never has to manually reload.
Before setting up the integration you need:
Create a Didit Business account
Go to business.didit.me and register. No credit card is required for the free tier.
Create a Verification Workflow
Inside the Didit Business Console:
550e8400-e29b-41d4-a716-446655440000)Get your API Key
Get your Webhook Secret
Configure WPsigner
In your WordPress admin, go to WPsigner → Integrations → Didit:
Register the Webhook URL
https://yoursite.com/?wps_didit_webhook=1Test the connection
Click Test Connection in the WPsigner Didit settings page. A success message confirms your API key is valid. The test sends a request to the Didit API and expects a 404 response (a valid key will get a 404 for a non-existent dummy session — that confirms authentication worked).
Enable KYC requirement
Go to WPsigner → More → Security & Compliance and set the KYC policy to Always, Per document, or Off. With Per document, enable KYC in the document’s Review step only when it is needed.
| Field | Where to Find It | Description |
|---|---|---|
| API Key | Business Console → Settings → API Keys | Server-to-server authentication key |
| Workflow ID | Business Console → Workflows → (your workflow) | UUID that identifies which verification steps to run |
| Webhook Secret | Business Console → Workflows → Webhook tab | HMAC-SHA256 signing secret for webhook verification |
| Webhook URL | WPsigner → Integrations → Didit (read-only) | URL Didit will POST results to — register this in your workflow |
All credentials are encrypted at rest and are never stored in plaintext in the database.
From the signer’s perspective, the verification flow is seamless:
/wpsigner/{token})BroadcastChannel / localStorageThe entire process typically takes 30–90 seconds.
WPsigner tracks the following KYC statuses per signer:
| Status | Meaning |
|---|---|
pending | A session has been created; the signer has not yet completed verification |
Started | The signer has opened the Didit verification page |
Callback_Received | The signer returned from Didit (browser callback received); awaiting authoritative webhook |
Approved | Identity verified — signer can proceed to sign |
Declined | Verification failed — signer cannot sign |
Expired | Session expired before completion (a new session will be created on next attempt) |
The integration is designed with a fail-closed security model — when in doubt, it blocks rather than allows.
Every webhook from Didit includes an X-Signature-V2 header and an X-Timestamp header. WPsigner verifies the webhook before processing:
HMAC-SHA256(sorted_json_body, webhook_secret) and compares with hash_equals() (timing-safe comparison)401The signature algorithm:
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODEOnce a signer’s KYC status reaches Approved, it cannot be downgraded by any subsequent webhook. This prevents an attacker from forging a Declined webhook after approval.
Each webhook is deduplicated using a transient key based on MD5(session_id + status + timestamp) with a 60-second TTL. Duplicate or replayed webhooks are silently ignored with 200 OK (to prevent Didit from retrying).
When a signer requests a KYC session via AJAX, WPsigner verifies that the submitted token matches the signer’s access_token using hash_equals(). This prevents Insecure Direct Object Reference (IDOR) attacks — a signer cannot create or check sessions for other signers.
| Operation | Limit |
|---|---|
| Create session | 50 attempts per signer per hour |
| Check status (poll) | 60 requests per signer per minute |
| Callback URL | 10 requests per signer per minute |
| Save settings (admin) | 10 saves per user per minute |
If a signer already has an active (non-terminal) session, WPsigner reuses the existing Didit verification URL instead of creating a new session. This prevents unnecessary API charges and avoids flooding Didit with duplicate sessions.
Terminal statuses that force a new session: Declined, Expired, Failed, Abandoned.
Credentials are stored encrypted in WordPress options:
| Option | Content |
|---|---|
wps_didit_api_key | AES-256 encrypted API key |
wps_didit_workflow_id | Plain text Workflow UUID |
wps_didit_webhook_secret | AES-256 encrypted webhook secret |
Every KYC event is automatically logged to the WPsigner Activity Timeline with full detail:
| Event | When it fires | Logged Detail |
|---|---|---|
kyc_session_started | When a new verification session is created | Session UUID |
kyc_callback_received | When the user returns from Didit’s page | ”Awaiting webhook confirmation” note |
kyc_verified | When webhook confirms Approved | Document type, issuing country, face match score, liveness result |
kyc_declined | When webhook confirms Declined | Provider and session info |
The biometric evidence stored on Approved includes:
face_match_score — percentage confidence (e.g., 98.2)document_type — e.g., PASSPORT, NATIONAL_ID, DRIVER_LICENSEissuing_country — ISO 3166-1 alpha-2 country codeliveness — liveness check resultprovider — Didit.memethod — ID + Selfie + LivenessThis data is stored as JSON in the kyc_data column and is included in the legal PDF certificate generated at signing completion.
WPsigner communicates with the Didit API v3 at https://verification.didit.me/v3.
Endpoint: POST /session/
Creates a new verification session for a signer.
Request body sent by WPsigner:
{ "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "vendor_data": "signer-42", "callback": "https://yoursite.com/?wps_didit_callback=1&signer_id=42", "callback_method": "both", "metadata": "{\"document_id\": 10, \"signer_id\": 42, \"site_url\": \"https://yoursite.com\"}", "language": "en", "contact_details": { "email": "signer@example.com", "send_notification_emails": false }, "expected_details": { "first_name": "John", "last_name": "Doe" }}Response (on success):
{ "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://verify.didit.me/session/a1b2c3d4-e5f6-7890-abcd-ef1234567890"}WPsigner stores session_id and url in the wps_signers row for that signer.
Endpoint: GET /session/{session_id}/decision/
Retrieves the authoritative decision for a completed session. Called by WPsigner:
Approved webhook is received (to fetch biometric details)Callback_Received status) to get the decision faster than waiting for the webhookResponse (on approval):
{ "session_id": "a1b2c3d4-...", "status": "Approved", "features": [ { "feature": "face_match", "score": 0.982 }, { "feature": "liveness", "status": "passed" } ], "id_verifications": [ { "document_type": "PASSPORT", "issuing_country": "US" } ]}All API requests include:
x-api-key: YOUR_API_KEYContent-Type: application/jsonAccept: application/jsonUser-Agent: WPsigner/3.x.xDidit sends POST requests to your configured webhook URL when a session status changes.
Your webhook URL (shown in WPsigner settings) follows this format:
https://yoursite.com/?wps_didit_webhook=1{ "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "Approved", "vendor_data": "signer-42", "metadata": "{\"document_id\": 10, \"signer_id\": 42, \"site_url\": \"https://yoursite.com\"}", "features": [ { "feature": "face_match", "score": 0.982 }, { "feature": "liveness", "status": "passed" } ], "id_verifications": [ { "document_type": "PASSPORT", "issuing_country": "US" } ]}| Header | Description |
|---|---|
X-Signature-V2 | HMAC-SHA256 signature of the request body |
X-Timestamp | Unix timestamp of when the request was sent |
Content-Type | application/json |
WPsigner only accepts the following status values from Didit webhooks. Unknown statuses are silently ignored:
ApprovedDeclinedPendingStartedExpiredWPsigner always responds with 200 OK and a JSON body. Didit will retry on non-200 responses, so returning 200 even for invalid payloads (after logging) is intentional.
The Didit integration adds four columns to the {prefix}_wps_signers table via a one-time migration:
| Column | Type | Description |
|---|---|---|
kyc_session_id | VARCHAR(100) | Didit session UUID |
kyc_status | VARCHAR(50) | Current KYC status (see status table above) |
kyc_verified_at | DATETIME | Timestamp when Approved was received |
kyc_data | LONGTEXT | JSON blob with biometric evidence and session metadata |
The migration runs automatically on plugin initialization via WPS_Didit::ensure_tables().
401 or 403 → API key is invalid404 → API key is valid (expected for dummy UUID)WP_DEBUG to see error logs: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true);The webhook secret is mandatory. If you see this error:
This usually means the webhook was not received. Check:
WPsigner also queries the Didit API directly when the signer returns to the signing page (Callback_Received status), so even if the webhook is delayed, the status should update within a few seconds of the signer completing verification.
This means the webhook secret saved in WPsigner does not match the one in Didit. Resave the correct secret in WPsigner → Integrations → Didit.
Ensure that you are saving the policy from WPsigner → More → Security & Compliance, not the Didit integration settings page. The credentials and the KYC policy are configured separately.
Which countries and document types are supported?
Didit supports government-issued IDs from 190+ countries, including passports, national identity cards, and driver’s licenses.
Does WPsigner store biometric data (photos, scans)?
No. WPsigner never receives or stores the actual ID photos or selfie images. Only the verification decision metadata (face match score, document type, country, liveness result) is stored for the audit trail.
Can signers retry if their verification is declined?
Yes. A new verification session is automatically created when the signer clicks “Verify My Identity” again after a Declined or Expired status.
Can I use Didit.me on a per-document basis instead of globally?
Yes. Set the KYC policy to Per document in Security & Compliance, then enable KYC in the document’s Review step. Always requires KYC for every signing request, and Off disables the gate.
What happens if the Didit service is down?
If the Didit API is unreachable when creating a session, the signer sees an error message and cannot proceed. WPsigner uses a 15-second timeout for API requests. If the webhook does not arrive but the signer completes verification, WPsigner will query the Didit API directly on the next status poll to retrieve the decision.
Is Didit.me GDPR compliant?
Yes. Didit.me processes verification data under its own privacy policy. WPsigner only stores the non-PII verification result metadata described above. Refer to Didit’s Privacy Policy for their data processing terms.
What does “face match score” mean?
The face match score (0–100%) represents the biometric confidence that the selfie matches the ID document photo. A score ≥ 70% is considered a passing match by WPsigner’s audit trail logic.