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
Create session, render QR, poll for result. Best for desktop websites where users verify with their phone.
Deep Link Flow
Open the deeplink URL directly. The idonce app handles verification and returns control to your app.
Pre-Registration
Require verification before account creation. Only real humans can register. Eliminates bot signups entirely.
On-Demand
Trigger verification at checkout, before voting, or when posting. Step-up authentication without passwords.
Use Cases
Forums & Comments
Verify before first post. No CAPTCHAs, no email confirmation.
biometricConfirmed: true and allow posting. Use subject to detect ban evasion across accounts.Fraud Prevention
Require verification at checkout for high-value orders.
HumanVerification + EmailVerification for verified email from a verified human.Voting & Polls
One person, one vote. Without collecting names or IDs.
subject hash is stable per device. Same human = same subject = duplicate vote detected.Bot Protection
Gate API access behind human verification. Stronger than rate limiting.
Email + Human
Know it's a real email from a real person. Without knowing who.
HumanVerification + EmailVerification. Verified email bound to a verified human.Verified Badges
Give users a "Verified Human" badge backed by real verification.
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.
| Field | Type | Description |
|---|---|---|
client_id | string | Your platform identifier (optional, defaults to "anonymous") |
requested_credentials | array | Credentials to request (optional, defaults to HumanVerification) |
GET /vp/sessions/{id}
Poll for verification result.
| Field | Type | Description |
|---|---|---|
status | string | pending, presented, or expired |
subject | string | Stable device hash (only when presented) |
disclosed_claims | object | Verified claims the user consented to share |
vct | string | Credential type URI |
verified_at | string | ISO 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
| Variable | Default | Description |
|---|---|---|
BASE_URL | http://localhost:8080 | Public URL (determines DID, JWKS, all credential links) |
PORT | 8080 | Server port |
ATTESTATION_STRICT | false | Enforce 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 |