XMiete Developer Documentation
The XMiete Core API (schema v2.0.0) manages the complete lifecycle of a rental
deposit across European jurisdictions — from tenant identity verification and legal pledge
to QEAA credential issuance, partial release, settlement negotiation, and final closure.
Jurisdiction-specific rules (deposit caps, return deadlines, interest obligations, escrow
requirements, and ADR bodies) are driven by the meta.jurisdiction field.
Base URL
Supported jurisdictions
Pass an ISO 3166-1 alpha-2 code in meta.jurisdiction.
For countries with regional rules, also supply meta.jurisdiction_sub
(ISO 3166-2 subdivision code).
| Code | Country | Subdivision required | Notes |
|---|---|---|---|
| DE | Germany | No | BGB § 551; up to 3 cold months; TREUHANDKONTO |
| AT | Austria | No | MRG; up to 3 months; interest required |
| CH | Switzerland | No | Art. 257e OR; MIETKAUTIONSKONTO (blocked account); up to 3 months |
| FR | France | No | Loi ALUR; 1 month unfurnished, 2 furnished; LANDLORD_HELD; no interest |
| GB | United Kingdom | Yes (GB-ENG, GB-SCT, GB-NIR, GB-WLS) | Housing Act 2004; mandatory protection scheme; caps vary by region |
| NL | Netherlands | No | Woningwet; typically 1–2 months; Huurcommissie ADR |
| BE | Belgium | No | e-DEPO mandatory blocked escrow; 2 months (private) or 3 months (commercial) |
| ES | Spain | Yes (e.g. ES-AN, ES-MD) | LAU Art. 36; 1 month; fianza held by regional authority (INCASOL etc.) |
| IT | Italy | No | Codice Civile Art. 1590; up to 3 months; LANDLORD_HELD |
| IE | Ireland | No | RTB registration; protection scheme pending 2025 Bill |
| NO | Norway | No | Husleieloven §3-5; MIETKAUTIONSKONTO; Husleietvistutvalget ADR |
| SE | Sweden | No | Jordabalken; typically 1–3 months; Hyresnämnden ADR |
| PL | Poland | No | No statutory cap; PESEL tenant ID; civil courts |
Supported SDK languages
| Language | Min version | Dependencies |
|---|---|---|
| Go | 1.21+ | Standard library only |
| Rust | 2021 edition | tokio, reqwest, serde |
| Java | 17+ | Standard library only (java.net.http) |
| Python | Coming soon | |
| TypeScript | Coming soon | |
Authentication
All requests require an OAuth2 Bearer token in the
Authorization header. The auth module validates
tokens against your OIDC provider's JWKS endpoint and checks required scopes
in a single call.
Required scopes
| Endpoint | Scope |
|---|---|
POST/deposits | deposit:create |
GET/deposits/{id} | deposit:read |
PATCH/deposits/{id}/identity | deposit:write |
POST/deposits/{id}/pledge | deposit:pledge |
POST/deposits/{id}/release | deposit:release |
POST/deposits/{id}/claim | deposit:claim |
POST/deposits/{id}/settle | deposit:settle |
Token validation
func handleRequest(w http.ResponseWriter, r *http.Request) {
validator := auth.NewOidcTokenValidator(
"https://auth.xmiete.example/.well-known/openid-configuration",
)
claims, err := validator.ValidateToken(r.Header.Get("Authorization"), "deposit:create")
if err != nil {
if auth.IsInsufficientScopeError(err) {
http.Error(w, "forbidden", http.StatusForbidden)
} else {
http.Error(w, "unauthorized", http.StatusUnauthorized)
}
return
}
log.Printf("authenticated: sub=%s expires=%s", claims.Subject, claims.ExpiresAt)
// ... process the request
}fn validate_bearer_token(header: &str) -> Result<(), AuthError> {
let validator = OidcTokenValidator::new(
"https://auth.xmiete.example/.well-known/openid-configuration",
);
let claims = validator.validate_token(header, &["deposit:create"])?;
println!("authenticated: sub={} expires={}", claims.subject, claims.expires_at);
Ok(())
}var validator = new OidcTokenValidator(
"https://auth.xmiete.example/.well-known/openid-configuration"
);
// In production, pass request.getHeader("Authorization").
String incomingAuthHeader = "Bearer <token>";
try {
TokenClaims claims = validator.validateToken(
incomingAuthHeader, "deposit:read", "deposit:create"
);
System.out.println("Token valid — sub: " + claims.subject()
+ ", scopes: " + claims.scopes());
} catch (OidcTokenValidator.TokenValidationException e) {
// HTTP 401 — malformed JWT, expired, or wrong issuer
System.err.println("Token rejected: " + e.getMessage());
return;
} catch (OidcTokenValidator.InsufficientScopeException e) {
// HTTP 403 — valid token, scope not granted
System.err.println("Access denied: " + e.getMessage());
return;
}Deposit Lifecycle
Each deposit moves through a deterministic state machine. Illegal transitions
return 409 Conflict. The full set of states covers standard release,
partial release with utility reservation, structured settlement, and dispute escalation.
From ACTIVE:
→ RELEASED → CLOSED
→ PARTIALLY_RELEASED → RELEASED → CLOSED
→ SETTLE_PROPOSED → RELEASED or DISPUTED → CLOSED
→ CLAIMED → CLOSED
| State | Trigger | Notes |
|---|---|---|
| REQUESTED | POST/deposits | Deposit object created |
| IDENTIFIED | PATCH/deposits/{id}/identity with VERIFIED | eID / EUDI Wallet flow complete |
| FUNDED | Bank or scheme confirms receipt of funds | External event; scheme registration triggered for GB/BE |
| PLEDGED | POST/deposits/{id}/pledge | Legal pledge confirmed per jurisdiction (see pledge.statutory_basis) |
| ACTIVE | Lease begins | QEAA DepositPledgeAttestation issued to tenant EUDI Wallet |
| PARTIALLY_RELEASED | POST/deposits/{id}/release with partial amount | Utility reservation held back; see utility_reservation object |
| SETTLE_PROPOSED | POST/deposits/{id}/settle | Itemized claim submitted; counter-party has response_deadline to respond |
| DISPUTED | Settlement rejected; escalated to ADR or court | ADR body set in settlement.escalation_type / specialist_tribunal_id |
| RELEASED | POST/deposits/{id}/release (full) | Landlord authorises full release; return deadline driven by jurisdiction |
| CLAIMED | POST/deposits/{id}/claim | Landlord initiates unilateral claim |
| CLOSED | Final state after full resolution | Agreed amounts set in settlement.agreed_tenant_refund / agreed_landlord_retention |
deposit.history[]
with a timestamp, actor, and optional JWS signature for non-repudiation of critical transitions
(pledge, release, claim).
SDK Quick Start
Go
Requires Go 1.21+. Uses only the standard library — no external dependencies.
go get github.com/xmiete/xmiete-go-sdk
# Import the sub-packages you need:
# github.com/xmiete/xmiete-go-sdk/auth
# github.com/xmiete/xmiete-go-sdk/eid
# github.com/xmiete/xmiete-go-sdk/openid4vpRust
Requires Rust 2021 edition. Add to Cargo.toml:
[dependencies]
xmiete-sdk = { path = "sdk-examples/rust" }
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"Java
Requires Java 17+. No external dependencies beyond the Java standard library.
# Build a JAR from the sdk-examples/java directory:
mvn package -f sdk-examples/java/pom.xml
# Or copy the src/ tree directly into your project.Minimal deposit request (schema v2.0.0)
Set meta.jurisdiction to the ISO 3166-1 alpha-2 country code.
The API validates the deposit amount against the jurisdiction-specific cap and
selects the correct statutory basis for the pledge.
{
"meta": {
"version": "2.0.0",
"timestamp": "2026-05-09T10:00:00Z",
"jurisdiction": "GB",
"jurisdiction_sub": "GB-ENG"
},
"tenant": {
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com"
},
"landlord": {
"name": "Acme Lettings Ltd",
"type": "COMMERCIAL",
"iban": "GB29NWBK60161331926819"
},
"property": {
"address": {
"street": "10 Downing Street",
"zip": "SW1A 2AA",
"city": "London",
"country": "GB"
}
},
"deposit": {
"amount": 1800.00,
"currency": "GBP",
"type": "CASH_EQUIVALENT",
"lifecycle_state": "REQUESTED"
}
}Jurisdiction Configuration
The meta object drives all jurisdiction-specific behaviour.
The API uses jurisdiction (and jurisdiction_sub where applicable)
to enforce deposit caps, return deadlines, interest obligations, and required escrow types.
meta object
| Field | Type | Required | Description |
|---|---|---|---|
version | string | Yes | Must be "2.0.0" |
timestamp | date-time | Yes | ISO 8601 creation timestamp |
jurisdiction | string | Yes | ISO 3166-1 alpha-2 country code (e.g. DE, GB, FR) |
jurisdiction_sub | string | Conditional | ISO 3166-2 code — required for GB (region caps differ) and ES (regional fianza authority) |
external_id | string | No | Caller-assigned reference |
Deposit cap validation
The API rejects POST/deposits when
deposit.amount exceeds the statutory cap for the jurisdiction.
Cap reference amounts depend on the rent type set in the lease object:
| Jurisdiction | Statutory cap | Rent reference | Statutory basis |
|---|---|---|---|
| DE | 3 months | lease.monthly_cold_rent | BGB § 551 |
| AT | 3 months | lease.monthly_cold_rent | MRG § 16b |
| CH | 3 months | lease.monthly_cold_rent | OR Art. 257e |
| FR | 1 month (unfurnished) / 2 months (furnished) | lease.monthly_cold_rent | Loi ALUR |
| GB (ENG/WLS) | 5 weeks (≤£50k/yr) or 6 weeks | lease.monthly_warm_rent × 12/52 | Tenant Fees Act 2019 |
| GB (SCT) | 2 months | lease.monthly_warm_rent | Private Housing (Tenancies) (Scotland) Act 2016 |
| NL | No statutory cap (typically 1–2 months) | — | — |
| BE | 2 months (private) / 3 months (commercial) | lease.monthly_cold_rent | Woninghuurwet |
| ES | 1 month (legal fianza) + optional contractual surety | lease.monthly_cold_rent | LAU Art. 36 |
| IT | 3 months | lease.monthly_cold_rent | Codice Civile Art. 1590 |
| IE | No statutory cap (typically 1 month) | — | — |
| NO | 6 months | lease.monthly_cold_rent | Husleieloven §3-5 |
Return deadlines
deposit.return_deadline is computed from settlement.handover_date
plus the jurisdiction-specific window. The API sets this automatically when the
deposit transitions to ACTIVE.
| Jurisdiction | Return window |
|---|---|
| DE | 3–6 months (practice; BGB does not specify exact deadline) |
| FR | 1 month (no damage) / 2 months (with deductions) |
| GB | 10 days from agreement or ADR decision |
| NL | 14 days (no damage) / 30 days (with deductions) |
| BE | Released by e-DEPO within 14 days of agreement |
| ES | 30 days from key handover |
| CZ | 30 days |
| LV | 10 days |
Tax identifier types
tenant.tax_id_type tells the API which format to expect in tenant.tax_id:
| Value | Country | Format |
|---|---|---|
STEUER_ID | DE | 11-digit Steueridentifikationsnummer |
NIR | FR | 15-digit Numéro de Sécurité Sociale |
CODICE_FISCALE | IT | 16-char alphanumeric |
NIF | ES | 9-char (nationals) |
NIE | ES | 9-char (foreign residents) |
BSN | NL | 9-digit Burgerservicenummer |
NI_NUMBER | GB | National Insurance number |
PESEL | PL | 11-digit national ID |
OTHER | — | Free-form; document in comment |
Trusteeship & Escrow
The trusteeship object describes how and where the deposit funds are held.
The account_type value is jurisdiction-specific and determines interest
obligations, insolvency protection requirements, and tenant withdrawal rights.
Account types
| Value | Countries | Description |
|---|---|---|
TREUHANDKONTO | DE AT | Segregated landlord-held trust account; insolvency-proof separation required |
ANDERKONTO | DE | Restricted to licensed professionals (lawyers, notaries); highest segregation standard |
POOLED_TREUHAND | DE | Multiple deposits in one designated account with per-tenant sub-accounting |
MIETKAUTIONSKONTO | CH NO | Blocked account in tenant's name — landlord has no access without tenant consent or court order |
CUSTODIAL_SCHEME | GB | Government-approved scheme holds funds directly (TDS, DPS, SafeDeposits Scotland) |
INSURED_SCHEME | GB | Landlord holds funds; insured via government-approved scheme (MyDeposits, TDS Insured) |
EDEPO | BE | Mandatory Belgian e-DEPO system; funds cannot be paid directly to landlord |
FIANZA_REGIONAL | ES | Legal fianza held by regional authority (INCASOL, IVIMA, etc.) |
STATE_GUARANTEE | LU | State-backed guarantee for low-income tenants |
DEPOSIT_INSURANCE | CH DE | Annual-premium insurance product (SwissCaution, goCaution, getmomo) |
LANDLORD_HELD | FR NL IT CEE | Landlord holds funds without statutory escrow requirement |
Interest obligations
Set trusteeship.interest_required based on the jurisdiction.
When true, supply trusteeship.interest_rate and keep
trusteeship.interest_rate_history[] up to date so tenants can independently
verify accrued interest.
| Jurisdiction | Interest required | Notes |
|---|---|---|
| DE AT | Yes | Landlord must pass bank interest to tenant |
| CH | Yes | Interest accrues on MIETKAUTIONSKONTO in tenant's favour |
| CZ HU IT | Yes | Statutory interest obligation |
| FR NL ES IE | No | No statutory interest obligation |
| GB | No (custodial schemes may pay interest) | Depends on scheme; not mandated by statute |
Utility reservation (partial release)
When a tenancy ends with annual utility reconciliation pending — primarily
DE and AT (Betriebskosten) —
use the utility_reservation object alongside the
PARTIALLY_RELEASED state.
{
"utility_reservation": {
"released_amount": 1500.00,
"reserved_amount": 300.00,
"monthly_advance": 150.00,
"billing_period_end": "2026-12-31",
"resolution_deadline": "2027-12-31"
}
}resolution_deadline defaults to billing_period_end + 12 months
for DE (BGB § 556 Abs. 3). After this date the reservation
must be released regardless of whether the utility statement has arrived.
Deposit Protection Schemes
The protection_scheme object is required wherever scheme registration is
mandatory or carries statutory consequences. Currently: GB
(all regions) and BE. Ireland (IE)
will require it when the Protection of Tenants' Deposits Bill 2025 is enacted.
Approved schemes
| Value | Region | Type | Notes |
|---|---|---|---|
TDS | GB England & Wales | Custodial | Tenancy Deposit Scheme |
TDS_INSURED | GB England & Wales | Insured | Landlord holds funds; TDS insures |
DPS | GB England & Wales | Custodial | Deposit Protection Service |
DPS_INSURED | GB England & Wales | Insured | |
MYDEPOSITS | GB England & Wales | Insured | |
SAFEDEPOSITS_SCOTLAND | GB Scotland | Custodial only | Scotland permits custodial schemes only |
LPS_SCOTLAND | GB Scotland | Custodial only | Letting Protection Service Scotland |
TDSNORTHERNIRELAND | GB Northern Ireland | Custodial & Insured | 28-day registration window |
EDEPO_BE | BE | Custodial (mandatory) | Belgian electronic escrow system |
RTB_IE | IE | Custodial (pending) | Residential Tenancies Board — awaiting legislation |
OTHER | — | — | Supply free-text name in scheme_name_other |
Protection scheme object
{
"protection_scheme": {
"scheme_name": "DPS",
"registration_number": "DPS-2026-123456789",
"registration_date": "2026-05-09",
"registration_deadline": "2026-06-08",
"protection_type": "CUSTODIAL",
"statutory_basis": "Housing Act 2004 s.213"
}
}Settlement & Dispute Resolution
At tenancy end either party can propose an itemised split of the deposit via
POST/deposits/{id}/settle.
The deposit enters SETTLE_PROPOSED state.
The counter-party has until settlement.response_deadline to accept or
counter-propose. Unresolved proposals escalate to DISPUTED.
settlement object
| Field | Description |
|---|---|
initiated_by | LANDLORD or TENANT |
handover_date | Date keys were returned; starts return-deadline clock |
handover_protocol_ref | Reference to signed check-out report (Übergabeprotokoll, état des lieux) |
claim_items[] | Itemized deductions — see categories below |
proposed_tenant_refund | Amount proposed to be returned to tenant |
proposed_landlord_retention | Amount proposed to be retained by landlord |
response_deadline | Counter-party must respond by this date (14 days typical) |
escalation_type | ADR route if not agreed — see escalation table below |
agreed_tenant_refund | Set on CLOSED — final refund amount |
agreed_landlord_retention | Set on CLOSED — final retention amount |
Claim item categories
| Category | Enforceability notes |
|---|---|
RENT_ARREARS | Universally enforceable with evidence |
UTILITY_ARREARS | Universally enforceable with evidence |
DAMAGE | Enforceable; must distinguish from fair wear and tear |
CLEANING | Enforceable if property was not cleaned to move-in standard |
COSMETIC_REPAIRS | Frequently unenforceable in DE (BGH Schönheitsreparaturen); generally valid in GB and FR if evidenced |
UNAUTHORIZED_ALTERATIONS | Enforceable where alterations were not consented |
MISSING_ITEMS | Requires inventory reference in evidence_refs[] |
OTHER | Free-form; document clearly in description |
ADR escalation
| Value | Countries | Body |
|---|---|---|
PLATFORM_MEDIATION | All | XMiete's own mediation service |
SPECIALIST_TRIBUNAL | GB NL NO IE DK SE FI | Government-mandated ADR body (set specialist_tribunal_id) |
CIVIL_COURT | FR ES IT PT GR CEE | General civil jurisdiction |
REGIONAL_AUTHORITY | ES | Regional authority holding the fianza |
specialist_tribunal_id values
| Value | Body | Country |
|---|---|---|
TDS / DPS / MYDEPOSITS | Scheme ADR panels | GB England & Wales |
SAFEDEPOSITS_SCOTLAND | SafeDeposits Scotland ADR | GB Scotland |
TDSNORTHERNIRELAND | TDS Northern Ireland ADR | GB NI |
HUURCOMMISSIE | Huurcommissie | NL |
HUSLEIETVISTUTVALGET | Husleietvistutvalget | NO |
RTB | Residential Tenancies Board | IE |
HUSLEJENAEVN | Huslejenævn | DK |
HYRESNAMNDEN | Hyresnämnden | SE |
KULUTTAJARIITALAUTAKUNTA | Consumer Disputes Board | FI |
eID Verification
Tenant identity verification supports multiple methods depending on jurisdiction.
All SDKs expose a pluggable interface — swap providers without changing application code.
The verification outcome is stored in tenant.eid_status and triggers the
IDENTIFIED transition when set to VERIFIED.
Supported verification methods
| Method | Jurisdictions | Standard |
|---|---|---|
| EUDI Wallet (OpenID4VP) | All EU member states | eIDAS 2.0 / EUDI ARF; vc+sd-jwt or mso_mdoc |
| BSI TR-03130 (Online-Ausweisfunktion) | DE | Chip-based, AusweisApp2 or third-party SDK |
| Generic OIDC eID | DE AT BE and others | OpenID Connect front-end for national eID |
| GOV.UK Verify / One Login | GB | OIDC + LOA 2 |
| FranceConnect | FR | OIDC + Substantial assurance |
| iDIN (iDEAL-based) | NL | Bank-based identity; not eIDAS |
| itsme | BE NL | Mobile ID, eIDAS Substantial |
Flow
AuthorizationURL2. Redirect tenant's browser (or present QR for EUDI Wallet) to
AuthorizationURL3. Provider POSTs signed webhook to your endpoint
4. WebhookHandler validates HMAC and calls
PATCH /deposits/{id}/identity5. Verified claims stored in
tenant.wallet_metadata (EUDI path) or tenant.eid_status
Initiate a session
func initiateEIDVerification(ctx context.Context, depositID, tenantEmail string) (string, error) {
service := eid.NewVerificationService(
"https://eid-provider.example.com", // e.g., Authada, SkIDentity
"https://api.xmiete.org/v1",
)
session, err := service.InitiateVerification(ctx, eid.VerificationRequest{
DepositID: depositID,
TenantEmail: tenantEmail,
RedirectURI: "https://yourapp.example.com/eid-callback",
ClientID: "xmiete-fintech-client",
})
if err != nil {
return "", fmt.Errorf("eid: initiate session: %w", err)
}
// Redirect the tenant's browser to session.AuthorizationURL.
// The provider POSTs the result to your /webhook/eid endpoint.
return session.AuthorizationURL, nil
}async fn initiate_eid_verification(deposit_id: &str, tenant_email: &str) -> String {
let service = EidVerificationService::new(
"https://eid-provider.example.com", // e.g., Authada, SkIDentity
"https://api.xmiete.org/v1",
);
let session = service
.initiate_verification(&VerificationRequest {
deposit_id: deposit_id.to_string(),
tenant_email: tenant_email.to_string(),
redirect_uri: "https://yourapp.example.com/eid-callback".to_string(),
client_id: "xmiete-fintech-client".to_string(),
})
.await
.expect("initiate eID session");
// Redirect the tenant's browser to session.authorization_url.
// The provider POSTs the result to your /webhook/eid endpoint.
session.authorization_url
}var eidService = new EidVerificationService(
"https://eid-provider.example", // e.g., Authada, SkIDentity
"https://api.xmiete.org/v1"
);
var verificationRequest = new EidVerificationRequest(
"DEP-123",
"max.mustermann@example.de",
"https://app.xmiete.example/eid-callback",
"xmiete-fintech-client"
);
EidVerificationSession session = eidService
.initiateVerification(verificationRequest)
.exceptionally(ex -> new EidVerificationSession(
"SESSION-STUB-456",
"https://eid-provider.example/authorize?session_id=SESSION-STUB-456",
OffsetDateTime.now().plusMinutes(15)
))
.join();
System.out.println("Redirect tenant to: " + session.authorizationUrl());
// The eID provider will POST the result to your /webhook/eid endpoint.Custom provider adapter
All SDKs define the identity verifier as an interface / trait. Supply your own adapter to integrate any compatible provider.
type MyProviderAdapter struct{ /* provider-specific config */ }
func (a *MyProviderAdapter) InitiateVerification(ctx context.Context, req eid.VerificationRequest) (*eid.VerificationSession, error) {
// call your eID SDK or internal REST service
}
func (a *MyProviderAdapter) UpdateDepositKYCStatus(ctx context.Context, depositID string, payload eid.KYCUpdatePayload, bearerToken string) error {
// push result to XMiete API
}
handler := eid.NewWebhookHandler(&MyProviderAdapter{}, bearerToken, nil)provider_reference is stored by the
XMiete API — raw PII from the eID chip or EUDI Wallet is never forwarded or persisted.
Credential assurance level is stored in tenant.wallet_metadata.assurance_level.
Webhook Handling
eID provider webhooks are authenticated with HMAC-SHA256.
The hex digest is sent in the X-Signature header.
All SDKs verify with a constant-time comparison to prevent timing attacks.
Event payload
{
"session_id": "SESSION-XYZ-456",
"deposit_id": "DEP-123",
"status": "VERIFIED",
"provider_reference": "EID-AUTHADA-789",
"completed_at": "2026-05-09T14:23:00Z",
"error_code": null
}Register the handler
func registerEIDWebhook(service eid.IdentityVerifier, bearerToken, webhookSecret string) {
handler := eid.NewWebhookHandler(service, bearerToken, func(event eid.WebhookEvent) {
log.Printf("eID done: deposit=%s status=%s", event.DepositID, event.Status)
})
http.HandleFunc("/webhook/eid", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if err := handler.HandleWebhook(body, r.Header.Get("X-Signature"), webhookSecret); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
})
}fn build_webhook_handler(
service: Arc<dyn EidVerifier>,
bearer_token: &str,
) -> WebhookHandler {
WebhookHandler::new(
service,
bearer_token.to_string(),
Some(Box::new(|event| {
println!("eID done: deposit={} status={:?}", event.deposit_id, event.status);
})),
)
}
// In your Axum / Actix handler:
// let sig = headers["X-Signature"].to_str().unwrap_or("");
// handler.handle_webhook(&body, sig, &webhook_secret).await?;String webhookSecret = System.getenv().getOrDefault("EID_WEBHOOK_SECRET", "dev-secret-only");
var webhookHandler = new EidWebhookHandler(
eidService,
"Bearer <xmiete-service-token>",
event -> System.out.println("eID done — status: " + event.status()
+ ", providerRef: " + event.providerReference())
);
// In your servlet / Spring @PostMapping:
// byte[] body = request.getInputStream().readAllBytes();
// String sig = request.getHeader("X-Signature");
// webhookHandler.handleWebhook(body, sig, webhookSecret);Status values
| Status | Meaning | SDK action |
|---|---|---|
VERIFIED | eID check passed | Calls PATCH /identity with VERIFIED |
FAILED | Identity mismatch or user abort | Logs; invokes onComplete callback |
EXPIRED | Session timeout | Logs; invokes onComplete callback |
PENDING | Verification in progress | No action |
OpenID4VP — EUDI Wallet Presentation
Landlords and property managers can request that tenants present their
DepositPledgeAttestation QEAA directly from an eIDAS 2.0 EUDI Wallet
(SD-JWT VC format, vc+sd-jwt). The SDK handles the full
OpenID4VP request / response / verification loop.
This flow is available across all supported EU jurisdictions.
Flow
VpRequest JSON to wallet (QR or deep-link)2. Wallet presents → wallet POSTs
vp_token to your response_uri3. VerifyVpToken → verify SD-JWT VP, KB-JWT, disclosures →
VerifiedClaims
Request and verify
func requestWalletPresentation(ctx context.Context, depositID, responseURI string) error {
verifier := openid4vp.NewVpVerifierService(
"https://verifier.yourapp.example.com",
"https://auth.example.com/.well-known/jwks.json",
)
// Step 1 — build the VP request and deliver it to the wallet (QR or deep-link).
result, err := verifier.BuildVpRequest(ctx, depositID, responseURI)
if err != nil {
return fmt.Errorf("openid4vp: build request: %w", err)
}
// Persist result.Nonce in your session store — required for step 3.
// Serialize result.VpRequest as JSON and embed in QR code or deep-link.
// Step 3 — wallet POSTs vp_token to responseURI; verify it here.
claims, err := verifier.VerifyVpToken(ctx, "<vp_token from wallet>", result.Nonce, responseURI)
if err != nil {
return fmt.Errorf("openid4vp: invalid presentation: %w", err)
}
log.Printf("verified: deposit=%s pledged=%s bank=%s",
claims.DepositID, claims.PledgeDate, claims.IssuingBank)
return nil
}async fn request_wallet_presentation(
deposit_id: &str,
response_uri: &str,
) -> Result<(), VpError> {
let verifier = VpVerifierService::new(
"https://verifier.yourapp.example.com".to_string(),
"https://auth.example.com/.well-known/jwks.json".to_string(),
);
// Step 1 — build the VP request; deliver it to the wallet via QR or deep-link.
let (nonce, _vp_request) = verifier.build_vp_request(deposit_id, response_uri).await?;
// Persist nonce in your session store — required for step 3.
// Serialize _vp_request as JSON and embed in QR code or deep-link.
// Step 3 — wallet POSTs vp_token to response_uri; verify it here.
let claims = verifier
.verify_vp_token("<vp_token from wallet>", &nonce, response_uri)
.await?;
println!(
"verified: deposit={} pledged={} bank={}",
claims.deposit_id, claims.pledge_date, claims.issuing_bank
);
Ok(())
}var vpVerifier = new OpenId4VpService(
"https://verifier.yourapp.example.com",
"https://auth.example.com/.well-known/jwks.json"
);
// Step 1 — build a VP request and deliver it to the wallet (QR or deep-link).
String responseUri = "https://yourapp.example.com/vp-response";
VpRequestResult req = vpVerifier.buildVpRequest("DEP-123", responseUri).join();
String nonce = req.nonce();
// Persist nonce; serialize req.vpRequest() as JSON and embed in QR code.
// Step 3 — wallet POSTs vp_token to responseUri; verify it here.
VerifiedClaims vp = vpVerifier.verifyVpToken("<vp_token>", nonce, responseUri).join();
System.out.println("Deposit: " + vp.depositId());
System.out.println("Pledge date: " + vp.pledgeDate());
System.out.println("Issuing bank: " + vp.issuingBank());DepositPledgeAttestation credential fields
The QEAA is issued at the ACTIVE transition and contains both always-revealed and selectively disclosed (SD) claims:
| Field | Selectively disclosed | Description |
|---|---|---|
deposit_id | No | Stable deposit identifier |
pledge_date | No | Date pledge was confirmed |
statutory_basis | No | Jurisdiction-specific legal basis (e.g. BGB § 551, Art. 257e CO, Housing Act 2004) |
issuing_bank | No | Legal name of issuing bank |
deposit_amount | Yes | Pledged amount |
currency | Yes | ISO 4217 currency |
pledged_until | Yes | Pledge expiry date |
property_address | Yes | Rental property address |
tenant_first_name | Yes | |
tenant_last_name | Yes |
What VerifyVpToken checks
| Check | Detail |
|---|---|
| Issuer JWT structure | 3-part JWT; base64url payload parseable |
| Credential expiry | exp claim in issuer JWT |
| Disclosure integrity | SHA-256 digest of each disclosure must appear in _sd array |
KB-JWT typ | Must be kb+jwt |
KB-JWT nonce | Must match the nonce from BuildVpRequest |
KB-JWT aud | Must match response_uri |
KB-JWT sd_hash | SHA-256 over issuerJWT~disc1~…~discN~ |
KB-JWT iat | Must be within the last 5 minutes |
Error Handling
HTTP status codes
| Code | Meaning |
|---|---|
400 Bad Request | Malformed JSON, deposit cap exceeded for jurisdiction, or missing required field |
401 Unauthorized | Missing, expired, or structurally invalid Bearer token |
403 Forbidden | Valid token, but insufficient scope for this deposit or action |
404 Not Found | Deposit ID does not exist |
409 Conflict | Transition not permitted from the current lifecycle state |
422 Unprocessable Entity | Jurisdiction-specific validation failure (e.g. scheme registration window elapsed for GB) |
SDK error types
func handleAuthError(w http.ResponseWriter, err error) {
switch {
case auth.IsInsufficientScopeError(err):
// HTTP 403 — valid token, but the required scope is not granted
http.Error(w, "insufficient scope", http.StatusForbidden)
case auth.IsTokenValidationError(err):
// HTTP 401 — malformed JWT, expired, or wrong issuer
http.Error(w, "unauthorized", http.StatusUnauthorized)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
}fn http_status_for_auth_error(err: &AuthError) -> u16 {
match err {
AuthError::TokenExpired | AuthError::MalformedJwt(_) => 401,
AuthError::InsufficientScope { .. } => 403,
AuthError::UnexpectedIssuer(_) => 401,
_ => 500,
}
}
fn log_vp_error(err: &VpError) {
match err {
VpError::NonceMismatch => eprintln!("replay attack: nonce mismatch"),
VpError::StaleKbJwt => eprintln!("presentation expired (>5 min old)"),
VpError::AudMismatch => eprintln!("response_uri mismatch"),
other => eprintln!("VP verification failed: {other}"),
}
}// Java uses checked exceptions — see Authentication section above.API Reference
| Endpoint | Action | Result state |
|---|---|---|
POST/deposits |
Create deposit request | REQUESTED |
GET/deposits/{id} |
Retrieve deposit + history | — |
PATCH/deposits/{id}/identity |
Report eID verification outcome | IDENTIFIED |
POST/deposits/{id}/pledge |
Confirm legal pledge (bank/insurer/scheme) | PLEDGED |
POST/deposits/{id}/release |
Landlord authorises full or partial release | RELEASED or PARTIALLY_RELEASED |
POST/deposits/{id}/claim |
Landlord initiates unilateral claim | CLAIMED |
POST/deposits/{id}/settle |
Propose or respond to itemised settlement | SETTLE_PROPOSED or CLOSED |
Full schema: xmiete_schema.json (v2.0.0) in the repository root.
OpenAPI spec: openapi.yaml.
Jurisdiction Rules Reference
| Code | Cap | Return window | Interest | Escrow type | ADR body |
|---|---|---|---|---|---|
| DE | 3× cold rent | 3–6 months (practice) | Yes | TREUHANDKONTO | Civil court |
| AT | 3× cold rent | MRG §16b | Yes | TREUHANDKONTO | Civil court / Schlichtungsstelle |
| CH | 3× cold rent | 30 days | Yes (blocked account) | MIETKAUTIONSKONTO | Schlichtungsbehörde |
| FR | 1 month (2 furnished) | 1–2 months from handover | No | LANDLORD_HELD | Civil court / Commission départementale |
| GB ENG/WLS | 5–6 weeks warm rent | 10 days from agreement | No (scheme may) | CUSTODIAL_SCHEME or INSURED_SCHEME | TDS / DPS / MyDeposits ADR |
| GB SCT | 2 months warm rent | 10 days | No | CUSTODIAL_SCHEME only | SafeDeposits Scotland / LPS ADR |
| NL | No statutory cap | 14–30 days | No | LANDLORD_HELD | Huurcommissie |
| BE | 2–3 months cold rent | 14 days via e-DEPO | No | EDEPO | Justice of the Peace / Huurcommissie BE |
| ES | 1 month + contractual surety | 30 days from handover | No | FIANZA_REGIONAL | Regional authority |
| IT | 3 months cold rent | Reasonable time (Art. 1590) | Yes | LANDLORD_HELD | Civil court |
| IE | No statutory cap | Reasonable (RTB guidance: 28 days) | No | LANDLORD_HELD (scheme pending) | RTB |
| NO | 6 months cold rent | 4 weeks after end of tenancy | Yes (MIETKAUTIONSKONTO) | MIETKAUTIONSKONTO | Husleietvistutvalget |
| SE | No statutory cap | Reasonable time | No | LANDLORD_HELD | Hyresnämnden |
| PL | No statutory cap | 30 days | No | LANDLORD_HELD | Civil court |
Licensed under Apache 2.0. API specification licensed under CC BY 4.0.