Example flow — Low-fidelity event ticket issuance¶
Audience: Developers new to the EUDI Wallet Ecosystem who need to understand how low-assurance credential issuance works in practice.
Overview¶
This document walks through a complete low-fidelity credential issuance flow using the Pre-Authorized Code pattern, designed for credentials that don't require strong online identification — event tickets, membership cards, loyalty credentials, or other low-risk attestations.
What is low-fidelity issuance?¶
Low-fidelity (low-assurance) issuance is appropriate for credentials where:
- The risk of fraud or misuse is limited and acceptable
- Strong online identification is not required by the rulebook
- Identification may have happened prior to issuance (e.g., at purchase, registration, or in-person)
- The credential's purpose is convenience rather than legal or regulatory compliance
The Pre-Authorized Code flow simplifies the issuance process by eliminating the interactive authorization step, making it faster and more user-friendly for low-risk use cases.
When to use this flow¶
Use a low-fidelity Pre-Authorized Code flow when:
- Your credential has low risk if misused (limited financial, legal, or safety impact)
- Identification happened before issuance (e.g., ticket purchase, membership registration)
- You can deliver the pre-authorized code securely to the intended holder (e.g., via email, SMS, authenticated portal)
- Your rulebook does not require strong online identification at issuance time
- User experience is a priority (quick, frictionless issuance)
Examples: event tickets, conference badges, gym memberships, loyalty cards, promotional credentials, low-value vouchers.
Complete flow walkthrough¶
Use case example¶
Scenario: A concert ticket issued as a verifiable credential.
- User purchases a concert ticket online
- Payment is confirmed
- Ticket issuer generates a verifiable credential offer
- User receives a QR code or deep link via email
- User opens the link in their EUDI Wallet
- Wallet automatically retrieves the ticket credential
Prerequisites¶
Before issuance can begin, the issuer must publish two metadata documents that the wallet fetches automatically when it receives a credential offer.
Issuer metadata (OID4VCI §12.2) is served from a well-known URI. Per the spec, the .well-known path segment is inserted between the host and the issuer path — it is not appended. For https://tickets.festival.example, the URL is:
Minimal issuer metadata for this flow:
{
"credential_issuer": "https://tickets.festival.example",
"authorization_servers": ["https://tickets.festival.example"],
"credential_endpoint": "https://tickets.festival.example/credential",
"display": [
{ "name": "Festival Tickets GmbH", "locale": "de-DE" }
],
"credential_configurations_supported": {
"concert_ticket_sd_jwt": {
"format": "dc+sd-jwt",
"vct": "https://tickets.festival.example/concert-ticket/1.0",
"cryptographic_binding_methods_supported": ["jwk"],
"credential_signing_alg_values_supported": ["ES256"],
"proof_types_supported": {
"jwt": { "proof_signing_alg_values_supported": ["ES256"] }
},
"credential_metadata": {
"display": [
{
"name": "Konzertticket",
"locale": "de-DE",
"background_color": "#1a1a2e",
"text_color": "#FFFFFF"
}
]
}
}
}
}
authorization_servers points to the issuer's own URL because, in this pre-auth flow, the token endpoint is co-located with the credential issuer rather than on a separate authorization server. The wallet discovers the token endpoint from the authorization server metadata, which must be published separately at:
{
"issuer": "https://tickets.festival.example",
"token_endpoint": "https://tickets.festival.example/token",
"grant_types_supported": [
"urn:ietf:params:oauth:grant-type:pre-authorized_code"
]
}
See EAA Issuance §4.2 for the full issuer metadata reference. The concert_ticket_sd_jwt key in credential_configurations_supported is the identifier wallets place in credential_configuration_ids in the offer — they must match exactly.
Step 1: User completes a transaction or registration¶
The holder completes an action that makes them eligible for the credential. This could be purchasing a ticket, registering for an event, signing up for a membership, or completing a form.
The issuer's source system records the transaction (order ID, user email, ticket details) and has enough information to generate a credential (name, ticket type, event details, seat number, etc.) and a delivery channel to the holder.
Example:
Bestellbestätigung
Vielen Dank für Ihren Kauf!
Veranstaltung: Lollapalooza Berlin 2026
Datum: 12. September 2026
Ticketart: Tagesticket
Bestellnummer: ORD-123456
Ticket zum EUDI Wallet hinzufügen:
[Zum Wallet hinzufügen]
In this flow, identification is not performed during issuance. The issuer relies on the identification that happened during purchase (e.g., the user provided their email and payment details). The pre-authorized code is delivered to that same email, creating a reasonable assurance that the right person receives the credential.
Step 2: Pre-authorized code generation¶
The issuer generates a pre-authorized code bound to this specific transaction and credential.
A pre-authorized code must be:
- A one-time-use secret token with high entropy (minimum 128 bits; a 32-character base64url value gives ~192 bits)
- Short-lived — set expiry based on the use case (hours for urgent redemption, days or weeks for advance purchases)
- Bound to a specific credential and holder via an internal order or user ID, so it cannot be redeemed for a different credential
- Marked as used atomically upon successful token exchange to prevent replay; use database transactions to prevent race conditions
- Optionally protected with a user PIN (
tx_code) for an additional layer of security
Step 3: Credential offer generation and delivery¶
The issuer creates a credential offer (OID4VCI §4) containing the pre-authorized code and delivers it to the holder.
{
"credential_issuer": "https://tickets.festival.example",
"credential_configuration_ids": ["concert_ticket_sd_jwt"],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code": {
"pre-authorized_code": "q8fM2xK9pL3wN7vR5tY8jH4bV6nC1mZ0",
"tx_code": { "input_mode": "numeric", "length": 4, "description": "PIN sent to your email" }
}
}
}
credential_configuration_idsmust match a key in your issuer metadata'scredential_configurations_supportedexactlypre-authorized_codeis the one-time code for this issuancetx_codesignals that the wallet must prompt the user for a PIN before calling the token endpoint
To deliver this to the wallet, serialize the JSON offer, URL-encode it, and embed it in the openid-credential-offer:// URI scheme:
openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Ftickets.festival.example%22%2C...%7D
For same-device flows (user directly on their phone), render this as a deep link — the OS will open the wallet app:
For cross-device flows (wallet on phone, user browsing on desktop), render the URI as a QR code that the user scans with their wallet. To keep QR codes small and scannable, you can instead host the offer at a URL and reference it with credential_offer_uri:
openid-credential-offer://?credential_offer_uri=https%3A%2F%2Ftickets.festival.example%2Foffer%2Fabc123
The wallet fetches the offer JSON from that URL. This avoids embedding a large JSON blob in the QR code.
Example email delivery:
Betreff: Ihr Lollapalooza Berlin 2026 Ticket
Ihr Ticket ist bereit!
Ticket zum Wallet hinzufügen:
Option 1: QR-Code mit Ihrem EUDI Wallet scannen
[QR-Code-Bild]
Option 2: Auf Ihrem Mobilgerät auf diesen Link klicken
[Zum Wallet hinzufügen]
Ihr PIN: 8274
(Wird beim Hinzufügen des Tickets benötigt)
Dieser Link läuft ab am: 30. August 2026
The pre-authorized code is delivered via the same channel used during purchase (email, SMS, portal), creating a binding between the purchaser and the credential recipient. For low-fidelity credentials, the limited attack surface and bounded misuse impact make this acceptable, even though these channels are not highly secure.
Step 4: Wallet receives the credential offer¶
The holder scans the QR code or clicks the deep link, and the wallet receives the credential offer. The wallet parses the offer, fetches issuer metadata from the .well-known endpoint, confirms the offered credential types are supported and the issuer is trusted, and presents the offer to the holder for acceptance.
Neues Credential-Angebot
Von: Festival Tickets GmbH
Typ: Konzertticket
Veranstaltung: Lollapalooza Berlin 2026
Datum: 12. September 2026
[Akzeptieren] [Ablehnen]
If a PIN is required:
Enter PIN
The issuer requires a PIN to add this credential.
Check your email or purchase confirmation.
PIN: [____]
[Continue]
Step 5: Token request with pre-authorized code¶
The wallet exchanges the pre-authorized code (and optional PIN) for an access token at the issuer's token endpoint (OID4VCI §6, §3.5).
POST /token HTTP/1.1
Host: tickets.festival.example
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code&
pre-authorized_code=q8fM2xK9pL3wN7vR5tY8jH4bV6nC1mZ0&
tx_code=8274
The token endpoint:
- Validates the pre-authorized code: checks it exists, hasn't been used, hasn't expired, and that the PIN matches (if required)
- Marks the code as used to prevent replay
- Looks up the order/transaction associated with this code and loads the credential attributes
- Issues a short-lived access token bound to the credential configuration and order ID
(The c_nonce for the wallet's proof-of-possession is included in this token response — the wallet uses it in Step 6.)
def handle_preauth_token_request(code: str, tx_code: str = None):
# Validate code
preauth = get_preauth_code(code)
if not preauth:
return error("invalid_grant", "Code not found")
if preauth.used:
return error("invalid_grant", "Code already used")
if preauth.expires_at < time.time():
return error("invalid_grant", "Code expired")
if preauth.tx_code and preauth.tx_code != tx_code:
return error("invalid_grant", "Invalid transaction code")
# Mark as used
mark_code_as_used(code)
# Generate access token
access_token = create_access_token(
subject=preauth.order_id,
credential_config=preauth.credential_type,
audience="https://tickets.festival.example/credential",
expires_in=3600 # 1 hour
)
return {
"access_token": access_token,
"token_type": "Bearer",
"expires_in": 3600
}
Token response:
{
"access_token": "eyJhbGc...access_token_jwt",
"token_type": "Bearer",
"expires_in": 3600,
"c_nonce": "nonce_for_credential_proof_xyz789",
"c_nonce_expires_in": 3600
}
Low-fidelity credentials typically don't include a refresh token — the ticket is single-use or time-limited, so refresh is not needed. The access token is short-lived and the pre-authorized code is one-time use only.
Step 6: Credential request with proof-of-possession¶
Using the c_nonce received in the token response, the wallet constructs a proof-of-possession (OID4VCI Appendix F.1) and requests the credential from the credential endpoint (OID4VCI §8).
POST /credential HTTP/1.1
Host: tickets.festival.example
Authorization: Bearer eyJhbGc...access_token_jwt
Content-Type: application/json
{
"credential_configuration_id": "concert_ticket_sd_jwt",
"proof": {
"proof_type": "jwt",
"jwt": "eyJhbGc...credential_proof_jwt"
}
}
Proof-of-possession JWT (OID4VCI Appendix F.1)
The wallet creates a signed JWT proving it controls the private key that will be bound to the credential. Below is the decoded structure before signing:
// Proof JWT — header
{
"alg": "ES256",
"typ": "openid4vci-proof+jwt",
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
// Proof JWT — payload
{
"iss": "wallet-client-id",
"aud": "https://tickets.festival.example",
"iat": 1234567890,
"nonce": "nonce_for_credential_proof_xyz789"
}
The jwk in the header is the wallet's public key — this is what will be bound into the issued credential. The nonce from the token response ties this proof to this specific issuance request and prevents replay.
Step 7: Credential issuance¶
The issuer validates the request, retrieves ticket data, constructs the credential, signs it, and returns it to the wallet.
- Validate the access token: verify signature and issuer, check audience (should be the credential endpoint), check expiry, and extract the order ID
- Validate the credential proof: verify the JWT signature, check that the nonce matches the
c_nonceissued in the token response and is unused and unexpired, verify the audience matches the issuer, and extract the public key from thejwkclaim - Retrieve credential attributes from the order database using the order ID in the access token
- Construct and sign the credential using the correct schema, the holder's public key from the proof, and the issuer's registered signing key
// Header
{
"alg": "ES256",
"typ": "vc+sd-jwt",
"kid": "issuer-signing-key-1"
}
// Payload
{
"vct": "https://tickets.festival.example/concert-ticket/1.0",
"iss": "https://tickets.festival.example",
"iat": 1234567890,
"exp": 1720915200,
"cnf": {
"jwk": { /* holder's public key from proof */ }
},
"event_name": "Lollapalooza Berlin 2026",
"event_date": "2026-09-12",
"event_location": "Olympiagelände, Berlin",
"ticket_type": "Tagesticket",
"order_id": "ORD-123456",
"holder_name": "Anna Müller",
"holder_email": "anna.mueller@beispiel.de",
"barcode_data": "LOLLA2026-TAG-123456-MUELLERA"
}
Step 8: Wallet stores and displays the credential¶
The wallet receives the credential, verifies the issuer signature, checks expiry, confirms the public key matches the wallet's key, and stores it.
✓ Ticket hinzugefügt
Lollapalooza Berlin 2026
Festival Tickets GmbH
Datum: 12. September 2026
Ticketart: Tagesticket
Ort: Olympiagelände, Berlin
Gültig bis: 13. September 2026
[Details anzeigen] [Ticket vorzeigen]
Security considerations for low-fidelity flows¶
Pre-authorized code security¶
Use a cryptographically secure random number generator with at least 128 bits of entropy (32 characters base64url gives ~192 bits). Never use predictable patterns or sequential codes.
Set expiry based on the use case — hours for urgent redemption, days or weeks for advance ticket purchases. Clean up expired codes from the database. Mark codes as used immediately upon successful token exchange, using database transactions to prevent race conditions under concurrent requests. Log all redemption attempts, successful and failed.
User PIN protection¶
Consider adding a PIN (tx_code) when the delivery channel is less secure (email, SMS), when additional friction is acceptable, or for higher-value low-fidelity credentials such as valuable vouchers.
Use 4–8 digit numeric PINs for usability, generated randomly. Where possible, deliver the PIN via a different channel from the QR code (e.g., PIN via SMS, QR via email). Implement rate limiting on PIN attempts (3–5 attempts before lockout).
Delivery channel security¶
Email: use TLS for transport, include expiry information, and warn users not to forward the email.
SMS: be aware of SIM-swapping risk; consider using SMS only for PIN delivery, not the full credential offer.
Authenticated portal: delivering via a logged-in portal requires the user to authenticate before retrieving the offer, which provides stronger binding than email or SMS.
Rate limiting and abuse prevention¶
Limit token endpoint requests per IP address (e.g., 10 attempts per hour) and per pre-authorized code (e.g., 5 attempts total). Monitor for unusual redemption patterns such as bulk redemptions or high failure rates that may indicate brute-force attempts. Log all token requests with timestamps, IP addresses, and user agents.
Fraud detection¶
Watch for a single user redeeming many codes in a short time (possible account compromise), codes redeemed from unusual locations, or a high volume of failed PIN attempts. Responses can include temporary code lockout, requiring additional verification, or notifying the legitimate holder of suspicious activity.
When NOT to use low-fidelity flows¶
Pre-authorized code flows are not appropriate when:
- High assurance is required: legal, regulatory, or high-value credentials
- Strong identification must happen at issuance time: rulebook mandates PID or equivalent
- Credential refresh is needed: pre-auth codes don't naturally support refresh
- Risk of misuse is high: financial credentials, health credentials, identity credentials
- Accountability is critical: strong non-repudiation of issuance is required
For these cases, use the Authorization Code flow with HAIP instead.
Comparison: Pre-Authorized Code vs Authorization Code¶
| Aspect | Pre-Authorized Code | Authorization Code |
|---|---|---|
| User experience | Fast, minimal interaction | Requires redirect and authentication |
| Identification | Prior or not required | At issuance time |
| Security | Lower (relies on channel security) | Higher (interactive authentication) |
| Complexity | Simple to implement | More complex (authorization server) |
| Use case | Low-risk credentials | High-assurance credentials |
| Refresh support | Not typically supported | Native refresh token support |
| Best for | Tickets, memberships, vouchers | Licenses, health, legal credentials |
Implementation checklist¶
Before you start¶
- Rulebook permits low-fidelity issuance for your credential type
- Identified appropriate use case (low risk, prior identification acceptable)
- Chosen delivery channel (QR code, email, SMS, portal)
- Decided whether to require user PIN
Issuer metadata¶
- Issuer metadata published at
/.well-known/openid-credential-issuer(or path-inserted form per §12.2) -
credential_configurations_supportedkey matches thecredential_configuration_idsin your offer - AS metadata published at
/.well-known/oauth-authorization-serverwithtoken_endpointdeclared
Pre-authorized code generation¶
- Use cryptographically secure random number generator
- Generate codes with sufficient entropy (min 128 bits)
- Bind codes to specific orders/transactions
- Set appropriate expiry times
- Store codes securely in database
Credential offer delivery¶
- Implement chosen delivery method (QR code, email, SMS, etc.)
- Include user-friendly instructions
- Include PIN (if required) via appropriate channel
- Include expiry information
- Test delivery reliability
Token endpoint¶
- Validate pre-authorized code (existence, expiry, not-used)
- Validate user PIN (if required)
- Mark code as used immediately after validation
- Generate short-lived access token, including
c_nonceandc_nonce_expires_inin the response - Implement rate limiting (per-IP, per-code)
- Log all requests (successful and failed)
Credential endpoint¶
- Validate access token (signature, audience, expiry)
- Validate proof-of-possession (signature, nonce, audience)
- Retrieve credential attributes from source systems
- Construct credential with correct schema
- Include holder's public key (cnf claim)
- Sign credential with registered issuer key
- Set appropriate expiry date
Security measures¶
- Implement rate limiting on token endpoint
- Monitor for suspicious redemption patterns
- Log all issuance events with sufficient detail
- Implement PIN retry limits (if using PINs)
- Clean up expired codes from database
- Set up alerts for unusual activity
Testing¶
- Test successful issuance flow end-to-end
- Verify wallet fetches both metadata documents correctly before the flow starts
- Test pre-authorized code expiry handling
- Test pre-authorized code replay prevention
- Test incorrect PIN handling (if applicable)
- Test rate limiting enforcement
- Test delivery channel reliability (deep link and QR code)
Next steps¶
After implementing a low-fidelity Pre-Authorized Code flow:
- Test end-to-end: test the complete flow from purchase to credential delivery, starting with verifying the wallet can resolve your metadata
- Security testing: test rate limiting, code expiry, PIN validation, replay prevention
- User testing: test with real users to identify friction points in delivery and redemption
- Monitor and optimize: track redemption rates and optimize expiry windows
- Support readiness: train support team and document common issues
Further reading¶
- EAA Issuance Implementation Guide - Comprehensive implementation guidance
- High-Assurance HAIP Flow - For credentials requiring strong identification
- OpenID4VCI Specification - Official protocol specification