Developer
Guide

Add human verification to your platform in 5 minutes. No API keys, no SDKs, just HTTP.

Quick Start

Create Session

POST to create a verification session. Get a QR code back.

Show QR

Display the QR code. User scans with any eIDAS wallet.

Get Result

Poll for the result. Get verified claims.

1. Create a verification session

curl -X POST https://www.idonce.com/vp/sessions \
  -H 'Content-Type: application/json' \
  -d '{"client_id": "your-platform"}'

Response:

{
  "session_id": "vp_a1b2c3d4...",
  "qr_data": "openid4vp://authorize?request_uri=...",
  "deeplink": "idonce://vp?request_uri=...",
  "poll_url": "https://www.idonce.com/vp/sessions/vp_a1b2c3d4...",
  "expires_at": "2026-04-02T15:05:00Z"
}

2. Show the QR code to your user

Render qr_data as a QR code on your page. On mobile, open deeplink directly. The user scans with the idonce app (or any eIDAS-compatible wallet), confirms with biometrics, and the credential is presented automatically.

3. Poll for the result

curl https://www.idonce.com/vp/sessions/vp_a1b2c3d4...

When the user completes verification:

{
  "session_id": "vp_a1b2c3d4...",
  "status": "presented",
  "verified_at": "2026-04-02T15:01:23Z",
  "subject": "a9f3...e7b1",
  "vct": "https://idonce.com/credentials/HumanVerification/v1",
  "disclosed_claims": {
    "biometricConfirmed": true,
    "deviceBound": true,
    "attestationPlatform": "ios"
  }
}

That's it. biometricConfirmed: true means a real human on a real device. subject is a stable, pseudonymous device hash you can use to detect duplicates.

Integration Patterns

QR Code Flow

Desktop Web

Create session, render QR, poll for result. Best for desktop websites where users verify with their phone.

Deep Link Flow

Mobile Web & Apps

Open the deeplink URL directly. The idonce app handles verification and returns control to your app.

Pre-Registration

Signup Flows

Require verification before account creation. Only real humans can register. Eliminates bot signups entirely.

On-Demand

Sensitive Actions

Trigger verification at checkout, before voting, or when posting. Step-up authentication without passwords.

Use Cases

Community

Forums & Comments

Verify before first post. No CAPTCHAs, no email confirmation.

Check biometricConfirmed: true and allow posting. Use subject to detect ban evasion across accounts.
Commerce

Fraud Prevention

Require verification at checkout for high-value orders.

Combine HumanVerification + EmailVerification for verified email from a verified human.
Democracy

Voting & Polls

One person, one vote. Without collecting names or IDs.

The subject hash is stable per device. Same human = same subject = duplicate vote detected.
API

Bot Protection

Gate API access behind human verification. Stronger than rate limiting.

Verify once per session, then issue your own session token. Or verify per sensitive operation.
Identity

Email + Human

Know it's a real email from a real person. Without knowing who.

Request both: HumanVerification + EmailVerification. Verified email bound to a verified human.
Platform

Verified Badges

Give users a "Verified Human" badge backed by real verification.

Store the subject hash with the user profile. Re-verify periodically or on suspicious activity.

Request Specific Credentials

By default, a VP session requests HumanVerification. You can request any credential type:

curl -X POST https://www.idonce.com/vp/sessions \
  -H 'Content-Type: application/json' \
  -d '{
    "client_id": "your-platform",
    "requested_credentials": [
      {
        "id": "human",
        "vct": "https://idonce.com/credentials/HumanVerification/v1",
        "claims": ["biometricConfirmed", "attestationPlatform"]
      },
      {
        "id": "email",
        "vct": "https://idonce.com/credentials/EmailVerification/v1",
        "claims": ["emailVerified", "email"]
      }
    ]
  }'

The user chooses which claims to disclose. You only get what they consent to share.

API Reference

POST /vp/sessions

Create a new verification session.

FieldTypeDescription
client_idstringYour platform identifier (optional, defaults to "anonymous")
requested_credentialsarrayCredentials to request (optional, defaults to HumanVerification)

GET /vp/sessions/{id}

Poll for verification result.

FieldTypeDescription
statusstringpending, presented, or expired
subjectstringStable device hash (only when presented)
disclosed_claimsobjectVerified claims the user consented to share
vctstringCredential type URI
verified_atstringISO 8601 timestamp of verification

Trust Model

How do you know a credential is real?

Every credential contains an iss (issuer) field signed into the JWT. The issuer is identified by a did:web identifier tied to their domain. When a credential is presented, two things happen automatically:

1. Cryptographic verification — The verifier resolves the issuer's public key by fetching https://www.idonce.com/.well-known/jwks.json (derived from the did:web identifier) and verifies the JWT signature. This happens on every presentation, not just once. Nobody can forge credentials for a domain they don't control — same trust model as HTTPS.

2. Issuer trust — The verifier checks whether the issuer is trusted. In eIDAS 2.0, this will be handled by EU Trusted Lists — official registries of recognized issuers maintained by member states. Until then, you maintain your own allowlist.

If you use idonce as your verifier (the /vp/sessions API), step 1 is done for you automatically. You only need to decide which issuers to trust. If you build your own verifier, both steps are your responsibility:

// Your server-side verification (step 2)
trustedIssuers := []string{"did:web:www.idonce.com"}

if !contains(trustedIssuers, credential.Issuer) {
    return errors.New("untrusted issuer")
}
// Step 1 (signature check via JWKS) must also be implemented
// — or use idonce's /vp/sessions API which handles both

Self-Hosting

idonce is fully open source. You can run the entire stack yourself — issuer, verifier, or both.

1. Run the Issuer

# Docker
docker run -p 8080:8080 -v issuer_data:/app/data gradient0/idonce-issuer

# Or build from source
git clone https://github.com/idonce/issuer
cd issuer && go run .

On first start, the issuer generates a unique ES256 signing key and persists it to data/server_key.pem. This key is your issuer's identity — back it up. If you lose it, all previously issued credentials become unverifiable.

2. Configure your domain

Set BASE_URL to your public domain. This determines your issuer's DID:

# Your issuer becomes did:web:verify.example.com
BASE_URL=https://verify.example.com go run .

The issuer automatically serves the correct did:web resolution endpoints:

  • /.well-known/did.json — DID Document with your public key
  • /.well-known/jwks.json — JWKS for signature verification
  • /.well-known/openid-credential-issuer — OpenID4VCI metadata

Your domain must be reachable via HTTPS for verifiers to resolve your DID. Use a reverse proxy like Caddy or nginx for TLS.

3. What changes

Credentials issued by your instance will have:

  • iss: "did:web:verify.example.com" — your DID, not idonce.com
  • Signed with your key — only your JWKS can verify them
  • Same credential format (SD-JWT-VC), same claims, same eIDAS 2.0 compatibility

4. What stays the same

  • The idonce mobile app accepts credentials from any issuer — users just scan your QR code
  • The OpenID4VCI flow is standard — any eIDAS wallet works, not just the idonce app
  • Credential format, claims, and verification logic are identical

5. Optional: Run the verifier too

git clone https://github.com/idonce/web
cd web
BASE_URL=https://verify.example.com go run .

This gives you the full /vp/sessions API for verifying credentials on your own infrastructure.

6. Tell verifiers to trust you

Verifiers who want to accept your credentials need to add your DID to their trust list:

trustedIssuers := []string{
    "did:web:www.idonce.com",          // official idonce
    "did:web:verify.example.com",      // your self-hosted instance
}

When eIDAS 2.0 Trusted Lists go live, self-hosted issuers can register there for automatic trust across the EU ecosystem.

Environment Variables

VariableDefaultDescription
BASE_URLhttp://localhost:8080Public URL (determines DID, JWKS, all credential links)
PORT8080Server port
ATTESTATION_STRICTfalseEnforce Apple/Google hardware attestation
ADMIN_API_KEY-Protect admin endpoints (stats, revocations)
SENDGRID_API_KEY-Email verification OTP (logs code if unset)
ALLOWED_ORIGIN*CORS origin

Ready to integrate?

No signup needed. Just start making API calls.