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

https://api.xmiete.org/v1

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

CodeCountrySubdivision requiredNotes
DEGermanyNoBGB § 551; up to 3 cold months; TREUHANDKONTO
ATAustriaNoMRG; up to 3 months; interest required
CHSwitzerlandNoArt. 257e OR; MIETKAUTIONSKONTO (blocked account); up to 3 months
FRFranceNoLoi ALUR; 1 month unfurnished, 2 furnished; LANDLORD_HELD; no interest
GBUnited KingdomYes (GB-ENG, GB-SCT, GB-NIR, GB-WLS)Housing Act 2004; mandatory protection scheme; caps vary by region
NLNetherlandsNoWoningwet; typically 1–2 months; Huurcommissie ADR
BEBelgiumNoe-DEPO mandatory blocked escrow; 2 months (private) or 3 months (commercial)
ESSpainYes (e.g. ES-AN, ES-MD)LAU Art. 36; 1 month; fianza held by regional authority (INCASOL etc.)
ITItalyNoCodice Civile Art. 1590; up to 3 months; LANDLORD_HELD
IEIrelandNoRTB registration; protection scheme pending 2025 Bill
NONorwayNoHusleieloven §3-5; MIETKAUTIONSKONTO; Husleietvistutvalget ADR
SESwedenNoJordabalken; typically 1–3 months; Hyresnämnden ADR
PLPolandNoNo statutory cap; PESEL tenant ID; civil courts

Supported SDK languages

LanguageMin versionDependencies
Go1.21+Standard library only
Rust2021 editiontokio, reqwest, serde
Java17+Standard library only (java.net.http)
PythonComing soon
TypeScriptComing 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.

Authorization: Bearer <access_token>

Required scopes

EndpointScope
POST/depositsdeposit:create
GET/deposits/{id}deposit:read
PATCH/deposits/{id}/identitydeposit:write
POST/deposits/{id}/pledgedeposit:pledge
POST/deposits/{id}/releasedeposit:release
POST/deposits/{id}/claimdeposit:claim
POST/deposits/{id}/settledeposit: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;
}
Production note: JWT signature verification is currently stubbed in all SDKs. Replace the stub with a JWKS-backed RS256/ES256 verifier before going to production. See the stub comments in each SDK file for the recommended library per language.

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.

REQUESTEDIDENTIFIEDFUNDEDPLEDGEDACTIVE

From ACTIVE:
  → RELEASEDCLOSED
  → PARTIALLY_RELEASEDRELEASEDCLOSED
  → SETTLE_PROPOSEDRELEASED or DISPUTEDCLOSED
  → CLAIMEDCLOSED
StateTriggerNotes
REQUESTEDPOST/depositsDeposit object created
IDENTIFIEDPATCH/deposits/{id}/identity with VERIFIEDeID / EUDI Wallet flow complete
FUNDEDBank or scheme confirms receipt of fundsExternal event; scheme registration triggered for GB/BE
PLEDGEDPOST/deposits/{id}/pledgeLegal pledge confirmed per jurisdiction (see pledge.statutory_basis)
ACTIVELease beginsQEAA DepositPledgeAttestation issued to tenant EUDI Wallet
PARTIALLY_RELEASEDPOST/deposits/{id}/release with partial amountUtility reservation held back; see utility_reservation object
SETTLE_PROPOSEDPOST/deposits/{id}/settleItemized claim submitted; counter-party has response_deadline to respond
DISPUTEDSettlement rejected; escalated to ADR or courtADR body set in settlement.escalation_type / specialist_tribunal_id
RELEASEDPOST/deposits/{id}/release (full)Landlord authorises full release; return deadline driven by jurisdiction
CLAIMEDPOST/deposits/{id}/claimLandlord initiates unilateral claim
CLOSEDFinal state after full resolutionAgreed amounts set in settlement.agreed_tenant_refund / agreed_landlord_retention
History audit trail: every transition is appended to 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/openid4vp

Rust

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

FieldTypeRequiredDescription
versionstringYesMust be "2.0.0"
timestampdate-timeYesISO 8601 creation timestamp
jurisdictionstringYesISO 3166-1 alpha-2 country code (e.g. DE, GB, FR)
jurisdiction_substringConditionalISO 3166-2 code — required for GB (region caps differ) and ES (regional fianza authority)
external_idstringNoCaller-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:

JurisdictionStatutory capRent referenceStatutory basis
DE3 monthslease.monthly_cold_rentBGB § 551
AT3 monthslease.monthly_cold_rentMRG § 16b
CH3 monthslease.monthly_cold_rentOR Art. 257e
FR1 month (unfurnished) / 2 months (furnished)lease.monthly_cold_rentLoi ALUR
GB (ENG/WLS)5 weeks (≤£50k/yr) or 6 weekslease.monthly_warm_rent × 12/52Tenant Fees Act 2019
GB (SCT)2 monthslease.monthly_warm_rentPrivate Housing (Tenancies) (Scotland) Act 2016
NLNo statutory cap (typically 1–2 months)
BE2 months (private) / 3 months (commercial)lease.monthly_cold_rentWoninghuurwet
ES1 month (legal fianza) + optional contractual suretylease.monthly_cold_rentLAU Art. 36
IT3 monthslease.monthly_cold_rentCodice Civile Art. 1590
IENo statutory cap (typically 1 month)
NO6 monthslease.monthly_cold_rentHusleieloven §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.

JurisdictionReturn window
DE3–6 months (practice; BGB does not specify exact deadline)
FR1 month (no damage) / 2 months (with deductions)
GB10 days from agreement or ADR decision
NL14 days (no damage) / 30 days (with deductions)
BEReleased by e-DEPO within 14 days of agreement
ES30 days from key handover
CZ30 days
LV10 days

Tax identifier types

tenant.tax_id_type tells the API which format to expect in tenant.tax_id:

ValueCountryFormat
STEUER_IDDE11-digit Steueridentifikationsnummer
NIRFR15-digit Numéro de Sécurité Sociale
CODICE_FISCALEIT16-char alphanumeric
NIFES9-char (nationals)
NIEES9-char (foreign residents)
BSNNL9-digit Burgerservicenummer
NI_NUMBERGBNational Insurance number
PESELPL11-digit national ID
OTHERFree-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

ValueCountriesDescription
TREUHANDKONTODE ATSegregated landlord-held trust account; insolvency-proof separation required
ANDERKONTODERestricted to licensed professionals (lawyers, notaries); highest segregation standard
POOLED_TREUHANDDEMultiple deposits in one designated account with per-tenant sub-accounting
MIETKAUTIONSKONTOCH NOBlocked account in tenant's name — landlord has no access without tenant consent or court order
CUSTODIAL_SCHEMEGBGovernment-approved scheme holds funds directly (TDS, DPS, SafeDeposits Scotland)
INSURED_SCHEMEGBLandlord holds funds; insured via government-approved scheme (MyDeposits, TDS Insured)
EDEPOBEMandatory Belgian e-DEPO system; funds cannot be paid directly to landlord
FIANZA_REGIONALESLegal fianza held by regional authority (INCASOL, IVIMA, etc.)
STATE_GUARANTEELUState-backed guarantee for low-income tenants
DEPOSIT_INSURANCECH DEAnnual-premium insurance product (SwissCaution, goCaution, getmomo)
LANDLORD_HELDFR NL IT CEELandlord 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.

JurisdictionInterest requiredNotes
DE ATYesLandlord must pass bank interest to tenant
CHYesInterest accrues on MIETKAUTIONSKONTO in tenant's favour
CZ HU ITYesStatutory interest obligation
FR NL ES IENoNo statutory interest obligation
GBNo (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.

GB deadline: the deposit must be registered with an approved scheme within 30 days of receipt (28 days in Northern Ireland), and the prescribed information provided to the tenant. Failure voids the landlord's right to serve a Section 21 notice and exposes them to penalties of 1–3× the deposit amount.

Approved schemes

ValueRegionTypeNotes
TDSGB England & WalesCustodialTenancy Deposit Scheme
TDS_INSUREDGB England & WalesInsuredLandlord holds funds; TDS insures
DPSGB England & WalesCustodialDeposit Protection Service
DPS_INSUREDGB England & WalesInsured
MYDEPOSITSGB England & WalesInsured
SAFEDEPOSITS_SCOTLANDGB ScotlandCustodial onlyScotland permits custodial schemes only
LPS_SCOTLANDGB ScotlandCustodial onlyLetting Protection Service Scotland
TDSNORTHERNIRELANDGB Northern IrelandCustodial & Insured28-day registration window
EDEPO_BEBECustodial (mandatory)Belgian electronic escrow system
RTB_IEIECustodial (pending)Residential Tenancies Board — awaiting legislation
OTHERSupply 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

FieldDescription
initiated_byLANDLORD or TENANT
handover_dateDate keys were returned; starts return-deadline clock
handover_protocol_refReference to signed check-out report (Übergabeprotokoll, état des lieux)
claim_items[]Itemized deductions — see categories below
proposed_tenant_refundAmount proposed to be returned to tenant
proposed_landlord_retentionAmount proposed to be retained by landlord
response_deadlineCounter-party must respond by this date (14 days typical)
escalation_typeADR route if not agreed — see escalation table below
agreed_tenant_refundSet on CLOSED — final refund amount
agreed_landlord_retentionSet on CLOSED — final retention amount

Claim item categories

CategoryEnforceability notes
RENT_ARREARSUniversally enforceable with evidence
UTILITY_ARREARSUniversally enforceable with evidence
DAMAGEEnforceable; must distinguish from fair wear and tear
CLEANINGEnforceable if property was not cleaned to move-in standard
COSMETIC_REPAIRSFrequently unenforceable in DE (BGH Schönheitsreparaturen); generally valid in GB and FR if evidenced
UNAUTHORIZED_ALTERATIONSEnforceable where alterations were not consented
MISSING_ITEMSRequires inventory reference in evidence_refs[]
OTHERFree-form; document clearly in description

ADR escalation

ValueCountriesBody
PLATFORM_MEDIATIONAllXMiete's own mediation service
SPECIALIST_TRIBUNALGB NL NO IE DK SE FIGovernment-mandated ADR body (set specialist_tribunal_id)
CIVIL_COURTFR ES IT PT GR CEEGeneral civil jurisdiction
REGIONAL_AUTHORITYESRegional authority holding the fianza

specialist_tribunal_id values

ValueBodyCountry
TDS / DPS / MYDEPOSITSScheme ADR panelsGB England & Wales
SAFEDEPOSITS_SCOTLANDSafeDeposits Scotland ADRGB Scotland
TDSNORTHERNIRELANDTDS Northern Ireland ADRGB NI
HUURCOMMISSIEHuurcommissieNL
HUSLEIETVISTUTVALGETHusleietvistutvalgetNO
RTBResidential Tenancies BoardIE
HUSLEJENAEVNHuslejenævnDK
HYRESNAMNDENHyresnämndenSE
KULUTTAJARIITALAUTAKUNTAConsumer Disputes BoardFI

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

MethodJurisdictionsStandard
EUDI Wallet (OpenID4VP)All EU member stateseIDAS 2.0 / EUDI ARF; vc+sd-jwt or mso_mdoc
BSI TR-03130 (Online-Ausweisfunktion)DEChip-based, AusweisApp2 or third-party SDK
Generic OIDC eIDDE AT BE and othersOpenID Connect front-end for national eID
GOV.UK Verify / One LoginGBOIDC + LOA 2
FranceConnectFROIDC + Substantial assurance
iDIN (iDEAL-based)NLBank-based identity; not eIDAS
itsmeBE NLMobile ID, eIDAS Substantial

Flow

1. InitiateVerification → receive AuthorizationURL
2. Redirect tenant's browser (or present QR for EUDI Wallet) to AuthorizationURL
3. Provider POSTs signed webhook to your endpoint
4. WebhookHandler validates HMAC and calls PATCH /deposits/{id}/identity
5. 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)
Privacy note: only the 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

StatusMeaningSDK action
VERIFIEDeID check passedCalls PATCH /identity with VERIFIED
FAILEDIdentity mismatch or user abortLogs; invokes onComplete callback
EXPIREDSession timeoutLogs; invokes onComplete callback
PENDINGVerification in progressNo 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

1. BuildVpRequest → send VpRequest JSON to wallet (QR or deep-link)
2. Wallet presents → wallet POSTs vp_token to your response_uri
3. 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:

FieldSelectively disclosedDescription
deposit_idNoStable deposit identifier
pledge_dateNoDate pledge was confirmed
statutory_basisNoJurisdiction-specific legal basis (e.g. BGB § 551, Art. 257e CO, Housing Act 2004)
issuing_bankNoLegal name of issuing bank
deposit_amountYesPledged amount
currencyYesISO 4217 currency
pledged_untilYesPledge expiry date
property_addressYesRental property address
tenant_first_nameYes
tenant_last_nameYes

What VerifyVpToken checks

CheckDetail
Issuer JWT structure3-part JWT; base64url payload parseable
Credential expiryexp claim in issuer JWT
Disclosure integritySHA-256 digest of each disclosure must appear in _sd array
KB-JWT typMust be kb+jwt
KB-JWT nonceMust match the nonce from BuildVpRequest
KB-JWT audMust match response_uri
KB-JWT sd_hashSHA-256 over issuerJWT~disc1~…~discN~
KB-JWT iatMust be within the last 5 minutes
Production note: ES256 signature verification over the issuer JWT is currently stubbed in all SDKs. Implement it using the p256 / ECDSA library for your language before production use. The KB-JWT holder signature verification is also noted as a TODO.

Error Handling

HTTP status codes

CodeMeaning
400 Bad RequestMalformed JSON, deposit cap exceeded for jurisdiction, or missing required field
401 UnauthorizedMissing, expired, or structurally invalid Bearer token
403 ForbiddenValid token, but insufficient scope for this deposit or action
404 Not FoundDeposit ID does not exist
409 ConflictTransition not permitted from the current lifecycle state
422 Unprocessable EntityJurisdiction-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

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