Skip to content

Didit.me — Identity Verification (KYC)

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:

  1. Configure credentials in WPsigner → Integrations → Didit.
  2. Choose the KYC policy in WPsigner → More → Security & Compliance.

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 document

The 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:

  • A Didit Business account at business.didit.me
  • At least one Verification Workflow created in the Didit console
  • An API Key with sufficient permissions
  • A Webhook Secret from your workflow settings

  1. Create a Didit Business account

    Go to business.didit.me and register. No credit card is required for the free tier.

  2. Create a Verification Workflow

    Inside the Didit Business Console:

    • Go to WorkflowsCreate Workflow
    • Add the following steps (in order):
      • ID Document Verification — scans and validates government-issued ID
      • Selfie Capture — takes a photo of the signer
      • Liveness Detection — confirms the person is physically present
    • Save and note the Workflow ID (a UUID like 550e8400-e29b-41d4-a716-446655440000)
  3. Get your API Key

    • Go to Settings → API Keys in the Didit Business Console
    • Click Create API Key
    • Copy the key immediately — it is only shown once
  4. Get your Webhook Secret

    • Open your Workflow settings
    • Go to the Webhook tab
    • Copy the Webhook Signing Secret (used for HMAC-SHA256 signature verification)
  5. Configure WPsigner

    In your WordPress admin, go to WPsigner → Integrations → Didit:

    • Paste your API Key
    • Paste your Workflow ID
    • Paste your Webhook Secret
    • Click Save Settings
  6. Register the Webhook URL

    • Copy the Webhook URL shown in the WPsigner Didit settings page
    • It follows this format: https://yoursite.com/?wps_didit_webhook=1
    • Go back to Didit Business Console → your Workflow → Webhook tab
    • Paste the URL and save
  7. Test 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).

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


FieldWhere to Find ItDescription
API KeyBusiness Console → Settings → API KeysServer-to-server authentication key
Workflow IDBusiness Console → Workflows → (your workflow)UUID that identifies which verification steps to run
Webhook SecretBusiness Console → Workflows → Webhook tabHMAC-SHA256 signing secret for webhook verification
Webhook URLWPsigner → 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:

  1. The signer opens the signing link (/wpsigner/{token})
  2. If KYC is required and not yet verified, a verification prompt appears — they cannot proceed to sign without completing it
  3. Clicking “Verify My Identity” creates a session and opens Didit’s verification page in a new tab (popup)
  4. The signer completes three steps within the Didit interface:
    • Scans or photographs their ID document (passport, national ID, or driver’s license)
    • Takes a live selfie
    • Completes a liveness challenge (e.g., blink, turn head)
  5. Upon completion, the Didit tab closes automatically and the signing page detects the result via BroadcastChannel / localStorage
  6. If approved, the signing interface unlocks and the signer can proceed
  7. If declined, an error message is shown and the signer can retry

The entire process typically takes 30–90 seconds.


WPsigner tracks the following KYC statuses per signer:

StatusMeaning
pendingA session has been created; the signer has not yet completed verification
StartedThe signer has opened the Didit verification page
Callback_ReceivedThe signer returned from Didit (browser callback received); awaiting authoritative webhook
ApprovedIdentity verified — signer can proceed to sign
DeclinedVerification failed — signer cannot sign
ExpiredSession 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.

Webhook Signature Verification (X-Signature-V2)

Section titled “Webhook Signature Verification (X-Signature-V2)”

Every webhook from Didit includes an X-Signature-V2 header and an X-Timestamp header. WPsigner verifies the webhook before processing:

  1. Timestamp freshness check — Rejects webhooks older than ±5 minutes to prevent replay attacks
  2. HMAC-SHA256 verification — Computes HMAC-SHA256(sorted_json_body, webhook_secret) and compares with hash_equals() (timing-safe comparison)
  3. Fail-closed — If no webhook secret is configured, all webhooks are rejected with 401

The signature algorithm:

  • Sorts all JSON keys recursively
  • Converts integer-value floats to integers
  • Encodes with JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
  • Signs with HMAC-SHA256

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

Token Ownership Verification (IDOR Prevention)

Section titled “Token Ownership Verification (IDOR Prevention)”

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.

OperationLimit
Create session50 attempts per signer per hour
Check status (poll)60 requests per signer per minute
Callback URL10 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:

OptionContent
wps_didit_api_keyAES-256 encrypted API key
wps_didit_workflow_idPlain text Workflow UUID
wps_didit_webhook_secretAES-256 encrypted webhook secret

Every KYC event is automatically logged to the WPsigner Activity Timeline with full detail:

EventWhen it firesLogged Detail
kyc_session_startedWhen a new verification session is createdSession UUID
kyc_callback_receivedWhen the user returns from Didit’s page”Awaiting webhook confirmation” note
kyc_verifiedWhen webhook confirms ApprovedDocument type, issuing country, face match score, liveness result
kyc_declinedWhen webhook confirms DeclinedProvider 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_LICENSE
  • issuing_country — ISO 3166-1 alpha-2 country code
  • liveness — liveness check result
  • providerDidit.me
  • methodID + Selfie + Liveness

This 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:

  • Always when an Approved webhook is received (to fetch biometric details)
  • Proactively when the signer returns from Didit (Callback_Received status) to get the decision faster than waiting for the webhook

Response (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_KEY
Content-Type: application/json
Accept: application/json
User-Agent: WPsigner/3.x.x

Didit 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"
}
]
}
HeaderDescription
X-Signature-V2HMAC-SHA256 signature of the request body
X-TimestampUnix timestamp of when the request was sent
Content-Typeapplication/json

WPsigner only accepts the following status values from Didit webhooks. Unknown statuses are silently ignored:

  • Approved
  • Declined
  • Pending
  • Started
  • Expired

WPsigner 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:

ColumnTypeDescription
kyc_session_idVARCHAR(100)Didit session UUID
kyc_statusVARCHAR(50)Current KYC status (see status table above)
kyc_verified_atDATETIMETimestamp when Approved was received
kyc_dataLONGTEXTJSON blob with biometric evidence and session metadata

The migration runs automatically on plugin initialization via WPS_Didit::ensure_tables().


”API request failed” on Test Connection

Section titled “”API request failed” on Test Connection”
  • Double-check the API key has been copied correctly (no trailing spaces)
  • Make sure the key has not been revoked in the Didit console
  • Test Connection returns 401 or 403 → API key is invalid
  • Test Connection returns 404 → API key is valid (expected for dummy UUID)
  • Verify the webhook URL is registered in Didit’s Business Console under your workflow
  • Make sure your site is publicly reachable (not localhost or behind a firewall)
  • Check your WordPress permalink settings — pretty permalinks must be enabled
  • Check for a web application firewall (WAF) blocking POST requests from Didit’s IPs
  • Enable 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:

  1. Go to WPsigner → Integrations → Didit
  2. Paste your Webhook Secret from the Didit Business Console
  3. Click Save Settings

This usually means the webhook was not received. Check:

  1. Is the webhook URL registered in Didit?
  2. Did Didit receive a non-200 response? Check the Didit Business Console → Webhook logs
  3. Is the webhook secret correct?

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.

Didit KYC toggle reverts to OFF after saving

Section titled “Didit KYC toggle reverts to OFF after saving”

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.